Skip to content

Commit 2fae440

Browse files
feat(auth): auto submit email confirmation code form on paste (non-sync only)
Because: * we want to automatically submit email confirmation code form when a valid code is pasted. This commit: * implements such feature for email registration in non-sync flows. Closes #FXA-11671
1 parent 9716083 commit 2fae440

9 files changed

Lines changed: 114 additions & 8 deletions

File tree

packages/fxa-settings/src/components/FormVerifyCode/index.stories.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,9 @@ export const WithCustomErrorMessage = () => (
2626
<Subject localizedCustomCodeRequiredMessage="This is a spoofed custom error" />
2727
</AppLayout>
2828
);
29+
30+
export const SubmitOnPasteDisabled = () => (
31+
<AppLayout>
32+
<Subject submitFormOnPaste={false} />
33+
</AppLayout>
34+
);

packages/fxa-settings/src/components/FormVerifyCode/index.test.tsx

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { screen } from '@testing-library/react';
77
import { renderWithLocalizationProvider } from 'fxa-react/lib/test-utils/localizationProvider'; // import { getFtlBundle, testAllL10n } from 'fxa-react/lib/test-utils';
88
// import { FluentBundle } from '@fluent/bundle';
99
import { Subject } from './mocks';
10+
import userEvent from '@testing-library/user-event';
1011

