Troubleshooting & FAQ
Symptom → cause → fix, for the failures integration testing actually hits.
Link opens the browser instead of the app
iOS — the AASA has not reached the device. The backend serves /.well-known/apple-app-site-association from your AppConfig, but Apple's CDN publication can lag (budget roughly 25 minutes) and the OS then caches the file aggressively (up to 48 hours). Checks:
curl -s https://links.yourdomain.com/.well-known/apple-app-site-association
This must return HTTP 200 JSON naming <TEAMID>.<bundleId>. While iterating, add ?mode=developer to the entitlement entry to bypass the cache (remove it before any TestFlight or App Store upload). Confirm the built binary actually carries the entitlement:
codesign -d --entitlements :- build/ios/iphoneos/Runner.app
Android — App Link verification failed. The backend refreshes assetlinks.json hourly, so a freshly registered fingerprint can take up to an hour to verify. Check and re-verify:
adb shell pm get-app-links com.example.shop # want: verified
adb shell pm verify-app-links --re-verify com.example.shop
If verification never succeeds, the signing cert is usually missing from the portal AppConfig (debug installs verify against the debug cert).
Paste dialog denied (iOS)
Denying the first-launch paste prompt skips the clipboard leg; matching falls through to fingerprint/IP, so a would-be CLIPBOARD match becomes a weaker match or none. Per user: Settings → your app → Paste from Other Apps → Allow, then relaunch. For testing, uninstall/reinstall to re-arm the prompt. Opting out deliberately: clipboardEnabled: false in OptoLinkConfig.
Cold start delivered nothing
Two causes, both common:
initialize()was not awaited beforerunApp(). The launch link is delivered exactly once at startup, and a late initialize misses it. Moveawait OptoLink.initialize(...)aboverunApp().- The code listened on
onLinkfor it. Cold-start direct links never stream; they arrive viagetInitialLink()only. The delivery rule has the full table.
StateError: OptoLink not initialized
OptoLink.instance was read before initialize completed. Await initialize in main() and pass the instance down, or read OptoLink.instance only after initialization.
No match on the fingerprint/IP legs
The probabilistic legs degrade with real-world conditions, and a no-match there is often the correct answer:
FINGERPRINT_SCOREDneeds enough collected attributes to agree between click and first open. A different browser, VPN, or carrier NAT between the click and the install lowers the score; below the backend's threshold it is a no-match, by design.IP_FUZZYbecomes a no-match under Private Relay, VPNs, and shared carrier IPs, where the egress address stops being a signal.- iOS omits timezone from its match body (a deliberate platform asymmetry), so iOS scored matches land a tier lower than Android's.
The payload tells you what you got: matchType, confidence, and score on scored matches. Treat medium/low as "confirm with the user", not as a failure.
trackEvent returns false with status 400
The serialized properties map exceeded the backend's 10 KiB cap; the payload is rejected, not truncated. Values are stringified before the size check, so numbers count as their text form. Shrink the payload and resend.
MissingPluginException on a channel call
No native handler answered the call, so the Dart edge recorded it in lastError (and initialize throws it). Seen on the hot-restart edge, where the Dart-side registration from the previous run is gone; a full app restart (stop, then flutter run) restores it. The same error in pure Dart unit tests means there is no native side there.
Upgrading from 1.x: the one-time identity reset
The first launch after upgrading gets a new deviceId (1.x device profiles are not migrated), one spurious deferred-match attempt, and on iOS possibly the one-time paste prompt. Expected, once, nothing to fix. If you key server-side data off deviceId, re-key it at upgrade.