
Rc Subscription States
- 193 installs
- 55 repo stars
- Updated August 3, 2026
- revenuecat/ai-toolkit
Helps with ai & agent building tasks.
About
rc-subscription-states is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- rc-subscription-states
- AI & Agent Building
- AI-coding skill
Rc Subscription States by the numbers
- 193 all-time installs (skills.sh)
- +28 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #2,936 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/revenuecat/ai-toolkit --skill rc-subscription-statesAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 193 |
|---|---|
| repo stars | ★ 55 |
| Last updated | August 3, 2026 |
| Repository | revenuecat/ai-toolkit ↗ |
What it does
Helps with ai & agent building tasks.
Files
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 |
| IN_GRACE_PERIOD | 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 == trueawaitCustomerInfo() 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.createdawaitLogIn() 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.