1112
jest.mock('../../lib/metrics', () => ({
1213
logViewEvent: jest.fn(),
@@ -28,5 +29,48 @@ describe('FormVerifyCode component', () => {
2829
screen.getByRole('button', { name: 'Check that code' });
2930
});
3031

32+
it('submits when a valid code is pasted', async () => {
33+
const user = userEvent.setup();
34+
const verifyCode = jest.fn();
35+
renderWithLocalizationProvider(<Subject verifyCode={verifyCode} />);
36+
const input = screen.getByRole('textbox', {
37+
name: 'Enter your 4-digit code',
38+
});
39+
input.focus();
40+
await user.keyboard('1'); // should submit regardless of existing value
41+
await user.paste('1234');
42+
expect(verifyCode).toHaveBeenCalledWith('1234');
43+
});
44+
45+
it('handles pasted content normally when an invalid code is pasted', async () => {
46+
const user = userEvent.setup();
47+
const verifyCode = jest.fn();
48+
renderWithLocalizationProvider(<Subject verifyCode={verifyCode} />);
49+
const input = screen.getByRole('textbox', {
50+
name: 'Enter your 4-digit code',
51+
});
52+
input.focus();
53+
await user.keyboard('1');
54+
await user.paste('123');
55+
// should not submit even though the value after pasting is valid
56+
// because we only care about pasted content here.
57+
expect(input).toHaveValue('1123');
58+
expect(verifyCode).not.toHaveBeenCalled();
59+
});
60+
61+
it('does not submit on paste when submitFormOnPaste is false', async () => {
62+
const user = userEvent.setup();
63+
const verifyCode = jest.fn();
64+
renderWithLocalizationProvider(
65+
<Subject verifyCode={verifyCode} submitFormOnPaste={false} />
66+
);
67+
const input = screen.getByRole('textbox', {
68+
name: 'Enter your 4-digit code',
69+
});
70+
input.focus();
71+
await user.paste('1234');
72+
expect(verifyCode).not.toHaveBeenCalled();
73+
});
74+
3175
// TODO Add tests for (engage, success, etc.) metrics events once submit button enabled
3276
});

packages/fxa-settings/src/components/FormVerifyCode/index.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ export type FormVerifyCodeProps = {
4444
setClearMessages?: React.Dispatch<React.SetStateAction<boolean>>;
4545
isLoading?: boolean;
4646
gleanDataAttrs?: GleanClickEventDataAttrs;
47+
submitFormOnPaste?: boolean;
4748
};
4849

4950
type FormData = {
@@ -59,6 +60,7 @@ const FormVerifyCode = ({
5960
setCodeErrorMessage,
6061
setClearMessages,
6162
gleanDataAttrs,
63+
submitFormOnPaste,
6264
}: FormVerifyCodeProps) => {
6365
const [isFocused, setIsFocused] = useState<boolean>(false);
6466
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
@@ -109,6 +111,14 @@ const FormVerifyCode = ({
109111
setIsSubmitting(false);
110112
};
111113

114+
const onPaste = async (event: React.ClipboardEvent<HTMLInputElement>) => {
115+
const pastedText = event.clipboardData.getData('text').trim();
116+
const isValid = new RegExp(formAttributes.pattern).test(pastedText);
117+
if (isValid) {
118+
onSubmit({ code: pastedText });
119+
}
120+
};
121+
112122
return (
113123
<form
114124
noValidate
@@ -128,6 +138,7 @@ const FormVerifyCode = ({
128138
: () => setCodeErrorMessage('')
129139
}
130140
onFocusCb={viewName ? onFocus : undefined}
141+
onPaste={submitFormOnPaste ? onPaste : undefined}
131142
errorText={codeErrorMessage}
132143
autoFocus
133144
pattern={formAttributes.pattern}
@@ -138,7 +149,9 @@ const FormVerifyCode = ({
138149
spellCheck={false}
139150
prefixDataTestId={viewName}
140151
tooltipPosition="bottom"
141-
inputRef={register({ required: true })}
152+
inputRef={register({
153+
required: true,
154+
})}
142155
/>
143156

144157
<FtlMsg id={formAttributes.submitButtonFtlId}>

packages/fxa-settings/src/components/FormVerifyCode/mocks.tsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,14 @@
55
import React, { useState } from 'react';
66
import FormVerifyCode, { FormAttributes, FormVerifyCodeProps } from '.';
77

8+
const onFormSubmit = async () => {
9+
alert('Trying to submit');
10+
};
11+
812
export const Subject = ({
913
localizedCustomCodeRequiredMessage = '',
14+
verifyCode = onFormSubmit,
15+
submitFormOnPaste = true,
1016
}: Partial<FormVerifyCodeProps>) => {
1117
const [codeErrorMessage, setCodeErrorMessage] = useState<string>('');
1218

@@ -19,17 +25,14 @@ export const Subject = ({
1925
submitButtonText: 'Check that code',
2026
};
2127

22-
const onFormSubmit = async () => {
23-
alert('Trying to submit');
24-
};
25-
2628
return (
2729
<FormVerifyCode
28-
verifyCode={onFormSubmit}
2930
viewName="default-view"
3031
{...{
3132
formAttributes,
3233
localizedCustomCodeRequiredMessage,
34+
verifyCode,
35+
submitFormOnPaste,
3336
codeErrorMessage,
3437
setCodeErrorMessage,
3538
}}

packages/fxa-settings/src/components/InputText/index.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export type InputTextProps = {
2828
onChange?: (event: ChangeEvent<HTMLInputElement>) => void;
2929
onFocusCb?: () => void;
3030
onBlurCb?: () => void;
31+
onPaste?: (event: React.ClipboardEvent<HTMLInputElement>) => void;
3132
type?: 'text' | 'email' | 'tel' | 'number' | 'url' | 'password';
3233
name?: string;
3334
prefixDataTestId?: string;
@@ -59,6 +60,7 @@ export const InputText = ({
5960
onChange,
6061
onFocusCb,
6162
onBlurCb,
63+
onPaste,
6264
hasErrors,
6365
errorText,
6466
className = '',
@@ -174,12 +176,12 @@ export const InputText = ({
174176
data-testid={formatDataTestId('input-field')}
175177
onChange={textFieldChange}
176178
ref={combinedRef}
177-
// ref={inputRef}
178179
{...{
179180
name,
180181
disabled,
181182
onFocus,
182183
onBlur,
184+
onPaste,
183185
placeholder,
184186
type,
185187
autoFocus,

packages/fxa-settings/src/pages/Signup/ConfirmSignupCode/index.test.tsx

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import { OAUTH_ERRORS } from '../../../lib/oauth';
3535
import firefox from '../../../lib/channels/firefox';
3636
import { AuthUiErrors } from '../../../lib/auth-errors/auth-errors';
3737
import { mockWebIntegration } from '../../Signin/SigninRecoveryCode/mocks';
38+
import userEvent from '@testing-library/user-event';
3839

3940
jest.mock('../../../lib/metrics', () => ({
4041
usePageViewEvent: jest.fn(),
@@ -268,6 +269,24 @@ describe('ConfirmSignupCode page', () => {
268269
});
269270
expect(screen.queryByText(serviceRelayText)).not.toBeInTheDocument();
270271
});
272+
273+
it('submits when a valid code is pasted', async () => {
274+
const user = userEvent.setup();
275+
renderWithSession({
276+
session,
277+
integration,
278+
finishOAuthFlowHandler: jest.fn(),
279+
});
280+
const input = screen.getByRole('textbox', {
281+
name: 'Enter 6-digit code',
282+
});
283+
input.focus();
284+
await user.paste('123456');
285+
expect(session.verifySession).toHaveBeenCalledWith(
286+
'123456',
287+
expect.anything()
288+
);
289+
});
271290
});
272291

273292
describe('OAuth native integration', () => {
@@ -315,6 +334,22 @@ describe('ConfirmSignupCode page', () => {
315334
});
316335
});
317336
});
337+
338+
it('does not submit on paste when service=sync', async () => {
339+
const user = userEvent.setup();
340+
const integration = createMockOAuthNativeIntegration();
341+
renderWithSession({
342+
session,
343+
integration,
344+
finishOAuthFlowHandler: mockFinishOAuthFlowHandler,
345+
});
346+
const input = screen.getByRole('textbox', {
347+
name: 'Enter 6-digit code',
348+
});
349+
input.focus();
350+
await user.paste('123456');
351+
expect(session.verifySession).not.toHaveBeenCalled();
352+
});
318353
});
319354

320355
describe('Web integration on submission', () => {

packages/fxa-settings/src/pages/Signup/ConfirmSignupCode/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ const ConfirmSignupCode = ({
6868
const navigateWithQuery = useNavigateWithQuery();
6969
const webRedirectCheck = useWebRedirect(integration.data.redirectTo);
7070
const isDesktopRelay = integration.isDesktopRelay();
71+
const submitFormOnPaste = !integration.isSync();
7172

7273
// Make sure data is valid. If it isn't fail fast.
7374
integration.data.validate();
@@ -327,6 +328,7 @@ const ConfirmSignupCode = ({
327328
localizedCustomCodeRequiredMessage,
328329
codeErrorMessage,
329330
setCodeErrorMessage,
331+
submitFormOnPaste,
330332
}}
331333
/>
332334

packages/fxa-settings/src/pages/Signup/ConfirmSignupCode/interfaces.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export interface ConfirmSignupCodeFormData {
3939

4040
export type ConfirmSignupCodeBaseIntegration = Pick<
4141
Integration,
42-
'type' | 'data' | 'getService' | 'getClientId' | 'isDesktopRelay'
42+
'type' | 'data' | 'getService' | 'getClientId' | 'isDesktopRelay' | 'isSync'
4343
>;
4444

4545
export type ConfirmSignupCodeOAuthIntegration = Pick<

packages/fxa-settings/src/pages/Signup/ConfirmSignupCode/mocks.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ export function createMockWebIntegration({
7878
expect(integration.getService()).toEqual(MozServices.Default);
7979
expect(integration.getClientId()).toBeUndefined();
8080
expect(integration.isDesktopRelay()).toBeFalsy();
81+
expect(integration.isSync()).toBeFalsy();
8182
}
8283

8384
return integration;

0 commit comments

Comments
 (0)