Configuration
Everything definePaywall accepts — products, presentation, transitions, trial reminders, and the behavior settings that shape a paywall.
Every paywall declares itself in a required config.ts: its dashboard name, its products, and any behavior settings. This file is the whole truth for the paywall — nothing is inherited from anywhere else.
import { definePaywall } from "superwall/config";
export default definePaywall({
name: "Plus — Annual, 3-day trial",
products: {
monthly: "pro_999_month",
annual: "pro_5999_year",
},
});TypeScript is the only validation, so keep the object literal inline — that's what lets the compiler catch typos. If you want to share settings between paywalls, export a plain object and spread it:
// superwall/components/shared-config.ts
export const shared = { transition: "slide", purchaseTimeoutMs: 300_000 } as const;
// paywalls/pro/config.ts
export default definePaywall({ ...shared, name: "Pro", products: { … } });Options
| Key | Type | Default | What it does |
|---|---|---|---|
name | string | directory name, title-cased | The label shown in the dashboard. The directory stays the identifier. |
products | Record<string, string | { productId }> | — | Product slots by reference — see below. |
transition | "push" | "slide" | "fade" | "none" or custom | "push" | Default page transition — see Transitions. |
checkout | mode or { mode, prefetch? } | — | Sell on the web. Omit for native-only — see Web checkout. |
presentation | see below | — | How the native SDK presents the paywall. |
featureGating | "gated" | "nonGated" | "nonGated" | Whether users must pay to pass the placement. |
introductoryOfferEligibility | "automatic" | "alwaysEligible" | "alwaysIneligible" | "automatic" | Trial eligibility — automatic lets the store decide. |
dismissOnPurchase | boolean | — | Auto-dismiss the paywall after a completed purchase. |
purchaseTimeoutMs | number | — | Resolve a purchase as failed after this long with no result. |
notifications | { trialReminder } | — | Trial-reminder notification — see below. |
localization | { defaultLocale, messages? } | "en" | Fallback locale; file-based catalogs need no config — see Localization. |
scrollEnabled | boolean | true | Whether the paywall scrolls. |
gameControllerEnabled | boolean | — | Forward game-controller input to the paywall. |
onDeviceCacheEnabled | boolean | true | Cache the paywall on device. |
There is deliberately no identifier field. The directory path is the paywall's identity, and the dashboard binding lives in superwall.lock — never in this file. See Project structure.
Presentation
The presentation object controls how the native SDK presents the paywall over your app:
presentation: {
style: "fullscreen" | "modal" | "push" | "drawer" | "popup" | "noAnimation", // default "fullscreen"
condition: "checkUserSubscription" | "always", // default "checkUserSubscription"
drawer: { height, cornerRadius }, // when style === "drawer"
popup: { width, height, cornerRadius }, // when style === "popup"
}style—fullscreencovers the screen,modaluses the platform's modal presentation,pushpushes onto the navigation hierarchy,drawerrises from the bottom edge to the height you set,popupfloats as a centered window, andnoAnimationpresents modally without animating.condition— with the defaultcheckUserSubscription, the SDK skips presentation for users who are already subscribed. Usealwaysto show the paywall regardless.drawerandpopuptake sizing options that only apply to their matching style.
Products
Products are slots. The key is the reference your code uses; the value is the store identifier:
products: {
annual: "pro_5999_year", // shorthand
monthly: { productId: "pro_999_month" }, // same thing
}Your components read them by reference — getProduct("annual"), purchase("annual") — and the references are typed, so a typo is a compile error. Web/Stripe products put the Stripe price inside the identifier using the {test|live}:price_…:{offer} format.
Product data — price, period, trial — never appears in this file. It's store-owned and arrives at runtime; a reference the dashboard has no product for renders undefined variables and blocks publishing. The full story, including how to read product variables safely, is in Products.
Trial reminder notifications
Declare a local notification and the SDK schedules it when a trial actually starts — the paywall doesn't need to be open when it fires:
notifications: {
trialReminder: {
title: "Your trial ends tomorrow",
body: "Keep Pro, or cancel in Settings — no charge either way.",
beforeTrialEndDays: 1, // default 1
},
},title, subtitle, and body accept message keys (resolved through t()) or literal copy. For full control, pass a function instead — it receives { trialEndDate, product, t, locale } and returns { title, body, delayMs }, or null to skip the notification entirely. The trial reminders example shows both forms — see Examples.
Experimenting without rebuilds
Variables are never declared in this file: everything the paywall reads — useVariables(), product variables, trial eligibility — is supplied by your app and the store at runtime, and the studio can override all of it live while you preview. Write your paywall to read variables defensively and every one of them becomes experimentable from the dashboard, no rebuild required. See Variables & personalization.
How is this guide?