Skip to content

Commit

Permalink
Merge pull request #18044 from mozilla/FXA-6651
Browse files Browse the repository at this point in the history
feat(react): Convert third party auth 'Set password' page to React
  • Loading branch information
LZoog authored Nov 26, 2024
2 parents 4abcedf + 3e592b4 commit da9ec5a
Show file tree
Hide file tree
Showing 35 changed files with 1,231 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
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 da9ec5a

Please sign in to comment.