
Revenuecat Entitlements Gate
- 313 installs
- 55 repo stars
- Updated August 3, 2026
- revenuecat/ai-toolkit
Gate premium screens, APIs, or features using RevenueCat entitlement state so only paying or trialing users access subscribed capabilities.
About
Implements RevenueCat entitlement gating: fetch CustomerInfo, evaluate active entitlements, block or unlock features in mobile and API layers, and handle restore, expiration, and grace-period edge cases for subscription-backed access control.
- Reads RevenueCat CustomerInfo entitlement flags
- Guards routes, components, and API handlers by entitlement
- Handles expired, grace-period, and restored purchases
- Supports client-side and server-validated gating patterns
Revenuecat Entitlements Gate by the numbers
- 313 all-time installs (skills.sh)
- +37 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #365 of 1,039 Mobile Development 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 revenuecat-entitlements-gateAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 313 |
|---|---|
| repo stars | ★ 55 |
| Last updated | August 3, 2026 |
| Repository | revenuecat/ai-toolkit ↗ |
What it does
Gate premium screens, APIs, or features using RevenueCat entitlement state so only paying or trialing users access subscribed capabilities.
Files
revenuecat-entitlements-gate: check a RevenueCat entitlement
Use this skill when the user wants to decide whether to show or hide a feature based on an active RevenueCat entitlement. The skill covers the one shot check and the reactive listener; it does not cover purchasing (see revenuecat-purchase-flow) or auth (revenuecat-identify-user).
1. Detect the platform
Inspect the working directory and pick the first match, from top to bottom:
1. React Native: package.json has a react-native-purchases entry, or react-native as a dependency → read platforms/react-native.md. If expo is also a dependency, note it as an Expo project. 2. Flutter: pubspec.yaml exists at the project root → read platforms/flutter.md. 3. Kotlin Multiplatform: build.gradle.kts contains a kotlin { … } multiplatform source sets block, or depends on com.revenuecat.purchases:purchases-kmp* → read platforms/kmp.md. 4. Android (native): build.gradle(.kts) applies com.android.application (and is not KMP) → read platforms/android.md. 5. iOS (native): Package.swift, *.xcodeproj, *.xcworkspace, or Podfile at the project root → read platforms/ios.md.
If several match (e.g. an ios/ folder inside a Flutter project), pick the outermost project, the one that owns the build. If still ambiguous, ask the user which platform they want to configure.
2. Shared concepts (all platforms)
- Check the entitlement identifier, not the product ID. The identifier (for example
"premium") is configured in the RevenueCat dashboard and mapped to one or more products. Using the entitlement lets you change products, prices, and stores without touching app code. - `customerInfo.entitlements.active` is the source of truth. It is a
Map<String, EntitlementInfo>keyed by entitlement identifier. Presence inactivemeans the user currently has access. Absence means they do not, regardless of past purchases. - Do not gate on purchase history. Expired subscriptions still appear in
customerInfo.entitlements.allbut drop out ofactive. Useactiveonly. - Fetch once, then subscribe. The first
customerInfocall returns a cached value quickly, and the SDK refreshes in the background. Every SDK exposes a listener or stream that fires when entitlements change (after a purchase, restore, renewal, or expiration). Subscribe to that instead of polling. - The SDK must be configured first. If
Purchases.configure(…)has not run, the entitlement call will fail. Set up the SDK viaintegrate-revenuecatbefore using this skill.
3. Implementation
Read the platform file that matches detection:
platforms/ios.mdplatforms/android.mdplatforms/kmp.mdplatforms/flutter.mdplatforms/react-native.md
Each platform file shows the one shot check, the reactive subscription, and where to place each in a typical app.
4. Verify
Do not claim the gate works until:
1. A user with an active entitlement sees the gated feature, and a user without it does not. 2. When the entitlement state changes (test with a sandbox purchase or a manual grant in the dashboard), the UI updates without a manual restart, confirming the listener is wired. 3. The entitlement identifier in the code matches an identifier that exists in the RevenueCat dashboard. A typo here silently gates everyone out.
revenuecat-entitlements-gate: Android (native Kotlin)
One shot check (coroutines)
Use the awaitCustomerInfo() suspend extension. It throws a PurchasesException on failure.
import com.revenuecat.purchases.Purchases
import com.revenuecat.purchases.PurchasesException
import com.revenuecat.purchases.awaitCustomerInfo
suspend fun hasPremium(): Boolean = try {
val info = Purchases.sharedInstance.awaitCustomerInfo()
info.entitlements.active["premium"] != null
} catch (e: PurchasesException) {
// Network or auth error. Treat as "no access" and log for diagnostics.
false
}info.entitlements["premium"]?.isActive == true is the equivalent check against the full map.
One shot check (callback, Java friendly)
Purchases.sharedInstance.getCustomerInfo(object : ReceiveCustomerInfoCallback {
override fun onReceived(customerInfo: CustomerInfo) {
val hasPremium = customerInfo.entitlements.active["premium"] != null
// update UI
}
override fun onError(error: PurchasesError) {
// treat as no access
}
})Reactive subscription
Purchases.sharedInstance.updatedCustomerInfoListener is a single listener property. Assign a lambda early (for example in the custom Application or a DI scoped singleton) so every screen can observe the same state.
import com.revenuecat.purchases.Purchases
import com.revenuecat.purchases.interfaces.UpdatedCustomerInfoListener
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
object EntitlementsRepository {
private val _hasPremium = MutableStateFlow(false)
val hasPremium = _hasPremium.asStateFlow()
fun start() {
Purchases.sharedInstance.updatedCustomerInfoListener =
UpdatedCustomerInfoListener { info ->
_hasPremium.value = info.entitlements.active["premium"] != null
}
}
}Seed the flow once at startup so the first emission does not wait for a customer info update:
// In a coroutine scope tied to app lifetime.
runCatching {
val info = Purchases.sharedInstance.awaitCustomerInfo()
EntitlementsRepository._hasPremium.value =
info.entitlements.active["premium"] != null
}Compose usage
@Composable
fun RootScreen() {
val hasPremium by EntitlementsRepository.hasPremium.collectAsState()
if (hasPremium) PremiumScreen() else PaywallScreen()
}Notes
updatedCustomerInfoListenerholds a single reference. Setting it again replaces the previous listener, so centralize ownership in a repository or theApplication.- Replace
"premium"with the entitlement identifier configured in the RevenueCat dashboard. It is case sensitive. awaitCustomerInfo()is declared incom.revenuecat.purchases.awaitCustomerInfo. If the import is missing, add the latestcom.revenuecat.purchases:purchasesdependency (see <https://github.com/RevenueCat/purchases-android/releases>) and re-sync.
Verify
1. A sandbox user with the entitlement renders PremiumScreen; a fresh user renders PaywallScreen. 2. Make a sandbox purchase. The listener fires, the state flow updates, and Compose recomposes without restarting the app. 3. Check logcat for Purchases logs. An error fetching customer info on launch usually means the SDK was not configured, or the API key is wrong.
revenuecat-entitlements-gate: Flutter
One shot check
import 'package:purchases_flutter/purchases_flutter.dart';
Future<bool> hasPremium() async {
try {
final info = await Purchases.getCustomerInfo();
return info.entitlements.active.containsKey('premium');
} catch (e) {
// Network or auth error. Treat as "no access" and log for diagnostics.
return false;
}
}info.entitlements.all['premium']?.isActive == true is equivalent, but active is usually what you want.
Reactive subscription
Purchases.addCustomerInfoUpdateListener registers a callback that fires on every entitlement change. Feed it into a ChangeNotifier, StreamController, or your state management library.
import 'package:flutter/foundation.dart';
import 'package:purchases_flutter/purchases_flutter.dart';
class EntitlementsModel extends ChangeNotifier {
bool _hasPremium = false;
bool get hasPremium => _hasPremium;
late final CustomerInfoUpdateListener _listener;
EntitlementsModel() {
_listener = (info) {
final next = info.entitlements.active.containsKey('premium');
if (next != _hasPremium) {
_hasPremium = next;
notifyListeners();
}
};
Purchases.addCustomerInfoUpdateListener(_listener);
_seed();
}
Future<void> _seed() async {
try {
final info = await Purchases.getCustomerInfo();
_listener(info);
} catch (_) {/* ignore; listener will fire on next update */}
}
@override
void dispose() {
Purchases.removeCustomerInfoUpdateListener(_listener);
super.dispose();
}
}Widget usage
ChangeNotifierProvider(
create: (_) => EntitlementsModel(),
child: Consumer<EntitlementsModel>(
builder: (_, model, __) =>
model.hasPremium ? const PremiumScreen() : const PaywallScreen(),
),
);For a one off check with less ceremony, FutureBuilder<CustomerInfo>(future: Purchases.getCustomerInfo(), …) works, but it will not react to purchase events.
Notes
- Always call
removeCustomerInfoUpdateListenerwhen the owning object is disposed. Registered listeners leak otherwise. - Replace
'premium'with the entitlement identifier configured in the RevenueCat dashboard. It is case sensitive. purchases_fluttertargets iOS and Android only. On other platforms the calls throw; guard withPlatform.isIOS || Platform.isAndroidif your app has additional targets.
Verify
1. A sandbox user with the entitlement renders PremiumScreen; a fresh user renders PaywallScreen. 2. Make a sandbox purchase. The listener fires, notifyListeners() runs, and the widget tree rebuilds without a hot restart. 3. Watch the native logs (flutter logs / Xcode console) for Purchases entries. A repeated auth error on launch means the API key is wrong or the SDK was never configured.
revenuecat-entitlements-gate: iOS (native)
One shot check
Use Purchases.shared.customerInfo() from an async context. It returns a cached value on the first call and refreshes in the background.
import RevenueCat
func hasPremium() async -> Bool {
do {
let info = try await Purchases.shared.customerInfo()
return info.entitlements["premium"]?.isActive == true
} catch {
// Network or auth error. Treat as "no access" and log for diagnostics.
print("RevenueCat customerInfo failed: \(error)")
return false
}
}info.entitlements.active["premium"] != nil is equivalent and slightly shorter. Either form is fine.
Reactive subscription (SwiftUI)
Purchases.shared.customerInfoStream is an AsyncStream<CustomerInfo> that emits the current value and every subsequent update.
import SwiftUI
import RevenueCat
@MainActor
final class EntitlementsModel: ObservableObject {
@Published var hasPremium = false
func observe() async {
for await info in Purchases.shared.customerInfoStream {
hasPremium = info.entitlements["premium"]?.isActive == true
}
}
}
struct RootView: View {
@StateObject private var model = EntitlementsModel()
var body: some View {
Group {
if model.hasPremium {
PremiumView()
} else {
PaywallView()
}
}
.task { await model.observe() }
}
}The .task modifier starts the stream when the view appears and cancels it on disappear. No manual teardown is needed.
UIKit alternative
For UIKit, call customerInfo(completion:) on screens that need a fresh value, and keep a long lived Task observing customerInfoStream on a singleton or scene delegate.
Task {
for await info in Purchases.shared.customerInfoStream {
let isPremium = info.entitlements["premium"]?.isActive == true
await MainActor.run { /* update UI / notify observers */ }
}
}Notes
customerInfo()requires iOS 13+. For older targets, usecustomerInfo(completion:).- Replace
"premium"with the entitlement identifier configured in the RevenueCat dashboard. It is case sensitive. - Do not call
customerInfo()in a tight loop. One initial fetch plus the stream is sufficient for the lifetime of the app.
Verify
1. A sandbox user with the entitlement renders PremiumView; a fresh user renders PaywallView. 2. Make a sandbox purchase. The stream fires and the UI swaps without relaunching. 3. Revoke the entitlement in the dashboard (or let the sandbox subscription expire). Within a few minutes, or on next app foreground, the stream emits the downgrade.
revenuecat-entitlements-gate: Kotlin Multiplatform
purchases-kmp wraps the native iOS and Android SDKs. The commonMain API looks the same on both sides; only the initial configuration differs (see integrate-revenuecat).
One shot check (coroutines, commonMain)
Use the awaitCustomerInfo suspend extension from com.revenuecat.purchases.kmp.ktx. It throws a PurchasesException on failure.
import com.revenuecat.purchases.kmp.Purchases
import com.revenuecat.purchases.kmp.ktx.awaitCustomerInfo
import com.revenuecat.purchases.kmp.models.PurchasesException
suspend fun hasPremium(): Boolean = try {
val info = Purchases.sharedInstance.awaitCustomerInfo()
info.entitlements.active["premium"] != null
} catch (e: PurchasesException) {
// Network or auth error. Treat as "no access" and log for diagnostics.
false
}One shot check (callbacks)
If you do not want the coroutine dependency:
Purchases.sharedInstance.getCustomerInfo(
onError = { /* treat as no access */ },
onSuccess = { info ->
val hasPremium = info.entitlements.active["premium"] != null
// publish to your state holder
}
)Reactive subscription
purchases-kmp exposes a single PurchasesDelegate. Implement onCustomerInfoUpdated and publish the current entitlement state into a MutableStateFlow that your UI observes.
import com.revenuecat.purchases.kmp.Purchases
import com.revenuecat.purchases.kmp.PurchasesDelegate
import com.revenuecat.purchases.kmp.models.CustomerInfo
import com.revenuecat.purchases.kmp.models.StoreProduct
import com.revenuecat.purchases.kmp.models.StoreTransaction
import com.revenuecat.purchases.kmp.models.PurchasesError
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
object EntitlementsRepository : PurchasesDelegate {
private val _hasPremium = MutableStateFlow(false)
val hasPremium = _hasPremium.asStateFlow()
fun start() {
Purchases.sharedInstance.delegate = this
}
override fun onCustomerInfoUpdated(customerInfo: CustomerInfo) {
_hasPremium.value = customerInfo.entitlements.active["premium"] != null
}
override fun onPurchasePromoProduct(
product: StoreProduct,
startPurchase: (
onError: (error: PurchasesError, userCancelled: Boolean) -> Unit,
onSuccess: (storeTransaction: StoreTransaction, customerInfo: CustomerInfo) -> Unit
) -> Unit
) {
// Ignore App Store promoted purchases here, or forward to your purchase flow.
}
}Seed the flow once at startup so the first emission does not wait for an update:
// In a coroutine scope tied to app lifetime.
runCatching {
val info = Purchases.sharedInstance.awaitCustomerInfo()
EntitlementsRepository.onCustomerInfoUpdated(info)
}Notes
Purchases.sharedInstance.delegateholds a single reference. If you need to fan out to multiple consumers, funnel through a single repository (as shown) and expose aStateFlow.- Replace
"premium"with the entitlement identifier configured in the RevenueCat dashboard. It is case sensitive. - If your installed version of
purchases-kmpdoes not exposeawaitCustomerInfoincom.revenuecat.purchases.kmp.ktx, prefer what the IDE autocompletes and see thepurchases-kmpREADME. Some versions ship result based variants in a separate module.
Verify
1. A sandbox user with the entitlement renders the premium UI; a fresh user sees the paywall. 2. Make a sandbox purchase on each target. The delegate's onCustomerInfoUpdated fires and the state flow updates without relaunching. 3. On each platform, check the native logs (Xcode console on iOS, logcat on Android) for Purchases entries. The KMP SDK is a thin wrapper and its logs come from the native SDKs.
revenuecat-entitlements-gate: React Native
One shot check
import Purchases from 'react-native-purchases';
export async function hasPremium(): Promise<boolean> {
try {
const info = await Purchases.getCustomerInfo();
return info.entitlements.active['premium'] !== undefined;
} catch (e) {
// Network or auth error. Treat as "no access" and log for diagnostics.
console.warn('RevenueCat getCustomerInfo failed', e);
return false;
}
}Reactive hook
Purchases.addCustomerInfoUpdateListener registers a callback that fires on every entitlement change. Wrap it in a hook so components can subscribe and clean up automatically.
import { useEffect, useState } from 'react';
import Purchases, { CustomerInfo } from 'react-native-purchases';
export function useHasPremium(entitlementId = 'premium'): boolean {
const [hasPremium, setHasPremium] = useState(false);
useEffect(() => {
let cancelled = false;
const apply = (info: CustomerInfo) => {
if (cancelled) return;
setHasPremium(info.entitlements.active[entitlementId] !== undefined);
};
const listener = (info: CustomerInfo) => apply(info);
Purchases.addCustomerInfoUpdateListener(listener);
// Seed initial value.
Purchases.getCustomerInfo().then(apply).catch(() => {/* ignore */});
return () => {
cancelled = true;
Purchases.removeCustomerInfoUpdateListener(listener);
};
}, [entitlementId]);
return hasPremium;
}Component usage
function RootScreen() {
const hasPremium = useHasPremium();
return hasPremium ? <PremiumScreen /> : <PaywallScreen />;
}Notes
- Always call
removeCustomerInfoUpdateListeneron unmount. Listeners registered without cleanup accumulate across navigation. - Replace
'premium'with the entitlement identifier configured in the RevenueCat dashboard. It is case sensitive. - Under Expo,
react-native-purchasesrequires a development build. Entitlement calls throw on Expo Go. Verify withnpx expo start --dev-client.
Verify
1. A sandbox user with the entitlement renders <PremiumScreen />; a fresh user renders <PaywallScreen />. 2. Make a sandbox purchase. The listener fires, state updates, and the component re-renders without reloading the bundle. 3. On iOS check the Xcode console, on Android check adb logcat filtered by Purchases. Metro's JS console will not show native SDK logs.