| name | integrating-revenuecat |
| description | RevenueCat SDK setup, offerings, entitlements, and purchase flows for React Native Expo. Use when implementing in-app purchases, subscriptions, or paywalls. |
RevenueCat Integration Reference
Plugin Tools
Extract product IDs and entitlements from your codebase:
cd /path/to/expo-toolkit && npm run extract-product-ids
node tools/extract-product-ids.js /path/to/your/app
node tools/extract-product-ids.js /path/to/your/app --json
This extracts:
- Product IDs (e.g.,
com.yourapp.premium.monthly)
- Entitlement names (e.g.,
premium)
- Offering identifiers
- Package types (
$rc_monthly, $rc_annual, etc.)
Use the output to cross-reference with RevenueCat Dashboard.
RevenueCat Concepts
Hierarchy
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)
Key Terms
| 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 |
SDK Installation
For Expo (Managed Workflow)
npx expo install react-native-purchases
For Expo (Prebuild/Bare)
npm install react-native-purchases
npx expo prebuild
Plugin Configuration
Add to app.config.js or app.json:
{
"expo": {
"plugins": [
[
"react-native-purchases",
{
"REVENUECAT_API_KEY": "appl_xxxxxxxx",
}
]
]
}
}
Multi-platform setup:
plugins: [
[
"react-native-purchases",
{
"REVENUECAT_API_KEY_IOS": "appl_xxxxxxxx",
"REVENUECAT_API_KEY_ANDROID": "goog_xxxxxxxx"
}
]
]
SDK Initialisation
Basic Setup
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 });
}
With User Identification
export async function initPurchases(userId?: string) {
await Purchases.configure({ apiKey: API_KEY });
if (userId) {
await Purchases.logIn(userId);
}
}
App Initialisation
useEffect(() => {
initPurchases();
}, []);
Entitlements
Setting Up Entitlements
In RevenueCat Dashboard:
- Go to Project Settings → Entitlements
- Create entitlement (e.g., "premium", "pro")
- This represents what the user gets access to
Checking Entitlements
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;
}
}
Using a Hook
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();
const listener = Purchases.addCustomerInfoUpdateListener((info: CustomerInfo) => {
(info..[] !== );
});
listener.();
}, []);
{ isPremium, loading };
}
Offerings and Products
Fetching Offerings
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;
}
}
Displaying Products
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);
});
}
Package Types
| Type | Description |
|---|
$rc_monthly | Monthly subscription |
$rc_annual | Annual subscription |
$rc_weekly | Weekly subscription |
$rc_lifetime | Lifetime (one-time) purchase |
| Custom | Your own identifier |
Making Purchases
Purchase Flow
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) {
return false;
}
throw error;
}
}
Complete Paywall Component
import React, { useEffect, useState } from 'react';
import { View, Text, TouchableOpacity, ActivityIndicator, StyleSheet } 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);
} {
();
}
};
();
}, []);
= () => {
();
{
{ customerInfo } = .(pkg);
(customerInfo..[]) {
();
}
} (: ) {
(!error.) {
.(, error);
}
} {
();
}
};
= () => {
();
{
customerInfo = .();
(customerInfo..[]) {
();
}
} (error) {
.(, error);
} {
();
}
};
(loading) {
;
}
(
);
}
styles = .({
: { : , : },
: { : , : , : },
: { : , : , : , : },
: { : , : },
: { : , : , : },
: { : , : , : },
: { : , : },
: { : , : },
});
Restore Purchases
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 Paywalls
RevenueCat offers pre-built paywall templates:
Installation
npx expo install react-native-purchases-ui
Usage
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);
}}
/>
);
}
Presenting as Modal
import { presentPaywall, presentPaywallIfNeeded } from 'react-native-purchases-ui';
await presentPaywall();
await presentPaywallIfNeeded({ requiredEntitlementIdentifier: 'premium' });
Cross-Platform Product Setup
App Store Connect → RevenueCat
- Create product in App Store Connect
- Note the Product ID (e.g.,
com.yourapp.premium.monthly)
- In RevenueCat → Products → New
- Enter the Product ID exactly as in ASC
- RevenueCat auto-fetches price and details
Play Console → RevenueCat
- Create product in Play Console (In-app products or Subscriptions)
- Note the Product ID
- In RevenueCat → Products → New
- Enter the Product ID exactly as in Play Console
- RevenueCat auto-fetches details
Product ID Best Practices
Use consistent naming across platforms:
com.yourcompany.yourapp.premium.monthly
com.yourcompany.yourapp.premium.annual
com.yourcompany.yourapp.premium.lifetime
Offerings Configuration
Default Offering
The "Default" offering is what offerings.current returns. Always have one.
Structure Example
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
Fetching Specific Offering
const offerings = await Purchases.getOfferings();
const current = offerings.current;
const saleOffering = offerings.all['sale_50_off'];
Subscription Management
Getting Subscription Status
const customerInfo = await Purchases.getCustomerInfo();
const activeSubscriptions = customerInfo.activeSubscriptions;
const premiumEntitlement = customerInfo.entitlements.active['premium'];
if (premiumEntitlement) {
console.log('Expires:', premiumEntitlement.expirationDate);
console.log('Will renew:', premiumEntitlement.willRenew);
}
Managing Subscriptions
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');
}
}
Sandbox Testing
iOS Sandbox
- Create Sandbox Tester in App Store Connect
- Sign out of App Store on device/simulator
- Make purchase (will prompt for Sandbox login)
- Use sandbox credentials
Android Testing
- Add tester emails in Play Console (License testers)
- Publish to internal testing track
- Install via Play Store
- Purchases will be test transactions
RevenueCat Sandbox Mode
RevenueCat automatically detects sandbox purchases. In dashboard:
- Toggle "Sandbox" filter to see test purchases
- Sandbox transactions don't count toward revenue
Webhooks and Events
Server-Side Verification
Configure webhooks in RevenueCat Dashboard:
- Settings → Webhooks
- Add your endpoint URL
- Select events to receive
Event Types
| Event | Description |
|---|
INITIAL_PURCHASE | First purchase |
RENEWAL | Subscription renewed |
CANCELLATION | Subscription cancelled |
EXPIRATION | Subscription expired |
BILLING_ISSUE | Payment failed |
PRODUCT_CHANGE | Plan changed |
Troubleshooting
Common Issues
Products not loading:
- Verify product IDs match exactly
- Check App Store Connect / Play Console product status
- Products must be "Ready to Submit" or approved
- Agreements must be signed in stores
Purchases failing:
- Check API key is correct for platform
- Verify sandbox/production environment
- Check device is signed in to store account
- Review RevenueCat error logs
Entitlements not granting:
- Verify entitlement is linked to product in RC dashboard
- Check offering configuration
- Verify customer info is refreshing
Debug Logging
import Purchases, { LOG_LEVEL } from 'react-native-purchases';
if (__DEV__) {
Purchases.setLogLevel(LOG_LEVEL.VERBOSE);
}
Checking Configuration
const appUserId = await Purchases.getAppUserID();
console.log('App User ID:', appUserId);
const isConfigured = Purchases.isConfigured;
console.log('Is Configured:', isConfigured);
Pre-Flight Checklist
Before launching with RevenueCat:
RevenueCat Dashboard
App Store Connect
Play Console
App Code
Testing
Quick Reference
await Purchases.configure({ apiKey: 'your_key' });
const offerings = await Purchases.getOfferings();
const current = offerings.current;
const { customerInfo } = await Purchases.purchasePackage(package);
const isPremium = customerInfo.entitlements.active['premium'] !== undefined;
await Purchases.restorePurchases();
const info = await Purchases.getCustomerInfo();
Purchases.addCustomerInfoUpdateListener((info) => {
});
await Purchases.logIn('user_id');
await Purchases.logOut();