iOS SDK (Swift) / Native handlers
Native handlers
The flow draws the paywall; your app charges the card. Six seams, each one a closure you register once.
A flow can ask questions and draw screens on its own. It cannot charge a card, sign someone in, raise a system prompt or open a photo picker — those belong to your app, and to the entitlements it was reviewed with. The engine hands the request over and waits for your answer.
import UpliftFunnel
// Register once, at startup, AFTER configure and BEFORE presenting a flow.
UpliftFunnel.registerPurchaseHandler { request in
// request.productId is already resolved for this platform.
do {
try await Purchases.shared.purchase(productId: request.productId)
return .purchased // the only result that advances
} catch StoreError.cancelled {
return .cancelled
} catch {
return .failed
}
}
UpliftFunnel.registerRestoreHandler {
// true only when something was actually restored.
await Purchases.shared.restore()
}
UpliftFunnel.registerSignInHandler { provider in
// "apple" / "google" / "facebook" / "email" / "anonymous"
await auth.signIn(with: provider)
}
UpliftFunnel.registerPermissionHandler { permission in
switch permission {
case "notifications":
return (try? await UNUserNotificationCenter.current()
.requestAuthorization(options: [.alert, .badge, .sound])) ?? false
case "tracking":
return await ATTrackingManager.requestTrackingAuthorization() == .authorized
default:
return false
}
}
UpliftFunnel.registerPhotoUploadHandler { request in
// request.source: "camera" / "library" / "both"
// Return a URL or a local path; nil means the user backed out.
await picker.pick(source: request.source)
}
UpliftFunnel.registerLinkHandler { url in
guard let parsed = URL(string: url) else { return }
UIApplication.shared.open(parsed)
}What each one does if you skip it
Every handoff has a defined no-handler behaviour, and one of them will bite you. They exist so a flow can be authored and walked before billing is wired — not so it can ship that way.
| Handler | Type | Description |
|---|---|---|
| registerPurchaseHandler | advances | The paywall renders, the user taps, the flow continues — and nothing is charged. Register this before you ship. |
| registerRestoreHandler | no-ops | Worth wiring even on a simple paywall: App Store review expects a working restore on any screen that sells a subscription. |
| registerSignInHandler | succeeds | The tap is treated as a success. Right for walking an unwired flow, wrong for shipping one — nobody is actually signed in. |
| registerPermissionHandler | denies | Recorded as a denial, which a later transition can still branch on. |
| registerPhotoUploadHandler | cannot deliver | The screen has no way to produce a photo. |
| registerLinkHandler | does nothing | A tap on Terms or Privacy goes nowhere. |
The purchase request
| Field | Type | Description |
|---|---|---|
| productId | String? | The store product, already resolved for this platform from the plan's configuration. |
| planId | String? | The plan the user selected, as authored in the flow. |
| flowId / screenId / sessionId | String | Where the tap happened, for your own analytics and receipts. |
Link schemes are allow-listed
A url:action is authored content that arrived over the network. Without a check, "can author a flow" would mean "can make the app open any URL on the device" — including your own deep links, which usually skip the auth the UI would have enforced. The engine forwards https, http, mailto, tel and sms and drops everything else before your handler runs.
// The engine filters a url: action against this set BEFORE calling you.
// Spread the default rather than replacing it — naming your own schemes is
// how you opt a deep link in, not how you opt https out.
UpliftFunnel.registerLinkHandler(
open,
allowedSchemes: UpliftFunnel.defaultAllowedLinkSchemes.union(["myapp"])
)Next
Products & paywalls — give the paywall real store prices to draw, so {{product.price}} is what the user will actually be charged.