react-native-iap vs expo-iap vs react-native-purchases: which to use
Use expo-iap in an Expo app, or react-native-iap in a bare React Native app, when you will run your own server to verify purchases. Use react-native-purchases when you want a backend to verify purchases and track subscriptions for you. The first two come from the same OpenIAP project. react-native-purchases is RevenueCat's MIT-licensed SDK. It sends every purchase to a backend, which can be RevenueCat or an open-source server such as RevenueDot, and tells your app if the user is Pro on iOS and Android.
This post compares the three packages with code, cost and fit. Every fact about the packages, Expo, Google and RevenueCat links its source and was checked in October 2026.
The short answer#
react-native-iapis version 16.7.2, MIT, built on Nitro Modules (npm). It targets bare React Native 0.79 or later, and its docs say Expo support ended in v15.0.0 (OpenIAP).expo-iapis version 5.8.2, MIT, an Expo Module from the same team (npm). It is the one the OpenIAP docs tell Expo users to install.react-native-purchasesis version 10.11.0, MIT, published by RevenueCat (npm). It needs a backend that speaks RevenueCat's API.- Pick an OpenIAP package if you already run a backend or must not depend on any vendor.
- Pick
react-native-purchasesif you want entitlements, webhooks and charts without writing store server code. - Cost: the OpenIAP packages cost your engineering time.
react-native-purchaseswith RevenueCat is free up to $2,500 in monthly tracked revenue, then 1% (RevenueCat). With RevenueDot Cloud it is free up to $10,000, and self-hosting is free.
Is react-native-iap still maintained?#
Yes, but it moved. The old hyochan/react-native-iap repository is archived, and its notice says the package "is now developed in the monorepo" at hyodotdev/openiap (GitHub). expo-iap lives in the same monorepo, and its README says it "has been migrated from react-native-iap" as an Expo Module (GitHub).
The react-native-iap README says "Use expo-iap for Expo projects" (GitHub). It also needs react-native-nitro-modules installed next to it.
What does each package do?#
The two OpenIAP packages are thin layers over StoreKit 2 and Google Play Billing. react-native-purchases wraps the same store libraries and adds a client for a backend.
react-native-iap |
expo-iap |
react-native-purchases |
|
|---|---|---|---|
| Publisher and license | OpenIAP, MIT | OpenIAP, MIT | RevenueCat, MIT |
| Project type | Bare React Native 0.79+ | Expo | Bare React Native or Expo |
| Verify a purchase | Your server or IAPKit | Your server or IAPKit | The backend |
| Acknowledge Google purchases within 3 days | Your code calls finishTransaction |
Your code calls finishTransaction |
The backend |
| Renewals, refunds and billing failures | Your server | Your server | The backend |
| One entitlement across iOS and Android | Your server and database | Your server and database | Built in (entitlements.active) |
IAPKit is the OpenIAP team's own verifier, described as "open-source (MIT) purchase validation and entitlement infrastructure" (OpenIAP). So the OpenIAP path still expects a server.
Which ones work in Expo Go?#
None of them makes a real purchase in Expo Go. Expo describes a development build as "your own version of Expo Go where you are free to use any native libraries" (Expo), and every package here has native code.
| Expo Go | Development build | Bare React Native | |
|---|---|---|---|
react-native-iap |
No | Use expo-iap instead |
Yes |
expo-iap |
No | Yes | Yes, with the expo package |
react-native-purchases |
Mock calls only | Yes | Yes |
The expo-iap guide says its native modules "are not available in Expo Go" (OpenIAP). In Expo Go, react-native-purchases "replaces native calls with JavaScript-level mock APIs", so you can build a paywall, but real purchases need a development build (RevenueCat). Against RevenueDot, Expo Go and the web also accept a Test Store (test_) key (React Native docs).
Our Expo tutorial walks through the development build.
The same purchase, side by side#
With expo-iap#
react-native-iap code is almost the same.
import { useEffect } from 'react';
import { useIAP, finishTransaction, ErrorCode } from 'expo-iap';
export function usePro() {
const { connected, subscriptions, fetchProducts, requestPurchase, restorePurchases } = useIAP({
onPurchaseSuccess: async (purchase) => {
// Your server checks the token with Apple or Google.
const ok = await myServer.verify(purchase.productId, purchase.purchaseToken);
if (ok) setPro(true);
// Within 3 days on Android, or Google refunds it.
await finishTransaction({ purchase, isConsumable: false });
},
onPurchaseError: (error) => {
if (error.code !== ErrorCode.UserCancelled) console.warn(error.message);
},
});
useEffect(() => {
if (connected) fetchProducts({ skus: ['pro_monthly'], type: 'subs' });
}, [connected]);
const buyPro = async () => {
const sub = subscriptions.find((s) => s.id === 'pro_monthly');
if (!sub) return;
// Android needs an offer token from the product. The result arrives in onPurchaseSuccess.
const offerToken = sub.subscriptionOffers?.[0]?.offerTokenAndroid ?? '';
await requestPurchase({
type: 'subs',
request: {
apple: { sku: sub.id },
google: { skus: [sub.id], subscriptionOffers: [{ sku: sub.id, offerToken }] },
},
});
};
// The Restore purchases button: restored purchases come back through the same flow.
return { buyPro, restore: restorePurchases };
}myServer.verify is the part you write. It calls each store with your keys, stores the result, and must follow renewals and refunds through store notifications. The hook also offers hasActiveSubscriptions, but the docs call it a device-side check that should not grant access without server validation (OpenIAP).
With react-native-purchases and RevenueDot#
import { Platform } from 'react-native';
import Purchases from 'react-native-purchases';
export async function startStore() {
// One line of setup sends the SDK to RevenueDot instead of RevenueCat.
await Purchases.setProxyURL('https://api.revenuedot.app');
Purchases.configure({ apiKey: Platform.OS === 'ios' ? 'appl_YourKey' : 'goog_YourKey' });
}
export async function buyPro() {
const offerings = await Purchases.getOfferings();
const pkg = offerings.current?.monthly;
if (!pkg) return false;
const { customerInfo } = await Purchases.purchasePackage(pkg);
return customerInfo.entitlements.active['pro'] !== undefined;
}
// The Restore purchases button
export async function restore() {
const info = await Purchases.restorePurchases();
return info.entitlements.active['pro'] !== undefined;
}There is no verify function to write. The backend verifies each purchase, acknowledges Google purchases, follows store notifications, and returns entitlements. Drop the setProxyURL line and the same code talks to RevenueCat.
What goes wrong most often?#
With expo-iap or react-native-iap:
- Granting access without a server check. The OpenIAP docs warn that "local StoreKit or Play Billing state alone can be bypassed" (OpenIAP). Google says the same thing: send the purchase "to your secure backend" before you grant anything (Android Developers). Our server-side validation guide shows how much code that step takes.
- Forgetting
finishTransaction. Google requires each purchase to be acknowledged "within three days so that the purchase isn't automatically refunded" (Android Developers). In the OpenIAP packages,finishTransactiondoes that (OpenIAP). - Treating restore as a server sync.
restorePurchasesrefreshes purchases on the device, not on your server (OpenIAP). See restore purchases on iOS and Android.
With react-native-purchases against RevenueDot, from our React Native docs:
- Calling
configurebeforesetProxyURLfinishes. It returns a promise. Await it first, or the first requests go to RevenueCat. - Turning on entitlement verification. The stock package checks signatures against RevenueCat's key, so
ENFORCEDfails every request. The default,DISABLED, is correct. - Shipping a Test Store key. Native builds accept
test_keys only in debug builds. Release builds need theappl_andgoog_keys.
The first list is code you maintain. The second is setup you do once.
What does each choice cost?#
| Package | Backend | What you pay | |
|---|---|---|---|
expo-iap or react-native-iap + your server |
Free (MIT) | You build and run it | Engineering time, hosting and upkeep |
react-native-purchases + RevenueCat |
Free (MIT) | Hosted by RevenueCat | Free to $2,500 in monthly tracked revenue, then 1% (RevenueCat) |
react-native-purchases + RevenueDot Cloud |
Free (MIT) | Hosted by RevenueDot | Free to $10,000 in monthly tracked revenue; a paid plan is planned (pricing) |
react-native-purchases + RevenueDot self-hosted |
Free (MIT) | You run the open-source server (AGPL-3.0) | Your hosting, no revenue share |
At $50,000 in monthly tracked revenue, RevenueCat's fee is $500 a month. The fee calculator works it out for your numbers. Our RevenueCat pricing explainer shows worked bills.
RevenueCat earns part of that fee with maturity. It has a longer track record in production than RevenueDot, and that can matter more than price.
When does each one fit?#
expo-iap or react-native-iap fits when:
- You sell on one store, or only consumables whose balance lives on your server.
- You already run a backend, and someone will own both stores' server APIs and notifications.
- Your rules forbid any purchase vendor, even a self-hosted one.
Choose expo-iap for an Expo app and react-native-iap for a bare app on React Native 0.79 or later.
react-native-purchases fits when:
- You sell subscriptions on iOS and Android and want one entitlement across them.
- You want webhooks, charts and paywalls without building them.
- You want to switch backends later without rewriting purchase code.
Do it with RevenueDot#
RevenueDot is an open-source server that speaks the RevenueCat SDK's API. You install the stock react-native-purchases package, add one setProxyURL line, and get:
- Purchase verification and Google acknowledgement for the App Store and Google Play.
- One entitlement across platforms, plus webhooks, charts and paywalls.
- Setup details on the React Native SDK page and in the React Native docs.
You can also install RevenueDot's fork. Version 10.10.2 is on npm as @revenuedot/react-native-purchases, and an npm alias keeps every import ... from "react-native-purchases" as it is (React Native docs). It has passed a Test Store purchase on the web, and native builds are not verified yet. Why we forked the RevenueCat SDKs explains the reasons.
The limits matter. No real store purchase has run end to end against RevenueDot yet, so test each store in its sandbox before launch. The RevenueCat comparison lists the other differences.
Start free on RevenueDot Cloud (free up to $10,000 monthly tracked revenue).
FAQ#
Should I use react-native-iap or expo-iap in an Expo app?#
Use expo-iap. The OpenIAP docs say Expo support in react-native-iap ended in v15.0.0, and that expo-iap "provides the same API on Expo Modules" (OpenIAP).
Can I test in-app purchases in Expo Go?#
Not real ones. Both need a development build for real purchases (see the table above). With RevenueDot, a test_ key lets you buy from the Test Store in Expo Go (Test Store).
Do I need a server with expo-iap?#
Yes, if you want to trust the purchase. The docs say to validate each receipt with your backend or IAPKit before you grant access (OpenIAP). Renewals and refunds also reach a server, not the app.
Can I move from expo-iap to react-native-purchases later?#
Yes. Replace the purchase code, configure the backend, and call Purchases.syncPurchases() once on the first launch of the update so existing subscribers are recorded (React Native docs). Do not run both packages at once.
About RevenueDot. RevenueDot is an open-source (AGPL-3.0) backend for in-app purchases and subscriptions that works with the RevenueCat SDK. Start free on RevenueDot Cloud, free up to $10,000 in monthly tracked revenue, or self-host it with Docker and Postgres. Point the SDK's proxy URL at RevenueDot and keep your app code, your offerings and your customers. Read the quickstart or the code on GitHub.