rc-payment-recovery

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Payment Recovery

订阅续费恢复

Failed renewals on Google Play move a subscription through two states: grace period (user keeps access while Google retries the card) and account hold (access revoked until the user fixes the payment method). With RevenueCat, both states land in
CustomerInfo
automatically, and Google's in app message shows by default.
Google Play平台上的订阅续费失败后,订阅会进入两个状态:宽限期(用户在Google重试支付期间仍可访问服务)和账户冻结(用户修复支付方式前将被取消访问权限)。通过RevenueCat,这两种状态都会自动同步到
CustomerInfo
中,且Google的应用内消息默认会自动显示。

Phase 1: Understand

第一阶段:理解

Three things happen when a renewal fails:
StateAccessHow RevenueCat surfaces itUser sees
Grace periodRetained
entitlement.isActive == true
and
billingIssueDetectedAt != null
Google in app snackbar by default
Account holdRevoked
entitlement.isActive == false
and
billingIssueDetectedAt != null
Google in app snackbar by default
RecoveredRetained
billingIssueDetectedAt == null
Nothing
Two signals matter in the SDK:
  • EntitlementInfo.billingIssueDetectedAt
    is non null from the moment Google reports a billing problem until the user resolves it.
  • EntitlementInfo.isActive
    tells you whether they still have access.
On the backend, a
BILLING_ISSUE
webhook fires once per transition. You do not decode RTDNs.
续费失败时会发生以下三件事:
状态访问权限RevenueCat的呈现方式用户看到的内容
宽限期保留
entitlement.isActive == true
billingIssueDetectedAt != null
默认显示Google应用内 Snackbar
账户冻结取消
entitlement.isActive == false
billingIssueDetectedAt != null
默认显示Google应用内 Snackbar
已恢复保留
billingIssueDetectedAt == null
无内容
SDK中有两个关键信号:
  • EntitlementInfo.billingIssueDetectedAt
    从Google报告账单问题开始,到用户解决问题前,该值始终非空。
  • EntitlementInfo.isActive
    用于判断用户是否仍拥有访问权限。
在后端,每次状态转换时都会触发
BILLING_ISSUE
webhook。无需解析RTDNs。

Phase 2: Plan

第二阶段:规划

Before you write app code, decide what you actually need. Most apps need none.
Ask:
  1. Do you want the default Google in app message? If yes, do nothing. The SDK calls
    showInAppMessagesIfNeeded
    on BillingClient connect.
  2. Do you want your own banner or dialog? If yes, read
    billingIssueDetectedAt
    from
    CustomerInfo
    and branch on
    isActive
    .
  3. Do you want to gate the message to specific screens? If yes, disable the automatic call and invoke
    showInAppMessagesIfNeeded(activity)
    yourself.
  4. Do you need a server side flag (for example, to send a recovery email)? If yes, handle the
    BILLING_ISSUE
    webhook. No app code required.
If you only want the default behavior, stop here.
在编写应用代码前,先确定实际需求。大多数应用无需额外开发。
思考以下问题:
  1. 是否需要默认的Google应用内消息?如果是,无需任何操作。SDK会在BillingClient连接时自动调用
    showInAppMessagesIfNeeded
  2. 是否需要自定义横幅或弹窗?如果是,从
    CustomerInfo
    中读取
    billingIssueDetectedAt
    并根据
    isActive
    进行分支处理。
  3. 是否需要将消息限定在特定页面显示?如果是,禁用自动调用,自行在指定的Activity中调用
    showInAppMessagesIfNeeded(activity)
  4. 是否需要服务端标识(例如发送恢复邮件)?如果是,处理
    BILLING_ISSUE
    webhook即可,无需编写应用代码。
如果仅需要默认行为,到此为止即可。

Phase 3: Execute

第三阶段:实施

Default (recommended)

默认方式(推荐)

Leave automatic in app messages on. This is the default:
kotlin
PurchasesConfiguration.Builder(context, apiKey)
    .showInAppMessagesAutomatically(true)
    .build()
保持应用内消息自动开启。这是默认配置:
kotlin
PurchasesConfiguration.Builder(context, apiKey)
    .showInAppMessagesAutomatically(true)
    .build()

Manual trigger

手动触发

Disable the automatic call and show the message from your chosen activity:
kotlin
PurchasesConfiguration.Builder(context, apiKey)
    .showInAppMessagesAutomatically(false)
    .build()

Purchases.sharedInstance.showInAppMessagesIfNeeded(activity)
禁用自动调用,在指定的Activity中手动显示消息:
kotlin
PurchasesConfiguration.Builder(context, apiKey)
    .showInAppMessagesAutomatically(false)
    .build()

Purchases.sharedInstance.showInAppMessagesIfNeeded(activity)

Your own UI during grace period

宽限期内自定义UI

Read the entitlement and branch on both flags:
kotlin
val entitlement = customerInfo.entitlements["pro_access"]
when {
    entitlement == null || !entitlement.isActive ->
        showSubscribeScreen()
    entitlement.billingIssueDetectedAt != null && entitlement.isActive ->
        showGracePeriodWarning()
    entitlement.billingIssueDetectedAt != null && !entitlement.isActive ->
        showAccountHoldScreen()
    else ->
        showPremiumContent()
}
读取订阅权限并根据两个标志进行分支处理:
kotlin
val entitlement = customerInfo.entitlements["pro_access"]
when {
    entitlement == null || !entitlement.isActive ->
        showSubscribeScreen()
    entitlement.billingIssueDetectedAt != null && entitlement.isActive ->
        showGracePeriodWarning()
    entitlement.billingIssueDetectedAt != null && !entitlement.isActive ->
        showAccountHoldScreen()
    else ->
        showPremiumContent()
}

Send the user to fix payment

引导用户修复支付方式

CustomerInfo.managementURL
points to the Google Play subscription page:
kotlin
customerInfo.managementURL?.let { url ->
    startActivity(Intent(Intent.ACTION_VIEW, url))
}
CustomerInfo.managementURL
指向Google Play订阅管理页面:
kotlin
customerInfo.managementURL?.let { url ->
    startActivity(Intent(Intent.ACTION_VIEW, url))
}

Phase 4: Verify

第四阶段:验证

Test each transition:
  • Use a Google Play test card that declines renewals to push a subscription into grace period.
  • Confirm
    entitlement.billingIssueDetectedAt
    becomes non null and
    isActive
    stays
    true
    .
  • Wait for account hold and confirm
    isActive
    flips to
    false
    while
    billingIssueDetectedAt
    remains non null.
  • Update the payment method and confirm
    billingIssueDetectedAt
    returns to
    null
    .
  • On backend, confirm a
    BILLING_ISSUE
    webhook fires on the first transition.
测试每一种状态转换:
  • 使用Google Play的测试拒付卡片,将订阅推入宽限期。
  • 确认
    entitlement.billingIssueDetectedAt
    变为非空,且
    isActive
    保持为
    true
  • 等待进入账户冻结状态,确认
    isActive
    变为
    false
    ,同时
    billingIssueDetectedAt
    仍为非空。
  • 更新支付方式,确认
    billingIssueDetectedAt
    恢复为
    null
  • 在后端,确认首次状态转换时触发了
    BILLING_ISSUE
    webhook。

References

参考资料