Skip to content

Commit

Permalink
Merge pull request #17616 from mozilla/FXA-10390
Browse files Browse the repository at this point in the history
fix(settings): Use single input code component in ConfirmResetPassword
  • Loading branch information
vpomerleau authored Sep 19, 2024
2 parents 9de1a3e + 59a31b7 commit 3b185ed
Show file tree
Hide file tree
Showing 20 changed files with 225 additions and 647 deletions.
2 changes: 1 addition & 1 deletion packages/fxa-settings/src/components/Banner/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ const Banner = ({
}: BannerProps) => {
// Transparent border is for Windows HCM - to ensure there is a border around the banner
const baseClassNames =
'text-xs font-bold p-3 my-3 rounded border border-transparent';
'text-xs font-bold p-3 my-3 rounded border border-transparent animate-fade-in';

return (
<div
Expand Down
14 changes: 14 additions & 0 deletions packages/fxa-settings/src/components/FormVerifyTotp/en.ftl
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
## FormVerifyTotp component
## Form to enter a time-based one-time-passcode (e.g., 6-digit numeric code or 8-digit alphanumeric code)

# Information explaining why button is disabled, also read to screen readers
# Submit button is disabled unless a valid code format is entered
# Used when the code may only contain numbers
# $codeLength : number of digits in a valid code
form-verify-totp-disabled-button-title-numeric = Enter { $codeLength }-digit code to continue
# Information explaining why button is disabled, also read to screen readers
# Submit button is disabled unless a valid code format is entered
# Used when the code may contain numbers and/or letters
# $codeLength : number of characters in a valid code
form-verify-totp-disabled-button-title-alphanumeric = Enter {$ codeLength }-character code to continue bloop
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,8 @@ export const With6DigitCode = () => <Subject />;

export const With8DigitCode = () => <Subject codeLength={8} />;

export const With10CharAlphanumericCode = () => (
<Subject codeLength={10} codeType="alphanumeric" />
);

export const WithErrorOnSubmit = () => <Subject success={false} />;
40 changes: 5 additions & 35 deletions packages/fxa-settings/src/components/FormVerifyTotp/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ describe('FormVerifyTotp component', () => {
it('renders as expected with default props', async () => {
renderWithLocalizationProvider(<Subject />);
expect(screen.getByText('Enter 6-digit code')).toBeVisible();
expect(screen.getAllByRole('textbox')).toHaveLength(6);
expect(screen.getAllByRole('textbox')).toHaveLength(1);
const button = screen.getByRole('button');
expect(button).toHaveTextContent('Submit');
});
Expand All @@ -28,36 +28,25 @@ describe('FormVerifyTotp component', () => {
it('is enabled when numbers are typed into all inputs', async () => {
const user = userEvent.setup();
renderWithLocalizationProvider(<Subject />);
const input = screen.getByRole('textbox');
const button = screen.getByRole('button');
expect(button).toHaveTextContent('Submit');
expect(button).toBeDisabled();

await waitFor(() =>
user.click(screen.getByRole('textbox', { name: 'Digit 1 of 6' }))
);
await user.type(input, '123456');

// type in each input
for (let i = 1; i <= 6; i++) {
await waitFor(() =>
user.type(
screen.getByRole('textbox', { name: `Digit ${i} of 6` }),
i.toString()
)
);
}
expect(button).toBeEnabled();
});

it('is enabled when numbers are pasted into all inputs', async () => {
const user = userEvent.setup();
renderWithLocalizationProvider(<Subject />);
const input = screen.getByRole('textbox');
const button = screen.getByRole('button');
expect(button).toHaveTextContent('Submit');
expect(button).toBeDisabled();

await waitFor(() =>
user.click(screen.getByRole('textbox', { name: 'Digit 1 of 6' }))
);
await user.click(input);

await waitFor(() => {
user.paste('123456');
Expand All @@ -66,23 +55,4 @@ describe('FormVerifyTotp component', () => {
expect(button).toBeEnabled();
});
});

describe('errors', () => {
it('are cleared when typing in input', async () => {
const user = userEvent.setup();
renderWithLocalizationProvider(
<Subject initialErrorMessage="Something went wrong" />
);

expect(screen.getByText('Something went wrong')).toBeVisible();

await waitFor(() =>
user.type(screen.getByRole('textbox', { name: 'Digit 1 of 6' }), '1')
);

expect(
screen.queryByText('Something went wrong')
).not.toBeInTheDocument();
});
});
});
157 changes: 95 additions & 62 deletions packages/fxa-settings/src/components/FormVerifyTotp/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,87 +2,120 @@
* 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, { useRef, useState } from 'react';
import TotpInputGroup from '../TotpInputGroup';
import { FtlMsg } from 'fxa-react/lib/utils';
import Banner, { BannerType } from '../Banner';
import React, { useState } from 'react';
import InputText from '../InputText';
import { useForm } from 'react-hook-form';
import { getLocalizedErrorMessage } from '../../lib/error-utils';
import { useFtlMsgResolver } from '../../models';
import { AuthUiErrors } from '../../lib/auth-errors/auth-errors';
import { FormVerifyTotpProps, VerifyTotpFormData } from './interfaces';

export type CodeArray = Array<string | undefined>;

export type FormVerifyTotpProps = {
codeLength: 6 | 8;
errorMessage: string;
localizedInputGroupLabel: string;
localizedSubmitButtonText: string;
setErrorMessage: React.Dispatch<React.SetStateAction<string>>;
verifyCode: (code: string) => void;
};
// Split inputs are not recommended for accesibility
// Code inputs should be a single input field
// See FXA-10390 for details on reverting from split input to single input

const FormVerifyTotp = ({
clearBanners,
codeLength,
codeType,
errorMessage,
localizedInputGroupLabel,
localizedInputLabel,
localizedSubmitButtonText,
setErrorMessage,
verifyCode,
}: FormVerifyTotpProps) => {
const inputRefs = useRef(
Array.from({ length: codeLength }, () =>
React.createRef<HTMLInputElement>()
)
);
const [isSubmitDisabled, setIsSubmitDisabled] = useState(true);

const [codeArray, setCodeArray] = useState<CodeArray>(new Array(codeLength));
const [isSubmitting, setIsSubmitting] = useState(false);
const ftlMsgResolver = useFtlMsgResolver();

const stringifiedCode = codeArray.join('');
const { handleSubmit, register } = useForm<VerifyTotpFormData>({
mode: 'onBlur',
criteriaMode: 'all',
defaultValues: {
code: '',
},
});

const isFormValid = stringifiedCode.length === codeLength && !errorMessage;
const isSubmitDisabled = isSubmitting || !isFormValid;
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (errorMessage) {
setErrorMessage('');
}
// only accept characters that match the code type (numeric or alphanumeric)
// strip out any other characters
const filteredCode = e.target.value.replace(
codeType === 'numeric' ? /[^0-9]/g : /[^a-zA-Z0-9]/g,
''
);
e.target.value = filteredCode;
console.log(e.target.value.length);
e.target.value.length === codeLength
? setIsSubmitDisabled(false)
: setIsSubmitDisabled(true);
};

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const onSubmit = async ({ code }: VerifyTotpFormData) => {
clearBanners && clearBanners();
setIsSubmitDisabled(true);
// Only submit the code if it is the correct length
// Otherwise, show an error message
if (code.length !== codeLength) {
setErrorMessage(
getLocalizedErrorMessage(ftlMsgResolver, AuthUiErrors.INVALID_OTP_CODE)
);
} else if (!isSubmitDisabled) {
await verifyCode(code);
}
setIsSubmitDisabled(false);
};

if (!isSubmitDisabled) {
setIsSubmitting(true);
await verifyCode(stringifiedCode);
setIsSubmitting(false);
const getDisabledButtonTitle = () => {
if (codeType === 'numeric') {
return ftlMsgResolver.getMsg(
'form-verify-totp-disabled-button-title-numeric',
`Enter ${codeLength}-digit code to continue`,
{ codeLength }
);
} else {
return ftlMsgResolver.getMsg(
'form-verify-totp-disabled-button-title-alphanumeric',
`Enter ${codeLength}-character code to continue`,
{ codeLength }
);
}
};

return (
<>
{errorMessage && <Banner type={BannerType.error}>{errorMessage}</Banner>}
<form
noValidate
className="flex flex-col gap-4 my-6"
onSubmit={handleSubmit}
<form
noValidate
className="flex flex-col gap-4 my-6"
onSubmit={handleSubmit(onSubmit)}
>
{/* Using `type="text" inputmode="numeric"` shows the numeric keyboard on mobile
and strips out whitespace on desktop, but does not add an incrementer. */}
<InputText
name="code"
type="text"
inputMode={codeType === 'numeric' ? 'numeric' : 'text'}
label={localizedInputLabel}
onChange={handleChange}
autoFocus
maxLength={codeLength}
className="text-start"
anchorPosition="start"
autoComplete="one-time-code"
spellCheck={false}
inputRef={register({ required: true })}
hasErrors={!!errorMessage}
/>
<button
type="submit"
className="cta-primary cta-xl"
disabled={isSubmitDisabled}
title={isSubmitDisabled ? getDisabledButtonTitle() : ''}
>
<TotpInputGroup
{...{
codeArray,
codeLength,
inputRefs,
localizedInputGroupLabel,
setCodeArray,
setErrorMessage,
errorMessage,
}}
/>
<FtlMsg
id="form-verify-code-submit-button-2"
vars={{ codeValue: stringifiedCode }}
>
<button
type="submit"
className="cta-primary cta-xl"
disabled={isSubmitDisabled}
>
{localizedSubmitButtonText}
</button>
</FtlMsg>
</form>
</>
{localizedSubmitButtonText}
</button>
</form>
);
};

Expand Down
18 changes: 18 additions & 0 deletions packages/fxa-settings/src/components/FormVerifyTotp/interfaces.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/* 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/. */

export type FormVerifyTotpProps = {
clearBanners?: () => void;
codeLength: 6 | 8 | 10;
codeType: 'numeric' | 'alphanumeric';
errorMessage: string;
localizedInputLabel: string;
localizedSubmitButtonText: string;
setErrorMessage: React.Dispatch<React.SetStateAction<string>>;
verifyCode: (code: string) => void;
};

export type VerifyTotpFormData = {
code: string;
};
9 changes: 6 additions & 3 deletions packages/fxa-settings/src/components/FormVerifyTotp/mocks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,19 @@

import React, { useCallback, useState } from 'react';
import AppLayout from '../AppLayout';
import FormVerifyTotp, { FormVerifyTotpProps } from '.';
import FormVerifyTotp from '.';
import { FormVerifyTotpProps } from './interfaces';

export const Subject = ({
codeLength = 6,
codeType = 'numeric',
success = true,
initialErrorMessage = '',
}: Partial<FormVerifyTotpProps> & {
success?: Boolean;
initialErrorMessage?: string;
}) => {
const localizedInputGroupLabel = `Enter ${codeLength.toString()}-digit code`;
const localizedInputLabel = `Enter ${codeLength.toString()}-digit code`;
const localizedSubmitButtonText = 'Submit';
const [errorMessage, setErrorMessage] = useState(initialErrorMessage);

Expand All @@ -35,8 +37,9 @@ export const Subject = ({
<FormVerifyTotp
{...{
codeLength,
codeType,
errorMessage,
localizedInputGroupLabel,
localizedInputLabel,
localizedSubmitButtonText,
setErrorMessage,
verifyCode,
Expand Down
8 changes: 7 additions & 1 deletion packages/fxa-settings/src/components/InputText/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,13 @@ export type InputTextProps = {
pattern?: string;
anchorPosition?: 'start' | 'middle' | 'end';
spellCheck?: boolean;
autoComplete?: string;
// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofilling-form-controls:-the-autocomplete-attribute
autoComplete?:
| 'off'
| 'username'
| 'current-password'
| 'new-password'
| 'one-time-code';
inputMode?: 'text' | 'numeric' | 'tel' | 'email';
required?: boolean;
tooltipPosition?: 'top' | 'bottom';
Expand Down
7 changes: 0 additions & 7 deletions packages/fxa-settings/src/components/TotpInputGroup/en.ftl

This file was deleted.

This file was deleted.

Loading

0 comments on commit 3b185ed

Please sign in to comment.