Install
$ agentstack add skill-revenuecat-play-billing-skills-subscription-states ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
Subscription States
Decide whether a user has access, and drive state aware UI, using CustomerInfo and EntitlementInfo on Android.
Phase 1: Discover
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.
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 | | INGRACEPERIOD | yes | | CANCELED (before expirationDate) | yes | | ON_HOLD | no | | PAUSED | no | | EXPIRED | no | | PENDING | no |
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.
What to read, and when
| Need | Field | |---|---| | 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 |
Decision rules
- Gate features on
isActive == true. Nothing else. - Use
billingIssueDetectedAt != nullto show a fix payment prompt. - Use
unsubscribeDetectedAt != nullwithexpirationDateto 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.
Phase 3: Execute
Read CustomerInfo and check access
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.
Force a fresh fetch when you must
Use this after a server side grant (for example, a support agent issued a promo).
val fresh = Purchases.sharedInstance.awaitCustomerInfo(
fetchPolicy = CacheFetchPolicy.FETCH_CURRENT
)
Drive state aware UI
isActive gates access; the other fields explain context.
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 expirationDate | | willRenew == false (no other signal) | Will not renew this period | | periodType == TRIAL or INTRO | Trial or intro pricing in effect |
Identify the user for multi device
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.
Phase 4: Verify
Listen for CustomerInfo updates
Register a listener so UI reacts to purchases, restores, and background refreshes without manual polling.
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.
Test matrix
Walk through each case and confirm the UI responds correctly.
| Case | Expected isActive | Expected UI | |---|---|---| | Fresh purchase | true | Premium content | | Grace period (billing issue, still granted) | true | Premium + billing warning | | Canceled, still before expirationDate | 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.
FETCH_CURRENTupdatesCustomerInfoafter a backend grant (promo, refund, support action).- After
logOut(),CustomerInforeflects an anonymous user andisActiveresets accordingly. - Offline launch still returns cached
CustomerInfoand gates access without a network call.
References
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: RevenueCat
- Source: RevenueCat/play-billing-skills
- License: Apache-2.0
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.