-
- To keep your data safe, you need to enter your password when you use
- this passkey.
-
-
+ {reason === 'passkey' ? (
+
+
+ To keep your data safe, you need to enter your password when you use
+ this passkey.
+
+
+ ) : (
+
+
+ Your password encrypts your synced data so only you can access it.
+
+
+ )}
{email && (
diff --git a/packages/fxa-settings/src/pages/Signin/SigninPasswordlessCode/index.test.tsx b/packages/fxa-settings/src/pages/Signin/SigninPasswordlessCode/index.test.tsx
index 3673cb522c2..fb468e7a472 100644
--- a/packages/fxa-settings/src/pages/Signin/SigninPasswordlessCode/index.test.tsx
+++ b/packages/fxa-settings/src/pages/Signin/SigninPasswordlessCode/index.test.tsx
@@ -144,7 +144,7 @@ function resetMockAuthClient() {
function render(
props: Partial
& {
isSignup?: boolean;
- supportsKeysOptionalLogin?: boolean;
+ browserSupportsKeysOptional?: boolean;
} = {}
) {
if (!props.integration) {
@@ -849,7 +849,7 @@ describe('SigninPasswordlessCode page', () => {
render({
integration,
isSignup: false,
- supportsKeysOptionalLogin: false,
+ browserSupportsKeysOptional: false,
});
await submitCode();
@@ -878,7 +878,7 @@ describe('SigninPasswordlessCode page', () => {
render({
integration,
isSignup: false,
- supportsKeysOptionalLogin: true,
+ browserSupportsKeysOptional: true,
});
await submitCode();
diff --git a/packages/fxa-settings/src/pages/Signin/SigninPasswordlessCode/index.tsx b/packages/fxa-settings/src/pages/Signin/SigninPasswordlessCode/index.tsx
index b5f01c99ca3..fb5d6cedbb2 100644
--- a/packages/fxa-settings/src/pages/Signin/SigninPasswordlessCode/index.tsx
+++ b/packages/fxa-settings/src/pages/Signin/SigninPasswordlessCode/index.tsx
@@ -101,7 +101,7 @@ const SigninPasswordlessCode = ({
flowQueryParams,
surface: 'login_otp',
isButtonVisible: showPasskeySignin,
- supportsKeysOptionalLogin: useFxAStatusResult.supportsKeysOptionalLogin,
+ browserSupportsKeysOptional: useFxAStatusResult.browserSupportsKeysOptional,
});
const [localizedErrorBannerMessage, setLocalizedErrorBannerMessage] =
@@ -328,7 +328,7 @@ const SigninPasswordlessCode = ({
// since /password/create requires a verifiedSessionToken.
if (
integration.requiresPasswordForLogin(
- useFxAStatusResult.supportsKeysOptionalLogin
+ useFxAStatusResult.browserSupportsKeysOptional
)
) {
const accountData = {
diff --git a/packages/fxa-settings/src/pages/Signin/SigninPasswordlessCode/mocks.tsx b/packages/fxa-settings/src/pages/Signin/SigninPasswordlessCode/mocks.tsx
index d2dd1bb9213..a985fb3c78f 100644
--- a/packages/fxa-settings/src/pages/Signin/SigninPasswordlessCode/mocks.tsx
+++ b/packages/fxa-settings/src/pages/Signin/SigninPasswordlessCode/mocks.tsx
@@ -95,9 +95,9 @@ export const Subject = ({
hasPasskey = undefined,
isSignedIntoFirefox = false,
sendError = null,
- supportsKeysOptionalLogin = false,
+ browserSupportsKeysOptional = false,
}: Partial & {
- supportsKeysOptionalLogin?: boolean;
+ browserSupportsKeysOptional?: boolean;
}) => {
return (
@@ -112,7 +112,7 @@ export const Subject = ({
hasPasskey,
isSignedIntoFirefox,
sendError,
- useFxAStatusResult: mockUseFxAStatus({ supportsKeysOptionalLogin }),
+ useFxAStatusResult: mockUseFxAStatus({ browserSupportsKeysOptional }),
}}
/>
diff --git a/packages/fxa-settings/src/pages/Signin/SigninRecoveryCode/container.tsx b/packages/fxa-settings/src/pages/Signin/SigninRecoveryCode/container.tsx
index 8ef57846081..25aca5aa674 100644
--- a/packages/fxa-settings/src/pages/Signin/SigninRecoveryCode/container.tsx
+++ b/packages/fxa-settings/src/pages/Signin/SigninRecoveryCode/container.tsx
@@ -32,13 +32,13 @@ type SigninRecoveryCodeLocationState = {
export type SigninRecoveryCodeContainerProps = {
integration: Integration;
setCurrentSplitLayout?: (value: boolean) => void;
- supportsKeysOptionalLogin?: boolean;
+ browserSupportsKeysOptional?: boolean;
};
export const SigninRecoveryCodeContainer = ({
integration,
setCurrentSplitLayout,
- supportsKeysOptionalLogin,
+ browserSupportsKeysOptional,
}: SigninRecoveryCodeContainerProps) => {
const authClient = useAuthClient();
const { finishOAuthFlowHandler, oAuthDataError } = useFinishOAuthFlowHandler(
@@ -154,7 +154,7 @@ export const SigninRecoveryCodeContainer = ({
unwrapBKey,
loading: sendingPhoneCode,
setCurrentSplitLayout,
- supportsKeysOptionalLogin,
+ browserSupportsKeysOptional,
}}
/>
);
diff --git a/packages/fxa-settings/src/pages/Signin/SigninRecoveryCode/index.test.tsx b/packages/fxa-settings/src/pages/Signin/SigninRecoveryCode/index.test.tsx
index ab80242bfdd..ffd2700c2ce 100644
--- a/packages/fxa-settings/src/pages/Signin/SigninRecoveryCode/index.test.tsx
+++ b/packages/fxa-settings/src/pages/Signin/SigninRecoveryCode/index.test.tsx
@@ -334,7 +334,7 @@ describe('PageSigninRecoveryCode', () => {
navigateToRecoveryPhone={jest.fn()}
signinState={{ ...mockSigninLocationState, [marker]: true }}
submitRecoveryCode={submitSuccess()}
- supportsKeysOptionalLogin={false}
+ browserSupportsKeysOptional={false}
/>
);
@@ -374,7 +374,7 @@ describe('PageSigninRecoveryCode', () => {
isPasswordlessOtpSignin: true,
}}
submitRecoveryCode={submitSuccess()}
- supportsKeysOptionalLogin={true}
+ browserSupportsKeysOptional={true}
/>
);
diff --git a/packages/fxa-settings/src/pages/Signin/SigninRecoveryCode/index.tsx b/packages/fxa-settings/src/pages/Signin/SigninRecoveryCode/index.tsx
index 6c9fe8612c4..0ccdcf22d80 100644
--- a/packages/fxa-settings/src/pages/Signin/SigninRecoveryCode/index.tsx
+++ b/packages/fxa-settings/src/pages/Signin/SigninRecoveryCode/index.tsx
@@ -45,7 +45,7 @@ const SigninRecoveryCode = ({
unwrapBKey,
loading = false,
setCurrentSplitLayout,
- supportsKeysOptionalLogin,
+ browserSupportsKeysOptional,
}: SigninRecoveryCodeProps) => {
useEffect(() => {
GleanMetrics.loginBackupCode.view();
@@ -104,7 +104,7 @@ const SigninRecoveryCode = ({
if (
(signinState.isPasswordlessOtpSignin ||
signinState.isSignInWithThirdPartyAuth) &&
- integration.requiresPasswordForLogin(supportsKeysOptionalLogin)
+ integration.requiresPasswordForLogin(browserSupportsKeysOptional)
) {
navigateWithQuery('/post_verify/set_password', {
replace: true,
@@ -162,7 +162,7 @@ const SigninRecoveryCode = ({
ftlMsgResolver,
authClient,
signinState,
- supportsKeysOptionalLogin,
+ browserSupportsKeysOptional,
navigateWithQuery,
]);
diff --git a/packages/fxa-settings/src/pages/Signin/SigninRecoveryCode/interfaces.ts b/packages/fxa-settings/src/pages/Signin/SigninRecoveryCode/interfaces.ts
index f00042670c4..8a1c13ead0d 100644
--- a/packages/fxa-settings/src/pages/Signin/SigninRecoveryCode/interfaces.ts
+++ b/packages/fxa-settings/src/pages/Signin/SigninRecoveryCode/interfaces.ts
@@ -16,7 +16,7 @@ export type SigninRecoveryCodeProps = {
lastFourPhoneDigits?: string;
loading?: boolean;
setCurrentSplitLayout?: (value: boolean) => void;
- supportsKeysOptionalLogin?: boolean;
+ browserSupportsKeysOptional?: boolean;
} & SensitiveData.AuthData;
export type SubmitRecoveryCode = (
diff --git a/packages/fxa-settings/src/pages/Signin/SigninRecoveryPhone/container.tsx b/packages/fxa-settings/src/pages/Signin/SigninRecoveryPhone/container.tsx
index 204e14f3811..d995d36ebd7 100644
--- a/packages/fxa-settings/src/pages/Signin/SigninRecoveryPhone/container.tsx
+++ b/packages/fxa-settings/src/pages/Signin/SigninRecoveryPhone/container.tsx
@@ -35,7 +35,7 @@ import { SigninLocationState } from '../interfaces';
const SigninRecoveryPhoneContainer = ({
integration,
setCurrentSplitLayout,
- supportsKeysOptionalLogin,
+ browserSupportsKeysOptional,
}: SigninRecoveryPhoneContainerProps) => {
const alertBar = useAlertBar();
const authClient = useAuthClient();
@@ -116,7 +116,7 @@ const SigninRecoveryPhoneContainer = ({
// defer the success alert and browser login messages.
if (
isPasswordlessSignin &&
- integration.requiresPasswordForLogin(supportsKeysOptionalLogin)
+ integration.requiresPasswordForLogin(browserSupportsKeysOptional)
) {
navigateWithQuery('/post_verify/set_password', {
replace: true,
diff --git a/packages/fxa-settings/src/pages/Signin/SigninRecoveryPhone/interfaces.ts b/packages/fxa-settings/src/pages/Signin/SigninRecoveryPhone/interfaces.ts
index 0a76c19482c..398615d1ad0 100644
--- a/packages/fxa-settings/src/pages/Signin/SigninRecoveryPhone/interfaces.ts
+++ b/packages/fxa-settings/src/pages/Signin/SigninRecoveryPhone/interfaces.ts
@@ -10,7 +10,7 @@ import { SigninIntegration, SigninLocationState } from '../interfaces';
export interface SigninRecoveryPhoneContainerProps {
integration: Integration;
setCurrentSplitLayout?: (value: boolean) => void;
- supportsKeysOptionalLogin?: boolean;
+ browserSupportsKeysOptional?: boolean;
}
export interface SigninRecoveryPhoneLocationState extends SigninLocationState {
diff --git a/packages/fxa-settings/src/pages/Signin/SigninTotpCode/index.test.tsx b/packages/fxa-settings/src/pages/Signin/SigninTotpCode/index.test.tsx
index 26998191ea3..83cd6479fd8 100644
--- a/packages/fxa-settings/src/pages/Signin/SigninTotpCode/index.test.tsx
+++ b/packages/fxa-settings/src/pages/Signin/SigninTotpCode/index.test.tsx
@@ -95,7 +95,11 @@ describe('Sign in with TOTP code page', () => {
});
it('renders as expected', () => {
- renderWithLocalizationProvider();
+ renderWithLocalizationProvider(
+
+
+
+ );
const headingEl = screen.getByRole('heading', { level: 2 });
expect(headingEl).toHaveTextContent('Enter two-step authentication code');
@@ -108,7 +112,11 @@ describe('Sign in with TOTP code page', () => {
});
it('enables submit button when code entered', async () => {
- renderWithLocalizationProvider();
+ renderWithLocalizationProvider(
+
+
+
+ );
const inputEl = screen.getByLabelText('Enter 6-digit code');
await waitFor(() => userEvent.type(inputEl, '123456'));
@@ -159,7 +167,11 @@ describe('Sign in with TOTP code page', () => {
});
it('emits a metrics event on render', () => {
- renderWithLocalizationProvider();
+ renderWithLocalizationProvider(
+
+
+
+ );
expect(GleanMetrics.totpForm.view).toHaveBeenCalledTimes(1);
expect(GleanMetrics.totpForm.submit).toHaveBeenCalledTimes(0);
expect(GleanMetrics.totpForm.success).toHaveBeenCalledTimes(0);
@@ -205,7 +217,9 @@ describe('Sign in with TOTP code page', () => {
expect(GleanMetrics.totpForm.view).toHaveBeenCalledTimes(1);
expect(GleanMetrics.totpForm.submit).toHaveBeenCalledTimes(1);
expect(GleanMetrics.totpForm.success).toHaveBeenCalledTimes(1);
- expect(mockNavigate).toHaveBeenCalledWith('/settings', { replace: false });
+ expect(mockNavigate).toHaveBeenCalledWith('/settings', {
+ replace: false,
+ });
});
describe('fxaLogin webchannel message', () => {
@@ -522,7 +536,7 @@ describe('Sign in with TOTP code page', () => {
{
{
false,
wantsKeysIfPasswordEntered: () => false,
wantsKeys: () => false,
- requiresPasswordForLogin(supportsKeysOptionalLogin = false) {
+ requiresPasswordForLogin(browserSupportsKeysOptional = false) {
return (
this.requiresKeys() ||
- (!supportsKeysOptionalLogin && this.wantsKeysIfPasswordEntered())
+ (!browserSupportsKeysOptional && this.wantsKeysIfPasswordEntered())
);
},
isFirefoxClientServiceRelay: () => false,
@@ -61,10 +61,10 @@ export const mockOAuthNativeSigninIntegration = (
requiresKeys: () => false,
wantsKeysIfPasswordEntered: () => false,
wantsKeys: () => false,
- requiresPasswordForLogin(supportsKeysOptionalLogin = false) {
+ requiresPasswordForLogin(browserSupportsKeysOptional = false) {
return (
this.requiresKeys() ||
- (!supportsKeysOptionalLogin && this.wantsKeysIfPasswordEntered())
+ (!browserSupportsKeysOptional && this.wantsKeysIfPasswordEntered())
);
},
isFirefoxClientServiceRelay: () => isRelay,
@@ -112,9 +112,9 @@ export const Subject = ({
serviceName = MozServices.Default,
signinState = MOCK_TOTP_LOCATION_STATE,
submitTotpCode = mockSubmitTotpCode,
- supportsKeysOptionalLogin = false,
+ browserSupportsKeysOptional = false,
}: Partial & {
- supportsKeysOptionalLogin?: boolean;
+ browserSupportsKeysOptional?: boolean;
}) => {
return (
@@ -125,7 +125,7 @@ export const Subject = ({
serviceName,
signinState,
submitTotpCode,
- useFxAStatusResult: mockUseFxAStatus({ supportsKeysOptionalLogin }),
+ useFxAStatusResult: mockUseFxAStatus({ browserSupportsKeysOptional }),
}}
/>
diff --git a/packages/fxa-settings/src/pages/Signin/SigninUnblock/mocks.tsx b/packages/fxa-settings/src/pages/Signin/SigninUnblock/mocks.tsx
index 06e7097658a..93dd22fb81a 100644
--- a/packages/fxa-settings/src/pages/Signin/SigninUnblock/mocks.tsx
+++ b/packages/fxa-settings/src/pages/Signin/SigninUnblock/mocks.tsx
@@ -84,6 +84,13 @@ export function createMockSigninWebSyncIntegration() {
isFirefoxClientServiceSmartWindow: () => false,
isFirefoxClientServiceVpn: () => false,
isFirefoxNonSync: () => false,
+ supportsKeylessLogin(b: boolean) {
+ return b && this.isFirefoxNonSync();
+ },
+ allowsPreKeysSyncLogin: () => false,
+ nonSyncKeysRequirePassword(b: boolean) {
+ return !b && this.wantsKeysIfPasswordEntered();
+ },
getWebChannelServices: mockGetWebChannelServices({ isSync: true }),
wantsLogin: () => false,
getCmsInfo: () => undefined,
diff --git a/packages/fxa-settings/src/pages/Signin/components/SigninAlternativeAuthOptions/index.tsx b/packages/fxa-settings/src/pages/Signin/components/SigninAlternativeAuthOptions/index.tsx
index a8766ae4fdd..30519f8b1a0 100644
--- a/packages/fxa-settings/src/pages/Signin/components/SigninAlternativeAuthOptions/index.tsx
+++ b/packages/fxa-settings/src/pages/Signin/components/SigninAlternativeAuthOptions/index.tsx
@@ -43,7 +43,7 @@ const SigninAlternativeAuthOptions = ({
localizedSuccessBannerDescription,
isSignedIntoFirefox = false,
setCurrentSplitLayout,
- supportsKeysOptionalLogin,
+ browserSupportsKeysOptional,
}: SigninAlternativeAuthOptionsProps) => {
const config = useConfig();
const authClient = useAuthClient();
@@ -66,7 +66,7 @@ const SigninAlternativeAuthOptions = ({
flowQueryParams,
surface: 'alternative_auth',
isButtonVisible: showPasskeySignin,
- supportsKeysOptionalLogin,
+ browserSupportsKeysOptional,
});
const {
diff --git a/packages/fxa-settings/src/pages/Signin/components/SigninCached/index.test.tsx b/packages/fxa-settings/src/pages/Signin/components/SigninCached/index.test.tsx
index facd761d813..4844574d037 100644
--- a/packages/fxa-settings/src/pages/Signin/components/SigninCached/index.test.tsx
+++ b/packages/fxa-settings/src/pages/Signin/components/SigninCached/index.test.tsx
@@ -128,7 +128,7 @@ describe('SigninCached', () => {
}),
hasPassword: false,
hasLinkedAccount: true,
- supportsKeysOptionalLogin: false,
+ browserSupportsKeysOptional: false,
cachedSigninHandler: mockCachedSigninSuccess(),
});
@@ -140,7 +140,7 @@ describe('SigninCached', () => {
isSignInWithThirdPartyAuth: true,
handleFxaLogin: false,
handleFxaOAuthLogin: false,
- supportsKeysOptionalLogin: false,
+ browserSupportsKeysOptional: false,
})
);
});
@@ -155,7 +155,7 @@ describe('SigninCached', () => {
}),
hasPassword: false,
hasLinkedAccount: true,
- supportsKeysOptionalLogin: true,
+ browserSupportsKeysOptional: true,
cachedSigninHandler: mockCachedSigninSuccess(),
});
@@ -167,7 +167,7 @@ describe('SigninCached', () => {
isSignInWithThirdPartyAuth: false,
handleFxaLogin: true,
handleFxaOAuthLogin: true,
- supportsKeysOptionalLogin: true,
+ browserSupportsKeysOptional: true,
})
);
});
@@ -182,7 +182,7 @@ describe('SigninCached', () => {
}),
hasPassword: true,
isSignedIntoFirefox: true,
- supportsKeysOptionalLogin: false,
+ browserSupportsKeysOptional: false,
cachedSigninHandler: mockCachedSigninSuccess(),
});
diff --git a/packages/fxa-settings/src/pages/Signin/components/SigninCached/index.tsx b/packages/fxa-settings/src/pages/Signin/components/SigninCached/index.tsx
index 1d2b66c9798..1123c4ae4a4 100644
--- a/packages/fxa-settings/src/pages/Signin/components/SigninCached/index.tsx
+++ b/packages/fxa-settings/src/pages/Signin/components/SigninCached/index.tsx
@@ -46,7 +46,7 @@ const SigninCached = ({
isSignedIntoFirefox = false,
setCurrentSplitLayout,
onSessionExpired,
- supportsKeysOptionalLogin,
+ browserSupportsKeysOptional,
}: SigninCachedProps) => {
const authClient = useAuthClient();
const config = useConfig();
@@ -63,7 +63,7 @@ const SigninCached = ({
// and let handleNavigation route to set_password.
const deferKeysUntilPasswordSet =
!hasPassword &&
- integration.requiresPasswordForLogin(supportsKeysOptionalLogin);
+ integration.requiresPasswordForLogin(browserSupportsKeysOptional);
const {
clientId,
@@ -166,7 +166,7 @@ const SigninCached = ({
// auth or OTP) defer web channel messages until after password creation.
handleFxaLogin: !deferKeysUntilPasswordSet,
handleFxaOAuthLogin: !deferKeysUntilPasswordSet,
- supportsKeysOptionalLogin,
+ browserSupportsKeysOptional,
// Redirect these passwordless users to set_password after session
// verification.
isSignInWithThirdPartyAuth: deferKeysUntilPasswordSet,
@@ -203,7 +203,7 @@ const SigninCached = ({
integration,
finishOAuthFlowHandler,
deferKeysUntilPasswordSet,
- supportsKeysOptionalLogin,
+ browserSupportsKeysOptional,
location.search,
webRedirectCheck,
isServiceWithEmailVerification,
diff --git a/packages/fxa-settings/src/pages/Signin/components/SigninDecider/index.test.tsx b/packages/fxa-settings/src/pages/Signin/components/SigninDecider/index.test.tsx
index 9dcdc423e1f..0b469445868 100644
--- a/packages/fxa-settings/src/pages/Signin/components/SigninDecider/index.test.tsx
+++ b/packages/fxa-settings/src/pages/Signin/components/SigninDecider/index.test.tsx
@@ -15,7 +15,8 @@ import {
createMockSigninOAuthNativeIntegration,
createMockSigninOAuthNativeSyncIntegration,
} from '../../mocks';
-import { MOCK_EMAIL, MOCK_SESSION_TOKEN } from '../../../mocks';
+import { MOCK_EMAIL, MOCK_SESSION_TOKEN, MOCK_UID } from '../../../mocks';
+import * as CacheModule from '../../../../lib/cache';
jest.mock('../../../../lib/storage-utils', () => ({
storeAccountData: jest.fn(),
@@ -67,7 +68,7 @@ describe('SigninDecider routing', () => {
});
describe('cached vs password based on keys-optional', () => {
- it('routes to cached signin for service=relay when supportsKeysOptionalLogin is true', () => {
+ it('routes to cached signin for service=relay when browserSupportsKeysOptional is true', () => {
const integration = createMockSigninOAuthNativeIntegration({
service: OAuthNativeServices.Relay,
isSync: false,
@@ -75,13 +76,13 @@ describe('SigninDecider routing', () => {
render({
integration,
sessionToken: MOCK_SESSION_TOKEN,
- supportsKeysOptionalLogin: true,
+ browserSupportsKeysOptional: true,
});
passwordInputNotRendered();
});
- it('routes to cached signin for service=smartwindow when supportsKeysOptionalLogin is true', () => {
+ it('routes to cached signin for service=smartwindow when browserSupportsKeysOptional is true', () => {
const integration = createMockSigninOAuthNativeIntegration({
service: OAuthNativeServices.SmartWindow,
isSync: false,
@@ -89,13 +90,13 @@ describe('SigninDecider routing', () => {
render({
integration,
sessionToken: MOCK_SESSION_TOKEN,
- supportsKeysOptionalLogin: true,
+ browserSupportsKeysOptional: true,
});
passwordInputNotRendered();
});
- it('routes to password signin for service=relay when supportsKeysOptionalLogin is false', () => {
+ it('routes to password signin for service=relay when browserSupportsKeysOptional is false', () => {
const integration = createMockSigninOAuthNativeIntegration({
service: OAuthNativeServices.Relay,
isSync: false,
@@ -103,7 +104,7 @@ describe('SigninDecider routing', () => {
render({
integration,
sessionToken: MOCK_SESSION_TOKEN,
- supportsKeysOptionalLogin: false,
+ browserSupportsKeysOptional: false,
isSignedIntoFirefox: false,
});
@@ -125,7 +126,7 @@ describe('SigninDecider routing', () => {
integration,
sessionToken: MOCK_SESSION_TOKEN,
isSignedIntoFirefox: true,
- supportsKeysOptionalLogin: false,
+ browserSupportsKeysOptional: false,
});
passwordInputNotRendered();
@@ -142,7 +143,7 @@ describe('SigninDecider routing', () => {
integration,
sessionToken: MOCK_SESSION_TOKEN,
isSignedIntoFirefox: true,
- supportsKeysOptionalLogin: false,
+ browserSupportsKeysOptional: false,
});
passwordInputNotRendered();
@@ -158,7 +159,7 @@ describe('SigninDecider routing', () => {
integration,
sessionToken: MOCK_SESSION_TOKEN,
isSignedIntoFirefox: false,
- supportsKeysOptionalLogin: false,
+ browserSupportsKeysOptional: false,
});
passwordInputRendered();
@@ -174,7 +175,7 @@ describe('SigninDecider routing', () => {
integration,
sessionToken: MOCK_SESSION_TOKEN,
isSignedIntoFirefox: true,
- supportsKeysOptionalLogin: false,
+ browserSupportsKeysOptional: false,
});
passwordInputRendered();
@@ -320,4 +321,59 @@ describe('SigninDecider routing', () => {
});
});
});
+
+ describe('resume reroute to enter-password fallback', () => {
+ const storedAccount = {
+ uid: MOCK_UID,
+ email: MOCK_EMAIL,
+ sessionToken: MOCK_SESSION_TOKEN,
+ verified: true,
+ };
+
+ it('reroutes a cached has-password Sync account signed into Firefox to /signin_passkey_fallback', async () => {
+ jest.spyOn(CacheModule, 'currentAccount').mockReturnValue(storedAccount);
+ render({
+ integration: createMockSigninOAuthNativeSyncIntegration(),
+ sessionToken: MOCK_SESSION_TOKEN,
+ isSignedIntoFirefox: true,
+ });
+
+ await waitFor(() => {
+ expect(mockPasswordlessNavigate).toHaveBeenCalledWith(
+ '/signin_passkey_fallback',
+ { state: { reason: 'resume' } }
+ );
+ });
+ });
+
+ it('does NOT reroute when the browser is not signed in (no session to resume)', () => {
+ jest.spyOn(CacheModule, 'currentAccount').mockReturnValue(storedAccount);
+ render({
+ integration: createMockSigninOAuthNativeSyncIntegration(),
+ sessionToken: MOCK_SESSION_TOKEN,
+ isSignedIntoFirefox: false,
+ });
+
+ expect(mockPasswordlessNavigate).not.toHaveBeenCalledWith(
+ '/signin_passkey_fallback',
+ expect.anything()
+ );
+ passwordInputRendered();
+ });
+
+ it('does NOT reroute when there is no stored account to hydrate the fallback', () => {
+ jest.spyOn(CacheModule, 'currentAccount').mockReturnValue(undefined);
+ render({
+ integration: createMockSigninOAuthNativeSyncIntegration(),
+ sessionToken: MOCK_SESSION_TOKEN,
+ isSignedIntoFirefox: true,
+ });
+
+ expect(mockPasswordlessNavigate).not.toHaveBeenCalledWith(
+ '/signin_passkey_fallback',
+ expect.anything()
+ );
+ passwordInputRendered();
+ });
+ });
});
diff --git a/packages/fxa-settings/src/pages/Signin/components/SigninDecider/index.tsx b/packages/fxa-settings/src/pages/Signin/components/SigninDecider/index.tsx
index f5579fd23b0..2d8452e75b9 100644
--- a/packages/fxa-settings/src/pages/Signin/components/SigninDecider/index.tsx
+++ b/packages/fxa-settings/src/pages/Signin/components/SigninDecider/index.tsx
@@ -12,6 +12,7 @@ import { UseFxAStatusResult } from '../../../../lib/hooks/useFxAStatus';
import { MozServices } from '../../../../lib/types';
import { useFinishOAuthFlowHandler } from '../../../../lib/oauth/hooks';
import { useNavigateWithQuery } from '../../../../lib/hooks/useNavigateWithQuery';
+import { currentAccount } from '../../../../lib/cache';
import type { QueryParams } from '../../../..';
import {
AvatarResponse,
@@ -128,19 +129,48 @@ export const SigninDecider = ({
hasPasskey,
]);
+ // Turn-on-Sync (and similar resumes) for an already-signed-in account that
+ // has a password and needs Sync keys: send them to the enter-password
+ // fallback to reauth for keys, instead of a full re-sign-in. Desktop only.
+ // Gated on `isSignedIntoFirefox` — App validates the browser's signed-in
+ // session server-side before setting it, so this both confirms there's a
+ // session to resume AND avoids rerouting onto a stale local-storage token
+ // (e.g. after signing out from the browser menu). The stored-uid check keeps
+ // the fallback able to hydrate from local storage.
+ const enterPasswordForSyncKeys =
+ hasCachedSession &&
+ hasPassword &&
+ integration.requiresKeys() &&
+ !integration.isFirefoxMobileClient() &&
+ !!isSignedIntoFirefox &&
+ !!currentAccount()?.uid;
+
+ useEffect(() => {
+ if (enterPasswordForSyncKeys) {
+ navigateWithQuery('/signin_passkey_fallback', {
+ state: { reason: 'resume' },
+ });
+ }
+ }, [enterPasswordForSyncKeys, navigateWithQuery]);
+
if (shouldRedirectToPasswordless) {
return null;
}
+ if (enterPasswordForSyncKeys) {
+ return null;
+ }
+
const isOAuth = isOAuthIntegration(integration);
+ const browserSupportsKeysOptional =
+ useFxAStatusResult.browserSupportsKeysOptional;
// Relay browser service login launched in Firefox desktop 135, and the "keys optional"
// capability (Sync decoupling) launched in Fx desktop 147, meaning all Relay service users
// in those Fx versions require a password.
// This also covers Mobile until Sync is decoupled except for the authorization state below.
const syncNotDecoupledRequiresPassword =
- !useFxAStatusResult.supportsKeysOptionalLogin &&
- integration.wantsKeysIfPasswordEntered();
+ integration.nonSyncKeysRequirePassword(browserSupportsKeysOptional);
// In Firefox Android 153, Sync is not decoupled, but we need to show cached sign-in
// for signed-in users (e.g., signed into Sync) that are authorizing VPN.
@@ -164,7 +194,8 @@ export const SigninDecider = ({
// Do we have a session token, and can we defer the key fetch because sync is decoupled?
const keysOptional =
- hasCachedSession && useFxAStatusResult.supportsKeysOptionalLogin;
+ hasCachedSession &&
+ integration.supportsKeylessLogin(browserSupportsKeysOptional);
// Determine whether to show the cached view (no password input). Keys always
// require a password for derivation, but we can skip it when:
@@ -202,8 +233,7 @@ export const SigninDecider = ({
isSignedIntoFirefox,
setCurrentSplitLayout,
onSessionExpired,
- supportsKeysOptionalLogin:
- useFxAStatusResult.supportsKeysOptionalLogin,
+ browserSupportsKeysOptional,
}}
/>
);
@@ -237,8 +267,7 @@ export const SigninDecider = ({
flowQueryParams,
isSignedIntoFirefox,
setCurrentSplitLayout,
- supportsKeysOptionalLogin:
- useFxAStatusResult.supportsKeysOptionalLogin,
+ browserSupportsKeysOptional,
}}
/>
);
diff --git a/packages/fxa-settings/src/pages/Signin/index.stories.tsx b/packages/fxa-settings/src/pages/Signin/index.stories.tsx
index aa0368fbca9..bb187eb4642 100644
--- a/packages/fxa-settings/src/pages/Signin/index.stories.tsx
+++ b/packages/fxa-settings/src/pages/Signin/index.stories.tsx
@@ -83,7 +83,7 @@ export const NonCachedNonSyncBrowserServiceBrowserHasPasswordlessCapability: Sto
service: OAuthNativeServices.SmartWindow,
isSync: false,
}),
- supportsKeysOptionalLogin: true,
+ browserSupportsKeysOptional: true,
}),
name: 'Non-Cached > Non-Sync browser service > Browser has Sync keys optional capability',
};
@@ -96,7 +96,7 @@ export const NonCachedNonSyncBrowserServiceBrowserDoesNotHavePasswordlessCapabil
service: OAuthNativeServices.Relay,
isSync: false,
}),
- supportsKeysOptionalLogin: false,
+ browserSupportsKeysOptional: false,
}),
name: 'Non-Cached > Non-Sync browser service > Browser does not have Sync keys optional capability',
};
@@ -147,7 +147,7 @@ export const CachedNonSyncBrowserServiceWithoutPasswordlessCapability: Story = {
service: OAuthNativeServices.SmartWindow,
isSync: false,
}),
- supportsKeysOptionalLogin: false,
+ browserSupportsKeysOptional: false,
}),
name: 'Cached > Non-Sync browser service > Browser does not have Sync keys optional capability',
};
@@ -161,7 +161,7 @@ export const CachedNonSyncBrowserServiceWithPasswordlessCapabilitySignedIntoDesk
service: OAuthNativeServices.SmartWindow,
isSync: false,
}),
- supportsKeysOptionalLogin: true,
+ browserSupportsKeysOptional: true,
isSignedIntoFirefox: true,
}),
name: 'Cached > Non-Sync browser service > Browser has Sync keys optional capability > Account is signed into Firefox Desktop',
@@ -178,7 +178,7 @@ export const CachedNonSyncBrowserServiceWithPasswordlessCapabilityNotSignedIntoD
service: OAuthNativeServices.SmartWindow,
isSync: false,
}),
- supportsKeysOptionalLogin: true,
+ browserSupportsKeysOptional: true,
}),
name: 'Cached > Non-Sync browser service > Browser has Sync keys optional capability > Account is not signed into Firefox Desktop',
};
@@ -192,7 +192,7 @@ export const CachedNonSyncBrowserServiceMobileAuthorizationFlow: Story = {
isSync: false,
isMobile: true,
}),
- supportsKeysOptionalLogin: false,
+ browserSupportsKeysOptional: false,
isSignedIntoFirefox: true,
}),
name: 'Cached > Non-Sync browser service > Browser does not have keys optional capability > Account is signed into Firefox Mobile',
@@ -207,7 +207,7 @@ export const CachedNonSyncBrowserServiceMobileNotSignedIn: Story = {
isSync: false,
isMobile: true,
}),
- supportsKeysOptionalLogin: false,
+ browserSupportsKeysOptional: false,
isSignedIntoFirefox: false,
}),
name: 'Cached > Non-Sync browser service > Browser does not have keys optional capability > Account is not signed into Firefox Mobile',
diff --git a/packages/fxa-settings/src/pages/Signin/index.test.tsx b/packages/fxa-settings/src/pages/Signin/index.test.tsx
index 94ae7ef73d5..33e243f6e1f 100644
--- a/packages/fxa-settings/src/pages/Signin/index.test.tsx
+++ b/packages/fxa-settings/src/pages/Signin/index.test.tsx
@@ -308,14 +308,14 @@ describe('Signin component', () => {
});
});
- it('does not render third party auth when service=relay and supportsKeysOptionalLogin is false', () => {
+ it('does not render third party auth when service=relay and browserSupportsKeysOptional is false', () => {
const integration = createMockSigninOAuthNativeIntegration({
service: OAuthNativeServices.Relay,
isSync: false,
});
render({
integration,
- supportsKeysOptionalLogin: false,
+ browserSupportsKeysOptional: false,
});
thirdPartyAuthNotRendered();
@@ -324,14 +324,14 @@ describe('Signin component', () => {
});
});
- it('renders third party auth when service=relay and supportsKeysOptionalLogin is true', () => {
+ it('renders third party auth when service=relay and browserSupportsKeysOptional is true', () => {
const integration = createMockSigninOAuthNativeIntegration({
service: OAuthNativeServices.Relay,
isSync: false,
});
render({
integration,
- supportsKeysOptionalLogin: true,
+ browserSupportsKeysOptional: true,
});
expect(
@@ -345,14 +345,14 @@ describe('Signin component', () => {
});
});
- it('renders third party auth when service=smartwindow and supportsKeysOptionalLogin is true', () => {
+ it('renders third party auth when service=smartwindow and browserSupportsKeysOptional is true', () => {
const integration = createMockSigninOAuthNativeIntegration({
service: OAuthNativeServices.SmartWindow,
isSync: false,
});
render({
integration,
- supportsKeysOptionalLogin: true,
+ browserSupportsKeysOptional: true,
});
expect(
@@ -1027,7 +1027,7 @@ describe('Signin component', () => {
passwordInputNotRendered();
});
- it('sends webchannel message if cached signin for service=relay when supportsKeysOptionalLogin is true', async () => {
+ it('sends webchannel message if cached signin for service=relay when browserSupportsKeysOptional is true', async () => {
const fxaLoginSpy = jest.spyOn(firefox, 'fxaLogin');
const integration = createMockSigninOAuthNativeIntegration({
@@ -1037,7 +1037,7 @@ describe('Signin component', () => {
render({
integration,
sessionToken: MOCK_SESSION_TOKEN,
- supportsKeysOptionalLogin: true,
+ browserSupportsKeysOptional: true,
});
await submit();
await waitFor(() => {
@@ -1679,7 +1679,7 @@ describe('Signin component', () => {
integration={integration}
sessionToken={MOCK_SESSION_TOKEN}
isSignedIntoFirefox={true}
- supportsKeysOptionalLogin={true}
+ browserSupportsKeysOptional={true}
/>
);
@@ -1931,7 +1931,7 @@ describe('Signin component', () => {
cmsInfo: cachedCmsInfo,
}),
isSignedIntoFirefox: true,
- supportsKeysOptionalLogin: true,
+ browserSupportsKeysOptional: true,
sessionToken: MOCK_SESSION_TOKEN,
hasPassword: true,
});
diff --git a/packages/fxa-settings/src/pages/Signin/index.tsx b/packages/fxa-settings/src/pages/Signin/index.tsx
index a9e9db6d6bd..e5cc7fbb2ed 100644
--- a/packages/fxa-settings/src/pages/Signin/index.tsx
+++ b/packages/fxa-settings/src/pages/Signin/index.tsx
@@ -54,7 +54,7 @@ const Signin = ({
localizedSuccessBannerHeading,
localizedSuccessBannerDescription,
flowQueryParams,
- useFxAStatusResult: { supportsKeysOptionalLogin },
+ useFxAStatusResult: { browserSupportsKeysOptional },
isSignedIntoFirefox = false,
setCurrentSplitLayout,
}: SigninProps) => {
@@ -84,7 +84,7 @@ const Signin = ({
flowQueryParams,
surface: 'login',
isButtonVisible: showPasskeySignin,
- supportsKeysOptionalLogin,
+ browserSupportsKeysOptional,
});
const [localizedBannerError, setLocalizedBannerError] = useState(
@@ -140,7 +140,8 @@ const Signin = ({
// Show for all other cases.
const hideThirdPartyAuth = isSync
? hasPassword
- : isOAuthNative && !supportsKeysOptionalLogin;
+ : isOAuthNative &&
+ !integration.supportsKeylessLogin(browserSupportsKeysOptional);
useEffect(() => {
GleanMetrics.login.view({
diff --git a/packages/fxa-settings/src/pages/Signin/interfaces.ts b/packages/fxa-settings/src/pages/Signin/interfaces.ts
index e9a12c776c4..b958ac383f6 100644
--- a/packages/fxa-settings/src/pages/Signin/interfaces.ts
+++ b/packages/fxa-settings/src/pages/Signin/interfaces.ts
@@ -34,6 +34,9 @@ export type SigninUnblockIntegration = Pick<
| 'wantsKeysIfPasswordEntered'
| 'wantsKeys'
| 'requiresPasswordForLogin'
+ | 'nonSyncKeysRequirePassword'
+ | 'supportsKeylessLogin'
+ | 'allowsPreKeysSyncLogin'
| 'data'
| 'isDesktopSync'
| 'isFirefoxClientServiceRelay'
@@ -60,6 +63,9 @@ export type SigninIntegration =
| 'wantsKeysIfPasswordEntered'
| 'wantsKeys'
| 'requiresPasswordForLogin'
+ | 'nonSyncKeysRequirePassword'
+ | 'supportsKeylessLogin'
+ | 'allowsPreKeysSyncLogin'
| 'data'
| 'isDesktopSync'
| 'isFirefoxClientServiceRelay'
@@ -86,6 +92,9 @@ export type SigninOAuthIntegration = Pick<
| 'wantsKeysIfPasswordEntered'
| 'wantsKeys'
| 'requiresPasswordForLogin'
+ | 'nonSyncKeysRequirePassword'
+ | 'supportsKeylessLogin'
+ | 'allowsPreKeysSyncLogin'
| 'wantsLogin'
| 'data'
| 'isDesktopSync'
@@ -144,11 +153,11 @@ export interface SigninCachedProps extends SigninSharedProps {
sessionToken: hexstring;
cachedSigninHandler: CachedSigninHandler;
onSessionExpired: (localizedErrorMessage: string) => void;
- supportsKeysOptionalLogin?: boolean;
+ browserSupportsKeysOptional?: boolean;
}
export type SigninAlternativeAuthOptionsProps = SigninSharedProps & {
- supportsKeysOptionalLogin?: boolean;
+ browserSupportsKeysOptional?: boolean;
};
export type BeginSigninHandler = (
@@ -275,7 +284,7 @@ export interface NavigationOptions {
// Fx desktop 147+). When false, a non-Sync Firefox client that wants keys
// (e.g. Android VPN) must set a password before keys can be derived. Used together
// with `requiresPasswordForLogin` to decide the set_password redirect.
- supportsKeysOptionalLogin?: boolean;
+ browserSupportsKeysOptional?: boolean;
handleFxaLogin?: boolean;
handleFxaOAuthLogin?: boolean;
syncEngines?: {
@@ -301,6 +310,14 @@ export interface OAuthSigninResult {
shouldHardNavigate: string;
}
+/**
+ * Why the user reached the enter-password-for-keys page
+ * (`SigninPasskeyFallback`): `passkey` after a passkey sign-in, or `resume`
+ * when turning on Sync for an already-signed-in keyless account. Drives the
+ * page copy, Glean events, and `verificationMethod`.
+ */
+export type EnterPasswordReason = 'passkey' | 'resume';
+
export interface SigninLocationState {
email: string;
uid: hexstring;
@@ -320,4 +337,15 @@ export interface SigninLocationState {
* events.
*/
passkeySurface?: PasskeyMetricsSurface;
+ /**
+ * True when the passkey flow already sent the keyless `fxaccounts:login` at
+ * verification time (desktop pre-keys Sync login). The destination page then
+ * sends only the keyed `oauth_login`, avoiding a duplicate login message.
+ */
+ syncPreKeysLoginSent?: boolean;
+ /**
+ * Why the enter-password-for-keys page was shown (default `passkey`); set to
+ * `resume` for the Turn-on-Sync resume of a keyless account.
+ */
+ reason?: EnterPasswordReason;
}
diff --git a/packages/fxa-settings/src/pages/Signin/mocks.tsx b/packages/fxa-settings/src/pages/Signin/mocks.tsx
index f4c86adb80e..2049f5971cd 100644
--- a/packages/fxa-settings/src/pages/Signin/mocks.tsx
+++ b/packages/fxa-settings/src/pages/Signin/mocks.tsx
@@ -99,10 +99,10 @@ export function createMockSigninWebIntegration({
requiresKeys: () => false,
wantsKeysIfPasswordEntered: () => false,
wantsKeys: () => false,
- requiresPasswordForLogin(supportsKeysOptionalLogin = false) {
+ requiresPasswordForLogin(browserSupportsKeysOptional = false) {
return (
this.requiresKeys() ||
- (!supportsKeysOptionalLogin && this.wantsKeysIfPasswordEntered())
+ (!browserSupportsKeysOptional && this.wantsKeysIfPasswordEntered())
);
},
data: new IntegrationData(new GenericData({})),
@@ -111,6 +111,13 @@ export function createMockSigninWebIntegration({
isFirefoxClientServiceSmartWindow: () => false,
isFirefoxClientServiceVpn: () => false,
isFirefoxNonSync: () => false,
+ supportsKeylessLogin(b: boolean) {
+ return b && this.isFirefoxNonSync();
+ },
+ allowsPreKeysSyncLogin: () => false,
+ nonSyncKeysRequirePassword(b: boolean) {
+ return !b && this.wantsKeysIfPasswordEntered();
+ },
getWebChannelServices: mockGetWebChannelServices(),
wantsLogin: () => false,
wantsTwoStepAuthentication: () => false,
@@ -137,10 +144,10 @@ export function createMockSigninOAuthNativeSyncIntegration({
requiresKeys: () => isSync,
wantsKeysIfPasswordEntered: () => !isSync,
wantsKeys: () => true,
- requiresPasswordForLogin(supportsKeysOptionalLogin = false) {
+ requiresPasswordForLogin(browserSupportsKeysOptional = false) {
return (
this.requiresKeys() ||
- (!supportsKeysOptionalLogin && this.wantsKeysIfPasswordEntered())
+ (!browserSupportsKeysOptional && this.wantsKeysIfPasswordEntered())
);
},
getService: () => MozServices.FirefoxSync,
@@ -151,6 +158,13 @@ export function createMockSigninOAuthNativeSyncIntegration({
isFirefoxClientServiceSmartWindow: () => false,
isFirefoxClientServiceVpn: () => false,
isFirefoxNonSync: () => !isSync && !isMobile,
+ supportsKeylessLogin(b: boolean) {
+ return b && this.isFirefoxNonSync();
+ },
+ allowsPreKeysSyncLogin: () => false,
+ nonSyncKeysRequirePassword(b: boolean) {
+ return !b && this.wantsKeysIfPasswordEntered();
+ },
getWebChannelServices: mockGetWebChannelServices({ isSync }),
wantsLogin: () => false,
wantsTwoStepAuthentication: () => false,
@@ -181,10 +195,10 @@ export function createMockSigninOAuthIntegration({
requiresKeys: () => false,
wantsKeysIfPasswordEntered: () => false,
wantsKeys: () => false,
- requiresPasswordForLogin(supportsKeysOptionalLogin = false) {
+ requiresPasswordForLogin(browserSupportsKeysOptional = false) {
return (
this.requiresKeys() ||
- (!supportsKeysOptionalLogin && this.wantsKeysIfPasswordEntered())
+ (!browserSupportsKeysOptional && this.wantsKeysIfPasswordEntered())
);
},
wantsLogin: () => false,
@@ -195,6 +209,13 @@ export function createMockSigninOAuthIntegration({
isFirefoxClientServiceSmartWindow: () => false,
isFirefoxClientServiceVpn: () => false,
isFirefoxNonSync: () => false,
+ supportsKeylessLogin(b: boolean) {
+ return b && this.isFirefoxNonSync();
+ },
+ allowsPreKeysSyncLogin: () => false,
+ nonSyncKeysRequirePassword(b: boolean) {
+ return !b && this.wantsKeysIfPasswordEntered();
+ },
getWebChannelServices: mockGetWebChannelServices({ isSync }),
getCmsInfo: () => cmsInfo,
isFirefoxClient: () => false,
@@ -225,10 +246,10 @@ export function createMockSigninOAuthNativeIntegration({
requiresKeys: () => isSync,
wantsKeysIfPasswordEntered: () => isRelay || isSmartWindow || isVpn,
wantsKeys: () => true,
- requiresPasswordForLogin(supportsKeysOptionalLogin = false) {
+ requiresPasswordForLogin(browserSupportsKeysOptional = false) {
return (
this.requiresKeys() ||
- (!supportsKeysOptionalLogin && this.wantsKeysIfPasswordEntered())
+ (!browserSupportsKeysOptional && this.wantsKeysIfPasswordEntered())
);
},
wantsLogin: () => false,
@@ -239,6 +260,13 @@ export function createMockSigninOAuthNativeIntegration({
isFirefoxClientServiceSmartWindow: () => isSmartWindow,
isFirefoxClientServiceVpn: () => isVpn,
isFirefoxNonSync: () => isRelay || isSmartWindow || isVpn,
+ supportsKeylessLogin(b: boolean) {
+ return b && this.isFirefoxNonSync();
+ },
+ allowsPreKeysSyncLogin: () => false,
+ nonSyncKeysRequirePassword(b: boolean) {
+ return !b && this.wantsKeysIfPasswordEntered();
+ },
getWebChannelServices: mockGetWebChannelServices({
isSync,
isRelay,
@@ -367,7 +395,7 @@ export const mockSendUnblockEmailHandler: SendUnblockEmailHandler = () =>
export type SubjectProps = Partial & {
sessionToken?: hexstring;
cachedSigninHandler?: CachedSigninHandler;
- supportsKeysOptionalLogin?: boolean;
+ browserSupportsKeysOptional?: boolean;
passwordlessSupported?: boolean;
skipPasswordlessRedirect?: boolean;
/**
@@ -395,11 +423,11 @@ export const Subject = ({
sendUnblockEmailHandler = mockSendUnblockEmailHandler,
finishOAuthFlowHandler = mockFinishOAuthFlowHandler,
isSignedIntoFirefox = false,
- supportsKeysOptionalLogin = false,
+ browserSupportsKeysOptional = false,
passkeyEnabled = false,
...props // overrides
}: SubjectProps = {}) => {
- const useFxAStatusResult = mockUseFxAStatus({ supportsKeysOptionalLogin });
+ const useFxAStatusResult = mockUseFxAStatus({ browserSupportsKeysOptional });
const contextValue = mockAppContext();
if (passkeyEnabled && contextValue.config) {
contextValue.config.featureFlags = {
diff --git a/packages/fxa-settings/src/pages/Signin/utils.test.ts b/packages/fxa-settings/src/pages/Signin/utils.test.ts
index 954cbf71e01..44939748345 100644
--- a/packages/fxa-settings/src/pages/Signin/utils.test.ts
+++ b/packages/fxa-settings/src/pages/Signin/utils.test.ts
@@ -397,7 +397,9 @@ describe('Signin utils', () => {
const result = await handleNavigation(navigationOptions);
expect(result.error).toBeUndefined();
- expect(sessionResendVerifyCode).toHaveBeenCalledWith(MOCK_SESSION_TOKEN);
+ expect(sessionResendVerifyCode).toHaveBeenCalledWith(
+ MOCK_SESSION_TOKEN
+ );
expect(mockNavigate).toHaveBeenCalledWith(
'/confirm_signup_code',
expect.any(Object)
@@ -521,7 +523,7 @@ describe('Signin utils', () => {
service: OAuthNativeServices.Vpn,
}),
isSignInWithThirdPartyAuth: true,
- supportsKeysOptionalLogin: false,
+ browserSupportsKeysOptional: false,
performNavigation: true,
handleFxaOAuthLogin: false,
});
@@ -552,7 +554,7 @@ describe('Signin utils', () => {
service: OAuthNativeServices.Vpn,
}),
isSignInWithThirdPartyAuth: true,
- supportsKeysOptionalLogin: true,
+ browserSupportsKeysOptional: true,
performNavigation: true,
handleFxaOAuthLogin: true,
});
diff --git a/packages/fxa-settings/src/pages/Signin/utils.ts b/packages/fxa-settings/src/pages/Signin/utils.ts
index 9ad5c3a5032..95d45851822 100644
--- a/packages/fxa-settings/src/pages/Signin/utils.ts
+++ b/packages/fxa-settings/src/pages/Signin/utils.ts
@@ -298,7 +298,8 @@ export async function handleNavigation(navigationOptions: NavigationOptions) {
// unverified email) and we know their session isn't fully verified, then send them
// an otp code. Sending here couples the email with the actual navigation action.
if (
- (to?.includes('signin_token_code') || to?.includes('confirm_signup_code')) &&
+ (to?.includes('signin_token_code') ||
+ to?.includes('confirm_signup_code')) &&
navigationOptions.signinData.sessionToken &&
navigationOptions.signinData.verificationMethod ===
VerificationMethods.EMAIL_OTP
@@ -332,7 +333,8 @@ export async function handleNavigation(navigationOptions: NavigationOptions) {
return { error };
}
if (to) {
- performNavigation({ navigate,
+ performNavigation({
+ navigate,
to,
locationState,
shouldHardNavigate,
@@ -368,7 +370,8 @@ export async function handleNavigation(navigationOptions: NavigationOptions) {
if (navigationOptions.performNavigation !== false) {
const { to, locationState, shouldHardNavigate } =
await getNonOAuthNavigationTarget(navigationOptions);
- performNavigation({ navigate,
+ performNavigation({
+ navigate,
to,
locationState,
shouldHardNavigate,
@@ -409,7 +412,8 @@ export async function handleNavigation(navigationOptions: NavigationOptions) {
if (to === '/post_verify/service_welcome') {
navigate(to, { state: { origin: 'signin' }, replace: true });
} else {
- performNavigation({ navigate,
+ performNavigation({
+ navigate,
to,
locationState,
shouldHardNavigate,
@@ -574,7 +578,7 @@ const getOAuthNavigationTarget = async (
if (
navigationOptions.isSignInWithThirdPartyAuth &&
navigationOptions.integration.requiresPasswordForLogin(
- navigationOptions.supportsKeysOptionalLogin
+ navigationOptions.browserSupportsKeysOptional
)
) {
const syncNav = getSyncNavigate(navigationOptions.queryParams, {
diff --git a/packages/fxa-settings/src/pages/Signup/container.test.tsx b/packages/fxa-settings/src/pages/Signup/container.test.tsx
index c9b3d75f770..7ac1868497b 100644
--- a/packages/fxa-settings/src/pages/Signup/container.test.tsx
+++ b/packages/fxa-settings/src/pages/Signup/container.test.tsx
@@ -61,6 +61,9 @@ function mockIntegration() {
isFirefoxClientServiceSmartWindow: () => false,
isFirefoxClientServiceVpn: () => false,
isFirefoxNonSync: () => false,
+ supportsKeylessLogin(b: boolean) {
+ return b && this.isFirefoxNonSync();
+ },
getWebChannelServices: mockGetWebChannelServices({ isSync: true }),
getCmsInfo: () => undefined,
getLegalTerms: () => undefined,
@@ -192,7 +195,7 @@ async function render(text?: string) {
offeredSyncEngineConfigs: [],
selectedEnginesForGlean: {},
declinedSyncEngines: [],
- supportsKeysOptionalLogin: false,
+ browserSupportsKeysOptional: false,
supportsCanLinkAccountUid: false,
}}
flowQueryParams={{ flowId: MOCK_FLOW_ID }}
diff --git a/packages/fxa-settings/src/pages/Signup/index.stories.tsx b/packages/fxa-settings/src/pages/Signup/index.stories.tsx
index e985f0073d2..47efa43161f 100644
--- a/packages/fxa-settings/src/pages/Signup/index.stories.tsx
+++ b/packages/fxa-settings/src/pages/Signup/index.stories.tsx
@@ -31,16 +31,16 @@ const StoryWithProps = ({
integration = createMockSignupOAuthWebIntegration(),
isMobile = false,
offeredSyncEnginesOverride,
- supportsKeysOptionalLogin = false,
+ browserSupportsKeysOptional = false,
}: {
integration?: SignupIntegration;
offeredSyncEnginesOverride?: ReturnType;
isMobile?: boolean;
- supportsKeysOptionalLogin?: boolean;
+ browserSupportsKeysOptional?: boolean;
}) => {
const useFxAStatusResult = mockUseFxAStatus({
offeredSyncEnginesOverride,
- supportsKeysOptionalLogin,
+ browserSupportsKeysOptional,
});
return (
@@ -91,7 +91,7 @@ export const WithThirdPartyAuthServiceRelayIntegration = () => (
OAuthNativeServices.Relay,
false
)}
- supportsKeysOptionalLogin={true}
+ browserSupportsKeysOptional={true}
/>
);
@@ -101,7 +101,7 @@ export const WithThirdPartyAuthServiceSmartWindowIntegration = () => (
OAuthNativeServices.SmartWindow,
false
)}
- supportsKeysOptionalLogin={true}
+ browserSupportsKeysOptional={true}
/>
);
diff --git a/packages/fxa-settings/src/pages/Signup/index.test.tsx b/packages/fxa-settings/src/pages/Signup/index.test.tsx
index 794224d8381..4cdbb0d03d4 100644
--- a/packages/fxa-settings/src/pages/Signup/index.test.tsx
+++ b/packages/fxa-settings/src/pages/Signup/index.test.tsx
@@ -113,7 +113,7 @@ const user = userEvent.setup();
// Password confirmation is required only when the password derives encryption
// keys: Sync (keys mandatory), and non-Sync Firefox services (Relay/VPN/
// SmartWindow) when the browser lacks the keys-optional capability
-// (`supportsKeysOptionalLogin` false). Plain web/OAuth-web signups omit it.
+// (`browserSupportsKeysOptional` false). Plain web/OAuth-web signups omit it.
describe('Signup page', () => {
beforeEach(() => {
@@ -205,7 +205,7 @@ describe('Signup page', () => {
);
});
- it('renders third party auth when service=relay and supportsKeysOptionalLogin is true', async () => {
+ it('renders third party auth when service=relay and browserSupportsKeysOptional is true', async () => {
await act(() => {
renderWithLocalizationProvider(
{
OAuthNativeServices.Relay,
false
)}
- supportsKeysOptionalLogin={true}
+ browserSupportsKeysOptional={true}
/>
);
});
@@ -226,7 +226,7 @@ describe('Signup page', () => {
).toBeInTheDocument();
});
- it('renders third party auth when service=smartwindow and supportsKeysOptionalLogin is true', async () => {
+ it('renders third party auth when service=smartwindow and browserSupportsKeysOptional is true', async () => {
await act(() => {
renderWithLocalizationProvider(
{
OAuthNativeServices.SmartWindow,
false
)}
- supportsKeysOptionalLogin={true}
+ browserSupportsKeysOptional={true}
/>
);
});
@@ -255,7 +255,7 @@ describe('Signup page', () => {
OAuthNativeServices.Vpn,
false
)}
- supportsKeysOptionalLogin={false}
+ browserSupportsKeysOptional={false}
/>
);
});
@@ -275,7 +275,7 @@ describe('Signup page', () => {
// password, so the confirmation field is dropped.
/* requiresPasswordForLogin */ false
)}
- supportsKeysOptionalLogin={true}
+ browserSupportsKeysOptional={true}
/>
);
});
diff --git a/packages/fxa-settings/src/pages/Signup/index.tsx b/packages/fxa-settings/src/pages/Signup/index.tsx
index 3ef9d95cbdd..e9ee95d92db 100644
--- a/packages/fxa-settings/src/pages/Signup/index.tsx
+++ b/packages/fxa-settings/src/pages/Signup/index.tsx
@@ -50,7 +50,7 @@ export const Signup = ({
offeredSyncEngineConfigs,
declinedSyncEngines,
selectedEnginesForGlean,
- supportsKeysOptionalLogin,
+ browserSupportsKeysOptional,
},
flowQueryParams,
isMobile,
@@ -72,7 +72,7 @@ export const Signup = ({
const paymentMethodsWillSync =
isSync && checkPaymentMethodsWillSync(offeredSyncEngines);
const showThirdPartyAuth = isOAuthNative
- ? supportsKeysOptionalLogin
+ ? integration.supportsKeylessLogin(browserSupportsKeysOptional)
: !isSync;
const legalTerms = integration.getLegalTerms();
@@ -341,10 +341,10 @@ export const Signup = ({
// Require password confirmation whenever the password is used for key
// derivation — Sync (keys mandatory) and non-Sync Firefox services
// that derive keys when the browser can't complete a keyless login
- // (`supportsKeysOptionalLogin` false).
+ // (`browserSupportsKeysOptional` false).
requirePasswordConfirmation:
integration.isSync() ||
- integration.requiresPasswordForLogin(supportsKeysOptionalLogin),
+ integration.requiresPasswordForLogin(browserSupportsKeysOptional),
setSelectedNewsletterSlugs,
cmsButton: {
text: cmsInfo?.SignupSetPasswordPage.primaryButtonText,
diff --git a/packages/fxa-settings/src/pages/Signup/interfaces.ts b/packages/fxa-settings/src/pages/Signup/interfaces.ts
index a2f35e9b88b..22d3cf8ecfa 100644
--- a/packages/fxa-settings/src/pages/Signup/interfaces.ts
+++ b/packages/fxa-settings/src/pages/Signup/interfaces.ts
@@ -55,6 +55,7 @@ export type SignupOAuthIntegration = Pick<
| 'requiresKeys'
| 'wantsKeys'
| 'requiresPasswordForLogin'
+ | 'supportsKeylessLogin'
| 'getClientId'
| 'getCmsInfo'
| 'getLegalTerms'
@@ -73,6 +74,7 @@ export type SignupBaseIntegration = Pick<
| 'requiresKeys'
| 'wantsKeys'
| 'requiresPasswordForLogin'
+ | 'supportsKeylessLogin'
| 'getClientId'
| 'getCmsInfo'
| 'getLegalTerms'
diff --git a/packages/fxa-settings/src/pages/Signup/mocks.tsx b/packages/fxa-settings/src/pages/Signup/mocks.tsx
index 45d813f251b..ae3e5671957 100644
--- a/packages/fxa-settings/src/pages/Signup/mocks.tsx
+++ b/packages/fxa-settings/src/pages/Signup/mocks.tsx
@@ -36,6 +36,9 @@ export function createMockSignupWebIntegration(): SignupBaseIntegration {
isFirefoxClientServiceSmartWindow: () => false,
isFirefoxClientServiceVpn: () => false,
isFirefoxNonSync: () => false,
+ supportsKeylessLogin(b: boolean) {
+ return b && this.isFirefoxNonSync();
+ },
getWebChannelServices: mockGetWebChannelServices(),
requiresKeys: () => false,
wantsKeys: () => false,
@@ -55,6 +58,9 @@ export function createMockSignupSyncDesktopV3Integration(): SignupBaseIntegratio
isFirefoxClientServiceSmartWindow: () => false,
isFirefoxClientServiceVpn: () => false,
isFirefoxNonSync: () => false,
+ supportsKeylessLogin(b: boolean) {
+ return b && this.isFirefoxNonSync();
+ },
getWebChannelServices: mockGetWebChannelServices({ isSync: true }),
requiresKeys: () => true,
wantsKeys: () => true,
@@ -80,6 +86,9 @@ export function createMockSignupOAuthWebIntegration(
isFirefoxClientServiceSmartWindow: () => false,
isFirefoxClientServiceVpn: () => false,
isFirefoxNonSync: () => false,
+ supportsKeylessLogin(b: boolean) {
+ return b && this.isFirefoxNonSync();
+ },
getWebChannelServices: mockGetWebChannelServices(),
requiresKeys: () => false,
wantsKeys: () => false,
@@ -109,6 +118,9 @@ export function createMockSignupOAuthNativeIntegration(
isFirefoxClientServiceSmartWindow: () => isSmartWindow,
isFirefoxClientServiceVpn: () => isVpn,
isFirefoxNonSync: () => isRelay || isSmartWindow || isVpn,
+ supportsKeylessLogin(b: boolean) {
+ return b && this.isFirefoxNonSync();
+ },
getWebChannelServices: mockGetWebChannelServices({
isSync,
isRelay,
@@ -172,16 +184,16 @@ export const Subject = ({
beginSignupHandler = mockBeginSignupHandler,
email = MOCK_EMAIL,
isMobile = false,
- supportsKeysOptionalLogin = false,
+ browserSupportsKeysOptional = false,
}: {
email?: string;
integration?: SignupIntegration;
beginSignupHandler?: BeginSignupHandler;
isMobile?: boolean;
- supportsKeysOptionalLogin?: boolean;
+ browserSupportsKeysOptional?: boolean;
}) => {
const mockUseFxAStatusResult = mockUseFxAStatus({
- supportsKeysOptionalLogin,
+ browserSupportsKeysOptional,
});
return (