rc-subscription-states

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Subscription States

订阅状态

Decide whether a user has access, and drive state aware UI, using
CustomerInfo
and
EntitlementInfo
on Android.
在Android上使用
CustomerInfo
EntitlementInfo
判断用户是否拥有访问权限,并实现感知状态的UI。

Phase 1: Discover

阶段1:明确需求

Confirm what you are actually checking before you write code.
  • Which entitlement identifier gates the feature? (for example
    pro_access
    )
  • Do you need a plain access boolean, or do you also need to explain why the user has or lacks access (billing issue, canceled but still paid, paused)?
  • Do you need the cached value (fast, possibly stale) or a freshly fetched value (server authoritative)?
  • Are you refreshing UI once on launch, or reacting to live changes (purchase, restore, background refresh)?
If you only need access on/off, you only need
isActive
. Everything else is optional context.
在编写代码前,先确认实际需要检查的内容:
  • 哪个权限标识符用于限制功能?(例如
    pro_access
  • 你只需要一个表示是否有权限的布尔值,还是需要解释用户拥有或缺少权限的原因(账单问题、已取消但仍在有效期、暂停)?
  • 你需要缓存值(速度快,但可能过期)还是最新获取的值(服务器权威数据)?
  • 你是在启动时刷新一次UI,还是需要响应实时变化(购买、恢复、后台刷新)?
如果只需要判断是否有权限,仅使用
isActive
即可,其他字段都是可选的补充信息。

Phase 2: Plan

阶段2:规划实现

Google's seven states versus RevenueCat's boolean

Google的七种状态 vs RevenueCat的布尔值

Rolling your own tracker with the Google Play Developer API means mapping seven subscription states and deciding which grant access.
Google stateGrants access?
ACTIVEyes
IN_GRACE_PERIODyes
CANCELED (before
expirationDate
)
yes
ON_HOLDno
PAUSEDno
EXPIREDno
PENDINGno
RevenueCat computes this on the backend from the Google
SubscriptionPurchaseV2
resource and exposes the result as one field:
EntitlementInfo.isActive
. You read a boolean instead of implementing the state machine.
使用Google Play开发者API自行实现追踪逻辑,需要映射七种订阅状态并确定哪些状态可授予权限:
Google状态是否授予权限?
ACTIVE
IN_GRACE_PERIOD
CANCELED(在
expirationDate
之前)
ON_HOLD
PAUSED
EXPIRED
PENDING
RevenueCat会在后端从Google的
SubscriptionPurchaseV2
资源计算出结果,并将其作为一个字段暴露:
EntitlementInfo.isActive
。你只需读取一个布尔值,无需实现状态机。

What to read, and when

读取内容及时机

NeedField
Does the user have access right now?
entitlement.isActive
Will the subscription renew at period end?
entitlement.willRenew
When does paid access end?
entitlement.expirationDate
Is this a trial, intro, prepaid, or normal period?
entitlement.periodType
Is there a payment problem?
entitlement.billingIssueDetectedAt
Has the user canceled but still has time left?
entitlement.unsubscribeDetectedAt
Which store issued the entitlement?
entitlement.store
需求字段
用户当前是否拥有访问权限?
entitlement.isActive
订阅会在周期结束时自动续费吗?
entitlement.willRenew
付费访问何时到期?
entitlement.expirationDate
当前是试用、首单优惠、预付费还是常规周期?
entitlement.periodType
是否存在支付问题?
entitlement.billingIssueDetectedAt
用户已取消订阅但仍在有效期内?
entitlement.unsubscribeDetectedAt
哪个应用商店颁发的权限?
entitlement.store

Decision rules

判断规则

  • Gate features on
    isActive == true
    . Nothing else.
  • Use
    billingIssueDetectedAt != null
    to show a fix payment prompt.
  • Use
    unsubscribeDetectedAt != null
    with
    expirationDate
    to show a renewal reminder while access is still valid.
  • Use
    !willRenew
    (when no billing issue and no explicit cancel timestamp) to show a non renewing notice.
  • 功能权限判断仅基于
    isActive == true
    ,其他字段均不用于权限校验。
  • 使用
    billingIssueDetectedAt != null
    显示修复支付提示。
  • 使用
    unsubscribeDetectedAt != null
    结合
    expirationDate
    ,在用户仍有权限时显示续费提醒。
  • 使用
    !willRenew
    (无账单问题且无明确取消时间戳时)显示不再续费通知。

Phase 3: Execute

阶段3:执行实现

Read CustomerInfo and check access

读取CustomerInfo并检查权限

kotlin
val customerInfo = Purchases.sharedInstance.awaitCustomerInfo()
val hasAccess = customerInfo.entitlements["pro_access"]?.isActive == true
awaitCustomerInfo()
returns the disk cache immediately, then refreshes from the network in the background. Entitlement checks stay fast, even offline.
kotlin
val customerInfo = Purchases.sharedInstance.awaitCustomerInfo()
val hasAccess = customerInfo.entitlements["pro_access"]?.isActive == true
awaitCustomerInfo()
会立即返回磁盘缓存中的数据,然后在后台从网络刷新数据。权限检查保持快速,即使离线也能正常进行。

Force a fresh fetch when you must

必要时强制获取最新数据

Use this after a server side grant (for example, a support agent issued a promo).
kotlin
val fresh = Purchases.sharedInstance.awaitCustomerInfo(
    fetchPolicy = CacheFetchPolicy.FETCH_CURRENT
)
在服务器端授予权限后(例如客服发放促销权限)使用此方法:
kotlin
val fresh = Purchases.sharedInstance.awaitCustomerInfo(
    fetchPolicy = CacheFetchPolicy.FETCH_CURRENT
)

Drive state aware UI

实现感知状态的UI

isActive
gates access; the other fields explain context.
kotlin
fun updateUI(entitlement: EntitlementInfo?) {
    if (entitlement == null || !entitlement.isActive) {
        showSubscribeScreen(); return
    }
    showPremiumContent()
    when {
        entitlement.billingIssueDetectedAt != null -> showBillingIssueWarning()
        entitlement.unsubscribeDetectedAt != null ->
            entitlement.expirationDate?.let { showExpiryNotice(it) }
        !entitlement.willRenew -> showNonRenewingNotice()
    }
}
isActive
用于控制权限访问,其他字段用于补充上下文信息:
kotlin
fun updateUI(entitlement: EntitlementInfo?) {
    if (entitlement == null || !entitlement.isActive) {
        showSubscribeScreen(); return
    }
    showPremiumContent()
    when {
        entitlement.billingIssueDetectedAt != null -> showBillingIssueWarning()
        entitlement.unsubscribeDetectedAt != null ->
            entitlement.expirationDate?.let { showExpiryNotice(it) }
        !entitlement.willRenew -> showNonRenewingNotice()
    }
}

Messaging guide by signal

不同信号对应的提示文案指南

Signal on an active entitlementMessage to show
billingIssueDetectedAt != null
Payment problem, update method
unsubscribeDetectedAt != null
Access ends on
expirationDate
willRenew == false
(no other signal)
Will not renew this period
periodType == TRIAL
or
INTRO
Trial or intro pricing in effect
有效权限的信号显示的提示文案
billingIssueDetectedAt != null
支付出现问题,请更新支付方式
unsubscribeDetectedAt != null
访问权限将于
expirationDate
到期
willRenew == false
(无其他信号)
本期结束后将不再自动续费
periodType == TRIAL
INTRO
当前处于试用或首单优惠期

Identify the user for multi device

为多设备场景识别用户

kotlin
val result = Purchases.sharedInstance.awaitLogIn("your_user_id")
val customerInfo = result.customerInfo
val createdNewUser = result.created
awaitLogIn()
merges anonymous purchases with the identified user.
logOut()
starts a fresh anonymous session.
kotlin
val result = Purchases.sharedInstance.awaitLogIn("your_user_id")
val customerInfo = result.customerInfo
val createdNewUser = result.created
awaitLogIn()
会将匿名购买记录与已识别用户合并。
logOut()
会启动新的匿名会话。

Phase 4: Verify

阶段4:验证测试

Listen for CustomerInfo updates

监听CustomerInfo更新

Register a listener so UI reacts to purchases, restores, and background refreshes without manual polling.
kotlin
Purchases.sharedInstance.updatedCustomerInfoListener =
    UpdatedCustomerInfoListener { info ->
        val active = info.entitlements["pro_access"]?.isActive == true
        updateAccessGate(active)
    }
The listener does not fire when the SDK starts with a cache hit and nothing changed. Always call
awaitCustomerInfo()
on launch in addition to setting the listener.
注册监听器,使UI能够响应购买、恢复和后台刷新操作,无需手动轮询:
kotlin
Purchases.sharedInstance.updatedCustomerInfoListener =
    UpdatedCustomerInfoListener { info ->
        val active = info.entitlements["pro_access"]?.isActive == true
        updateAccessGate(active)
    }
当SDK启动时命中缓存且数据无变化,监听器不会触发。除设置监听器外,务必在启动时调用
awaitCustomerInfo()

Test matrix

测试矩阵

Walk through each case and confirm the UI responds correctly.
CaseExpected
isActive
Expected UI
Fresh purchasetruePremium content
Grace period (billing issue, still granted)truePremium + billing warning
Canceled, still before
expirationDate
truePremium + expiry notice
On holdfalseSubscribe screen
PausedfalseSubscribe screen
ExpiredfalseSubscribe screen
Pending (no payment yet)falseSubscribe screen
逐一测试以下场景,确认UI响应正确:
场景预期
isActive
预期UI表现
新购买true显示付费内容
宽限期(存在账单问题,但仍授予权限)true显示付费内容 + 账单警告
已取消订阅,但仍在
expirationDate
之前
true显示付费内容 + 到期提醒
暂停状态false显示订阅界面
已暂停false显示订阅界面
已过期false显示订阅界面
待处理(尚未支付)false显示订阅界面

Sanity checks

合理性检查

  • Access gate flips correctly when the listener fires after a purchase.
  • FETCH_CURRENT
    updates
    CustomerInfo
    after a backend grant (promo, refund, support action).
  • After
    logOut()
    ,
    CustomerInfo
    reflects an anonymous user and
    isActive
    resets accordingly.
  • Offline launch still returns cached
    CustomerInfo
    and gates access without a network call.
  • 购买后监听器触发时,权限控制逻辑正确切换。
  • 后端授予权限(促销、退款、客服操作)后,
    FETCH_CURRENT
    能更新
    CustomerInfo
  • 调用
    logOut()
    后,
    CustomerInfo
    反映匿名用户状态,
    isActive
    相应重置。
  • 离线启动时仍能返回缓存的
    CustomerInfo
    ,无需网络请求即可控制权限。

References

参考资料