At RevenueCat, we aim to make it as easy as possible for developers to monetize their apps. One of the ways we do this is by providing easy-to-use native and cross-platform SDKs like React Native. And we’re always looking for ways to improve the developer experience.

Expo is a framework that makes it easy to develop, test, and release React Native apps. Expo abstracts a lot of the Android- and iOS-native build complexity so that developers only have to write JavaScript/TypeScript. There are two ways to build an app in Expo: the bare workflow or the managed workflow. The bare workflow gives you more control but requires a more complex technical setup, while the managed workflow handles most of the complexity of building an app for you.

Now, Expo users can install RevenueCat’s react-native-purchases library and supercharge the Expo managed workflow to include cross-platform in-app purchases and subscriptions via RevenueCat — without having to worry about anything native!

In this post, we’ll talk about how smoothly RevenueCat and Expo work together, as well as walk through how to create a managed workflow with the react-native-purchases library.

Getting started with RevenueCat and Expo

Creating a new managed-workflow React Native app with Expo and RevenueCat is pretty straightforward!

Step 1: Creating projects and installing dependencies

The first step is getting an app started. We’ll need to create the app and install the project dependencies.

Run the following command to scaffold a new Expo project:

npx create-expo-app@latest

Change into the project directory:

cd <your-project-name>

Then install expo-dev-client. This gives you extra debug capabilities and lets you load projects from the Expo CLI into the native builds created by Expo’s managed workflow:

npx expo install expo-dev-client

Next, install RevenueCat’s SDKs — react-native-purchases for core functionality, and react-native-purchases-ui for pre-built UI components like Paywalls and Customer Center:

npx expo install react-native-purchases react-native-purchases-ui

Note: After installing RevenueCat’s SDKs, you must run the full build process described in Step 4 before testing. Hot reloading without a fresh build will result in errors like Invariant Violation: new NativeEventEmitter() requires a non-null argument.

Step 2: Configure your RevenueCat products and offerings

After you’ve started the base project, it’s time to configure your app’s products and offerings. Here’s what you’ll need to set up in the RevenueCat dashboard:

  • Project: Top-level container for your apps, products, entitlements, and paywalls. Create one here.
  • Store connection: Connect your RevenueCat project to Apple, Google, or both. Set up supported stores here.
  • Products: The individual items you plan to sell. Add products for each store here.
  • Entitlement: Represents a level of access your customer unlocks after a purchase. Create an entitlement, then attach your products to it.
  • Offering: A collection of products shown to customers on your paywall. Create an offering here.
  • Paywall: Where customers purchase your products. RevenueCat’s Paywalls let you build and configure your paywall remotely — no code changes or app updates required.

If this is your first time setting up RevenueCat (or you need a quick refresher), the Quickstart is the best place to begin.

Step 3: Integrate react-native-purchases in the app

Now that we’ve created the app and installed the SDKs, we can wire up RevenueCat. Here’s a complete App.tsx that initializes the SDK, checks the customer’s subscription status, and presents a paywall if needed.

Replace the API key placeholders with your project’s API keys and YOUR_ENTITLEMENT_ID with the entitlement identifier you created in Step 2.

1import React, { useEffect, useState } from 'react';
2import { Platform, View, Text, ActivityIndicator, StyleSheet } from 'react-native';
3import Purchases, { LOG_LEVEL, CustomerInfo } from 'react-native-purchases';
4import RevenueCatUI, { PAYWALL_RESULT } from 'react-native-purchases-ui';
5
6const API_KEYS = {
7  apple: 'your_revenuecat_apple_api_key',
8  google: 'your_revenuecat_google_api_key',
9};
10
11const ENTITLEMENT_ID = 'YOUR_ENTITLEMENT_ID';
12
13export default function App() {
14  const [isLoading, setIsLoading] = useState(true);
15  const [isPro, setIsPro] = useState(false);
16
17  useEffect(() => {
18    const setup = async () => {
19      // Enable verbose logging during development
20      Purchases.setLogLevel(LOG_LEVEL.VERBOSE);
21
22      // Configure the SDK with the right API key for each platform
23      if (Platform.OS === 'ios') {
24        Purchases.configure({ apiKey: API_KEYS.apple });
25      } else if (Platform.OS === 'android') {
26        Purchases.configure({ apiKey: API_KEYS.google });
27      }
28
29      await checkSubscriptionStatus();
30    };
31
32    setup().catch(console.error);
33  }, []);
34
35  const checkSubscriptionStatus = async () => {
36    try {
37      const customerInfo: CustomerInfo = await Purchases.getCustomerInfo();
38      const hasAccess = typeof customerInfo.entitlements.active[ENTITLEMENT_ID] !== 'undefined';
39      setIsPro(hasAccess);
40
41      // If the user doesn't have access, show the paywall
42      if (!hasAccess) {
43        await presentPaywall();
44      }
45    } catch (e) {
46      console.error('Error fetching customer info', e);
47    } finally {
48      setIsLoading(false);
49    }
50  };
51
52  const presentPaywall = async () => {
53    // presentPaywallIfNeeded only shows the paywall if the customer
54    // doesn't already have the entitlement — safe to call at any time
55    const result: PAYWALL_RESULT = await RevenueCatUI.presentPaywallIfNeeded({
56      requiredEntitlementIdentifier: ENTITLEMENT_ID,
57    });
58
59    switch (result) {
60      case PAYWALL_RESULT.PURCHASED:
61      case PAYWALL_RESULT.RESTORED:
62        setIsPro(true);
63        break;
64      case PAYWALL_RESULT.NOT_PRESENTED:
65      case PAYWALL_RESULT.CANCELLED:
66      case PAYWALL_RESULT.ERROR:
67      default:
68        break;
69    }
70  };
71
72  if (isLoading) {
73    return (
74      <View style={styles.container}>
75        <ActivityIndicator />
76      </View>
77    );
78  }
79
80  return (
81    <View style={styles.container}>
82      <Text style={styles.status}>
83        {isPro ? '✅ Pro access unlocked' : '🔒 Free tier'}
84      </Text>
85    </View>
86  );
87}
88
89const styles = StyleSheet.create({
90  container: {
91    flex: 1,
92    alignItems: 'center',
93    justifyContent: 'center',
94  },
95  status: {
96    fontSize: 18,
97  },
98});
99

