Skip to content

Add FXIOS-16685 [FxA Pairflow] Support pairing v2 via WebChannel OAuth Part 1 - #35460

Merged
issammani merged 1 commit into
mozilla-mobile:mainfrom
vbudhram:fxa-pair-ios-v2
Sep 2, 2026
Merged

Add FXIOS-16685 [FxA Pairflow] Support pairing v2 via WebChannel OAuth Part 1#35460
issammani merged 1 commit into
mozilla-mobile:mainfrom
vbudhram:fxa-pair-ios-v2

Conversation

@vbudhram

@vbudhram vbudhram commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

📜 Tickets

Jira ticket
Github issue

💡 Description

In v2 the page drives pairing: it asks the browser for OAuth parameters over the WebChannel rather than the app minting a supplicant URL itself. Follows the Android implementation.

Scope note. This PR was split. It now contains only the pairing v2 feature. The changes that let the parser accept a local FxA stack, plus the testPairingV2 UI test that drives the whole flow, move to a follow-up on fxa-pair-ios-v2-e2e, which I will open once this merges. See Testing.

What changed

  • FxAPairingOAuthHandler (new) answers fxaccounts:pair_oauth_start, calling beginAuthentication and returning state, scope, code_challenge, code_challenge_method and keys_jwk. Scopes are profile, oldsync and session.
  • FxAWebViewModel advertises pairingVersion: 2 in the fxaccounts:fxa_status reply. That reply is now built with JSONSerialization instead of string interpolation, removing an escaping hazard in the engines list.
  • presentPairingViewController no longer routes through beginPairingAuthentication. The page starts pairing, so the app loads the pairing URL directly.
  • FxAPageType.pairingV2 (new) is the only page type allowed to satisfy pair_oauth_start. See below.
  • The pairing modal waits on .accountManagerInitialized rather than dropping a cold-launch deep link that arrives before the account manager is ready.
  • The WebChannel bridge ignores messages from subframes.
  • Message ids are accepted as either Int or String.

Acceptance criteria

  • ✅ Report pairingVersion: 2 in the fxaccounts:fxa_status response
  • ✅ Handle fxaccounts:pair_oauth_start and return the OAuth parameters required to continue pairing, with the profile, oldsync and session scopes; pairing is started by the webpage
  • ⚠️ A successful pairing signs the user into Firefox, starts Sync and closes the pairing modal — verified end to end before the split, and asserted by PairingTests/testPairingV2, which lands in the follow-up
  • ✅ Existing email sign-in and in-app QR pairing continue working: both keep the v1 beginPairingAuthentication path and cannot trigger pair_oauth_start
  • ✅ Non-v2 pairing links retain their existing behavior: FxAPairingURLParser returns .notPairing without v=2, so they fall through to normal URL handling

Testing

37 unit tests pass on this branch, covering the OAuth handler, the fxa_status capabilities, the pair OAuth reply shapes and the page-type gate.

The full flow was verified end to end before the split, driven by a Playwright functional test in mozilla/fxa (pairingFlowV2iOS.spec.ts): a real Firefox Nightly authority over Marionette, an iOS Simulator supplicant and a live local FxA stack. The authority reached sync_success, and the device registered on the account and appeared in Connected Services.

That test cannot run against this branch alone. It pairs against http://localhost:3030, which the current FxAPairingURLParser allowlist rejects, so the parser change and the UI test ship together in the follow-up.

Depends on the FxA-side counterpart for the pair_oauth_start round trip.

📝 Checklist

  • I filled in the ticket numbers and a description of my work
  • I updated the PR name to follow our PR naming guidelines
  • I ensured unit tests pass and wrote tests for new code
  • If working on UI, I checked and implemented accessibility (Dynamic Text and VoiceOver) — n/a, no new native UI
  • If adding telemetry, I read the data stewardship requirements and will request a data review — n/a, no telemetry added
  • If adding or modifying strings, I read the guidelines and will request a string review from l10n — n/a, no strings added or modified
  • If needed, I updated documentation and added comments to complex code

@vbudhram
vbudhram marked this pull request as ready for review September 1, 2026 14:24
@vbudhram
vbudhram requested review from a team as code owners September 1, 2026 14:24
@vbudhram
vbudhram requested a review from isabelrios September 1, 2026 14:24
@issammani
issammani self-requested a review September 1, 2026 14:53
@github-actions
github-actions Bot requested a review from OrlaM September 1, 2026 15:52
@github-actions github-actions Bot added the needs-tech-lead-review Label to ask a review from a member of the firefox-ios-tech-leads label Sep 1, 2026
@vbudhram vbudhram changed the title Add FXIOS-16685 [FxA Pairflow] Support pairing v2 via WebChannel OAuth Add FXIOS-16685 [FxA Pairflow] Support pairing v2 via WebChannel OAuth Part 1 Sep 1, 2026

