diff --git a/packages/fxa-settings/src/lib/passkeys/signin-flow.test.tsx b/packages/fxa-settings/src/lib/passkeys/signin-flow.test.tsx index 4c821d136a8..7a2728c986a 100644 --- a/packages/fxa-settings/src/lib/passkeys/signin-flow.test.tsx +++ b/packages/fxa-settings/src/lib/passkeys/signin-flow.test.tsx @@ -360,6 +360,32 @@ describe('usePasskeySignIn', () => { } ); + it('sends service=sync for Sync integrations even when the service URL param is absent', async () => { + // Mobile Firefox omits service=sync and defaults via clientId, so + // getService() returns undefined; isSync() must still drive service=sync. + const { args, spies } = buildArgs({ + integration: { + isSync: () => true, + isFirefoxNonSync: () => false, + getService: () => undefined, + type: IntegrationType.OAuthNative, + data: {}, + } as unknown as PasskeySignInIntegration, + }); + + const { result } = renderHook(() => usePasskeySignIn(args)); + + await act(async () => { + await result.current.onClick(); + }); + + expect(spies.completePasskeyAuthentication).toHaveBeenCalledWith( + MOCK_CREDENTIAL, + CHALLENGE, + { service: 'sync' } + ); + }); + // Locks the contract: each DOMException name maps to its expected FTL id // AND its fallback string. Drift in either lands silently otherwise. // The fallback substrings are stable phrases pulled from webauthn-errors.ts diff --git a/packages/fxa-settings/src/lib/passkeys/signin-flow.ts b/packages/fxa-settings/src/lib/passkeys/signin-flow.ts index 0cb055f864f..f43813a8420 100644 --- a/packages/fxa-settings/src/lib/passkeys/signin-flow.ts +++ b/packages/fxa-settings/src/lib/passkeys/signin-flow.ts @@ -277,7 +277,9 @@ export function usePasskeySignIn({ throw err; } - const service = integration.getService(); + // Sync clients (notably mobile) often omit `service=sync` and default via + // clientId, leaving getService() undefined; isSync() is the reliable signal. + const service = integration.isSync() ? 'sync' : integration.getService(); const completion = await authClient.completePasskeyAuthentication( credential, challengeOptions.challenge, diff --git a/packages/fxa-settings/src/pages/Signin/SigninPasskeyFallback/container.test.tsx b/packages/fxa-settings/src/pages/Signin/SigninPasskeyFallback/container.test.tsx index 6307f1db58c..a46fa5fe4ec 100644 --- a/packages/fxa-settings/src/pages/Signin/SigninPasskeyFallback/container.test.tsx +++ b/packages/fxa-settings/src/pages/Signin/SigninPasskeyFallback/container.test.tsx @@ -37,6 +37,7 @@ function createMockSyncIntegration() { type: IntegrationType.OAuthNative, getService: () => MozServices.FirefoxSync, isSync: () => true, + isFirefoxMobileClient: () => false, requiresKeys: () => true, wantsKeys: () => true, getCmsInfo: () => undefined, @@ -199,6 +200,32 @@ describe('SigninPasskeyFallback container', () => { ); }); + it('navigates after reauth on desktop (performNavigation true)', async () => { + const { getByTestId } = render(); + submitPassword(getByTestId); + + await waitFor(() => { + expect(mockHandleNavigation).toHaveBeenCalledWith( + expect.objectContaining({ performNavigation: true }) + ); + }); + }); + + it('skips navigation after reauth on mobile so Firefox drives Sync completion', async () => { + const mobileIntegration = { + ...createMockSyncIntegration(), + isFirefoxMobileClient: () => true, + } as unknown as Integration; + const { getByTestId } = render(mobileIntegration); + submitPassword(getByTestId); + + await waitFor(() => { + expect(mockHandleNavigation).toHaveBeenCalledWith( + expect.objectContaining({ performNavigation: false }) + ); + }); + }); + it('renders an error banner when sessionReauth throws', async () => { mockSessionReauth.mockRejectedValueOnce({ errno: 103, diff --git a/packages/fxa-settings/src/pages/Signin/SigninPasskeyFallback/container.tsx b/packages/fxa-settings/src/pages/Signin/SigninPasskeyFallback/container.tsx index 5eb2e67d6aa..6a0e23003fd 100644 --- a/packages/fxa-settings/src/pages/Signin/SigninPasskeyFallback/container.tsx +++ b/packages/fxa-settings/src/pages/Signin/SigninPasskeyFallback/container.tsx @@ -149,6 +149,9 @@ const SigninPasskeyFallbackContainer = ({ queryParams: location.search, handleFxaLogin: true, handleFxaOAuthLogin: true, + // On mobile, navigating the WebView away leaves Sync paused; let Firefox + // drive completion via WebChannel instead. + performNavigation: !integration.isFirefoxMobileClient(), }); if (navError) { GleanMetrics.passkeyEnterPassword.submitFrontendError({