This skill provides expert knowledge on integrating RevenueCat for in-app purchases and subscriptions in React Native Expo apps.
/plugin marketplace add rahulkeerthi/expo-toolkit/plugin install expo-toolkit@withqwertyThis skill inherits all available tools. When active, it can use any tool Claude has access to.
This skill provides expert knowledge on integrating RevenueCat for in-app purchases and subscriptions in React Native Expo apps.
RevenueCat Account
└── Project (your app)
└── Apps (iOS, Android, etc.)
└── Products (from App Store Connect / Play Console)
└── Offerings (groups of products)
└── Packages (individual purchasable items)
└── Entitlements (what the user gets)
| Term | Description |
|---|---|
| Product | An item in App Store Connect or Play Console |
| Entitlement | A feature/access level users can unlock |
| Offering | A group of packages to present to users |
| Package | A specific purchasable item with a product |
| Customer | A user identified by app user ID |
npx expo install react-native-purchases
npm install react-native-purchases
npx expo prebuild
Add to app.config.js or app.json:
{
"expo": {
"plugins": [
[
"react-native-purchases",
{
"REVENUECAT_API_KEY": "appl_xxxxxxxx", // iOS key
// Or for Android:
// "REVENUECAT_API_KEY": "goog_xxxxxxxx"
}
]
]
}
}
Multi-platform setup:
plugins: [
[
"react-native-purchases",
{
"REVENUECAT_API_KEY_IOS": "appl_xxxxxxxx",
"REVENUECAT_API_KEY_ANDROID": "goog_xxxxxxxx"
}
]
]
import Purchases, { LOG_LEVEL } from 'react-native-purchases';
import { Platform } from 'react-native';
const API_KEY = Platform.select({
ios: 'appl_xxxxxxxx',
android: 'goog_xxxxxxxx',
});
export async function initPurchases() {
if (__DEV__) {
Purchases.setLogLevel(LOG_LEVEL.VERBOSE);
}
await Purchases.configure({ apiKey: API_KEY });
}
export async function initPurchases(userId?: string) {
await Purchases.configure({ apiKey: API_KEY });
if (userId) {
await Purchases.logIn(userId);
}
}
// In App.tsx or root component
useEffect(() => {
initPurchases();
}, []);
In RevenueCat Dashboard:
import Purchases from 'react-native-purchases';
export async function checkPremiumAccess(): Promise<boolean> {
try {
const customerInfo = await Purchases.getCustomerInfo();
return customerInfo.entitlements.active['premium'] !== undefined;
} catch (error) {
console.error('Error checking entitlements:', error);
return false;
}
}
import { useEffect, useState } from 'react';
import Purchases, { CustomerInfo } from 'react-native-purchases';
export function usePremiumStatus() {
const [isPremium, setIsPremium] = useState(false);
const [loading, setLoading] = useState(true);
useEffect(() => {
const checkStatus = async () => {
try {
const customerInfo = await Purchases.getCustomerInfo();
setIsPremium(customerInfo.entitlements.active['premium'] !== undefined);
} catch (error) {
console.error(error);
} finally {
setLoading(false);
}
};
checkStatus();
// Listen for changes
const listener = Purchases.addCustomerInfoUpdateListener((info: CustomerInfo) => {
setIsPremium(info.entitlements.active['premium'] !== undefined);
});
return () => listener.remove();
}, []);
return { isPremium, loading };
}
import Purchases, { PurchasesOffering } from 'react-native-purchases';
export async function getOfferings(): Promise<PurchasesOffering | null> {
try {
const offerings = await Purchases.getOfferings();
return offerings.current;
} catch (error) {
console.error('Error fetching offerings:', error);
return null;
}
}
const offering = await getOfferings();
if (offering) {
offering.availablePackages.forEach(pkg => {
console.log('Package:', pkg.identifier);
console.log('Product:', pkg.product.title);
console.log('Price:', pkg.product.priceString);
console.log('Description:', pkg.product.description);
});
}
| Type | Description |
|---|---|
$rc_monthly | Monthly subscription |
$rc_annual | Annual subscription |
$rc_weekly | Weekly subscription |
$rc_lifetime | Lifetime (one-time) purchase |
| Custom | Your own identifier |
import Purchases, { PurchasesPackage } from 'react-native-purchases';
export async function purchasePackage(pkg: PurchasesPackage): Promise<boolean> {
try {
const { customerInfo } = await Purchases.purchasePackage(pkg);
if (customerInfo.entitlements.active['premium']) {
return true;
}
return false;
} catch (error: any) {
if (error.userCancelled) {
// User cancelled, not an error
return false;
}
throw error;
}
}
import React, { useEffect, useState } from 'react';
import { View, Text, TouchableOpacity, ActivityIndicator } from 'react-native';
import Purchases, { PurchasesOffering, PurchasesPackage } from 'react-native-purchases';
export function Paywall({ onPurchase }: { onPurchase: () => void }) {
const [offering, setOffering] = useState<PurchasesOffering | null>(null);
const [loading, setLoading] = useState(true);
const [purchasing, setPurchasing] = useState(false);
useEffect(() => {
const fetchOfferings = async () => {
try {
const offerings = await Purchases.getOfferings();
setOffering(offerings.current);
} catch (error) {
console.error(error);
} finally {
setLoading(false);
}
};
fetchOfferings();
}, []);
const handlePurchase = async (pkg: PurchasesPackage) => {
setPurchasing(true);
try {
const { customerInfo } = await Purchases.purchasePackage(pkg);
if (customerInfo.entitlements.active['premium']) {
onPurchase();
}
} catch (error: any) {
if (!error.userCancelled) {
console.error('Purchase error:', error);
}
} finally {
setPurchasing(false);
}
};
if (loading) {
return <ActivityIndicator />;
}
return (
<View>
<Text>Upgrade to Premium</Text>
{offering?.availablePackages.map(pkg => (
<TouchableOpacity
key={pkg.identifier}
onPress={() => handlePurchase(pkg)}
disabled={purchasing}
>
<Text>{pkg.product.title}</Text>
<Text>{pkg.product.priceString}</Text>
</TouchableOpacity>
))}
</View>
);
}
export async function restorePurchases(): Promise<boolean> {
try {
const customerInfo = await Purchases.restorePurchases();
return customerInfo.entitlements.active['premium'] !== undefined;
} catch (error) {
console.error('Error restoring purchases:', error);
throw error;
}
}
Important: Apple requires a "Restore Purchases" button in your app.
RevenueCat offers pre-built paywall templates:
npx expo install react-native-purchases-ui
import RevenueCatUI from 'react-native-purchases-ui';
function MyPaywall() {
return (
<RevenueCatUI.Paywall
options={{
displayCloseButton: true,
}}
onDismiss={() => console.log('Dismissed')}
onPurchaseCompleted={({ customerInfo }) => {
console.log('Purchased!', customerInfo);
}}
/>
);
}
import { presentPaywall, presentPaywallIfNeeded } from 'react-native-purchases-ui';
// Present paywall
await presentPaywall();
// Only show if not subscribed
await presentPaywallIfNeeded({ requiredEntitlementIdentifier: 'premium' });
com.yourapp.premium.monthly)Use consistent naming across platforms:
com.yourcompany.yourapp.premium.monthly
com.yourcompany.yourapp.premium.annual
com.yourcompany.yourapp.premium.lifetime
The "Default" offering is what offerings.current returns. Always have one.
Offering: default
├── Package: $rc_monthly
│ └── Product: com.app.premium.monthly
├── Package: $rc_annual (Best Value)
│ └── Product: com.app.premium.annual
└── Package: $rc_lifetime
└── Product: com.app.premium.lifetime
Offering: sale_50_off
├── Package: $rc_monthly
│ └── Product: com.app.premium.monthly.sale
└── Package: $rc_annual
└── Product: com.app.premium.annual.sale
const offerings = await Purchases.getOfferings();
// Current (default) offering
const current = offerings.current;
// Specific offering by ID
const saleOffering = offerings.all['sale_50_off'];
const customerInfo = await Purchases.getCustomerInfo();
// Active subscriptions
const activeSubscriptions = customerInfo.activeSubscriptions;
// Entitlement expiration
const premiumEntitlement = customerInfo.entitlements.active['premium'];
if (premiumEntitlement) {
console.log('Expires:', premiumEntitlement.expirationDate);
console.log('Will renew:', premiumEntitlement.willRenew);
}
// Open subscription management (platform native)
import { Linking, Platform } from 'react-native';
function openSubscriptionManagement() {
if (Platform.OS === 'ios') {
Linking.openURL('https://apps.apple.com/account/subscriptions');
} else {
Linking.openURL('https://play.google.com/store/account/subscriptions');
}
}
RevenueCat automatically detects sandbox purchases. In dashboard:
Configure webhooks in RevenueCat Dashboard:
| Event | Description |
|---|---|
INITIAL_PURCHASE | First purchase |
RENEWAL | Subscription renewed |
CANCELLATION | Subscription cancelled |
EXPIRATION | Subscription expired |
BILLING_ISSUE | Payment failed |
PRODUCT_CHANGE | Plan changed |
Products not loading:
Purchases failing:
Entitlements not granting:
import Purchases, { LOG_LEVEL } from 'react-native-purchases';
// Enable verbose logging in development
if (__DEV__) {
Purchases.setLogLevel(LOG_LEVEL.VERBOSE);
}
// Get app user ID
const appUserId = await Purchases.getAppUserID();
console.log('App User ID:', appUserId);
// Check if configured
const isConfigured = Purchases.isConfigured;
console.log('Is Configured:', isConfigured);
Before launching with RevenueCat:
// Initialise
await Purchases.configure({ apiKey: 'your_key' });
// Get offerings
const offerings = await Purchases.getOfferings();
const current = offerings.current;
// Make purchase
const { customerInfo } = await Purchases.purchasePackage(package);
// Check entitlement
const isPremium = customerInfo.entitlements.active['premium'] !== undefined;
// Restore
await Purchases.restorePurchases();
// Get customer info
const info = await Purchases.getCustomerInfo();
// Listen for updates
Purchases.addCustomerInfoUpdateListener((info) => {
// Handle updates
});
// User identification
await Purchases.logIn('user_id');
await Purchases.logOut();
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.