implementing subscriptions the right way
Subscription commerce in React Native means far more than adding a buy button. Entitlements, paywall design, server-side validation and how you handle failed renewals decide whether a subscription model reliably generates revenue or quietly leaks it. This article walks through the full path from the first product definition to a synchronized entitlement status.
Table of Contents
- 1. Why subscription commerce works differently from web checkout
- 2. Defining products, entitlements and offerings
- 3. RevenueCat as an abstraction over StoreKit and Play Billing
- 4. Paywall patterns: soft, hard and trial-first
- 5. Server-side validation and webhook events
- 6. Upgrades, downgrades and proration between tiers
- 7. Grace periods and billing retry for failed payments
- 8. Sandbox testing with StoreKit configuration and license testers
- 9. Custom IAP integration versus RevenueCat
- 10. Summary
- 11. FAQ
1. Why subscription commerce works differently from web checkout
Subscription commerce in a mobile app follows a fundamentally different rulebook than a classic web checkout. Apple and Google require that recurring payments for digital content and features go through their respective in-app purchase infrastructure, a direct Stripe form in a WebView is not allowed for digital in-app subscriptions and reliably gets rejected in review. That means: anyone who wants to sell a subscription in a React Native app has to deal with StoreKit on iOS and the Google Play Billing Library on Android, whether they want to or not.
This store requirement also brings advantages. Users manage their subscriptions centrally in their operating system settings, payment data never touches your own infrastructure, and cancellations follow a flow users already know. The price for that is a commission of typically 15 to 30 percent, plus significantly more complexity around status checks, since the app initially has no information at all about whether a subscription is active, paused, in a refund window, or already canceled.
Anyone setting up subscription commerce in React Native properly from the ground up therefore does not think first about the paywall UI, but about the entitlement model behind it: which features which subscription unlocks, how that state stays reliably available across app restarts, device switches and store outages, and how that state synchronizes with your own backend.
2. Defining products, entitlements and offerings
The first step of every subscription model is modeling it in the respective store backend: App Store Connect and the Google Play Console require defining product IDs, regional price tiers and subscription groups within which users can switch between tiers. A typical setup has three product IDs for monthly, yearly and a cheaper introductory offer, all within the same subscription group, so Apple and Google automatically prevent a user from paying for two overlapping subscriptions at once.
On top of that sits the entitlement layer: instead of checking directly against product IDs in the app code, you define logical permissions like premium_access or pro_features, mapped to one or more products. This layer of indirection saves you a code change whenever pricing structure or product names change, since the app keeps asking only for the entitlement, never for the concrete product ID.
{
"entitlements": {
"premium_access": {
"productIdentifiers": [
"com_app_premium_monthly",
"com_app_premium_yearly",
"com_app_premium_intro"
]
}
},
"offerings": {
"default": {
"packages": [
{ "identifier": "monthly", "productId": "com_app_premium_monthly" },
{ "identifier": "annual", "productId": "com_app_premium_yearly" }
]
}
}
}
3. RevenueCat as an abstraction over StoreKit and Play Billing
Implementing StoreKit 2 directly means fully managing transaction verification, receipt parsing, restore logic and renewal events yourself, and then again in a separate form for the Google Play Billing Library. RevenueCat abstracts exactly that difference and provides a unified API for both platforms, including server-side validation, analytics and webhook events, without needing to build your own validation server. For a React Native team, that means a considerable time saving compared to a custom react-native-iap integration.
Setup happens through the react-native-purchases SDK, configured on app start and providing offerings, active entitlements and purchase flows through one shared interface afterward. The decisive advantage over a raw StoreKit integration: RevenueCat caches the entitlement status locally and syncs it in the background, so the app can still show the last known subscription status even during a brief loss of internet connectivity.
// purchases.ts — configure RevenueCat and check entitlement status
import Purchases from 'react-native-purchases';
export function configurePurchases(apiKey: string, userId: string) {
Purchases.configure({ apiKey, appUserID: userId });
}
export async function hasPremiumAccess(): Promise<boolean> {
const info = await Purchases.getCustomerInfo();
return typeof info.entitlements.active['premium_access'] !== 'undefined';
}
export async function purchasePackage(packageId: string) {
const offerings = await Purchases.getOfferings();
const pkg = offerings.current?.availablePackages.find(p => p.identifier === packageId);
if (!pkg) throw new Error('Package not found in current offering');
const { customerInfo } = await Purchases.purchasePackage(pkg);
return typeof customerInfo.entitlements.active['premium_access'] !== 'undefined';
}
4. Paywall patterns: soft, hard and trial-first
The paywall decides the conversion rate of the entire subscription model more than any other UI element in the app. A soft paywall shows premium content in a reduced or blurred form and still allows limited use of the app, a hard paywall blocks the app entirely until a subscription is completed. Hard paywalls usually achieve higher conversion rates right after onboarding, but they lose users who have not yet decided, without them ever trying the app.
Trial-first strategies, where the user immediately starts a free trial instead of making a purchase decision upfront, lower the barrier considerably, but push the actual payment commitment out by several days. In any case, a clear, non-misleading price display is essential: Apple and Google explicitly check during review whether price, billing interval and cancellation terms are clearly visible before a purchase is triggered, obscured paywalls reliably lead to rejections.
5. Server-side validation and webhook events
Pure client-side checking of purchase status is never sufficient for a production subscription model, since manipulated clients or jailbroken devices could fake a purchase. The reliable source of truth is always server-side validation, either through your own endpoints querying Apple and Google servers directly, or through RevenueCat's webhook system, which sends a signed event to your own endpoint on every purchase, renewal and cancellation.
These webhooks are the anchor for keeping entitlements current in your own backend, for example a Magento instance or a separate commerce API. A typical flow: RevenueCat sends an INITIAL_PURCHASE event, your server verifies the signature, updates the user record with the active entitlement, and then makes that status available through your own API for web or desktop clients of the same user account as well.
# Verify a RevenueCat webhook signature on your own backend endpoint
# (pseudo-CLI illustrating the verification step, actual code lives in your API layer)
curl -X POST https://api.example.com/webhooks/revenuecat \
-H "Authorization: Bearer YOUR_WEBHOOK_SECRET" \
-H "Content-Type: application/json" \
--data '{"event": {"type": "INITIAL_PURCHASE", "app_user_id": "user_123"}}'
# Typical handling steps on the receiving backend endpoint:
# 1. Reject the request if the Authorization header does not match the shared secret
# 2. Parse the event type: INITIAL_PURCHASE, RENEWAL, CANCELLATION, BILLING_ISSUE, EXPIRATION
# 3. Look up the local user record via app_user_id
# 4. Update the stored entitlement state and its expiration timestamp
# 5. Return HTTP 200 quickly, RevenueCat retries on non-2xx responses
echo "Webhook processed for user_123: entitlement premium_access updated"
A robust webhook handler should also be idempotent: RevenueCat does not guarantee exactly-once delivery, it can send the same event multiple times during network issues. Your own endpoint should therefore check via an event ID whether processing has already happened before writing the entitlement status again, to avoid duplicate notifications or inconsistent timestamps.
6. Upgrades, downgrades and proration between tiers
As soon as a subscription model offers more than one price tier, for example Basic and Pro, switching between tiers becomes its own challenge. Both Apple and Google support upgrades and downgrades within the same subscription group, but calculate proration differently: Apple credits the remaining value of the old subscription proportionally toward the new one, Google offers several proration modes to choose from, such as an immediate switch with a credit, or a switch only at the next billing period.
For the app side that means: the entitlement status needs to be resynchronized after a tier switch, since both the unlocked features and the next billing date may change. RevenueCat represents these switches as new customer info events, so the app can react through the same listener mechanism used for the initial purchase, without needing separate special-case logic for upgrades.
7. Grace periods and billing retry for failed payments
An underestimated source of revenue loss in subscription commerce is silent cancellation caused by failed payments, for example when a stored credit card has expired. Both stores offer a grace period for this: the user temporarily keeps access to premium features while Apple or Google retry the payment in the background multiple times, often spread across several days with increasing intervals.
The app needs to explicitly handle this intermediate state, rather than revoking access immediately on the first failed payment attempt. RevenueCat provides a dedicated billing_issue_detected_at timestamp in the customer info object, which the app can use to show a subtle warning, such as "please update your payment method," without immediately blocking usage. That noticeably reduces involuntary churn compared to a hard immediate lockout.
8. Sandbox testing with StoreKit configuration and license testers
Testing subscription flows manually in production is neither practical nor safe, since real payments would be triggered. Xcode offers local StoreKit configuration files for this, letting you simulate products, prices and even accelerated subscription cycles, entirely without a connection to App Store Connect. A monthly subscription can be tested end to end in a few minutes instead of a real thirty days, including renewal, cancellation and failed payment.
On the Android side, license testers in the Play Console take on this role: for registered test accounts, purchases go through the real Play Billing flow but are not actually charged. What matters for both platforms is that these sandbox purchases show up clearly separated from real production purchases in RevenueCat's dashboard, so test transactions do not distort real revenue metrics.
9. Custom IAP integration versus RevenueCat
Whether a custom react-native-iap integration or RevenueCat is the right choice depends heavily on team size and the desired level of control. The table below contrasts both approaches along the most important decision dimensions.
| Dimension | Custom IAP integration | RevenueCat |
|---|---|---|
| Development effort | High, custom StoreKit and Billing integration | Low, one SDK for both platforms |
| Server validation | Must be built and operated yourself | Included, accessible via webhooks |
| Cross-platform sync | Needs custom app-user-ID mapping logic | Built-in app-user-ID concept |
| Cost | No additional service fee | Revenue-based fee above a certain threshold |
| Maintenance for store changes | Your own team has to keep up with API changes | Maintained centrally by the provider |
For most teams the time saving from RevenueCat clearly outweighs the added cost, especially as long as no highly specialized billing requirements exist. A custom integration usually only pays off when revenue volume is large enough that the RevenueCat fee exceeds the saved development time, or when regulatory reasons demand a fully self-controlled validation chain.
Mironsoft
React Native development, subscription commerce and mobile backend integration
Implementing a subscription model in your React Native app?
We build subscription commerce with RevenueCat or a custom StoreKit and Play Billing integration, including paywall design, server sync and grace-period handling for your subscription model.
Paywall & entitlements
High-converting paywalls with a clear entitlement model
Server sync
Webhook integration and entitlement reconciliation with your backend
Testing & rollout
Sandbox setup, grace-period handling and store review preparation
10. Summary
Subscription commerce in React Native starts with a clean entitlement model, not with the paywall UI. Products, price tiers and subscription groups are defined in the store backend, RevenueCat abstracts the differences between StoreKit 2 and Play Billing behind a shared API, and webhooks keep the entitlement status current in your own backend. Paywall patterns, proration on tier switches and correctly handling grace periods ultimately decide how much of the theoretical revenue actually arrives.
The biggest lever for a working subscription model is rarely the last UI detail of the paywall, but the robustness of the status logic behind it. A user locked out instantly and without warning because of an expired credit card is more likely to cancel than to update their payment method. Thorough sandbox testing with StoreKit configuration files and Play Console license testers uncovers these edge cases before they cost real revenue in production.
React Native Subscription Commerce — The Essentials at a Glance
Entitlement model
Logical permissions instead of hard product ID checks decouple code from pricing changes.
RevenueCat as abstraction
One API for StoreKit 2 and Play Billing saves considerable integration effort.
Server sync via webhooks
Signed events keep entitlements current in your own backend, across platforms.
Grace period & testing
Subtle warnings instead of instant lockout, thorough sandbox testing before every release.