Short answers
- Config plugin? None.
react-native-purchasesdoes not go in app.json plugins. - Expo Go? Paywall screens run with mock data. Real purchases need a development build.
- Cost? Free up to $2,500 monthly tracked revenue, then 1%.
Before you start
- An Apple Developer Program membership ($99 a year), with the Paid Applications Agreementsigned and banking and tax details complete in App Store Connect. Sandbox purchases don’t work until the agreement is signed.
- A Google Play developer account ($25 once) with a payments profile set up.
- An Expo project you build with EAS, and a physical iPhone and Android device for testing.
- A free RevenueCat account.
RevenueCat saves you from writing receipt validation, subscription state, renewals, trials, and refunds yourself, and from keeping two store APIs in sync. Its fee applies only once you pass $2,500 a month in tracked revenue.
Step 1: Install the SDK (no config plugin)
RevenueCat’s Expo guide uses a development build. Install the dev client and both RevenueCat packages:
npx expo install expo-dev-client npx expo install react-native-purchases react-native-purchases-ui # then rebuild the development build eas build --profile development --platform ios eas build --profile development --platform android
Do not add "react-native-purchases" to the plugins array in app.json. Older tutorials say to, but the package ships no config plugin, and Expo stops with:
Package "react-native-purchases" does not contain a valid config plugin.
The other common install error is Invariant Violation: new NativeEventEmitter() requires a non-null argument. It means the app is running a build made before the native module was installed. Make a new development build and install it on the device.
What works in Expo Go
From SDK version 8.11.0, react-native-purchases detects Expo Go and switches to Preview API Mode. Native store calls are replaced with mock responses, so your paywall screens, loading states, and navigation can be built and clicked through in Expo Go.
Nothing is actually bought in that mode, and the prices and products are not your real ones. Use Expo Go to design the purchase flow, and a development build to test it. See Expo Go vs a development build for the difference.
Step 2: Create products in App Store Connect and Play Console
- App Store Connect: in your app, create a subscription group, then add products such as monthly and annual. Fill in price, duration, and localization until each product reaches Ready to Submit. Products missing metadata don’t load in sandbox.
- Google Play Console:first upload a build that includes the billing permission to a testing track. Google’s billing setup guide puts this step before creating products. Then create subscriptions under Monetize with Play > Products.
- Use matching product IDs across platforms where you can, for example
pro_monthlyandpro_annual. It keeps the RevenueCat side readable.
Step 3: Set up RevenueCat
- Create a project and add an iOS app and an Android app with their exact bundle ID and package name.
- Connect each store’s credentials as the RevenueCat setup screens ask, so it can validate purchases.
- Import or add your products.
- Create an entitlement, for example
pro, and attach every product that should unlock it. - Create an offering (the
defaultone is marked current) with a package for each product.
Your app checks the entitlement, not product IDs. That’s what lets you change prices, add a yearly plan, or run a different offering later without shipping new code.
Step 4: Configure the SDK and check entitlements
Configure once, before anything else calls RevenueCat. With Expo Router, the top of the root layout works. The keys are RevenueCat’s public SDK keys, which are meant to ship in the app.
// app/_layout.tsx
import { Platform } from 'react-native';
import { Stack } from 'expo-router';
import Purchases, { LOG_LEVEL } from 'react-native-purchases';
if (__DEV__) Purchases.setLogLevel(LOG_LEVEL.VERBOSE);
Purchases.configure({
apiKey:
Platform.OS === 'ios'
? process.env.EXPO_PUBLIC_REVENUECAT_IOS_KEY!
: process.env.EXPO_PUBLIC_REVENUECAT_ANDROID_KEY!,
});
export default function RootLayout() {
return <Stack />;
}Then read the entitlement, and keep it current with a listener so the app updates when a purchase, renewal, or restore happens:
// hooks/useIsPro.ts
import { useEffect, useState } from 'react';
import Purchases, { CustomerInfo } from 'react-native-purchases';
export function useIsPro() {
const [isPro, setIsPro] = useState(false);
useEffect(() => {
const update = (info: CustomerInfo) =>
setIsPro(info.entitlements.active['pro'] !== undefined);
Purchases.getCustomerInfo().then(update).catch(() => {});
Purchases.addCustomerInfoUpdateListener(update);
return () => {
Purchases.removeCustomerInfoUpdateListener(update);
};
}, []);
return isPro;
}Step 5: Show a paywall and handle purchases
The fastest path is RevenueCat’s paywall UI, which you design in the dashboard. This shows it only if the user doesn’t already have the entitlement:
import RevenueCatUI, { PAYWALL_RESULT } from 'react-native-purchases-ui';
async function openPaywall() {
const result = await RevenueCatUI.presentPaywallIfNeeded({
requiredEntitlementIdentifier: 'pro',
});
return result === PAYWALL_RESULT.PURCHASED || result === PAYWALL_RESULT.RESTORED;
}If you build your own paywall screen, study a few that convert first: the paywall screens from top-grossing iOS apps show the plan layouts, trial copy, and restore placement that revenue-verified apps use. Then fetch the current offering and buy a package. Treat a cancelled purchase as a normal outcome, not an error:
import { Alert } from 'react-native';
import Purchases from 'react-native-purchases';
async function buyFirstPackage() {
const offerings = await Purchases.getOfferings();
const pkg = offerings.current?.availablePackages[0];
if (!pkg) return; // empty offering: see troubleshooting below
try {
const { customerInfo } = await Purchases.purchasePackage(pkg);
return customerInfo.entitlements.active['pro'] !== undefined;
} catch (e: any) {
if (!e.userCancelled) Alert.alert('Purchase failed', e.message);
return false;
}
}
async function restore() {
const customerInfo = await Purchases.restorePurchases();
return customerInfo.entitlements.active['pro'] !== undefined;
}Put a visible Restore Purchases button on the paywall or in settings that calls restore(). Apple’s Guideline 3.1.1 expects a restore mechanism for restorable purchases.
Step 6: Test in sandbox
- iOS: create a sandbox tester in App Store Connect. On a physical iPhone with Developer Mode on, sign in under Settings > Developer > Sandbox Apple Account, then run your development build. Test on a device; simulators are a common cause of products failing to load.
- Renewals: by default a sandbox monthly subscription renews every 5 minutes, up to 12 times, so you can watch renewals and expiry in about an hour. The rate can be changed in the sandbox account settings.
- Android: add tester Gmail accounts under Settings > License testing in Play Console, put the app on a closed testing track, and install it through that track as a tester.
Troubleshooting: empty offerings
If offerings.current is null or has no packages, the logs usually show:
There's a problem with your configuration. None of the products registered in the RevenueCat dashboard could be fetched from the App Store.
Work down this list, most common first:
- Paid Applications Agreement not signed, or banking and tax details incomplete (iOS).
- Products not in Ready to Submit or Approved state (iOS).
- App not published to a closed testing track, or no license tester set up (Android).
- Bundle ID, package name, or product IDs differ by a character between the store and RevenueCat.
- Testing on a simulator, or a StoreKit Configuration file still active in the build.
- Products created less than 24 hours ago. New products can take that long to appear.
Web purchases
If you also sell on a website, RevenueCat Billing handles web subscriptions with Stripe as the payment gateway, and RevenueCat can also connect existing Stripe Billing or Paddle setups. Web and app purchases then unlock the same entitlement.
If you built the app with ShipNative
ShipNative can generate the paywall screen itself: plan cards, a subscribe button, and a restore link. The purchase logic in this guide is added after you export the Expo project, since it needs a development build and store setup the in-browser preview can’t provide. The EAS submission checklist covers the rest of the path to review.