subscriptions without custom StoreKit and Billing logic
Implementing in-app purchases directly against StoreKit and Google Play Billing in a React Native app means duplicating every change to products, prices and receipt verification for both platforms. RevenueCat unifies both billing systems behind a shared API of offerings, entitlements and server-side receipt validation, letting paywall logic, purchase flow and subscription status live in a single React Native codebase.
Table of contents
- 1. Why building StoreKit and Billing integration yourself hurts
- 2. RevenueCat core concepts: offerings, packages, entitlements
- 3. Setting up products in App Store Connect and Play Console
- 4. SDK installation and configuration
- 5. Paywall UI: fetching and rendering offerings
- 6. Purchase flow: purchasePackage and restore
- 7. Checking entitlement status reactively
- 8. Server-side validation and webhooks
- 9. Testing and comparison to alternatives
- 10. Summary
- 11. FAQ
1. Why building StoreKit and Billing integration yourself hurts
Implementing in-app purchases in React Native without an abstraction layer means maintaining two completely different billing systems in parallel. StoreKit on iOS and Google Play Billing on Android have different product models, different receipt formats and different error states. Every change to a subscription product, every new price tier and every adjustment to receipt verification has to be implemented and tested separately on both platforms. For in-app purchases in a React Native app that means doubled maintenance effort for the exact same business logic.
Even more critical is server-side receipt validation. A purchase that is only verified on the client can be forged with modified apps or tampered receipts. Proper security requires a dedicated server that validates purchase receipts against the App Store and Play Store APIs, keeps subscription status in sync, and reacts to revocations, refunds and payment issues. This exact server infrastructure, receipt validation, retry logic and status synchronization is what RevenueCat takes over as a managed service, so the React Native app only ever talks to a single, platform-independent API.
The third pain point is sandbox testing. Apple's sandbox environment has its own latencies, accelerated subscription cycles and occasional inconsistencies, while Google's testing track requires its own setup with license testers. Without an abstraction layer, developers have to master both test environments separately before the very first purchase reliably goes through. RevenueCat normalizes this part too, since sandbox purchases show up in the dashboard exactly like production purchases.
2. RevenueCat core concepts: offerings, packages, entitlements
RevenueCat introduces three central concepts that shift thinking about in-app purchases away from platform-specific product IDs toward business logic. A Product is the raw store reference, meaning the product ID from App Store Connect or the Play Console. A Package bundles a product with an identifier like $rc_monthly or $rc_annual, making duration variants addressable across platforms without the code ever needing to know the raw product ID. An Offering is a collection of packages that together correspond to a paywall, for example a "Standard Paywall" or an A/B test variant "Discount Paywall".
The fourth and most important concept is the Entitlement. An entitlement like premium or pro_features represents an unlock in the product, regardless of which package or which platform it was purchased through. React Native code never asks "did the user buy product X", it asks "does the user have the premium entitlement". This model lets pricing experiments, discount campaigns and new durations be changed in the RevenueCat dashboard without touching app code or store configuration.
This separation between product, package, offering and entitlement is the actual value RevenueCat provides over a homegrown solution. Price changes, new country-specific price tiers or an additional annual plan can be configured in the dashboard and are instantly available in the app, without a new deployment. App logic stays stable because it only ever checks against entitlements, while the underlying packages and offerings can keep evolving freely.
3. Setting up products in App Store Connect and Play Console
Before RevenueCat can display anything at all, the actual in-app products need to be created in App Store Connect and in the Google Play Console. In App Store Connect, an auto-renewable subscription group is created under "Subscriptions", and the individual durations (monthly, annual) are defined within it with unique product IDs, typically following a pattern like com.app.premium.monthly. In the Play Console, an analogous subscription product is created with base plans for the same durations. Both stores also require metadata such as display name, description and localized price tiers per country.
Once the products exist in both stores, they get imported into the RevenueCat dashboard under "Products" and assigned to an entitlement there, for example both com.app.premium.monthly and the corresponding Android product get assigned to the premium entitlement. An offering is then created that contains both durations as packages. This step happens entirely in the dashboard and requires no code change in the React Native app, which significantly speeds up pricing experiments and new durations.
4. SDK installation and configuration
Integration into a React Native or Expo app starts with installing react-native-purchases. In an Expo project with a development build or EAS build, the package works like any other native module, as long as Expo Go is not used, since native modules are not available in Expo Go. After installation, the API configuration is run once at app startup, ideally in the topmost component before the actual app navigation renders for the first time.
It's important that iOS and Android use different API keys from the RevenueCat dashboard, even though both platforms share the same entitlement setup. Additionally, an appUserID should be passed right at configuration time as soon as a user is logged in, so purchases get tied to an account rather than staying bound only to an anonymous, device-local ID. Skipping this step causes subscription assignments to get lost on device reinstalls or platform switches.
# Install the RevenueCat SDK for React Native
npm install react-native-purchases
# For bare React Native projects, install native pods
cd ios && pod install && cd ..
# Expo projects need a Development Build since this is a native module
npx expo install expo-dev-client
eas build --profile development --platform all
// App.tsx - configure RevenueCat once at app startup
import { useEffect } from 'react';
import { Platform } from 'react-native';
import Purchases, { LOG_LEVEL } from 'react-native-purchases';
const IOS_API_KEY = 'appl_XXXXXXXXXXXXXXXXXXXXXXXXXXX';
const ANDROID_API_KEY = 'goog_XXXXXXXXXXXXXXXXXXXXXXXXXXX';
export function configurePurchases(userId?: string) {
Purchases.setLogLevel(LOG_LEVEL.WARN);
const apiKey = Platform.OS === 'ios' ? IOS_API_KEY : ANDROID_API_KEY;
Purchases.configure({
apiKey,
appUserID: userId, // pass the logged-in user id to link purchases to an account
});
}
export default function App() {
useEffect(() => {
configurePurchases();
}, []);
return null; // rest of the app tree
}
5. Paywall UI: fetching and rendering offerings
Once the SDK is configured, the current offering can be fetched via Purchases.getOfferings(). The returned object contains a current offering with all the packages it holds, and every package already carries the formatted price in the user's local currency. For paywall UI, that means no custom price formatting or currency conversion needs to be implemented, RevenueCat already delivers store-localized prices.
A robust paywall component should show a loading state while offerings are being fetched, since the network call can take noticeable time depending on connection quality, and should provide a fallback for the case where no offering is configured or available, for instance because store products haven't been approved yet. It's also common to run an A/B test across multiple offerings, RevenueCat automatically assigns a variant to users and reports it back to analytics tools, so conversion differences between paywall variants become directly measurable.
// PaywallScreen.tsx - fetch offerings and render purchase options
import { useEffect, useState } from 'react';
import { View, Text, TouchableOpacity, ActivityIndicator } from 'react-native';
import Purchases, { PurchasesOffering } from 'react-native-purchases';
export function PaywallScreen() {
const [offering, setOffering] = useState<PurchasesOffering | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function loadOfferings() {
try {
const offerings = await Purchases.getOfferings();
setOffering(offerings.current);
} catch (error) {
console.error('Failed to load offerings', error);
} finally {
setLoading(false);
}
}
loadOfferings();
}, []);
if (loading) return <ActivityIndicator />;
if (!offering) return <Text>No offering available right now.</Text>;
return (
<View>
{offering.availablePackages.map((pkg) => (
<TouchableOpacity key={pkg.identifier} onPress={() => handlePurchase(pkg)}>
<Text>{pkg.product.title} - {pkg.product.priceString}</Text>
</TouchableOpacity>
))}
</View>
);
}
6. Purchase flow: purchasePackage and restore
The actual purchase is triggered via Purchases.purchasePackage(package), which internally opens the native store UI, handles the payment flow, and returns a CustomerInfo object with the current entitlement status once complete. Error handling is critical here: users cancel the purchase dialog, payment methods fail, or the network drops during receipt verification. RevenueCat distinguishes between a user-cancelled purchase and an actual error, so the app doesn't need to show an error message on cancellation.
Equally essential is the restore functionality via Purchases.restorePurchases(). Users switch devices, reinstall the app, or use a second device with the same store account, and in all these cases they expect already-purchased subscriptions to be automatically recognized again. A restore button in settings is even a requirement for App Store approval on iOS, since Apple explicitly checks for it as a review criterion. On restore, RevenueCat reconciles store account information with its own server and updates entitlements accordingly.
// Purchase flow with proper cancel vs. error handling
import Purchases, { PurchasesPackage, PurchasesError } from 'react-native-purchases';
async function handlePurchase(pkg: PurchasesPackage) {
try {
const { customerInfo } = await Purchases.purchasePackage(pkg);
const isPremium = customerInfo.entitlements.active['premium'] !== undefined;
if (isPremium) {
// Unlock premium content in the UI
}
} catch (error) {
const purchaseError = error as PurchasesError;
if (purchaseError.userCancelled) {
return; // user closed the native purchase sheet, no error UI needed
}
console.error('Purchase failed', purchaseError.message);
}
}
async function handleRestore() {
try {
const customerInfo = await Purchases.restorePurchases();
const isPremium = customerInfo.entitlements.active['premium'] !== undefined;
return isPremium;
} catch (error) {
console.error('Restore failed', error);
return false;
}
}
7. Checking entitlement status reactively
Instead of manually querying entitlement status on every app start, RevenueCat provides a listener via Purchases.addCustomerInfoUpdateListener that automatically fires on any change to subscription status. This matters because entitlements can change outside the app too, for instance when a subscription is cancelled in the App Store account settings screen, or a payment fails and the store automatically pauses the subscription.
A clean pattern is to hold entitlement status in a global context or store like Zustand and keep that context up to date through the CustomerInfo listener. Every component that needs to check whether a user has access to premium content then reads exclusively from this central state, instead of duplicating its own RevenueCat calls. That avoids race conditions between multiple concurrent status queries and keeps the UI consistent as soon as subscription status changes.
8. Server-side validation and webhooks
Even with RevenueCat, a server-side component still makes sense as soon as premium content or features are delivered through your own backend service, not just client-side in React Native. RevenueCat sends a webhook to a developer-configured server URL on every relevant event, such as a renewal, a cancellation, a billing issue or a refund. The own server then updates its own database and can decide, independently of client state, whether a user gets access to server-side premium resources.
This pattern is especially important for background subscription renewals that happen while the app isn't even open. Without a webhook, the server would only learn about the current status on the next app launch, with a webhook the server database stays in near real-time sync. For added security, RevenueCat signs every webhook call with an authorization header that your own endpoint should verify before processing, to reject forged calls.
{
"api_version": "1.0",
"event": {
"type": "RENEWAL",
"id": "evt_1234567890",
"app_user_id": "user_98765",
"product_id": "com.app.premium.monthly",
"entitlement_ids": ["premium"],
"period_type": "NORMAL",
"purchased_at_ms": 1769270400000,
"expiration_at_ms": 1771948800000,
"environment": "PRODUCTION",
"store": "APP_STORE",
"currency": "EUR",
"price": 4.99
}
}
9. Testing and comparison to alternatives
For iOS testing, Xcode offers the StoreKit Testing framework, which lets purchases, subscription renewals and even cancellations be fully simulated locally, without real sandbox accounts or network connections to Apple's infrastructure. For pre-launch production testing, real sandbox tester accounts in App Store Connect are additionally necessary, since only those reflect the real timing of subscription renewals, albeit heavily accelerated compared to production. On the Android side, license testers in the Play Console play a similar role.
RevenueCat is not the only solution for this problem class, Adapty and Qonversion follow a similar approach with their own strengths in paywall A/B testing and attribution respectively. The choice depends heavily on how deep the integration with existing analytics and attribution tools needs to be, and how mature the provider's server-side webhook infrastructure already is.
| Criterion | RevenueCat | Raw StoreKit / Play Billing | Adapty / Qonversion |
|---|---|---|---|
| Setup effort | Low, SDK + dashboard | High, two separate implementations | Low to medium |
| Cross-platform API | Yes, one shared API | No, platform-specific | Yes |
| Server receipt validation | Managed, including webhooks | Must be built yourself | Managed |
| Analytics/attribution | Good, many integrations | None built in | Often a focus feature |
| Pricing model | Free up to revenue threshold, then % | No additional cost | Similar to RevenueCat |
In practice, the time savings from RevenueCat clearly outweigh the moderate revenue fees for most teams, especially because the alternative, a homegrown server-side receipt validation with full webhook infrastructure, costs weeks of development time that keeps showing up as maintenance overhead as an app grows.
Mironsoft
React Native development, app monetization and backend integration
Want in-app purchases anchored reliably in your React Native app?
We implement RevenueCat paywalls, entitlement logic and server-side webhook integration, so subscription revenue arrives reliably and pricing experiments can run without a new app release.
RevenueCat setup
Offerings, entitlements and product configuration in App Store Connect and Play Console
Paywall development
Conversion-focused paywall UI with A/B test integration and restore flow
Backend integration
Webhook processing and entitlement synchronization with your own server
10. Summary
React Native in-app purchases with RevenueCat solve the core problem of duplicated billing implementation by making StoreKit and Google Play Billing disappear behind a shared API of offerings, packages and entitlements. Instead of checking product IDs directly in app code, the app only ever checks against entitlements like premium, while price changes and new durations get configured in the dashboard without requiring a new app release.
Server-side webhooks keep your own backend database in sync with renewals, cancellations and billing issues, even while the app isn't open. Combined with StoreKit Testing and sandbox accounts, the entire purchase and subscription lifecycle can be reliably validated before launch. Compared to a homegrown solution, the saved development and maintenance effort clearly outweighs the moderate RevenueCat fees in the vast majority of projects.
React Native In-App Purchases with RevenueCat — Key takeaways
Concepts
Offerings bundle packages, packages reference store products, entitlements decouple business logic from store details.
Integration
Configure react-native-purchases, fetch offerings, purchase with purchasePackage, restore with restorePurchases.
Server sync
Webhooks for renewals, cancellations and billing issues keep your own backend up to date in near real time.
Testing
StoreKit Testing in Xcode for local simulation, sandbox accounts and Play Console license testers before launch.