RevenueCat vs Adapty vs Superwall: Mobile In-App Purchases 2026
RevenueCat vs Adapty vs Superwall: Mobile In-App Purchases 2026
TL;DR
Managing in-app purchases across iOS and Android is one of the hardest parts of mobile development — StoreKit 2, Google Play Billing, receipt validation, subscription state, and paywall UX all need to work together. RevenueCat is the market leader — a battle-tested SDK that abstracts iOS and Android purchase APIs into one unified layer, with real-time subscription analytics, webhooks, and a no-code paywall builder. Adapty is the feature-rich challenger — RevenueCat-compatible API with more aggressive paywall A/B testing, lower pricing tiers, and a built-in profile-based targeting system. Superwall is the paywall-first platform — focused entirely on converting free users to paid through a powerful no-code paywall builder with declarative trigger logic, while outsourcing subscription management to RevenueCat or StoreKit directly. For a complete subscription management + analytics solution: RevenueCat. For paywall A/B testing on a budget: Adapty. For supercharged paywall experiments with existing purchase infrastructure: Superwall.
Key Takeaways
- RevenueCat processes $5B+ in annual subscriptions — most proven at scale
- Adapty paywalls support 30+ A/B test variants — more testing flexibility than RevenueCat
- Superwall trigger system — show paywalls based on events, user properties, and custom logic
- RevenueCat Entitlements — cross-platform subscription state in one call
- Adapty pricing starts lower — first $2,500 MRR tracked is free vs RevenueCat's $2,500 cap
- Superwall delegates purchases to RevenueCat/StoreKit — not a full purchase management replacement
- All three support React Native — via official SDKs
Why In-App Purchase Infrastructure Matters
Without a purchase SDK — manual approach:
iOS: StoreKit 2 (Swift-only, async/await)
Android: Google Play Billing Library (Kotlin)
Server: Receipt validation for each platform
State: Track subscription status across platforms
Edge cases: Family sharing, refunds, billing retries, grace periods
Reality: 6-12 months of work, constant OS update maintenance
With RevenueCat/Adapty:
One SDK → unified purchase flow
Server-side receipt validation (automatic)
Subscription state: Purchases.getCustomerInfo()
Webhooks: Send events to Mixpanel, Amplitude, Slack
Paywalls: No-code builder for non-engineers
RevenueCat: The Standard for Mobile Subscriptions
RevenueCat normalizes StoreKit and Google Play Billing into one API, handles server-side validation, and provides real-time subscription analytics.
Installation
npm install react-native-purchases
npx pod-install # iOS
Setup
// App.tsx
import Purchases, { LOG_LEVEL } from "react-native-purchases";
import { useEffect } from "react";
export default function App() {
useEffect(() => {
// Configure RevenueCat with platform-specific keys
Purchases.setLogLevel(LOG_LEVEL.VERBOSE);
if (Platform.OS === "ios") {
Purchases.configure({ apiKey: process.env.EXPO_PUBLIC_RC_IOS_KEY! });
} else if (Platform.OS === "android") {
Purchases.configure({ apiKey: process.env.EXPO_PUBLIC_RC_ANDROID_KEY! });
}
}, []);
return <RootNavigator />;
}
Check Subscription Status (Entitlements)
import Purchases, { CustomerInfo } from "react-native-purchases";
// Entitlement = what the user has access to (maps to products in each store)
async function checkSubscriptionStatus(): Promise<boolean> {
try {
const customerInfo: CustomerInfo = await Purchases.getCustomerInfo();
// Check if user has an active entitlement named "premium"
const isPremium = customerInfo.entitlements.active["premium"] !== undefined;
return isPremium;
} catch (error) {
console.error("Error fetching customer info:", error);
return false;
}
}
// Hook for subscription state
function usePremiumStatus() {
const [isPremium, setIsPremium] = useState(false);
const [loading, setLoading] = useState(true);
useEffect(() => {
checkSubscriptionStatus().then((status) => {
setIsPremium(status);
setLoading(false);
});
// Listen for subscription changes
const listener = Purchases.addCustomerInfoUpdateListener((info) => {
setIsPremium(info.entitlements.active["premium"] !== undefined);
});
return () => listener.remove();
}, []);
return { isPremium, loading };
}
Fetch Offerings and Purchase
import Purchases, { PurchasesOffering, PurchasesPackage } from "react-native-purchases";
// Offerings = groups of packages defined in RevenueCat dashboard
async function getOfferings(): Promise<PurchasesOffering | null> {
const offerings = await Purchases.getOfferings();
return offerings.current; // The "current" offering (A/B test or default)
}
// Purchase a package
async function purchasePackage(pkg: PurchasesPackage): Promise<boolean> {
try {
const { customerInfo } = await Purchases.purchasePackage(pkg);
const isPremium = customerInfo.entitlements.active["premium"] !== undefined;
return isPremium;
} catch (error: any) {
if (!error.userCancelled) {
console.error("Purchase error:", error);
}
return false;
}
}
// Paywall component using offerings
function PaywallScreen() {
const [offering, setOffering] = useState<PurchasesOffering | null>(null);
useEffect(() => {
getOfferings().then(setOffering);
}, []);
if (!offering) return <ActivityIndicator />;
return (
<View>
<Text>Choose Your Plan</Text>
{offering.availablePackages.map((pkg) => (
<TouchableOpacity
key={pkg.identifier}
onPress={() => purchasePackage(pkg)}
>
<Text>{pkg.product.title}</Text>
<Text>{pkg.product.priceString} / {pkg.packageType}</Text>
</TouchableOpacity>
))}
</View>
);
}
Restore Purchases and Identify Users
// Restore purchases (required by App Store guidelines)
async function restorePurchases() {
const customerInfo = await Purchases.restorePurchases();
return customerInfo.entitlements.active["premium"] !== undefined;
}
// Link RevenueCat user to your app's user ID
async function identifyUser(userId: string) {
await Purchases.logIn(userId);
// Now subscription state is tied to your user ID — syncs across devices
}
// Anonymous users on sign-out
async function logOut() {
await Purchases.logOut();
// User becomes anonymous again
}
RevenueCat Paywall Builder (No-Code)
import RevenueCatUI, { PAYWALL_RESULT } from "react-native-purchases-ui";
// Present pre-built paywall from RevenueCat dashboard
async function presentPaywall() {
const result = await RevenueCatUI.presentPaywall();
switch (result) {
case PAYWALL_RESULT.PURCHASED:
console.log("User purchased!");
break;
case PAYWALL_RESULT.RESTORED:
console.log("Purchases restored");
break;
case PAYWALL_RESULT.CANCELLED:
console.log("User cancelled");
break;
}
}
// Or as a component
function PremiumGate({ children }: { children: React.ReactNode }) {
const { isPremium } = usePremiumStatus();
if (isPremium) return <>{children}</>;
return (
<RevenueCatUI.Paywall
onDismiss={() => {/* handle dismiss */}}
/>
);
}
Adapty: Feature-Rich RevenueCat Alternative
Adapty offers a RevenueCat-compatible API with stronger paywall A/B testing, profile-based targeting, and more aggressive pricing for early-stage apps.
Installation
npm install react-native-adapty
npx pod-install # iOS
Setup and Activation
// App.tsx
import { activateAdapty } from "react-native-adapty";
export default function App() {
useEffect(() => {
activateAdapty({
apiKey: process.env.EXPO_PUBLIC_ADAPTY_KEY!,
logLevel: "verbose", // "error" | "warn" | "info" | "verbose" | "all" | "none"
});
}, []);
return <RootNavigator />;
}
Identify and Profile Users
import { adapty, AdaptyProfile } from "react-native-adapty";
// Identify user — links subscription state across devices
async function identifyUser(userId: string) {
const profile = await adapty.identify(userId);
return profile;
}
// Set user attributes for targeting
async function setUserAttributes() {
await adapty.updateProfile({
firstName: "John",
lastName: "Doe",
email: "john@example.com",
birthday: new Date("1990-01-15"),
customAttributes: {
plan: "free",
daysActive: 7,
onboardingComplete: true,
},
});
}
// Check access level (equivalent to RevenueCat entitlements)
async function checkPremiumAccess(): Promise<boolean> {
const profile: AdaptyProfile = await adapty.getProfile();
return profile.accessLevels["premium"]?.isActive ?? false;
}
Fetch Paywalls and Make Purchases
import { adapty, AdaptyPaywall, AdaptyProduct } from "react-native-adapty";
// Get paywall by ID (defined in Adapty dashboard)
async function getPaywall(placementId: string): Promise<AdaptyPaywall> {
return await adapty.getPaywall(placementId);
}
// Get products for a paywall
async function getProducts(paywall: AdaptyPaywall): Promise<AdaptyProduct[]> {
return await adapty.getPaywallProducts(paywall);
}
// Make a purchase
async function purchase(product: AdaptyProduct): Promise<boolean> {
try {
const profile = await adapty.makePurchase(product);
return profile.accessLevels["premium"]?.isActive ?? false;
} catch (error) {
console.error("Purchase failed:", error);
return false;
}
}
// Restore purchases
async function restore(): Promise<boolean> {
const profile = await adapty.restorePurchases();
return profile.accessLevels["premium"]?.isActive ?? false;
}
Adapty Paywall UI (Visual Builder)
import { AdaptyPaywallView } from "react-native-adapty";
// Render an Adapty-built paywall (designed in their visual editor)
function AdaptyPaywallScreen() {
const [paywall, setPaywall] = useState<AdaptyPaywall | null>(null);
const [products, setProducts] = useState<AdaptyProduct[]>([]);
useEffect(() => {
async function load() {
const pw = await adapty.getPaywall("main-paywall");
const prods = await adapty.getPaywallProducts(pw);
setPaywall(pw);
setProducts(prods);
}
load();
}, []);
if (!paywall) return null;
return (
<AdaptyPaywallView
paywall={paywall}
products={products}
onPurchaseCompleted={(profile) => {
const isPremium = profile.accessLevels["premium"]?.isActive;
if (isPremium) navigation.goBack();
}}
onRestoreCompleted={(profile) => {
console.log("Restored:", profile);
}}
onClose={() => navigation.goBack()}
/>
);
}
Superwall: Paywall-First Conversion Platform
Superwall is focused on one thing: maximizing subscription conversion through powerful paywall triggers and A/B testing. It delegates actual purchases to RevenueCat or StoreKit directly.
Installation
npm install @superwall/react-native-superwall
npx pod-install # iOS
Setup with RevenueCat
// Superwall works alongside RevenueCat — they are complementary, not competing
import Superwall from "@superwall/react-native-superwall";
import Purchases from "react-native-purchases";
// Purchase controller — Superwall delegates purchases to RevenueCat
class RCPurchaseController implements SuperwallDelegate {
async purchase(product: StoreProduct): Promise<PurchaseResult> {
try {
const pkg = await findRevenueCatPackage(product.productIdentifier);
const { customerInfo } = await Purchases.purchasePackage(pkg);
if (customerInfo.entitlements.active["premium"]) {
return { result: "purchased" };
}
return { result: "failed", error: new Error("Entitlement not active") };
} catch (error: any) {
if (error.userCancelled) return { result: "cancelled" };
return { result: "failed", error };
}
}
async restorePurchases(): Promise<RestorationResult> {
const customerInfo = await Purchases.restorePurchases();
return customerInfo.entitlements.active["premium"]
? { result: "restored" }
: { result: "failed", error: new Error("Nothing to restore") };
}
}
// Initialize both SDKs
export async function initializePurchases(userId: string) {
// RevenueCat for purchase management
Purchases.configure({ apiKey: process.env.EXPO_PUBLIC_RC_IOS_KEY! });
await Purchases.logIn(userId);
// Superwall for paywall display + triggers
const purchaseController = new RCPurchaseController();
await Superwall.configure(
process.env.EXPO_PUBLIC_SUPERWALL_KEY!,
{ purchaseController }
);
await Superwall.shared.identify({ userId });
await Superwall.shared.setUserAttributes({
plan: "free",
daysActive: 0,
});
}
Trigger-Based Paywalls
import Superwall from "@superwall/react-native-superwall";
// Register events in Superwall dashboard, then trigger by name
// No need to check subscription state — Superwall handles gating
async function trackEvent(eventName: string, params?: Record<string, unknown>) {
await Superwall.shared.register(eventName, params);
}
// Feature gate with paywall
async function accessPremiumFeature() {
// Superwall checks if user is subscribed:
// - If yes: executes feature immediately
// - If no: shows paywall defined in dashboard, then executes on purchase
await Superwall.shared.register("premium_feature_accessed", {
featureName: "advanced_analytics",
source: "home_screen",
});
// This callback only fires if user has/gets access
// Superwall calls this after successful purchase or if already subscribed
navigateToAnalytics();
}
// Custom handler with callbacks
async function openPremiumWithCallbacks() {
await Superwall.shared.register(
"export_data",
{ format: "csv" },
{
onPresent: (paywallInfo) => {
analytics.track("Paywall Shown", { id: paywallInfo.identifier });
},
onPurchase: () => {
analytics.track("Paywall Converted");
},
onRestore: () => {
analytics.track("Purchases Restored");
},
onDismiss: (paywallInfo, result) => {
analytics.track("Paywall Dismissed", { result });
},
}
);
}
Superwall Attributes for Targeting
// Set attributes to target paywalls to specific user segments
async function updateSuperwallAttributes(user: User) {
await Superwall.shared.setUserAttributes({
// Standard attributes
email: user.email,
name: user.name,
// Custom attributes for targeting logic
daysActive: daysSinceSignup(user.createdAt),
sessionsCount: user.sessionCount,
hasCompletedOnboarding: user.onboardingComplete,
plan: user.plan,
referralSource: user.utmSource,
// Feature usage — trigger paywall when hitting limits
reportsCreated: user.stats.reports,
exportsThisMonth: user.stats.exports,
});
}
// Superwall dashboard: "Show paywall_B to users where daysActive >= 3 AND reportsCreated >= 2"
// No code changes needed — all logic in dashboard
Feature Comparison
| Feature | RevenueCat | Adapty | Superwall |
|---|---|---|---|
| Core focus | Subscription management | Subscription + paywalls | Paywall conversion |
| Manages purchases | ✅ | ✅ | ❌ (delegates to RC/StoreKit) |
| Paywall builder | ✅ | ✅ | ✅ Most advanced |
| A/B testing | ✅ Basic | ✅ 30+ variants | ✅ Advanced |
| Analytics | ✅ Revenue analytics | ✅ Revenue + funnel | ✅ Paywall-focused |
| Trigger system | ❌ | ❌ | ✅ Event-based triggers |
| Webhooks | ✅ | ✅ | ✅ |
| React Native SDK | ✅ | ✅ | ✅ |
| Cross-platform state | ✅ Entitlements | ✅ Access levels | ❌ (via RC) |
| Server-side validation | ✅ | ✅ | ❌ (via RC) |
| MRR free tier | Up to $2,500 | Up to $2,500 | $0 free (per event pricing) |
| iOS + Android | ✅ | ✅ | ✅ (iOS-first) |
| GitHub stars | 2.1k | 220 | 280 |
When to Use Each
Choose RevenueCat if:
- You want the most mature, battle-tested subscription infrastructure
- Cross-platform subscription state (iOS + Android) is critical
- Integration with third-party analytics (Amplitude, Mixpanel, Slack) via webhooks
- The paywall builder is sufficient for your needs
- You want the largest community and most Stack Overflow answers
Choose Adapty if:
- RevenueCat-compatible API but lower cost at early MRR levels
- More aggressive paywall A/B testing (30+ variants vs RevenueCat's basic tests)
- Profile-based targeting — show different paywalls based on user attributes
- You want to test an alternative to RevenueCat without rewriting all purchase code
Choose Superwall if:
- Your existing RevenueCat setup handles purchases, but conversion is low
- You want to run sophisticated paywall experiments without engineering changes
- Event-based paywall triggers — "show paywall when user creates 3rd project"
- Paywall personalization by user segment, usage pattern, or acquisition source
- Marketing/product teams want control over paywall copy/design without deploys
Methodology
Data sourced from official RevenueCat documentation (rev.cat/docs), Adapty documentation (docs.adapty.io), Superwall documentation (docs.superwall.com), GitHub star counts as of February 2026, pricing pages as of February 2026, and community discussions from the RevenueCat community forums, Indie Hackers, and r/iOSProgramming.
Related: Expo EAS vs Fastlane vs Bitrise for the CI/CD pipeline that builds and ships the apps using these SDKs, or React Native MMKV vs AsyncStorage vs Expo SecureStore for persisting subscription state locally.