Skip to main content

Troubleshooting & FAQ

Symptom → cause → fix, for the failures integration testing actually hits.

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 before runApp(). The launch link is delivered exactly once at startup, and a late initialize misses it. Move await OptoLink.initialize(...) above runApp().
  • The code listened on onLink for it. Cold-start direct links never stream; they arrive via getInitialLink() only. The delivery rule has the full table.

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_SCORED needs 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_FUZZY becomes 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.