Back to blog

How to Add RevenueCat Paywalls to an Expo App (Without Rebuilding Billing)

If you are shipping an Expo or React Native app and want subscriptions, you do not need to rebuild StoreKit or Google Play Billing from scratch. RevenueCat wraps those store APIs, and RevenueCat Paywalls give you a remotely configurable UI—so you can install the SDKs, map products to entitlements, and present a paywall with a few lines of code.

This how-to walks indie developers and small teams through a complete Expo RevenueCat paywall setup: development builds, dashboard config, react-native-purchases + react-native-purchases-ui, presentPaywallIfNeeded, and the difference between integrated and live. It also covers the failures that trip people up most often (Expo Go, missing current offering, sandbox).

Soft note for kit users: In the CodeBaseHub Expo starter kit, RevenueCat + paywall are integrated; add API keys and enable the feature flag to go live. That is not the same as "subscriptions work with zero setup"—you still need store products, a RevenueCat project, and a real device/dev build.

Key Takeaways

  • Use a development build, not Expo Go, for real purchases; Expo Go only mocks purchase APIs (RevenueCat Expo install guide).
  • Install react-native-purchases (purchases) and react-native-purchases-ui (paywalls), then configure platform API keys once at app startup.
  • Map products → entitlements → offerings → paywall in the RevenueCat dashboard before you expect UI to show packages.
  • Prefer RevenueCatUI.presentPaywallIfNeeded when access should depend on an entitlement like "pro" (displaying paywalls).
  • Integrated ≠ live: wiring SDKs and a feature flag is step one; going live still means store products, keys, and sandbox/production testing.

Why Expo + RevenueCat (Not Raw StoreKit / Play Billing)

Subscriptions are a major share of consumer app spend. According to Business of Apps, Android and iOS app consumer spending reached about $166.8 billion in 2025, and subscription revenues reached about $79.5 billion that year. For indie teams, the hard part is rarely "should we monetize?"—it is shipping billing without becoming a full-time StoreKit/Play Billing shop.

Raw store APIs force you to maintain:

  • Separate iOS and Android purchase flows and restore logic
  • Receipt validation and entitlement state across devices
  • Paywall UI that usually requires an app store update to change

RevenueCat provides a backend and SDKs that wrap StoreKit, Google Play Billing, and (where applicable) RevenueCat Billing. For Expo specifically, Expo's own walkthrough shows the same path: install the React Native SDKs, configure store products, then deploy a paywall from RevenueCat's visual editor (Expo × RevenueCat tutorial).

Why this pairing works for Expo/RN teams:

  1. One customer modelCustomerInfo and entitlements stay the source of truth across platforms.
  2. Remote paywalls — change copy, packages, and layout in the dashboard without rebuilding billing code.
  3. Expo-compatible installnpx expo install react-native-purchases react-native-purchases-ui plus a dev client / EAS build.
  4. Less custom native work — you still need native modules, but you do not implement store billing yourself.

You still own product strategy, pricing, and compliance. RevenueCat removes the need to rebuild the billing stack—not the need to configure stores and test carefully.

What You Need First (Dev Build, Store Products, RC Project)

Before any paywall code will behave in production-like conditions, lock these prerequisites. Skipping them is the usual reason "Expo RevenueCat paywall" demos fail on first run.

1. An Expo development build

RevenueCat's Expo docs are explicit: to use and test RevenueCat with Expo, create an Expo development build (installation guide). That typically means:

  • expo-dev-client in the project
  • EAS Build (or a local native build) so native purchase modules are present

Expo Go is useful for UI prototyping. It is not where you validate real IAP.

2. Store accounts and products

You need:

  • Apple Developer / App Store Connect (iOS subscriptions)
  • Google Play Console (Android subscriptions)
  • Matching product IDs that you will also register in RevenueCat

Both stores generally expect an uploaded app binary before subscription setup is complete. Expo's EAS Submit path helps ship that binary (Expo tutorial).

3. A RevenueCat project

In the RevenueCat dashboard, configure at least:

Building blockRole
App(s)Connected to Apple / Google (and keys)
ProductsMirror store product IDs
Entitlemente.g. pro — what "paid access" means in your app
OfferingPackages shown on the paywall (mark one current)
PaywallRemotely designed UI attached to an offering

Until products, entitlements, and a current offering exist, presentPaywall may open empty or show a default fallback UI. Treat dashboard setup as part of install—not an afterthought.

4. Platform API keys

Use the Apple and Google public SDK keys from your RevenueCat project (not a single shared secret for both platforms). Configure the correct key per Platform.OS at startup.

