Products
Declare product slots in config.ts, read live store data through useProducts, and follow the three rules that keep prices honest.
Products connect your paywall to the things it sells. You declare them once in config.ts, and everything about them — price, period, trial — arrives from the store at runtime, localized and formatted for each user. You never hardcode a price.
Declare products
Products are slots. The key is the reference your code uses; the value is the store identifier:
import { definePaywall } from "superwall/config";
export default definePaywall({
name: "Pro",
products: {
monthly: "pro_999_month",
annual: "pro_5999_year",
},
});The shorthand string and the object form mean the same thing:
products: {
annual: "pro_5999_year", // shorthand
monthly: { productId: "pro_999_month" }, // same thing
},Your code only ever speaks in references — getProduct("annual"), purchase("annual") — so swapping the underlying store product is a one-line config change.
Web and Stripe products
Web paywalls sell through Stripe, and the Stripe price lives inside the identifier — no separate mapping. The format is {environment}:{priceId}:{offer}:
products: {
monthly: "live:price_1ABC…:7days-free",
},A paywall can declare both kinds side by side — store products for native, Stripe products for the web. See Web checkout for how the same purchase() call sells on both.
Product data never appears in the file
Price, period, and trial are store-owned and arrive at runtime. superwall push refuses to publish a reference the dashboard has no product for — every variable on it would be undefined on device. Example identifiers in scaffolds and examples are placeholders to repoint at your own products.
Read product data
import { useProducts } from "superwall/hooks";
const { getProduct } = useProducts();
const annual = getProduct("annual");
annual?.variables.price // "$59.99" — formatted for the user's region
annual?.variables.monthlyPrice // "$5.00" — the store's own math
annual?.variables.trialPeriodDaysReferences are typed against your config, so a typo in getProduct("anual") is a compile error, not a runtime surprise.
Everything on variables, all optional:
| Group | Variables |
|---|---|
| Price | price, rawPrice, currencyCode, currencySymbol |
| Period | period ("year"), periodly ("yearly"), periodDays, periodWeeks, periodMonths, periodYears |
| Per-interval price | dailyPrice, weeklyPrice, monthlyPrice, yearlyPrice |
| Trial | trialPeriodDays, trialPeriodWeeks, trialPeriodMonths, trialPeriodYears, trialPeriodPrice, trialPeriodText ("7-day"), trialPeriodEndDate ("Jul 23, 2026"), per-interval trial prices |
| Locale | locale, languageCode |
| State | identifier, isSubscribed |
period and periodly arrive pre-localized to the device locale — "yearly" becomes "jährlich" on a German device, with no work on your side.
The three rules
Three habits keep product data honest.
1. Guard every read and design the unpriced state
A declared reference always exists, but its variables may not have arrived yet — and in superwall dev they're undefined until the studio injects your dashboard's products. Degrade the copy; never invent a number:
{annual?.variables.price ? `Subscribe · ${annual.variables.price}` : "Subscribe"}The unpriced state isn't an error state — your paywall will render it, so design it to read as intentional.
2. Number() before arithmetic
Numeric-looking variables arrive as strings on device ("59.99", "7"). A typeof x === "number" check passes in dev and silently fails on a real phone — treating every product as trial-less:
const days = Number(annual?.variables.trialPeriodDays);
const trialDays = Number.isFinite(days) ? days : 0;3. Display formatted, compute raw
Use price and monthlyPrice for copy — they're formatted by the store for the user's region and currency. Use rawPrice when you need to compute or animate. Never derive a displayed price the store already provides: your division will disagree with the store's own math somewhere in the world.
Selection state is ordinary React
The framework has no "selected plan" concept — selection is your state, typed against the config:
import { type ProductReference } from "superwall/hooks";
const [selected, setSelected] = React.useState<ProductReference>("annual");The product-selection example shows the full pattern: a typed plan union, haptics.selection() on choice, real role="radiogroup" semantics, and a designed unpriced state.
Create the products on the dashboard
A push refuses if config.ts names a product the dashboard doesn't have. Create products in the dashboard, or straight from the CLI:
superwall products create pro_5999_year \
--name "Annual" --price 59.99 --period year \
--trial-days 7 --entitlement <numeric-id>See the CLI reference for the full flags. Once the products exist, continue to Purchases.
How is this guide?