Tap App Link Flutter SDK
Attribution SDK for Flutter. Call configure, trackInstall, and setAppUserId with the same billing user id your webhook provider sends.
Install
dependencies:
tapapplink: ^0.3.3
Then flutter pub get.
Usage
import 'package:flutter/foundation.dart';
import 'package:tapapplink/tapapplink.dart';
TapAppLink.configure(TapAppLinkConfig(
publicKey: 'etk_live_…',
environment: kDebugMode
? TapAppLinkEnvironment.sandbox
: TapAppLinkEnvironment.production,
debug: kDebugMode,
));
await TapAppLink.trackInstall();
await TapAppLink.setAppUserId(await Purchases.appUserID);
final offer = TapAppLink.getOffer();
trackInstall() is safe on every launch. It creates a stable installId once, posts /ingestInstall only on the first open, and restores the attribution id and offer from local storage on later launches. On Android it reads the Play Install Referrer unless you pass installReferrer: yourself. iOS and other platforms skip the native referrer. setAppUserId sends that stored attributionId after hydrate, posts /ingestIdentify only when the app user id changes, and coalesces concurrent identical calls.
Set debug: true on TapAppLinkConfig to log each request, response and stored state. The API key is redacted in those logs.
Call resetForTesting() in debug builds before repeating a match test on the same install.
Purchases are attributed through billing webhooks. Leave out a client trackPurchase call.
Applying a code
applyCode returns success JSON only on a real 2xx result. Map each outcome to UI state like this.
On a network or timeout error only (never unknown, inactive or wrongEnvironment), retry applyCode once with the same code. That automatic retry is safe from 0.3.2 because the SDK reuses its request ID, so the server won't count the install twice. If the retry also fails, show the network copy with a Try again button. From 0.3.3, success results may include optional codeMatches and offer.creatorName.
String title;
String? subtitle;
String? shownCode;
String? offerLine;
var showTryAgain = false;
Future<TapAppLinkRedeemResult> applyCodeOnceOrRetry(String code) async {
try {
return await TapAppLink.applyCode(code);
} on TapAppLinkNetworkException {
// Network / timeout only: retry once. Safe from 0.3.2 (request ID reuse).
return await TapAppLink.applyCode(code);
}
}
try {
final result = await applyCodeOnceOrRetry(code);
final name = result.offer?.creatorName;
final promo = result.offer?.promoCode;
if (name != null && name.isNotEmpty) {
offerLine = promo != null && promo.isNotEmpty ? '$name · $promo' : name;
}
if (result.alreadyAttributed == true) {
// Success with alreadyAttributed true (may also include codeMatches)
title = "You're all set";
shownCode = null; // hide the code
subtitle = offerLine;
} else {
// Success with a new attribution
title = 'Code applied';
shownCode = code;
subtitle = offerLine;
}
} on TapAppLinkUnknownCodeException {
title = "We don't recognise that code. Check it and try again.";
subtitle = "Codes aren't case sensitive.";
} on TapAppLinkWrongEnvironmentException {
// Same customer copy as unknownCode. Never show the word "environment".
title = "We don't recognise that code. Check it and try again.";
subtitle = "Codes aren't case sensitive.";
debugPrint(TapAppLinkWrongEnvironmentException.developerWarning);
// "This code belongs to the other environment (Sandbox or Production). Check your API key."
} on TapAppLinkInactiveCodeException {
title = 'This code is no longer active.';
subtitle = 'You can still subscribe at the regular price.';
} on TapAppLinkNetworkException {
// N6: auto-retry already failed. Offer a manual Try again.
title = "We couldn't check your code. Check your connection and try again.";
showTryAgain = true;
} on TapAppLinkApplyCodeOtherException catch (error) {
title = "We couldn't check your code. Check your connection and try again.";
debugPrint('applyCode failed status=${error.status} message=${error.message}');
}
Typed failures from applyCode:
| Exception | Case |
|---|---|
TapAppLinkUnknownCodeException |
unknownCode |
TapAppLinkInactiveCodeException |
inactiveCode |
TapAppLinkWrongEnvironmentException |
wrongEnvironment |
TapAppLinkNetworkException |
network / timeout |
TapAppLinkApplyCodeOtherException |
other (has status and message) |
Publishing
CI runs format, analyse, test, publish dry-run, and an example Android debug build on every PR and push to main.
Releases publish to pub.dev from GitHub Actions via OIDC (no stored token). Push a tag that matches the package version, for example:
git tag v0.3.3
git push origin v0.3.3
The version in pubspec.yaml must match the tag (here 0.3.3).
One-time pub.dev admin setting (Kenny)
On https://pub.dev/packages/tapapplink/admin, under Automated publishing:
- Enable publishing from GitHub Actions.
- Repository:
tapapplink/tapapplink-flutter - Tag pattern:
v{{version}}
Without that, tag pushes will not be allowed to publish.
License
MIT