Prerequisites
If your app already uses Flutter deep linking with theRouter widget, disable Flutter’s automatic link handling so that you receive every link and decide where it goes.
iOS — in Info.plist:
AndroidManifest.xml, on the main Activity:
Router system, as shown in Handle the custom URL scheme.
Register the custom URL scheme
ReplaceAPP_TOKEN with your app’s Userpilot token, which you can obtain from your Environments Page. For example, if your token is NX-12345678, your scheme value is userpilot-nx-12345678.
iOS — in Info.plist:
AndroidManifest.xml, on the main Activity:
android:host="sdk" in this filter. Userpilot validates that incoming preview and push-notification URLs use the sdk host.
Forward links from iOS
Which iOS setup you use depends on your Flutter version. The recommended one uses theUIScene lifecycle and needs Flutter 3.38.0 or later, because it relies on FlutterSceneDelegate and FlutterImplicitEngineDelegate, which were introduced in that release. Below 3.38.0, use the app-delegate setup.
This affects only the integration code in your own app. The plugin itself works on any Flutter version its pubspec.yaml supports and requires no scene adoption.
- Flutter 3.38.0 and later
- Earlier Flutter versions
First, register your scene delegate in Under the scene lifecycle, your window and its
Info.plist:FlutterViewController are created after application(_:didFinishLaunchingWithOptions:) returns. That shapes the code below in two ways: the launch URL is delivered to the scene rather than in launchOptions, and there is no binary messenger to build channels on until the Flutter engine exists. So the scene delegate captures the launch URL, and plugin registration and channel setup happen in didInitializeImplicitFlutterEngine, which Flutter calls once the engine is ready.SceneDelegate.swift:AppDelegate.swift:LinkStreamHandler:
queuedLinks array matters for cold starts: a link can arrive before your Dart code has subscribed to the event channel. Holding it until onListen runs means the link is delivered once Dart is listening, rather than dropped.
Migrating to the scene lifecycle
Earlier versions of this guide built both channels insideapplication(_:didFinishLaunchingWithOptions:) and read the launch URL from launchOptions[.url]. Neither works once your app adopts the scene lifecycle. To migrate:
- Add the
UIApplicationSceneManifestentry shown above. - Add
SceneDelegate.swift, and add it to your Xcode target. - Move plugin registration and channel setup out of
didFinishLaunchingWithOptionsand intodidInitializeImplicitFlutterEngine. - Delete your
application(_:open:options:)override. Warm links now arrive throughscene(_:openURLContexts:). - Leave push notification handling in the
AppDelegate.didRegisterForRemoteNotificationsWithDeviceTokenand the notification-response callbacks still fire there.
Forward links from Android
MainActivity.kt:
onCreate captures the launch link and onNewIntent forwards links that arrive while the app runs, which mirrors the two iOS entry points.
Handle the custom URL scheme
In Dart, pass each incomingUri to Userpilot.didHandleURL. It returns true when the SDK recognized and processed the link, and false when your app should route it, at which point you can hand the .path to your own RouteInformationParser.
Supported links
The SDK only claims a URL when the scheme matches your token and the host issdk. Every other URL is passed back to your app.
On iOS, notification taps are handled natively by the SDK and never reach your link handling, so follow Push Notifications for that setup.
Preview an experience with a QR code
Userpilot can render a draft experience on your device before you publish it, so you can check layout, copy, and behavior on a real screen. Preview is supported for Flows and Surveys. The preview is delivered over the custom URL scheme. It works once your app is installed on the device and the scheme is registered and handled as described above.1
Open the experience
Open a Flow or Survey in the Experience Builder and enter Edit Mode.
2
Start the preview
Select Preview in the top-right corner of the builder. A popup with a QR code appears.
3
Scan the QR code
Scan the code with your device camera. Your app opens and the experience appears.
- You don’t need to apply or save your changes before previewing them.
- The experience doesn’t need to be live.
- A preview bypasses the experience’s targeting and frequency settings, so it appears even when the current user wouldn’t normally qualify for it.
- Once you close the experience, the app resumes normal behavior.
Test push notifications
You can send a test push to a specific user from the Userpilot dashboard and verify delivery and tap handling on a real device without sending anything to your wider audience. The notification doesn’t need to be live, so you can test it while it’s still a draft.- Test pushes work in both the staging and production environments.
- Test pushes are not counted as real sends, and they’re excluded from analytics.
- Test pushes bypass the notification’s targeting and frequency settings.
Test a deep link from the command line
Open a link directly to confirm your scheme is registered and your handler runs, without going through the Builder:12345 with a real experience ID.
Troubleshooting
Nothing happens when I scan the QR code
Nothing happens when I scan the QR code
Confirm the registered scheme matches your token exactly, lowercased. A mismatched scheme means the OS never routes the link to your app. Verify the registration with the commands above.
The app crashes on launch after I added the scene manifest
The app crashes on launch after I added the scene manifest
You have the
UIApplicationSceneManifest entry but still use the app-delegate code. Under the scene lifecycle, window is nil while didFinishLaunchingWithOptions runs, so window?.rootViewController as! FlutterViewController traps. Move to the Flutter 3.38.0 setup, or remove the manifest entry.Links work when the app is running but not from a cold start
Links work when the app is running but not from a cold start
Check three things. The platform side must capture the launch link (
captureInitialLinkIfNeeded under scenes, launchOptions[.url] otherwise, intent.data on Android). LinkStreamHandler must queue links until Dart subscribes. And your Dart code must actually request the launch link through the initialLink method channel.didHandleURL returns false for a valid preview link
didHandleURL returns false for a valid preview link
Most often the SDK was not initialized yet. Await
Userpilot.initialize before you consume the launch link. Also confirm you’re passing the complete URL, including scheme and host, rather than just uri.path.My Router opens a blank page instead of the experience
My Router opens a blank page instead of the experience
Flutter’s automatic deep link handling is intercepting the link. Set
FlutterDeepLinkingEnabled to false on iOS and flutter_deeplinking_enabled to false on Android, as described in the prerequisites.Preview works in production but not in staging
Preview works in production but not in staging
Each environment has its own token, and the scheme follows the token. Confirm the staging build registers the scheme for the token it initializes the SDK with.