Skip to content

Commit

Permalink
feat(react): Convert third party auth 'Set password' page to React
Browse files Browse the repository at this point in the history
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
  • Loading branch information
LZoog committed Nov 25, 2024
1 parent 939cd3f commit 8e1071c
Show file tree
Hide file tree
Showing 36 changed files with 1,210 additions and 304 deletions.
22 changes: 18 additions & 4 deletions packages/fxa-auth-client/lib/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1538,14 +1538,28 @@ export default class AuthClient {
email: string,
newPassword: string,
headers?: Headers
): Promise<number> {
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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
5 changes: 5 additions & 0 deletions packages/fxa-settings/src/components/App/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'));

Expand Down Expand Up @@ -318,6 +319,10 @@ const AuthAndAccountSetupRoutes = ({
path="/post_verify/third_party_auth/callback/*"
{...{ flowQueryParams }}
/>
<SetPasswordContainer
path="/post_verify/third_party_auth/set_password/*"
{...{ flowQueryParams, integration }}
/>

{/* Reset password */}
<ResetPasswordContainer
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export type FormPasswordWithBalloonsProps = {
loading: boolean;
children?: React.ReactNode;
disableButtonUntilValid?: boolean;
submitButtonGleanId?: string;
};

const getTemplateValues = (passwordFormType: PasswordFormType) => {
Expand Down Expand Up @@ -75,6 +76,7 @@ export const FormPasswordWithBalloons = ({
loading,
children,
disableButtonUntilValid = false,
submitButtonGleanId,
}: FormPasswordWithBalloonsProps) => {
const passwordValidator = new PasswordValidator(email);
const [passwordMatchErrorText, setPasswordMatchErrorText] =
Expand Down Expand Up @@ -394,6 +396,7 @@ export const FormPasswordWithBalloons = ({
disabled={
loading || (!formState.isValid && disableButtonUntilValid)
}
data-glean-id={submitButtonGleanId && submitButtonGleanId}
>
{templateValues.buttonText}
</button>
Expand Down
Empty file.
139 changes: 139 additions & 0 deletions packages/fxa-settings/src/components/FormSetupAccount/index.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<ChooseWhatToSync
{...{
offeredSyncEngineConfigs,
setDeclinedSyncEngines,
}}
/>
);
} else {
// Waiting to receive webchannel message from browser
return <LoadingSpinner className="flex justify-center mb-4" />;
}
} else {
// Display nothing if Sync flow that does not support webchannels
// or if CWTS is disabled
return <></>;
}
};

return (
<FormPasswordWithBalloons
{...{
formState,
errors,
trigger,
register,
getValues,
email,
onFocusMetricsEvent,
disableButtonUntilValid: true,
onSubmit,
loading,
submitButtonGleanId
}}
passwordFormType="signup"
>
{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. */}
<FtlMsg id="signup-age-check-label" attrs={{ label: true }}>
<InputText
name="age"
label="How old are you?"
inputMode="numeric"
className="mb-4"
pattern="[0-9]*"
maxLength={3}
onChange={() => {
// 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"
/>
</FtlMsg>
<FtlMsg id="signup-coppa-check-explanation-link">
<LinkExternal
href="https://www.ftc.gov/business-guidance/resources/childrens-online-privacy-protection-rule-not-just-kids-sites"
className={`link-blue text-sm py-1 -mt-2 self-start ${
isDesktopRelay ? 'mb-8' : 'mb-4'
}`}
onClick={() => GleanMetrics.registration.whyWeAsk()}
>
Why do we ask?
</LinkExternal>
</FtlMsg>
</>
)}

{isSync
? showCWTS()
: !isDesktopRelay &&
setSelectedNewsletterSlugs && (
<ChooseNewsletters
{...{
newsletters,
setSelectedNewsletterSlugs,
}}
/>
)}
</FormPasswordWithBalloons>
);
};
Original file line number Diff line number Diff line change
@@ -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<void>;
loading: boolean;
isSync: boolean;
offeredSyncEngineConfigs?: typeof syncEngineConfigs;
setDeclinedSyncEngines: React.Dispatch<React.SetStateAction<string[]>>;
isDesktopRelay: boolean;
setSelectedNewsletterSlugs?: React.Dispatch<React.SetStateAction<string[]>>;
// Age check props, if not provided it will not be rendered
ageCheckErrorText?: string;
setAgeCheckErrorText?: React.Dispatch<React.SetStateAction<string>>;
onFocusAgeInput?: () => void;
onBlurAgeInput?: () => void;
submitButtonGleanId?: string;
};
9 changes: 9 additions & 0 deletions packages/fxa-settings/src/lib/glean/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -517,6 +523,9 @@ const recordEventMetric = (
reason: gleanPingMetrics?.event?.['reason'] || '',
});
break;
case 'third_party_auth_set_password_success':
thirdPartyAuthSetPassword.success.record();
break;
}
};

Expand Down
Loading

0 comments on commit 8e1071c

Please sign in to comment.