Hooks Reference
Every hook the framework provides, signatures, what each returns, and the semantics that matter.
Everything a paywall reads or triggers comes through hooks. One concern each. There is deliberately no kitchen-sink hook.
import {
useProducts, usePurchase, useDiscount, useActions, useCheckoutRedemption, useHaptics,
useTranslation, useIntroductoryOffer, useDevice, useUser, useUserAttribute, useVariables,
useColorScheme, useSuperwallEvent, useSuperwallSnapshot, useSuperwallSession,
readAttribute, parseAsStringEnum,
type ProductReference,
} from "superwall/hooks";
import { useRouter, useIsFocused, replayStack } from "superwall/navigation";useProducts()
const { products, getProduct } = useProducts();
const annual = getProduct("annual"); // typed reference — typos are compile errorsProducts, keyed by the reference declared in config.ts, each carrying store-owned variables (price, period, trialPeriodDays, …). A declared reference always exists, but its variables may not have arrived, guard every read and design the empty state. The full variable list and reading rules are in Products.
usePurchase()
const { purchase, prefetch, isPurchasing, transaction, failure } = usePurchase();
const result = await purchase("annual");
// { status: "completed" | "abandoned" | "failed" } — never throws for flow outcomesThe whole purchase flow (outcomes, the no-loading-state rule, web checkout, and prefetch) is in Purchases.
useDiscount(options?)
const { redeem, clear, applied, isRedeeming, getDiscountedProduct } = useDiscount({
onDropped: (code) => toast(`${code} is no longer valid`),
});
const result = await redeem(code); // { valid: true, … } | { valid: false, reason }
const annual = getDiscountedProduct("annual"); // discounted prices, or undefinedStripe promotion codes for web checkout. A redeemed code rides on every checkout session for the products it applies to. applied.discount is Stripe's coupon under Stripe's own field names (percentOff, amountOff, duration, durationInMonths), and each product carries only what had to be computed: amount, discountedAmount and savings in minor units, plus price, discountedPrice and savingsPrice formatted for the locale. Only meaningful with checkout: "sheet" or "applePay". Web checkout has the flow.
useActions()
const { close, restore, openUrl, requestPermission, requestCallback } = useActions();Also on the object: openExternalUrl, openDeepLink, customPlacement, requestStoreReview, and the deprecated setUserAttributes (write attributes with useUserAttribute instead). Everything a paywall asks its host to do. Actions has the full table, the permission types, and the callback pattern.
useCheckoutRedemption()
const { completed, redemptionUrl, appLink, redeem, redirect, finish, openApp } = useCheckoutRedemption();Finishes a web checkout the paywall chose to keep with postPurchase: "stay". completed is false until such a purchase lands; then render your own success screen and call redeem() when the shopper taps continue, openApp() to jump straight into the installed app through appLink (show that button only when appLink is set), redirect(url) to send them somewhere of yours, or finish() to do whatever the paywall's stored behavior says. On any other postPurchase the host finishes the purchase itself and completed stays false. Only meaningful with web checkout.
useTeleport()
const teleport = useTeleport({ onOpen, onReturn, onCancel, onComplete });
// teleport.status: "idle" | "waiting" | "arrived" | "completed"
// teleport.prefetch(route?), teleport.open(), teleport.cancel(), teleport.returnToApp()Where a teleport stands, as one typed status. In the app, "waiting" from when Superwall's teleport screen would go up until the purchase settles, with productReference, returned once the shopper is back, and, once the store priced it, fallbackReference to buy with purchase(); open() and cancel() act on it, and prefetch(route) prepares a teleport ahead of router.push(route, { teleport: true }). On the web page, "arrived" with the productReference tapped in the app, "completed" once paid (with paidElsewhere), and returnToApp() to go back. The optional callbacks fire as the teleport opens, the shopper returns, the shopper taps Cancel, and it is paid. Only meaningful with teleport in the config.
useHaptics()
const haptics = useHaptics();
haptics.light(); // navigation, CTAs
haptics.selection(); // changing a choice
haptics.success(); // purchase landedAlso available: medium, heavy, warning, error. Fire one on every meaningful tap, iOS produces no feedback of its own inside a paywall. No-ops where haptics are unavailable, so call them unconditionally.
useTranslation()
const { t, locale, setLocale, locales } = useTranslation();
t("paywall.cta", { price });Localized copy from messages/<locale>.ts catalogs, the catalog system, fallback rules, and interpolation are in Localization.
useIntroductoryOffer()
const { eligible } = useIntroductoryOffer(); // boolean | undefined — the store decidesIntroductory-offer eligibility, the same signal the visual editor calls hasIntroductoryOffer. undefined until the SDK reports, so gate trial-only UI on eligible === true. Splits the paywall into eligible and ineligible versions, both must read as intentional. See Free trials.
useVariables()
const { device, user, params } = useVariables();Everything the app and SDK told this paywall about the presentation: the SDK-filled device record, user attributes your app set, and the placement's params. All three are host-filled, so guard every read. The records, fields, and guarding doctrine are in Variables & personalization.
useUser()
const user = useUser();Shorthand for useVariables().user when the device and params records aren't needed. The values are raw; to read or write one attribute as a typed value, use useUserAttribute.
useUserAttribute(key, parser?)
import { parseAsInteger, parseAsStringEnum, useUserAttribute } from "superwall/hooks";
const goals = parseAsStringEnum(["lose", "build", "endure"]);
const [goal, setGoal] = useUserAttribute("onboarding_goal", goals); // "lose" | "build" | "endure" | null
const [age, setAge] = useUserAttribute("age", parseAsInteger.withDefault(0)); // number
const [name] = useUserAttribute("first_name"); // string | null
setGoal("build"); // sent to the host, re-rendered at once
setAge((previous) => previous + 1); // an updater gets the current valueOne user attribute as typed state: useState that the host SDK keeps across presentations and app launches, and that campaigns and analytics can read. It is useQueryState's API with the same parsers (parseAsString, parseAsInteger, parseAsFloat, parseAsBoolean, parseAsStringEnum, parseAsArrayOf, createParser, .withDefault()), exported from superwall/hooks as well as superwall/navigation. This is the way to read and write an attribute — an onboarding answer, a captured email, a preference.
- The value is parsed. Anything the parser can't read is
null: a missing attribute, an option the paywall no longer offers, a value the app set under the same key, an object, the strings"null"and"undefined". With.withDefault(value)it is that value instead, andnullleaves the type. A number or boolean the app set reads as its string form, soparseAsIntegerreads an app's30as30. - The setter re-renders at once. It serializes with the parser, sends the attribute to the host (which persists it the same as
Superwall.shared.setUserAttributes), and the paywall's own value updates in the same event, so a selected card turns on the frame the haptic fires. Updaters called in one event each see the last write. Writing the value already stored sends nothing. The setter is stable across renders, safe in effect dependencies. - There is no
null. A paywall can't remove an attribute, so the setter doesn't takenull; one passed at runtime is ignored with a console warning, never stored as the string"null". Design "start over" as answering again.
readAttribute(user, key, parser?)
import { readAttribute } from "superwall/hooks";
export const entry: Entry = ({ user }) =>
readAttribute(user, "onboarding_goal", goals) ? ["index", "goal", "level"] : "index";The same read as useUserAttribute, as a plain function, for entry and anywhere outside a component. It shares the hook's parsing, so the two can never disagree about a value.
useDevice()
const { orientation, hostPlatform, cutout, deviceModel, simulatorModel } = useDevice();The same device record as useVariables().device, plus orientation ("portrait" | "landscape"), measured in the page, so it updates the moment the device turns, and hostPlatform ("ios" | "android" | "web" | undefined), the SDK showing the paywall, normalized and known from the first render. On iOS SDK 4.18.1 or later, simulatorModel identifies the model emulated by an iOS Simulator; it is absent on real devices. Branch platform-specific UI on hostPlatform, never on the raw platform string. cutout says what an iPhone's screen edge holds: "island", "notch", "none", or "strip" for iPhone Duo, whose camera strip moves between the top and the right; it is undefined off iPhone. Lay out with the inset variables rather than cutout. See Variables & personalization.
useLocalResource(id, fallback)
import hero from "@/assets/hero.mp4";
const src = useLocalResource("hero-video", hero);
<video src={src} autoPlay muted loop playsInline />The URL to load for a resource the host app bundled: swlocal://hero-video when the app registered that id (SuperwallOptions.localResources on iOS), otherwise fallback. The same code works on the web, in the studio, and on SDKs without local resources. useLocalResources() is the underlying record: { ids, has(id), resolve(id, fallback) }. See Assets.
useColorScheme()
const scheme = useColorScheme(); // "light" | "dark"Rarely needed: the framework already keeps a dark/light class on <html> from what the device reports, so style with plain CSS (:root.dark { … }). Reach for the hook only when you need the scheme in JavaScript. Never use @media (prefers-color-scheme: dark) as the mechanism, see Styling.
useSuperwallEvent(name, handler)
useSuperwallEvent("transaction_complete", () => haptics.success());Typed SDK events, subscribed for the component's lifetime; an inline arrow handler is fine. The event list and when each fires: Lifecycle & events. For anything a dedicated hook covers (products, trial, variables), use the hook. It cannot miss data that arrived before your component subscribed.
useSuperwallSnapshot()
const snapshot = useSuperwallSnapshot();
const opened = snapshot.paywall !== undefined;The whole runtime state as one subscribed object. Its most common use is gating entry animations on presentation, paywalls are preloaded hidden, and snapshot.paywall flips when the paywall is actually shown (Lifecycle & events). It also carries experiment (the A/B assignment), locale, and the current purchase and transaction state.
useSuperwallSession()
const session = useSuperwallSession();
session.postMessage({ event_name: "custom", data: "…" });The full session for advanced work: the few methods no hook surfaces (raw protocol messaging, accept) and use outside React components. If you're reaching for it for products, purchases, actions, user attributes, or events, use the dedicated hook instead.
useRouter()
import { useRouter } from "superwall/navigation";
const router = useRouter();
router.push("plans");
router.replace("terms");
router.back();
router.canGoBack();
router.dismiss(2);
router.dismissAll();
router.dismissTo("goals");
router.reset(["index", "goals"]); // mid-flow: the whole stack, no transition
router.name; // current page
router.depth; // pages underneath (index = 0)The stack router for multi-page flows, expo-router's API, method for method, plus reset to set the whole stack at once mid-flow. Where a flow opens is decided by the entry export of layout.tsx (Choose where a flow opens), which is how an onboarding resumes where the user left off. Page names autocomplete and reject typos via the generated superwall.d.ts. Pages & navigation covers the stack model, state between pages, and shared chrome.
useIsFocused()
import { useIsFocused } from "superwall/navigation";
const focused = useIsFocused();Whether this page is on top of the stack. Pages you navigate away from stay alive. A covered page can't be clicked or focused, and useIsFocused() tells it so, so it can pause video or timers. See Pages & navigation.
replayStack(start, next, answered)
import { replayStack, type Entry } from "superwall/navigation";
export const entry: Entry = ({ user }) =>
replayStack("index", (route) => next(route, user), (route) => answered(route, user));The stack to open a flow on: it walks next from start while each page is answered and returns every page it passed, the first unanswered one on top. Give it the same next your buttons push, and a branching flow resumes down the branch the user took. It never loops: a page next returns twice, an empty or non-string name, a name the surface doesn't have (checked when called from entry), or a stack past 100 pages stops the walk where it is, with a console warning. See Resume an onboarding.
useQueryState(key, parser?)
import { parseAsStringEnum, useQueryState } from "superwall/navigation";
const [goal, setGoal] = useQueryState("goal", parseAsStringEnum(["focus", "habit"]));
const [name, setName] = useQueryState("name"); // string | null
setGoal("focus"); // ?goal=focus
setGoal(null); // key removeduseState whose value lives in the page URL, so a flow resumes from any link: after a reload, in the OS browser after an in-app one, or back from hosted checkout. On a surface with web checkout the URL is kept automatically, and every answer in a web funnel goes through this hook (single choice, multi choice, inputs, the selected plan), never useState; see Web Funnels. On a native host the same hook is plain state shared across pages. The API is nuqs's: parseAsString, parseAsInteger, parseAsFloat, parseAsBoolean, parseAsStringEnum, parseAsArrayOf, createParser, plus .withDefault() and .withOptions({ history, clearOnDefault }). See Pages & navigation.
ProductReference
import { type ProductReference } from "superwall/hooks";
const [selected, setSelected] = React.useState<ProductReference>("annual");The union of product references declared in your config.ts, the type behind getProduct, purchase, and prefetch. Use it for selection state so an invalid reference is a compile error.
How is this guide?
Migrating native screens
Turn a SwiftUI, UIKit, Jetpack Compose, Android Views, React Native or Flutter screen into a Superwall surface the app can change without a release: what the CLI reads from the source, what an agent rebuilds, how you review it against a simulator screenshot, and how the app is wired afterwards.
CLI Reference
Every superwall command (create, dev, push, promote, publish) with flags, auth, and the checks that run before anything ships.