diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 554cbc1a6..45728a415 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -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.*" }, diff --git a/ui/components/app/app-components.scss b/ui/components/app/app-components.scss index b579de073..c868491ac 100644 --- a/ui/components/app/app-components.scss +++ b/ui/components/app/app-components.scss @@ -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'; diff --git a/ui/components/app/step-progress-bar/index.js b/ui/components/app/step-progress-bar/index.js new file mode 100644 index 000000000..8a02cf42f --- /dev/null +++ b/ui/components/app/step-progress-bar/index.js @@ -0,0 +1 @@ +export { default } from './step-progress-bar'; diff --git a/ui/components/app/step-progress-bar/index.scss b/ui/components/app/step-progress-bar/index.scss new file mode 100644 index 000000000..b2a35bef1 --- /dev/null +++ b/ui/components/app/step-progress-bar/index.scss @@ -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; +} diff --git a/ui/components/app/step-progress-bar/step-progress-bar.js b/ui/components/app/step-progress-bar/step-progress-bar.js new file mode 100644 index 000000000..72e80319c --- /dev/null +++ b/ui/components/app/step-progress-bar/step-progress-bar.js @@ -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 ( + + + + ); +} + +StepProgressBar.propTypes = { + stage: PropTypes.string, +}; diff --git a/ui/components/ui/chip/chip.scss b/ui/components/ui/chip/chip.scss index 51a176138..02c4df6a5 100644 --- a/ui/components/ui/chip/chip.scss +++ b/ui/components/ui/chip/chip.scss @@ -42,6 +42,8 @@ border: none; background: transparent; text-align: center; + width: 100%; + font-size: design-system.$font-size-h5; &:focus { text-align: left; diff --git a/ui/pages/onboarding-flow/recovery-phrase/confirm-recovery-phrase.js b/ui/pages/onboarding-flow/recovery-phrase/confirm-recovery-phrase.js new file mode 100644 index 000000000..52d3926e1 --- /dev/null +++ b/ui/pages/onboarding-flow/recovery-phrase/confirm-recovery-phrase.js @@ -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 ( +
+ + + + {t('seedPhraseConfirm')} + + + + + {t('seedPhraseEnterMissingWords')} + + + +
+ +
+
+ ); +} + +ConfirmRecoveryPhrase.propTypes = { + seedPhrase: PropTypes.string, +}; diff --git a/ui/pages/onboarding-flow/recovery-phrase/index.scss b/ui/pages/onboarding-flow/recovery-phrase/index.scss new file mode 100644 index 000000000..7c37d0059 --- /dev/null +++ b/ui/pages/onboarding-flow/recovery-phrase/index.scss @@ -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; + } + } +} diff --git a/ui/pages/onboarding-flow/recovery-phrase/recovery-phrase-chips.js b/ui/pages/onboarding-flow/recovery-phrase/recovery-phrase-chips.js new file mode 100644 index 000000000..c46193833 --- /dev/null +++ b/ui/pages/onboarding-flow/recovery-phrase/recovery-phrase-chips.js @@ -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 ( + +
+ {seedPhrase.map((word, index) => { + if ( + confirmPhase && + indicesToCheck && + indicesToCheck.includes(index) + ) { + return ( +
+
+ {`${index + 1}.`} +
+ { + setInputValue({ ...inputValue, [index]: value }); + }} + /> +
+ ); + } + return ( +
+
+ {`${index + 1}.`} +
+ + {word} + +
+ ); + })} +
+ + {hideSeedPhrase && ( +
+ + + {t('makeSureNoOneWatching')} + +
+ )} +
+ ); +} + +RecoveryPhraseChips.propTypes = { + seedPhrase: PropTypes.array, + seedPhraseRevealed: PropTypes.bool, + confirmPhase: PropTypes.bool, + setInputValue: PropTypes.func, + inputValue: PropTypes.string, + indicesToCheck: PropTypes.array, +}; diff --git a/ui/pages/onboarding-flow/recovery-phrase/review-recovery-phrase.js b/ui/pages/onboarding-flow/recovery-phrase/review-recovery-phrase.js new file mode 100644 index 000000000..78f8437dc --- /dev/null +++ b/ui/pages/onboarding-flow/recovery-phrase/review-recovery-phrase.js @@ -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 ( +
+ + + + {t('seedPhraseWriteDownHeader')} + + + + + {t('seedPhraseWriteDownDetails')} + + + + + {t('tips')}: + +
    +
  • + + {t('seedPhraseIntroSidebarBulletFour')} + +
  • +
  • + + {t('seedPhraseIntroSidebarBulletTwo')} + +
  • +
  • + + {t('seedPhraseIntroSidebarBulletThree')} + +
  • +
  • + + {t('seedPhraseIntroSidebarBulletFour')} + +
  • +
+
+ +
+ {seedPhraseRevealed ? ( +
+ + +
+ ) : ( + + )} +
+
+ ); +} + +RecoveryPhrase.propTypes = { + seedPhrase: PropTypes.string, +};