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.
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.
- Visit the RevenueCat website.
- Locate the “Sign Up” or “Get Started” button.
- Follow the on-screen instructions to create your account, providing the necessary information such as your email address, password, and company details.
- Verify your email address when prompted.
- 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. 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
Configure Android products in Google Play Console
If you haven’t done so already, follow the Google Play Store section of RevenueCat’s docs 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.
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.
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.
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:
1import { usePaymentsConfig } from "@/hooks/usePaymentsConfig";
2import { useState } from "react";
3import { useEffect } from "react";
4import Purchases, {
5 PurchasesPackage,
6 CustomerInfo,
7} from "react-native-purchases";
8
9export const initializePayments = async (apiKey: string) => {
10 await Purchases.configure({
11 apiKey,
12 appUserID: "test-email",
13 });
14};
15
16export const usePackages = () => {
17 const [packages, setPackages] = useState<PurchasesPackage[]>([]);
18 const [isLoading, setIsLoading] = useState(false);
19
20 useEffect(() => {
21 const fetchPackages = async () => {
22 try {
23 setIsLoading(true);
24 const offerings = await Purchases.getOfferings();
25 console.log("offerings", offerings);
26 setPackages(offerings.current?.availablePackages ?? []);
27 } catch (error) {
28 console.error("Error fetching packages:", error);
29 } finally {
30 setIsLoading(false);
31 }
32 };
33
34 fetchPackages();
35 }, []);
36
37 const purchasePackage = async (pkg: PurchasesPackage) => {
38 await Purchases.purchasePackage(pkg);
39 };
40
41 return { packages, isLoading, purchasePackage };
42};
43
44export const useCustomerInfo = () => {
45 const [customerInfo, setCustomerInfo] = useState<CustomerInfo | null>(null);
46 const [isLoading, setIsLoading] = useState(true);
47
48 useEffect(() => {
49 const fetchCustomerInfo = async () => {
50 try {
51 setIsLoading(true);
52 const info = await Purchases.getCustomerInfo();
53 setCustomerInfo(info);
54 } catch (error) {
55 console.error("Error fetching customer info:", error);
56 } finally {
57 setIsLoading(false);
58 }
59 };
60
61 fetchCustomerInfo();
62
63 // Set up listener for customer info updates
64 const customerInfoUpdated = (info: CustomerInfo) => {
65 setCustomerInfo(info);
66 };
67
68 Purchases.addCustomerInfoUpdateListener(customerInfoUpdated);
69
70 return () => {
71 Purchases.removeCustomerInfoUpdateListener(customerInfoUpdated);
72 };
73 }, []);
74
75 const hasActiveEntitlement = (entitlementId: string) => {
76 return !!customerInfo?.entitlements.active[entitlementId];
77 };
78
79 return {
80 customerInfo,
81 isLoading,
82 hasActiveEntitlement,
83 };
84};
We have three functions in this file which enable purchasing. Let’s look at these in more detail.
We have three custom hooks:
initializePaymentsis 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
useCustomerInfowhich 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:
1export default function HomeScreen() {
2 const { packages, isLoading, purchasePackage } = usePackages();
3 return (
4 <ScrollView style={styles.container}>
5 {packages?.map((p) => (
6 <PackageCard
7 key={p.identifier}
8 pkg={p}
9 purchasePackage={purchasePackage}
10 />
11 ))}
12 </ScrollView>
13 );
14}
15
You also need to create a new file PackageCard and paste create a component that looks like this:
1import { StyleSheet, TouchableOpacity } from "react-native";
2import { PurchasesPackage } from "react-native-purchases";
3import { ThemedView } from "./ThemedView";
4import { ThemedText } from "./ThemedText";
5
6type Props = {
7 pkg: PurchasesPackage;
8 purchasePackage: (pkg: PurchasesPackage) => void;
9};
10
11export const PackageCard = ({ pkg, purchasePackage }: Props) => {
12 return (
13 <TouchableOpacity
14 key={pkg.identifier}
15 style={styles.card}
16 onPress={() => purchasePackage(pkg)}
17 >
18 <ThemedView style={styles.cardContent}>
19 <ThemedText style={styles.packageTitle}>{pkg.product.title}</ThemedText>
20 <ThemedText style={styles.packageType}>
21 {pkg.product.description}
22 </ThemedText>
23 <ThemedText style={styles.price}>{pkg.product.priceString}</ThemedText>
24 </ThemedView>
25 </TouchableOpacity>
26 );
27};
28
29const styles = StyleSheet.create({
30 container: {
31 flex: 1,
32 padding: 16,
33 },
34 titleContainer: {
35 flexDirection: "row",
36 alignItems: "center",
37 gap: 8,
38 },
39 stepContainer: {
40 gap: 8,
41 marginBottom: 8,
42 padding: 8,
43 },
44 reactLogo: {
45 height: 178,
46 width: 290,
47 bottom: 0,
48 left: 0,
49 position: "absolute",
50 },
51 headerText: {
52 fontSize: 24,
53 marginBottom: 16,
54 fontWeight: "600",
55 },
56 card: {
57 marginVertical: 8,
58 borderRadius: 12,
59 overflow: "hidden",
60 elevation: 2,
61 boxShadow: "0px 2px 4px rgba(0, 0, 0, 0.1)",
62 },
63 cardContent: {
64 padding: 16,
65 borderRadius: 12,
66 },
67 packageTitle: {
68 fontSize: 18,
69 fontWeight: "600",
70 marginBottom: 8,
71 },
72 packageType: {
73 fontSize: 14,
74 opacity: 0.7,
75 marginBottom: 8,
76 },
77 price: {
78 fontSize: 20,
79 fontWeight: "700",
80 },
81});
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 and iOS testing documentation.
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
- for Android testing follow this guide
- and for Web take a look at the Web billings documentation and it’s testing part

