Subscription States
Decide whether a user has access, and drive state aware UI, using
and
on Android.
Phase 1: Discover
Confirm what you are actually checking before you write code.
- Which entitlement identifier gates the feature? (for example )
- 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.
Phase 2: Plan
Google's seven states versus RevenueCat's boolean
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.
What 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? | entitlement.expirationDate
|
| Is this a trial, intro, prepaid, or normal period? | |
| Is there a payment problem? | entitlement.billingIssueDetectedAt
|
| Has the user canceled but still has time left? | entitlement.unsubscribeDetectedAt
|
| Which store issued the entitlement? | |
Decision rules
- Gate features on . Nothing else.
- Use
billingIssueDetectedAt != null
to show a fix payment prompt.
- Use
unsubscribeDetectedAt != null
with to show a renewal reminder while access is still valid.
- Use (when no billing issue and no explicit cancel timestamp) to show a non renewing notice.
Phase 3: Execute
Read CustomerInfo and check access
kotlin
val customerInfo = Purchases.sharedInstance.awaitCustomerInfo()
val hasAccess = customerInfo.entitlements["pro_access"]?.isActive == true
returns the disk cache immediately, then refreshes from the network in the background. Entitlement checks stay fast, even offline.
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
)
Drive state aware UI
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()
}
}
Messaging guide by signal
| Signal on an active entitlement | Message to show |
|---|
billingIssueDetectedAt != null
| Payment problem, update method |
unsubscribeDetectedAt != null
| Access ends on |
| (no other signal) | Will not renew this period |
| or | 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.created
merges anonymous purchases with the identified user.
starts a fresh anonymous session.
Phase 4: Verify
Listen for CustomerInfo updates
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.
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 |
Sanity checks
- Access gate flips correctly when the listener fires after a purchase.
- updates after a backend grant (promo, refund, support action).
- After , reflects an anonymous user and resets accordingly.
- Offline launch still returns cached and gates access without a network call.
References