flutter_dectog 1.0.0
flutter_dectog: ^1.0.0 copied to clipboard
A Flutter plugin for DecTog dynamic links - similar to Firebase Dynamic Links but using your own backend.
flutter_dectog #
Deep links, deferred deep links and install attribution for Flutter, on your own DecTog server or the hosted one
Installation • Setup • Handle links • Install attribution • Events • Links • Privacy • Testing
The Flutter SDK for DecTog: links that open your app when it is
installed and the store when it is not, deferred deep links that survive the install, install
attribution you can audit, and post-install events. It defaults to the hosted service at
app.dectog.com and works unchanged against a self-hosted server.
Note
Pre-release. Not on pub.dev yet; the API may change until the first release.
For AI coding agents the package ships llms.txt: every public type and member,
the link URLs the SDK recognizes and the init snippets in one file, available offline in the
pub cache. It is generated from doc/api-reference.md.
Installation #
dependencies:
flutter_dectog: ^1.0.0
Requirements: Flutter 3.29 or newer (Dart 3.7), iOS 15+, Android 7+ (API 24). Dependency
ranges are kept wide (app_links 6–7, connectivity_plus 6–7, device_info_plus 11.2–13,
package_info_plus 8.3–10, http 1.x, rxdart 0.27–0.28) so the package drops into apps
that already pin these. On iOS the plugin is a Swift package: hosts on Swift Package Manager
need no extra step, and hosts that disabled SwiftPM get the same code through CocoaPods.
Setup #
1. Get an SDK key #
Sign up at app.dectog.com and pick a username: it is your link
subdomain, <username>.dectog.com. Dashboard → Settings shows your SDK key (dtg_pub_…).
The account key (dtg_…) is for servers and scripts and never goes in an app.
2. Register the link host #
Android, 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="YOUR_USERNAME.dectog.com" />
</intent-filter>
iOS, in ios/Runner/Runner.entitlements (create it if missing, and enable the Associated
Domains capability for the app in your Apple Developer account):
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:YOUR_USERNAME.dectog.com</string>
</array>
The dashboard's Apps page shows these entries filled in for your app, and its setup checklist confirms the association files are served. Custom link domains and the test environment host go in the same places; see Platform setup.
3. Initialize #
import 'package:flutter_dectog/flutter_dectog.dart';
final dectog = FlutterDectog();
await dectog.initialize(DectogConfig(
apiKey: 'dtg_pub_your_sdk_key',
enableDebugLogging: true,
));
That is all the hosted service needs: the SDK defaults to the API at https://app.dectog.com
and links on {username}.dectog.com. Self-hosting sets baseUrl, plus linkRootDomain when
the links live on a different host than the API:
await dectog.initialize(DectogConfig(
apiKey: 'dtg_pub_your_sdk_key',
baseUrl: 'https://api.example.com',
linkRootDomain: 'example.com', // links at {username}.example.com
));
Every DectogConfig option (consent, the collection switches, the Meta referrer, Apple Search
Ads, the test environment) is in the API reference.
Handle links #
Subscribe before initialize() finishes: the link that opened the app arrives during it.
dectog.onLinkClick.listen((linkClick) {
// Route on dec_deeplink when the URL carries one, else on the link's own path.
final route = linkClick.clickParams?.deepLink ?? linkClick.linkResolution?.deepLinkPath;
if (route != null) _openRoute(route);
});
// The link that launched the app, for code that runs after the stream delivered it.
final initialLink = await dectog.getInitialLink();
The launch link is consumed once per process, however many times getInitialLink() and
onLinkClick deliver it, and an Android launch from the recents screen is ignored. OAuth and
social-login callback URLs are never logged or emitted. handleUrl(url) routes a DecTog link
tapped inside the app. Details: Initialize and handle links.
Install attribution #
The SDK registers the install on the first launch and tells you where it came from:
dectog.onAppInstalled.listen((install) {
if (install.matched) {
// A user who tapped a DecTog link before installing.
print('Install attributed to ${install.username}/${install.slug}');
} else {
// Organic: no prior link click.
}
});
final verdict = await dectog.getAttributionData(); // the cached verdict, on any later launch
Routing vs credit. matched is attribution credit, granted only at matchConfidence
guaranteed (install referrer, Meta, pasteboard) or high. A probabilistic candidate the
server is less sure about still comes back as routing (DectogDeferredRoute: slug,
deepLink, clickParams, confidence) at medium confidence with matched: false: use it to
route a new user to the content they tapped, because routing is cheap to get wrong and credit
is not. A tie between candidates is low and yields neither.
dectog.onAppInstalled.listen((install) {
final route = install.routing?.deepLink ?? install.deepLink;
if (route != null) _openRoute(route); // medium or better
if (install.matched) _creditInstall(install); // high or guaranteed only
});
onAppInstalled can emit more than once: when a deterministic signal arrives after the first
registration (a Play referrer that was not ready in time, a link pasted through
DectogPasteButton) the SDK sends a late claim and re-emits the upgraded verdict
(claim == 'upgraded'). currentAppInstall always holds the latest one. An existing user who
only updated to an SDK build (installState == 'update') is never fingerprint-matched, and a
retried first launch can never register a second install.
Deterministic signals #
- Android install referrers. DecTog puts the click id in the store's install referrer
(
referrer=dtg_click=<id>&dtg_l=<username>/<slug>); the SDK's native reader (modelled on the Adjust and Branch readers) reads it back from Google Play and, withDectogCollection.oemReferrers(on by default), from Huawei AppGallery and vivo. The Samsung Galaxy Store and Xiaomi GetApps readers activate when the host app adds their referrer libraries. The read waits at mostinstallReferrerTimeout(5 s) and a late answer becomes a claim. No setup is required. - iOS pasteboard. On the first launch the SDK checks the pasteboard silently
(
UIPasteboard.hasURLs) and reads it only when it holds a URL; only a DecTog link that names a click is sent, anything else is left alone. The dashboard's iOS tap-to-store page is the web side that copies the link before opening the App Store. On iOS 16+ the read shows the system "Allow paste" alert;willShowPasteboardPrompt()tells you beforehand, andDectogPasteButton(Apple'sUIPasteControl) lets a user paste an invite link without it. - Meta's on-device referrer (
fbAppId) and Apple Search Ads (DectogCollection.appleSearchAdsToken, resolved server-side) are opt-in.
The whole ladder, with what each signal needs from the app, is under Deterministic attribution.
Events #
Events queue durably on the device and go out in batches. The server stamps each with the install's matched link and the link that last re-opened the app, so the dashboard shows leads and revenue per link.
// The first identify of a new user on a fresh install is their signup lead.
// Unchanged repeat calls are skipped, so calling it on every launch is safe.
await dectog.identify(customerId: user.id, email: user.email);
await dectog.trackConversion(
revenue: 9.99,
currency: 'USD',
transactionId: purchase.transactionId, // counted once, even if reported again
store: DectogStore.appStore,
);
await dectog.trackEvent('level_up', data: {'level': 3});
await dectog.trackScreenView('product', params: {'id': 42}); // outside every event total
// or every page route at once:
// MaterialApp(navigatorObservers: [DectogNavigatorObserver()])
await dectog.flush(); // force-send (otherwise 20 events / 30 s)
await dectog.logout(); // flush + forget the customer id
Consent first. Initialize with DectogConfig(enabled: false) and nothing is sent, and the
pasteboard is never read, until setEnabled(true); then the install, the held events and the
launch link's report go out. The choice is persisted and wins over enabled on later launches.
setEnabled(false) stops all collection at any time.
Create links from the app #
// Only slug is required; a null target inherits the app's fallback URL.
final link = await dectog.createLink(
slug: 'my-link',
target: 'https://example.com',
iosDeepLinkPath: 'products/42',
androidDeepLinkPath: 'products/42',
campaign: 'spring',
);
await dectog.updateLink('my-link', campaign: 'summer', ogTitle: ''); // null keeps, '' clears
final temp = await dectog.createTempLink(target: 'https://example.com', ttlSeconds: 3600);
// One referral link per customer, created on first use; needs no links:write key.
final invite = await dectog.createReferralLink(customerId: user.id);
getLinks, getLink, deleteLink, resolveLink and the temporary-link equivalents complete
the set; creating and changing links needs an SDK key with links:write. See
Create links from the app.
What the SDK collects #
Everything respects setEnabled(false) and the DectogConfig.collect switches, and is
declared in the bundled iOS privacy manifest.
- Device tier (on by default): a vendor-scoped device id (
ANDROID_ID, or the IDFV kept in the Keychain so it survives a reinstall;persistDeviceId: falsesends the plain IDFV), model, OS, screen metrics, connectivity and carrier, and the display and accessibility settings (text size and Increase Contrast on iOS, dark mode and reduce motion on both). - Store install referrers on Android and a copied DecTog link on iOS, as above.
- What the app passes to
identifyand the event calls.
The core package never reads an advertising id: no Play services dependency, no AD_ID
permission, no AdSupport or App Tracking Transparency. Apps that want one add
flutter_dectog_adid, which handles the ATT prompt and passes
the id through advertisingIdProvider. The store disclosure for each switch is in the
API reference and under
Device data and privacy.
Testing install attribution #
Keep test traffic out of your live numbers with a test environment (dashboard Settings → Test
environment): a second account, t-<username>, with the same apps. Put its SDK key in debug
builds and add t-<username>.dectog.com to their associated domains:
await dectog.initialize(DectogConfig(
apiKey: kReleaseMode ? liveSdkKey : testSdkKey,
environment: kReleaseMode ? DectogEnvironment.live : DectogEnvironment.test,
));
To simulate a first install, delete the app (iOS) or run adb shell pm clear <package>
(Android), tap a DecTog link, install and open the app: onAppInstalled fires with
matched: true. diagnostics() compares the build with the dashboard's record of the app
(bundle id or package, URL scheme, App Links verification per host, live or test key) and
reports every mismatch. See Testing.
Reference #
doc/api-reference.md: every public type and member, with the store disclosures.llms.txt: the same for AI coding agents.- docs.dectog.com/sdk: setup, attribution, events, privacy and troubleshooting.
example/: a Flutter app with every flow. Copyenv/dev.json.exampletoenv/dev.json, thenflutter run --dart-define-from-file=env/dev.json.- Related: the server and the React Native SDK, which shares this SDK's native reads and golden fixtures.
Development #
dart format --output=none --set-exit-if-changed lib test tool example/lib
flutter analyze
flutter test # mocks the platform channel
cd example && flutter build apk --debug && flutter build ios --simulator --debug # compiles the Kotlin and Swift
The Android plugin builds on AGP 8 and AGP 9 alike, applying the Kotlin Gradle plugin itself
only when neither the host nor Flutter's tooling already did. A public API change updates
doc/api-reference.md, then dart run tool/gen_llms.dart regenerates llms.txt. See
CONTRIBUTING.md.
