linktrail_flutter 0.0.3
linktrail_flutter: ^0.0.3 copied to clipboard
Flutter plugin wrapping the LinkTrail Android/iOS SDKs for mobile attribution and deferred deep linking.
LinkTrail Flutter SDK #
Mobile attribution and deferred deep linking for Flutter. A thin plugin over the native
LinkTrail SDKs — package linktrail_flutter, entry point LinkTrail. Wraps the
LinkTrail Android and
iOS SDKs, exposing one Dart API across both platforms.
- Package:
linktrail_flutter(pub.flutter-io.cn) · Android: minSdk 26 · iOS: 15+ - Native SDKs wrapped:
io.linktrail:sdk(Maven Central) ·LinkTrailSDK(CocoaPods)
Install #
flutter pub add linktrail_flutter
or add it to your app's pubspec.yaml:
dependencies:
linktrail_flutter: ^0.0.3
Then flutter pub get. The native SDKs are pulled in automatically — no manual Gradle or CocoaPods
edits needed. On iOS run pod install in ios/ (or let flutter run do it).
Platform minimums the SDK requires — set them if your app targets lower:
// android/app/build.gradle.kts
android { defaultConfig { minSdk = 26 } }
# ios/Podfile
platform :ios, '15.0'
Quick start #
import 'package:linktrail_flutter/linktrail_flutter.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
// One hook handles both first-launch (deferred) AND re-engagement links.
LinkTrail.onLink.listen((event) {
router.route(event.link.path, event.link.customData); // e.g. "/products/aj1" + {voucher: SUMMER25}
});
// Observe failures if you want (e.g. LinkTrailInvalidApiKeyException).
LinkTrail.onError.listen((error) => debugPrint('LinkTrail: $error'));
// The API key is required. The install is tracked automatically by configure().
await LinkTrail.configure(apiKey: 'lt_live_…');
runApp(const MyApp());
}
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 for you, on both cold start and while running. Every callback is a
broadcast Stream, so you can listen from multiple places (e.g. a StreamBuilder).
More #
// Configuration
await LinkTrail.configure(apiKey: 'lt_live_…', options: LinkTrailOptions(...));
// Streams (native callbacks, surfaced as Dart streams)
LinkTrail.onLink; // Stream<LinkTrailLinkEvent> — (link, source: deferred | reengagement)
LinkTrail.onAttribution; // Stream<LinkTrailAttribution>
LinkTrail.onError; // Stream<LinkTrailException>
// Actions
await LinkTrail.handleDeepLink(uri); // resolve a link manually (also auto-captured)
await LinkTrail.trackInstall(force: false); // called automatically by configure()
await LinkTrail.trackEvent(name: 'purchase', value: 9.99, currency: 'USD');
// Last known state
await LinkTrail.lastAttribution;
await LinkTrail.lastDeepLink;
// iOS-only (no-ops on Android)
await LinkTrail.requestTrackingAuthorization(); // App Tracking Transparency
await LinkTrail.registerForSKAdAttribution();
await LinkTrail.updateConversionValue(3, coarseValue: LinkTrailCoarseConversionValue.medium);
Errors from the native SDKs arrive as typed LinkTrailException subtypes on onError, and are
thrown from the Future-returning calls — so you can try/catch a specific case:
try {
await LinkTrail.trackEvent(name: 'purchase');
} on LinkTrailInvalidApiKeyException {
// the key was rejected by the server
}
Consent gating (GDPR / ePrivacy) #
LinkTrailOptions.requireConsent defaults to true — deny-by-default. Until you call
LinkTrail.setConsent(true), the SDK holds the install and drops events. Deep links still
route without consent (onLink fires and the user reaches the destination); only the
attribution/tracking is gated. setConsent(false) stops sending and clears the queue.
There is no consent getter — the app is the source of truth. Persist the choice yourself and
replay it on every launch right after configure, so a previously-granted user resumes
automatically:
await LinkTrail.configure(apiKey: 'lt_live_…', options: const LinkTrailOptions(requireConsent: true));
// Replay the persisted decision (e.g. from SharedPreferences). Do nothing while undecided.
final consent = await loadConsent(); // 'granted' | 'denied' | 'undecided'
if (consent == 'granted') await LinkTrail.setConsent(true);
if (consent == 'denied') await LinkTrail.setConsent(false);
Set requireConsent: false to opt out of gating and track automatically (the pre-consent behavior).
linkDomains — re-engagement host gating #
When linkDomains is non-empty, the SDK routes re-engagement opens (app already installed)
only for those hosts — a link on an unlisted host opens the app but never navigates.
Deferred (install-time) links skip this check and route regardless. Net effect: a missing host
looks fine on a fresh install yet silently fails once installed. Leave linkDomains empty to handle
every parseable link.
Deferred attribution & the paste button (iOS) #
On iOS, deferred attribution recovers a click token the tapped link leaves on the clipboard.
LinkTrailOptions.clickTokenSource chooses how it's read:
pasteButton(default) — the token is read only when the user taps aLinkTrailPasteButton(Apple'sUIPasteControl), with no system "Allow Paste" alert. Render the button and setautoTrackInstall: falseso the install waits for the tap.automatic— the SDK reads the clipboard itself at install (no UI, but iOS shows the "Allow Paste" alert on first launch).
LinkTrailPasteButton(
width: 240,
onToken: (token) async {
await LinkTrail.trackInstallWithClickToken(token);
},
)
The widget renders the native control on iOS 16+ and nothing on Android (Play Install Referrer
handles deferred attribution there, so clickTokenSource and the paste button are ignored). Apple
restricts customization — no custom label text, font or border.
Implementation notes #
- Subscribe to
onLinkbefore anyawaitafterconfigure. A cold-start deep link (deferred first-launch, or a Universal Link that launched the app) is delivered right afterconfigure; if you await something first (e.g. reading consent from storage), the delivery lands with no listener and is lost. WireonLinkfirst, then do async setup. - Consent has no getter — replay it from your own storage on every launch (see above).
Deep-link setup #
The plugin captures links automatically, but the OS still needs to route the link to your app.
Android #
Declare your App Links host (and optionally a custom scheme) in android/app/src/main/AndroidManifest.xml:
<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="kick.linktrail.io" />
</intent-filter>
Then host a Digital Asset Links file at https://<host>/.well-known/assetlinks.json listing your
package + signing-cert SHA-256 (LinkTrail infra hosts this for your links). If links open the
browser or Play Store instead of your installed app, that's almost always App Links verification —
see the Android SDK's TROUBLESHOOTING.md.
iOS #
Add the Associated Domains capability with applinks:kick.linktrail.io, and host an
apple-app-site-association file on that domain. For a custom scheme, add it under
CFBundleURLTypes in ios/Runner/Info.plist:
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array><string>kickflip</string></array>
</dict>
</array>
On iOS 13+ the plugin registers on the UIScene lifecycle, so links are delivered correctly on
iOS 26 (FlutterSceneDelegate) as well as the classic app-delegate lifecycle.
List every link host in linkDomains #
When linkDomains is non-empty, the SDK routes re-engagement opens (app already installed) only
for those hosts — a link on an unlisted host opens the app but never navigates. Deferred
(install-time) links skip this check and route regardless, so a missing host can look fine on a
fresh install yet fail once the app is installed. Leave linkDomains empty (the default) to handle
every parseable link.
Example app #
example/ is KickFlip, a small storefront that demonstrates deferred deep linking
end to end — the same demo shipped with the native Android and iOS SDKs, rebuilt in Flutter. A link
button fires the four scenarios (home · category · product · product + voucher):
cd example && flutter run --dart-define=LINKTRAIL_API_KEY=lt_live_…
Supply your key at build time so it never lands in source control. See example/README.md.
License #
MIT — see LICENSE. Note the plugin is open source, but the LinkTrail service it talks to requires a workspace API key.