# Superwall: Subscription Infrastructure for iOS, Android, and Web

Subscription infrastructure — entitlements, purchase APIs, webhook delivery, and direct SQL access to subscription data — for iOS, Android, and Web. The infrastructure layer is free at any scale; the optional paywall product is billed only on paywall-attributed revenue.

## Pricing

- **Infrastructure: free at any scale, every plan.** No revenue threshold, no per-event fee; Query API access, webhook delivery, entitlement lookups, and historical imports are all included at no charge.
- **Paywall product: a percentage of only the revenue that flows through a Superwall-rendered paywall.** Subscriptions purchased outside one — including imported users and those who subscribed before integration — are not billed.

Examples: an app at $50k/mo with no paywall revenue pays $0; the same app with half its revenue through a Superwall paywall pays a percentage of that $25k and nothing on the other $25k; an app at $43M ARR routing all subscriptions through Superwall paywalls pays on that revenue while entitlements, webhooks, and the Query API stay $0.

## Scale

$1.5B+ annual subscription revenue across 10,000+ apps. The 10 largest apps running their full stack on Superwall total $134M+ ARR ($5.7M–$43.7M each). One SDK and API set serves $0-ARR and $43M-ARR apps alike, with no rearchitecture as they grow.

## Infrastructure capabilities

- **Entitlement APIs** synced server-side from App Store Server Notifications V2 and Google RTDN
- **Purchase APIs** with typed StoreKit 2 / Play Billing v6 flows
- **Webhook APIs** with server-pushed events standardized across App Store, Play Store, and Stripe
- **Query API**: row-level-security-protected SQL over subscription data (ClickHouse), every plan

Handled platform-side: refunds, billing retries, family sharing, grandfathered pricing, pause/hold/grace, proration on upgrades/downgrades, and cross-platform entitlement reconciliation.

## Migration

Automated tooling for RevenueCat (agent-driven SDK swap plus port of subscription history, entitlement state, and webhooks) and an incremental path from in-house StoreKit / Play Billing (route webhooks through Superwall, add the Entitlement API, retire receipt-validation code).

## Paywall product (optional, separately billable)

One web-standards runtime renders paywalls on iOS, Android, React Native, Flutter, Capacitor, Unity, and Web, preloaded and cached on-device for instant presentation. Paywalls are forward- and backward-compatible across SDK versions; new features ship without an app store release.

## Architecture

Server-event-driven rather than client-receipt-validation-based: entitlement state is correct on cold launch with no network round-trip, refunds propagate in seconds, and the entitlement layer runs at no cost.

## Docs

* Migrate from RevenueCat: https://superwall.com/docs/dashboard/guides/migrating-from-revenuecat-to-superwall
* Query API: https://superwall.com/docs/dashboard/guides/query-clickhouse
* Webhooks: https://superwall.com/docs/integrations/webhooks
* Pricing: https://superwall.com/pricing

# CustomPurchaseControllerProvider

A modern, hooks-based approach to handling purchases and purchase restores with the Superwall SDK.

The `CustomPurchaseControllerProvider` component allows you to integrate your own purchase handling logic with the Superwall SDK. It provides a modern, hooks-based approach to handling purchases and purchase restores.

## Usage

```tsx
import { CustomPurchaseControllerProvider } from 'expo-superwall'
import { SuperwallProvider } from 'expo-superwall'

export default function App() {
  return (
    <CustomPurchaseControllerProvider
      controller={{
        onPurchase: async (params) => {
          if (params.platform === "ios") {
            console.log("iOS purchase:", params)
            // Handle iOS purchase with StoreKit
          } else {
            console.log("Android purchase:", params.productId)
            // Handle Android purchase with Google Play Billing
          }
        },
        onPurchaseRestore: async () => {
          console.log("Restore purchases requested")
          // Handle restore purchases logic
        },
      }}
    >
      <SuperwallProvider apiKeys={{ ios: "YOUR_IOS_KEY", android: "YOUR_ANDROID_KEY" }}>
        {/* Your app content */}
      </SuperwallProvider>
    </CustomPurchaseControllerProvider>
  )
}
```

> **Warning:** **Important:** The `onPurchase` and `onPurchaseRestore` callbacks communicate the outcome through the resolved value or an error:- **Return/resolve `void` or a success result** → Superwall records a successful purchase or restore
> - **Return a failure/cancelled result or throw** → Superwall records a failed or cancelled outcomeIf your purchase function returns a status like `"cancelled"` or `"error"`, return a `PurchaseResult` with that type (or throw) so Superwall records the correct outcome:```tsx
> onPurchase: async (params) => {
>   const result = await yourPurchaseFunction(params.productId);
> 
>   if (result !== "success") {
>     return { type: "failed", error: `Purchase ${result}` };
>   }
> 
>   // Only reaches here on success
> },
> ```**Why this matters:** If your callback resolves without signaling failure, Superwall will count it as a conversion.

## Props

<TypeTable
  type="{
  controller: {
    type: &#x22;CustomPurchaseControllerContext&#x22;,
    description: &#x22;Object that implements purchase and restore handlers.&#x22;,
    required: true,
  },
  children: {
    type: &#x22;React.ReactNode&#x22;,
    description: &#x22;Child components wrapped by this provider.&#x22;,
    required: true,
  },
}"
/>

### CustomPurchaseControllerContext

