RevenueCat wraps StoreKit and Google Play Billing behind one SDK, keeps the subscription state on its servers, and lets you change paywalls without shipping an update. This guide takes an Expo Router app from zero to a working, review-safe subscription on both stores.
One thing up front: in-app purchases need a development build. StoreKit and Play Billing are native, so Expo Go cannot run them (more in Expo Go vs a development build).
1. Install
npx expo install react-native-purchases react-native-purchases-uiNo config plugin is needed. Then make a development build: npx expo run:ios, npx expo run:android, or eas build --profile development.
2. Set up the dashboard
- Create a RevenueCat project and add an App Store app and a Play Store app with your bundle id and package name.
- Copy the public SDK keys into .env: the App Store key (appl_…) and the Play Store key (goog_…). They are safe to ship in the app.
- Create the subscription products in App Store Connect and Play Console, then add them under Products in RevenueCat. A product that exists only in RevenueCat never shows a price.
- Create an entitlement - say pro - and attach the products to it. Your app checks entitlements, never product ids, so you can change prices and plans later without code.
- Create an offering with a monthly (and maybe annual) package and mark it current. The app reads the current offering.
3. Configure once, at startup
import { Platform } from "react-native";
import Purchases, { LOG_LEVEL } from "react-native-purchases";
const apiKey = Platform.select({
ios: process.env.EXPO_PUBLIC_REVENUECAT_IOS_KEY,
android: process.env.EXPO_PUBLIC_REVENUECAT_ANDROID_KEY,
});
if (apiKey) {
Purchases.setLogLevel(__DEV__ ? LOG_LEVEL.DEBUG : LOG_LEVEL.WARN);
Purchases.configure({ apiKey });
}Do it before any screen calls the SDK. With Expo Router, a screen's effects run before a provider's in the root layout, so a cold start deep-linked straight to /paywall can crash with "There is no singleton instance". Configuring lazily on first use - every helper calls an ensureConfigured() first - avoids it.
4. Read entitlements
const info = await Purchases.getCustomerInfo();
const isPro = "pro" in info.entitlements.active;
// Stay current: renewals, refunds and purchases on another device.
Purchases.addCustomerInfoUpdateListener((info) => {
setPro("pro" in info.entitlements.active);
});With Expo Router, gate premium screens on that boolean - for example with a redirect in the screen's layout, or Stack.Protected - and keep the splash screen up until the first check resolves, so Pro users do not see a flash of the paywall.
5. Show a paywall
The quickest is RevenueCat's native paywall, designed in the dashboard:
import RevenueCatUI, { PAYWALL_RESULT } from "react-native-purchases-ui";
const result = await RevenueCatUI.presentPaywallIfNeeded({
requiredEntitlementIdentifier: "pro",
displayCloseButton: true,
});
// PURCHASED | RESTORED | CANCELLED | ERROR | NOT_PRESENTEDNOT_PRESENTED means nothing was shown - the user is already Pro, there is no paywall attached to the offering, or the device is offline - so fall back to your own screen. ERROR means the paywall already told the user, so do not show a second message. Before release, replace the sample paywall RevenueCat pre-fills - name, images and text - or your users will see its placeholder copy.
Or sell from your own screen:
const offerings = await Purchases.getOfferings();
const packages = offerings.current?.availablePackages ?? [];
try {
const { customerInfo } = await Purchases.purchasePackage(packages[0]);
} catch (e: any) {
if (!e.userCancelled) throw e; // cancelling is not an error
}6. Restore and manage
Apple requires a Restore button. Offer a way to manage or cancel too - showManageSubscriptions opens the store's own page:
await Purchases.restorePurchases();
await Purchases.showManageSubscriptions();7. Tie purchases to your users
RevenueCat starts every install as an anonymous user. If your app has accounts, call Purchases.logIn(user.id) on sign-in and Purchases.logOut() on sign-out, so a subscription follows the account across devices and your server can look it up by the same id. Run those calls one at a time - overlapping switches can leave the SDK on the wrong user.
8. Test in the sandbox
- iOS: add a sandbox tester in App Store Connect → Users and Access → Sandbox, and sign in on the device under Settings → App Store → Sandbox Account.
- Android: add a license tester in Play Console → Setup → License testing.
- RevenueCat's Test Store key (test_…) shows its own purchase dialog with no store setup - handy in the simulator - but never ship it: in a release build it shows a "Wrong API Key" alert and closes the app.
9. Pass App Review
- Guideline 3.1.2: show the renewal terms next to the price - length, price per period, that it renews automatically and how to cancel - plus links to your Terms and Privacy Policy.
- Offer Restore, and a way to manage the subscription.
- Digital content must use in-app purchase on iOS. The purchase sheet charges the Apple ID's payment method; it is not Apple Pay.
Read next: Supabase auth in Expo, for the accounts those purchases attach to, or the RevenueCat module docs.