Skip to content

Commit 5a76a54

Browse files
Merge pull request #19986 from mozilla/FXA-12906
feat(settings): create ButtonPasskeySignin component
2 parents 056e1dd + 0250eee commit 5a76a54

9 files changed

Lines changed: 190 additions & 0 deletions

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
## ButtonPasskeySignin
2+
3+
button-passkey-signin = Sign in with passkey
4+
5+
# This is a loading state indicating that we are waiting for the user to
6+
# interact with their authenticator to approve the sign-in. They should see a
7+
# device prompt/pop-up with authentication options (or message indicating that
8+
# no passkeys are available).
9+
button-passkey-signin-loading = Securely signing in…
10+
11+
##
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
import React from 'react';
6+
import ButtonPasskeySignin from '.';
7+
import { withLocalization } from 'fxa-react/lib/storybooks';
8+
import { Meta } from '@storybook/react';
9+
import { action } from '@storybook/addon-actions';
10+
11+
export default {
12+
title: 'components/ButtonPasskeySignin',
13+
component: ButtonPasskeySignin,
14+
decorators: [withLocalization],
15+
} as Meta;
16+
17+
export const Default = () => {
18+
const [loading, setLoading] = React.useState(false);
19+
20+
const handleClick = () => {
21+
action('clicked')();
22+
setLoading(true);
23+
setTimeout(() => {
24+
setLoading(false);
25+
}, 2000);
26+
};
27+
28+
return (
29+
<div className="card mx-auto">
30+
<ButtonPasskeySignin loading={loading} onClick={handleClick} />
31+
</div>
32+
);
33+
};
34+
35+
export const Loading = () => (
36+
<div className="card mx-auto">
37+
<ButtonPasskeySignin loading={true} onClick={action('clicked')} />
38+
</div>
39+
);
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
import React from 'react';
6+
import { screen } from '@testing-library/react';
7+
import userEvent from '@testing-library/user-event';
8+
import { renderWithLocalizationProvider } from 'fxa-react/lib/test-utils/localizationProvider';
9+
import ButtonPasskeySignin from '.';
10+
11+
describe('ButtonPasskeySignin', () => {
12+
it('renders in default state with passkey icon and text', () => {
13+
renderWithLocalizationProvider(<ButtonPasskeySignin />);
14+
15+
screen.getByText('Sign in with passkey');
16+
const button = screen.getByRole('button');
17+
expect(button).toBeInTheDocument();
18+
expect(button).toBeEnabled();
19+
20+
const passkeyIcon = screen.getByText('icon_passkey.min.svg');
21+
expect(passkeyIcon).toHaveAttribute('aria-hidden', 'true');
22+
});
23+
24+
it('renders loading state with spinner and loading text', () => {
25+
renderWithLocalizationProvider(<ButtonPasskeySignin loading={true} />);
26+
27+
screen.getByText('Securely signing in…');
28+
const button = screen.getByRole('button');
29+
expect(button).toBeInTheDocument();
30+
expect(button).toBeDisabled();
31+
32+
const loadingIcon = screen.getByText('icon_loading_arrow.min.svg');
33+
expect(loadingIcon).toBeInTheDocument();
34+
expect(loadingIcon).toHaveAttribute('aria-hidden', 'true');
35+
expect(loadingIcon).toHaveClass('animate-spin-slow');
36+
});
37+
38+
it('calls onClick handler when clicked in default state', async () => {
39+
const user = userEvent.setup();
40+
const handleClick = jest.fn();
41+
42+
renderWithLocalizationProvider(
43+
<ButtonPasskeySignin onClick={handleClick} />
44+
);
45+
46+
const button = screen.getByRole('button');
47+
await user.click(button);
48+
49+
expect(handleClick).toHaveBeenCalledTimes(1);
50+
});
51+
52+
it('does not call onClick handler when loading', async () => {
53+
const user = userEvent.setup();
54+
const handleClick = jest.fn();
55+
56+
renderWithLocalizationProvider(
57+
<ButtonPasskeySignin onClick={handleClick} loading={true} />
58+
);
59+
60+
const button = screen.getByRole('button');
61+
await user.click(button);
62+
63+
expect(handleClick).not.toHaveBeenCalled();
64+
});
65+
});
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
import React from 'react';
6+
import { LoadingArrowIcon, PasskeyIcon } from '../Icons';
7+
import { FtlMsg } from 'fxa-react/lib/utils';
8+
9+
export type ButtonPasskeySigninProps = {
10+
loading?: boolean;
11+
onClick?: () => void;
12+
};
13+
14+
const ButtonPasskeySignin = ({
15+
loading = false,
16+
onClick = () => {},
17+
}: ButtonPasskeySigninProps) => {
18+
const buttonText = loading ? (
19+
<>
20+
<LoadingArrowIcon className="h-5 w-5 me-4 animate-spin-slow" ariaHidden />
21+
<FtlMsg id="button-passkey-signin-loading">Securely signing in…</FtlMsg>
22+
</>
23+
) : (
24+
<>
25+
<PasskeyIcon className="h-5 w-5 me-4" ariaHidden />
26+
<FtlMsg id="button-passkey-signin">Sign in with passkey</FtlMsg>
27+
</>
28+
);
29+
30+
return (
31+
<button
32+
type="submit"
33+
className="cta-xl w-full justify-center font-header text-grey-900 bg-grey-10 border-grey-200 border text-center inline-flex items-center focus-visible-default outline-offset-2 hover:border-grey-600 active:bg-grey-100 disabled:bg-grey-100 disabled:border-grey-600 disabled:cursor-not-allowed"
34+
onClick={onClick}
35+
disabled={loading}
36+
>
37+
{buttonText}
38+
</button>
39+
);
40+
};
41+
42+
export default ButtonPasskeySignin;

packages/fxa-settings/src/components/Icons/en.ftl

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,13 @@ info-icon-aria-label =
6565
# Used to select United States as a country code for phone number
6666
usa-flag-icon-aria-label =
6767
.aria-label = United States Flag
68+
69+
# Used for loading arrow icon
70+
icon-loading-arrow-aria-label =
71+
.aria-label = Loading
72+
73+
# Used for passkey icon
74+
icon-passkey-aria-label =
75+
.aria-label = Passkey
76+
77+
##
Lines changed: 1 addition & 0 deletions
Loading
Lines changed: 1 addition & 0 deletions
Loading

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import { ReactComponent as FlagUsa } from './icon_flag_usa.min.svg';
2222
import { ReactComponent as InformationOutlineCurrent } from './icon_information_circle_outline_current.min.svg';
2323
import { ReactComponent as InformationOutlineBlue } from './icon_information_circle_outline_blue.min.svg';
2424
import { ReactComponent as Lightbulb } from './icon_lightbulb.min.svg';
25+
import { ReactComponent as LoadingArrow } from './icon_loading_arrow.min.svg';
26+
import { ReactComponent as Passkey } from './icon_passkey.min.svg';
2527

2628
type AlertMode = 'alert' | 'attention' | 'warning';
2729
function getAlertAria(mode: AlertMode) {
@@ -289,3 +291,21 @@ export const LightbulbIcon = ({ className, ariaHidden }: ImageProps) => (
289291
{...{ className, ariaHidden }}
290292
/>
291293
);
294+
295+
export const LoadingArrowIcon = ({ className, ariaHidden }: ImageProps) => (
296+
<PreparedIcon
297+
Image={LoadingArrow}
298+
ariaLabel="Loading"
299+
ariaLabelFtlId="loading-arrow-icon-aria-label"
300+
{...{ className, ariaHidden }}
301+
/>
302+
);
303+
304+
export const PasskeyIcon = ({ className, ariaHidden }: ImageProps) => (
305+
<PreparedIcon
306+
Image={Passkey}
307+
ariaLabel="Passkey"
308+
ariaLabelFtlId="passkey-icon-aria-label"
309+
{...{ className, ariaHidden }}
310+
/>
311+
);

packages/fxa-settings/tailwind.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,7 @@ config.theme.extend = {
288288
'wait-and-rotate': 'wait-and-rotate 5s infinite ease-out',
289289
'fade-in': 'fade-in 1s 1 ease-in',
290290
'spin-xl': 'spin-xl 1s forwards ease-in-out',
291+
'spin-slow': 'spin 1.2s linear infinite',
291292
'spin-pause': 'spin-pause 4s ease-in-out forwards',
292293
'fade-out-in': 'fade-out-in 2s forwards',
293294
'grow-and-stay': 'grow 1s ease-in-out forwards',

0 commit comments

Comments
 (0)