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

HandlerTypeDescription
registerSignInHandlerFuture<bool> Function(String provider)Fired by signin screens. Return true when authentication succeeded — the flow advances; false keeps the user on the screen.
registerPermissionHandlerFuture<bool> Function(String permission)Fired by permission and notification_opt_in screens. Return whether the permission was granted.
registerPurchaseHandlerFuture<PurchaseResult> Function(PurchaseRequest)Fired when the user confirms a purchase on a paywall. Only PurchaseResult.purchased advances the flow.
registerRestoreHandlerFuture<bool> Function()Fired by “Restore purchases”. Return whether an active entitlement was found.
registerPhotoUploadHandlerFuture<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.
registerLinkHandlervoid 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

After UpliftFunnel.configure
// 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:

dart
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:

HandoffTypeDescription
purchaseadvancesThe flow moves on and nothing is charged. The paywall is inert until you wire billing.
sign-incounts as successThe tapped provider is saved to the screen's variable and the flow advances, with nobody signed in.
permissioncounts as a denialRecorded as "false" in the screen's variable — which a later transition can branch on — and never auto-advanced past.
restoreno-opsThe button does nothing. App Store review expects a working restore wherever a subscription is sold.
photo / linkno-opsNo photo comes back; the link tap does nothing.