是否可以通过 BillingClient 检测 grace period?
结论
仅靠 BillingClient 无法准确区分正常有效的订阅和处于 grace period(宽限期)的订阅。
在这两种状态下,BillingClient.queryPurchasesAsync() 都可能返回:
purchaseState == Purchase.PurchaseState.PURCHASEDisAutoRenewing == true
这是正常现象。宽限期内,用户仍享有订阅权益,客户端返回的 Purchase 对象也没有单独表示”当前处于宽限期”的状态。
如果业务需要明确识别 grace period,应在服务端查询 Google Play Developer API。新接入的系统通常优先使用 purchases.subscriptionsv2.get;仍在使用旧版订阅接口的系统,可以通过 Purchases.subscriptions:get 获取相应的服务端状态。
为什么 BillingClient 无法判断
BillingClient 用于完成购买流程、查询设备当前能够识别的购买记录,以及确认用户是否拥有相应商品。它并不是完整的订阅生命周期状态接口。
进入宽限期意味着续费扣款暂时失败,但 Google Play 仍允许用户继续使用订阅权益。因此,从客户端授权的角度看,这笔订阅仍是有效购买:
purchase.purchaseState == Purchase.PurchaseState.PURCHASED
purchase.isAutoRenewing == true
这两个字段无法说明最近一次续费是否失败,也无法判断订阅是否已进入宽限期。
不要根据 purchaseState 的数字值判断状态。应使用 Purchase.PurchaseState.PURCHASED 等常量,避免代码依赖具体的实现值。
推荐的处理方式
一般可以按以下流程处理:
- Android 客户端通过
BillingClient获取purchaseToken。 - 客户端把
purchaseToken、应用包名和商品信息发送给自己的服务端。 - 服务端调用 Google Play Developer API 验证订阅。
- 服务端根据订阅状态决定是否授予权益,并把业务所需的状态返回客户端。
- 配置 Real-time Developer Notifications。订阅状态发生变化后及时重新查询,不要只依赖客户端轮询。
不要直接在 Android 应用中调用 Google Play Developer API,也不要把服务账号凭据打包进 APK。
使用 purchases.subscriptionsv2.get 判断
对于支持该接口的订阅实现,服务端可以查询:
GET https://androidpublisher.googleapis.com/androidpublisher/v3/applications/{packageName}/purchases/subscriptionsv2/tokens/{token}
返回结果中的 subscriptionState 会明确表示订阅是否处于宽限期:
SUBSCRIPTION_STATE_IN_GRACE_PERIOD
服务端判断逻辑可以写成:
const response = await androidpublisher.purchases.subscriptionsv2.get({
packageName,
token: purchaseToken,
});
const state = response.data.subscriptionState;
const inGracePeriod =
state === "SUBSCRIPTION_STATE_IN_GRACE_PERIOD";
const entitlementActive =
state === "SUBSCRIPTION_STATE_ACTIVE" ||
state === "SUBSCRIPTION_STATE_IN_GRACE_PERIOD";
这里要分别处理两个概念:
inGracePeriod:订阅是否处于宽限期。entitlementActive:是否仍应向用户提供订阅权益。
通常情况下,宽限期内应继续提供权益。
使用旧版 Purchases.subscriptions:get
仍在使用旧版接口的系统可以查询:
GET https://androidpublisher.googleapis.com/androidpublisher/v3/applications/{packageName}/purchases/subscriptions/{subscriptionId}/tokens/{token}
然后结合返回的 paymentState、expiryTimeMillis、autoRenewing 等服务端字段判断订阅状态。字段是否返回及其具体含义,应以当前接口文档和实际响应为准。客户端的 Purchase.purchaseState 与服务端的 paymentState 不是同一个字段,不能混用。
如果可以选择,建议使用带有明确 subscriptionState 的 purchases.subscriptionsv2.get,判断逻辑更直接。
客户端代码仍然应该做什么
客户端仍可使用 BillingClient 完成基础的购买检查:
billingClient.queryPurchasesAsync(
QueryPurchasesParams.newBuilder()
.setProductType(BillingClient.ProductType.SUBS)
.build()
) { billingResult, purchases ->
if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
return@queryPurchasesAsync
}
purchases.forEach { purchase ->
if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED) {
sendPurchaseTokenToBackend(purchase.purchaseToken)
}
}
}
这段代码只能说明客户端发现了一笔已购买的订阅,无法据此确定它处于正常续费状态还是宽限期。最终是否授权,应以服务端验证结果为准。
测试时的注意事项
使用测试卡模拟续费失败时,还要确认以下几点:
- Play Console 中对应订阅是否启用了 grace period。
- 测试订阅的续费、宽限期和账号保留时间通常会被压缩,不能按照生产环境的自然时间推算。
- 修改测试付款方式后,客户端状态不一定会立即出现可见变化。
queryPurchasesAsync()可能继续返回这笔购买,因为用户在宽限期内仍有权益。- grace period 结束后,订阅可能进入 account hold。account hold 通常不应继续授予权益,不能沿用宽限期的授权策略。
- Real-time Developer Notifications 只表示订阅发生了变化。收到通知后,仍需调用 Developer API 获取权威状态。
BillingClient 适合发现购买并获取 purchaseToken。grace period 等更细的订阅状态,应由服务端通过 Google Play Developer API 判断。
备注:内容仅供参考。
