Flutter SDK / Native handlers
Native handlers
Flows stop at the edge of the OS: sign-in, permission prompts, payments, and photo pickers belong to your app. Handlers are the bridge — a screen requests the action, your code performs it, and the returned value decides whether the flow advances.
The six handlers
| Handler | Type | Description |
|---|---|---|
| registerSignInHandler | Future<bool> Function(String provider) | Fired by signin screens. Return true when authentication succeeded — the flow advances; false keeps the user on the screen. |
| registerPermissionHandler | Future<bool> Function(String permission) | Fired by permission and notification_opt_in screens. Return whether the permission was granted. |
| registerPurchaseHandler | Future<PurchaseResult> Function(PurchaseRequest) | Fired when the user confirms a purchase on a paywall. Only PurchaseResult.purchased advances the flow. |
| registerRestoreHandler | Future<bool> Function() | Fired by “Restore purchases”. Return whether an active entitlement was found. |
| registerPhotoUploadHandler | Future<String?> Function(PhotoUploadRequest) | Fired by a screen that asks for a photo. The request tells you the source (camera, library, both) and crop shape. Return a path/URL, or null if the user cancelled. |
| registerLinkHandler | void Function(String url) | Fired for link taps (e.g. Terms and Privacy on a paywall). Open the URL however your app prefers. |
Wiring them up
// Register once, right after UpliftFunnel.configure. Handlers are
// app-global — every flow uses the same wiring.
await UpliftFunnel.registerSignInHandler((provider) async {
// provider: 'apple' | 'google' | 'facebook' | 'email' | 'anonymous'
switch (provider) {
case 'apple':
final cred = await SignInWithApple.getAppleIDCredential(
scopes: [AppleIDAuthorizationScopes.email],
);
return await myAuth.signInWithApple(cred);
default:
return false; // unknown provider → screen stays put
}
});
await UpliftFunnel.registerPermissionHandler((permission) async {
// permission: 'notifications' | 'camera' | 'tracking' | 'health' | …
if (permission == 'notifications') {
final status = await Permission.notification.request();
return status.isGranted;
}
return false;
});
await UpliftFunnel.registerLinkHandler((url) {
launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication);
});Purchases
The purchase handler receives a PurchaseRequest with everything needed to hand off to your billing stack — productId (the store product resolved for the current platform), planId (what the user selected), flowId, screenId, and sessionId for reconciliation. Here's a RevenueCat-flavored example:
await UpliftFunnel.registerPurchaseHandler((request) async {
// request.productId — store product id resolved for this platform
// request.planId — the plan the user picked on the screen
try {
final products = await Purchases.getProducts([request.productId!]);
await Purchases.purchaseStoreProduct(products.first);
return PurchaseResult.purchased; // only this advances the flow
} on PlatformException catch (e) {
final code = PurchasesErrorHelper.getErrorCode(e);
if (code == PurchasesErrorCode.purchaseCancelledError) {
return PurchaseResult.cancelled;
}
return PurchaseResult.failed;
}
});
await UpliftFunnel.registerRestoreHandler(() async {
final info = await Purchases.restorePurchases();
return info.entitlements.active.isNotEmpty;
});The SDK reports each purchase stage automatically (purchase_attempted, purchase_succeeded, purchase_cancelled, purchase_failed, purchase_pending) so conversion shows up in analytics without extra code. To show real prices on paywall screens, see Products & paywalls.
What happens without a handler
Handlers are optional so you can integrate incrementally, and every handoff has a defined answer without one. They do not all fail in the same direction:
| Handoff | Type | Description |
|---|---|---|
| purchase | advances | The flow moves on and nothing is charged. The paywall is inert until you wire billing. |
| sign-in | counts as success | The tapped provider is saved to the screen's variable and the flow advances, with nobody signed in. |
| permission | counts as a denial | Recorded as "false" in the screen's variable — which a later transition can branch on — and never auto-advanced past. |
| restore | no-ops | The button does nothing. App Store review expects a working restore wherever a subscription is sold. |
| photo / link | no-ops | No photo comes back; the link tap does nothing. |