A few things worth calling out about this setup:

  • presentPaywallIfNeeded checks the entitlement for you before showing the paywall, so it’s safe to call from multiple places in your app without worrying about showing it to users who are already subscribed.
  • PAYWALL_RESULT gives you fine-grained control over what happens after the user interacts with the paywall. PURCHASED and RESTORED both mean the user now has access; everything else means they don’t yet.
  • The paywall UI itself — what it looks like, which products it shows, the copy — is all configured in the RevenueCat dashboard. No app update needed to change it.

If you need more control over how the paywall is embedded (for example, presenting it as part of a navigation stack rather than as a modal), you can use the <RevenueCatUI.Paywall> component directly. See the React Native Paywalls docs for all the options.

Step 4: Native app building

This step differs from most other SDK configurations. Usually, at this point, we’d recommend running a development build from your IDE (Xcode or Android Studio) directly on your machine. This is not the case with Expo. The benefit of Expo’s managed workflow is that you don’t need to worry about the native parts! Instead, you’ll use the command line to kick off Android and iOS development builds on Expo’s server, wait for them to complete, and then install those apps on your test devices.

We’ll use the eas tool to start these builds. EAS stands for Expo Application Services — the cloud services Expo hosts for building native binaries.

First, install the EAS CLI and log in:

npm install -g eas-cli
eas login

Then initialize and configure EAS for your project:

eas init
eas build:configure

Android

Run the following command to start the Android development build on EAS:

eas build --profile development --platform android

Before the build starts, you’ll see a few prompts. The first asks for your application ID — the ID you registered in Google Play and RevenueCat. The second is about the Android Keystore, which is used to sign your APK so Google Play knows updates are actually from you. You can have Expo generate one, or specify your own.

Once the build completes, you can install the APK by scanning a QR code, downloading it from a URL, or using Expo Orbit to push it directly to a connected Android device or emulator.

To see your configured products in the Android app, you’ll need to upload this APK to Google Play. It doesn’t need to be released to production — uploading it to an internal track is enough. Just make sure the version number of the APK matches what you have set up in Google Play.

See our help docs to learn more about sandbox testing for Google Play.

iOS

Run the following command to start the iOS development build on EAS:

1eas build --profile development --platform ios

Just like with Android, this will prompt you for a few details before the build starts — your bundle identifier, and some questions about code signing. EAS can create a provisioning profile for you if you enter your Apple ID credentials, and register your devices to it automatically.

To test on a simulator without a physical device, add a simulator build profile to your eas.json:

1{
2  "build": {
3    "development": {
4      "developmentClient": true,
5      "distribution": "internal"
6    },
7    "ios-simulator": {
8      "extends": "development",
9      "ios": {
10        "simulator": true
11      }
12    }
13  }
14}
15

Then build for the simulator:

eas build --platform ios --profile ios-simulator

Once the build finishes, Expo will ask if you want to open it in the simulator. Choose yes, and it’ll launch with your app ready to go.

Step 5: Native app testing

Now that Expo has built the native apps, you can start testing your React Native app and RevenueCat integration. Run a local development server that your native apps can connect to:

npx expo start

When you open either the Android or iOS app, you’ll be presented with a screen to connect to the local development server. Your phone may find it automatically, but you might need to enter your computer’s local IP address manually.

Once connected, it’ll load your React Native app. And if all goes well, RevenueCat will load your offerings and products!

If something goes wrong, RevenueCat has debug logs that print to the console. On iOS, use Console.app on your Mac to view them. On Android, use LogCat (or Android Studio). The logs will show which products can or can’t be loaded, along with any associated errors.

Using Expo Go during development

Expo Go is a sandbox that lets you rapidly prototype your app. While it doesn’t support running custom native code — such as the native modules required for real in-app purchases — react-native-purchases includes a built-in Preview API Mode specifically for Expo Go.

When your app runs inside Expo Go, the SDK automatically detects the environment and replaces native calls with JavaScript-level mock APIs. This means your app can load and execute all subscription-related logic without errors, even though real purchases won’t function in this mode.

You can still preview subscription UIs, test integration flows, and keep building — without needing a full development client build every time. It’s a handy way to stay in the Expo Go fast-iteration loop before you’re ready to go deeper.

To fully test in-app purchases and access real RevenueCat functionality, you’ll need to switch to a development build.

What about web?

RevenueCat’s React Native SDK also supports web platforms, so you can manage subscriptions across React Native web, mobile, and desktop apps using the same SDK — including Expo projects targeting web.

Web purchases use RevenueCat Web Billing, which runs on Stripe as the payment processor and integrates with the same entitlements system as your iOS and Android products. Products need to be configured separately for web in the RevenueCat dashboard, but entitlements can be shared across platforms — so the same entitlement can unlock access whether a customer subscribed on iOS, Android, or the web.

If you’re building a cross-platform app, it’s worth taking a look at the Web Billing overview to understand how everything fits together.


Now it’s easier than ever to monetize your app. All you have to do is write some React, tell Expo to build your apps, and let RevenueCat handle all the tricky cross-platform in-app purchase logic.

We’re excited to help developers make more money with less work. You can create your free RevenueCat account here to get started.