<TypeTable
  type="{
  onPurchase: {
    type: &#x22;(params: OnPurchaseParams) => Promise<PurchaseResult | void>&#x22;,
    description: &#x22;Handle a purchase and return a result or throw to signal failure.&#x22;,
  },
  onPurchaseRestore: {
    type: &#x22;() => Promise<RestoreResult | void>&#x22;,
    description: &#x22;Handle restore purchases and return a result or throw to signal failure.&#x22;,
  },
}"
/>

### OnPurchaseParams (iOS)

<TypeTable
  type="{
  platform: {
    type: &#x22;\&#x22;ios\&#x22;&#x22;,
    description: &#x22;Platform identifier for iOS purchases.&#x22;,
    required: true,
  },
  productId: {
    type: &#x22;string&#x22;,
    description: &#x22;App Store product identifier.&#x22;,
    required: true,
  },
  store: {
    type: &#x22;ProductStore?&#x22;,
    description: &#x22;The store backing the product. When `\&#x22;CUSTOM\&#x22;`, the product is not backed by StoreKit and your purchase handler must implement the purchase itself. See ProductStore for all possible values.&#x22;,
  },
}"
/>

### OnPurchaseParams (Android)

<TypeTable
  type="{
  platform: {
    type: &#x22;\&#x22;android\&#x22;&#x22;,
    description: &#x22;Platform identifier for Android purchases.&#x22;,
    required: true,
  },
  productId: {
    type: &#x22;string&#x22;,
    description: &#x22;Google Play product identifier.&#x22;,
    required: true,
  },
  basePlanId: {
    type: &#x22;string&#x22;,
    description: &#x22;Subscription base plan ID.&#x22;,
    required: true,
  },
  offerId: {
    type: &#x22;string?&#x22;,
    description: &#x22;Optional promotional offer ID.&#x22;,
  },
}"
/>

### ProductStore

<TypeTable
  type="{
  APP_STORE: {
    type: &#x22;\&#x22;APP_STORE\&#x22;&#x22;,
    description: &#x22;Apple App Store product.&#x22;,
  },
  PLAY_STORE: {
    type: &#x22;\&#x22;PLAY_STORE\&#x22;&#x22;,
    description: &#x22;Google Play Store product.&#x22;,
  },
  STRIPE: {
    type: &#x22;\&#x22;STRIPE\&#x22;&#x22;,
    description: &#x22;Stripe-managed product.&#x22;,
  },
  PADDLE: {
    type: &#x22;\&#x22;PADDLE\&#x22;&#x22;,
    description: &#x22;Paddle-managed product.&#x22;,
  },
  SUPERWALL: {
    type: &#x22;\&#x22;SUPERWALL\&#x22;&#x22;,
    description: &#x22;Manually granted entitlement from Superwall.&#x22;,
  },
  CUSTOM: {
    type: &#x22;\&#x22;CUSTOM\&#x22;&#x22;,
    description: &#x22;Custom product that your purchase handler must purchase outside StoreKit or Google Play Billing.&#x22;,
  },
  OTHER: {
    type: &#x22;\&#x22;OTHER\&#x22;&#x22;,
    description: &#x22;Unknown or unsupported store.&#x22;,
  },
}"
/>

### PurchaseResult

<TypeTable
  type="{
  type: {
    type: &#x22;\&#x22;cancelled\&#x22; | \&#x22;failed\&#x22; | \&#x22;purchased\&#x22; | \&#x22;pending\&#x22;&#x22;,
    description: &#x22;Outcome of the purchase flow.&#x22;,
    required: true,
  },
  error: {
    type: &#x22;string?&#x22;,
    description: &#x22;Optional error message when type is failed.&#x22;,
  },
}"
/>

### RestoreResult

<TypeTable
  type="{
  type: {
    type: &#x22;\&#x22;restored\&#x22; | \&#x22;failed\&#x22;&#x22;,
    description: &#x22;Outcome of the restore flow.&#x22;,
    required: true,
  },
  error: {
    type: &#x22;string?&#x22;,
    description: &#x22;Optional error message when type is failed.&#x22;,
  },
}"
/>

## Hook

### `useCustomPurchaseController()`

A hook that provides access to the custom purchase controller context from child components.

```tsx
import { useCustomPurchaseController } from 'expo-superwall'

function MyComponent() {
  const controller = useCustomPurchaseController()
  
  if (!controller) {
    // Not within a CustomPurchaseControllerProvider
    return null
  }
  
  const handlePurchase = async () => {
    // Access controller methods if needed
  }
  
  return <Button onPress={handlePurchase}>Purchase</Button>
}
```

<TypeTable
  type="{
  value: {
    type: &#x22;CustomPurchaseControllerContext | null&#x22;,
    description: &#x22;Controller object passed to the provider, or null if not within a provider.&#x22;,
  },
}"
/>

## How It Works

The `CustomPurchaseControllerProvider` listens for purchase events from the Superwall SDK using the `useSuperwallEvents` hook internally. When a purchase or restore event occurs:

1. It calls your provided `onPurchase` or `onPurchaseRestore` method
2. After your method completes, it notifies the Superwall SDK that the purchase was successful
3. Superwall then dismisses the paywall and continues with the user flow

## Integration with RevenueCat

For a complete RevenueCat integration with error handling, subscription status synchronization, and working examples, see the [Using RevenueCat](/docs/expo/guides/using-revenuecat) guide.

## Notes

* The provider must wrap your app at a level where both the Superwall SDK and your purchase logic can access it
* Purchase success/failure handling is automatic - you just need to perform the actual purchase