RevenueCat Setup Guide for React Native Apps
Step-by-step guide to integrating RevenueCat in React Native with Expo. Covers configuration, paywalls, subscription management, and common pitfalls.
## Why RevenueCat
Handling in-app purchases and subscriptions directly through StoreKit (iOS) and Google Play Billing (Android) is a nightmare. Each platform has different APIs, receipt formats, and edge cases. You need a server to validate receipts. You need to handle subscription renewals, cancellations, grace periods, and billing retries.
RevenueCat wraps all of this in a single SDK. One API for both platforms. Server-side receipt validation. Webhooks for subscription events. A dashboard for analytics. The free tier covers up to $2,500 in monthly tracked revenue, which is more than enough for most indie apps.
We use RevenueCat in every monetized app we build. Here's the complete setup.
## Step 1: Create Your RevenueCat Project
1. Sign up at [revenucat.com](https://www.revenuecat.com) 2. Create a new project 3. Add your iOS app (you'll need your App Store Connect app bundle ID) 4. Add your Android app (you'll need your Google Play package name)
RevenueCat gives you two API keys: one for iOS, one for Android. You'll use these in your app.
## Step 2: Configure App Store Connect
### Create a Shared Secret
In App Store Connect, go to your app > App Information > App-Specific Shared Secret. Generate one and paste it into RevenueCat's iOS app settings.
### Create Subscription Products
In App Store Connect > Subscriptions:
1. Create a Subscription Group (e.g., "Premium") 2. Add your subscription products: - Monthly ($4.99/month, product ID: `premium_monthly`) - Yearly ($39.99/year, product ID: `premium_yearly`) 3. Add localized descriptions and pricing
### Create an App Store Server Notification URL
In App Store Connect > App Information > App Store Server Notifications, set the URL to RevenueCat's endpoint (found in your RevenueCat dashboard under iOS app settings).
## Step 3: Configure Google Play Console
### Upload a Signed APK
Google Play requires at least one signed APK/AAB before you can create in-app products. Upload a build through EAS or manually.
### Create Subscription Products
In Google Play Console > Monetize > Products > Subscriptions:
1. Create a base plan for each product 2. Use the same product IDs as iOS for simplicity 3. Set pricing for each region
### Service Account for Server Notifications
Create a Google Cloud service account, grant it access to your Play Console, and add the credentials JSON to RevenueCat.
## Step 4: Set Up RevenueCat Products
In RevenueCat dashboard:
### Entitlements
Create an entitlement called "premium" (or whatever access level your app grants). This is what your app checks to determine if the user has paid.
### Offerings
Create an offering called "default" with two packages: - Monthly: linked to your monthly product on both iOS and Android - Annual: linked to your yearly product on both iOS and Android
Offerings let you change pricing, products, and paywall configuration without an app update.
## Step 5: Install the SDK
```bash npx expo install react-native-purchases ```
If you're using Expo with a managed workflow, you'll need a development build (not Expo Go) since RevenueCat requires native modules.
Add the config plugin to `app.config.js`:
```javascript plugins: [ // ... other plugins 'react-native-purchases', ], ```
## Step 6: Initialize RevenueCat
```typescript // lib/revenuecat.ts import Purchases from 'react-native-purchases'; import { Platform } from 'react-native';
const API_KEY = Platform.select({ ios: process.env.EXPO_PUBLIC_REVENUECAT_API_KEY_IOS, android: process.env.EXPO_PUBLIC_REVENUECAT_API_KEY_ANDROID, });
export async function initRevenueCat(userId?: string) { if (!API_KEY) { console.warn('RevenueCat API key not set'); return; }
await Purchases.configure({ apiKey: API_KEY, appUserID: userId ?? null, }); } ```
**Critical note**: If the API key is empty or undefined, calling `Purchases.configure` can crash the app. Always guard against missing keys. This is especially important for development environments where the key might not be set.
Call `initRevenueCat` early in your app lifecycle, after authentication:
```typescript // In your auth provider or root layout useEffect(() => { if (user?.id) { initRevenueCat(user.id); } }, [user?.id]); ```
Passing the user ID links RevenueCat's subscription state to your user. This enables cross-device subscription restoration.
## Step 7: Build the Paywall
```typescript // components/Paywall.tsx import { useState, useEffect } from 'react'; import Purchases, { PurchasesPackage } from 'react-native-purchases';
export function Paywall({ onClose }: { onClose: () => void }) { const [packages, setPackages] = useState<PurchasesPackage[]>([]); const [purchasing, setPurchasing] = useState(false);
useEffect(() => { async function loadOfferings() { try { const offerings = await Purchases.getOfferings(); if (offerings.current?.availablePackages) { setPackages(offerings.current.availablePackages); } } catch (e) { console.error('Failed to load offerings', e); } } loadOfferings(); }, []);
const handlePurchase = async (pkg: PurchasesPackage) => { setPurchasing(true); try { const { customerInfo } = await Purchases.purchasePackage(pkg);
if (customerInfo.entitlements.active['premium']) { onClose(); } } catch (e: any) { if (!e.userCancelled) { Alert.alert('Purchase failed', e.message); } } finally { setPurchasing(false); } };
return ( <View style={styles.container}> <Text style={styles.title}>Go Premium</Text> <Text style={styles.subtitle}> Get access to all features </Text>
{packages.map((pkg) => ( <Pressable key={pkg.identifier} onPress={() => handlePurchase(pkg)} disabled={purchasing} style={styles.packageButton} > <Text>{pkg.product.title}</Text> <Text>{pkg.product.priceString}</Text> </Pressable> ))}
<Pressable onPress={onClose}> <Text>Maybe later</Text> </Pressable> </View> ); } ```
## Step 8: Check Subscription Status
```typescript // hooks/useSubscription.ts import { useState, useEffect } from 'react'; import Purchases, { CustomerInfo } from 'react-native-purchases';
export function useSubscription() { const [isPremium, setIsPremium] = useState(false); const [loading, setLoading] = useState(true);
useEffect(() => { async function checkStatus() { try { const customerInfo = await Purchases.getCustomerInfo(); setIsPremium(!!customerInfo.entitlements.active['premium']); } catch { setIsPremium(false); } finally { setLoading(false); } }
checkStatus();
// Listen for changes const listener = (info: CustomerInfo) => { setIsPremium(!!info.entitlements.active['premium']); }; Purchases.addCustomerInfoUpdateListener(listener);
return () => Purchases.removeCustomerInfoUpdateListener(listener); }, []);
return { isPremium, loading }; } ```
Use this hook anywhere you need to gate features:
```typescript const { isPremium } = useSubscription();
if (!isPremium) { return <Paywall onClose={() => {}} />; } ```
## Common Pitfalls
### 1. Not Testing with Sandbox Accounts
Create test accounts in App Store Connect (Users and Access > Sandbox) and Google Play Console. Never test purchases with your real account.
### 2. Forgetting Restore Purchases
Apple requires a "Restore Purchases" button. Users who reinstall or switch devices need to recover their subscription. Add this to your settings screen:
```typescript const handleRestore = async () => { const info = await Purchases.restorePurchases(); if (info.entitlements.active['premium']) { Alert.alert('Restored', 'Your premium access has been restored.'); } }; ```
### 3. Empty API Key Crash
As mentioned above, passing an empty string to `Purchases.configure` crashes the app. Always check the key exists before configuring.
### 4. Not Handling Subscription Expiry
A subscription can expire, be cancelled, or enter a grace period. Don't cache the premium status forever. Re-check on each app launch and listen for updates.
### 5. Pricing Display
Never hardcode prices. Use `pkg.product.priceString` which shows the localized price in the user's currency. Prices vary by region.
## Webhooks and Server-Side
For apps that need server-side entitlement checks (gating API access, for example), RevenueCat sends webhooks on subscription events:
- `INITIAL_PURCHASE` - `RENEWAL` - `CANCELLATION` - `EXPIRATION` - `BILLING_ISSUE`
Set up a webhook endpoint in your backend (a Supabase Edge Function works well) to handle these events and update your database.
## Analytics
RevenueCat's dashboard shows: - Monthly Recurring Revenue (MRR) - Active subscriptions and trials - Churn rate and renewal rate - Revenue by product and by country
For deeper analysis, integrate with your [analytics platform](/tech/posthog) by sending RevenueCat events to PostHog or your preferred tool.
RevenueCat is one of those tools that saves you weeks of development time. The setup takes about an hour, and you get a production-grade subscription system that handles all the edge cases you'd otherwise discover in production.
For more on app monetization strategies beyond subscriptions, check out our [monetization guide](/guides/app-monetization). And to see how RevenueCat fits into a complete app stack, visit our [pricing page](/pricing) for details on what our build pipeline includes.