ExpoIn-App PurchasesSubscriptionsRevenueCatMonetization

In-App Purchases and Subscriptions in Expo with RevenueCat (2026 Guide)

Add in-app purchases and subscriptions to an Expo React Native app with RevenueCat. Install react-native-purchases, configure keys, fetch offerings, buy packages, check entitlements, show native paywalls, and test with development builds.

DeveloperMill3 min read

Subscriptions are the main business model for mobile apps, and implementing them directly with StoreKit and Google Play Billing is notoriously painful: receipts, renewals, grace periods, refunds, and cross-platform status. RevenueCat wraps all of it in one SDK and a dashboard, and it works smoothly with Expo.

How it fits together

  1. App Store Connect and Google Play Console: create your products (for example pro_monthly and pro_yearly).
  2. RevenueCat dashboard: group products into an entitlement (pro) and an offering (what the paywall shows).
  3. Your app: fetch the offering, purchase a package, and check if pro is active.

Your code only cares about entitlements, so you can change prices, run trials, and A/B test paywalls without shipping an update.

Install

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

react-native-purchases is the core SDK. react-native-purchases-ui adds prebuilt native paywalls.

In Expo Go, the SDK automatically switches to Preview API Mode with mock responses, so you can build UI. Real purchases need a development build:

eas build --profile development --platform ios

Configure on app start

Use the platform-specific public API keys from the RevenueCat dashboard:

// app/_layout.tsx
import { useEffect } from "react"
import { Platform } from "react-native"
import Purchases from "react-native-purchases"
import { Stack } from "expo-router"

export default function RootLayout() {
  useEffect(() => {
    Purchases.configure({
      apiKey: Platform.OS === "ios"
        ? process.env.EXPO_PUBLIC_RC_IOS_KEY!
        : process.env.EXPO_PUBLIC_RC_ANDROID_KEY!,
    })
  }, [])

  return <Stack />
}

These are public SDK keys, designed to ship in the app, so EXPO_PUBLIC_ is fine here (unlike LLM provider keys). If you have accounts, call Purchases.logIn(userId) after sign-in so purchases follow the user across devices.

Check entitlements

import Purchases from "react-native-purchases"

export async function isPro() {
  const customerInfo = await Purchases.getCustomerInfo()
  return typeof customerInfo.entitlements.active["pro"] !== "undefined"
}

Wrap it in a hook and gate features with it. Keep the result in your client store (Zustand) and refresh it with Purchases.addCustomerInfoUpdateListener.

Option A: show a native paywall

The fastest route is a RevenueCat paywall designed in the dashboard:

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

export async function showPaywall() {
  const result = await RevenueCatUI.presentPaywallIfNeeded({
    requiredEntitlementIdentifier: "pro",
  })
  return result === PAYWALL_RESULT.PURCHASED || result === PAYWALL_RESULT.RESTORED
}

presentPaywallIfNeeded only shows the paywall if the user doesn't already have pro.

Option B: build your own paywall

For a fully branded paywall, fetch the current offering and render packages yourself:

import { useEffect, useState } from "react"
import { Pressable, Text, View } from "react-native"
import Purchases, { type PurchasesPackage } from "react-native-purchases"

export function Paywall({ onUnlocked }: { onUnlocked: () => void }) {
  const [packages, setPackages] = useState<PurchasesPackage[]>([])

  useEffect(() => {
    Purchases.getOfferings().then((o) => setPackages(o.current?.availablePackages ?? []))
  }, [])

  async function buy(pkg: PurchasesPackage) {
    try {
      const { customerInfo } = await Purchases.purchasePackage(pkg)
      if (customerInfo.entitlements.active["pro"]) onUnlocked()
    } catch (e: any) {
      if (!e.userCancelled) console.warn(e)
    }
  }

  return (
    <View className="gap-3 p-6">
      {packages.map((pkg) => (
        <Pressable key={pkg.identifier} onPress={() => buy(pkg)} className="rounded-2xl border p-4">
          <Text className="font-semibold">{pkg.product.title}</Text>
          <Text>{pkg.product.priceString}</Text>
        </Pressable>
      ))}
      <Pressable onPress={() => Purchases.restorePurchases()}>
        <Text className="text-center underline">Restore purchases</Text>
      </Pressable>
    </View>
  )
}

Always include Restore purchases. App Review requires it.

Testing purchases

  • iOS: use a sandbox tester account in a development build, or StoreKit configuration files for local testing.
  • Android: upload a build to an internal testing track, add license testers, and install from the Play Store link.
  • Sandbox subscriptions renew quickly (minutes, not months), so you can test renewals and expirations fast.

Paywall best practices

  • Show value before the price. List three concrete benefits.
  • Highlight the yearly plan with the per-month equivalent and savings.
  • Offer a free trial where it fits your product.
  • Don't block onboarding with a hard paywall unless you've tested it.
  • Present the paywall in a bottom sheet or full-screen modal, and celebrate a purchase with confetti and a success haptic.

Ship it

Products need to be approved in App Store Connect along with your first build that uses them. Follow our Expo App Store and Google Play publishing guide for the release, and use EAS Update to iterate on paywall UI afterwards.

Frequently asked questions

How do I add in-app purchases to an Expo app?

The most common approach is RevenueCat. Install `react-native-purchases` (and `react-native-purchases-ui` for paywalls) with `npx expo install`, configure it with your platform API keys, fetch offerings, call purchasePackage, and check entitlements on CustomerInfo to unlock features.

Do in-app purchases work in Expo Go?

Not for real purchases. react-native-purchases detects Expo Go and switches to a Preview API Mode with mock responses, so your UI runs, but actual App Store and Google Play purchases require a development build.

What is an entitlement in RevenueCat?

An entitlement is the access level a purchase unlocks, like 'pro'. You attach products (monthly, yearly, lifetime) to an entitlement in the RevenueCat dashboard, and your app only checks whether the entitlement is active — so you can change prices and products without changing code.

Can I sell digital features with Stripe instead of in-app purchases?

For digital content and features used inside an iOS or Android app, store rules generally require in-app purchase, with some regional and category exceptions that keep changing. Physical goods and real-world services can use Stripe or other payment processors. Check the current App Store and Google Play guidelines for your app.

Build it faster with a template

Production-ready Expo React Native templates with the patterns from this post already wired up.

Browse templates