ShipNative
PricingBlogFAQContact
Build my app
ShipNative
PricingBlogFAQContact
Build my app
TechnicalUpdated September 2026 · 13 min read · By Saurabh Singh

Expo In-App Purchases with RevenueCat: Setup from Zero

RevenueCat sits between your Expo app and the App Store and Google Play billing systems. It validates receipts, tracks who is subscribed, and gives you one entitlement check that works on both platforms. This guide sets it up from a clean Expo project to a tested paywall, including the three places most setups go wrong: the config plugin that doesn’t exist, what Expo Go can and can’t do, and offerings that come back empty.

Short answers

  • Config plugin? None. react-native-purchases does 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_monthly and pro_annual. It keeps the RevenueCat side readable.

Step 3: Set up RevenueCat

  1. Create a project and add an iOS app and an Android app with their exact bundle ID and package name.
  2. Connect each store’s credentials as the RevenueCat setup screens ask, so it can validate purchases.
  3. Import or add your products.
  4. Create an entitlement, for example pro, and attach every product that should unlock it.
  5. Create an offering (the default one 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:

  1. Paid Applications Agreement not signed, or banking and tax details incomplete (iOS).
  2. Products not in Ready to Submit or Approved state (iOS).
  3. App not published to a closed testing track, or no license tester set up (Android).
  4. Bundle ID, package name, or product IDs differ by a character between the store and RevenueCat.
  5. Testing on a simulator, or a StoreKit Configuration file still active in the build.
  6. 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.

Frequently Asked Questions

Does react-native-purchases need an Expo config plugin?

No. react-native-purchases does not ship a config plugin, and RevenueCat’s Expo installation guide does not ask you to add anything to app.json. If you add "react-native-purchases" to the plugins array, Expo fails with an error saying the package does not contain a valid config plugin. Install the package with expo-dev-client and rebuild your development build instead.

Does RevenueCat work in Expo Go?

Partly. Since version 8.11.0 the SDK detects Expo Go and runs in Preview API Mode, which replaces native calls with mock responses. That lets you build and click through paywall screens, but real purchases do not happen. To test actual purchases you need a development build or a TestFlight or Play testing build.

How much does RevenueCat cost?

RevenueCat is free until your app passes $2,500 in monthly tracked revenue. Above that it charges 1% of tracked revenue. Apple and Google still take their own commission on every purchase; RevenueCat’s fee is on top of that.

Why are my RevenueCat offerings empty?

The usual causes are an unsigned Paid Applications Agreement or incomplete banking details in App Store Connect, products that are not yet “Ready to Submit” or approved, an Android app that is not on a closed testing track, product IDs or bundle IDs that do not match exactly, testing on a simulator, or products created less than 24 hours ago.

Is a Restore Purchases button required?

Apple’s App Review Guideline 3.1.1 says apps should have a restore mechanism for restorable in-app purchases, such as subscriptions and non-consumables. A visible Restore Purchases option that calls Purchases.restorePurchases() covers it.

→

Expo Go vs Development Build

Why real purchases need a development build, and how to make one.

Read guide →
→

Expo EAS Submission Checklist

Everything else to verify before App Store review.

See checklist →

Ship a real React Native app today

Describe, preview, and export Expo code, free to start.

Build with ShipNative
ShipNative

Build mobile apps with AI. Describe it, try it on your phone, own the code.

Features

Text to App AIApp Generator from ScreenshotPRD to Mobile App

Tools

All free toolsApp Cost CalculatorApp Name GeneratorApp Store Keyword ToolReact Native Components

Blog

All blog postsHow to Build an App Without CodingBest AI Tools for Real Mobile AppsExpo EAS App Store ChecklistLovable, Cursor & v0 for MobileBest AI App Builders in 2026React Native AI App Builder Guide
© 2026 ShipNative
PrivacyTermsAboutFAQPricingDiscordShipNative - Featured on Startup FameFeatured on Twelve ToolsFeatured on TinyShelfShipNative - Launched on Fazier