1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-11-22 09:57:02 +01:00

Onboarding V2 Secure Your Wallet view (#12208)

* secure-your-wallet onboarding view
This commit is contained in:
Alex Donesky 2021-10-06 13:52:25 -05:00 committed by GitHub
parent fc41321470
commit 614228cba7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 463 additions and 34 deletions

View File

@ -1042,6 +1042,9 @@
"getStarted": {
"message": "Get Started"
},
"goBack": {
"message": "Go Back"
},
"goerli": {
"message": "Goerli Test Network"
},
@ -1900,6 +1903,12 @@
"seedPhraseEnterMissingWords": {
"message": "Confirm Secret Recovery Phrase"
},
"seedPhraseIntroNotRecommendedButtonCopy": {
"message": "Remind me later (not recommended)"
},
"seedPhraseIntroRecommendedButtonCopy": {
"message": "Secure my wallet (recommended)"
},
"seedPhraseIntroSidebarBulletFour": {
"message": "Write down and store in multiple secret places."
},
@ -1913,13 +1922,13 @@
"message": "Store in a bank vault."
},
"seedPhraseIntroSidebarCopyOne": {
"message": "Your Secret Recovery Phrase is the “master key” to your wallet and funds."
"message": "Your Secret Recovery Phrase is a 12-word phrase that is the “master key” to your wallet and your funds"
},
"seedPhraseIntroSidebarCopyThree": {
"message": "If someone asks for your Secret Recovery Phrase, they are most likely trying to scam you."
"message": "If someone asks for your recovery phrase they are likely trying to scam you and steal your wallet funds"
},
"seedPhraseIntroSidebarCopyTwo": {
"message": "Never, ever share your Secret Recovery Phrase, even with MetaMask!"
"message": "Never, ever share your Secret Recovery Phrase, not even with MetaMask!"
},
"seedPhraseIntroSidebarTitleOne": {
"message": "What is a Secret Recovery Phrase?"
@ -2071,6 +2080,15 @@
"signed": {
"message": "Signed"
},
"skip": {
"message": "Skip"
},
"skipAccountSecurity": {
"message": "Skip Account Security?"
},
"skipAccountSecurityDetails": {
"message": "I understand that until I back up my Secret Recovery Phrase, I may lose my accounts and all of their assets."
},
"slow": {
"message": "Slow"
},

BIN
app/images/warning-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -14,7 +14,7 @@ const stages = {
export default function StepProgressBar({ stage = 'PASSWORD_CREATE' }) {
const t = useI18nContext();
return (
<Box margin={4}>
<Box>
<ul className="progressbar">
<li
className={classnames({

View File

@ -10,6 +10,7 @@ import {
JUSTIFY_CONTENT,
SIZES,
TEXT_ALIGN,
FLEX_DIRECTION,
} from '../../../helpers/constants/design-system';
const ValidSize = PropTypes.oneOf([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]);
@ -72,6 +73,7 @@ export default function Box({
alignItems,
justifyContent,
textAlign,
flexDirection = FLEX_DIRECTION.ROW,
display,
width,
height,
@ -115,6 +117,7 @@ export default function Box({
!display && (Boolean(justifyContent) || Boolean(alignItems)),
[`box--justify-content-${justifyContent}`]: Boolean(justifyContent),
[`box--align-items-${alignItems}`]: Boolean(alignItems),
[`box--flex-direction-${flexDirection}`]: Boolean(flexDirection),
// text align
[`box--text-align-${textAlign}`]: Boolean(textAlign),
// display
@ -132,6 +135,7 @@ export default function Box({
Box.propTypes = {
children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
flexDirection: PropTypes.oneOf(Object.values(FLEX_DIRECTION)),
margin: MultipleSizes,
marginTop: ValidSize,
marginBottom: ValidSize,

View File

@ -84,6 +84,12 @@ $attributes: padding, margin;
}
}
@each $direction in design-system.$flex-direction {
&--flex-direction-#{$direction} {
flex-direction: $direction;
}
}
// Width and Height
&--width-full {
width: 100%;

View File

@ -10,7 +10,15 @@ const CHECKBOX_STATE = {
export const { CHECKED, INDETERMINATE, UNCHECKED } = CHECKBOX_STATE;
const CheckBox = ({ className, disabled, id, onClick, checked, title }) => {
const CheckBox = ({
className,
disabled,
id,
onClick,
checked,
title,
dataTestId,
}) => {
if (typeof checked === 'boolean') {
// eslint-disable-next-line no-param-reassign
checked = checked ? CHECKBOX_STATE.CHECKED : CHECKBOX_STATE.UNCHECKED;
@ -43,6 +51,7 @@ const CheckBox = ({ className, disabled, id, onClick, checked, title }) => {
readOnly
ref={ref}
title={title}
data-testid={dataTestId}
type="checkbox"
/>
);
@ -56,6 +65,7 @@ CheckBox.propTypes = {
checked: PropTypes.oneOf([...Object.keys(CHECKBOX_STATE), true, false])
.isRequired,
title: PropTypes.string,
dataTestId: PropTypes.string,
};
CheckBox.defaultProps = {

View File

@ -20,6 +20,7 @@ const Popover = ({
centerTitle,
}) => {
const t = useI18nContext();
const showHeader = title || onBack || subtitle || onClose;
return (
<div className="popover-container">
{CustomBackground ? (
@ -32,36 +33,38 @@ const Popover = ({
ref={popoverRef}
>
{showArrow ? <div className="popover-arrow" /> : null}
<header className="popover-header">
<div
className={classnames(
'popover-header__title',
centerTitle ? 'center' : '',
)}
>
<h2 title={title}>
{onBack ? (
{showHeader && (
<header className="popover-header">
<div
className={classnames(
'popover-header__title',
centerTitle ? 'center' : '',
)}
>
<h2 title={title}>
{onBack ? (
<button
className="fas fa-chevron-left popover-header__button"
title={t('back')}
onClick={onBack}
/>
) : null}
{title}
</h2>
{onClose ? (
<button
className="fas fa-chevron-left popover-header__button"
title={t('back')}
onClick={onBack}
className="fas fa-times popover-header__button"
title={t('close')}
data-testid="popover-close"
onClick={onClose}
/>
) : null}
{title}
</h2>
{onClose ? (
<button
className="fas fa-times popover-header__button"
title={t('close')}
data-testid="popover-close"
onClick={onClose}
/>
</div>
{subtitle ? (
<p className="popover-header__subtitle">{subtitle}</p>
) : null}
</div>
{subtitle ? (
<p className="popover-header__subtitle">{subtitle}</p>
) : null}
</header>
</header>
)}
{children ? (
<div className={classnames('popover-content', contentClassName)}>
{children}
@ -78,7 +81,7 @@ const Popover = ({
};
Popover.propTypes = {
title: PropTypes.string.isRequired,
title: PropTypes.string,
subtitle: PropTypes.string,
children: PropTypes.node,
footer: PropTypes.node,

View File

@ -13,6 +13,12 @@ $justify-content:
space-between,
space-evenly;
$flex-direction:
row,
row-reverse,
column,
column-reverse;
$fractions: (
1\/2: 50%,
1\/3: 33.333333%,

View File

@ -88,6 +88,13 @@ export const JUSTIFY_CONTENT = {
SPACE_EVENLY: 'space-evenly',
};
export const FLEX_DIRECTION = {
ROW: 'row',
ROW_REVERSE: 'row-reverse',
COLUMN: 'column',
COLUMN_REVERSE: 'column-reverse',
};
export const DISPLAY = {
BLOCK: 'block',
FLEX: 'flex',

View File

@ -1,6 +1,7 @@
@import 'recovery-phrase/index';
@import 'new-account/index';
@import 'onboarding-app-header/index';
@import 'secure-your-wallet/index';
.onboarding-flow {
width: 100%;

View File

@ -8,6 +8,7 @@ import {
ONBOARDING_CONFIRM_SRP_ROUTE,
ONBOARDING_UNLOCK_ROUTE,
DEFAULT_ROUTE,
ONBOARDING_SECURE_YOUR_WALLET_ROUTE,
} from '../../helpers/constants/routes';
import {
getCompletedOnboarding,
@ -23,6 +24,7 @@ import { getFirstTimeFlowTypeRoute } from '../../selectors';
import OnboardingFlowSwitch from './onboarding-flow-switch/onboarding-flow-switch';
import NewAccount from './new-account/new-account';
import ReviewRecoveryPhrase from './recovery-phrase/review-recovery-phrase';
import SecureYourWallet from './secure-your-wallet/secure-your-wallet';
import ConfirmRecoveryPhrase from './recovery-phrase/confirm-recovery-phrase';
export default function OnboardingFlow() {
@ -39,7 +41,7 @@ export default function OnboardingFlow() {
// For ONBOARDING_V2 dev purposes,
// Remove when ONBOARDING_V2 dev complete
if (process.env.ONBOARDING_V2) {
history.push(ONBOARDING_REVIEW_SRP_ROUTE);
history.push(ONBOARDING_SECURE_YOUR_WALLET_ROUTE);
return;
}
@ -87,6 +89,11 @@ export default function OnboardingFlow() {
/>
)}
/>
<Route
exact
path={ONBOARDING_SECURE_YOUR_WALLET_ROUTE}
component={SecureYourWallet}
/>
<Route
path={ONBOARDING_REVIEW_SRP_ROUTE}
render={() => <ReviewRecoveryPhrase seedPhrase={seedPhrase} />}

View File

@ -0,0 +1,47 @@
.secure-your-wallet {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
max-width: 1000px;
max-height: 1300px;
&__details {
max-width: 550px;
}
&__actions {
button {
margin: 16px;
}
}
&__list {
list-style: disc inside;
li {
font-size: 18px;
}
}
&__highlighted {
background-color: $primary-2;
padding: 12px;
border-radius: 10px;
}
}
.skip-srp-backup-popover {
width: 365px;
&__checkbox {
margin: 8px 12px 0 0;
}
&__footer {
button {
width: 140px;
margin: 0 10px;
}
}
}

View File

@ -0,0 +1,167 @@
import React, { useState } from 'react';
import { useHistory } from 'react-router-dom';
import { useSelector } from 'react-redux';
import Box from '../../../components/ui/box';
import Button from '../../../components/ui/button';
import Typography from '../../../components/ui/typography';
import {
TEXT_ALIGN,
TYPOGRAPHY,
JUSTIFY_CONTENT,
FONT_WEIGHT,
} from '../../../helpers/constants/design-system';
import ProgressBar from '../../../components/app/step-progress-bar';
import { useI18nContext } from '../../../hooks/useI18nContext';
import { ONBOARDING_REVIEW_SRP_ROUTE } from '../../../helpers/constants/routes';
import { getCurrentLocale } from '../../../ducks/metamask/metamask';
import SkipSRPBackup from './skip-srp-backup-popover';
export default function SecureYourWallet() {
const history = useHistory();
const t = useI18nContext();
const currentLocale = useSelector(getCurrentLocale);
const [showSkipSRPBackupPopover, setShowSkipSRPBackupPopover] = useState(
false,
);
const handleClickRecommended = () => {
history.push(ONBOARDING_REVIEW_SRP_ROUTE);
};
const handleClickNotRecommended = () => {
setShowSkipSRPBackupPopover(true);
};
const subtitles = {
en: 'English',
es: 'Spanish',
hi: 'Hindi',
id: 'Indonesian',
ja: 'Japanese',
ko: 'Korean',
pt: 'Portuguese',
ru: 'Russian',
tl: 'Tagalog',
vi: 'Vietnamese',
};
const defaultLang = subtitles[currentLocale] ? currentLocale : 'en';
return (
<div className="secure-your-wallet">
{showSkipSRPBackupPopover && (
<SkipSRPBackup handleClose={() => setShowSkipSRPBackupPopover(false)} />
)}
<ProgressBar stage="SEED_PHRASE_VIDEO" />
<Box
justifyContent={JUSTIFY_CONTENT.CENTER}
textAlign={TEXT_ALIGN.CENTER}
marginBottom={4}
>
<Typography variant={TYPOGRAPHY.H2} fontWeight={FONT_WEIGHT.BOLD}>
{t('seedPhraseIntroTitle')}
</Typography>
</Box>
<Box
justifyContent={JUSTIFY_CONTENT.CENTER}
textAlign={TEXT_ALIGN.CENTER}
marginBottom={6}
>
<Typography
variant={TYPOGRAPHY.H4}
className="secure-your-wallet__details"
>
{t('seedPhraseIntroTitleCopy')}
</Typography>
</Box>
<Box>
<video controls style={{ borderRadius: '10px' }}>
<source
type="video/webm"
src="./images/videos/recovery-onboarding/video.webm"
/>
{Object.keys(subtitles).map((key) => {
return (
<track
default={Boolean(key === defaultLang)}
srcLang={key}
label={subtitles[key]}
key={`${key}-subtitles`}
kind="subtitles"
src={`./images/videos/recovery-onboarding/subtitles/${key}.vtt`}
/>
);
})}
</video>
</Box>
<Box
margin={8}
width="10/12"
justifyContent={JUSTIFY_CONTENT.SPACE_BETWEEN}
className="secure-your-wallet__actions"
>
<Button
type="secondary"
rounded
large
onClick={handleClickNotRecommended}
>
{t('seedPhraseIntroNotRecommendedButtonCopy')}
</Button>
<Button type="primary" rounded large onClick={handleClickRecommended}>
{t('seedPhraseIntroRecommendedButtonCopy')}
</Button>
</Box>
<Box marginBottom={4} textAlign={TEXT_ALIGN.CENTER}>
<Typography
tag="span"
variant={TYPOGRAPHY.H4}
fontWeight={FONT_WEIGHT.BOLD}
boxProps={{ display: 'block' }}
>
{t('seedPhraseIntroSidebarTitleOne')}
</Typography>
<Typography tag="span" variant={TYPOGRAPHY.H4}>
{t('seedPhraseIntroSidebarCopyOne')}
</Typography>
</Box>
<Box marginBottom={4} textAlign={TEXT_ALIGN.CENTER}>
<Typography
tag="span"
variant={TYPOGRAPHY.H4}
fontWeight={FONT_WEIGHT.BOLD}
boxProps={{ display: 'block' }}
>
{t('seedPhraseIntroSidebarTitleTwo')}
</Typography>
<ul className="secure-your-wallet__list">
<li>{t('seedPhraseIntroSidebarBulletOne')}</li>
<li>{t('seedPhraseIntroSidebarBulletTwo')}</li>
<li>{t('seedPhraseIntroSidebarBulletThree')}</li>
<li>{t('seedPhraseIntroSidebarBulletFour')}</li>
</ul>
</Box>
<Box marginBottom={6} textAlign={TEXT_ALIGN.CENTER}>
<Typography
tag="span"
variant={TYPOGRAPHY.H4}
fontWeight={FONT_WEIGHT.BOLD}
boxProps={{ display: 'block' }}
>
{t('seedPhraseIntroSidebarTitleThree')}
</Typography>
<Typography tag="span" variant={TYPOGRAPHY.H4}>
{t('seedPhraseIntroSidebarCopyTwo')}
</Typography>
</Box>
<Box
className="secure-your-wallet__highlighted"
marginBottom={2}
textAlign={TEXT_ALIGN.CENTER}
>
<Typography tag="span" variant={TYPOGRAPHY.H4}>
{t('seedPhraseIntroSidebarCopyThree')}
</Typography>
</Box>
</div>
);
}

View File

@ -0,0 +1,15 @@
import React from 'react';
import SecureYourWallet from './secure-your-wallet';
export default {
title: 'Onboarding - Secure Your Wallet',
id: __filename,
};
export const Base = () => {
return (
<div style={{ maxHeight: '2000px' }}>
<SecureYourWallet />
</div>
);
};

View File

@ -0,0 +1,59 @@
import React from 'react';
import { fireEvent } from '@testing-library/react';
import configureMockStore from 'redux-mock-store';
import reactRouterDom from 'react-router-dom';
import { renderWithProvider } from '../../../../test/lib/render-helpers';
import { ONBOARDING_COMPLETION_ROUTE } from '../../../helpers/constants/routes';
import SecureYourWallet from './secure-your-wallet';
describe('Secure Your Wallet Onboarding View', () => {
const useHistoryOriginal = reactRouterDom.useHistory;
const pushMock = jest.fn();
beforeAll(() => {
jest
.spyOn(reactRouterDom, 'useHistory')
.mockImplementation()
.mockReturnValue({ push: pushMock });
});
afterAll(() => {
reactRouterDom.useHistory = useHistoryOriginal;
});
const mockStore = {
metamask: {
provider: {
type: 'test',
},
},
};
const store = configureMockStore()(mockStore);
it('should show a popover asking the user if they want to skip account security if they click "Remind me later"', () => {
const { queryAllByText, getByText } = renderWithProvider(
<SecureYourWallet />,
store,
);
const remindMeLaterButton = getByText('Remind me later (not recommended)');
expect(queryAllByText('Skip Account Security?')).toHaveLength(0);
fireEvent.click(remindMeLaterButton);
expect(queryAllByText('Skip Account Security?')).toHaveLength(1);
});
it('should not be able to click "skip" until "Skip Account Security" terms are agreed to', () => {
const { getByText, getByTestId } = renderWithProvider(
<SecureYourWallet />,
store,
);
const remindMeLaterButton = getByText('Remind me later (not recommended)');
fireEvent.click(remindMeLaterButton);
const skipButton = getByText('Skip');
fireEvent.click(skipButton);
expect(pushMock).toHaveBeenCalledTimes(0);
const checkbox = getByTestId('skip-srp-backup-popover-checkbox');
fireEvent.click(checkbox);
fireEvent.click(skipButton);
expect(pushMock).toHaveBeenCalledTimes(1);
expect(pushMock).toHaveBeenCalledWith(ONBOARDING_COMPLETION_ROUTE);
});
});

View File

@ -0,0 +1,79 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { useHistory } from 'react-router-dom';
import { useI18nContext } from '../../../hooks/useI18nContext';
import Button from '../../../components/ui/button';
import Popover from '../../../components/ui/popover';
import Box from '../../../components/ui/box';
import Typography from '../../../components/ui/typography';
import {
ALIGN_ITEMS,
FLEX_DIRECTION,
FONT_WEIGHT,
JUSTIFY_CONTENT,
TYPOGRAPHY,
} from '../../../helpers/constants/design-system';
import Checkbox from '../../../components/ui/check-box';
import { ONBOARDING_COMPLETION_ROUTE } from '../../../helpers/constants/routes';
export default function SkipSRPBackup({ handleClose }) {
const [checked, setChecked] = useState(false);
const t = useI18nContext();
const history = useHistory();
return (
<Popover
className="skip-srp-backup-popover"
footer={
<Box
className="skip-srp-backup-popover__footer"
justifyContent={JUSTIFY_CONTENT.CENTER}
alignItems={ALIGN_ITEMS.CENTER}
>
<Button onClick={handleClose} type="secondary" rounded>
{t('goBack')}
</Button>
<Button
disabled={!checked}
type="primary"
rounded
onClick={() => history.push(ONBOARDING_COMPLETION_ROUTE)}
>
{t('skip')}
</Button>
</Box>
}
>
<Box
flexDirection={FLEX_DIRECTION.COLUMN}
alignItems={ALIGN_ITEMS.CENTER}
justifyContent={JUSTIFY_CONTENT.CENTER}
margin={4}
>
<img src="./images/warning-icon.png" />
<Typography variant={TYPOGRAPHY.h3} fontWeight={FONT_WEIGHT.BOLD}>
{t('skipAccountSecurity')}
</Typography>
<Box justifyContent={JUSTIFY_CONTENT.CENTER} margin={3}>
<Checkbox
className="skip-srp-backup-popover__checkbox"
onClick={() => {
setChecked(!checked);
}}
checked={checked}
dataTestId="skip-srp-backup-popover-checkbox"
/>
<Typography
className="skip-srp-backup-popover__details"
variant={TYPOGRAPHY.h7}
>
{t('skipAccountSecurityDetails')}
</Typography>
</Box>
</Box>
</Popover>
);
}
SkipSRPBackup.propTypes = {
handleClose: PropTypes.func,
};

View File

@ -40,7 +40,6 @@ import {
CONFIRM_TRANSACTION_ROUTE,
CONNECT_ROUTE,
DEFAULT_ROUTE,
INITIALIZE_ROUTE,
INITIALIZE_UNLOCK_ROUTE,
LOCK_ROUTE,
MOBILE_SYNC_ROUTE,
@ -54,6 +53,7 @@ import {
BUILD_QUOTE_ROUTE,
CONFIRMATION_V_NEXT_ROUTE,
CONFIRM_IMPORT_TOKEN_ROUTE,
INITIALIZE_ROUTE,
ONBOARDING_ROUTE,
} from '../../helpers/constants/routes';