rc-subscription-states
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseSubscription States
订阅状态
Decide whether a user has access, and drive state aware UI, using and on Android.
CustomerInfoEntitlementInfo在Android上使用和判断用户是否拥有访问权限,并实现感知状态的UI。
CustomerInfoEntitlementInfoPhase 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 . Everything else is optional context.
isActive在编写代码前,先确认实际需要检查的内容:
- 哪个权限标识符用于限制功能?(例如)
pro_access - 你只需要一个表示是否有权限的布尔值,还是需要解释用户拥有或缺少权限的原因(账单问题、已取消但仍在有效期、暂停)?
- 你需要缓存值(速度快,但可能过期)还是最新获取的值(服务器权威数据)?
- 你是在启动时刷新一次UI,还是需要响应实时变化(购买、恢复、后台刷新)?
如果只需要判断是否有权限,仅使用即可,其他字段都是可选的补充信息。
isActivePhase 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 state | Grants access? |
|---|---|
| ACTIVE | yes |
| IN_GRACE_PERIOD | yes |
CANCELED (before | yes |
| ON_HOLD | no |
| PAUSED | no |
| EXPIRED | no |
| PENDING | no |
RevenueCat computes this on the backend from the Google resource and exposes the result as one field: . You read a boolean instead of implementing the state machine.
SubscriptionPurchaseV2EntitlementInfo.isActive使用Google Play开发者API自行实现追踪逻辑,需要映射七种订阅状态并确定哪些状态可授予权限:
| Google状态 | 是否授予权限? |
|---|---|
| ACTIVE | 是 |
| IN_GRACE_PERIOD | 是 |
CANCELED(在 | 是 |
| ON_HOLD | 否 |
| PAUSED | 否 |
| EXPIRED | 否 |
| PENDING | 否 |
RevenueCat会在后端从Google的资源计算出结果,并将其作为一个字段暴露:。你只需读取一个布尔值,无需实现状态机。
SubscriptionPurchaseV2EntitlementInfo.isActiveWhat to read, and when
读取内容及时机
| Need | Field |
|---|---|
| Does the user have access right now? | |
| Will the subscription renew at period end? | |
| When does paid access end? | |
| Is this a trial, intro, prepaid, or normal period? | |
| Is there a payment problem? | |
| Has the user canceled but still has time left? | |
| Which store issued the entitlement? | |
| 需求 | 字段 |
|---|---|
| 用户当前是否拥有访问权限? | |
| 订阅会在周期结束时自动续费吗? | |
| 付费访问何时到期? | |
| 当前是试用、首单优惠、预付费还是常规周期? | |
| 是否存在支付问题? | |
| 用户已取消订阅但仍在有效期内? | |
| 哪个应用商店颁发的权限? | |
Decision rules
判断规则
- Gate features on . Nothing else.
isActive == true - Use to show a fix payment prompt.
billingIssueDetectedAt != null - Use with
unsubscribeDetectedAt != nullto show a renewal reminder while access is still valid.expirationDate - Use (when no billing issue and no explicit cancel timestamp) to show a non renewing notice.
!willRenew
- 功能权限判断仅基于,其他字段均不用于权限校验。
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 == trueawaitCustomerInfo()kotlin
val customerInfo = Purchases.sharedInstance.awaitCustomerInfo()
val hasAccess = customerInfo.entitlements["pro_access"]?.isActive == trueawaitCustomerInfo()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
isActivekotlin
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()
}
}isActivekotlin
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 entitlement | Message to show |
|---|---|
| Payment problem, update method |
| Access ends on |
| Will not renew this period |
| Trial or intro pricing in effect |
| 有效权限的信号 | 显示的提示文案 |
|---|---|
| 支付出现问题,请更新支付方式 |
| 访问权限将于 |
| 本期结束后将不再自动续费 |
| 当前处于试用或首单优惠期 |
Identify the user for multi device
为多设备场景识别用户
kotlin
val result = Purchases.sharedInstance.awaitLogIn("your_user_id")
val customerInfo = result.customerInfo
val createdNewUser = result.createdawaitLogIn()logOut()kotlin
val result = Purchases.sharedInstance.awaitLogIn("your_user_id")
val customerInfo = result.customerInfo
val createdNewUser = result.createdawaitLogIn()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 on launch in addition to setting the listener.
awaitCustomerInfo()注册监听器,使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.
| Case | Expected | Expected UI |
|---|---|---|
| Fresh purchase | true | Premium content |
| Grace period (billing issue, still granted) | true | Premium + billing warning |
Canceled, still before | true | Premium + expiry notice |
| On hold | false | Subscribe screen |
| Paused | false | Subscribe screen |
| Expired | false | Subscribe screen |
| Pending (no payment yet) | false | Subscribe screen |
逐一测试以下场景,确认UI响应正确:
| 场景 | 预期 | 预期UI表现 |
|---|---|---|
| 新购买 | true | 显示付费内容 |
| 宽限期(存在账单问题,但仍授予权限) | true | 显示付费内容 + 账单警告 |
已取消订阅,但仍在 | true | 显示付费内容 + 到期提醒 |
| 暂停状态 | false | 显示订阅界面 |
| 已暂停 | false | 显示订阅界面 |
| 已过期 | false | 显示订阅界面 |
| 待处理(尚未支付) | false | 显示订阅界面 |
Sanity checks
合理性检查
- Access gate flips correctly when the listener fires after a purchase.
- updates
FETCH_CURRENTafter a backend grant (promo, refund, support action).CustomerInfo - After ,
logOut()reflects an anonymous user andCustomerInforesets accordingly.isActive - Offline launch still returns cached and gates access without a network call.
CustomerInfo
- 购买后监听器触发时,权限控制逻辑正确切换。
- 后端授予权限(促销、退款、客服操作)后,能更新
FETCH_CURRENT。CustomerInfo - 调用后,
logOut()反映匿名用户状态,CustomerInfo相应重置。isActive - 离线启动时仍能返回缓存的,无需网络请求即可控制权限。
CustomerInfo