1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-10-22 19:26:13 +02:00

Add recovery-phrase views for new onboarding flow (#11434)

* add recovery-phrase views for new onboarding flow

* black => ui-black

* minor syntax improvement
This commit is contained in:
Alex Donesky 2021-07-07 19:10:12 -05:00 committed by GitHub
parent 39906d6124
commit 0ca0f1784e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 577 additions and 0 deletions

View File

@ -322,6 +322,9 @@
"confirmSecretBackupPhrase": {
"message": "Confirm your Secret Backup Phrase"
},
"confirmSeedPhrase": {
"message": "Confirm Seed Phrase"
},
"confirmed": {
"message": "Confirmed"
},
@ -1140,6 +1143,10 @@
"makeAnotherSwap": {
"message": "Create a new swap"
},
"makeSureNoOneWatching": {
"message": "Make sure no one is watching your screen",
"description": "Warning to users to be care while creating and saving their new seed phrase"
},
"max": {
"message": "Max"
},
@ -1696,12 +1703,21 @@
"secretPhrase": {
"message": "Enter your secret phrase here to restore your vault."
},
"secureWallet": {
"message": "Secure Wallet"
},
"securityAndPrivacy": {
"message": "Security & Privacy"
},
"securitySettingsDescription": {
"message": "Privacy settings and wallet Secret Recovery Phrase"
},
"seedPhraseConfirm": {
"message": "Confirm Secret Recovery Phrase"
},
"seedPhraseEnterMissingWords": {
"message": "Confirm Secret Recovery Phrase"
},
"seedPhraseIntroSidebarBulletFour": {
"message": "Write down and store in multiple secret places."
},
@ -1747,6 +1763,12 @@
"seedPhraseReq": {
"message": "Secret Recovery Phrases contain 12, 15, 18, 21, or 24 words"
},
"seedPhraseWriteDownDetails": {
"message": "Write down this 12-word Secret Recovery Phrase and save it in a place that you trust and only you can access."
},
"seedPhraseWriteDownHeader": {
"message": "Write down your Secret Recovery Phrase"
},
"selectAHigherGasFee": {
"message": "Select a higher gas fee to accelerate the processing of your transaction.*"
},

View File

@ -28,6 +28,7 @@
@import 'permissions-connect-footer/index';
@import 'permissions-connect-header/index';
@import 'recovery-phrase-reminder/index';
@import 'step-progress-bar/index.scss';
@import 'selected-account/index';
@import 'sidebars/index';
@import 'signature-request/index';

View File

@ -0,0 +1 @@
export { default } from './step-progress-bar';

View File

@ -0,0 +1,66 @@
.progressbar {
counter-reset: step;
display: flex;
justify-content: space-evenly;
}
.progressbar li {
list-style-type: none;
width: 25%;
float: left;
font-size: 12px;
position: relative;
text-align: center;
text-transform: uppercase;
color: #7d7d7d;
z-index: 2;
}
.progressbar li::before {
width: 30px;
height: 30px;
content: counter(step);
counter-increment: step;
line-height: 30px;
border: 2px solid #d6d9dc;
display: block;
text-align: center;
margin: 0 auto 10px auto;
border-radius: 50%;
background-color: white;
z-index: -1;
}
.progressbar li::after {
width: 100%;
height: 2px;
content: '';
position: absolute;
background-color: #d6d9dc;
top: 15px;
right: 62%;
z-index: -1;
}
.progressbar li:first-child::after {
content: none;
}
.progressbar li.active {
color: $primary-blue;
}
.progressbar li.active::before {
border-color: $primary-blue;
z-index: 1;
}
.progressbar li.active + li::after {
background-color: $primary-blue;
z-index: -1;
}
.progressbar li.complete::before {
background-color: $primary-blue;
color: $ui-white;
}

View File

@ -0,0 +1,50 @@
import React from 'react';
import classnames from 'classnames';
import PropTypes from 'prop-types';
import { useI18nContext } from '../../../hooks/useI18nContext';
import Box from '../../ui/box';
const stages = {
PASSWORD_CREATE: 1,
SEED_PHRASE_VIDEO: 2,
SEED_PHRASE_REVIEW: 3,
SEED_PHRASE_CONFIRM: 4,
ONBOARDING_COMPLETE: 5,
};
export default function StepProgressBar({ stage = 'PASSWORD_CREATE' }) {
const t = useI18nContext();
return (
<Box margin={4}>
<ul className="progressbar">
<li
className={classnames({
active: stages[stage] >= 1,
complete: stages[stage] >= 1,
})}
>
{t('createPassword')}
</li>
<li
className={classnames({
active: stages[stage] >= 2,
complete: stages[stage] >= 3,
})}
>
{t('secureWallet')}
</li>
<li
className={classnames({
active: stages[stage] >= 4,
complete: stages[stage] >= 5,
})}
>
{t('confirmSeedPhrase')}
</li>
</ul>
</Box>
);
}
StepProgressBar.propTypes = {
stage: PropTypes.string,
};

View File

@ -42,6 +42,8 @@
border: none;
background: transparent;
text-align: center;
width: 100%;
font-size: design-system.$font-size-h5;
&:focus {
text-align: left;

View File

@ -0,0 +1,100 @@
import React, { useState, useMemo } from 'react';
import { useHistory } from 'react-router-dom';
import { debounce } from 'lodash';
import PropTypes from 'prop-types';
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 { INITIALIZE_END_OF_FLOW_ROUTE } from '../../../helpers/constants/routes';
import ProgressBar from '../../../components/app/step-progress-bar';
import { useI18nContext } from '../../../hooks/useI18nContext';
import RecoveryPhraseChips from './recovery-phrase-chips';
export default function ConfirmRecoveryPhrase({ seedPhrase = '' }) {
const history = useHistory();
const t = useI18nContext();
const splitSeedPhrase = seedPhrase.split(' ');
const indicesToCheck = [2, 3, 7];
const [matching, setMatching] = useState(false);
// Removes seed phrase words from chips corresponding to the
// indicesToCheck so that user has to complete the phrase and confirm
// they have saved it.
const initializePhraseElements = () => {
const phraseElements = { ...splitSeedPhrase };
indicesToCheck.forEach((i) => {
phraseElements[i] = '';
});
return phraseElements;
};
const [phraseElements, setPhraseElements] = useState(
initializePhraseElements(),
);
const validate = useMemo(
() =>
debounce((elements) => {
setMatching(Object.values(elements).join(' ') === seedPhrase);
}, 500),
[setMatching, seedPhrase],
);
const handleSetPhraseElements = (values) => {
setPhraseElements(values);
validate(values);
};
return (
<div>
<ProgressBar stage="SEED_PHRASE_CONFIRM" />
<Box
justifyContent={JUSTIFY_CONTENT.CENTER}
textAlign={TEXT_ALIGN.CENTER}
marginBottom={4}
>
<Typography variant={TYPOGRAPHY.H2} fontWeight={FONT_WEIGHT.BOLD}>
{t('seedPhraseConfirm')}
</Typography>
</Box>
<Box
justifyContent={JUSTIFY_CONTENT.CENTER}
textAlign={TEXT_ALIGN.CENTER}
marginBottom={4}
>
<Typography variant={TYPOGRAPHY.H4}>
{t('seedPhraseEnterMissingWords')}
</Typography>
</Box>
<RecoveryPhraseChips
seedPhrase={splitSeedPhrase}
confirmPhase
setInputValue={handleSetPhraseElements}
inputValue={phraseElements}
indicesToCheck={indicesToCheck}
/>
<div className="recovery-phrase__footer">
<Button
rounded
type="primary"
className="recovery-phrase__footer--button"
onClick={() => {
history.push(INITIALIZE_END_OF_FLOW_ROUTE);
}}
disabled={!matching}
>
{t('confirm')}
</Button>
</div>
</div>
);
}
ConfirmRecoveryPhrase.propTypes = {
seedPhrase: PropTypes.string,
};

View File

@ -0,0 +1,110 @@
.recovery-phrase {
&__tips {
flex-direction: column;
ul {
list-style: disc;
margin-left: 20px;
}
}
&__chips {
display: grid;
grid-template-columns: 160px 160px 160px;
justify-items: center;
align-items: center;
row-gap: 16px;
&--hidden {
filter: blur(5px);
}
}
&__secret {
position: relative;
}
&__secret-blocker {
position: absolute;
top: 0;
bottom: 0;
height: 100%;
width: 100%;
background-color: rgba(0, 0, 0, 0.6);
display: flex;
flex-flow: column nowrap;
align-items: center;
justify-content: center;
padding: 8px 0 18px;
border-radius: 4px;
color: $ui-white;
&--text {
margin-top: 32px;
}
}
&__chip-item {
display: flex;
flex-direction: row;
align-items: center;
text-align: center;
&__number {
font-size: $font-size-h5;
}
}
&__footer {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
&--button {
width: 50%;
padding: 20px;
}
&--copy {
width: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
&--button {
background-color: transparent;
border: none;
display: flex;
justify-content: space-evenly;
width: 40%;
color: $primary-blue;
cursor: pointer;
margin-bottom: 24px;
&:active {
color: $ui-black;
background-color: transparent;
border: none;
transform: scale(0.97);
}
}
}
}
&__chip {
justify-content: center;
border-radius: 13px;
height: 32px;
width: 120px;
&--with-input {
width: 120px;
box-shadow: 0 3px 4px -3px $Grey-800;
border-width: 2px;
border-radius: 13px;
height: 32px;
}
}
}

View File

@ -0,0 +1,101 @@
import React from 'react';
import classnames from 'classnames';
import PropTypes from 'prop-types';
import Chip from '../../../components/ui/chip';
import Box from '../../../components/ui/box';
import Typography from '../../../components/ui/typography';
import { ChipWithInput } from '../../../components/ui/chip/chip-with-input';
import { useI18nContext } from '../../../hooks/useI18nContext';
import {
TYPOGRAPHY,
COLORS,
BORDER_STYLE,
SIZES,
DISPLAY,
} from '../../../helpers/constants/design-system';
export default function RecoveryPhraseChips({
seedPhrase,
seedPhraseRevealed,
confirmPhase,
setInputValue,
inputValue,
indicesToCheck,
}) {
const t = useI18nContext();
const hideSeedPhrase = seedPhraseRevealed === false;
return (
<Box
borderColor={COLORS.UI2}
borderStyle={BORDER_STYLE.SOLID}
padding={4}
borderWidth={1}
borderRadius={SIZES.MD}
display={DISPLAY.GRID}
marginBottom={4}
className="recovery-phrase__secret"
>
<div
className={classnames('recovery-phrase__chips', {
'recovery-phrase__chips--hidden': hideSeedPhrase,
})}
>
{seedPhrase.map((word, index) => {
if (
confirmPhase &&
indicesToCheck &&
indicesToCheck.includes(index)
) {
return (
<div className="recovery-phrase__chip-item" key={index}>
<div className="recovery-phrase__chip-item__number">
{`${index + 1}.`}
</div>
<ChipWithInput
borderColor={COLORS.PRIMARY1}
className="recovery-phrase__chip--with-input"
inputValue={inputValue[index]}
setInputValue={(value) => {
setInputValue({ ...inputValue, [index]: value });
}}
/>
</div>
);
}
return (
<div className="recovery-phrase__chip-item" key={index}>
<div className="recovery-phrase__chip-item__number">
{`${index + 1}.`}
</div>
<Chip className="recovery-phrase__chip" borderColor={COLORS.UI3}>
{word}
</Chip>
</div>
);
})}
</div>
{hideSeedPhrase && (
<div className="recovery-phrase__secret-blocker">
<i className="far fa-eye-slash" color="white" />
<Typography
variant={TYPOGRAPHY.H6}
color={COLORS.WHITE}
className="recovery-phrase__secret-blocker--text"
>
{t('makeSureNoOneWatching')}
</Typography>
</div>
)}
</Box>
);
}
RecoveryPhraseChips.propTypes = {
seedPhrase: PropTypes.array,
seedPhraseRevealed: PropTypes.bool,
confirmPhase: PropTypes.bool,
setInputValue: PropTypes.func,
inputValue: PropTypes.string,
indicesToCheck: PropTypes.array,
};

View File

@ -0,0 +1,124 @@
import React, { useState } from 'react';
import { useHistory } from 'react-router-dom';
import PropTypes from 'prop-types';
import Box from '../../../components/ui/box';
import Button from '../../../components/ui/button';
import Typography from '../../../components/ui/typography';
import Copy from '../../../components/ui/icon/copy-icon.component';
import { useCopyToClipboard } from '../../../hooks/useCopyToClipboard';
import { useI18nContext } from '../../../hooks/useI18nContext';
import { INITIALIZE_CONFIRM_SEED_PHRASE_ROUTE } from '../../../helpers/constants/routes';
import {
TEXT_ALIGN,
TYPOGRAPHY,
JUSTIFY_CONTENT,
FONT_WEIGHT,
} from '../../../helpers/constants/design-system';
import ProgressBar from '../../../components/app/step-progress-bar';
import RecoveryPhraseChips from './recovery-phrase-chips';
export default function RecoveryPhrase({ seedPhrase }) {
const history = useHistory();
const t = useI18nContext();
const [copied, handleCopy] = useCopyToClipboard();
const [seedPhraseRevealed, setSeedPhraseRevealed] = useState(false);
return (
<div>
<ProgressBar stage="SEED_PHRASE_REVIEW" />
<Box
justifyContent={JUSTIFY_CONTENT.CENTER}
textAlign={TEXT_ALIGN.CENTER}
marginBottom={4}
>
<Typography variant={TYPOGRAPHY.H2} fontWeight={FONT_WEIGHT.BOLD}>
{t('seedPhraseWriteDownHeader')}
</Typography>
</Box>
<Box
justifyContent={JUSTIFY_CONTENT.CENTER}
textAlign={TEXT_ALIGN.CENTER}
marginBottom={4}
>
<Typography variant={TYPOGRAPHY.H4}>
{t('seedPhraseWriteDownDetails')}
</Typography>
</Box>
<Box
justifyContent={JUSTIFY_CONTENT.SPACE_EVENLY}
textAlign={TEXT_ALIGN.LEFT}
marginBottom={4}
className="recovery-phrase__tips"
>
<Typography variant={TYPOGRAPHY.H4} fontWeight={FONT_WEIGHT.BOLD}>
{t('tips')}:
</Typography>
<ul>
<li>
<Typography variant={TYPOGRAPHY.H4}>
{t('seedPhraseIntroSidebarBulletFour')}
</Typography>
</li>
<li>
<Typography variant={TYPOGRAPHY.H4}>
{t('seedPhraseIntroSidebarBulletTwo')}
</Typography>
</li>
<li>
<Typography variant={TYPOGRAPHY.H4}>
{t('seedPhraseIntroSidebarBulletThree')}
</Typography>
</li>
<li>
<Typography variant={TYPOGRAPHY.H4}>
{t('seedPhraseIntroSidebarBulletFour')}
</Typography>
</li>
</ul>
</Box>
<RecoveryPhraseChips
seedPhrase={seedPhrase.split(' ')}
seedPhraseRevealed={seedPhraseRevealed}
/>
<div className="recovery-phrase__footer">
{seedPhraseRevealed ? (
<div className="recovery-phrase__footer--copy">
<Button
onClick={() => {
handleCopy(seedPhrase);
}}
icon={copied ? null : <Copy size={20} color="#3098DC" />}
className="recovery-phrase__footer--copy--button"
>
{copied ? t('copiedExclamation') : t('copyToClipboard')}
</Button>
<Button
rounded
type="primary"
className="recovery-phrase__footer--button"
onClick={() => {
history.push(INITIALIZE_CONFIRM_SEED_PHRASE_ROUTE);
}}
>
{t('next')}
</Button>
</div>
) : (
<Button
rounded
type="primary"
className="recovery-phrase__footer--button"
onClick={() => {
setSeedPhraseRevealed(true);
}}
>
{t('revealSeedWords')}
</Button>
)}
</div>
</div>
);
}
RecoveryPhrase.propTypes = {
seedPhrase: PropTypes.string,
};