Purchases

Make the sale with usePurchase — handle completed, abandoned, and failed outcomes, restore purchases, and react to transactions from anywhere.

A purchase is one call: pass a product reference, await the result, react to what happened. The SDK owns the store sheet, the payment, and the receipt.

import { usePurchase, useHaptics } from "superwall/hooks";

const { purchase } = usePurchase();
const haptics = useHaptics();

<button
  onClick={async () => {
    haptics.light();
    const result = await purchase("annual");
    if (result.status === "completed") haptics.success();
  }}
>
  Subscribe
</button>

The three outcomes

purchase() resolves — it never throws for flow outcomes:

StatusMeaningRespond by
completedThe sale went throughhaptics.success(); the SDK dismisses the paywall if configured
abandonedThe user closed the store sheetTreat as an ordinary outcome — most people who open a sheet close it. This is the only place this paywall's own declined offer is visible: show a last-chance offer, or nothing
failedNo transaction happened — reason is "timeout" or "superseded" (a retry or re-presentation replaced this attempt)Usually nothing; haptics.error() at most

Never put the buy button in a loading state

No "One moment…", no disabling, no spinner. The store sheet is the feedback, and the SDK owns when it appears. A button that visibly waits makes the paywall feel broken in the gap the platform already covers.

Abandoned is a signal, not a failure

Someone opened the sheet and closed it — that's the closest thing a paywall gets to hearing "not at this price." A common pattern is pushing a last-chance offer:

const result = await purchase(selected);
if (result.status === "abandoned") {
  router.push("offer", { transition: "sheet" });
}

One recovery offer, not two. If the user abandons the discounted offer as well, let them be. The abandonment-offer example shows the full pattern — a second product, not a second design.

Options

purchase(reference, { shouldDismiss?, timeoutMs? })

Both default to what config.ts declares (dismissOnPurchase, purchaseTimeoutMs).

The two channels

Your purchase() call is one channel. The SDK reporting on its own is the other — and it reports transactions whoever started them. A successful restore arrives as a transaction_complete event with no purchase call in sight.

// this paywall's own attempt
const result = await purchase("annual");

// anything the SDK reports — purchase, restore, trial start
useSuperwallEvent("transaction_complete", () => haptics.success());
useSuperwallEvent("freeTrial_start", () => {});

Drive this paywall's flow from the awaited result; use events for side effects that should fire on any transaction, however it started. The purchase-states example shows both channels side by side — and it's the one example that demonstrates the full haptic vocabulary (success() and error() keyed to outcomes).

See Lifecycle & events for the full event list.

Restore

import { useActions, useHaptics } from "superwall/hooks";

const { restore } = useActions();

<button onClick={() => { haptics.light(); restore(); }}>
  Restore purchases
</button>

restore() is fire-and-forget — there is no result to await. Success surfaces as a transaction_complete event or a dismissed paywall. Every store paywall should offer restore — App Review expects it.

Haptics on outcomes

iOS fires no feedback of its own inside a paywall, so the vocabulary is yours to supply:

  • haptics.light() when the buy button is tapped
  • haptics.success() when a transaction completes — via the event, so restores count too
  • haptics.error() sparingly, on failed

Selling beyond the App Store

Trials — who's eligible, what to show each side — have their own page: Free trials. And a single config key sells the same paywall on the web through Stripe, with purchase() unchanged: Web checkout.

How is this guide?

On this page