@issammani issammani left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left some comments but this looks good already. Thanks for adding tests 👍


private func onPairOAuthStart(id: Int, webView: WKWebView) {
pairingOAuthHandler.start { [weak self, weak webView] result in
guard let self, let webView else { return }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Let's add a log here it might be useful for us in the future for debugging.

Suggested change
guard let self, let webView else { return }
guard let self, let webView else {
self?.logger.log("Pair OAuth reply dropped. View model or webview deallocated",
level: .info,
category: .sync)
return
}

}

/// Serialize a WebChannel payload for injection into the reply script.
private func webChannelJSONString(from object: Any) -> String? {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Let's move this method inside FxAWebViewModel and make it static

}
}

private func messageID(from value: Any?) -> Int? {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add a simple test for this just to prevent future regressions something like:

XCTAssertEqual(viewModel.messageID(from: 42), 42)
XCTAssertEqual(viewModel.messageID(from: "42"), 42)
XCTAssertNil(viewModel.messageID(from: "abc"))

// Without an account manager the web view never loads its first page, so the modal would
// present empty with no error and no way out. A cold-launch deep link can arrive before the
// account manager finishes initializing, so wait for it rather than dropping the route.
AppEventQueue.wait(for: .accountManagerInitialized) { [weak self] in

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we add a cancel in deinit just to be on the safer side something like:

if let token = pairingWaitToken {
    AppEventQueue.cancelAction(token: token)
}

@mobiletest-ci-bot

mobiletest-ci-bot commented Sep 2, 2026

Copy link
Copy Markdown
Warnings
⚠️

⚠️ userInterfaceIdiom usage detected

Per Apple's WWDC 26 "Modernize your UIKit app" guidance, avoid checking userInterfaceIdiom for layout decisions. iPhone apps running on iPad or in iPhone Mirroring on Mac still report the phone idiom. Prefer size classes or trait-based layout instead.

File Line Change
firefox-ios/Client/Frontend/Browser/BrowserViewController/Views/BrowserViewController.swift 3440 Added
Messages
📖 Project coverage: 44.2%

💪 Quality guardian

2 tests files modified. You're a champion of test coverage! 🚀

🧩 Neat Piece

This PR changes 659 lines. It's a substantial update,
but still review-friendly if there’s a clear description. Thanks for keeping things moving! 🚀

💬 Description craftsman

Great PR description! Reviewers salute you 🫡

🎉 BrowserViewController got smaller

Nice! BrowserViewController.swift got smaller by 4 lines.

✅ Code coverage

All new and modified files meet their coverage thresholds.

Client.app: Coverage: 46.08

File Coverage
BrowserViewController.swift 37.21% ⚠️
LaunchPairingFromURLSetting.swift 79.07%
FxAPairingOAuthHandler.swift 97.18%
FxAWebViewModel.swift 26.92% ⚠️

Generated by 🚫 Danger Swift against 9b60ced

Load the pairing deep link directly in the WebChannel web view, instead of
converting it to a supplicant URL through beginPairingAuthentication. The page
then asks the app for OAuth parameters with fxaccounts:pair_oauth_start, and
FxAPairingOAuthHandler answers with the state, scope, code_challenge,
code_challenge_method and keys_jwk it reads back from the flow.

Advertise pairingVersion 2 in the fxaccounts:fxa_status capabilities, so the
content server can choose the v2 path. The server gates the flow on its own
pairing.version config as well, so both sides must agree before v2 runs.

Give the deep link its own FxAPageType. The qrCode case also serves the in-app
scanner and the debug setting. Both start their own OAuth flow before the page
loads, so neither may satisfy the pair_oauth_start gate.

Wait for the account manager before presenting the modal, so a cold-launch
deep link is not dropped. The wait runs a @sendable closure off the main actor,
so hop back through ensureMainThread, matching the other AppEventQueue.wait
call sites in that file. Track the wait token and cancel it in deinit.

Reject WebChannel messages from subframes, and reply to a pair_oauth_start the
app cannot serve rather than leaving the page waiting. Log when a reply is
dropped because the view model or the web view went away.

Extract the WebChannel reply envelope into webChannelReplyScript and test it.
The envelope is a protocol contract with the content server: the page matches
the reply against the id it sent, so messageId is emitted as a number rather
than a string. Cover the redirect policy after login, the title composition,
the user script setup and message id parsing, none of which had tests.

Co-Authored-By: Matt Lichtenstein <lichtensteinmp@gmail.com>
@issammani
issammani merged commit 96c1e2c into mozilla-mobile:main Sep 2, 2026
10 checks passed
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

🚀 PR merged to main, targeting version: 156.0

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-tech-lead-review Label to ask a review from a member of the firefox-ios-tech-leads

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants