client/components/mma/identity/settings/SettingsFormSection.tsx (333 lines of code) (raw):
import { css } from '@emotion/react';
import { from, palette } from '@guardian/source/foundations';
import { Button } from '@guardian/source/react-components';
import type { FormikProps, FormikState } from 'formik';
import { Form, withFormik } from 'formik';
import { type FC } from 'react';
import {
FormEmailField,
FormSelectField,
FormTelephoneField,
FormTextField,
} from '../form/FormField';
import * as PhoneNumberAPI from '../idapi/phonenumber';
import { Users } from '../identity';
import { IdentityLocations } from '../IdentityLocations';
import { Lines } from '../Lines';
import type { User } from '../models';
import {
COUNTRIES,
ErrorTypes,
PHONE_CALLING_CODES,
RegistrationLocations,
RegistrationLocationStatesByLocation,
Titles,
} from '../models';
import { PageSection } from '../PageSection';
import { aCss, textSmall } from '../sharedStyles';
interface SettingsFormProps {
user: User;
saveUser: (values: User) => Promise<User>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- we're only assuming the onError function's argument is an error object?
onError: (error: any) => void;
onSuccess: (input: User, response: User) => void;
onDone: () => void;
emailMessage: string | null;
}
type SettingsFormSectionProps = SettingsFormProps;
const lines = () => <Lines n={1} margin="32px auto 16px" />;
const titles = Object.values(Titles);
const registrationLocations = Object.values(RegistrationLocations);
const registrationLocationLabelModifier = (location: string) => {
switch (location) {
case 'Europe':
return `${location} (non UK)`;
case 'Prefer not to say':
return `I prefer not to say`;
default:
return location;
}
};
const registrationLocationStates = (
props: FormikProps<User> & SettingsFormProps,
) => {
switch (props.values.registrationLocation) {
case 'Australia':
return [
...RegistrationLocationStatesByLocation['Australia'],
...RegistrationLocationStatesByLocation.general,
];
case 'United States':
return [
...RegistrationLocationStatesByLocation['United States'],
...RegistrationLocationStatesByLocation.general,
];
default:
return [];
}
};
const registrationLocationStateLabelModifier = (state: string) => {
switch (state) {
case 'Prefer not to say':
return `I prefer not to say`;
default:
return state;
}
};
const deletePhoneNumber = async () => {
await PhoneNumberAPI.remove();
return await Users.getCurrentUser();
};
const EmailMessage = (email: string) => (
<p
css={[
textSmall,
{
padding: '6px 14px',
backgroundColor: palette.neutral[97],
},
]}
>
To verify your new email address <strong>{email}</strong> please check
your inbox - the confirmation email is on its way. In the meantime you
should keep using your old credentials to sign in.
</p>
);
const BaseForm = (props: FormikProps<User> & SettingsFormProps) => {
const validationNotification = (status: FormikState<User>) => {
const errors = Object.entries(status).map((s) => (
<li key={s[0]}>{s[1]}</li>
));
return (
<div
css={[
{
color: palette.error[400],
backgroundColor: '#ffe1e1',
padding: '20px 15px',
},
textSmall,
]}
>
There were some problems submitting your form. Your information
has not been saved. Please resolve the following:
<ul>{errors}</ul>
</div>
);
};
const correpondenceDescription = (
<span>
If you wish to change the delivery address for your paper
subscription vouchers, home delivery, or Guardian Weekly please see{' '}
<a css={aCss} href={IdentityLocations.CONTACT_AND_DELIVERY_HELP}>
Help with updating your contact or delivery details.
</a>
</span>
);
const locationDescription = (
<span>
We work out your location using cookies, so your experience is more
relevant to you. You can make sure this is accurate when you are
signed in by selecting your location. If you don’t want to share
this information, please select{' '}
<span
css={{
[from.desktop]: {
display: 'block',
},
}}
>
“I prefer not to say”.
</span>
</span>
);
const deletePhoneNumberButton = (
<Button
onClick={async () => {
const response = await deletePhoneNumber();
props.resetForm({ values: response });
}}
>
Delete Phone Number
</Button>
);
return (
<Form>
{!props.status || validationNotification(props.status)}
{lines()}
<PageSection title="Email & Password">
<FormEmailField
name="primaryEmailAddress"
label="Email"
formikProps={props}
/>
{!props.emailMessage || EmailMessage(props.emailMessage)}
<label>
Password
<ul
css={css`
list-style: none;
margin: 0;
padding: 0;
li + li {
margin-top: 4px;
}
`}
>
<li>
<a
css={aCss}
href={IdentityLocations.RESET_PASSWORD}
>
Change password
</a>
</li>
</ul>
</label>
</PageSection>
{lines()}
<PageSection title="Phone">
<FormSelectField
name="countryCode"
label="Country code"
options={PHONE_CALLING_CODES}
formikProps={props}
labelModifier={(o: string) => `+${o}`}
/>
<FormTelephoneField
name="localNumber"
label="Local Number"
formikProps={props}
/>
{deletePhoneNumberButton}
</PageSection>
{lines()}
<PageSection title="Personal Information">
<FormSelectField
name="title"
label="Title"
options={titles}
formikProps={props}
/>
<FormTextField
name="firstName"
label="First Name"
formikProps={props}
/>
<FormTextField
name="secondName"
label="Last Name"
formikProps={props}
/>
</PageSection>
{lines()}
<PageSection
title="Correspondence address"
description={correpondenceDescription}
>
<FormTextField
name="address1"
label="Address line 1"
formikProps={props}
/>
<FormTextField
name="address2"
label="Address line 2"
formikProps={props}
/>
<FormTextField
name="address3"
label="Town"
formikProps={props}
/>
<FormTextField
name="address4"
label="County or State"
formikProps={props}
/>
<FormTextField
name="postcode"
label="Postcode/Zipcode"
formikProps={props}
/>
<FormSelectField
name="country"
label="Country"
options={COUNTRIES.flatMap((country) => country.name)}
formikProps={props}
/>
</PageSection>
{lines()}
<PageSection title="Location" description={locationDescription}>
<FormSelectField
name="registrationLocation" // must match api field name
label="Location"
labelModifier={registrationLocationLabelModifier}
options={registrationLocations}
firstOptionLabel="Unknown"
firstOptionDisabled={true}
formikProps={props}
/>
{['Australia', 'United States'].includes(
props.values.registrationLocation,
) && (
<FormSelectField
name="registrationLocationState"
label="State/Territory"
labelModifier={registrationLocationStateLabelModifier}
options={registrationLocationStates(props)}
firstOptionLabel="Unknown"
firstOptionDisabled={true}
formikProps={props}
/>
)}
</PageSection>
{lines()}
<PageSection title="Delete account">
<a css={aCss} href={IdentityLocations.DELETE_ACCOUNT}>
Delete your account
</a>
</PageSection>
{lines()}
<PageSection>
<Button
disabled={props.isSubmitting}
onClick={() => props.submitForm()}
>
Save changes
</Button>
</PageSection>
</Form>
);
};
const FormikForm = withFormik({
mapPropsToValues: (props: SettingsFormProps) => props.user,
handleSubmit: async (values, formikBag) => {
const { resetForm, setSubmitting, setStatus } = formikBag;
const { saveUser, onSuccess, onError, onDone } = formikBag.props;
// if registrationLocation is not Australia or United States, set registrationLocationState to blank
if (
!['Australia', 'United States'].includes(
values.registrationLocation,
)
) {
values.registrationLocationState = '';
}
setStatus(undefined);
try {
const response = await saveUser(values);
resetForm({ values: response });
onSuccess(values, response);
} catch (e) {
if (e.type && e.type === ErrorTypes.VALIDATION) {
setStatus(e.error);
} else {
onError(e);
}
}
onDone();
setSubmitting(false);
},
})(BaseForm);
export const SettingsFormSection: FC<SettingsFormSectionProps> = (props) => {
return <FormikForm {...props} />;
};