
Building something similar? Explore our MVP development services, AI MVP programs, or recent case studies to see how we ship revenue-ready products in 28 days.
Shipping a mobile app to both the App Store and Play Store can feel overwhelming, especially when you need to integrate payments and subscriptions. But with Expo and RevenueCat, you can go from zero to live on both stores in less than a week.
This comprehensive guide will walk you through every step: setting up Expo, integrating RevenueCat for one-time payments and subscriptions, building production-ready apps, and navigating both store submission processes. By the end, you'll have a clear roadmap to get your app live in 7 days or less.
Expo eliminates the complexity of native iOS and Android development. You write React Native code once, and Expo handles the native build configuration, app signing, and deployment. No Xcode or Android Studio setup required.
RevenueCat simplifies in-app purchases and subscriptions. Instead of managing Apple's StoreKit and Google's Billing Library separately, RevenueCat provides a unified API that handles receipt validation, subscription status, and cross-platform user management.
Together, they let you:
Start by creating a new Expo project with TypeScript:
npx create-expo-app@latest MyApp --template
# Select: blank (TypeScript)
cd MyApp
Install essential dependencies:
npx expo install expo-router react-native-safe-area-context react-native-screens
npm install @react-navigation/native
EAS is Expo's cloud service for building and submitting apps. Sign up and configure:
npm install -g eas-cli
eas login
eas build:configure
This creates an eas.json file. Configure it for both platforms:
{
"cli": {
"version": ">= 5.0.0"
},
"build": {
"development": {
"developmentClient": true,
"distribution": "internal"
},
"preview": {
"distribution": "internal",
"ios": {
"simulator": true
}
},
"production": {
"autoIncrement": true
}
},
"submit": {
"production": {}
}
}
Update app.json with your app information:
{
"expo": {
"name": "Your App Name",
"slug": "your-app-slug",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/icon.png",
"userInterfaceStyle": "light",
"splash": {
"image": "./assets/splash.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff"
},
"ios": {
"supportsTablet": true,
"bundleIdentifier": "com.yourcompany.yourapp",
"buildNumber": "1"
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#ffffff"
},
"package": "com.yourcompany.yourapp",
"versionCode": 1
},
"plugins": [
"expo-router"
]
}
}
Critical: Your bundle identifier (iOS) and package name (Android) must be unique and match what you'll configure in App Store Connect and Google Play Console.
In RevenueCat dashboard, create your products:
For One-Time Purchases:
premium_unlockFor Subscriptions:
premium_monthlynpm install react-native-purchases
npx expo install react-native-purchases
Create a RevenueCat service file:
// lib/revenuecat.ts
import Purchases, {
PurchasesOffering,
PurchasesPackage,
CustomerInfo
} from 'react-native-purchases';
import { Platform } from 'react-native';
const REVENUECAT_API_KEY = {
ios: 'your_ios_api_key',
android: 'your_android_api_key'
};
export class RevenueCatService {
static async initialize(userId?: string) {
const apiKey = Platform.OS === 'ios'
? REVENUECAT_API_KEY.ios
: REVENUECAT_API_KEY.android;
await Purchases.configure({ apiKey });
if (userId) {
await Purchases.logIn(userId);
}
}
static async getOfferings(): Promise<PurchasesOffering | null> {
try {
const offerings = await Purchases.getOfferings();
return offerings.current;
} catch (error) {
console.error('Error fetching offerings:', error);
return null;
}
}
static async purchasePackage(pkg: PurchasesPackage): Promise<CustomerInfo> {
try {
const { customerInfo } = await Purchases.purchasePackage(pkg);
return customerInfo;
} catch (error) {
console.error('Purchase error:', error);
throw error;
}
}
static async restorePurchases(): Promise<CustomerInfo> {
try {
return await Purchases.restorePurchases();
} catch (error) {
console.error('Restore error:', error);
throw error;
}
}
static async getCustomerInfo(): Promise<CustomerInfo> {
try {
return await Purchases.getCustomerInfo();
} catch (error) {
console.error('Error fetching customer info:', error);
throw error;
}
}
static async checkSubscriptionStatus(): Promise<boolean> {
try {
const customerInfo = await this.getCustomerInfo();
return customerInfo.entitlements.active['premium'] !== undefined;
} catch (error) {
return false;
}
}
}
// components/Paywall.tsx
import React, { useEffect, useState } from 'react';
import { View, Text, Button, ActivityIndicator } from 'react-native';
import { RevenueCatService } from '@/lib/revenuecat';
import { PurchasesOffering, PurchasesPackage } from 'react-native-purchases';
export function Paywall() {
const [offering, setOffering] = useState<PurchasesOffering | null>(null);
const [loading, setLoading] = useState(true);
const [purchasing, setPurchasing] = useState(false);
useEffect(() => {
loadOfferings();
}, []);
const loadOfferings = async () => {
try {
const currentOffering = await RevenueCatService.getOfferings();
setOffering(currentOffering);
} catch (error) {
console.error('Error loading offerings:', error);
} finally {
setLoading(false);
}
};
const handlePurchase = async (pkg: PurchasesPackage) => {
setPurchasing(true);
try {
const customerInfo = await RevenueCatService.purchasePackage(pkg);
if (customerInfo.entitlements.active['premium']) {
// User now has premium access
console.log('Purchase successful!');
// Navigate to premium content or update UI
}
} catch (error: any) {
if (error.userCancelled) {
console.log('User cancelled purchase');
} else {
console.error('Purchase failed:', error);
}
} finally {
setPurchasing(false);
}
};
const handleRestore = async () => {
try {
await RevenueCatService.restorePurchases();
// Check subscription status and update UI
} catch (error) {
console.error('Restore failed:', error);
}
};
if (loading) {
return <ActivityIndicator />;
}
if (!offering) {
return <Text>No offerings available</Text>;
}
return (
<View>
<Text>Choose Your Plan</Text>
{/* One-Time Purchase */}
{offering.availablePackages.find(pkg =>
pkg.identifier === '$rc_one_time_purchase'
) && (
<View>
<Text>Premium Unlock - One Time</Text>
<Button
title={`Buy for ${offering.availablePackages.find(pkg =>
pkg.identifier === '$rc_one_time_purchase'
)?.product.priceString}`}
onPress={() => handlePurchase(
offering.availablePackages.find(pkg =>
pkg.identifier === '$rc_one_time_purchase'
)!
)}
disabled={purchasing}
/>
</View>
)}
{/* Monthly Subscription */}
{offering.availablePackages.find(pkg =>
pkg.identifier === 'monthly'
) && (
<View>
<Text>Premium Monthly</Text>
<Button
title={`Subscribe for ${offering.availablePackages.find(pkg =>
pkg.identifier === 'monthly'
)?.product.priceString}/month`}
onPress={() => handlePurchase(
offering.availablePackages.find(pkg =>
pkg.identifier === 'monthly'
)!
)}
disabled={purchasing}
/>
</View>
)}
<Button title="Restore Purchases" onPress={handleRestore} />
</View>
);
}
// app/_layout.tsx or App.tsx
import { useEffect } from 'react';
import { RevenueCatService } from '@/lib/revenuecat';
export default function RootLayout() {
useEffect(() => {
// Initialize RevenueCat when app starts
RevenueCatService.initialize();
}, []);
// ... rest of your app
}
Pro Tip: Start this process immediately, even before your app is ready. Approval can be the longest wait.
app.json bundleIdentifieryourapp-001)For One-Time Purchase:
premium_unlock (must match RevenueCat)For Subscription:
premium_monthly (must match RevenueCat)Critical: Product IDs must exactly match what you configured in RevenueCat.
While building, prepare:
For One-Time Purchase:
premium_unlock (must match RevenueCat)For Subscription:
premium_monthly (must match RevenueCat)Update eas.json for production builds:
{
"build": {
"production": {
"ios": {
"bundleIdentifier": "com.yourcompany.yourapp"
},
"android": {
"package": "com.yourcompany.yourapp"
},
"env": {
"REVENUECAT_IOS_KEY": "your_ios_key",
"REVENUECAT_ANDROID_KEY": "your_android_key"
}
}
}
}
eas build --platform ios --profile production
This will:
.ipa fileFirst build takes 15-20 minutes. Subsequent builds are faster.
eas build --platform android --profile production
Generates an .aab (Android App Bundle) file, which is required for Play Store submission.
Download and install the builds on physical devices:
iOS:
Android:
.aab file (convert to APK for testing if needed)eas submit --platform ios --latest
EAS will:
Review Time: Typically 24-48 hours, but can take up to 7 days.
.aab file (from EAS build)Review Time: Usually 1-3 days, but can take up to 7 days for first submission.
Before submitting, thoroughly test:
// Test flow
1. Load paywall
2. Click "Buy Premium Unlock"
3. Complete purchase flow
4. Verify entitlement is active
5. Restore purchases (logout/login)
6. Verify entitlement persists
// Test flow
1. Subscribe to monthly plan
2. Verify active subscription
3. Test subscription renewal (use sandbox)
4. Test cancellation flow
5. Test grace period (if configured)
iOS:
Android:
Problem: Product IDs don't match between RevenueCat, App Store Connect, and Google Play.
Solution:
premium_unlock, premium_monthlyProblem: App doesn't recognize user has premium access.
Solution:
// Always check entitlements, not just purchases
const customerInfo = await RevenueCatService.getCustomerInfo();
const isPremium = customerInfo.entitlements.active['premium'] !== undefined;
Problem: EAS build fails due to missing configuration.
Solution:
app.json has correct bundle identifierseas.json configurationProblem: App rejected for missing information.
Solution:
Problem: Products not appearing in app.
Solution:
After Submission:
Apple Developer approval can take 24-48 hours. Start this process on Day 1, even before your app is ready.
Use sandbox testers for both platforms. Test:
Create screenshots, descriptions, and marketing materials while your app is building. Don't wait until submission day.
Check the RevenueCat dashboard regularly for:
Configure RevenueCat webhooks to your backend for:
// Example: Handle network errors gracefully
try {
await RevenueCatService.purchasePackage(pkg);
} catch (error: any) {
if (error.userCancelled) {
// User cancelled - don't show error
} else if (error.code === 'NETWORK_ERROR') {
// Show retry option
} else {
// Show generic error message
}
}
Always provide a "Restore Purchases" button. Users expect this, especially when switching devices.
Don't check for specific product IDs. Check for entitlements instead:
// ❌ Bad: Checking product ID
const hasPremium = customerInfo.activeSubscriptions.includes('premium_monthly');
// ✅ Good: Checking entitlement
const hasPremium = customerInfo.entitlements.active['premium'] !== undefined;
This works for both subscriptions and one-time purchases.
RevenueCat's offerings system lets you create different paywall configurations:
// In RevenueCat dashboard, create offerings:
// - Default: Standard paywall
// - Promo: Special promotion
// - A/B Test: Different pricing
// In your app:
const offering = await RevenueCatService.getOfferings();
const monthlyPackage = offering?.availablePackages.find(
pkg => pkg.identifier === 'monthly'
);
Offer discounts to specific users:
// In RevenueCat dashboard → Promotional Offers
// Create offers for:
// - Win-back campaigns
// - Special promotions
// - User segments
Set up webhooks for server-side processing:
// RevenueCat sends webhooks for:
// - Initial purchase
// - Subscription renewal
// - Cancellation
// - Billing issues
// Example webhook handler (Node.js):
app.post('/webhooks/revenuecat', async (req, res) => {
const event = req.body;
if (event.type === 'INITIAL_PURCHASE') {
// Grant premium access
await grantPremiumAccess(event.app_user_id);
}
if (event.type === 'CANCELLATION') {
// Handle cancellation
await handleCancellation(event.app_user_id);
}
res.status(200).send('OK');
});
RevenueCat provides built-in analytics:
Access these in the RevenueCat dashboard under "Charts" and "Customers".
Once your app is live:
Causes:
Solutions:
Causes:
Solutions:
Causes:
Solutions:
eas.json configurationeas build --localCauses:
Solutions:
Shipping a mobile app to both App Store and Play Store in one week is achievable with Expo and RevenueCat. The key is:
The combination of Expo's simplified build process and RevenueCat's unified payment API eliminates most of the complexity traditionally associated with mobile app development. By following this guide, you can go from zero to live on both stores in 7 days or less.
Remember: The longest wait is usually Apple Developer account approval and store reviews. Start the Apple Developer enrollment immediately, even before your app is ready. Everything else can be done in parallel.
Good luck with your launch! 🚀
This guide covers the complete process from setup to submission. For specific issues or advanced use cases, refer to the official documentation or community forums. The mobile app ecosystem evolves quickly, so always check for the latest best practices and requirements.
Need a build partner?
We deliver production-grade products in 28 days with research, design, engineering, and launch support handled end-to-end. Our team blends RevenueCat integration, App Store submission with senior founders so you can stay focused on growth.
START YOUR NEW PROJECT
WITH DREAMLAUNCH
TODAY!