PAYATHON 2026

如何在 Fire TV app 中实现订阅购买 payment gateway?

支付阿杰

明确结论

可以在 Fire TV 应用中销售订阅。如果应用通过 Amazon Appstore 分发,且订阅用于解锁应用内的数字内容或功能,通常需要接入 Amazon In-App Purchasing(Amazon IAP),不应自行嵌入信用卡、支付宝、Stripe 等第三方 payment gateway。

Amazon 负责处理支付、续费和取消。Fire TV 应用负责发起购买、接收购买结果和恢复订单,业务服务器则负责校验收据并维护订阅权益。

如果应用不通过 Amazon Appstore 分发,或者销售的是实体商品、线下服务,支付方案可能有所不同。采用第三方支付前,应先核对当前分发渠道的开发者协议和支付政策。

推荐的整体架构

较为可靠的订阅流程包含三部分:

  1. Amazon Developer Console

    • 创建订阅商品。
    • 配置 SKU、价格、订阅周期和展示信息。
    • 将订阅商品关联到 Fire TV 应用。
  2. Fire TV 客户端

    • 初始化 Amazon IAP。
    • 查询商品信息。
    • 发起订阅购买。
    • 接收购买结果。
    • 在启动时恢复已有购买。
  3. 业务服务器

    • 接收客户端上传的 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 和支付政策。

备注:内容仅供参考。