# 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 [#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 [#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 [#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) [#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) [#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 [#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 [#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 [#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 [#hook]

`useCustomPurchaseController()` [#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 [#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 [#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 [#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