diff --git a/packages/fxa-auth-client/lib/client.ts b/packages/fxa-auth-client/lib/client.ts index 0f14e61ce66..870fdb98a7b 100644 --- a/packages/fxa-auth-client/lib/client.ts +++ b/packages/fxa-auth-client/lib/client.ts @@ -1538,14 +1538,28 @@ export default class AuthClient { email: string, newPassword: string, headers?: Headers - ): Promise { - const newCredentials = await crypto.getCredentials(email, newPassword); + ): Promise<{ passwordCreated: number; authPW: string; unwrapBKey: string }> { + const { authPW, unwrapBKey } = await crypto.getCredentials( + email, + newPassword + ); const payload = { - authPW: newCredentials.authPW, + authPW, }; - return this.sessionPost('/password/create', sessionToken, payload, headers); + const passwordCreated = await this.sessionPost( + '/password/create', + sessionToken, + payload, + headers + ); + + return { + passwordCreated, + authPW, + unwrapBKey, + }; } async getRandomBytes(headers?: Headers) { diff --git a/packages/fxa-content-server/server/lib/routes/react-app/index.js b/packages/fxa-content-server/server/lib/routes/react-app/index.js index b6e8bc9b1a3..94304670f2a 100644 --- a/packages/fxa-content-server/server/lib/routes/react-app/index.js +++ b/packages/fxa-content-server/server/lib/routes/react-app/index.js @@ -128,7 +128,7 @@ const getReactRouteGroups = (showReactApp, reactRoute) => { 'post_verify/third_party_auth/callback', 'post_verify/third_party_auth/set_password', ]), - fullProdRollout: false, + fullProdRollout: true, }, webChannelExampleRoutes: { diff --git a/packages/fxa-settings/src/components/App/index.tsx b/packages/fxa-settings/src/components/App/index.tsx index 036ca21b40d..e7a95a95b12 100644 --- a/packages/fxa-settings/src/components/App/index.tsx +++ b/packages/fxa-settings/src/components/App/index.tsx @@ -79,6 +79,8 @@ import SignupConfirmed from '../../pages/Signup/SignupConfirmed'; import WebChannelExample from '../../pages/WebChannelExample'; import SignoutSync from '../Settings/SignoutSync'; import InlineRecoveryKeySetupContainer from '../../pages/InlineRecoveryKeySetup/container'; +import SetPassword from '../../pages/PostVerify/SetPassword'; +import SetPasswordContainer from '../../pages/PostVerify/SetPassword/container'; const Settings = lazy(() => import('../Settings')); @@ -318,6 +320,10 @@ const AuthAndAccountSetupRoutes = ({ path="/post_verify/third_party_auth/callback/*" {...{ flowQueryParams }} /> + {/* Reset password */} { + const showCWTS = () => { + if (isSync) { + if (offeredSyncEngineConfigs) { + return ( + + ); + } else { + // Waiting to receive webchannel message from browser + return ; + } + } else { + // Display nothing if Sync flow that does not support webchannels + // or if CWTS is disabled + return <>; + } + }; + + return ( + + {setAgeCheckErrorText && + setAgeCheckErrorText && + onFocusAgeInput && + onBlurAgeInput && ( + <> + {/* TODO: original component had a SR-only label that is not straightforward to implement with existing InputText component +SR-only text: "How old are you? To learn why we ask for your age, follow the “why do we ask” link below. */} + + { + // clear error tooltip if user types in the field + if (ageCheckErrorText) { + setAgeCheckErrorText(''); + } + }} + inputRef={register({ + pattern: /^[0-9]*$/, + maxLength: 3, + required: true, + })} + onFocusCb={onFocusAgeInput} + onBlurCb={onBlurAgeInput} + errorText={ageCheckErrorText} + tooltipPosition="bottom" + anchorPosition="end" + prefixDataTestId="age" + /> + + + GleanMetrics.registration.whyWeAsk()} + > + Why do we ask? + + + + )} + + {isSync + ? showCWTS() + : !isDesktopRelay && + setSelectedNewsletterSlugs && ( + + )} + + ); +}; diff --git a/packages/fxa-settings/src/components/FormSetupAccount/interfaces.ts b/packages/fxa-settings/src/components/FormSetupAccount/interfaces.ts new file mode 100644 index 00000000000..942459d7d4d --- /dev/null +++ b/packages/fxa-settings/src/components/FormSetupAccount/interfaces.ts @@ -0,0 +1,33 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { UseFormMethods } from 'react-hook-form'; +import { SetPasswordFormData } from '../../pages/PostVerify/SetPassword/interfaces'; +import { SignupFormData } from '../../pages/Signup/interfaces'; +import { syncEngineConfigs } from '../ChooseWhatToSync/sync-engines'; + +export type FormSetupAccountData = SignupFormData | SetPasswordFormData; + +export type FormSetupAccountProps = { + formState: UseFormMethods['formState']; + errors: UseFormMethods['errors']; + trigger: UseFormMethods['trigger']; + register: UseFormMethods['register']; + getValues: UseFormMethods['getValues']; + onFocus: () => void; + email: string; + onFocusMetricsEvent: () => void; + onSubmit: (e?: React.BaseSyntheticEvent) => Promise; + loading: boolean; + isSync: boolean; + offeredSyncEngineConfigs?: typeof syncEngineConfigs; + setDeclinedSyncEngines: React.Dispatch>; + isDesktopRelay: boolean; + setSelectedNewsletterSlugs?: React.Dispatch>; + // Age check props, if not provided it will not be rendered + ageCheckErrorText?: string; + setAgeCheckErrorText?: React.Dispatch>; + onFocusAgeInput?: () => void; + onBlurAgeInput?: () => void; +}; diff --git a/packages/fxa-settings/src/lib/hooks/useSyncEngines/index.tsx b/packages/fxa-settings/src/lib/hooks/useSyncEngines/index.tsx new file mode 100644 index 00000000000..4a5e2f248e7 --- /dev/null +++ b/packages/fxa-settings/src/lib/hooks/useSyncEngines/index.tsx @@ -0,0 +1,118 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { useEffect, useMemo, useState } from 'react'; +import { + Integration, + isOAuthIntegration, + isSyncDesktopV3Integration, +} from '../../../models'; +import { + defaultDesktopV3SyncEngineConfigs, + getSyncEngineIds, + syncEngineConfigs, + webChannelDesktopV3EngineConfigs, +} from '../../../components/ChooseWhatToSync/sync-engines'; +import firefox from '../../channels/firefox'; +import { Constants } from '../../constants'; + +type SyncEnginesIntegration = Pick; + +const useSyncEngines = (integration: SyncEnginesIntegration) => { + const isSyncOAuth = isOAuthIntegration(integration) && integration.isSync(); + const isSyncDesktopV3 = isSyncDesktopV3Integration(integration); + const isSync = integration.isSync(); + + const [webChannelEngines, setWebChannelEngines] = useState< + string[] | undefined + >(); + const [offeredSyncEngineConfigs, setOfferedSyncEngineConfigs] = useState< + typeof syncEngineConfigs | undefined + >(); + const [declinedSyncEngines, setDeclinedSyncEngines] = useState([]); + + useEffect(() => { + // This sends a web channel message to the browser to prompt a response + // that we listen for. + // TODO: In content-server, we send this on app-start for all integration types. + // Do we want to move this somewhere else once the index page is Reactified? + if (isSync) { + (async () => { + const status = await firefox.fxaStatus({ + // TODO: Improve getting 'context', probably set this on the integration + context: isSyncDesktopV3 + ? Constants.FX_DESKTOP_V3_CONTEXT + : Constants.OAUTH_CONTEXT, + isPairing: false, + service: Constants.SYNC_SERVICE, + }); + if (!webChannelEngines && status.capabilities.engines) { + // choose_what_to_sync may be disabled for mobile sync, see: + // https://github.com/mozilla/application-services/issues/1761 + // Desktop OAuth Sync will always provide this capability too + // for consistency. + if ( + isSyncDesktopV3 || + (isSyncOAuth && status.capabilities.choose_what_to_sync) + ) { + setWebChannelEngines(status.capabilities.engines); + } + } + })(); + } + }, [isSync, isSyncDesktopV3, isSyncOAuth, webChannelEngines]); + + useEffect(() => { + if (webChannelEngines) { + if (isSyncDesktopV3) { + // Desktop v3 web channel message sends additional engines + setOfferedSyncEngineConfigs([ + ...defaultDesktopV3SyncEngineConfigs, + ...webChannelDesktopV3EngineConfigs.filter((engine) => + webChannelEngines.includes(engine.id) + ), + ]); + } else if (isSyncOAuth) { + // OAuth Webchannel context sends all engines + setOfferedSyncEngineConfigs( + syncEngineConfigs.filter((engine) => + webChannelEngines.includes(engine.id) + ) + ); + } + } + }, [isSyncDesktopV3, isSyncOAuth, webChannelEngines]); + + useEffect(() => { + if (offeredSyncEngineConfigs) { + const defaultDeclinedSyncEngines = offeredSyncEngineConfigs + .filter((engineConfig) => !engineConfig.defaultChecked) + .map((engineConfig) => engineConfig.id); + setDeclinedSyncEngines(defaultDeclinedSyncEngines); + } + }, [offeredSyncEngineConfigs, setDeclinedSyncEngines]); + + const offeredSyncEngines = getSyncEngineIds(offeredSyncEngineConfigs || []); + + const selectedEngines = useMemo(() => { + if (isSync) { + return offeredSyncEngines.reduce((acc, syncEngId) => { + acc[syncEngId] = !declinedSyncEngines.includes(syncEngId); + return acc; + }, {} as Record); + } + return {}; + }, [isSync, declinedSyncEngines, offeredSyncEngines]); + + return { + offeredSyncEngines, + offeredSyncEngineConfigs, + declinedSyncEngines, + setDeclinedSyncEngines, + // for metrics + selectedEngines, + }; +}; + +export default useSyncEngines; diff --git a/packages/fxa-settings/src/lib/hooks/useSyncEngines/mocks.tsx b/packages/fxa-settings/src/lib/hooks/useSyncEngines/mocks.tsx new file mode 100644 index 00000000000..5e1e7568b35 --- /dev/null +++ b/packages/fxa-settings/src/lib/hooks/useSyncEngines/mocks.tsx @@ -0,0 +1,27 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { useState } from 'react'; +import { + getSyncEngineIds, + syncEngineConfigs, +} from '../../../components/ChooseWhatToSync/sync-engines'; + +export const useMockSyncEngines = () => { + const [declinedSyncEngines, setDeclinedSyncEngines] = useState([]); + const offeredSyncEngines = getSyncEngineIds(syncEngineConfigs); + + const selectedEngines = offeredSyncEngines.reduce((acc, syncEngId) => { + acc[syncEngId] = !declinedSyncEngines.includes(syncEngId); + return acc; + }, {} as Record); + + return { + offeredSyncEngines, + offeredSyncEngineConfigs: syncEngineConfigs, + declinedSyncEngines, + setDeclinedSyncEngines, + selectedEngines, + }; +}; diff --git a/packages/fxa-settings/src/models/Account.ts b/packages/fxa-settings/src/models/Account.ts index 16bd812ddd6..74f642c9daa 100644 --- a/packages/fxa-settings/src/models/Account.ts +++ b/packages/fxa-settings/src/models/Account.ts @@ -540,7 +540,7 @@ export class Account implements AccountData { } async createPassword(newPassword: string) { - const passwordCreated = await this.withLoadingStatus( + const passwordCreatedResult = await this.withLoadingStatus( this.authClient.createPassword( sessionToken()!, this.primaryEmail.email, @@ -552,7 +552,7 @@ export class Account implements AccountData { id: cache.identify({ __typename: 'Account' }), fields: { passwordCreated() { - return passwordCreated; + return passwordCreatedResult.passwordCreated; }, }, }); diff --git a/packages/fxa-settings/src/pages/InlineRecoveryKeySetup/container.test.tsx b/packages/fxa-settings/src/pages/InlineRecoveryKeySetup/container.test.tsx index 118f82c61c4..f34564af7b1 100644 --- a/packages/fxa-settings/src/pages/InlineRecoveryKeySetup/container.test.tsx +++ b/packages/fxa-settings/src/pages/InlineRecoveryKeySetup/container.test.tsx @@ -8,17 +8,19 @@ import InlineRecoveryKeySetupContainer from './container'; import * as InlineRecoveryKeySetupModule from '.'; import * as ModelsModule from '../../models'; import * as utils from 'fxa-react/lib/utils'; +import * as CacheModule from '../../lib/cache'; import AuthClient from 'fxa-auth-client/browser'; import { mockSensitiveDataClient as createMockSensitiveDataClient } from '../../models/mocks'; import { - MOCK_EMAIL, - MOCK_UID, MOCK_SESSION_TOKEN, MOCK_UNWRAP_BKEY, MOCK_AUTH_PW, + MOCK_STORED_ACCOUNT, } from '../../pages/mocks'; import { AUTH_DATA_KEY } from '../../lib/sensitive-data-client'; import { InlineRecoveryKeySetupProps } from './interfaces'; +import { MOCK_EMAIL } from '../InlineTotpSetup/mocks'; +import { LocationProvider } from '@reach/router'; jest.mock('../../models', () => ({ ...jest.requireActual('../../models'), @@ -59,34 +61,26 @@ function mockModelsModule() { }); } +// Call this when testing local storage +function mockCurrentAccount( + storedAccount = { + uid: '123', + sessionToken: MOCK_SESSION_TOKEN, + email: MOCK_EMAIL, + } +) { + jest.spyOn(CacheModule, 'currentAccount').mockReturnValue(storedAccount); + jest.spyOn(CacheModule, 'discardSessionToken'); +} + function applyDefaultMocks() { jest.resetAllMocks(); jest.restoreAllMocks(); mockModelsModule(); mockInlineRecoveryKeySetupModule(); - mockLocationState = { - email: MOCK_EMAIL, - uid: MOCK_UID, - sessionToken: MOCK_SESSION_TOKEN, - unwrapBKey: MOCK_UNWRAP_BKEY, - }; + mockCurrentAccount(MOCK_STORED_ACCOUNT); } -let mockLocationState = {}; -const mockLocation = () => { - return { - pathname: '/inline_recovery_key_setup', - state: mockLocationState, - }; -}; -jest.mock('@reach/router', () => { - return { - __esModule: true, - ...jest.requireActual('@reach/router'), - useLocation: () => mockLocation(), - }; -}); - let currentProps: InlineRecoveryKeySetupProps | undefined; function mockInlineRecoveryKeySetupModule() { currentProps = undefined; @@ -103,13 +97,22 @@ describe('InlineRecoveryKeySetupContainer', () => { applyDefaultMocks(); }); - it('navigates to CAD when location state values are missing', () => { + it('navigates to CAD when local storage values are missing', () => { let hardNavigateSpy: jest.SpyInstance; hardNavigateSpy = jest .spyOn(utils, 'hardNavigate') .mockImplementation(() => {}); - mockLocationState = {}; - render(); + const storedAccount = { + ...MOCK_STORED_ACCOUNT, + email: '', + }; + mockCurrentAccount(storedAccount); + + render( + + + + ); expect(hardNavigateSpy).toHaveBeenCalledWith( '/pair?showSuccessMessage=true' @@ -118,13 +121,21 @@ describe('InlineRecoveryKeySetupContainer', () => { }); it('gets data from sensitive data client, renders component', async () => { - render(); + render( + + + + ); expect(mockSensitiveDataClient.getData).toHaveBeenCalledWith(AUTH_DATA_KEY); expect(InlineRecoveryKeySetupModule.default).toBeCalled(); }); it('createRecoveryKey calls expected authClient methods', async () => { - render(); + render( + + + + ); expect(currentProps).toBeDefined(); await currentProps?.createRecoveryKeyHandler(); @@ -139,7 +150,11 @@ describe('InlineRecoveryKeySetupContainer', () => { }); it('updateRecoveryHint calls authClient', async () => { - render(); + render( + + + + ); expect(currentProps).toBeDefined(); await currentProps?.updateRecoveryHintHandler('take the hint'); diff --git a/packages/fxa-settings/src/pages/InlineRecoveryKeySetup/container.tsx b/packages/fxa-settings/src/pages/InlineRecoveryKeySetup/container.tsx index 5d46875be60..bfa7d135337 100644 --- a/packages/fxa-settings/src/pages/InlineRecoveryKeySetup/container.tsx +++ b/packages/fxa-settings/src/pages/InlineRecoveryKeySetup/container.tsx @@ -10,8 +10,7 @@ import { } from '../../models'; import { RouteComponentProps, useLocation } from '@reach/router'; import InlineRecoveryKeySetup from '.'; -import { SigninLocationState } from '../Signin/interfaces'; -import { cache } from '../../lib/cache'; +import { cache, currentAccount } from '../../lib/cache'; import { generateRecoveryKey } from 'fxa-auth-client/browser'; import { CreateRecoveryKeyHandler } from './interfaces'; import { AUTH_DATA_KEY } from '../../lib/sensitive-data-client'; @@ -25,10 +24,11 @@ export const InlineRecoveryKeySetupContainer = (_: RouteComponentProps) => { const ftlMsgResolver = useFtlMsgResolver(); const authClient = useAuthClient(); - const location = useLocation() as ReturnType & { - state?: SigninLocationState; - }; - const { email, uid, sessionToken } = location.state || {}; + const location = useLocation(); + const storedLocalAccount = currentAccount(); + const email = storedLocalAccount?.email; + const sessionToken = storedLocalAccount?.sessionToken; + const uid = storedLocalAccount?.uid; const sensitiveDataClient = useSensitiveDataClient(); const sensitiveData = sensitiveDataClient.getData(AUTH_DATA_KEY); @@ -111,14 +111,7 @@ export const InlineRecoveryKeySetupContainer = (_: RouteComponentProps) => { [authClient] ); - if ( - !uid || - !sessionToken || - !unwrapBKey || - !email || - !emailForAuth || - !authPW - ) { + if (!uid || !sessionToken || !unwrapBKey || !email || !authPW) { // go to CAD with success messaging, we do not want to re-prompt for password const { to } = getSyncNavigate(location.search); hardNavigate(to); @@ -130,7 +123,9 @@ export const InlineRecoveryKeySetupContainer = (_: RouteComponentProps) => { uid, sessionToken, unwrapBKey, - emailForAuth, + // Fallback to email if emailForAuth isn't set, this might happen + // from the third party auth set password page + emailForAuth || email, authPW ); const updateRecoveryHintHandler = updateRecoveryHint(sessionToken); diff --git a/packages/fxa-settings/src/pages/PostVerify/SetPassword/container.tsx b/packages/fxa-settings/src/pages/PostVerify/SetPassword/container.tsx new file mode 100644 index 00000000000..0cb89545059 --- /dev/null +++ b/packages/fxa-settings/src/pages/PostVerify/SetPassword/container.tsx @@ -0,0 +1,171 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { RouteComponentProps } from '@reach/router'; +import SetPassword from '.'; +import { currentAccount } from '../../../lib/cache'; +import LoadingSpinner from 'fxa-react/components/LoadingSpinner'; +import { useNavigateWithQuery as useNavigate } from '../../../lib/hooks/useNavigateWithQuery'; +import { isOAuthNativeIntegration, useAuthClient } from '../../../models'; +import { cache } from '../../../lib/cache'; +import { useCallback } from 'react'; +import { CreatePasswordHandler, SetPasswordIntegration } from './interfaces'; +import { HandledError } from '../../../lib/error-utils'; +import { + AuthUiErrorNos, + AuthUiErrors, +} from '../../../lib/auth-errors/auth-errors'; +import useSyncEngines from '../../../lib/hooks/useSyncEngines'; +import firefox from '../../../lib/channels/firefox'; +import { useFinishOAuthFlowHandler } from '../../../lib/oauth/hooks'; +import OAuthDataError from '../../../components/OAuthDataError'; + +const SetPasswordContainer = ({ + integration, +}: { integration: SetPasswordIntegration } & RouteComponentProps) => { + const navigate = useNavigate(); + const authClient = useAuthClient(); + const storedLocalAccount = currentAccount(); + const email = storedLocalAccount?.email; + const sessionToken = storedLocalAccount?.sessionToken; + const uid = storedLocalAccount?.uid; + + const { + offeredSyncEngines, + offeredSyncEngineConfigs, + declinedSyncEngines, + setDeclinedSyncEngines, + // TODO some metrics on this page? + // selectedEngines, + } = useSyncEngines(integration); + const isOAuthNative = isOAuthNativeIntegration(integration); + + const { finishOAuthFlowHandler, oAuthDataError } = useFinishOAuthFlowHandler( + authClient, + integration + ); + + const createPassword = useCallback( + (uid: string, email: string, sessionToken: string): CreatePasswordHandler => + async (newPassword: string) => { + try { + const { passwordCreated, authPW, unwrapBKey } = + await authClient.createPassword(sessionToken, email, newPassword); + cache.modify({ + id: cache.identify({ __typename: 'Account' }), + fields: { + passwordCreated() { + return passwordCreated; + }, + }, + }); + + let keyFetchToken; + if (!isOAuthNative) { + // We must reauth for another `keyFetchToken` because it was used in + // the oauth flow + const reauth = await authClient.sessionReauthWithAuthPW( + sessionToken, + email, + authPW, + { + keys: true, + reason: 'signin', + } + ); + // TODO, handle this better? Should we display a message to the user saying + // their password was created but they need to sign in again? + if (!reauth.keyFetchToken) throw new Error('Invalid keyFetchToken'); + keyFetchToken = reauth.keyFetchToken; + } + + const syncEngines = { + offeredEngines: offeredSyncEngines, + declinedEngines: declinedSyncEngines, + }; + + // TODO metrics + // GleanMetrics.registration.cwts({ sync: { cwts: syncOptions } }); + + firefox.fxaLogin({ + email, + // Do not send these values if OAuth. Mobile doesn't care about this message, and + // sending these values can cause intermittent sync disconnect issues in oauth desktop. + ...(!isOAuthNative && { + keyFetchToken, + unwrapBKey, + }), + sessionToken, + uid, + verified: true, + services: { + sync: syncEngines, + }, + }); + + if (isOAuthNative) { + const { error, redirect, code, state } = + await finishOAuthFlowHandler( + uid, + sessionToken, + keyFetchToken, + unwrapBKey + ); + + // TODO, handle this better? Should we display a message to the user saying + // their password was created but they need to sign in again? + if (error) { + return { error }; + } + + firefox.fxaOAuthLogin({ + action: 'signin', + code, + redirect, + state, + }); + } + return { error: null }; + } catch (error) { + const { errno } = error as HandledError; + if (errno && AuthUiErrorNos[errno]) { + return { error }; + } + return { error: AuthUiErrors.UNEXPECTED_ERROR as HandledError }; + } + }, + [ + authClient, + declinedSyncEngines, + isOAuthNative, + finishOAuthFlowHandler, + offeredSyncEngines, + ] + ); + + // Users must be already authenticated on this page. + // This page is currently always for the Sync flow. + if (!email || !sessionToken || !uid || !integration.isSync()) { + navigate('/signin', { replace: true }); + return ; + } + if (oAuthDataError) { + return ; + } + // Curry already checked values + const createPasswordHandler = createPassword(uid, email, sessionToken); + + return ( + + ); +}; + +export default SetPasswordContainer; diff --git a/packages/fxa-settings/src/pages/PostVerify/SetPassword/en.ftl b/packages/fxa-settings/src/pages/PostVerify/SetPassword/en.ftl new file mode 100644 index 00000000000..405f9c0f524 --- /dev/null +++ b/packages/fxa-settings/src/pages/PostVerify/SetPassword/en.ftl @@ -0,0 +1,6 @@ +## SetPassword page +## Third party auth users that do not have a password set yet are prompted for a +## password to complete their sign-in when they want to login to a service requiring it. + +set-password-heading = Create password +set-password-info = Your sync data is encrypted with your password to protect your privacy. diff --git a/packages/fxa-settings/src/pages/PostVerify/SetPassword/index.stories.tsx b/packages/fxa-settings/src/pages/PostVerify/SetPassword/index.stories.tsx new file mode 100644 index 00000000000..3cf59de93d5 --- /dev/null +++ b/packages/fxa-settings/src/pages/PostVerify/SetPassword/index.stories.tsx @@ -0,0 +1,25 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import React from 'react'; +import { withLocalization } from 'fxa-react/lib/storybooks'; +import SetPassword from '.'; +import { Meta } from '@storybook/react'; +import { SetPasswordProps } from './interfaces'; +import { Subject } from './mocks'; + +export default { + title: 'Pages/PostVerify/SetPassword', + component: SetPassword, + decorators: [withLocalization], +} as Meta; + +const storyWithProps = ({ + ...props // overrides +}: Partial = {}) => { + const story = () => ; + return story; +}; + +export const Default = storyWithProps(); diff --git a/packages/fxa-settings/src/pages/PostVerify/SetPassword/index.test.tsx b/packages/fxa-settings/src/pages/PostVerify/SetPassword/index.test.tsx new file mode 100644 index 00000000000..40b67e6b067 --- /dev/null +++ b/packages/fxa-settings/src/pages/PostVerify/SetPassword/index.test.tsx @@ -0,0 +1,21 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { renderWithLocalizationProvider } from 'fxa-react/lib/test-utils/localizationProvider'; +import { Subject } from './mocks'; +import { screen } from '@testing-library/react'; +import { MOCK_EMAIL } from '../../mocks'; + +describe('SetPassword page', () => { + it('renders as expected', () => { + renderWithLocalizationProvider(); + + screen.getByRole('heading', { name: 'Set your password' }); + screen.getByText(MOCK_EMAIL); + screen.getByText( + 'Please create a password to continue to Firefox Sync. Your data is encrypted with your password to protect your privacy.' + ); + screen.getByText('Choose what to sync'); + }); +}); diff --git a/packages/fxa-settings/src/pages/PostVerify/SetPassword/index.tsx b/packages/fxa-settings/src/pages/PostVerify/SetPassword/index.tsx new file mode 100644 index 00000000000..e59c5488225 --- /dev/null +++ b/packages/fxa-settings/src/pages/PostVerify/SetPassword/index.tsx @@ -0,0 +1,113 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import React from 'react'; +import { FtlMsg } from 'fxa-react/lib/utils'; +import AppLayout from '../../../components/AppLayout'; +import { FormSetupAccount } from '../../../components/FormSetupAccount'; +import { SetPasswordFormData, SetPasswordProps } from './interfaces'; +import { useForm } from 'react-hook-form'; +import { useCallback, useState } from 'react'; +import { useFtlMsgResolver } from '../../../models'; +import { getLocalizedErrorMessage } from '../../../lib/error-utils'; +import Banner from '../../../components/Banner'; +import { useLocation, useNavigate } from '@reach/router'; +import { getSyncNavigate } from '../../Signin/utils'; + +export const SetPassword = ({ + email, + createPasswordHandler, + offeredSyncEngineConfigs, + setDeclinedSyncEngines, +}: SetPasswordProps) => { + const ftlMsgResolver = useFtlMsgResolver(); + const location = useLocation(); + const navigate = useNavigate(); + const [createPasswordLoading, setCreatePasswordLoading] = + useState(false); + const [bannerErrorText, setBannerErrorText] = useState(''); + + const onSubmit = useCallback( + async ({ newPassword }: SetPasswordFormData) => { + setCreatePasswordLoading(true); + setBannerErrorText(''); + + const { error } = await createPasswordHandler(newPassword); + + if (error) { + const localizedErrorMessage = getLocalizedErrorMessage( + ftlMsgResolver, + error + ); + setBannerErrorText(localizedErrorMessage); + // if the request errored, loading state must be marked as false to reenable submission + setCreatePasswordLoading(false); + return; + } + + // navigate to inline recovery key setup + const { to } = getSyncNavigate(location.search, true); + navigate(to); + }, + [createPasswordHandler, ftlMsgResolver, location.search, navigate] + ); + + const { handleSubmit, register, getValues, errors, formState, trigger } = + useForm({ + mode: 'onChange', + criteriaMode: 'all', + defaultValues: { + email, + newPassword: '', + confirmPassword: '', + }, + }); + + return ( + + +

Create password

+
+

{email}

+ + {bannerErrorText && ( + + )} + + +

+ Your sync data is encrypted with your password to protect your + privacy. +

+
+ + { + /* TODO metrics */ + }} + onFocus={() => { + /* TODO stuff for metrics */ + }} + loading={createPasswordLoading} + onSubmit={handleSubmit(onSubmit)} + // This page is only shown during the Sync flow + isSync={true} + isDesktopRelay={false} + /> +
+ ); +}; + +export default SetPassword; diff --git a/packages/fxa-settings/src/pages/PostVerify/SetPassword/interfaces.ts b/packages/fxa-settings/src/pages/PostVerify/SetPassword/interfaces.ts new file mode 100644 index 00000000000..bd2a3adf7ac --- /dev/null +++ b/packages/fxa-settings/src/pages/PostVerify/SetPassword/interfaces.ts @@ -0,0 +1,33 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { syncEngineConfigs } from '../../../components/ChooseWhatToSync/sync-engines'; +import { HandledError } from '../../../lib/error-utils'; +import { Integration } from '../../../models'; + +export type SetPasswordIntegration = Pick< + Integration, + 'type' | 'isSync' | 'wantsKeys' | 'data' | 'clientInfo' +>; + +export interface SetPasswordFormData { + email: string; + newPassword: string; + confirmPassword: string; +} + +export interface CreatePasswordHandlerError { + error: HandledError | null; +} + +export type CreatePasswordHandler = ( + newPassword: string +) => Promise; + +export interface SetPasswordProps { + email: string; + createPasswordHandler: CreatePasswordHandler; + offeredSyncEngineConfigs?: typeof syncEngineConfigs; + setDeclinedSyncEngines: React.Dispatch>; +} diff --git a/packages/fxa-settings/src/pages/PostVerify/SetPassword/mocks.tsx b/packages/fxa-settings/src/pages/PostVerify/SetPassword/mocks.tsx new file mode 100644 index 00000000000..b42e4af0745 --- /dev/null +++ b/packages/fxa-settings/src/pages/PostVerify/SetPassword/mocks.tsx @@ -0,0 +1,42 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import React from 'react'; +import SetPassword from '.'; +import { LocationProvider } from '@reach/router'; +import { CreatePasswordHandler, SetPasswordIntegration } from './interfaces'; +import { MOCK_EMAIL } from '../../mocks'; +import { IntegrationType } from '../../../models'; +import { useMockSyncEngines } from '../../../lib/hooks/useSyncEngines/mocks'; + +export const createMockSetPasswordIntegration = (): SetPasswordIntegration => ({ + type: IntegrationType.OAuthNative, + isSync: () => true, + wantsKeys: () => true, + data: {}, + clientInfo: undefined, +}); + +export const Subject = ({ + email = MOCK_EMAIL, + createPasswordHandler = () => Promise.resolve({ error: null }), +}: { + email?: string; + createPasswordHandler?: CreatePasswordHandler; +}) => { + const { offeredSyncEngineConfigs, setDeclinedSyncEngines } = + useMockSyncEngines(); + return ( + + + + ); +}; diff --git a/packages/fxa-settings/src/pages/Signin/utils.ts b/packages/fxa-settings/src/pages/Signin/utils.ts index b5e187c3a7a..95d3908d1db 100644 --- a/packages/fxa-settings/src/pages/Signin/utils.ts +++ b/packages/fxa-settings/src/pages/Signin/utils.ts @@ -235,7 +235,6 @@ const getNonOAuthNavigationTarget = async ( if (integration.isSync()) { return { ...getSyncNavigate(queryParams, showInlineRecoveryKeySetup), - locationState: createSigninLocationState(navigationOptions), }; } if (redirectTo) { diff --git a/packages/fxa-settings/src/pages/Signup/container.tsx b/packages/fxa-settings/src/pages/Signup/container.tsx index d2e8e407acc..f0388e1a37f 100644 --- a/packages/fxa-settings/src/pages/Signup/container.tsx +++ b/packages/fxa-settings/src/pages/Signup/container.tsx @@ -4,12 +4,7 @@ import { RouteComponentProps, useLocation } from '@reach/router'; import { useNavigateWithQuery as useNavigate } from '../../lib/hooks/useNavigateWithQuery'; -import { - isOAuthIntegration, - isSyncDesktopV3Integration, - useAuthClient, - useConfig, -} from '../../models'; +import { useAuthClient, useConfig } from '../../models'; import { Signup } from '.'; import { useValidatedQueryParams } from '../../lib/hooks/useValidate'; import { SignupQueryParams } from '../../models/pages/signup'; @@ -29,8 +24,6 @@ import { getKeysV2, } from 'fxa-auth-client/lib/crypto'; import { LoadingSpinner } from 'fxa-react/components/LoadingSpinner'; -import { firefox } from '../../lib/channels/firefox'; -import { Constants } from '../../lib/constants'; import { createSaltV2 } from 'fxa-auth-client/lib/salt'; import { KeyStretchExperiment } from '../../models/experiments/key-stretch-experiment'; import { handleGQLError } from './utils'; @@ -38,6 +31,7 @@ import VerificationMethods from '../../constants/verification-methods'; import { queryParamsToMetricsContext } from '../../lib/metrics'; import { QueryParams } from '../..'; import { isFirefoxService } from '../../models/integrations/utils'; +import useSyncEngines from '../../lib/hooks/useSyncEngines'; /* * In content-server, the `email` param is optional. If it's provided, we @@ -87,16 +81,14 @@ const SignupContainer = ({ // Since we may perform an async call on initial render that can affect what is rendered, // return a spinner on first render. const [showLoadingSpinner, setShowLoadingSpinner] = useState(true); - const [webChannelEngines, setWebChannelEngines] = useState< - string[] | undefined - >(); - - const isOAuth = isOAuthIntegration(integration); - const isSyncOAuth = isOAuth && integration.isSync(); - const isSyncDesktopV3 = isSyncDesktopV3Integration(integration); - const isSync = integration.isSync(); const wantsKeys = integration.wantsKeys(); + // TODO: in PostVerify/SetPassword we call this and handle web channel messaging + // in the container compoment, but here we handle web channel messaging in the + // presentation component and we should be consistent. Calling this here allows for + // some easier mocking, especially until we can upgrade to Storybook 8. + const useSyncEnginesResult = useSyncEngines(integration); + useEffect(() => { (async () => { // Modify this once index is converted to React @@ -131,37 +123,6 @@ const SignupContainer = ({ })(); }); - useEffect(() => { - // This sends a web channel message to the browser to prompt a response - // that we listen for. - // TODO: In content-server, we send this on app-start for all integration types. - // Do we want to move this somewhere else once the index page is Reactified? - if (isSync) { - (async () => { - const status = await firefox.fxaStatus({ - // TODO: Improve getting 'context', probably set this on the integration - context: isSyncDesktopV3 - ? Constants.FX_DESKTOP_V3_CONTEXT - : Constants.OAUTH_CONTEXT, - isPairing: false, - service: Constants.SYNC_SERVICE, - }); - if (!webChannelEngines && status.capabilities.engines) { - // choose_what_to_sync may be disabled for mobile sync, see: - // https://github.com/mozilla/application-services/issues/1761 - // Desktop OAuth Sync will always provide this capability too - // for consistency. - if ( - isSyncDesktopV3 || - (isSyncOAuth && status.capabilities.choose_what_to_sync) - ) { - setWebChannelEngines(status.capabilities.engines); - } - } - })(); - } - }, [isSync, isSyncDesktopV3, isSyncOAuth, webChannelEngines]); - const [beginSignup] = useMutation(BEGIN_SIGNUP_MUTATION); const beginSignupHandler: BeginSignupHandler = useCallback( @@ -273,7 +234,7 @@ const SignupContainer = ({ integration, queryParamModel, beginSignupHandler, - webChannelEngines, + useSyncEnginesResult, }} /> ); diff --git a/packages/fxa-settings/src/pages/Signup/index.stories.tsx b/packages/fxa-settings/src/pages/Signup/index.stories.tsx index 1a1f1fa6d66..43a643f072d 100644 --- a/packages/fxa-settings/src/pages/Signup/index.stories.tsx +++ b/packages/fxa-settings/src/pages/Signup/index.stories.tsx @@ -21,8 +21,8 @@ import { MONITOR_CLIENTIDS, POCKET_CLIENTIDS, } from '../../models/integrations/client-matching'; -import { getSyncEngineIds } from '../../components/ChooseWhatToSync/sync-engines'; import { AppContext } from '../../models'; +import { useMockSyncEngines } from '../../lib/hooks/useSyncEngines/mocks'; export default { title: 'Pages/Signup', @@ -33,10 +33,14 @@ export default { const urlQueryData = mockUrlQueryData(signupQueryParams); const queryParamModel = new SignupQueryParams(urlQueryData); -const storyWithProps = ( - integration: SignupIntegration = createMockSignupOAuthWebIntegration() -) => { - const story = () => ( +const StoryWithProps = ({ + integration = createMockSignupOAuthWebIntegration(), +}: { + integration?: SignupIntegration; +}) => { + const useSyncEnginesResult = useMockSyncEngines(); + + return ( ); - return story; }; -export const Default = storyWithProps(); - -export const CantChangeEmail = storyWithProps(); - -export const ClientIsPocket = storyWithProps( - createMockSignupOAuthWebIntegration(POCKET_CLIENTIDS[0]) +export const Default = () => ; +export const CantChangeEmail = () => ; +export const ClientIsPocket = () => ( + ); - -export const ClientIsMonitor = storyWithProps( - createMockSignupOAuthWebIntegration(MONITOR_CLIENTIDS[0]) +export const ClientIsMonitor = () => ( + ); - -export const SyncDesktopV3 = storyWithProps( - createMockSignupSyncDesktopV3Integration() +export const SyncDesktopV3 = () => ( + ); - -export const SyncOAuth = storyWithProps( - createMockSignupOAuthNativeIntegration() +export const SyncOAuth = () => ( + ); - -export const OAuthDestkopServiceRelay = storyWithProps( - createMockSignupOAuthNativeIntegration('relay', false) +export const OAuthDesktopServiceRelay = () => ( + ); diff --git a/packages/fxa-settings/src/pages/Signup/index.tsx b/packages/fxa-settings/src/pages/Signup/index.tsx index 8703716b866..876d050e814 100644 --- a/packages/fxa-settings/src/pages/Signup/index.tsx +++ b/packages/fxa-settings/src/pages/Signup/index.tsx @@ -4,25 +4,12 @@ import React from 'react'; import { useLocation } from '@reach/router'; -import LinkExternal from 'fxa-react/components/LinkExternal'; -import LoadingSpinner from 'fxa-react/components/LoadingSpinner'; import { FtlMsg, hardNavigate } from 'fxa-react/lib/utils'; import { isEmailMask } from 'fxa-shared/email/helpers'; import { useCallback, useEffect, useState } from 'react'; import { useForm } from 'react-hook-form'; import AppLayout from '../../components/AppLayout'; import CardHeader from '../../components/CardHeader'; -import ChooseNewsletters from '../../components/ChooseNewsletters'; -import { newsletters } from '../../components/ChooseNewsletters/newsletters'; -import ChooseWhatToSync from '../../components/ChooseWhatToSync'; -import { - defaultDesktopV3SyncEngineConfigs, - getSyncEngineIds, - syncEngineConfigs, - webChannelDesktopV3EngineConfigs, -} from '../../components/ChooseWhatToSync/sync-engines'; -import FormPasswordWithBalloons from '../../components/FormPasswordWithBalloons'; -import InputText from '../../components/InputText'; import TermsPrivacyAgreement from '../../components/TermsPrivacyAgreement'; import ThirdPartyAuth from '../../components/ThirdPartyAuth'; import { REACT_ENTRYPOINT } from '../../constants'; @@ -41,7 +28,6 @@ import { MozServices } from '../../lib/types'; import { isOAuthIntegration, isOAuthNativeIntegrationSync, - isSyncDesktopV3Integration, useFtlMsgResolver, useSensitiveDataClient, } from '../../models'; @@ -52,6 +38,7 @@ import { import { SignupFormData, SignupProps } from './interfaces'; import Banner from '../../components/Banner'; import { AUTH_DATA_KEY } from '../../lib/sensitive-data-client'; +import { FormSetupAccount } from '../../components/FormSetupAccount'; export const viewName = 'signup'; @@ -59,7 +46,13 @@ export const Signup = ({ integration, queryParamModel, beginSignupHandler, - webChannelEngines, + useSyncEnginesResult: { + offeredSyncEngines, + offeredSyncEngineConfigs, + declinedSyncEngines, + setDeclinedSyncEngines, + selectedEngines, + }, }: SignupProps) => { const sensitiveDataClient = useSensitiveDataClient(); usePageViewEvent(viewName, REACT_ENTRYPOINT); @@ -70,7 +63,6 @@ export const Signup = ({ const isOAuth = isOAuthIntegration(integration); const isSyncOAuth = isOAuthNativeIntegrationSync(integration); - const isSyncDesktopV3 = isSyncDesktopV3Integration(integration); const isSync = integration.isSync(); const isDesktopRelay = integration.isDesktopRelay(); const email = queryParamModel.email; @@ -90,7 +82,7 @@ export const Signup = ({ ] = useState(false); const navigate = useNavigate(); const location = useLocation(); - const [declinedSyncEngines, setDeclinedSyncEngines] = useState([]); + // no newsletters are selected by default const [selectedNewsletterSlugs, setSelectedNewsletterSlugs] = useState< string[] @@ -111,40 +103,6 @@ export const Signup = ({ } }, [integration, isOAuth]); - const [offeredSyncEngineConfigs, setOfferedSyncEngineConfigs] = useState< - typeof syncEngineConfigs | undefined - >(); - - useEffect(() => { - if (webChannelEngines) { - if (isSyncDesktopV3) { - // Desktop v3 web channel message sends additional engines - setOfferedSyncEngineConfigs([ - ...defaultDesktopV3SyncEngineConfigs, - ...webChannelDesktopV3EngineConfigs.filter((engine) => - webChannelEngines.includes(engine.id) - ), - ]); - } else if (isSyncOAuth) { - // OAuth Webchannel context sends all engines - setOfferedSyncEngineConfigs( - syncEngineConfigs.filter((engine) => - webChannelEngines.includes(engine.id) - ) - ); - } - } - }, [isSyncDesktopV3, isSyncOAuth, webChannelEngines]); - - useEffect(() => { - if (offeredSyncEngineConfigs) { - const defaultDeclinedSyncEngines = offeredSyncEngineConfigs - .filter((engineConfig) => !engineConfig.defaultChecked) - .map((engineConfig) => engineConfig.id); - setDeclinedSyncEngines(defaultDeclinedSyncEngines); - } - }, [offeredSyncEngineConfigs, setDeclinedSyncEngines]); - const { handleSubmit, register, getValues, errors, formState, trigger } = useForm({ mode: 'onChange', @@ -253,22 +211,15 @@ export const Signup = ({ unwrapBKey: data.unwrapBKey, }); - const getOfferedSyncEngines = () => - getSyncEngineIds(offeredSyncEngineConfigs || []); - if (isSync) { const syncEngines = { - offeredEngines: getOfferedSyncEngines(), + offeredEngines: offeredSyncEngines, declinedEngines: declinedSyncEngines, }; - const syncOptions = syncEngines.offeredEngines.reduce( - (acc, syncEngId) => { - acc[syncEngId] = !declinedSyncEngines.includes(syncEngId); - return acc; - }, - {} as Record - ); - GleanMetrics.registration.cwts({ sync: { cwts: syncOptions } }); + GleanMetrics.registration.cwts({ + sync: { cwts: selectedEngines }, + }); + firefox.fxaLogin({ email, // Do not send these values if OAuth. Mobile doesn't care about this message, and @@ -313,9 +264,11 @@ export const Signup = ({ origin: 'signup', selectedNewsletterSlugs, // Sync desktop v3 sends a web channel message up on Signup - // while OAuth Sync does on confirm signup + // while OAuth Sync (mobile) does on confirm signup. + // Once mobile clients read this from fxaLogin to match + // oauth desktop, we can stop sending this on confirm signup code. ...(isSyncOAuth && { - offeredSyncEngines: getOfferedSyncEngines(), + offeredSyncEngines, declinedSyncEngines, }), }, @@ -340,7 +293,8 @@ export const Signup = ({ declinedSyncEngines, email, isSync, - offeredSyncEngineConfigs, + offeredSyncEngines, + selectedEngines, isSyncOAuth, localizedValidAgeError, isDesktopRelay, @@ -349,28 +303,6 @@ export const Signup = ({ ] ); - const showCWTS = () => { - if (isSync) { - if (offeredSyncEngineConfigs) { - return ( - - ); - } else { - // Waiting to receive webchannel message from browser - return ; - } - } else { - // Display nothing if Sync flow that does not support webchannels - // or if CWTS is disabled - return <>; - } - }; - return ( // TODO: FXA-8268, if force_auth && AuthErrors.is(error, 'DELETED_ACCOUNT'): // - forceMessage('Account no longer exists. Recreate it?') @@ -454,7 +386,7 @@ export const Signup = ({ - - {/* TODO: original component had a SR-only label that is not straightforward to implement with existing InputText component - SR-only text: "How old are you? To learn why we ask for your age, follow the “why do we ask” link below. */} - - { - // clear error tooltip if user types in the field - if (ageCheckErrorText) { - setAgeCheckErrorText(''); - } - }} - inputRef={register({ - pattern: /^[0-9]*$/, - maxLength: 3, - required: true, - })} - onFocusCb={onFocusAgeInput} - onBlurCb={onBlurAgeInput} - errorText={ageCheckErrorText} - tooltipPosition="bottom" - anchorPosition="end" - prefixDataTestId="age" - /> - - - GleanMetrics.registration.whyWeAsk()} - > - Why do we ask? - - - - {isSync - ? showCWTS() - : !isDesktopRelay && ( - - )} - + onSubmit={handleSubmit(onSubmit)} + /> {/* Third party auth is not currently supported for sync */} {!isSync && !isDesktopRelay && } diff --git a/packages/fxa-settings/src/pages/Signup/interfaces.ts b/packages/fxa-settings/src/pages/Signup/interfaces.ts index 955df92f394..5706d3f098e 100644 --- a/packages/fxa-settings/src/pages/Signup/interfaces.ts +++ b/packages/fxa-settings/src/pages/Signup/interfaces.ts @@ -3,6 +3,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import { HandledError } from '../../lib/error-utils'; +import useSyncEngines from '../../lib/hooks/useSyncEngines'; import { BaseIntegration, OAuthIntegration } from '../../models'; import { SignupQueryParams } from '../../models/pages/signup'; import { MetricsContext } from 'fxa-auth-client/browser'; @@ -42,7 +43,7 @@ export interface SignupProps { integration: SignupIntegration; queryParamModel: SignupQueryParams; beginSignupHandler: BeginSignupHandler; - webChannelEngines: string[] | undefined; + useSyncEnginesResult: ReturnType; } export type SignupIntegration = SignupOAuthIntegration | SignupBaseIntegration; diff --git a/packages/fxa-settings/src/pages/Signup/mocks.tsx b/packages/fxa-settings/src/pages/Signup/mocks.tsx index a9667566277..5ee63202551 100644 --- a/packages/fxa-settings/src/pages/Signup/mocks.tsx +++ b/packages/fxa-settings/src/pages/Signup/mocks.tsx @@ -24,7 +24,7 @@ import { SignupIntegration, SignupOAuthIntegration, } from './interfaces'; -import { getSyncEngineIds } from '../../components/ChooseWhatToSync/sync-engines'; +import { useMockSyncEngines } from '../../lib/hooks/useSyncEngines/mocks'; export const MOCK_SEARCH_PARAMS = { email: MOCK_EMAIL, @@ -139,6 +139,7 @@ export const Subject = ({ }) => { const urlQueryData = mockUrlQueryData(queryParams); const queryParamModel = new SignupQueryParams(urlQueryData); + const useMockSyncEnginesResult = useMockSyncEngines(); return (