
Learn how to build high-converting mobile app paywalls and handle in-app purchases smoothly in React Native without third-party bloat.
When you spend months designing, building, and refining a mobile application—whether it is an offline habit tracker like Nept Track or a facial fitness tool like FaceSculpt AI—the moment of truth always arrives at the billing screen. Monetizing mobile apps can feel daunting. Apple and Google take a cut, receipt validation can fail silently, and building a user-friendly paywall requires careful state management.
Over the years of building cross-platform mobile apps for clients worldwide, I have learned that handling in-app purchases (IAP) in React Native does not need to be a nightmare. In this guide, I will walk you through setting up native subscriptions, designing a high-converting paywall UI, and validating purchases securely.
Before writing any code, you need to decide how your app will charge users. The most common models for indie developers and startups are:
For most utility apps and productivity tools, a monthly or yearly subscription model with a free trial works best. It provides predictable MRR (Monthly Recurring Revenue) while lowering the barrier to entry for hesitant users.
You have two primary paths when implementing IAPs in React Native:
react-native-iap): You talk directly to the App Store and Google Play billing APIs. This gives you total control and zero third-party subscription fees, but you must handle receipt validation, server-side storage, and edge cases yourself.react-native-purchases by RevenueCat): RevenueCat handles the complex logic of receipt validation, cross-platform syncing, entitlement management, and webhooks.For most projects, I recommend starting with RevenueCat unless you have strict data privacy requirements that forbid third-party analytics on payment data. It saves dozens of hours of debugging sandbox user accounts.
Tagged with
Leave a comment
Let us look at how to set up react-native-purchases in a modern Expo or bare React Native project. First, install the package:
npm install react-native-purchases
cd ios && pod install
Next, initialize the SDK inside your root component, usually App.tsx or _layout.tsx if you are using Expo Router:
import { useEffect } from 'react';
import Purchases from 'react-native-purchases';
export default function App() {
useEffect(() => {
if (__DEV__) {
Purchases.setLogLevel(Purchases.LOG_LEVEL.DEBUG);
}
// Initialize with your App Store or Play Store public API key
Purchases.configure({ apiKey: 'appl_your_api_key_here' });
}, []);
return (
// Your app navigation tree
);
}
A paywall should never feel like a blocker; it should feel like an invitation to unlock more value. Here are three design rules I follow when building paywalls for my clients:
Here is a simple, clean paywall screen component written in React Native:
import React, { useState, useEffect } from 'react';
import { View, Text, TouchableOpacity, StyleSheet, ActivityIndicator, Alert } from 'react-native';
import Purchases, { PurchasesPackage } from 'react-native-purchases';
export default function PaywallScreen({ navigation }: { navigation: any }) {
const [packages, setPackages] = useState<PurchasesPackage[]>([]);
const [loading, setLoading] = useState<boolean>(true);
const [purchasing, setPurchasing] = useState<boolean>(false);
useEffect(() => {
async function loadOfferings() {
try {
const offerings = await Purchases.getOfferings();
if (offerings.current && offerings.current.availablePackages.length > 0) {
setPackages(offerings.current.availablePackages);
}
} catch (error: any) {
Alert.alert('Error', error.message);
} finally {
setLoading(false);
}
}
loadOfferings();
}, []);
const handlePurchase = async (pkg: PurchasesPackage) => {
setPurchasing(true);
try {
const { customerInfo } = await Purchases.purchasePackage(pkg);
if (customerInfo.entitlements.active['pro'] !== undefined) {
Alert.alert('Success', 'Welcome to Pro!');
navigation.goBack();
}
} catch (error: any) {
if (!error.userCancelled) {
Alert.alert('Purchase Failed', error.message);
}
} finally {
setPurchasing(false);
}
};
if (loading) {
return (
<View style={styles.center}>
<ActivityIndicator size="large" color="#000" />
</View>
);
}
return (
<View style={styles.container}>
<Text style={styles.title}>Unlock Full Access</Text>
<Text style={styles.subtitle}>Get unlimited features and remove all limits.</Text>
{packages.map((pkg) => (
<TouchableOpacity
key={pkg.identifier}
style={styles.packageCard}
onPress={() => handlePurchase(pkg)}
disabled={purchasing}
>
<Text style={styles.packageName}>{pkg.product.title}</Text>
<Text style={styles.packagePrice}>{pkg.product.priceString}</Text>
</TouchableOpacity>
))}
{purchasing && <ActivityIndicator style={{ marginTop: 20 }} color="#000" />}
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, padding: 24, justifyContent: 'center', backgroundColor: '#fff' },
center: { flex: 1, justifyContent: 'center', alignItems: 'center' },
title: { fontSize: 28, fontWeight: 'bold', textAlign: 'center', marginBottom: 8 },
subtitle: { fontSize: 16, color: '#666', textAlign: 'center', marginBottom: 32 },
packageCard: { padding: 20, borderWidth: 1, borderColor: '#ddd', borderRadius: 12, marginBottom: 16, backgroundColor: '#f9f9f9' },
packageName: { fontSize: 18, fontWeight: '600' },
packagePrice: { fontSize: 16, color: '#007AFF', marginTop: 4 }
});
Apple and Google are notoriously strict when reviewing apps with in-app purchases. To avoid getting rejected on your first submission, ensure you meet these criteria:
Implementing in-app purchases does not have to be a painful detour in your mobile app development journey. By combining a reliable service like RevenueCat with a clean, well-designed React Native UI, you can launch your monetization engine quickly and securely.
Need help building, scaling, or monetizing your next mobile app or SaaS product? Get in touch and let us chat about your project.