Skip to content

decisa-ai/decisa-flutter

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

decisa_sdk — Decisa mobile attribution SDK for Flutter

⚠️ Unverified scaffold — not yet built or tested on a Flutter toolchain. The Dart, Kotlin, and Swift sources in this package were written to be clean, correct, and idiomatic, but have not been compiled or run against flutter, gradle, or pod. Verify on a real machine before publishing.

A first-party mobile attribution SDK that connects an ad click → app install → in-app conversions, reusing Decisa's existing public attribution ingest. It is a "native pixel": it authenticates with your public app_key, reads the platform's deferred-attribution signal on first launch, and then posts identify/track events to Decisa's public ingest. Pixel membership is configured server-side in the dashboard — no rebuild when you add pixels.

  • Android is deterministic: the store redirect carries a match token (dcs_mclid) in the Play Install Referrer.
  • iOS is probabilistic: there is no referrer, so the install is matched server-side by IP + timestamp; the AdServices token enriches Apple Search Ads.

The public app_key, never a secret

The SDK authenticates only with your app's public app_key — the string that begins with dcs_app_. It is a public credential (same trust class as the web pixel_key) and is sent in the request body. It is not a secret and is safe to ship inside an APK/IPA.

Which pixels receive events is configured in the Decisa dashboard — add or remove member pixels without rebuilding the app or waiting for store review.

Never put a secret key in a mobile app. The server-side Decisa SDKs (Node, PHP, Python) authenticate with a secret dcs_ak_ / dcs_sk_ key. A mobile binary can be decompiled, so a secret in the binary can be extracted and used to forge conversions and poison your attribution. This SDK refuses anything that is not a dcs_app_ key (an assertion fires in debug builds).


Install

Add the dependency (from pub.dev once published, or via a path/git ref while it lives in this monorepo):

dependencies:
  decisa_sdk: ^0.2.0

Minimum platform versions: Android minSdk 21, iOS 12.0 (the AdServices token requires iOS 14.3+, guarded at runtime).


Quickstart

import 'package:decisa_sdk/decisa_sdk.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // First launch: reads the native deferred-attribution signal, POSTs
  // /v1/resolve, and persists the visitor id + UTM attribution. Subsequent
  // launches reuse the persisted visitor id (no re-resolve).
  Decisa.start(appKey: 'dcs_app_your_public_key');

  runApp(const MyApp());
}

// When a user signs in or is known, associate them. Email/phone are SHA-256
// hashed on-device — raw PII never leaves the phone.
await Decisa.identify(
  userId: 'user_123',          // sent as external_id (not hashed)
  email: 'jane@example.com',   // hashed client-side → email_sha256
);

// Record a conversion. The install's utm_* attribution (and the device madid,
// if already available) ride along in the event metadata.
await Decisa.track(DecisaEvent.purchase(value: 49.90, currency: 'USD'));

Events

DecisaEvent has a named constructor per canonical event, each mapped to the right event_name the backend expects:

DecisaEvent.purchase(value: 49.90, currency: 'USD');
DecisaEvent.lead();
DecisaEvent.completeRegistration();
DecisaEvent.startTrial();
DecisaEvent.subscribe();
DecisaEvent.addToCart(value: 19.99, currency: 'USD');
DecisaEvent.initiateCheckout();
DecisaEvent.addPaymentInfo();
DecisaEvent.viewContent();
DecisaEvent.pageView();
DecisaEvent.search();
DecisaEvent.appInstall();
DecisaEvent.custom('viewed_pricing', metadata: {'plan': 'pro'});

A fresh event_id (evt_ + UUID v4) is generated per event so retries dedupe server-side and never double-count. The canonical names are PageView, ViewContent, Search, AddToCart, AddPaymentInfo, InitiateCheckout, Lead, CompleteRegistration, Purchase, StartTrial, Subscribe, AppInstall, Custom.


Deferred deep links (how the install gets attributed)

The hard part of mobile attribution is connecting "who clicked the ad" to "who opened the app after installing" across a multi-day gap with no shared cookie. Decisa solves this with a server-minted click and a per-platform signal:

  1. Mint the click. Point your ad at a Decisa UTM short link in ?app=1 mode: https://api.decisa.ai/k/<slug>?app=1. The backend mints a click (carrying the UTM attribution, a hashed IP, and a timestamp) and redirects the user to the right store.
  2. Configure store URLs. On the UTM link's metadata set android_store_url and ios_store_url so the ?app=1 redirect sends each platform to the correct store listing. For Android, the Play redirect embeds dcs_mclid=<token> in the install referrer.
  3. Resolve on first launch. Decisa.initialize reads the native signal:
    • Android — the Play Install Referrer's dcs_mclid → a deterministic match.
    • iOS — no referrer; the first /v1/resolve call is matched probabilistically by IP + timestamp server-side. The AdServices token additionally enriches Apple Search Ads campaigns only.
  4. Bind and track. /v1/resolve returns a visitor_id bound to the click. The SDK persists it and uses it for every later identify / track — from then on it is just a native pixel reusing Decisa's existing matcher and CAPI fanout.

If resolve finds no match (or the key is unknown, returning a silent 204), the SDK mints a local fallback visitor_id (v_…) so tracking still works; that install is simply unattributed.

The SDK does not prompt for App Tracking Transparency (ATT) or read the IDFA in v1. madid (IDFA/GAID) is attached to event metadata only if it is already available without a prompt.


Backend contract

All endpoints are public and live under the base URL (default https://api.decisa.ai). Responses use the envelope { data, meta, error }.

Endpoint Purpose Returns
POST /v1/resolve First-run deferred-attribution lookup. Body: { app_key, mclid?, adservices_token? }. 200 { data: { visitor_id, matched, match_type, utm_* } } or silent 204 for an unknown key.
POST /v1/identify Associate hashed identity with the visitor. Body: { app_key, visitor_id, email_sha256?, phone_sha256?, fn_sha256?, ln_sha256?, external_id? }. 202
POST /v1/track Record a pixel event. Body: { event_id, event_name, visitor_id, app_key, value? | value_cents?, currency?, url?, occurred_at?, is_test?, metadata? }. 202

Architecture (where things live)

lib/
  decisa_sdk.dart            # public exports (Decisa, DecisaEvent, DecisaAttribution)
  src/
    decisa_client.dart       # Decisa.initialize / identify / track orchestration
    decisa_event.dart        # DecisaEvent + named constructors + wire mapping
    decisa_attribution.dart  # resolved attribution model + JSON (de)serialization
    transport.dart           # HTTP POST + { data, meta, error } envelope decode
    persistence.dart         # shared_preferences-backed visitor_id / external_id
    hashing.dart             # client-side SHA-256 of email/phone/name
    deferred_channel.dart    # MethodChannel('ai.decisa.sdk/deferred') wrapper
android/                     # Kotlin plugin: Play Install Referrer → dcs_mclid
ios/                         # Swift plugin: AdServices attribution token
example/                     # minimal example app
test/                        # Dart unit tests (injectable transport/persistence/channel)

The transport, persistence, and deferred-signal channel are all injectable on Decisa.initialize (@visibleForTesting), which is how the unit tests exercise resolve/identify/track without a live backend or a real device.


License

MIT. See LICENSE.

About

Official Flutter mobile attribution SDK for Decisa (deferred deep link + identify + CAPI events)

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors