Skip to content

Commit

Permalink
feat(settings): Update 2FA Settings Row
Browse files Browse the repository at this point in the history
Because:

* We are preparing to add a new recovery method for 2FA and are updating some design pieces

This commit:

* Update the 2FA settings row design
* Add new SubRowBackupMethods component
* Updates stories, tests, l10n

Closes #FXA-10206
  • Loading branch information
vpomerleau committed Nov 5, 2024
1 parent c494968 commit 5cc246e
Show file tree
Hide file tree
Showing 14 changed files with 695 additions and 326 deletions.
9 changes: 9 additions & 0 deletions packages/fxa-settings/src/components/Icons/en.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,18 @@
## Images - these are all aria labels used for illustrations
## Aria labels are used as alternate text that can be read aloud by screen readers.

# Aria-label option for an alert symbol
alert-icon-aria-label =
.aria-label = Alert
# Aria-label option for an alert symbol
icon-attention-aria-label =
.aria-label = Attention
# Aria-label option for an alert symbol
icon-warning-aria-label =
.aria-label = Warning
authenticator-app-aria-label =
.aria-label = Authenticator Application
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export default {
decorators: [withLocalization],
} as Meta;

export const Alert = () => <AlertIcon />;
export const Alert = () => <AlertIcon mode="alert" />;
export const AuthenticatorApp = () => <AuthenticatorAppIcon />;
export const BackupCodes = () => <BackupCodesIcon />;
export const BackupCodesDisabled = () => <BackupCodesDisabledIcon />;
Expand Down
29 changes: 25 additions & 4 deletions packages/fxa-settings/src/components/Icons/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,33 @@ import { ReactComponent as FlagUsa } from './icon_flag_usa.svg';
import { ReactComponent as InformationCircleOutlineBlue } from './icon_information_circle_outline_blue.svg';
import { ReactComponent as InformationCircleOutlineBlack } from './icon_information_circle_outline_black.svg';

export const AlertIcon = ({ className, ariaHidden }: ImageProps) => (
type AlertMode = 'alert' | 'attention' | 'warning';
function getAlertAria(mode: AlertMode) {
switch (mode) {
case 'alert':
return {
ariaLabel: 'Alert',
ariaLabelFtlId: 'alert-icon-aria-label',
};
case 'attention':
return {
ariaLabel: 'Attention',
ariaLabelFtlId: 'icon-attention-aria-label',
};
case 'warning':
default:
return {
ariaLabel: 'Warning',
ariaLabelFtlId: 'icon-warning-aria-label',
};
}
}

export type AlertProps = ImageProps & { mode: AlertMode };
export const AlertIcon = ({ className, ariaHidden, mode }: AlertProps) => (
<PreparedIcon
Image={Alert}
ariaLabel="Alert"
ariaLabelFtlId="alert-icon-aria-label"
{...{ className, ariaHidden }}
{...{ className, ariaHidden, ...getAlertAria(mode) }}
/>
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,14 @@ import { screen } from '@testing-library/react';
import Security from '.';
import { mockAppContext, renderWithRouter } from '../../../models/mocks';
import { Account, AppContext } from '../../../models';
import { getFtlBundle, testAllL10n } from 'fxa-react/lib/test-utils';
import { FluentBundle } from '@fluent/bundle';
import { MOCK_EMAIL } from '../../../pages/mocks';

describe('Security', () => {
let bundle: FluentBundle;
beforeAll(async () => {
bundle = await getFtlBundle('settings');
});

it('renders "fresh load" <Security/> with correct content', async () => {
const account = {
avatar: { url: null, id: null },
primaryEmail: {
email: '[email protected]',
email: MOCK_EMAIL,
},
emails: [],
displayName: 'Jody',
Expand All @@ -40,15 +34,15 @@ describe('Security', () => {
expect(await screen.findByText('Account recovery key')).toBeTruthy();
expect(await screen.findByText('Two-step authentication')).toBeTruthy();

const result = await screen.findAllByText('Not Set');
expect(result).toHaveLength(2);
expect(await screen.findAllByText('Not Set')).toHaveLength(1);
expect(await screen.findAllByText('Disabled')).toHaveLength(1);
});

it('renders "enabled two factor" and "account recovery key present" <Security/> with correct content', async () => {
const account = {
avatar: { url: null, id: null },
primaryEmail: {
email: '[email protected]',
email: MOCK_EMAIL,
},
emails: [],
displayName: 'Jody',
Expand All @@ -72,7 +66,7 @@ describe('Security', () => {
recoveryKey: { exists: false },
totp: { exists: false },
primaryEmail: {
email: '[email protected]',
email: MOCK_EMAIL,
},
passwordCreated: 1234567890,
hasPassword: true,
Expand All @@ -85,10 +79,6 @@ describe('Security', () => {
);
const passwordRouteLink = screen.getByTestId('password-unit-row-route');

testAllL10n(screen, bundle, {
date: createDate,
});

await screen.findByText('••••••••••••••••••');
await screen.findByText(`Created ${createDate}`);

Expand All @@ -104,7 +94,7 @@ describe('Security', () => {
recoveryKey: { exists: false },
totp: { exists: false },
primaryEmail: {
email: '[email protected]',
email: MOCK_EMAIL,
},
passwordCreated: 0,
hasPassword: false,
Expand All @@ -118,7 +108,8 @@ describe('Security', () => {
const passwordRouteLink = screen.getByTestId('password-unit-row-route');

await screen.findByText('Set a password', { exact: false });
expect(await screen.findAllByText('Not Set')).toHaveLength(3);
expect(await screen.findAllByText('Not Set')).toHaveLength(2);
expect(await screen.findAllByText('Disabled')).toHaveLength(1);

expect(passwordRouteLink).toHaveTextContent('Create');
expect(passwordRouteLink).toHaveAttribute(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import React from 'react';
import { Meta, StoryFn } from '@storybook/react';
import BackupMethodSubRow, {
BackupCodesAvailable,
BackupCodesUnavailable,
BackupRowProps,
} from './index';
import { action } from '@storybook/addon-actions';
import { CheckmarkGreenIcon, AlertIcon } from '../../Icons';

export default {
title: 'Components/Settings/SubRowBackupMethods',
component: BackupMethodSubRow,
} as Meta;

const Template: StoryFn<BackupRowProps> = (args) => (
<BackupMethodSubRow {...args} />
);

export const AvailableMethod = Template.bind({});
AvailableMethod.args = {
title: 'Method available',
icon: (
<CheckmarkGreenIcon mode="enabled" className="grow-0 shrink-0 scale-50" />
),
message: (
<>
<CheckmarkGreenIcon className="grow-0 shrink-0 me-2" mode="enabled" />
<p>Method is available</p>
</>
),
ctaMessage: 'Use method',
onCtaClick: action('Use method clicked'),
isAvailable: true,
moreInfoMessage: 'This method is available for use.',
};

export const UnavailableMethod = Template.bind({});
UnavailableMethod.args = {
title: 'Method unavailable',
icon: <AlertIcon mode="attention" className="grow-0 shrink-0 scale-50" />,
message: (
<>
<AlertIcon className="grow-0 shrink-0 me-1" mode="attention" />
<p>Method is unavailable</p>
</>
),
ctaMessage: 'Enable method',
onCtaClick: action('Enable method clicked'),
isAvailable: false,
moreInfoMessage: 'This method is currently unavailable.',
};

export const BackupCodesAvailableStory: StoryFn = () => (
<BackupCodesAvailable
numCodesAvailable={5}
onCtaClick={action('Get new codes clicked')}
/>
);

export const BackupCodesUnavailableStory: StoryFn = () => (
<BackupCodesUnavailable onCtaClick={action('Add clicked')} />
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import BackupMethodSubRow, {
BackupCodesAvailable,
BackupCodesUnavailable,
} from './index';

describe('BackupMethodSubRow', () => {
const defaultProps = {
title: 'Backup codes',
icon: <div>Icon</div>,
message: <div>Message</div>,
ctaMessage: 'Get new codes',
onCtaClick: jest.fn(),
isAvailable: true,
moreInfoMessage: 'More info message',
};

it('renders correctly when available', () => {
render(<BackupMethodSubRow {...defaultProps} />);
expect(screen.getByText('Backup codes')).toBeInTheDocument();
expect(screen.getByText('Message')).toBeInTheDocument();
expect(screen.getByText('Get new codes')).toBeInTheDocument();
expect(screen.getByText('More info message')).toBeInTheDocument();
});

it('renders correctly when unavailable', () => {
render(<BackupMethodSubRow {...defaultProps} isAvailable={false} />);
expect(screen.getByText('Backup codes')).toBeInTheDocument();
expect(screen.getByText('Message')).toBeInTheDocument();
expect(screen.getByText('Get new codes')).toBeInTheDocument();
expect(screen.getByText('More info message')).toBeInTheDocument();
});

it('calls onCtaClick when CTA button is clicked', () => {
render(<BackupMethodSubRow {...defaultProps} />);
fireEvent.click(screen.getByText('Get new codes'));
expect(defaultProps.onCtaClick).toHaveBeenCalled();
});
});

describe('BackupCodesAvailable', () => {
const defaultProps = {
numCodesAvailable: 5,
onCtaClick: jest.fn(),
};

it('renders correctly', () => {
render(<BackupCodesAvailable {...defaultProps} />);
expect(screen.getByText('Backup codes')).toBeInTheDocument();
expect(screen.getByText('5 codes remaining')).toBeInTheDocument();
expect(screen.getByText('Get new codes')).toBeInTheDocument();
expect(
screen.getByText(
'This is the safest recovery method if you canʼt access your mobile device or authenticator app.'
)
).toBeInTheDocument();
});

it('calls onCtaClick when CTA button is clicked', () => {
render(<BackupCodesAvailable {...defaultProps} />);
fireEvent.click(screen.getByText('Get new codes'));
expect(defaultProps.onCtaClick).toHaveBeenCalled();
});
});

describe('BackupCodesUnavailable', () => {
const defaultProps = {
onCtaClick: jest.fn(),
};

it('renders correctly', () => {
render(<BackupCodesUnavailable {...defaultProps} />);
expect(screen.getByText('Backup codes')).toBeInTheDocument();
expect(screen.getByText('No codes available')).toBeInTheDocument();
expect(screen.getByText('Add')).toBeInTheDocument();
expect(
screen.getByText(
'This is the safest recovery method if you canʼt access your mobile device or authenticator app.'
)
).toBeInTheDocument();
});

it('calls onCtaClick when CTA button is clicked', () => {
render(<BackupCodesUnavailable {...defaultProps} />);
fireEvent.click(screen.getByText('Add'));
expect(defaultProps.onCtaClick).toHaveBeenCalled();
});
});
Loading

0 comments on commit 5cc246e

Please sign in to comment.