From 05ed305371234e6674d3e84e81b1bb947b6dfcb8 Mon Sep 17 00:00:00 2001 From: Lauren Zugai Date: Tue, 19 Nov 2024 11:46:54 -0600 Subject: [PATCH] feat(react): Convert third party auth 'Set password' page to React Because: * There are data-sharing problems with this page currently being on Backbone while other pages in the flow are in React, causing a double sign-in for all 'Set password' flows, plus the inability to sign into Sync and maintain CWTS choices for desktop oauth * We want to move completely over from Backbone to React This commit: * Creates 'post_verify/third_party_auth/set_password' page in React with container component * Sends web channel messages up to Sync after password create success * Shares form logic with Signup, includes useSyncEngine hook for DRYness * Changes InlineRecoveryKeySetup to check local storage instead of location state, which prevents needing to prop drill as users should always be signed in and these values available in local storage on this page * Returns authPW and unwrapBKey from create password in auth-client closes FXA-6651 --- packages/fxa-auth-client/lib/client.ts | 22 ++- .../server/lib/routes/react-app/index.js | 2 +- .../fxa-settings/src/components/App/index.tsx | 6 + .../src/components/FormSetupAccount/en.ftl | 0 .../src/components/FormSetupAccount/index.tsx | 138 ++++++++++++++ .../components/FormSetupAccount/interfaces.ts | 33 ++++ .../src/lib/hooks/useSyncEngines/index.tsx | 118 ++++++++++++ .../src/lib/hooks/useSyncEngines/mocks.tsx | 27 +++ packages/fxa-settings/src/models/Account.ts | 4 +- .../InlineRecoveryKeySetup/container.test.tsx | 73 ++++--- .../InlineRecoveryKeySetup/container.tsx | 25 +-- .../PostVerify/SetPassword/container.tsx | 171 +++++++++++++++++ .../src/pages/PostVerify/SetPassword/en.ftl | 6 + .../PostVerify/SetPassword/index.stories.tsx | 25 +++ .../PostVerify/SetPassword/index.test.tsx | 21 ++ .../pages/PostVerify/SetPassword/index.tsx | 113 +++++++++++ .../PostVerify/SetPassword/interfaces.ts | 33 ++++ .../pages/PostVerify/SetPassword/mocks.tsx | 42 ++++ .../fxa-settings/src/pages/Signin/utils.ts | 1 - .../src/pages/Signup/container.tsx | 57 +----- .../src/pages/Signup/index.stories.tsx | 53 +++--- .../fxa-settings/src/pages/Signup/index.tsx | 179 ++++-------------- .../src/pages/Signup/interfaces.ts | 3 +- .../fxa-settings/src/pages/Signup/mocks.tsx | 5 +- 24 files changed, 884 insertions(+), 273 deletions(-) create mode 100644 packages/fxa-settings/src/components/FormSetupAccount/en.ftl create mode 100644 packages/fxa-settings/src/components/FormSetupAccount/index.tsx create mode 100644 packages/fxa-settings/src/components/FormSetupAccount/interfaces.ts create mode 100644 packages/fxa-settings/src/lib/hooks/useSyncEngines/index.tsx create mode 100644 packages/fxa-settings/src/lib/hooks/useSyncEngines/mocks.tsx create mode 100644 packages/fxa-settings/src/pages/PostVerify/SetPassword/container.tsx create mode 100644 packages/fxa-settings/src/pages/PostVerify/SetPassword/en.ftl create mode 100644 packages/fxa-settings/src/pages/PostVerify/SetPassword/index.stories.tsx create mode 100644 packages/fxa-settings/src/pages/PostVerify/SetPassword/index.test.tsx create mode 100644 packages/fxa-settings/src/pages/PostVerify/SetPassword/index.tsx create mode 100644 packages/fxa-settings/src/pages/PostVerify/SetPassword/interfaces.ts create mode 100644 packages/fxa-settings/src/pages/PostVerify/SetPassword/mocks.tsx 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 (