wherefrom
A Flutter plugin that tells your app where an install came from (the Google Play Install Referrer on Android, the Apple AdServices token on iOS) and turns it into the deep link the user wanted before they installed.
Why
- Firebase Dynamic Links is gone. To carry a link through a store install, you now read the platform's own referrer yourself.
- Reading it safely is full of traps: a Binder call that freezes the UI, a null bundle that throws, a value decoded twice, and a 90-day-old referrer routed again on every launch or after an update.
- You want one typed answer on both platforms, without manifest edits,
Info.plistkeys or an App Tracking Transparency prompt.
Install
flutter pub add wherefrom
Flutter 3.44+ / Dart 3.12+, Android minSdk 21, iOS 15. Swift Package Manager and CocoaPods both work. Nothing to configure on Android or iOS, no R8 keep rules, no native code (16 KB page ready); CI builds release versions for both stores (setup details).
Coming from imaji_install_attribution
This package was imaji_install_attribution up to 0.2.2. Replace the
dependency with wherefrom: ^0.4.0 and the import with
package:wherefrom/wherefrom.dart; fakes in tests extend
WherefromPlatform. An upgraded app keeps its consume-once state. See the
CHANGELOG.
Quick start
import 'package:wherefrom/wherefrom.dart';
Future<void> routeFromInstall(GoRouter router) async {
// null once handled, while another startup path holds the claim, or after
// a failed read (retried on the next launch, up to a budget).
final claim = await InstallAttribution.claimOnce();
if (claim == null) return;
switch (claim.result) {
case Attributed(:final referrer?):
final link = referrer.deepLink(allow: ['/items/', '/profile/']);
if (link != null) router.go(link.toString());
case Attributed(:final token?):
await sendToServer(token); // iOS: resolve with Apple's attribution API
case _:
break; // installed with no campaign
}
await claim.complete(); // never again on this install
}
Failures are typed, never thrown: InstallAttribution.lastSkipReason and
lastFailure say why claimOnce() returned null. To try your routing
without a Play install, call InstallAttribution.debugOverride(referrer: ...)
in a debug build; see the
testing guide.
Example output
The quick start above, run against the referrer
utm_source=newsletter&utm_campaign=spring&utm_content=%2Fitems%2F42:
read(): Attributed(source: googlePlay, referrer: utm_source=newsletter&utm_campaign=spring&utm_content=%2Fitems%2F42, token: null, clickedAt: null)
routed: [/items/42]
second launch: claimOnce() -> null (handled)
referrer.deepLinkResult(allow: ['/items/']) says why a referrer has no
link:
utm_content=%2Fitems%2F42 -> DeepLinkFound(/items/42)
utm_content=%252Fitems%252F42 -> DeepLinkRejected(looksDoubleEncoded)
utm_content=%2Fadmin -> DeepLinkRejected(notAllowlisted)
utm_content=https%3A%2F%2Fevil.example%2Fitems%2F1 -> DeepLinkRejected(hostNotAllowed)
utm_content=%2F%2Fevil.example -> DeepLinkRejected(unsafe)
utm_source=google-play&utm_medium=organic -> DeepLinkRejected(absent)
Features
| Feature | What you get |
|---|---|
| One typed result | Attributed, Organic or Unavailable(reason) from a read() that never throws |
| No ANR | Every Play Binder call runs on a background thread, with a timeout (default 10 s) |
| Typed failures | DeadObjectException, PERMISSION_ERROR, a null bundle and more each get their own UnavailableReason |
| Consume once | readOnce() / claimOnce(): first launch only, one claim per process, a persisted retry budget, upgrades skipped |
| Safe deep links | deepLink(allow: [...]) decodes exactly once and only returns allowlisted paths |
| Installer detection | InstallSource: Play, Galaxy Store, AppGallery, Amazon, side-load, restore |
| Link building | PlayStoreUrl here, and wherefrom_builder for pure-Dart backends, both tested against shared JSON vectors |
| Built-in Kotlin | No Kotlin Gradle Plugin line, so Flutter's KGP warning never names this plugin |
| Testable | debugOverride() for debug builds, a replaceable platform for unit tests |
Documentation
- API guide: results, reasons,
InstallReferrer,InstallSource,PlayStoreUrl - Consume once:
readOnce(), upgrade detection, the install-flow recipe,claimOnce() - The decoding rule and the language-neutral test vectors
- Adopting in an app that already reads the referrer
- Setup: requirements, tracking
main, Android build setup - Testing guide: how to see a real referrer from Play
- FAQ: ATT, iOS deep links, non-goals, alternatives
Links
Trademarks
Google Play and Android are trademarks of Google LLC. Apple, App Store and iOS are trademarks of Apple Inc. This project is independent and is not affiliated with, sponsored by or endorsed by Google or Apple.
License
MIT © saimskywalker. See LICENSE.
Libraries
- wherefrom
- Flutter install attribution and deferred deep links.