Install & Configure (react-native-purchases + UI)

Install packages

From your Expo project root:

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

After adding native modules, rebuild the development client. Hot reload alone is not enough; RevenueCat documents errors such as new NativeEventEmitter() requires a non-null argument when native deps were not built (Expo install).

Then create or refresh your EAS development build (simulator or device). Official steps cover eas build:configure, iOS simulator profiles, and Android development profiles in the same guide.

Configure the SDK once at startup

Add configuration near your app entry (Expo Router root layout or App.tsx). Replace placeholders with your project keys:

import { useEffect } from "react";
import { Platform } from "react-native";
import Purchases, { LOG_LEVEL } from "react-native-purchases";

export function useConfigurePurchases() {
  useEffect(() => {
    Purchases.setLogLevel(LOG_LEVEL.VERBOSE);

    if (Platform.OS === "ios") {
      Purchases.configure({
        apiKey: process.env.EXPO_PUBLIC_RC_APPLE_API_KEY!,
      });
    } else if (Platform.OS === "android") {
      Purchases.configure({
        apiKey: process.env.EXPO_PUBLIC_RC_GOOGLE_API_KEY!,
      });
    }
  }, []);
}

Practical tips:

  • Keep keys in env / secrets; never commit production secrets to public repos.
  • Use verbose logs while integrating; then dial logging back for production.
  • Identify users with a stable appUserID when you have auth, so entitlements sync across devices (Expo install → identify / CustomerInfo).

Check entitlement status

import Purchases from "react-native-purchases";

export async function hasProAccess(): Promise<boolean> {
  try {
    const customerInfo = await Purchases.getCustomerInfo();
    return typeof customerInfo.entitlements.active["pro"] !== "undefined";
  } catch {
    return false;
  }
}

Gate premium screens on the entitlement identifier you created in the dashboard—not on a local boolean you set after a single purchase event.

Entitlements, Offerings, and presentPaywallIfNeeded

Dashboard concepts map cleanly to code:

  1. User buys a product
  2. RevenueCat grants an entitlement (e.g. pro)
  3. The offering (usually current) defines which packages the paywall shows
  4. The paywall is the UI attached to that offering

Present only when access is missing

For React Native, RevenueCat documents three patterns (displaying paywalls):

  • RevenueCatUI.presentPaywall — always attempt to show
  • RevenueCatUI.presentPaywallIfNeeded — show only if the entitlement is not active
  • <RevenueCatUI.Paywall /> — embed manually for custom navigation

Most subscription gates use presentPaywallIfNeeded:

import RevenueCatUI, { PAYWALL_RESULT } from "react-native-purchases-ui";

export async function unlockProIfNeeded(): Promise<boolean> {
  const result: PAYWALL_RESULT = await RevenueCatUI.presentPaywallIfNeeded({
    requiredEntitlementIdentifier: "pro",
  });

  switch (result) {
    case PAYWALL_RESULT.PURCHASED:
    case PAYWALL_RESULT.RESTORED:
      return true;
    case PAYWALL_RESULT.NOT_PRESENTED:
      // Entitlement already active — treat as unlocked
      return true;
    case PAYWALL_RESULT.CANCELLED:
    case PAYWALL_RESULT.ERROR:
    default:
      return false;
  }
}

Call this from a premium feature entry point (or after login), not necessarily on every cold start—hard-gating the whole app can hurt conversion and review experience. Soft-gate individual features when you can.

Optional: button-triggered paywall

import { Button } from "react-native";
import RevenueCatUI from "react-native-purchases-ui";

export function UpgradeButton() {
  return (
    <Button
      title="Go Pro"
      onPress={() =>
        RevenueCatUI.presentPaywallIfNeeded({
          requiredEntitlementIdentifier: "pro",
        })
      }
    />
  );
}

Remotely iterate the paywall

After the SDK is live, you can revise layout and copy in RevenueCat's paywall editor and publish. Clients fetch the updated paywall without rewriting purchase plumbing (Expo tutorial). That is the "without rebuilding billing" benefit in practice: you still ship app updates for product logic, but monetization UI experiments move faster.

For a product-oriented overview of how kits structure monetization modules, see Expo starter kit — RevenueCat monetization.

Integrated vs Live — Starter Kit Framing (Key + Flag)

Shipping teams often confuse code integration with go-live. Keep the distinction sharp.

