attriax_flutter 0.4.1
attriax_flutter: ^0.4.1 copied to clipboard
Attriax SDK for Flutter - A powerful and easy-to-use solution for mobile attribution, deep-linking, and analytics.
attriax_flutter #
Attriax mobile attribution SDK for Flutter.
Overview #
This is the main Flutter package for Attriax. The public Attriax class is a thin wrapper over an internal runtime that owns orchestration, logging, request queuing, synchronization state, and deep-link handling.
Public SDK surfaces are grouped behind focused facades such as attriax.consent, attriax.tracking, attriax.synchronization, attriax.deepLinks, attriax.referrer, and attriax.skan. The root Attriax entrypoint now stays focused on lifecycle, reset, receipt validation, and those top-level facades.
Installation #
Add to your pubspec.yaml:
dependencies:
attriax_flutter: ^0.4.1
For local workspace development inside this repository, keep using the existing path-based workspace setup instead of a hosted dependency.
Requirements #
- Dart
^3.8.0 - Flutter
>=3.29.0
These floors match the package pubspec.yaml and the current federated SDK
workspace. If your app uses an older toolchain, upgrade before evaluating this
package so plugin registration and generated-client dependencies stay aligned.
Usage #
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:attriax_flutter/attriax.dart';
final navigatorKey = GlobalKey<NavigatorState>();
final attriax = Attriax(
config: const AttriaxConfig(
projectToken: 'ax_your_app_token',
gdprEnabled: true,
),
);
// Initialize the local runtime first.
await attriax.init();
// Handle startup attribution in the background.
unawaited(processAttriaxStartup(attriax));
Future<void> processAttriaxStartup(Attriax attriax) async {
final initialDeepLink = await attriax.deepLinks.waitForInitialDeepLink();
final originalInstallReferrer = await attriax.referrer
.getOriginalInstallReferrer();
final sessionReferrer = await attriax.referrer.getSessionReferrer(
timeout: const Duration(seconds: 5),
safe: true,
);
debugPrint(
'Original install referrer: ${originalInstallReferrer?.campaign}',
);
debugPrint('Session referrer: ${sessionReferrer?.uri.toString() ?? 'none'}');
debugPrint(
'Initial deep link: ${initialDeepLink?.uri.toString() ?? 'none'} (found: ${initialDeepLink?.found ?? false})',
);
}
await attriax.tracking.recordPurchase(
revenue: 99,
currency: 'USD',
productId: 'pro_yearly',
transactionId: 'order_123',
store: 'app_store',
metadata: const <String, Object?>{'paywallVariant': 'spring_2026'},
);
final createdDynamicLink = await attriax.deepLinks.createDynamicLink(
name: 'Referral share link',
destinationUrl: 'https://attriax.com/invite',
group: 'referrals',
socialPreview: const AttriaxDynamicLinkSocialPreview(
title: 'Join me on Attriax',
description: 'Open the app with my referral attached.',
),
data: const <String, Object?>{
'inviterId': 'user_123',
'campaign': 'spring_referral',
},
);
debugPrint('Share this short URL: ${createdDynamicLink.link.shortUrl}');
attriax.deepLinks.stream.listen((event) {
if (!event.found) {
return;
}
navigatorKey.currentState?.pushNamed(
'/deep-link',
arguments: <String, Object?>{
'uri': event.uri.toString(),
'data': event.data,
},
);
});
MaterialApp(
navigatorKey: navigatorKey,
navigatorObservers: [
AttriaxNavigationObserver(attriax: attriax),
],
// routes: ...
);
await attriax.init() only waits for local SDK startup work such as restoring persisted state, registering listeners, and starting the queue. It does not wait for the network-backed app-open request to finish.
The SDK still enforces app-open-first delivery behind the scenes: it sends the app-open request before any other queued SDK request, and if app-open fails, later queued requests stay deferred until an app-open request succeeds.
Do not block your splash screen, router construction, runApp(), or other first-frame startup work on referrer.getOriginalInstallReferrer() or deepLinks.waitForInitialDeepLink(). Those results may wait on cached or network-backed attribution work and should normally be handled in background tasks after init() resolves.
Use attriax.referrer.getReinstallReferrer() when you specifically need reinstall attribution, getSessionReferrer() for the deep link that opened the current session, and getLatestDeepLinkReferrer() for the most recent handled deep-link event.
If you truly need a fire-and-forget local startup path, you can still intentionally call unawaited(attriax.init()), but that is separate from deferred deep-link and install-referrer handling. The recommended baseline is still: await init(), then process startup attribution asynchronously.
The appVersion, appBuildNumber, and appPackageName fields let you override what the SDK reports to the API. That is useful for staged rollouts, white-label apps, and internal testing. On Flutter web, when those overrides are not set, the SDK also tries to read the build's version.json file so hosted deployments can report the app version automatically.
If your app consumes the incoming URL before Attriax sees it, forward the accepted
route manually with attriax.deepLinks.recordDeepLink(uri: incomingUri, source: 'custom_router').
GDPR Consent #
gdprEnabled defaults to false. Enable it when your app wants Attriax to
wait for a GDPR decision before sending GDPR-gated tracking activity.
anonymousTracking defaults to true and keeps anonymous-capable traffic
flowing without deviceId while consent is unresolved. Set it to false when
your app wants to buffer that traffic locally until consent allows identified
delivery.
final needsLocalConsent = await attriax.consent.gdpr.needsConsent(
localOnly: true,
);
if (needsLocalConsent) {
// Present the app's consent UI here.
}
final needsConsent = await attriax.consent.gdpr.needsConsent();
attriax.consent.gdpr.setConsent(
analytics: true,
attribution: true,
adEvents: false,
);
// Or, when the device should not be gated at all:
attriax.consent.gdpr.setNotRequired();
// Reset later if the app needs to re-ask:
attriax.consent.gdpr.reset();
Use state, values, and isWaitingForConsent to drive your privacy UI and
settings screen.
See doc/gdpr-and-anonymous-analytics.md for the full GDPR and anonymous analytics behavior, including how anonymous tracking avoids device identity before consent and how to opt into local buffering instead.
Dynamic Link Creation #
Use attriax.deepLinks.createDynamicLink() when your app needs a short shareable URL created at
runtime. Attriax generates the short code server-side, applies app-level
dynamic-link defaults when fields are omitted, and returns the saved link data
including the final short URL.
final result = await attriax.deepLinks.createDynamicLink(
destinationUrl: 'https://attriax.com/invite',
group: 'creator-program',
socialPreview: const AttriaxDynamicLinkSocialPreview(
title: 'Creator invite',
description: 'Open the app with the creator campaign attached.',
),
data: const <String, Object?>{
'creatorId': 'alex',
'source': 'flutter_demo',
},
);
debugPrint(result.link.shortUrl);
Notes:
prefixis optional and only works when the current app plan allows custom prefixes.destinationUrlmay be omitted when the app already defines a default dynamic-link destination.- The preview image always comes from the app-level dynamic-link defaults.
datamust be a JSON object and is returned later in resolved deep-link payloads.
Page Tracking #
Use attriax.tracking.recordPageView() when you want page-level analytics and funnels without
manually naming a raw custom event. Attriax stores these under the standardized
page_view event name and surfaces them separately in the dashboard.
If your app relies on Flutter navigation, attach AttriaxNavigationObserver
to your MaterialApp or CupertinoApp to emit page views automatically for
named PageRoutes. If you use a router that does not populate route names,
provide a custom routeNameResolver.
MaterialApp(
navigatorKey: navigatorKey,
navigatorObservers: [
AttriaxNavigationObserver(attriax: attriax),
],
);
await attriax.tracking.recordPageView(
'/checkout',
pageClass: 'CheckoutPage',
previousPageName: '/cart',
parameters: const <String, Object?>{'experiment': 'paywall_v2'},
);
Analytics Vocabulary #
The package exports AttriaxAnalyticsEventKeys and
AttriaxAnalyticsParamKeys so your app, dashboard funnels, and SKAN schema can
share the same event vocabulary.
Use the predefined event keys for the most common conversion milestones:
- account lifecycle:
sign_up,login - onboarding and game progression:
tutorial_begin,tutorial_complete,level_start,level_complete,level_up - checkout and revenue:
add_payment_info,add_to_cart,checkout_started,purchase,refund,subscription_started,subscription_renewed,trial_started - ads and navigation:
ad_*events pluspage_view
Use the predefined parameter keys when those events need consistent payloads.
The most common ones are revenue, currency, productId, transactionId,
paymentType, method, level, value, pageName, adPlacement, and
source.
await attriax.tracking.recordEvent(
AttriaxAnalyticsEventKeys.addPaymentInfo,
eventData: const <String, Object?>{
AttriaxAnalyticsParamKeys.paymentType: 'apple_pay',
AttriaxAnalyticsParamKeys.value: 'annual_paywall',
},
);
await attriax.tracking.recordEvent(
AttriaxAnalyticsEventKeys.tutorialBegin,
eventData: const <String, Object?>{
AttriaxAnalyticsParamKeys.source: 'first_session',
},
);
These constants are intentionally curated rather than exhaustive. If your app
needs a custom event name, you can still send it with
attriax.tracking.recordEvent(...); use the shared constants whenever you want
a stable, SDK-documented conversion event name.
Ad Events #
Use the standardized ad lifecycle methods when you want ad delivery, engagement, failures, rewards, and paid callbacks to show up in the same analytics vocabulary across SDKs.
await attriax.tracking.recordAdEvent(
AttriaxAdEventType.load,
adNetwork: 'admob',
adUnitId: rewardedAdUnitId,
adPlacement: 'level_complete',
adFormat: 'rewarded',
);
await attriax.tracking.recordAdEvent(
AttriaxAdEventType.show,
adNetwork: 'admob',
adUnitId: rewardedAdUnitId,
adPlacement: 'level_complete',
adFormat: 'rewarded',
);
await attriax.tracking.recordAdEvent(
AttriaxAdEventType.impression,
adNetwork: 'admob',
adUnitId: rewardedAdUnitId,
adPlacement: 'level_complete',
adFormat: 'rewarded',
);
await attriax.tracking.recordAdEvent(
AttriaxAdEventType.reward,
adNetwork: 'admob',
adUnitId: rewardedAdUnitId,
adPlacement: 'level_complete',
adFormat: 'rewarded',
rewardType: 'coins',
rewardAmount: 50,
);
await attriax.tracking.recordAdRevenue(
revenue: 125000,
currency: 'USD',
revenueInMicros: true,
adNetwork: 'admob',
adPlacement: 'level_complete',
adFormat: 'rewarded',
adType: 'paid_event',
);
When an ad SDK exposes failures, clicks, dismissals, or mediation metadata,
send them through attriax.tracking.recordAdEvent(...) with the matching
AttriaxAdEventType, failureReason, and metadata so the ad-events
analytics page can group the callbacks cleanly.
Host Deep Link Setup #
attriax_flutter uses an internal Attriax deep-link bridge on Android and iOS, but the
host app still owns the platform registration files and most runner hooks.
- Android: add the intent filter to your launcher activity and keep your SHA-256 fingerprints current. The
attriax_flutter_androidplugin already injectsflutter_deeplinking_enabled=falseso Flutter's built-in handler does not compete with Attriax. - iOS: add
<key>FlutterDeepLinkingEnabled</key><false/>toios/Runner/Info.plist, add the Associated Domains entitlement, and test on a physical device after reinstalling. If your app requests ATT through Attriax or its own consent flow, the same plist must also includeNSUserTrackingUsageDescriptionbeforerequestTrackingAuthorizationOnInitorconsent.att.requestTrackingAuthorization()runs. These plist changes still belong to the consuming app. - Web: the SDK reads the initial URL automatically. If your router consumes the incoming URL first, forward it with
attriax.deepLinks.recordDeepLink(uri: Uri.base, source: 'web_router'). The Attriax app configuration must also allow every browser origin that will call the SDK, including local dev origins such ashttp://localhost:3000, in the dashboard setup page's Web allowed browser origins list. - macOS, Linux, Windows: automatic deep-link capture is not bundled yet. Accept the URI in your runner or activation handler and forward it with
attriax.deepLinks.recordDeepLink(uri: incomingUri, source: 'desktop_router').
The example runner files shipped with this package include the Android and iOS host-side setup. Desktop examples stay intentionally minimal and expect manual forwarding when you wire a desktop protocol handler.
Because Android install referrer is the strongest attribution input for mobile installs, validate at least one Play-distributed Android build before release. iOS does not have an install-referrer equivalent, so universal-link handling and the initial app-open request become the primary checks there.
SKAdNetwork Developer-Copy Setup #
SKAdNetwork developer-copy reporting is separate from Attriax deep-link setup. There are two independent pieces:
- Your app usually lets Attriax download the SKAN schema from the dashboard during app open.
- Apple sends developer-copy install-validation postbacks to the URL declared in the consuming app's
Info.plist.
Use local AttriaxConfig.skan only when you want to disable SDK-side conversion updates in app code:
final attriax = Attriax(
config: const AttriaxConfig(
projectToken: 'ax_your_app_token',
skan: AttriaxSkanConfig(enabled: false),
),
);
On iOS, add the hardcoded Attriax developer-copy host to the advertised app's
ios/Runner/Info.plist:
<key>NSAdvertisingAttributionReportEndpoint</key>
<string>https://skan.attriax.com</string>
<key>SKAdNetworkPostbackURLList</key>
<array>
<string>https://skan.attriax.com</string>
</array>
Keep the configured value pathless. Apple and newer toolchains derive the
well-known callback path automatically. Apple documents that
NSAdvertisingAttributionReportEndpoint uses only the registrable domain and
ignores subdomains, so do not point these settings at an app-specific host such
as myapp.attriax.com.
Attriax accepts developer-copy callbacks at:
https://skan.attriax.com/.well-known/skadnetwork/report-attribution/https://attriax.com/.well-known/skadnetwork/report-attribution/
Dashboard SKAN surfaces are now the normal source of runtime SKAN rules. The SDK
downloads the app schema during app open, while local AttriaxConfig.skan
configuration is reserved for explicit app-code opt-out cases such as disabling
local SKAN updates in a particular build.
Save the numeric iOS App Store ID in the Attriax dashboard so Attriax can map incoming postbacks back to the app.
Uninstall Tracking #
Attriax accepts uninstall-tracking tokens from mobile apps so the backend can probe whether the app instance is still reachable.
- Android: call
tracking.registerFirebaseMessagingToken(token)after your app receives an FCM registration token and again whenever Firebase rotates that token. - Apple platforms: if your app receives an FCM registration token, call
tracking.registerFirebaseMessagingToken(token)there too. - Apple platforms: if your app also receives the native APNs device token, call
tracking.registerApplePushToken(token)to register it as a separate Apple token provider. - Pass
nullor whitespace to either method when you need to clear the currently registered token for that provider.
Attriax probes FCM registrations through Firebase Admin. When you configure the APNs auth key in the Attriax dashboard, Attriax can also probe native APNs tokens directly for Apple-platform uninstall detection.
import 'dart:async';
import 'package:attriax_flutter/attriax.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/foundation.dart';
Future<void> syncPushTokensWithAttriax(Attriax attriax) async {
final messaging = FirebaseMessaging.instance;
await messaging.requestPermission();
final fcmToken = await messaging.getToken();
await attriax.tracking.registerFirebaseMessagingToken(
fcmToken,
metadata: const <String, Object?>{'source': 'firebase_messaging'},
);
if (!kIsWeb &&
(defaultTargetPlatform == TargetPlatform.iOS ||
defaultTargetPlatform == TargetPlatform.macOS)) {
final apnsToken = await messaging.getAPNSToken();
await attriax.tracking.registerApplePushToken(
apnsToken,
metadata: const <String, Object?>{'source': 'firebase_messaging_apns'},
);
}
messaging.onTokenRefresh.listen((token) {
unawaited(
attriax.tracking.registerFirebaseMessagingToken(
token,
metadata: const <String, Object?>{'source': 'firebase_messaging_refresh'},
),
);
});
}
Synchronization #
Most apps do not need to surface synchronization state in normal product UI. Treat it as an optional diagnostics hook for QA, support flows, offline-aware surfaces, or developer tooling.
- Use
attriax.synchronization.isSynchronizedwhen you only need a single readiness boolean. - Use
attriax.synchronization.stateorattriax.synchronization.stateswhen you need to distinguish between initializing, actively syncing, deferred queue work, offline, failed, disabled, and fully synchronized states.
Local Storage And Reset #
The SDK persists a small amount of local state so it can survive process restarts:
- stable Attriax device identity and device-id source
- enabled and events-enabled runtime flags
- first-launch marker
- queued requests and queue diagnostics, including pending crash/error payloads
- current session snapshot
- cached install-referrer details
Use await attriax.reset() when the host app needs to clear SDK-owned local
state for privacy tooling, logout cleanup, QA reset flows, or support recovery.
After reset() completes, call await attriax.init() again before reusing the
same instance.
reset() clears SDK-owned local storage only. It does not delete any server-side
analytics or attribution data that has already been delivered to Attriax.
Logging #
- Debug builds use verbose SDK logging by default.
- Non-debug builds keep logging to warning and error paths.
- Set
enableDebugLogsinAttriaxConfigwhen you need to override debug verbosity.
Privacy Controls #
Attriax collects only the platform context needed for attribution and analytics, but the host app owns its store disclosures and consent flow. Configure these options before init():
final attriax = Attriax(
config: const AttriaxConfig(
projectToken: 'ax_your_app_token',
collectAdvertisingId: false,
automaticCrashReportingEnabled: false,
anonymousTracking: true,
requestTrackingAuthorizationOnInit: false,
trackingAuthorizationStatusTimeout: Duration(seconds: 15),
),
);
collectAdvertisingIdcontrols GAID collection on Android and IDFA collection on Apple platforms.- When
collectAdvertisingIdisfalse, the SDK stops using ATT and advertising IDs for its own native context collection, but host apps can still callconsent.att.getTrackingAuthorizationStatus()andconsent.att.requestTrackingAuthorization()for their own consent flow. automaticCrashReportingEnabledcontrols automatic Flutter/native crash handlers. Manualtracking.recordError()calls remain available when automatic handlers are disabled.anonymousTrackingkeeps anonymous-capable GDPR traffic flowing without device identity while consent is unresolved. Disable it if your app prefers to buffer that activity locally until consent allows identified delivery.requestTrackingAuthorizationOnInitrequests ATT during SDK startup when advertising ID collection is enabled, then waits for the user-driven result before iOS context collection continues. AddNSUserTrackingUsageDescriptiontoios/Runner/Info.plistbefore enabling this on iOS.trackingAuthorizationStatusTimeoutonly applies whenrequestTrackingAuthorizationOnInitisfalse. During startup, the SDK polls ATT status for up to that duration so an app-managed consent flow can still callconsent.att.requestTrackingAuthorization()without being raced by SDK initialization.
To check ATT state or request ATT manually after your own consent or onboarding UI:
Add NSUserTrackingUsageDescription to ios/Runner/Info.plist before shipping or testing the manual ATT prompt.
final currentStatus = await attriax.consent.att.getTrackingAuthorizationStatus();
debugPrint('Current ATT status: $currentStatus');
final updatedStatus = await attriax.consent.att.requestTrackingAuthorization();
debugPrint('Updated ATT status: $updatedStatus');
By default, manual ATT requests do not use a timeout. Pass timeout: only if your own flow needs one.
The Apple implementation package now bundles PrivacyInfo.xcprivacy files for its own SDK-side required-reason API usage and SDK-owned data collection declarations. Today those manifests cover Device ID on iOS and macOS plus Crash Data on iOS.
Android apps that allow advertising ID collection must account for the AD_ID permission and Play Console Data Safety answers. iOS apps that enable tracking or IDFA collection still own the App Store privacy labels, ATT purpose string, and any app-level tracking domains or privacy-manifest declarations that match the configuration they actually ship.
Deep Links #
- Read
attriax.deepLinks.streamas a broadcast stream with no buffering. - Use
attriax.deepLinks.initialDeepLink,initialDeepLinkResolved, andwaitForInitialDeepLink()when you need synchronous initial-link state plus an awaitable completion handle. - Read
attriax.deepLinks.latestDeepLinkwhen you need the most recent handled deep-link event, including deferred deep links. - Each
AttriaxDeepLinkEventis already resolved and exposesuri,clickedAt,consumedAt,trigger,isAttriaxSubDomain,found, and any matcheddataimmediately. - Use
attriax.deepLinks.rawStreamtogether withwaitResolution(rawEvent)only when you specifically need to observe the pre-resolution raw input and then await its resolved event.
Startup handling:
final initialDeepLink = await attriax.deepLinks.waitForInitialDeepLink();
final path = initialDeepLink?.uri.path;
debugPrint(
'Initial deep link path: ${path ?? 'none'} (found: ${initialDeepLink?.found ?? false})',
);
Stream handling:
attriax.deepLinks.stream.listen((event) {
if (!event.found) {
return;
}
navigatorKey.currentState?.pushNamed(
'/deep-link',
arguments: <String, Object?>{
'uri': event.uri.toString(),
'data': event.data,
},
);
});
Manual forwarding for custom routers, web, or desktop runners:
await attriax.deepLinks.recordDeepLink(
uri: incomingUri,
source: 'custom_router',
);
Typed Payloads #
- Custom request metadata uses regular
Map<String, Object?>values. - For deep-link data and dynamic-link payloads, prefer primitive JSON values (
String,num,bool, ornull) so typed decoding stays predictable across platforms and backend versions. - SDK transport requests and responses are concrete types internally; public payloads do not rely on
dynamicmaps.
Examples #
- Package example app:
example/ - Package example tests:
example/test/ - This publishable package now keeps the shipped example focused on minimal integration. Richer public demo flows live elsewhere in the repository, and internal QA flows stay in the non-public internal tester app.
Platform Support #
- Android ≥ Android 5.0 (API Level 21) with built-in Attriax deep-link bridge
- iOS ≥ iOS 13.0 with built-in Attriax deep-link bridge
- Web with initial-URL deep-link support
- Windows with manual forwarding
- macOS with manual forwarding
- Linux with manual forwarding
Architecture #
This package provides the public API and uses the platform interface only for native-only data.
- Depends on:
attriax_flutter_platform_interface - Implements native collectors via
attriax_flutter_androidandattriax_flutter_ios - Keeps the public
AttriaxAPI as a wrapper over internal runtime, queue, logger, and typed transport components
Contributing #
See the parent README.md for contribution guidelines.