Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions packages/fxa-settings/src/lib/passkeys/signin-flow.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion packages/fxa-settings/src/lib/passkeys/signin-flow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ function createMockSyncIntegration() {
type: IntegrationType.OAuthNative,
getService: () => MozServices.FirefoxSync,
isSync: () => true,
isFirefoxMobileClient: () => false,
requiresKeys: () => true,
wantsKeys: () => true,
getCmsInfo: () => undefined,
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
Loading