Integrated

  • SDKs installed and configured behind a feature flag
  • Paywall presentation helpers wired to an entitlement ID
  • Env placeholders for Apple/Google API keys
  • UI paths that call presentPaywallIfNeeded when the flag is on

Live

  • Real store products created and approved for testing
  • Products attached to entitlements; offering marked current
  • Paywall published in RevenueCat
  • Correct API keys in the build that testers install
  • Feature flag enabled for the cohort you want
  • Sandbox (and later production) purchase + restore verified on a dev/production build

Safe product claim (CodeBaseHub): RevenueCat + paywall are integrated in the Expo kit; add API keys and enable the feature flag to go live.

That claim deliberately does not mean "subscriptions work out of the box with zero setup." Store configuration, RevenueCat dashboard work, and device testing remain on you. It also does not claim deep links or every edge-case restore flow are finished for every app—those depend on your navigation and identity design.

If you are evaluating a starter, look for:

  • Clear env var names for RC keys
  • A single feature flag that disables purchase UI until you are ready
  • Documented entitlement ID conventions (pro, etc.)
  • Explicit "dev build required" notes in the README

Explore the kit at codebasehub.pro when you want that wiring already in the repo instead of starting from a blank Expo app. Related reading: CodeBaseHub Expo starter kit.

Common Failures (Expo Go, No Current Offering, Sandbox)

"It works in Expo Go" — until it doesn't

Expo Go does not include the full native IAP stack. RevenueCat's SDK can run a Preview API Mode in Expo Go so subscription logic and UI load without crashing, but real purchases will not function there. Full testing requires a development build (RevenueCat Expo docs; Expo blog).

Fix: Build with EAS (development / simulator profiles), install the binary, then npx expo start against that client.

Paywall opens with no packages / "no current offering"

Typical causes:

  • No offering marked current in RevenueCat
  • Products not attached to the offering or entitlement
  • Wrong API key / wrong app in the project
  • Store products not yet available to the sandbox account

Fix: In the dashboard, confirm current offering → packages → product IDs match the stores. Enable verbose SDK logs and inspect offering fetch errors.

Sandbox purchase quirks

  • Use a Sandbox Apple ID / license testers on Google
  • Wait for store propagation after creating products
  • Restore purchases when reinstalling or switching devices
  • Remember that entitlement checks should use getCustomerInfo(), not a one-off local flag

Native module / rebuild errors

If you see NativeEventEmitter or missing native module errors after expo install, rebuild the development client. Partial Metro reloads do not link new native code (RevenueCat).

Hard paywall on first launch

Technically easy with presentPaywallIfNeeded in a root useEffect; product-wise often harsh. Prefer gating premium features and keeping a clear restore path. Review guidelines and user trust both matter here.

FAQ

Do I need separate RevenueCat API keys for iOS and Android?

Yes. Configure the Apple key on iOS and the Google key on Android when calling Purchases.configure (Expo install guide).

Can I change the paywall without a new App Store release?

You can update many paywall presentation details remotely via RevenueCat's paywall tools after the SDK and offering are in place (displaying paywalls; Expo tutorial). App binary updates are still required when you change native dependencies or local gating logic.

Is Expo Go enough to demo subscriptions to investors?

You can demo UI and flows in Preview API Mode, but you should not treat Expo Go as proof that billing works. Use a development build for demos that involve real sandbox purchases.

Where do entitlements live—on device or in RevenueCat?

Treat RevenueCat CustomerInfo / entitlements as the source of truth, then mirror access in your UI. Re-fetch after purchase, restore, and app foreground as needed.

Does a starter kit remove App Store / Play Console work?

No. A kit can integrate SDKs and a feature flag; you still create store products, attach them in RevenueCat, add keys, and test. See React Native starter kit.

Conclusion + CTA

Adding a RevenueCat Expo paywall is mostly a disciplined checklist: development build, store products, RevenueCat entitlements and offerings, SDK configure, then presentPaywallIfNeeded for gated features. You avoid rebuilding StoreKit and Play Billing—but you do not avoid configuration and sandbox testing.

Subscriptions remain a meaningful part of the app economy (~$79.5B subscription revenue and ~$166.8B consumer spend in 2025). The teams that ship faster are usually the ones who separate integration from go-live and refuse to claim zero-setup billing.

If you want that integration already structured in an Expo/RN codebase, RevenueCat + paywall are integrated in the CodeBaseHub Expo kit; add API keys and enable the feature flag to go live. Start from the docs above, wire your keys, flip the flag when sandbox purchases succeed, and iterate the paywall remotely instead of rewriting billing.

Primary references

Related articles