---
title: "Build a single Expo app with subscriptions on iOS, Android, and Web using RevenueCat"
description: "Use one React Native codebase and RevenueCat’s Web Billing SDK to support subscriptions across platforms in 30 mins"
language: "en"
publishedAt: "2025-05-17T02:36:25Z"
updatedAt: "2025-05-17T02:36:25Z"
authors:
  - name: "Perttu Lähteenlahti"
    url: "https://www.revenuecat.com/blog/author/perttu-lahteenlahti"
category: "Engineering"
categoryUrl: "https://www.revenuecat.com/blog/engineering"
readingTime: 7
canonical: "https://www.revenuecat.com/blog/engineering/build-a-single-expo-app-with-subscriptions-on-ios-android-and-web-using-revenuecat"
---

# Build a single Expo app with subscriptions on iOS, Android, and Web using RevenueCat

Use one React Native codebase and RevenueCat’s Web Billing SDK to support subscriptions across platforms in 30 mins

## Table of contents

- [Step 1: Configure your subscription products](#step-1-configure-your-subscription-products)
  - [Configure iOS products in App Store Connect](#configure-ios-products-in-app-store-connect)
  - [Configure Android products in Google Play Console](#configure-android-products-in-google-play-console)
  - [Configure Web Billing products in RevenueCat](#configure-web-billing-products-in-revenuecat)
- [Step 2: Add in-app purchase to your React Native app](#step-2-add-in-app-purchase-to-your-react-native-app)
  - [Configure react-native-purchases](#configure-react-native-purchases)
  - [Conclusion](#conclusion)

Learn how to build and monetize a cross-platform app using Expo and RevenueCat. This guide walks you through setting up in-app subscriptions on iOS, Android, and the web—all from a single codebase. You’ll use Expo Router, Stripe for web payments, and RevenueCat’s SDKs to manage purchases and sync subscription status across platforms. Whether you’re launching your first product or scaling a subscription-based app, this post will help you streamline development and revenue tracking.

With the launch of RevenueCat Web Billing and the web SDK , it’s now easier than ever to offer subscriptions across iOS, Android, and Web — all from a single codebase. No more platform silos: users can subscribe on one platform and unlock premium access everywhere.

In this tutorial we are going to make use of RevenueCat Web billing to build a simple React Native app with Expo for iOS, Android and Web, with subscriptions seamlessly working across all three platforms. We’ll minimize platform-specific code while walking through the key setup steps. If you have not already set up a RevenueCat account, we will start with that and configure the products for app three platforms first.

[The full code is available here.](https://github.com/RevenueCat/expo-web-billing-demo)

## Step 1: Configure your subscription products

To begin monetizing your app across iOS, Android, and Web with RevenueCat, you’ll first need to create a RevenueCat account. This account will serve as the central hub for managing your subscriptions, products, and customer data.

1. Visit the [RevenueCat](https://revenuecat.com) website.
1. Locate the “Sign Up” or “Get Started” button.
1. Follow the on-screen instructions to create your account, providing the necessary information such as your email address, password, and company details.
1. Verify your email address when prompted.
1. Once your account is created, you’ll have access to the RevenueCat dashboard where you can begin configuring your products and integrating the SDK into your Expo app.
For setting up a connection between App Store Connect and Revenuecat follow this guide. For Android and Play Console, we have a similar one here. Web Billing requires a Stripe account, so also create one and [follow this guide linking RevenueCat and Stripe.](https://www.revenuecat.com/docs/web/connect-stripe-account) Once you have set up the platforms your app is available, install the react-native-purchases library in your project by following this guide.

### Configure iOS products in App Store Connect

To set up a one-month subscription on iOS, start by logging into App Store Connect and navigating to My Apps. Select the app you want to work on, then go to the Features tab and open the In-App Purchases section. Click the + button and choose New Subscription.

Create a subscription with a reference name **1_month_premium_ios** , and use the same value for the Product ID. Assign this subscription to a subscription group — if you don’t have one yet, you’ll need to create it now. For the duration, select **1 Month**, and choose a pricing tier that reflects the monthly cost you want to charge, such as **Tier 3**.

You’ll also need to provide localized display names, a screenshot of the subscription in the app (during testing, an empty image is fine), and descriptions for the subscription, then save the product. Remember, you’ll need to submit the subscription for review when you submit your app and then it needs to have up to date and correct information about your subscriptions.

Next, head over to your RevenueCat dashboard. Under the Products tab, click + New to add the same subscription. Use the exact same Product ID **1_month_premium_ios**, set the store to App Store, and the type to Subscription. Finally, link this product to an entitlement — for example, **premium** — which you’ll later use in your app to check if the user has access.

For more information about product setup of iOS and App Store Connect, check out [RevenueCat’s iOS product setup guide](https://www.revenuecat.com/docs/getting-started/entitlements/ios-products)

### Configure Android products in Google Play Console

If you haven’t done so already,[ follow the Google Play Store section of RevenueCat’s docs](https://www.revenuecat.com/docs/service-credentials/creating-play-service-credentials) to make RevenueCat and Google Play communicate with each other. After everything looks good there, navigate to Google Play Console’s ‘App Applications’ page and select the app you are working on. From the sidebar, select the **Products** dropdown and **Subscriptions**.

After clicking Create, you need to provide a name and a Product ID for the subscription. RevenueCat uses this unique Product ID to sync the subscription. You should use a naming that is something like <app>_<entitlement>_<version>, since Product IDs cannot be used again, even after deleting.

The next step is adding a base plan, which will define the billing period, price and renewal type. This should match the details of the subscription we created for the iOS version, so 1 month and auto-renewing. After this, you can start using your subscription in RevenueCat.
For more information about product setup for Android and Google Play, check out [RevenueCat’s documentation for Google Play product setup](https://www.revenuecat.com/docs/getting-started/entitlements/android-products).

### Configure Web Billing products in RevenueCat

We can easily create new Web Billing products through the RevenueCat dashboard. Go to your project’s settings, and under “Products” click on “New”, and then select your Web Billing App. The fields should be pretty self explanatory so go ahead and fill the details for a similar monthly subscription we had for iOS and Android. If you need any help, see the [guide for configuring Web Billing Product](https://www.revenuecat.com/docs/web/web-billing/product-setup.).

Once you’ve created a RevenueCat account and set up subscriptions for iOS, Android, and Web. Each product is linked to the same premium entitlement in RevenueCat, ensuring shared access across platforms. With Stripe connected for Web Billing and all products configured in your dashboard, you’re now ready to move on to implementation.

## Step 2: Add in-app purchase to your React Native app

Note: Expo Go does not support react-native-purchases as it contains native code. In order to make your React Native project support this package, you need to use a development build. [More information about development builds here.](https://docs.expo.dev/develop/development-builds/create-a-build/)

Now that we have everything configured from the platform side, it’s time to look at the code part that powers all this. Since we are using Expo, we can easily ship code for three platforms by just installing the `react-native-purchases` package. It works on all three platforms: iOS, Android, and web.

### Configure react-native-purchases

Create a file called `purchases.ts` and paste this inside of it:

```javascript
import { usePaymentsConfig } from "@\/hooks\/usePaymentsConfig";
import { useState } from "react";
import { useEffect } from "react";
import Purchases, {
  PurchasesPackage,
  CustomerInfo,
} from "react-native-purchases";

export const initializePayments = async (apiKey: string) => {
  await Purchases.configure({
    apiKey,
    appUserID: "test-email",
  });
};

export const usePackages = () => {
  const [packages, setPackages] = useState<PurchasesPackage[]>([]);
  const [isLoading, setIsLoading] = useState(false);

  useEffect(() => {
    const fetchPackages = async () => {
      try {
        setIsLoading(true);
        const offerings = await Purchases.getOfferings();
        console.log("offerings", offerings);
        setPackages(offerings.current?.availablePackages ?? []);
      } catch (error) {
        console.error("Error fetching packages:", error);
      } finally {
        setIsLoading(false);
      }
    };

    fetchPackages();
  }, []);

  const purchasePackage = async (pkg: PurchasesPackage) => {
    await Purchases.purchasePackage(pkg);
  };

  return { packages, isLoading, purchasePackage };
};

export const useCustomerInfo = () => {
  const [customerInfo, setCustomerInfo] = useState<CustomerInfo | null>(null);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    const fetchCustomerInfo = async () => {
      try {
        setIsLoading(true);
        const info = await Purchases.getCustomerInfo();
        setCustomerInfo(info);
      } catch (error) {
        console.error("Error fetching customer info:", error);
      } finally {
        setIsLoading(false);
      }
    };

    fetchCustomerInfo();

    \/\/ Set up listener for customer info updates
    const customerInfoUpdated = (info: CustomerInfo) => {
      setCustomerInfo(info);
    };

    Purchases.addCustomerInfoUpdateListener(customerInfoUpdated);

    return () => {
      Purchases.removeCustomerInfoUpdateListener(customerInfoUpdated);
    };
  }, []);

  const hasActiveEntitlement = (entitlementId: string) => {
    return !!customerInfo?.entitlements.active[entitlementId];
  };

  return {
    customerInfo,
    isLoading,
    hasActiveEntitlement,
  };
};
```

We have three functions in this file which enable purchasing. Let’s look at these in more detail.

We have three custom hooks:

- `initializePayments` is for initializing the RevenueCat SDK when the app is mounted.
- `usePackages`, for both displaying and purchasing packages. We are going to call this hook in the screen which displays the packages available for purchasing. The hook also returns a function to purchase available packages.
- Last we have `useCustomerInfo` which allows us to check if user has the correct entitlements and can access content
In our `App.tsx` file replace the contents with this to make use of those hooks:

```javascript
export default function HomeScreen() {
  const { packages, isLoading, purchasePackage } = usePackages();
  return (
    <ScrollView style={styles.container}>
      {packages?.map((p) => (
        <PackageCard
          key={p.identifier}
          pkg={p}
          purchasePackage={purchasePackage}
        \/>
      ))}
    <\/ScrollView>
  );
}

```

You also need to create a new file `PackageCard` and paste create a component that looks like this:

```javascript
import { StyleSheet, TouchableOpacity } from "react-native";
import { PurchasesPackage } from "react-native-purchases";
import { ThemedView } from ".\/ThemedView";
import { ThemedText } from ".\/ThemedText";

type Props = {
  pkg: PurchasesPackage;
  purchasePackage: (pkg: PurchasesPackage) => void;
};

export const PackageCard = ({ pkg, purchasePackage }: Props) => {
  return (
    <TouchableOpacity
      key={pkg.identifier}
      style={styles.card}
      onPress={() => purchasePackage(pkg)}
    >
      <ThemedView style={styles.cardContent}>
        <ThemedText style={styles.packageTitle}>{pkg.product.title}<\/ThemedText>
        <ThemedText style={styles.packageType}>
          {pkg.product.description}
        <\/ThemedText>
        <ThemedText style={styles.price}>{pkg.product.priceString}<\/ThemedText>
      <\/ThemedView>
    <\/TouchableOpacity>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    padding: 16,
  },
  titleContainer: {
    flexDirection: "row",
    alignItems: "center",
    gap: 8,
  },
  stepContainer: {
    gap: 8,
    marginBottom: 8,
    padding: 8,
  },
  reactLogo: {
    height: 178,
    width: 290,
    bottom: 0,
    left: 0,
    position: "absolute",
  },
  headerText: {
    fontSize: 24,
    marginBottom: 16,
    fontWeight: "600",
  },
  card: {
    marginVertical: 8,
    borderRadius: 12,
    overflow: "hidden",
    elevation: 2,
    boxShadow: "0px 2px 4px rgba(0, 0, 0, 0.1)",
  },
  cardContent: {
    padding: 16,
    borderRadius: 12,
  },
  packageTitle: {
    fontSize: 18,
    fontWeight: "600",
    marginBottom: 8,
  },
  packageType: {
    fontSize: 14,
    opacity: 0.7,
    marginBottom: 8,
  },
  price: {
    fontSize: 20,
    fontWeight: "700",
  },
});
```

With this we should have a functioning code for purchases. You can go ahead and run the app on your simulator or emulator. The configured subscriptions should pop up and pressing on them should initiate a purchase process. To learn more about testing subscriptions, refer to [Android testing documentation](https://www.revenuecat.com/blog/engineering/the-ultimate-guide-to-android-subscription-testing/) and [iOS testing documentation](https://www.revenuecat.com/blog/engineering/the-ultimate-guide-to-subscription-testing-on-ios/).


To support subscriptions, you’ve used react-native-purchases within a purchases.ts file. This includes initializing RevenueCat, fetching packages, handling purchases, and monitoring entitlements. These shared hooks keep native logic clean and consistent, ready to plug into your Expo app UI.

### Conclusion

With just a bit of configuration and some platform-aware code, you’ve now got a single React Native app that can sell subscriptions across iOS, Android, and Web using RevenueCat. You’ve set up products in App Store Connect, Google Play Console, and Stripe, and wired them up to a shared entitlement in RevenueCat. Thanks to platform-specific modules and a unified API surface, the app logic stays clean and consistent.

The next step would be to test your subscriptions. We just happen to have guides for all of these:

- for [iOS testing follow this guide](https://www.revenuecat.com/blog/engineering/the-ultimate-guide-to-subscription-testing-on-ios/)
- for [Android testing follow this guide](https://www.revenuecat.com/blog/engineering/the-ultimate-guide-to-android-subscription-testing/)
- and for Web take a look at the Web [billings documentation and it’s testing part](https://www.revenuecat.com/docs/web/web-billing/testing)

---

## Related posts

- [A beginner’s guide to implementing ad-free subscriptions in your React Native app](https://www.revenuecat.com/blog/engineering/ad-free-subscriptions-in-react-native)
- [How to add subscriptions to a Bolt app](https://www.revenuecat.com/blog/engineering/how-to-add-in-app-purchases-to-your-bolt-generated-expo-app)
- [Expo In-App Purchase Tutorial](https://www.revenuecat.com/blog/engineering/expo-in-app-purchase-tutorial)
