iOS SDK (Swift) / Products & paywalls
Products & paywalls
A paywall renders without this — using the prices someone typed into the dashboard. Those are a preview and an offline fallback, not what the store will charge.
Hand over the catalog
import UpliftFunnel
// After your billing stack has offerings — and again on refresh.
// It REPLACES the catalog rather than merging.
UpliftFunnel.setProducts([
UpliftFunnelProduct(
id: "yearly_pro", // must match the plan's product_id
price: "$59.99", // exactly as the store formatted it
priceAmount: 59.99, // lets per-month / per-week be derived
currencyCode: "USD",
period: .year,
trialDays: 7,
trialEligible: await store.isEligibleForIntroOffer("yearly_pro"),
originalPrice: "$155.88", // the struck-through one
pricePerMonth: "$5.00",
savings: "Save 61%" // your words, your language
),
UpliftFunnelProduct(id: "monthly_pro", price: "$12.99",
priceAmount: 12.99, period: .month)
])The product
| Field | Type | Description |
|---|---|---|
| id | String | Store product identifier. Must match the plan's product_id (or its platform override) as authored in the dashboard. |
| price | String | The localized price string exactly as the store formats it ("₺899,99"). Injected as price.<id>. |
| priceAmount | Double? | The numeric price. Enables derived per-period prices when the explicit overrides are absent. |
| currencyCode | String? | ISO 4217. Informational — derived formatting borrows the symbol from `price`. |
| period | ProductPeriod? | .week / .month / .year / .lifetime. Injected as period.<id> and decides which derived prices make sense. |
| trialDays | Int? | Free-trial length. Injected as trial_days.<id>. |
| trialEligible | Bool? | Whether THIS user can still take the intro offer — a per-user store check only your app can make. Flow conditions can branch on it. |
| originalPrice | String? | Pre-discount price. A plan card's struck-through price binds to it automatically. |
| pricePerMonth / pricePerWeek | String? | Explicit per-period strings; they win over derived values. |
| savings | String? | The badge phrase, in your words and your language ("Save 44%", "%44 tasarruf"). Left out, the engine derives an English "Save NN%" from originalPrice and priceAmount, and shows nothing if it cannot. |
What the flow can then write
A plan card bound to a product publishes {{product.*}} into everything beneath it, so the card's text never has to name which product it is. Every product also fans out into a flat <field>.<id> namespace at session start, for copy outside a card.
Start your {{product.trial}} trial
Then {{product.price}} / {{product.period}}
{{price.yearly_pro}} billed once a yearFrom RevenueCat
let offerings = try await Purchases.shared.offerings()
guard let current = offerings.current else { return }
UpliftFunnel.setProducts(current.availablePackages.map { package in
let p = package.storeProduct
return UpliftFunnelProduct(
id: p.productIdentifier,
price: p.localizedPriceString,
priceAmount: (p.price as NSDecimalNumber).doubleValue,
currencyCode: p.currencyCode,
period: p.subscriptionPeriod.flatMap(period),
trialDays: p.introductoryDiscount?.subscriptionPeriod.days
)
})Adapty and raw StoreKit map the same way — the SDK only wants strings and a period. To get revenue back into your funnel numbers, connect the provider on the dashboard's Integrations page.
Charging
Drawing the plans is this page; taking the money is the purchase handler. Without one, a purchase button simply advances the flow.