如何使用 Google Play Billing API 取消订阅?
结论
不能在 Android 客户端通过 BillingClient 直接取消订阅。
一般有两种处理方式:
- 由用户取消:应用跳转到 Google Play 的订阅管理页面,由用户确认取消。
- 由开发者取消:在可信任的服务端调用 Google Play Developer API,不能直接从 Android 客户端调用。
BillingClient 用于发起购买、查询商品和确认交易等客户端操作,没有取消订阅的方法。取消订阅涉及账户和订阅生命周期管理,需要用户在 Google Play 中确认,或者由获得授权的开发者服务端执行。
方案一:跳转到 Google Play 订阅管理页面
对于普通用户,通常会在应用中提供“管理订阅”或“取消订阅”按钮。用户点击后,打开对应订阅的 Google Play 管理页面:
fun openSubscriptionManagement(
context: Context,
productId: String
) {
val uri = Uri.parse(
"https://play.google.com/store/account/subscriptions" +
"?sku=${Uri.encode(productId)}" +
"&package=${Uri.encode(context.packageName)}"
)
val intent = Intent(Intent.ACTION_VIEW, uri).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
context.startActivity(intent)
}
参数说明:
productId是订阅商品 ID。package是应用的包名。- 用户需要在 Google Play 页面中确认取消。
如果无法确定具体的商品 ID,也可以打开通用订阅管理页面:
fun openAllSubscriptions(context: Context) {
val intent = Intent(
Intent.ACTION_VIEW,
Uri.parse("https://play.google.com/store/account/subscriptions")
).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
context.startActivity(intent)
}
启动前最好检查是否有应用能够处理该链接。如果无法打开,可以向用户显示提示:
fun openSubscriptionManagementSafely(
context: Context,
productId: String
) {
val uri = Uri.parse(
"https://play.google.com/store/account/subscriptions" +
"?sku=${Uri.encode(productId)}" +
"&package=${Uri.encode(context.packageName)}"
)
val intent = Intent(Intent.ACTION_VIEW, uri)
if (intent.resolveActivity(context.packageManager) != null) {
context.startActivity(intent)
} else {
Toast.makeText(
context,
"无法打开 Google Play 订阅管理页面",
Toast.LENGTH_LONG
).show()
}
}
方案二:通过服务端取消订阅
如果开发者需要主动取消订阅,例如处理客服请求、风控处置或账户关闭,可以从服务端调用 Google Play Developer API。
当前应优先参考 purchases.subscriptionsv2.cancel,请求形式如下:
POST https://androidpublisher.googleapis.com/androidpublisher/v3/applications/{packageName}/purchases/subscriptionsv2/tokens/{token}:cancel
Authorization: Bearer {accessToken}
Content-Type: application/json
{}
示例:
curl -X POST \
"https://androidpublisher.googleapis.com/androidpublisher/v3/applications/com.example.app/purchases/subscriptionsv2/tokens/PURCHASE_TOKEN:cancel" \
-H "Authorization: Bearer ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
这里的 token 是购买成功后取得的 purchaseToken,不是商品 ID。服务端应保存订阅与以下信息的关联:
用户账号
packageName
productId
purchaseToken
当前订阅状态
到期时间
调用 Developer API 前,需要完成以下配置和检查:
- 在 Google Cloud 项目中启用 Google Play Android Developer API。
- 配置有权访问对应 Play Console 应用的服务账号,或其他受支持的服务端身份。
- 在服务端获取 OAuth 2.0 访问令牌。
- 验证
purchaseToken确实属于当前用户和当前应用。 - 调用取消接口后重新查询订阅状态,或者通过 Real-time Developer Notifications 同步后续变化。
Developer API 的请求体和取消类型可能会随接口演进而变化,应以项目当前使用版本的官方接口定义为准。部分现有集成中可能仍会使用旧的 purchases.subscriptions.cancel 接口,但新实现不应在没有确认当前支持状态时依赖该接口。
为什么不能在 BillingClient 中直接取消
订阅购买和订阅取消有不同的安全边界。
购买时,BillingClient 只是启动由 Google Play 控制的结算界面,应用本身不能静默完成扣款。取消订阅同样不能由客户端代码静默执行,否则遭到篡改的应用、恶意脚本或错误逻辑都可能擅自取消用户的订阅。
因此,Google Play 提供了两种取消方式:
- 用户操作:进入 Google Play 的订阅管理页面并确认取消。
- 开发者操作:使用受保护的服务端凭据调用 Developer API。
不要把服务账号密钥、OAuth 客户端密钥或具备 Play Console 权限的访问凭据打包进 APK。即使做了混淆,这些凭据仍可能被提取。
取消后的订阅状态
取消订阅通常只是关闭自动续订,并不代表当前权益会立即失效。多数情况下,用户仍能使用已付费周期内的订阅内容,直至当前计费周期结束。
用户点击取消后,应用不应立即删除其权益,而应根据服务端查询结果判断:
- 是否仍在有效期内;
- 自动续订是否已经关闭;
- 实际到期时间;
- 是否出现退款、撤销、宽限期或账号保留等状态变化。
如果业务要求立即终止权益或办理退款,普通的“取消续订”并不能解决全部问题。此时需要根据具体场景选择相应的 Developer API,或通过 Play Console 处理。
推荐实现
客户端只提供“管理订阅”入口,并将用户带到 Google Play。服务端负责保存和验证 purchaseToken、同步订阅状态,并在确有业务需要时调用 Google Play Developer API。
因此,BillingClient 中没有取消方法是正常的。用户取消订阅时进入 Google Play 管理页面,开发者代为取消时则调用受保护的服务端 API。
备注:内容仅供参考。
