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.

AppDelegate.swift
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.

HandlerTypeDescription
registerPurchaseHandleradvancesThe paywall renders, the user taps, the flow continues — and nothing is charged. Register this before you ship.
registerRestoreHandlerno-opsWorth wiring even on a simple paywall: App Store review expects a working restore on any screen that sells a subscription.
registerSignInHandlersucceedsThe tap is treated as a success. Right for walking an unwired flow, wrong for shipping one — nobody is actually signed in.
registerPermissionHandlerdeniesRecorded as a denial, which a later transition can still branch on.
registerPhotoUploadHandlercannot deliverThe screen has no way to produce a photo.
registerLinkHandlerdoes nothingA tap on Terms or Privacy goes nowhere.

The purchase request

FieldTypeDescription
productIdString?The store product, already resolved for this platform from the plan's configuration.
planIdString?The plan the user selected, as authored in the flow.
flowId / screenId / sessionIdStringWhere 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.

swift
// 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.