如何在 Fire TV app 中实现订阅购买 payment gateway?
明确结论
可以在 Fire TV 应用中销售订阅。如果应用通过 Amazon Appstore 分发,且订阅用于解锁应用内的数字内容或功能,通常需要接入 Amazon In-App Purchasing(Amazon IAP),不应自行嵌入信用卡、支付宝、Stripe 等第三方 payment gateway。
Amazon 负责处理支付、续费和取消。Fire TV 应用负责发起购买、接收购买结果和恢复订单,业务服务器则负责校验收据并维护订阅权益。
如果应用不通过 Amazon Appstore 分发,或者销售的是实体商品、线下服务,支付方案可能有所不同。采用第三方支付前,应先核对当前分发渠道的开发者协议和支付政策。
推荐的整体架构
较为可靠的订阅流程包含三部分:
-
Amazon Developer Console
- 创建订阅商品。
- 配置 SKU、价格、订阅周期和展示信息。
- 将订阅商品关联到 Fire TV 应用。
-
Fire TV 客户端
- 初始化 Amazon IAP。
- 查询商品信息。
- 发起订阅购买。
- 接收购买结果。
- 在启动时恢复已有购买。
-
业务服务器
- 接收客户端上传的
receiptId和userId。 - 通过 Amazon 的收据验证服务校验订单。
- 保存订阅状态和有效期。
- 根据验证结果开放或关闭用户权益。
- 接收客户端上传的
不要仅凭客户端返回的”购买成功”永久开通订阅。客户端数据可能被篡改,最终权益应以服务端校验结果为准。
第一步:创建订阅商品
在 Amazon Developer Console 中为应用创建 subscription 类型的应用内商品,例如:
premium_monthly
premium_yearly
SKU 投入正式使用后,最好保持稳定,也不要把价格或语言信息写入 SKU。例如,premium_monthly 比 premium_9_99_usd 更便于长期维护。
完成商品配置后,需要确认:
- 商品已关联到正确的应用。
- 商品状态允许测试或发布。
- 客户端查询时使用的 SKU 与控制台完全一致。
- 测试账号和测试环境已按照 Amazon 当前要求配置。
控制台界面和审核流程可能会调整,具体选项应以当前 Amazon Appstore Developer Console 为准。
第二步:集成 Amazon IAP SDK
按照 Amazon 当前的官方集成方式,将 Amazon Appstore SDK 加入 Android/Fire TV 工程。不同版本的 SDK 可能通过本地 AAR、Maven 依赖或其他方式发布,因此依赖版本和仓库地址应以当前 SDK 文档为准,不要填写未经确认的版本号。
核心 API 位于以下包中:
com.amazon.device.iap
com.amazon.device.iap.model
应用需要实现 PurchasingListener,并在启动时完成注册。
第三步:注册购买监听器
下面是一个简化的 Kotlin 示例:
import android.app.Application
import com.amazon.device.iap.PurchasingService
import com.amazon.device.iap.PurchasingListener
import com.amazon.device.iap.model.ProductDataResponse
import com.amazon.device.iap.model.PurchaseResponse
import com.amazon.device.iap.model.PurchaseUpdatesResponse
import com.amazon.device.iap.model.UserDataResponse
class TvApplication : Application() {
override fun onCreate() {
super.onCreate()
PurchasingService.registerListener(
applicationContext,
AmazonPurchasingListener()
)
}
}
class AmazonPurchasingListener : PurchasingListener {
override fun onUserDataResponse(response: UserDataResponse) {
when (response.requestStatus) {
UserDataResponse.RequestStatus.SUCCESSFUL -> {
val userData = response.userData
// 保存当前 Amazon 用户信息。
// 不要把它当作应用自身账号体系的唯一身份凭据。
}
else -> {
// 记录错误,并根据需要提示用户重试。
}
}
}
override fun onProductDataResponse(response: ProductDataResponse) {
when (response.requestStatus) {
ProductDataResponse.RequestStatus.SUCCESSFUL -> {
val products = response.productData
products.forEach { (sku, product) ->
val title = product.title
val price = product.price
val description = product.description
// 使用 Amazon 返回的本地化价格和商品信息更新界面。
}
val unavailableSkus = response.unavailableSkus
// unavailableSkus 中的商品不应显示购买按钮。
}
else -> {
// 商品信息查询失败。
}
}
}
override fun onPurchaseResponse(response: PurchaseResponse) {
when (response.requestStatus) {
PurchaseResponse.RequestStatus.SUCCESSFUL -> {
val receipt = response.receipt
val userData = response.userData
// 将 receipt.receiptId、userData.userId 和 SKU 上传到业务服务器。
// 服务端校验成功后再开通订阅权益。
verifyReceiptWithBackend(
userId = userData.userId,
receiptId = receipt.receiptId,
sku = receipt.sku
)
}
PurchaseResponse.RequestStatus.ALREADY_PURCHASED -> {
// 用户可能已经拥有该商品。
// 调用 getPurchaseUpdates() 恢复并重新校验订阅状态。
PurchasingService.getPurchaseUpdates(false)
}
PurchaseResponse.RequestStatus.INVALID_SKU -> {
// 检查 SKU 是否与控制台配置一致。
}
PurchaseResponse.RequestStatus.NOT_SUPPORTED -> {
// 当前设备、账号或运行环境不支持 Amazon IAP。
}
PurchaseResponse.RequestStatus.FAILED -> {
// 购买失败。不要开通权益。
}
}
}
override fun onPurchaseUpdatesResponse(response: PurchaseUpdatesResponse) {
when (response.requestStatus) {
PurchaseUpdatesResponse.RequestStatus.SUCCESSFUL -> {
val userId = response.userData.userId
response.receipts.forEach { receipt ->
verifyReceiptWithBackend(
userId = userId,
receiptId = receipt.receiptId,
sku = receipt.sku
)
}
if (response.hasMore()) {
PurchasingService.getPurchaseUpdates(false)
}
}
else -> {
// 恢复购买失败,可记录日志并允许用户手动重试。
}
}
}
private fun verifyReceiptWithBackend(
userId: String,
receiptId: String,
sku: String
) {
// 调用自己的 HTTPS API。
// 不要在客户端直接保存 Amazon 开发者密钥。
}
}
还需要在 AndroidManifest.xml 中指定自定义的 Application:
<application
android:name=".TvApplication"
...>
</application>
实际项目中,可以通过 Repository、ViewModel 或 UseCase 分离购买逻辑与界面,避免在回调中直接执行复杂业务。
第四步:查询订阅商品
打开购买页面时,先向 Amazon 查询商品信息:
private val subscriptionSkus = setOf(
"premium_monthly",
"premium_yearly"
)
fun loadSubscriptions() {
PurchasingService.getProductData(subscriptionSkus)
}
价格应使用 Product 对象返回的本地化字符串,不要在客户端写死:
priceTextView.text = product.price
这样可以避免币种、税费和地区价格的显示与 Amazon 实际结算页面不一致。
如果 SKU 出现在 unavailableSkus 中,应检查:
- SKU 是否拼写错误。
- 商品是否属于当前应用。
- 商品是否已提交或启用。
- 当前账号所在地区是否支持该商品。
- 测试环境是否配置正确。
第五步:发起订阅购买
用户通过遥控器选中订阅方案并确认后,调用:
fun buyMonthlySubscription() {
PurchasingService.purchase("premium_monthly")
}
purchase() 是异步调用。不能根据该方法是否成功返回来判断购买结果,必须等待 onPurchaseResponse()。
等待结果期间,建议:
- 暂时禁用重复点击。
- 显示清晰的处理中状态。
- 不要自行绘制可能误导用户的支付成功页面。
- 超时后允许重新查询购买记录,不要直接再次扣款。
第六步:在服务器校验收据
客户端购买成功后,将必要信息发送到自己的服务器:
{
"amazonUserId": "AMAZON_USER_ID",
"receiptId": "RECEIPT_ID",
"sku": "premium_monthly"
}
服务器使用保存在安全环境中的 Amazon 开发者凭据,调用 Amazon Receipt Verification Service。由于接口要求可能调整,具体请求地址、认证方式和响应字段应以 Amazon 当前官方文档为准。
服务端至少需要校验:
- 收据是否真实存在。
- 收据是否属于预期用户。
- SKU 是否为服务器允许的订阅商品。
- 商品类型是否为订阅。
- 订阅是否已取消。
- 当前时间是否仍在有效期内。
- 收据是否已经处理,防止重复入账。
- 返回环境是否与测试或生产环境匹配。
下面是示意性的服务端逻辑:
async function activateSubscription(user, purchase) {
const result = await verifyWithAmazon({
amazonUserId: purchase.amazonUserId,
receiptId: purchase.receiptId
});
if (!result.valid) {
throw new Error("Invalid Amazon receipt");
}
const allowedSkus = new Set([
"premium_monthly",
"premium_yearly"
]);
if (!allowedSkus.has(result.sku)) {
throw new Error("Unexpected subscription SKU");
}
if (result.cancelled || result.expired) {
await revokeSubscription(user.id, result.sku);
return;
}
await saveSubscription({
userId: user.id,
provider: "amazon",
providerUserId: purchase.amazonUserId,
receiptId: purchase.receiptId,
sku: result.sku,
expiresAt: result.expiresAt
});
}
其中,verifyWithAmazon()、cancelled、expired 和 expiresAt 只是业务层示意,并非 Amazon SDK 的固定字段名称。实现时,需要按照当前收据验证接口的实际响应进行映射。
第七步:确认交付
服务器确认收据有效并成功开通权益后,客户端可以按照 Amazon IAP 的交付流程通知购买系统:
import com.amazon.device.iap.PurchasingService
import com.amazon.device.iap.model.FulfillmentResult
fun notifyPurchaseFulfilled(receiptId: String) {
PurchasingService.notifyFulfillment(
receiptId,
FulfillmentResult.FULFILLED
)
}
只有在业务服务器安全保存订单并开通权益后,才能报告 FULFILLED。如果交付尚未完成,不要提前确认。
不同商品类型和 SDK 版本对 fulfillment 的处理要求可能不同。订阅商品应按照当前 Amazon IAP 文档的规定处理。
第八步:恢复已有订阅
用户可能会重新安装应用、清除数据或更换 Fire TV 设备。应用启动时,或者用户点击”恢复购买”时,应调用:
fun restorePurchases() {
PurchasingService.getPurchaseUpdates(false)
}
false 表示继续基于已有同步状态获取更新。需要完整重新同步时,可以根据 SDK 语义使用:
PurchasingService.getPurchaseUpdates(true)
完整重置可能返回更多历史数据,不应在每次刷新页面时使用。
恢复得到的每张收据仍需提交服务器验证。收据出现在本地回调中,并不代表订阅当前有效。
订阅状态如何维护
订阅可能续费、到期或取消,因此不能在首次购买成功后停止状态管理。
较为稳妥的做法包括:
- 用户进入应用时恢复购买并同步收据。
- 用户进入会员页面时,向业务服务器查询最新权益。
- 服务器定期重新校验仍处于有效状态的订阅。
- 接口异常时保留短暂的宽限状态,避免因临时网络故障立即锁定已付费用户。
- 明确区分
ACTIVE、EXPIRED、CANCELLED、PENDING_VERIFICATION等业务状态。 - 使用
receiptId建立唯一约束,避免重复回调创建重复订单。
取消续费通常不代表订阅立即失效。用户往往仍可使用服务,直到当前计费周期结束。因此,应根据收据中的有效期决定权益,不要在看到取消标记后立即关闭服务。
Fire TV 界面需要特别处理的地方
Fire TV 主要通过遥控器操作,购买页面需要适配十字键和焦点导航:
- 每个订阅方案都必须有清晰的焦点状态。
- 不要只用颜色区分选中和未选中状态。
- 返回键应能安全退出购买流程。
- 不要要求用户使用鼠标完成操作。
- 展示订阅周期、价格、自动续费说明和取消方式。
- 购买按钮应防止快速连续触发。
- 不要让用户在电视端手工输入银行卡信息,结算应交给 Amazon 提供的购买界面。
不建议直接接入第三方支付网关
Fire TV 基于 Fire OS,Android 应用在技术上可以实现网页支付或调用自己的支付服务。但如果订阅解锁的是应用内数字内容,这种做法可能不符合 Amazon Appstore 的支付和审核要求。
尤其不要通过以下方式绕过 IAP:
在 WebView 中嵌入信用卡收银台
引导用户扫描二维码购买应用内会员
在应用内显示第三方数字订阅购买链接
购买后仅依赖客户端接口解锁内容
这些方式是否允许,取决于应用类别、销售内容、分发地区以及 Amazon 当前政策。技术上能够运行,不代表能够通过审核。
如果业务属于实体商品、线下服务或其他特殊场景,应先确认政策的适用范围,再决定是否接入 Stripe、Adyen 等第三方网关。
上线前检查清单
- Amazon Developer Console 中的 SKU 与代码完全一致。
- 测试账号、测试商品和正式环境已严格区分。
- 客户端没有保存开发者密钥或服务器密钥。
- 所有收据都经过服务端验证。
receiptId具有幂等处理机制。- 已覆盖购买成功、失败、取消和重复购买场景。
- 已测试重新安装、切换设备和恢复购买。
- 已处理订阅续费、取消、过期和验证服务暂时不可用的情况。
- 价格使用 Amazon 返回的本地化数据。
- 购买页面可以完全通过电视遥控器操作。
- 隐私政策和服务条款说明了订阅与账号的绑定关系。
- 上架前已重新核对 Amazon Appstore 最新的 IAP 和支付政策。
备注:内容仅供参考。
