Skip to main content

RevenueCat vs Adapty vs Superwall: Mobile In-App Purchases 2026

·PkgPulse Team

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

FeatureRevenueCatAdaptySuperwall
Core focusSubscription managementSubscription + paywallsPaywall 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 tierUp to $2,500Up to $2,500$0 free (per event pricing)
iOS + Android✅ (iOS-first)
GitHub stars2.1k220280

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.

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.