Flutter SDK

Install and integrate the LinkTrail Flutter SDK — package linktrail_flutter, one Dart API over the native iOS and Android SDKs.

A thin plugin over the native LinkTrail SDKs · package linktrail_flutter 0.1.0 on pub.dev · entry point LinkTrail · Android minSdk 26, iOS 15+. Wraps io.linktrail:sdk 0.1.0 (Maven Central) and LinkTrailSDK 0.1.0 (CocoaPods) behind one Dart API.

Prerequisites

Before this works, set up three things in the dashboard. Skip one and links fail silently — they open the browser instead of your app. Full flow: Quickstart.

  1. 1Register both app targets — Apps → Register app. iOS needs your Apple Team ID and bundle ID; Android needs its package name and SHA-256 fingerprints.
  2. 2Register the identifier your build really has — the SDK sends the iOS bundle ID and the Android applicationId, and it must equal the identifier you registered, in the same workspace as the API key. A debug build with a suffixed ID (com.acme.shop.debug) needs its own registered app, or every install answers 404 (onError reports it as a server error).
  3. 3Add a link domain — Domains. Or use your free *.linktrail.io subdomain.
  4. 4Create an API key — Settings → API Keys. That is the lt_live_… value below.
  1. Install

    Shell
    flutter pub add linktrail_flutter

    Or add it to your app's pubspec.yaml, then flutter pub get:

    pubspec.yamlYAML
    dependencies:
      linktrail_flutter: ^0.1.0

    The native SDKs are pulled in automatically — no manual Gradle or CocoaPods edits. Platform minimums the SDK requires (set them if your app targets lower): android { defaultConfig { minSdk = 26 } } and platform :ios, '15.0'.

  2. Integrate

    main.dartDart
    import 'package:flutter/material.dart';
    import 'package:linktrail_flutter/linktrail_flutter.dart';
    
    // MyApp and router are your own.
    Future<void> main() async {
      WidgetsFlutterBinding.ensureInitialized();
    
      // One stream — fires for deferred (first-launch) AND re-engagement links.
      // Subscribe BEFORE configure so a cold-start link is not missed:
      LinkTrail.onLink.listen((event) {
        router.route(event.link.path, event.link.customData);
      });
    
      LinkTrail.onError.listen((error) => debugPrint('LinkTrail: $error'));
    
      // The API key is required. The install is sent automatically — once your app
      // records a consent decision (see Consent below).
      await LinkTrail.configure(apiKey: 'lt_live_...');
    
      runApp(const MyApp());
    }

    customData is a map of strings to strings. A nested object or array set on the link arrives as a compact JSON string — decode it yourself.

    Incoming links are captured automatically — you do not need to override MainActivity (Android) or AppDelegate/SceneDelegate (iOS). The plugin forwards App Links, Universal Links, and custom-scheme opens to the SDK on both cold start and while running. Every callback is a broadcast Stream, so you can listen from multiple places.

  3. Consent gating is on by default (requireConsent: true) and it holds the install: nothing is sent and the deferred link does not route until your app records a decision. setConsent(true) sends the install counted and attributed and routes the deferred link; setConsent(false) sends one uncounted install that still resolves it, so the user lands in the right place. Re-engagement links (app already installed) route regardless. There is no consent getter — persist the choice yourself, and on every launch subscribe to onLink and replay the stored decision with setConsent right after configure. Full model, CMP integration and store questionnaires: Consent & privacy.

    Important

    An app that never calls setConsent gets no deferred link and no attribution. Wire both answers of your consent prompt to setConsent, or pass requireConsent: false if consent is collected elsewhere.

    Dart
    // Call once your consent UI resolves — and replay the stored decision on every
    // launch, right after configure:
    await LinkTrail.setConsent(true);   // grant → install sent + attributed, deferred link routes, queue flushed
    await LinkTrail.setConsent(false);  // deny  → one uncounted install, deferred link still routes, queue cleared
    
    // Collecting consent elsewhere (a CMP)? Opt out of the gate — tracking runs from configure,
    // and an explicit setConsent(false) still wins:
    await LinkTrail.configure(apiKey: 'lt_live_...',
        options: LinkTrailOptions(requireConsent: false));
  4. The plugin captures links automatically, but the OS still needs to route the link to your app. Android: declare your App Links host in AndroidManifest.xml (see below). iOS: add the Associated Domains capability with applinks:yourapp.linktrail.io.

    Warning

    handleDeepLink claims only https links on linktrail.io (or a subdomain) and on the hosts listed in linkDomains (or their subdomains). Custom domains must be listed: links on go.yourbrand.com need LinkTrailOptions(linkDomains: ['go.yourbrand.com']), or the app opens and never navigates. Any other URL (mailto:, an OAuth callback, myapp://settings) returns false from handleDeepLink with no request and no onError, so route it yourself. Custom-scheme LinkTrail links (myapp://slug) are no longer handled; use the https link. The full rules: Which links the SDK claims. Upgrading from an earlier version? See Upgrading to 0.1.0.

    android/app/src/main/AndroidManifest.xmlXML
    <intent-filter android:autoVerify="true">
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="https" android:host="yourapp.linktrail.io" />
    </intent-filter>
    Warning

    The apple-app-site-association / assetlinks.json files are generated and hosted by LinkTrail from the credentials you registered in the dashboard. On iOS the plugin registers on the UIScene lifecycle, so links are delivered on both the scene and classic app-delegate lifecycles.

  5. Verify it works

    Every failure on this page is silent unless you look in the right place. Check these in order:

    1. 1SDK logs. Pass LinkTrailOptions(logEnabled: true, logLevel: LinkTrailLogLevel.debug) to configure and read the native logs — the Xcode console (lines prefixed [LinkTrail]) or adb logcat -s LinkTrail. On the first launch expect Install held — awaiting consent decision; after setConsent(true) expect Consent granted, Sending install event… (consent: true), then Install attributed — Install attributed (deterministic click token) when the click token was recovered — or Organic install (no attribution match). Both platforms print [LinkTrail] [LEVEL] message, so these lines read [LinkTrail] [INFO] … on iOS and Android (Android logs them under the LinkTrail tag). No lines at all means configure never ran.
    2. 2API key Last used. Dashboard → Settings → API Keys: the key's Last used changes from Never after the first call made with a valid key.
    3. 3Overview and Links. Dashboard → Overview: the installs count goes up. Links → your link shows a click and an install.
    4. 4onError. A wrong or revoked key does not fail configure. The backend answers 401 with {"code":401,"message":"Invalid or missing API key"} and the SDK reports it through onError as LinkTrailInvalidApiKeyException — but only once the install is sent: after setConsent, or at configure with requireConsent: false. Nothing is sent before that.
    5. 5Re-test deferred without reinstalling. Call LinkTrail.resetForTesting() — it clears the install flag, cached attribution, device ID and the consent decision, so the next launch behaves like a genuine first launch and asks for consent again. Testing only; never ship it.
    [LinkTrail] [INFO] Install held — awaiting consent decision
    [LinkTrail] [INFO] Consent granted
    [LinkTrail] [INFO] Sending install event… (consent: true)
    [LinkTrail] [INFO] Install attributed (deterministic click token)
    [LinkTrail] [INFO] Install attributed
    [LinkTrail] [INFO] Organic install (no attribution match)

    Still nothing? Troubleshooting lists the usual causes by symptom.

More

Dart
// Custom + revenue events:
await LinkTrail.trackEvent(name: 'purchase', value: 59.99, currency: 'USD');

// Cached results:
final attribution = await LinkTrail.lastAttribution;
final lastLink = await LinkTrail.lastDeepLink;

// Attribution stream (fires when an install is attributed):
LinkTrail.onAttribution.listen((attribution) { /* … */ });

// Manual install (rare): autoTrackInstall: false, then call trackInstall() yourself.
// Still subject to the consent gate above — call it after setConsent.
await LinkTrail.configure(apiKey: 'lt_live_...',
    options: LinkTrailOptions(autoTrackInstall: false));
await LinkTrail.trackInstall();

// iOS ATT / SKAdNetwork (on Android, requestTrackingAuthorization returns true; the rest are no-ops):
await LinkTrail.requestTrackingAuthorization();
await LinkTrail.registerForSKAdAttribution();
await LinkTrail.updateConversionValue(3, coarseValue: LinkTrailCoarseConversionValue.medium);

Errors arrive as typed LinkTrailException subtypes on onError, and are thrown from the Future-returning calls — so you can try/catch a specific case like LinkTrailInvalidApiKeyException.

Deferred attribution & the paste button (iOS)

On iOS, deferred attribution recovers a click token the tapped link leaves on the clipboard, and LinkTrailOptions.clickTokenSource chooses how it is read. The widget renders the native control on iOS 16+ and nothing on Android, where the Play Install Referrer handles deferred attribution instead.

clickTokenSourceBehaviourTrade-off
pasteButton (default)Read only when the user taps a LinkTrailPasteButton.No system alert, but it needs the widget and autoTrackInstall: false.
automaticThe SDK reads the clipboard itself at install.No UI, but iOS shows the “Allow Paste” alert on first launch.
noneThe clipboard is never read, so no click token is recovered.No alert and no UI; deferred matching falls back to probabilistic only.
Warning

With the default pasteButton you must render the widget and set autoTrackInstall: false, or deferred installs on iOS stay unattributed. Apple restricts customization of the control — no custom label text, font, or border.

Dart
// The widget fills its parent's width — wrap it in a SizedBox to constrain it.
LinkTrailPasteButton(
  height: 48,
  // null (default): the tap grants consent only while it is still undecided.
  // true: always grants, even after a denial. false: never touches consent.
  grantsConsent: null,
  onToken: (token) {
    // The SDK has already sent the install with the token; advance your UI here.
  },
)

The widget uses the native paste handling: the tap records consent per grantsConsent and sends the install with the click token itself, so onToken is a side-effect hook — don't call trackInstallWithClickToken from it. Using a CMP that answers later? Pass grantsConsent: false: the install is held with the token until your own setConsent. Its parameters are grantsConsent, onToken, color and height; there is no width parameter.

API surface

APIPurpose
configure(apiKey:, options:)Initialize; the install is held until a setConsent decision
setConsent(granted)Record the decision; releases the held install either way (see Consent above)
onLink — Stream<LinkTrailLinkEvent>Deep link delivered (deferred + re-engagement)
onAttribution — Stream<LinkTrailAttribution>Install attribution result
onError — Stream<LinkTrailException>Failures (invalid key, network…)
handleDeepLink(uri)Resolve a link manually (links are also auto-captured)
trackEvent(name:, value:, currency:)Custom + revenue events
trackInstall(force:)Manual install track (with autoTrackInstall: false)
trackInstallWithClickToken(token, force:)Install carrying a pasted click token (iOS; same as trackInstall on Android)
lastAttribution / lastDeepLinkCached last results
requestTrackingAuthorization()ATT prompt (iOS; returns true on Android)
registerForSKAdAttribution() / updateConversionValue(…)SKAdNetwork (iOS; no-op on Android)
resetForTesting()Clears the install flag, attribution, device ID and consent — testing only
Note

Example app: KickFlip — a Flutter storefront in the linktrail-flutter repo's example folder, wired end to end. cd example && flutter run --dart-define=LINKTRAIL_API_KEY=lt_live_…. Source · example app · issue tracker · changelog.

Step-by-step walkthrough for both platforms: Deferred deep linking in Flutter.