Flutter SDK contract
Scope: the platform-channel boundary optolink_flutter owns. It is a thin
plugin channelling the native SDKs — every HTTP call, deferred match, signal
collection, and identity store runs natively, so the wire contract is
Android + iOS, not this page.
Source-checked against optolink-flutter @ 6ad24b4, 2026-10-07 (
pubspec.yaml,lib/src/optolink.dart,link_data.dart,optolink_error.dart,android/build.gradle.kts,ios/optolink_flutter/Package.swift,CHANGELOG.md). Native pins:com.optomatica:optolink-android0.1.1,Optomatica/optolink-ios-sdkfrom 0.1.0.
Identity
| pub.dev | optolink_flutter 2.0.1 (2.0.1 is a README-only fix over 2.0.0 — no code changes) |
| Runtime | zero non-Flutter Dart dependencies — the 1.x Dart match stack is deleted, no fallback path |
| Repo | optolink-flutter/ |
| Native pins | Android optolink-android 0.1.1 (Gradle); iOS optolink-ios-sdk from 0.1.0 (SPM; CocoaPods gets the identical bits vendored in ios/optolink_flutter/Frameworks/OptoLink.xcframework) |
| Compat line | backend requirements unchanged from 1.x — "Compatible with OptoLink backend v2.1.0+" |
Spec delta: the hand-off spec records a git-dependency install; the plugin now ships on pub.dev (2.0.1's only change is the README for that release).
Floors
| Floor | Version | Enforced by |
|---|---|---|
| Flutter | ≥ 3.38.0 | pubspec flutter: constraint (the scene-lifecycle plugin API the iOS bridge calls) |
| Dart | ≥ 3.10 | pubspec sdk: constraint |
| iOS deployment target | 15.0 | Package.swift .iOS(.v15) + podspec |
| Android minSdk | 23 | plugin build.gradle.kts (the native SDK's floor) |
Three independent semver clocks (plugin / natives / backend), no release train. A breaking native or backend surface change forces a plugin major.
Channel contract (pinned)
Two channels, fixed names, identical on both platforms:
- MethodChannel
com.optomatica.optolink_flutter/channel— six methods:initialize({apiKey, orgKey, baseUrl, timeoutMs, maxRetries, clipboardEnabled}→{deviceId}),getInitialLink(payload map or null, idempotent),resolveDeferredLink(payload map or null),setIdentity({externalId}),clearIdentity,trackEvent({eventName, properties}— values coerced to strings, nulls dropped, each coercion logged). Native → DartonLogcallbacks forward the natives' INFO decision trail fire-and-forget; the Dart client registers the handler before invokinginitialize. - EventChannel
com.optomatica.optolink_flutter/links— one stream of link-payload maps, replay-last. Payload keys = native property names verbatim:linkId, path, params, matchMethod, confidence, isDeferred, score.
Delivery rule: the first isDeferred == false emission following a
plugin-performed cold-start ingestion goes to the initial-link cache
(getInitialLink() only, never the stream); every other emission (deferred,
warm) flows to the EventChannel, which replays its last emission to a late
subscriber. Preserves 1.x observable behavior: cold-start direct links on
getInitialLink(), everything else on onLink.
Error codes (both platforms, surfaced as typed OptoLinkError in
lastError): api_error (envelope-mapped — status, message, requestId,
retryAfterSeconds, details[]), connection_error, invalid_argument (bad
config, or a call made before initialize). initialize is the only thrower;
the identity trio returns Future<bool> (false only on native error); the Dart
edge never retries — pacing is the natives' maxRetries.
Wire mapping at the Dart edge
matchType carries the native matchMethod wire string verbatim (UPPER_SNAKE
— DIRECT, CLIPBOARD, INSTALL_REFERRER, FINGERPRINT_EXACT,
FINGERPRINT_SCORED, IP_FUZZY; 'unknown' if omitted). confidence maps the
lowercase wire values to MatchConfidence; unknown wire values fall back to
medium. score is the backend matchScore (int, FINGERPRINT_SCORED
only). Platform asymmetry is native fidelity, not a wrapper bug: an iOS scored
match reads 79/MEDIUM where Android reads 86/HIGH (tz omission vs client hints).
Backend attribution write path: Device identity & match attribution.
Migration from 1.x (the integrator-visible deltas)
Fresh-start identity: the natives keep their own device/first-launch stores —
upgrading installs get a new deviceId, first launch after upgrade makes
one spurious deferred-match attempt, iOS may show the one-time paste prompt.
getInitialLink() returns natively-resolved data; custom URI schemes deliver no
payload (only https links are ingested); audit headers switched to the natives'
ios/<version> / optolink-android/<version> formats. Unchanged: the bool
trio, resolveDeferredLink() → null on failure, initialize idempotence,
dispose() (Dart-side reset only).
Source-checked against optolink-flutter @ 6ad24b4, 2026-10-07. Channel/behavior claims are code-read (plus the plugin's own two-platform T1/T2 gates recorded in the spec); the natives' wire contracts are re-verified on their pages.