Variables & Personalization

React to user attributes, device state, and placement parameters, and write paywalls the dashboard can experiment on without a rebuild.

Everything your app and the SDK tell a paywall about the presentation arrives through useVariables(): who the user is, what device they're on, and what the placement was called with. Read these defensively and a single paywall can greet a returning user by name, adapt to platform, or react to any parameter your app passes, all without a rebuild.

useVariables()

import { useVariables } from "superwall/hooks";

const { device, user, params } = useVariables();

Three records, three sources:

  • device, filled in by the SDK: platform, deviceModel, simulatorModel (iOS SDK 4.18.1+, on simulators), osVersion, appVersion, deviceLocale, regionCode, deviceCurrencyCode, subscriptionStatus, activeEntitlements, daysSinceInstall, totalPaywallViews, safeAreaInsets, and more.
  • user, whatever your app set via Superwall.shared.setUserAttributes, plus what the paywall wrote with useUserAttribute (user.firstName, user.plan, …).
  • params, whatever the placement was called with (params.event_name is the placement's name; $-prefixed keys are SDK-set, and anything the app passed alongside comes through unprefixed).
const name = typeof user.firstName === "string" ? user.firstName : undefined;

<h1>{name ? `Welcome back, ${name}` : "Go Pro"}</h1>
<span>{device.platform ?? "—"}</span>

Guard every read

All three records are filled in by the host, your paywall controls none of them, so every read needs a fallback:

  • For device fields, ?? "—" (or any sensible default) suffices. The SDK guarantees the shape, just not that a value has arrived yet.
  • For user and params, the host controls the type too, so check it before using it: typeof params.event_name === "string". An attribute your app sets as a number today might be a string tomorrow, and the paywall must not crash either way.

device.isSandbox is a string, not a boolean. Compare it as one.

The optional device.simulatorModel variable is available on iOS SDK 4.18.1 or later. It is absent on real devices, older iOS SDK versions, and other platforms. Use it when checking a device-model-specific layout in the simulator.

On a simulator, deviceModel is the Mac's architecture (arm64), not a phone. SDKs that know it report the simulated phone as simulatorModel, and the framework reads simulatorModel first wherever it needs the model, for useDevice().cutout, the insets, and the web checkout sheet, so a paywall in the iOS simulator lays out like the phone it simulates. Do the same if you branch on the model yourself: device.simulatorModel || device.deviceModel.

While previewing, every one of these values is editable live in the studio's Variables panel: user attributes, device properties, placement params, and per-product variables, all seeded from your app's real sample data. Change a value and watch the paywall react. See The studio.

useUser()

Shorthand for when you only need the user record:

import { useUser } from "superwall/hooks";

const user = useUser();

Identical to useVariables().user, reach for it when the device and params records aren't needed.

To read one attribute as a typed value (and write it back), use useUserAttribute with a parser instead of guarding by hand. Anything the parser can't read is null, so the type check is done for you:

import { parseAsStringEnum, useUserAttribute } from "superwall/hooks";

const [plan, setPlan] = useUserAttribute("plan", parseAsStringEnum(["free", "pro"])); // "free" | "pro" | null

readAttribute(user, key, parser) is the same read outside a component. See Hooks.

useDevice()

The same device record as useVariables().device, plus orientation, hostPlatform, host and cutout:

import { useDevice } from "superwall/hooks";

const { orientation, platform, deviceModel, simulatorModel } = useDevice();

orientation is "portrait" | "landscape", measured in the page itself, it updates the moment the device turns, so you can build layouts that answer to rotation. The orientation example reflows to a two-column grid in landscape rather than shrinking the portrait layout; see Examples.

hostPlatform names the SDK showing the paywall: "ios" (an iPad app on a Mac too), "android", "web" for the Web SDK on your own site, or undefined for a host that hasn't said, such as the studio's device-less preview. It is normalized, and it is known from the first render: until the host's device record arrives it reads the platform the host stamped on the paywall URL. Branch on it for anything one platform must never show, and show neither variant while it is undefined:

const { hostPlatform } = useDevice();
const native = hostPlatform === "ios" || hostPlatform === "android";

{native ? <RestoreLink /> : null}
{hostPlatform === "web" ? <ManageOnWebLink /> : null}

host names the surface showing the paywall, which hostPlatform can't: the same web paywall can run in the Web SDK on your site or on Superwall's hosted page, and both are "web". It is known from the first render and never undefined:

hostShowing the paywall
"ios-sdk"an iOS app (SuperwallKit, and Flutter or React Native on top of it)
"android-sdk"an Android app
"web-sdk"the Web SDK, on your own site
"web-paywall"Superwall's hosted web paywall page
"web-teleport"the page an app teleported to
"studio"a preview in the studio
"browser"anything else, such as the built file opened directly
const { host } = useDevice();

{host === "web-sdk" ? <ContinueOnSite /> : null}
{host === "web-paywall" ? <OpenTheAppBanner /> : null}

Use hostPlatform for what the user's platform allows (restore, store links) and host for what the surface around the paywall does.

cutout is what an iPhone's screen edge holds: "island", "notch", "none", or "strip" for iPhone Duo, whose camera strip sits on the right when the phone is closed or in landscape and on top when it's open in portrait. It is undefined off iPhone. The inset variables already move with the strip; use cutout only for a design choice they can't make.

The raw platform field is the host's own string ("iOS", "macOS", "Web") and is empty until the device record arrives, so don't compare against it.

Built to be experimented on

Notice what's missing: variables are never declared in code. What the paywall reads, user attributes, device state, placement params, product variables, trial eligibility, is supplied by the app and the store at runtime, and the studio overrides all of it live while previewing.

Write every read defensively, guarded, typed, with a designed fallback, and every one of those values becomes a knob the dashboard can turn without a rebuild. A paywall that renders sensibly for any combination of inputs can be A/B tested freely.

The personalization example shows the full doctrine in one project: ?? "—" for SDK-guaranteed device fields, typeof checks for host-controlled user and params reads, and designed fallbacks for every string. See Examples.

How is this guide?

On this page