From 3e592b41ddbad1cd877a52c7277cbe8a1007e1e5 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 | 5 + .../FormPasswordWithBalloons/index.tsx | 3 + .../src/components/FormSetupAccount/index.tsx | 139 +++++++++ .../components/FormSetupAccount/interfaces.ts | 34 +++ packages/fxa-settings/src/lib/glean/index.ts | 9 + .../src/lib/hooks/useSyncEngines/index.tsx | 117 ++++++++ .../src/lib/hooks/useSyncEngines/mocks.tsx | 29 ++ .../fxa-settings/src/lib/storage-utils.ts | 7 + packages/fxa-settings/src/models/Account.ts | 4 +- .../InlineRecoveryKeySetup/container.test.tsx | 73 +++-- .../InlineRecoveryKeySetup/container.tsx | 12 +- .../PostVerify/SetPassword/container.test.tsx | 276 ++++++++++++++++++ .../PostVerify/SetPassword/container.tsx | 174 +++++++++++ .../src/pages/PostVerify/SetPassword/en.ftl | 6 + .../PostVerify/SetPassword/index.stories.tsx | 25 ++ .../PostVerify/SetPassword/index.test.tsx | 23 ++ .../pages/PostVerify/SetPassword/index.tsx | 100 +++++++ .../PostVerify/SetPassword/interfaces.ts | 27 ++ .../pages/PostVerify/SetPassword/mocks.tsx | 33 +++ .../ThirdPartyAuthCallback/index.tsx | 8 +- .../pages/Signin/SigninRecoveryCode/index.tsx | 7 +- .../pages/Signin/SigninTokenCode/index.tsx | 7 +- .../src/pages/Signin/SigninTotpCode/index.tsx | 7 +- .../src/pages/Signin/SigninUnblock/index.tsx | 7 +- .../fxa-settings/src/pages/Signin/index.tsx | 7 +- .../src/pages/Signin/interfaces.ts | 7 + .../fxa-settings/src/pages/Signin/utils.ts | 61 ++-- .../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 +- .../fxa-shared/metrics/glean/web/index.ts | 7 + 35 files changed, 1231 insertions(+), 304 deletions(-) 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.test.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..dc8d3d3507f 100644 --- a/packages/fxa-settings/src/components/App/index.tsx +++ b/packages/fxa-settings/src/components/App/index.tsx @@ -79,6 +79,7 @@ import SignupConfirmed from '../../pages/Signup/SignupConfirmed'; import WebChannelExample from '../../pages/WebChannelExample'; import SignoutSync from '../Settings/SignoutSync'; import InlineRecoveryKeySetupContainer from '../../pages/InlineRecoveryKeySetup/container'; +import SetPasswordContainer from '../../pages/PostVerify/SetPassword/container'; const Settings = lazy(() => import('../Settings')); @@ -318,6 +319,10 @@ const AuthAndAccountSetupRoutes = ({ path="/post_verify/third_party_auth/callback/*" {...{ flowQueryParams }} /> + {/* Reset password */} { @@ -75,6 +76,7 @@ export const FormPasswordWithBalloons = ({ loading, children, disableButtonUntilValid = false, + submitButtonGleanId, }: FormPasswordWithBalloonsProps) => { const passwordValidator = new PasswordValidator(email); const [passwordMatchErrorText, setPasswordMatchErrorText] = @@ -394,6 +396,7 @@ export const FormPasswordWithBalloons = ({ disabled={ loading || (!formState.isValid && disableButtonUntilValid) } + data-glean-id={submitButtonGleanId && submitButtonGleanId} > {templateValues.buttonText} diff --git a/packages/fxa-settings/src/components/FormSetupAccount/index.tsx b/packages/fxa-settings/src/components/FormSetupAccount/index.tsx new file mode 100644 index 00000000000..4e67483b29e --- /dev/null +++ b/packages/fxa-settings/src/components/FormSetupAccount/index.tsx @@ -0,0 +1,139 @@ +/* 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 FormPasswordWithBalloons from '../FormPasswordWithBalloons'; +import InputText from '../InputText'; +import LinkExternal from 'fxa-react/components/LinkExternal'; +import GleanMetrics from '../../lib/glean'; +import ChooseNewsletters from '../ChooseNewsletters'; +import ChooseWhatToSync from '../ChooseWhatToSync'; +import LoadingSpinner from 'fxa-react/components/LoadingSpinner'; +import { FormSetupAccountProps } from './interfaces'; +import { newsletters } from '../ChooseNewsletters/newsletters'; + +export const FormSetupAccount = ({ + formState, + errors, + trigger, + register, + getValues, + onFocus, + email, + onFocusMetricsEvent, + onSubmit, + loading, + isSync, + offeredSyncEngineConfigs, + setDeclinedSyncEngines, + isDesktopRelay, + setSelectedNewsletterSlugs, + ageCheckErrorText, + setAgeCheckErrorText, + onFocusAgeInput, + onBlurAgeInput, + submitButtonGleanId +}: FormSetupAccountProps) => { + 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..f8a073bbfa3 --- /dev/null +++ b/packages/fxa-settings/src/components/FormSetupAccount/interfaces.ts @@ -0,0 +1,34 @@ +/* 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; + submitButtonGleanId?: string; +}; diff --git a/packages/fxa-settings/src/lib/glean/index.ts b/packages/fxa-settings/src/lib/glean/index.ts index 010039c9145..478e2626012 100644 --- a/packages/fxa-settings/src/lib/glean/index.ts +++ b/packages/fxa-settings/src/lib/glean/index.ts @@ -31,6 +31,7 @@ import * as accountPref from 'fxa-shared/metrics/glean/web/accountPref'; import * as accountBanner from 'fxa-shared/metrics/glean/web/accountBanner'; import * as deleteAccount from 'fxa-shared/metrics/glean/web/deleteAccount'; import * as thirdPartyAuth from 'fxa-shared/metrics/glean/web/thirdPartyAuth'; +import * as thirdPartyAuthSetPassword from 'fxa-shared/metrics/glean/web/thirdPartyAuthSetPassword'; import { userIdSha256, userId } from 'fxa-shared/metrics/glean/web/account'; import { oauthClientId, @@ -182,6 +183,11 @@ const populateMetrics = async (gleanPingMetrics: GleanPingMetrics) => { } } + // Initial cwts values will be included not only in the cwtsEngage event, + // but also in subsequent events (sucha as page load events). This is because there was no suitable data type for an + // event's extra keys that worked for both string and event metrics. + // It should be noted that the user may change their sync settings after the initial cwtsEngage event + // but the new settings will not be reflected in the glean pings. if (gleanPingMetrics?.sync?.cwts) { Object.entries(gleanPingMetrics.sync.cwts).forEach(([k, v]) => { sync.cwts[k].set(v); @@ -517,6 +523,9 @@ const recordEventMetric = ( reason: gleanPingMetrics?.event?.['reason'] || '', }); break; + case 'third_party_auth_set_password_success': + thirdPartyAuthSetPassword.success.record(); + break; } }; 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..f5078c545e0 --- /dev/null +++ b/packages/fxa-settings/src/lib/hooks/useSyncEngines/index.tsx @@ -0,0 +1,117 @@ +/* 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; + +export function 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, + 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..dc69a951a95 --- /dev/null +++ b/packages/fxa-settings/src/lib/hooks/useSyncEngines/mocks.tsx @@ -0,0 +1,29 @@ +/* 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 function 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, + }; +} + +export default useMockSyncEngines; diff --git a/packages/fxa-settings/src/lib/storage-utils.ts b/packages/fxa-settings/src/lib/storage-utils.ts index 0c02403befe..c9d658d5fb1 100644 --- a/packages/fxa-settings/src/lib/storage-utils.ts +++ b/packages/fxa-settings/src/lib/storage-utils.ts @@ -101,3 +101,10 @@ export function storeAccountData(accountData: StoredAccountData) { setCurrentAccount(accountData.uid); sessionToken(accountData.sessionToken); // Can we remove this? It seems unnecessary... } + +export function getCurrentAccountData(): StoredAccountData { + const storage = localStorage(); + const uid = storage.get('currentAccountUid'); + let accounts = storage.get('accounts') || {}; + return accounts[uid]; +} diff --git a/packages/fxa-settings/src/models/Account.ts b/packages/fxa-settings/src/models/Account.ts index 8058edb3037..97b3b488635 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..3673b424f3c 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); diff --git a/packages/fxa-settings/src/pages/PostVerify/SetPassword/container.test.tsx b/packages/fxa-settings/src/pages/PostVerify/SetPassword/container.test.tsx new file mode 100644 index 00000000000..7fcfd842c5b --- /dev/null +++ b/packages/fxa-settings/src/pages/PostVerify/SetPassword/container.test.tsx @@ -0,0 +1,276 @@ +/* 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 * as ModelsModule from '../../../models'; +import * as CacheModule from '../../../lib/cache'; +import * as SetPasswordModule from '.'; + +import AuthClient from 'fxa-auth-client/browser'; +import { + MOCK_AUTH_PW, + MOCK_EMAIL, + MOCK_KEY_FETCH_TOKEN, + MOCK_OAUTH_FLOW_HANDLER_RESPONSE, + MOCK_PASSWORD, + MOCK_SESSION_TOKEN, + MOCK_STORED_ACCOUNT, + MOCK_UID, + MOCK_UNWRAP_BKEY, +} from '../../mocks'; +import { SetPasswordProps } from './interfaces'; +import { LocationProvider } from '@reach/router'; +import SetPasswordContainer from './container'; +import { renderWithLocalizationProvider } from 'fxa-react/lib/test-utils/localizationProvider'; +import { mockSensitiveDataClient as createMockSensitiveDataClient } from '../../../models/mocks'; +import { act } from '@testing-library/react'; +import { AUTH_DATA_KEY } from '../../../lib/sensitive-data-client'; +import { + getSyncEngineIds, + syncEngineConfigs, +} from '../../../components/ChooseWhatToSync/sync-engines'; +import { + useFinishOAuthFlowHandler, + useOAuthKeysCheck, +} from '../../../lib/oauth/hooks'; +import firefox from '../../../lib/channels/firefox'; +import GleanMetrics from '../../../lib/glean'; + +jest.mock('../../../models', () => ({ + ...jest.requireActual('../../../models'), + useAuthClient: jest.fn(), + useSensitiveDataClient: jest.fn(), +})); + +jest.mock('../../../lib/glean', () => ({ + __esModule: true, + default: { + thirdPartyAuthSetPassword: { + success: jest.fn(), + }, + }, +})); + +const mockAuthClient = new AuthClient('http://localhost:9000', { + keyStretchVersion: 1, +}); +jest.mock('../../../lib/oauth/hooks.tsx', () => { + return { + __esModule: true, + useFinishOAuthFlowHandler: jest.fn(), + useOAuthKeysCheck: jest.fn(), + }; +}); +jest.mock('../../../lib/hooks/useSyncEngines', () => { + const useMockSyncEngines = + require('../../../lib/hooks/useSyncEngines/mocks').default; + return { + __esModule: true, + default: useMockSyncEngines, + }; +}); + +const mockSensitiveDataClient = createMockSensitiveDataClient(); +mockSensitiveDataClient.setData = jest.fn(); + +const mockNavigate = jest.fn(); +jest.mock('@reach/router', () => ({ + ...jest.requireActual('@reach/router'), + navigate: jest.fn(), + useNavigate: () => mockNavigate, +})); +function mockModelsModule() { + mockAuthClient.createPassword = jest.fn().mockResolvedValue({ + passwordCreated: 123456, + authPW: MOCK_AUTH_PW, + unwrapBKey: MOCK_UNWRAP_BKEY, + }); + mockAuthClient.sessionReauthWithAuthPW = jest + .fn() + .mockResolvedValue({ keyFetchToken: MOCK_KEY_FETCH_TOKEN }); + (ModelsModule.useAuthClient as jest.Mock).mockImplementation( + () => mockAuthClient + ); + (ModelsModule.useSensitiveDataClient as jest.Mock).mockImplementation( + () => mockSensitiveDataClient + ); + (useOAuthKeysCheck as jest.Mock).mockImplementation(() => ({ + oAuthKeysCheckError: null, + })); +} +// Call this when testing local storage +function mockCurrentAccount( + storedAccount = { + uid: MOCK_UID, + sessionToken: MOCK_SESSION_TOKEN, + email: MOCK_EMAIL, + } +) { + jest.spyOn(CacheModule, 'currentAccount').mockReturnValue(storedAccount); +} + +let currentSetPasswordProps: SetPasswordProps | undefined; +function mockInlineRecoveryKeySetupModule() { + jest + .spyOn(SetPasswordModule, 'default') + .mockImplementation((props: SetPasswordProps) => { + currentSetPasswordProps = props; + return
set password mock
; + }); +} + +function applyDefaultMocks() { + jest.resetAllMocks(); + jest.restoreAllMocks(); + mockModelsModule(); + mockInlineRecoveryKeySetupModule(); + mockCurrentAccount(MOCK_STORED_ACCOUNT); + (useFinishOAuthFlowHandler as jest.Mock).mockImplementation(() => ({ + finishOAuthFlowHandler: jest + .fn() + .mockReturnValueOnce(MOCK_OAUTH_FLOW_HANDLER_RESPONSE), + oAuthDataError: null, + })); +} + +function render(integration = mockSyncDesktopV3Integration()) { + renderWithLocalizationProvider( + + + + ); +} +function mockSyncDesktopV3Integration() { + return { + type: ModelsModule.IntegrationType.SyncDesktopV3, + getService: () => 'sync', + getClientId: () => undefined, + isSync: () => true, + wantsKeys: () => true, + data: { service: 'sync' }, + isDesktopSync: () => true, + isDesktopRelay: () => false, + } as ModelsModule.Integration; +} +function mockOAuthNativeIntegration() { + return { + type: ModelsModule.IntegrationType.OAuthNative, + getService: () => 'sync', + getClientId: () => undefined, + isSync: () => true, + wantsKeys: () => true, + data: { service: 'sync' }, + isDesktopSync: () => true, + isDesktopRelay: () => false, + } as ModelsModule.Integration; +} + +describe('SetPassword container', () => { + const offeredEngines = getSyncEngineIds(syncEngineConfigs); + + beforeEach(() => { + applyDefaultMocks(); + }); + + it('navigates to signin when local storage values are missing', async () => { + const storedAccount = { + ...MOCK_STORED_ACCOUNT, + email: '', + }; + mockCurrentAccount(storedAccount); + + render(); + expect(mockNavigate).toHaveBeenCalledWith('/signin', { replace: true }); + expect(SetPasswordModule.default).not.toBeCalled(); + }); + + it('renders the component when local storage values are present', async () => { + render(); + expect(mockNavigate).not.toBeCalled(); + expect(SetPasswordModule.default).toBeCalled(); + expect(currentSetPasswordProps).toBeDefined(); + }); + + describe('calling createPassword', () => { + let fxaLoginSpy: jest.SpyInstance; + let fxaOAuthLoginSpy: jest.SpyInstance; + beforeEach(() => { + fxaLoginSpy = jest.spyOn(firefox, 'fxaLogin'); + fxaOAuthLoginSpy = jest.spyOn(firefox, 'fxaOAuthLogin'); + }); + + it('does the expected things with desktop v3', async () => { + render(); + + expect(currentSetPasswordProps?.createPasswordHandler).toBeDefined(); + await act(async () => { + await currentSetPasswordProps?.createPasswordHandler(MOCK_PASSWORD); + }); + expect(mockSensitiveDataClient.setData).toBeCalledWith(AUTH_DATA_KEY, { + authPW: MOCK_AUTH_PW, + emailForAuth: MOCK_EMAIL, + unwrapBKey: MOCK_UNWRAP_BKEY, + }); + expect(mockAuthClient.sessionReauthWithAuthPW).toBeCalledWith( + MOCK_SESSION_TOKEN, + MOCK_EMAIL, + MOCK_AUTH_PW, + { + keys: true, + reason: 'signin', + } + ); + expect(GleanMetrics.thirdPartyAuthSetPassword.success).toBeCalledWith({ + sync: { + cwts: Object.fromEntries( + offeredEngines.map((engine) => [engine, true]) + ), + }, + }); + expect(fxaLoginSpy).toBeCalledWith({ + email: MOCK_EMAIL, + sessionToken: MOCK_SESSION_TOKEN, + uid: MOCK_UID, + verified: true, + keyFetchToken: MOCK_KEY_FETCH_TOKEN, + unwrapBKey: MOCK_UNWRAP_BKEY, + services: { + sync: { + offeredEngines, + declinedEngines: [], + }, + }, + }); + expect(fxaOAuthLoginSpy).not.toBeCalled(); + }); + + it('does the expected things with oauth native', async () => { + render(mockOAuthNativeIntegration()); + + expect(currentSetPasswordProps?.createPasswordHandler).toBeDefined(); + await act(async () => { + await currentSetPasswordProps?.createPasswordHandler(MOCK_PASSWORD); + }); + expect(fxaLoginSpy).toBeCalledWith({ + email: MOCK_EMAIL, + sessionToken: MOCK_SESSION_TOKEN, + uid: MOCK_UID, + verified: true, + services: { + sync: { + offeredEngines, + declinedEngines: [], + }, + }, + }); + expect(firefox.fxaOAuthLogin).toBeCalledWith({ + action: 'signin', + ...MOCK_OAUTH_FLOW_HANDLER_RESPONSE, + }); + }); + }); +}); 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..4a62a4fba31 --- /dev/null +++ b/packages/fxa-settings/src/pages/PostVerify/SetPassword/container.tsx @@ -0,0 +1,174 @@ +/* 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, useLocation } 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 { + Integration, + useAuthClient, + useSensitiveDataClient, +} from '../../../models'; +import { cache } from '../../../lib/cache'; +import { useCallback } from 'react'; +import { CreatePasswordHandler } from './interfaces'; +import { HandledError } from '../../../lib/error-utils'; +import { + AuthUiErrorNos, + AuthUiErrors, +} from '../../../lib/auth-errors/auth-errors'; +import useSyncEngines from '../../../lib/hooks/useSyncEngines'; +import { useFinishOAuthFlowHandler } from '../../../lib/oauth/hooks'; +import OAuthDataError from '../../../components/OAuthDataError'; +import { AUTH_DATA_KEY } from '../../../lib/sensitive-data-client'; +import { NavigationOptions } from '../../Signin/interfaces'; +import { handleNavigation } from '../../Signin/utils'; +import GleanMetrics from '../../../lib/glean'; + +const SetPasswordContainer = ({ + integration, +}: { integration: Integration } & 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, + selectedEngines, + } = useSyncEngines(integration); + const sensitiveDataClient = useSensitiveDataClient(); + const location = useLocation(); + + const { finishOAuthFlowHandler, oAuthDataError } = useFinishOAuthFlowHandler( + authClient, + integration + ); + + const getKeyFetchToken = useCallback( + async (authPW: string, email: string, sessionToken: string) => { + // We must reauth for another `keyFetchToken` because it was used in + // the oauth flow + const { keyFetchToken } = await authClient.sessionReauthWithAuthPW( + sessionToken, + email, + authPW, + { + keys: true, + reason: 'signin', + } + ); + return keyFetchToken; + }, + [authClient] + ); + + 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; + }, + }, + }); + + sensitiveDataClient.setData(AUTH_DATA_KEY, { + // Store for inline recovery key flow + authPW, + emailForAuth: email, + unwrapBKey, + }); + + const keyFetchToken = await getKeyFetchToken( + authPW, + email, + sessionToken + ); + + GleanMetrics.thirdPartyAuthSetPassword.success({ + sync: { cwts: selectedEngines }, + }); + + const navigationOptions: NavigationOptions = { + email, + signinData: { + uid, + sessionToken, + verified: true, + keyFetchToken, + }, + unwrapBKey, + integration, + finishOAuthFlowHandler, + queryParams: location.search, + handleFxaLogin: true, + handleFxaOAuthLogin: true, + showInlineRecoveryKeySetup: true, + syncEngines: { + offeredEngines: offeredSyncEngines, + declinedEngines: declinedSyncEngines, + }, + }; + + const { error } = await handleNavigation(navigationOptions); + return { error }; + } catch (error) { + const { errno } = error as HandledError; + if (errno && AuthUiErrorNos[errno]) { + return { error }; + } + return { error: AuthUiErrors.UNEXPECTED_ERROR as HandledError }; + } + }, + [ + authClient, + declinedSyncEngines, + integration, + finishOAuthFlowHandler, + getKeyFetchToken, + offeredSyncEngines, + selectedEngines, + sensitiveDataClient, + location.search, + ] + ); + + // 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..e36979cf162 --- /dev/null +++ b/packages/fxa-settings/src/pages/PostVerify/SetPassword/index.test.tsx @@ -0,0 +1,23 @@ +/* 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, waitFor } from '@testing-library/react'; +import { MOCK_EMAIL } from '../../mocks'; + +describe('SetPassword page', () => { + it('renders as expected', async () => { + renderWithLocalizationProvider(); + + screen.getByRole('heading', { name: 'Create password' }); + screen.getByText(MOCK_EMAIL); + screen.getByText( + 'Your sync data is encrypted with your password to protect your privacy.' + ); + await waitFor(() => { + 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..b3909776b6a --- /dev/null +++ b/packages/fxa-settings/src/pages/PostVerify/SetPassword/index.tsx @@ -0,0 +1,100 @@ +/* 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'; + +export const SetPassword = ({ + email, + createPasswordHandler, + offeredSyncEngineConfigs, + setDeclinedSyncEngines, +}: SetPasswordProps) => { + const ftlMsgResolver = useFtlMsgResolver(); + 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; + } + }, + [createPasswordHandler, ftlMsgResolver] + ); + + 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. +

+
+ + +
+ ); +}; + +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..10594aeaf55 --- /dev/null +++ b/packages/fxa-settings/src/pages/PostVerify/SetPassword/interfaces.ts @@ -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 { syncEngineConfigs } from '../../../components/ChooseWhatToSync/sync-engines'; +import { HandledError } from '../../../lib/error-utils'; + +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..6c066d92fb9 --- /dev/null +++ b/packages/fxa-settings/src/pages/PostVerify/SetPassword/mocks.tsx @@ -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 React from 'react'; +import SetPassword from '.'; +import { LocationProvider } from '@reach/router'; +import { CreatePasswordHandler } from './interfaces'; +import { MOCK_EMAIL } from '../../mocks'; +import { useMockSyncEngines } from '../../../lib/hooks/useSyncEngines/mocks'; + +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/PostVerify/ThirdPartyAuthCallback/index.tsx b/packages/fxa-settings/src/pages/PostVerify/ThirdPartyAuthCallback/index.tsx index c383a2311f0..c7b7d7e2f9c 100644 --- a/packages/fxa-settings/src/pages/PostVerify/ThirdPartyAuthCallback/index.tsx +++ b/packages/fxa-settings/src/pages/PostVerify/ThirdPartyAuthCallback/index.tsx @@ -90,12 +90,12 @@ const ThirdPartyAuthCallback = ({ integration, finishOAuthFlowHandler, queryParams: location.search, - }; - - const { error: navError } = await handleNavigation(navigationOptions, { + isSignInWithThirdPartyAuth: true, handleFxaLogin: false, handleFxaOAuthLogin: false, - }); + }; + + const { error: navError } = await handleNavigation(navigationOptions); if (navError) { // TODO validate what should happen here diff --git a/packages/fxa-settings/src/pages/Signin/SigninRecoveryCode/index.tsx b/packages/fxa-settings/src/pages/Signin/SigninRecoveryCode/index.tsx index 0e5d3515475..92b4e9b7266 100644 --- a/packages/fxa-settings/src/pages/Signin/SigninRecoveryCode/index.tsx +++ b/packages/fxa-settings/src/pages/Signin/SigninRecoveryCode/index.tsx @@ -84,12 +84,11 @@ const SigninRecoveryCode = ({ finishOAuthFlowHandler, redirectTo, queryParams: location.search, - }; - - const { error } = await handleNavigation(navigationOptions, { handleFxaLogin: true, handleFxaOAuthLogin: true, - }); + }; + + const { error } = await handleNavigation(navigationOptions); if (error) { setBannerErrorMessage(getLocalizedErrorMessage(ftlMsgResolver, error)); } diff --git a/packages/fxa-settings/src/pages/Signin/SigninTokenCode/index.tsx b/packages/fxa-settings/src/pages/Signin/SigninTokenCode/index.tsx index c7341211439..b1b3ceba13a 100644 --- a/packages/fxa-settings/src/pages/Signin/SigninTokenCode/index.tsx +++ b/packages/fxa-settings/src/pages/Signin/SigninTokenCode/index.tsx @@ -149,14 +149,13 @@ const SigninTokenCode = ({ queryParams: location.search, redirectTo, showInlineRecoveryKeySetup, + handleFxaLogin: false, + handleFxaOAuthLogin: true, }; await GleanMetrics.isDone(); - const { error: navError } = await handleNavigation(navigationOptions, { - handleFxaLogin: false, - handleFxaOAuthLogin: true, - }); + const { error: navError } = await handleNavigation(navigationOptions); if (navError) { setLocalizedErrorBannerMessage( getLocalizedErrorMessage(ftlMsgResolver, navError) diff --git a/packages/fxa-settings/src/pages/Signin/SigninTotpCode/index.tsx b/packages/fxa-settings/src/pages/Signin/SigninTotpCode/index.tsx index 5a074fa81c2..01986426e9c 100644 --- a/packages/fxa-settings/src/pages/Signin/SigninTotpCode/index.tsx +++ b/packages/fxa-settings/src/pages/Signin/SigninTotpCode/index.tsx @@ -130,12 +130,11 @@ export const SigninTotpCode = ({ redirectTo, queryParams: location.search, showInlineRecoveryKeySetup, - }; - - const { error } = await handleNavigation(navigationOptions, { handleFxaLogin: true, handleFxaOAuthLogin: true, - }); + }; + + const { error } = await handleNavigation(navigationOptions); if (error) { setBannerError(getLocalizedErrorMessage(ftlMsgResolver, error)); } diff --git a/packages/fxa-settings/src/pages/Signin/SigninUnblock/index.tsx b/packages/fxa-settings/src/pages/Signin/SigninUnblock/index.tsx index 4f7a3040dc2..163794afead 100644 --- a/packages/fxa-settings/src/pages/Signin/SigninUnblock/index.tsx +++ b/packages/fxa-settings/src/pages/Signin/SigninUnblock/index.tsx @@ -127,12 +127,11 @@ export const SigninUnblock = ({ integration, finishOAuthFlowHandler, queryParams: location.search, - }; - - const { error: navError } = await handleNavigation(navigationOptions, { handleFxaLogin: true, handleFxaOAuthLogin: true, - }); + }; + + const { error: navError } = await handleNavigation(navigationOptions); if (navError) { setBannerErrorMessage( getLocalizedErrorMessage(ftlMsgResolver, navError) diff --git a/packages/fxa-settings/src/pages/Signin/index.tsx b/packages/fxa-settings/src/pages/Signin/index.tsx index 57fe4912807..ddce30fae14 100644 --- a/packages/fxa-settings/src/pages/Signin/index.tsx +++ b/packages/fxa-settings/src/pages/Signin/index.tsx @@ -213,12 +213,11 @@ const Signin = ({ : '', queryParams: location.search, showInlineRecoveryKeySetup: data.showInlineRecoveryKeySetup, - }; - - const { error: navError } = await handleNavigation(navigationOptions, { handleFxaLogin: true, handleFxaOAuthLogin: true, - }); + }; + + const { error: navError } = await handleNavigation(navigationOptions); if (navError) { setLocalizedBannerError( getLocalizedErrorMessage(ftlMsgResolver, navError) diff --git a/packages/fxa-settings/src/pages/Signin/interfaces.ts b/packages/fxa-settings/src/pages/Signin/interfaces.ts index c3813ff8a39..985e5351311 100644 --- a/packages/fxa-settings/src/pages/Signin/interfaces.ts +++ b/packages/fxa-settings/src/pages/Signin/interfaces.ts @@ -199,6 +199,13 @@ export interface NavigationOptions { redirectTo?: string; queryParams: string; showInlineRecoveryKeySetup?: boolean; + isSignInWithThirdPartyAuth?: boolean; + handleFxaLogin?: boolean; + handleFxaOAuthLogin?: boolean; + syncEngines?: { + offeredEngines: string[]; + declinedEngines: string[]; + } } export interface OAuthSigninResult { diff --git a/packages/fxa-settings/src/pages/Signin/utils.ts b/packages/fxa-settings/src/pages/Signin/utils.ts index b5e187c3a7a..4e4d47169d9 100644 --- a/packages/fxa-settings/src/pages/Signin/utils.ts +++ b/packages/fxa-settings/src/pages/Signin/utils.ts @@ -32,9 +32,18 @@ interface NavigationTargetError { export function getSyncNavigate( queryParams: string, - showInlineRecoveryKeySetup?: boolean + showInlineRecoveryKeySetup?: boolean, + isSignInWithThirdPartyAuth?: boolean ) { const searchParams = new URLSearchParams(queryParams); + + if (isSignInWithThirdPartyAuth) { + return { + to: `/post_verify/third_party_auth/set_password?${searchParams}`, + shouldHardNavigate: false, + }; + } + if (showInlineRecoveryKeySetup) { return { to: `/inline_recovery_key_setup?${searchParams}`, @@ -60,13 +69,7 @@ export function getSyncNavigate( // React signin until CAD/pair is converted to React because we'd need to pass this // data back to Backbone. This means temporarily we need to send the sync data up // _before_ we hard navigate to CAD/pair in these flows. -export async function handleNavigation( - navigationOptions: NavigationOptions, - { - handleFxaLogin = false, - handleFxaOAuthLogin = false, - }: { handleFxaLogin?: boolean; handleFxaOAuthLogin?: boolean } = {} -) { +export async function handleNavigation(navigationOptions: NavigationOptions) { const { integration } = navigationOptions; const isOAuth = isOAuthIntegration(integration); const isWebChannelIntegration = @@ -77,7 +80,7 @@ export async function handleNavigation( getUnverifiedNavigationTarget(navigationOptions); if ( isWebChannelIntegration && - handleFxaLogin && + navigationOptions.handleFxaLogin === true && // If the _next page_ is `signin_totp_code`, we don't want to send this // because we end up sending it twice with the first message containing // `verified: false`, causing a Sync sign-in issue (see FXA-9837). @@ -98,7 +101,7 @@ export async function handleNavigation( return { error: undefined }; } - if (isWebChannelIntegration && handleFxaLogin) { + if (isWebChannelIntegration && navigationOptions.handleFxaLogin === true) { // This _must_ be sent before fxaOAuthLogin for Desktop OAuth flow. // Mobile doesn't care about this message (see FXA-10388) sendFxaLogin(navigationOptions); @@ -122,7 +125,7 @@ export async function handleNavigation( } if ( isOAuthNativeIntegration(integration) && - handleFxaOAuthLogin && + navigationOptions.handleFxaOAuthLogin === true && oauthData ) { firefox.fxaOAuthLogin({ @@ -179,7 +182,7 @@ function sendFxaLogin(navigationOptions: NavigationOptions) { }), services: navigationOptions.integration.isDesktopRelay() ? { relay: {} } - : { sync: {} }, + : { sync: navigationOptions.syncEngines || {} }, }); } @@ -230,12 +233,20 @@ function performNavigation({ const getNonOAuthNavigationTarget = async ( navigationOptions: NavigationOptions ): Promise => { - const { integration, queryParams, showInlineRecoveryKeySetup, redirectTo } = - navigationOptions; + const { + integration, + queryParams, + showInlineRecoveryKeySetup, + redirectTo, + isSignInWithThirdPartyAuth, + } = navigationOptions; if (integration.isSync()) { return { - ...getSyncNavigate(queryParams, showInlineRecoveryKeySetup), - locationState: createSigninLocationState(navigationOptions), + ...getSyncNavigate( + queryParams, + showInlineRecoveryKeySetup, + isSignInWithThirdPartyAuth + ), }; } if (redirectTo) { @@ -248,6 +259,21 @@ const getOAuthNavigationTarget = async ( navigationOptions: NavigationOptions ): Promise => { const locationState = createSigninLocationState(navigationOptions); + + if ( + navigationOptions.integration.isSync() && + navigationOptions.isSignInWithThirdPartyAuth + ) { + return { + ...getSyncNavigate( + navigationOptions.queryParams, + locationState.showInlineRecoveryKeySetup, + navigationOptions.isSignInWithThirdPartyAuth + ), + locationState, + }; + } + const { error, redirect, code, state } = await navigationOptions.finishOAuthFlowHandler( navigationOptions.signinData.uid, @@ -273,7 +299,8 @@ const getOAuthNavigationTarget = async ( return { ...getSyncNavigate( navigationOptions.queryParams, - locationState.showInlineRecoveryKeySetup + locationState.showInlineRecoveryKeySetup, + navigationOptions.isSignInWithThirdPartyAuth ), oauthData: { code, 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 ( diff --git a/packages/fxa-shared/metrics/glean/web/index.ts b/packages/fxa-shared/metrics/glean/web/index.ts index a19df5daec7..f75f6a4984b 100644 --- a/packages/fxa-shared/metrics/glean/web/index.ts +++ b/packages/fxa-shared/metrics/glean/web/index.ts @@ -138,6 +138,13 @@ export const eventsMap = { viewWithNoPasswordSet: 'third_party_auth_login_no_pw_view', }, + thirdPartyAuthSetPassword: { + view: 'third_party_auth_set_password_view', + engage: 'third_party_auth_set_password_engage', + submit: 'third_party_auth_set_password_submit', + success: 'third_party_auth_set_password_success', + }, + cadMobilePair: { view: 'cad_mobile_pair_view', submit: 'cad_mobile_pair_submit',