Skip to content

feat(settings): Auto-proceed to Firefox on Android in the deep-link POC - #20969

Open
dschom wants to merge 1 commit into
mainfrom
FXA-14304
Open

feat(settings): Auto-proceed to Firefox on Android in the deep-link POC#20969
dschom wants to merge 1 commit into
mainfrom
FXA-14304

Conversation

@dschom

@dschom dschom commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Because

  • The "Continue with Firefox" tap on /poc_pair_init exists only because WebKit requires external-scheme navigations to be top-level and user-initiated. It is an iOS constraint, not a general one.
  • Chromium follows a top-level intent:// navigation made from page script with no user-activation bit required, and the Android intent already carries its own store fallback in S.browser_fallback_url — so there is nothing for the JS watchdog to infer either.
  • On Android the tap therefore buys nothing: the user already acted, by scanning the QR.

This pull request

  • Fires the deep link from the mount effect on Android, keeping the happy path at zero taps. iOS and armStoreFallback are untouched.
  • Spends a one-shot sessionStorage token keyed on the target. Back from the Play Store returns to this page in the same tab, and an unguarded effect would re-fire the intent and bounce the user straight back out. sessionStorage is the only store that survives both a reload and the native-app round-trip.
  • Splits the read-only decision (shouldAutoAttempt) from the write (claimAutoAttempt), so the decision is safe to evaluate during render — including under StrictMode's double render — and the caller only navigates once the write actually succeeded.
  • Treats unavailable or throwing storage as "do not auto-attempt". Without a spent-token record there is no way to prevent the bounce loop, so falling back to the manual CTA is the safe failure.
  • Adds ?auto=0 to restore the tap-to-continue flow, following the existing ?timeout= / ?grace= on-device override convention.
  • Reveals the manual CTA after timeout for in-app WebViews that swallow intent:// (a link opened from Gmail, say). State only — it never navigates, so it cannot race the store fallback.
  • Shows the "Get Firefox" escape hatch on Android too. Reaching that card there means the auto-attempt was spent, blocked, or swallowed, so S.browser_fallback_url never got the chance to run.
  • Matches isAndroid explicitly rather than inferring "mobile and not iOS", which had swept in mobile UAs that cannot handle intent://. Those now fall through to the firefox:// + watchdog path.
  • Adds unit tests for the guard and records the Android finding in the file's header log.

Issue that this pull request solves

Closes: FXA-14304

Checklist

Put an x in the boxes that apply

  • My commit is GPG signed.
  • If applicable, I have modified or added tests which pass locally.
  • I have added necessary documentation (if appropriate).
  • I have verified that my changes render correctly in RTL (if appropriate).
  • I have manually reviewed all AI generated code.

How to review (Optional)

  • Key files/areas to focus on: PocPairInit/index.tsx — the mount effect and the shouldAutoAttempt / claimAutoAttempt pair. The guard is the part worth scrutiny; everything else is small.
  • Suggested review order: the header findings log (finding 4 explains the whole change), then the guard helpers, then the mount effect, then the test file.
  • Risky or complex parts: the one-shot guard. If it is too permissive, Back from the Play Store bounces the user out again; too strict and Android silently regresses to needing a tap. Both failure modes are silent, which is why it has direct unit coverage rather than being tested through the component.

Other information (Optional)

Draft — the central claim is not yet verified on a real device. Chromium has been progressively tightening external-app launches without user activation, and a QR-initiated page load does not hand the page a user-activation bit. Commercial deep-link services (Branch, AppsFlyer, Firebase Dynamic Links) all rely on this working, so it is expected to, but that needs confirming on a phone — which is the point of the POC. If it turns out blocked, the WebView timer degrades the page to today's behaviour, so there is no regression risk either way.

On-device matrix still to run:

Case Expected
Chrome + Firefox installed Firefox opens at the target, no tap
Chrome, Firefox not installed Play Store, no tap
Back from that Play Store page stays on /poc_pair_init showing the manual card; must not bounce back to the store. Check both a bfcache restore and a hard reload
Firefox for Android unchanged — navigates straight to the target, never shows the CTA
In-app WebView (link from Gmail) spinner for timeout ms, then the manual card
?auto=0 today's manual behaviour exactly
iOS regression CTA still present, armStoreFallback still runs

Judge "did we remove a tap" against stage/https, not localhost: over plain http the Android activity chooser can still appear, because the no-chooser path is verified App Links (app-side, FXA-13732).

🤖 Generated with Claude Code

Because:
 - The "Continue with Firefox" tap on /poc_pair_init exists only because WebKit
   requires external-scheme navigations to be top-level AND user-initiated
 - Chromium has no such requirement, and the Android intent:// already carries
   its own store fallback in S.browser_fallback_url, so there is nothing for the
   JS watchdog to infer either
 - On Android the tap therefore buys nothing: the user already acted, by
   scanning the QR

This commit:
 - Fires the deep link from the mount effect on Android, keeping the happy path
   at zero taps; iOS and armStoreFallback are untouched
 - Spends a one-shot sessionStorage token keyed on the target, because Back from
   the Play Store returns to this page in the same tab and an unguarded effect
   would bounce the user straight back out
 - Splits the read-only decision from the write, so the decision is safe to
   evaluate during render and the caller only navigates once the write succeeded
 - Treats unavailable or throwing storage as "do not auto-attempt", since
   without a spent-token record the bounce loop cannot be prevented
 - Adds ?auto=0 to restore the tap-to-continue flow for on-device A/B
 - Reveals the manual CTA after `timeout` for in-app WebViews that swallow
   intent://; state only, so it cannot race the store fallback
 - Shows the "Get Firefox" escape hatch on Android too, since reaching that card
   means S.browser_fallback_url never got the chance to run
 - Matches isAndroid explicitly rather than inferring "mobile and not iOS",
   which had swept in mobile UAs that cannot handle intent://
 - Covers the guard with unit tests and records the Android finding in the
   file's header log

Closes #FXA-14304
@dschom
dschom marked this pull request as ready for review August 5, 2026 00:38
@dschom
dschom requested a review from a team as a code owner August 5, 2026 00:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant