From 62eca27173fd1b3976a13975b856e2da657924fd Mon Sep 17 00:00:00 2001 From: Lauren Zugai Date: Fri, 3 Jul 2026 09:34:38 -0500 Subject: [PATCH] fix(settings): VPN polish - CMS copy on set-password, repeat PW when Sync isn't decoupled, service_welcome routing Because: - The CMS text for passwordless OTP users when Sync is not decoupled is not rendered on the set password page - The repeat password field is not displayed when Sync is not decoupled - Mobile users should not be navigated to the service welcome screen for VPN because the browser automatically closes it This commit: - Renders the CMS text and repeat password fields accordingly - Returns a fixed requiresPasswordForLogin value from signup mock instead of rederiving it - Adds a Mobile check and does not navigate those users to the service welcome screen closes FXA-14047 closes FXA-14088 --- .../tests/misc/relayIntegration.spec.ts | 4 ++ .../tests/misc/smartWindowIntegration.spec.ts | 4 ++ .../PostVerify/SetPassword/index.stories.tsx | 5 ++ .../PostVerify/SetPassword/index.test.tsx | 43 +++++++++++++++-- .../pages/PostVerify/SetPassword/index.tsx | 22 ++++----- .../Signup/ConfirmSignupCode/index.test.tsx | 25 ++++++++++ .../pages/Signup/ConfirmSignupCode/index.tsx | 12 +++-- .../src/pages/Signup/container.test.tsx | 1 + .../src/pages/Signup/index.test.tsx | 47 +++++++++++++++++-- .../fxa-settings/src/pages/Signup/index.tsx | 8 +++- .../src/pages/Signup/interfaces.ts | 2 + .../fxa-settings/src/pages/Signup/mocks.tsx | 7 ++- packages/fxa-settings/src/pages/mocks.tsx | 7 +++ 13 files changed, 162 insertions(+), 25 deletions(-) diff --git a/packages/functional-tests/tests/misc/relayIntegration.spec.ts b/packages/functional-tests/tests/misc/relayIntegration.spec.ts index 171975a47d3..12efa9a946c 100644 --- a/packages/functional-tests/tests/misc/relayIntegration.spec.ts +++ b/packages/functional-tests/tests/misc/relayIntegration.spec.ts @@ -32,6 +32,10 @@ test.describe('relay integration', () => { ).toBeVisible(); await signup.passwordTextbox.fill(password); + // keys_optional can't be sent in Firefox tests yet (FXA-13647), so this + // key-deriving desktop signup requires a password and renders the + // confirmation field. + await signup.verifyPasswordTextbox.fill(password); await signup.createAccountButton.click(); await page.waitForURL(/confirm_signup_code/); diff --git a/packages/functional-tests/tests/misc/smartWindowIntegration.spec.ts b/packages/functional-tests/tests/misc/smartWindowIntegration.spec.ts index 365eecfd19e..f25665ef223 100644 --- a/packages/functional-tests/tests/misc/smartWindowIntegration.spec.ts +++ b/packages/functional-tests/tests/misc/smartWindowIntegration.spec.ts @@ -28,6 +28,10 @@ test.describe('smart window integration', () => { await page.waitForURL(/signup/); await signup.passwordTextbox.fill(password); + // keys_optional can't be sent in Firefox tests yet (FXA-13647), so this + // key-deriving desktop signup requires a password and renders the + // confirmation field. + await signup.verifyPasswordTextbox.fill(password); await signup.createAccountButton.click(); await page.waitForURL(/confirm_signup_code/); diff --git a/packages/fxa-settings/src/pages/PostVerify/SetPassword/index.stories.tsx b/packages/fxa-settings/src/pages/PostVerify/SetPassword/index.stories.tsx index b7497f01b3d..12c51650c50 100644 --- a/packages/fxa-settings/src/pages/PostVerify/SetPassword/index.stories.tsx +++ b/packages/fxa-settings/src/pages/PostVerify/SetPassword/index.stories.tsx @@ -27,3 +27,8 @@ export const Default = storyWithProps(); export const WithCms = storyWithProps({ integration: createMockIntegrationWithCms(), }); + +export const OtpWithCms = storyWithProps({ + passwordCreationReason: 'otp', + integration: createMockIntegrationWithCms(), +}); diff --git a/packages/fxa-settings/src/pages/PostVerify/SetPassword/index.test.tsx b/packages/fxa-settings/src/pages/PostVerify/SetPassword/index.test.tsx index e906f17e8be..bd046e13d12 100644 --- a/packages/fxa-settings/src/pages/PostVerify/SetPassword/index.test.tsx +++ b/packages/fxa-settings/src/pages/PostVerify/SetPassword/index.test.tsx @@ -6,7 +6,11 @@ import { renderWithLocalizationProvider } from 'fxa-react/lib/test-utils/localiz import { createMockIntegration, Subject } from './mocks'; import { screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { MOCK_EMAIL } from '../../mocks'; +import { + MOCK_EMAIL, + MOCK_CMS_INFO, + createMockIntegrationWithCms, +} from '../../mocks'; import { RelierCmsInfo } from '../../../models'; import GleanMetrics from '../../../lib/glean'; @@ -55,14 +59,14 @@ describe('SetPassword page', () => { }); it('renders CMS overrides when PostVerifySetPasswordPage is set', () => { - const cmsInfo = { - shared: { buttonColor: '#333' }, + const cmsInfo: RelierCmsInfo = { + ...MOCK_CMS_INFO, PostVerifySetPasswordPage: { headline: 'Set a sync password', description: 'Your password encrypts your synced data.', primaryButtonText: 'Sync now', }, - } as unknown as RelierCmsInfo; + }; renderWithLocalizationProvider( @@ -80,6 +84,37 @@ describe('SetPassword page', () => { ).not.toBeInTheDocument(); }); + it.each([['otp'], ['passkey']] as const)( + 'renders the CMS description instead of the passwordless copy on the %s path', + (passwordCreationReason) => { + renderWithLocalizationProvider( + + ); + + expect( + screen.getByText(MOCK_CMS_INFO.PostVerifySetPasswordPage.description) + ).toBeInTheDocument(); + expect( + screen.queryByText( + 'This password encrypts your synced data and keeps it secure.' + ) + ).not.toBeInTheDocument(); + } + ); + + it('falls back to the passwordless info copy on the OTP path when no CMS description is set', () => { + renderWithLocalizationProvider(); + + expect( + screen.getByText( + 'This password encrypts your synced data and keeps it secure.' + ) + ).toBeInTheDocument(); + }); + describe('Glean events', () => { // First row asserts the default-arg behaviour (no prop passed); the // others assert explicit pass-through for each non-default value. diff --git a/packages/fxa-settings/src/pages/PostVerify/SetPassword/index.tsx b/packages/fxa-settings/src/pages/PostVerify/SetPassword/index.tsx index d64575d162d..65b7dd338e3 100644 --- a/packages/fxa-settings/src/pages/PostVerify/SetPassword/index.tsx +++ b/packages/fxa-settings/src/pages/PostVerify/SetPassword/index.tsx @@ -124,17 +124,17 @@ export const SetPassword = ({ )} - {passwordCreationReason === 'third_party_auth' ? ( - cmsDescription ? ( -

{cmsDescription}

- ) : ( - -

- This encrypts your data. It needs to be different from your Google - or Apple account password. -

-
- ) + {/* CMS copy is client-scoped, so it wins on every arrival path; the + hardcoded copy (which differs per path) is the no-CMS fallback. */} + {cmsDescription ? ( +

{cmsDescription}

+ ) : passwordCreationReason === 'third_party_auth' ? ( + +

+ This encrypts your data. It needs to be different from your Google + or Apple account password. +

+
) : (

diff --git a/packages/fxa-settings/src/pages/Signup/ConfirmSignupCode/index.test.tsx b/packages/fxa-settings/src/pages/Signup/ConfirmSignupCode/index.test.tsx index a055eac3d67..11fbb486cee 100644 --- a/packages/fxa-settings/src/pages/Signup/ConfirmSignupCode/index.test.tsx +++ b/packages/fxa-settings/src/pages/Signup/ConfirmSignupCode/index.test.tsx @@ -418,6 +418,31 @@ describe('ConfirmSignupCode page', () => { }); }); + it('does not navigate to service_welcome when service=vpn on mobile (page automatically closes on fxaOAuthLogin)', async () => { + const integration = createMockOAuthNativeIntegration( + false, + OAuthNativeServices.Vpn + ); + jest.spyOn(integration, 'isFirefoxMobileClient').mockReturnValue(true); + renderWithSession({ + session, + integration, + finishOAuthFlowHandler: mockFinishOAuthFlowHandler, + }); + submit(); + + await waitFor(() => { + expect(fxaOAuthLoginSpy).toHaveBeenCalledWith({ + action: 'signup', + code: MOCK_OAUTH_FLOW_HANDLER_RESPONSE.code, + redirect: MOCK_OAUTH_FLOW_HANDLER_RESPONSE.redirect, + state: MOCK_OAUTH_FLOW_HANDLER_RESPONSE.state, + scope: MOCK_OAUTH_FLOW_HANDLER_RESPONSE.scope, + }); + }); + expect(mockNavigate).not.toHaveBeenCalled(); + }); + it('does not submit on paste when service=sync', async () => { const user = userEvent.setup(); const integration = createMockOAuthNativeIntegration(); diff --git a/packages/fxa-settings/src/pages/Signup/ConfirmSignupCode/index.tsx b/packages/fxa-settings/src/pages/Signup/ConfirmSignupCode/index.tsx index b6d70129f7f..ad524b45e77 100644 --- a/packages/fxa-settings/src/pages/Signup/ConfirmSignupCode/index.tsx +++ b/packages/fxa-settings/src/pages/Signup/ConfirmSignupCode/index.tsx @@ -294,9 +294,15 @@ const ConfirmSignupCode = ({ scope, }); if (integration.isFirefoxClientServiceVpn()) { - navigate('/post_verify/service_welcome', { - state: { origin: 'signup' }, - }); + // Don't navigate mobile users. The client controls the web view + // and users will see a "blip" of the service welcome page before + // the client closes the view in response to the fxaOAuthLogin web + // channel message. See FXA-11944. + if (!integration.isFirefoxMobileClient()) { + navigate('/post_verify/service_welcome', { + state: { origin: 'signup' }, + }); + } } else { goToSettingsWithAlertSuccess(); } diff --git a/packages/fxa-settings/src/pages/Signup/container.test.tsx b/packages/fxa-settings/src/pages/Signup/container.test.tsx index c7b172f1525..2c59b337cf7 100644 --- a/packages/fxa-settings/src/pages/Signup/container.test.tsx +++ b/packages/fxa-settings/src/pages/Signup/container.test.tsx @@ -58,6 +58,7 @@ function mockIntegration() { isSync: () => true, requiresKeys: () => true, wantsKeys: () => true, + requiresPasswordForLogin: () => true, isFirefoxClientServiceRelay: () => false, isFirefoxClientServiceSmartWindow: () => false, isFirefoxClientServiceVpn: () => false, diff --git a/packages/fxa-settings/src/pages/Signup/index.test.tsx b/packages/fxa-settings/src/pages/Signup/index.test.tsx index f676c9430e6..722d4170fbb 100644 --- a/packages/fxa-settings/src/pages/Signup/index.test.tsx +++ b/packages/fxa-settings/src/pages/Signup/index.test.tsx @@ -110,9 +110,10 @@ async function submit() { const user = userEvent.setup(); -// Note: we are removing the password confirmation requirement for signup -// except for Sync (in in FXA-10467). All tests by default will assume that -// password confirmation is not present or required except for Sync/Desktop Relay. +// 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. describe('Signup page', () => { beforeEach(() => { @@ -187,7 +188,7 @@ describe('Signup page', () => { ); }); - expect(screen.queryByLabelText('Repeat password')).not.toBeInTheDocument(); + expect(screen.getByLabelText('Repeat password')).toBeVisible(); // newsletters and third party auth should not be displayed expect(screen.queryAllByRole('checkbox')).toHaveLength(0); @@ -246,6 +247,42 @@ describe('Signup page', () => { ).toBeInTheDocument(); }); + it('requires password confirmation for a service when keys-optional login is unsupported', async () => { + await act(() => { + renderWithLocalizationProvider( + + ); + }); + + expect(screen.getByLabelText('Repeat password')).toBeVisible(); + }); + + it('does not require password confirmation for a service when keys-optional login is supported', async () => { + await act(() => { + renderWithLocalizationProvider( + + ); + }); + + expect(screen.queryByLabelText('Repeat password')).not.toBeInTheDocument(); + }); + it('renders as expected when cms enabled', async () => { renderWithLocalizationProvider( { beginSignupHandler={mockBeginSignupHandler} /> ); - await fillOutForm(false); + await fillOutForm(true); await submit(); await waitFor(() => { diff --git a/packages/fxa-settings/src/pages/Signup/index.tsx b/packages/fxa-settings/src/pages/Signup/index.tsx index bba652648a3..3ef9d95cbdd 100644 --- a/packages/fxa-settings/src/pages/Signup/index.tsx +++ b/packages/fxa-settings/src/pages/Signup/index.tsx @@ -338,7 +338,13 @@ export const Signup = ({ handleSubmit, onSubmit, offeredSyncEngineConfigs, - requirePasswordConfirmation: isSync, + // 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). + requirePasswordConfirmation: + integration.isSync() || + integration.requiresPasswordForLogin(supportsKeysOptionalLogin), 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 dc614dcdb38..a2f35e9b88b 100644 --- a/packages/fxa-settings/src/pages/Signup/interfaces.ts +++ b/packages/fxa-settings/src/pages/Signup/interfaces.ts @@ -54,6 +54,7 @@ export type SignupOAuthIntegration = Pick< | 'getWebChannelServices' | 'requiresKeys' | 'wantsKeys' + | 'requiresPasswordForLogin' | 'getClientId' | 'getCmsInfo' | 'getLegalTerms' @@ -71,6 +72,7 @@ export type SignupBaseIntegration = Pick< | 'getWebChannelServices' | 'requiresKeys' | 'wantsKeys' + | 'requiresPasswordForLogin' | 'getClientId' | 'getCmsInfo' | 'getLegalTerms' diff --git a/packages/fxa-settings/src/pages/Signup/mocks.tsx b/packages/fxa-settings/src/pages/Signup/mocks.tsx index 9f0f3e3aa55..3cec44b8295 100644 --- a/packages/fxa-settings/src/pages/Signup/mocks.tsx +++ b/packages/fxa-settings/src/pages/Signup/mocks.tsx @@ -39,6 +39,7 @@ export function createMockSignupWebIntegration(): SignupBaseIntegration { getWebChannelServices: mockGetWebChannelServices(), requiresKeys: () => false, wantsKeys: () => false, + requiresPasswordForLogin: () => false, getCmsInfo: () => undefined, getLegalTerms: () => undefined, }; @@ -57,6 +58,7 @@ export function createMockSignupSyncDesktopV3Integration(): SignupBaseIntegratio getWebChannelServices: mockGetWebChannelServices({ isSync: true }), requiresKeys: () => true, wantsKeys: () => true, + requiresPasswordForLogin: () => true, getCmsInfo: () => undefined, getLegalTerms: () => undefined, }; @@ -81,6 +83,7 @@ export function createMockSignupOAuthWebIntegration( getWebChannelServices: mockGetWebChannelServices(), requiresKeys: () => false, wantsKeys: () => false, + requiresPasswordForLogin: () => false, getCmsInfo: () => cmsInfo, getLegalTerms: () => undefined, }; @@ -89,7 +92,8 @@ export function createMockSignupOAuthWebIntegration( export function createMockSignupOAuthNativeIntegration( service?: string, isSync = true, - cmsInfo?: RelierCmsInfo + cmsInfo?: RelierCmsInfo, + requiresPasswordForLogin = true ): SignupOAuthIntegration { const isRelay = service === OAuthNativeServices.Relay; const isSmartWindow = service === OAuthNativeServices.SmartWindow; @@ -113,6 +117,7 @@ export function createMockSignupOAuthNativeIntegration( }), requiresKeys: () => true, wantsKeys: () => true, + requiresPasswordForLogin: () => requiresPasswordForLogin, getCmsInfo: () => cmsInfo, getLegalTerms: () => undefined, }; diff --git a/packages/fxa-settings/src/pages/mocks.tsx b/packages/fxa-settings/src/pages/mocks.tsx index a7b5874f26d..07bfdb7a2b6 100644 --- a/packages/fxa-settings/src/pages/mocks.tsx +++ b/packages/fxa-settings/src/pages/mocks.tsx @@ -157,6 +157,13 @@ export const MOCK_CMS_INFO = { }, }, + PostVerifySetPasswordPage: { + headline: 'Create a password to sync', + description: + 'A Mozilla account works with sync to help protect your browsing, passwords, and more.', + primaryButtonText: 'Continue', + pageTitle: 'Create a password to sync', + }, SigninPage: { headline: 'Enter your password', description: 'for your Mozilla account',