diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index a593774a3..f98bd83a4 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -1524,6 +1524,42 @@ "securitySettingsDescription": { "message": "Privacy settings and wallet seed phrase" }, + "seedPhraseIntroSidebarBulletFour": { + "message": "Write down and store in multiple secret places." + }, + "seedPhraseIntroSidebarBulletOne": { + "message": "Save in a password manager" + }, + "seedPhraseIntroSidebarBulletThree": { + "message": "Store in a safe-deposit box." + }, + "seedPhraseIntroSidebarBulletTwo": { + "message": "Store in a bank vault." + }, + "seedPhraseIntroSidebarCopyOne": { + "message": "Your recovery phrase is the “master key” to your wallet and funds." + }, + "seedPhraseIntroSidebarCopyThree": { + "message": "If someone asks for your recovery phrase, they are most likely trying to scam you." + }, + "seedPhraseIntroSidebarCopyTwo": { + "message": "Never, ever share your recovery phrase, even with MetaMask!" + }, + "seedPhraseIntroSidebarTitleOne": { + "message": "What is a recovery phrase?" + }, + "seedPhraseIntroSidebarTitleThree": { + "message": "Should I share my recovery phrase?" + }, + "seedPhraseIntroSidebarTitleTwo": { + "message": "How do I save my recovery phrase?" + }, + "seedPhraseIntroTitle": { + "message": "Secure your wallet" + }, + "seedPhraseIntroTitleCopy": { + "message": "Before getting started, watch this short video to learn about your recovery phrase and how to keep your wallet safe." + }, "seedPhrasePlaceholder": { "message": "Separate each word with a single space" }, diff --git a/app/images/videos/recovery-onboarding/subtitles-en.vtt b/app/images/videos/recovery-onboarding/subtitles-en.vtt new file mode 100644 index 000000000..c78e8ff15 --- /dev/null +++ b/app/images/videos/recovery-onboarding/subtitles-en.vtt @@ -0,0 +1,116 @@ +WEBVTT + +1 +00:00:00.780 --> 00:00:04.580 +MetaMask is a new way to connect +to sites and applications. + +2 +00:00:04.580 --> 00:00:08.860 +On traditional websites, a central database +or bank is responsible for controlling and + +3 +00:00:08.860 --> 00:00:10.179 +recovering your accounts. + +4 +00:00:10.179 --> 00:00:15.050 +But on MetaMask, all of the power belongs +to the holder of a master key. + +5 +00:00:15.050 --> 00:00:18.460 +Whoever holds the key, controls the accounts. + +6 +00:00:18.460 --> 00:00:21.110 +Your secret recovery phrase +is your "master key". + +7 +00:00:21.110 --> 00:00:26.070 +It's a series of 12 words that are generated +when you first set up MetaMask, which allow + +8 +00:00:26.070 --> 00:00:30.120 +you to recover your wallet and funds if you +ever lose access. + +9 +00:00:30.120 --> 00:00:33.451 +It's important that you secure +your wallet by keeping your + +10 +00:00:33.451 --> 00:00:37.510 +secret recovery phrase +very safe, and very secret. + +11 +00:00:37.510 --> 00:00:41.429 +If anyone gets access to it, they will have +the "master key" to your wallet and can + +12 +00:00:41.429 --> 00:00:45.190 +freely access and take all of your funds. + +13 +00:00:45.190 --> 00:00:50.109 +To secure your MetaMask wallet you'll want +to safely save your secret recovery phrase. + +14 +00:00:50.109 --> 00:00:54.930 +You can write it down, hide it somewhere, +put it in a safe deposit box + +15 +00:00:54.930 --> 00:00:57.729 +or use a secure password manager. + +16 +00:00:57.729 --> 00:01:01.050 +Some users even engrave their +phrase onto a metal plate! + +17 +00:01:01.050 --> 00:01:04.440 +Nobody, not even the team +at MetaMask, can help you + +18 +00:01:04.440 --> 00:01:07.820 +recover your wallet if you lose +your secret recovery phrase. + +19 +00:01:07.820 --> 00:01:12.072 +If you haven't written down your secret recovery +phrase and stored it somewhere safe, + +20 +00:01:12.072 --> 00:01:15.492 +do it now. We'll wait. + +21 +00:01:15.500 --> 00:01:20.780 +And remember, never share your secret recovery +phrase with anyone: not even us. + +22 +00:01:20.780 --> 00:01:24.910 +If anyone ever asks you for it, +they're trying to scam you. + +23 +00:01:24.910 --> 00:01:26.250 +That's it! + +24 +00:01:26.250 --> 00:01:31.020 +Now you know what a secret recovery phrase +is and how to keep your wallet safe and secure. + diff --git a/app/images/videos/recovery-onboarding/video.webm b/app/images/videos/recovery-onboarding/video.webm new file mode 100644 index 000000000..a0d5cedeb Binary files /dev/null and b/app/images/videos/recovery-onboarding/video.webm differ diff --git a/test/e2e/metamask-ui.spec.js b/test/e2e/metamask-ui.spec.js index 78ed5f59d..f970ebedf 100644 --- a/test/e2e/metamask-ui.spec.js +++ b/test/e2e/metamask-ui.spec.js @@ -83,6 +83,11 @@ describe('MetaMask', function () { let seedPhrase; + it('renders the seed phrase intro screen', async function () { + await driver.clickElement('.seed-phrase-intro__left button'); + await driver.delay(regularDelayMs); + }); + it('reveals the seed phrase', async function () { const byRevealButton = '.reveal-seed-phrase__secret-blocker .reveal-seed-phrase__reveal-button'; diff --git a/ui/app/components/ui/box/box.js b/ui/app/components/ui/box/box.js index bf4756084..47d9617e6 100644 --- a/ui/app/components/ui/box/box.js +++ b/ui/app/components/ui/box/box.js @@ -9,6 +9,7 @@ import { DISPLAY, JUSTIFY_CONTENT, SIZES, + TEXT_ALIGN, } from '../../../helpers/constants/design-system'; const ValidSize = PropTypes.oneOf([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); @@ -70,6 +71,7 @@ export default function Box({ borderStyle, alignItems, justifyContent, + textAlign, display, width, height, @@ -113,6 +115,8 @@ export default function Box({ !display && (Boolean(justifyContent) || Boolean(alignItems)), [`box--justify-content-${justifyContent}`]: Boolean(justifyContent), [`box--align-items-${alignItems}`]: Boolean(alignItems), + // text align + [`box--text-align-${textAlign}`]: Boolean(textAlign), // display [`box--display-${display}`]: Boolean(display), // width & height @@ -144,6 +148,7 @@ Box.propTypes = { borderStyle: PropTypes.oneOf(Object.values(BORDER_STYLE)), alignItems: PropTypes.oneOf(Object.values(ALIGN_ITEMS)), justifyContent: PropTypes.oneOf(Object.values(JUSTIFY_CONTENT)), + textAlign: PropTypes.oneOf(Object.values(TEXT_ALIGN)), display: PropTypes.oneOf(Object.values(DISPLAY)), width: PropTypes.oneOf(Object.values(BLOCK_SIZES)), height: PropTypes.oneOf(Object.values(BLOCK_SIZES)), diff --git a/ui/app/components/ui/box/box.scss b/ui/app/components/ui/box/box.scss index 6ef762766..9e7c0b776 100644 --- a/ui/app/components/ui/box/box.scss +++ b/ui/app/components/ui/box/box.scss @@ -129,6 +129,8 @@ $attributes: padding, margin; // text @each $alignment in design-system.$text-align { - text-align: $alignment; + &--text-align-#{$alignment} { + text-align: $alignment; + } } } diff --git a/ui/app/components/ui/box/box.stories.js b/ui/app/components/ui/box/box.stories.js index a76d2fff5..ba41f454f 100644 --- a/ui/app/components/ui/box/box.stories.js +++ b/ui/app/components/ui/box/box.stories.js @@ -7,6 +7,7 @@ import { COLORS, DISPLAY, JUSTIFY_CONTENT, + TEXT_ALIGN, } from '../../../helpers/constants/design-system'; import Box from './box'; @@ -38,6 +39,7 @@ export const box = () => { undefined, 'display', )} + textAlign={select('textAlign', TEXT_ALIGN, undefined, 'left')} alignItems={select('alignItems', ALIGN_ITEMS, undefined, 'display')} margin={select('margin', sizeKnobOptions, undefined, 'margin')} marginTop={select('marginTop', sizeKnobOptions, undefined, 'margin')} diff --git a/ui/app/helpers/constants/routes.js b/ui/app/helpers/constants/routes.js index 1ed9a7f0d..f2666ffbf 100644 --- a/ui/app/helpers/constants/routes.js +++ b/ui/app/helpers/constants/routes.js @@ -45,6 +45,7 @@ const INITIALIZE_IMPORT_WITH_SEED_PHRASE_ROUTE = const INITIALIZE_SELECT_ACTION_ROUTE = '/initialize/select-action'; const INITIALIZE_SEED_PHRASE_ROUTE = '/initialize/seed-phrase'; const INITIALIZE_BACKUP_SEED_PHRASE_ROUTE = '/initialize/backup-seed-phrase'; +const INITIALIZE_SEED_PHRASE_INTRO_ROUTE = '/initialize/seed-phrase-intro'; const INITIALIZE_END_OF_FLOW_ROUTE = '/initialize/end-of-flow'; const INITIALIZE_CONFIRM_SEED_PHRASE_ROUTE = '/initialize/seed-phrase/confirm'; const INITIALIZE_METAMETRICS_OPT_IN_ROUTE = '/initialize/metametrics-opt-in'; @@ -118,6 +119,7 @@ const PATH_NAME_MAP = { [INITIALIZE_SEED_PHRASE_ROUTE]: 'Initialization Seed Phrase Page', [INITIALIZE_BACKUP_SEED_PHRASE_ROUTE]: 'Initialization Backup Seed Phrase Page', + [INITIALIZE_SEED_PHRASE_INTRO_ROUTE]: 'Initialization Seed Phrase Intro Page', [INITIALIZE_END_OF_FLOW_ROUTE]: 'End of Initialization Page', [INITIALIZE_CONFIRM_SEED_PHRASE_ROUTE]: 'Initialization Confirm Seed Phrase Page', @@ -178,6 +180,7 @@ export { NETWORKS_ROUTE, NETWORKS_FORM_ROUTE, INITIALIZE_BACKUP_SEED_PHRASE_ROUTE, + INITIALIZE_SEED_PHRASE_INTRO_ROUTE, CONNECT_ROUTE, CONNECT_CONFIRM_PERMISSIONS_ROUTE, CONNECTED_ROUTE, diff --git a/ui/app/pages/first-time-flow/create-password/create-password.component.js b/ui/app/pages/first-time-flow/create-password/create-password.component.js index 6913c10fa..357ce283b 100644 --- a/ui/app/pages/first-time-flow/create-password/create-password.component.js +++ b/ui/app/pages/first-time-flow/create-password/create-password.component.js @@ -5,7 +5,7 @@ import MetaFoxLogo from '../../../components/ui/metafox-logo'; import { INITIALIZE_CREATE_PASSWORD_ROUTE, INITIALIZE_IMPORT_WITH_SEED_PHRASE_ROUTE, - INITIALIZE_SEED_PHRASE_ROUTE, + INITIALIZE_SEED_PHRASE_INTRO_ROUTE, } from '../../../helpers/constants/routes'; import NewAccount from './new-account'; import ImportWithSeedPhrase from './import-with-seed-phrase'; @@ -22,7 +22,7 @@ export default class CreatePassword extends PureComponent { const { isInitialized, history } = this.props; if (isInitialized) { - history.push(INITIALIZE_SEED_PHRASE_ROUTE); + history.push(INITIALIZE_SEED_PHRASE_INTRO_ROUTE); } } diff --git a/ui/app/pages/first-time-flow/create-password/new-account/new-account.component.js b/ui/app/pages/first-time-flow/create-password/new-account/new-account.component.js index b74cce8a9..1e9417eb1 100644 --- a/ui/app/pages/first-time-flow/create-password/new-account/new-account.component.js +++ b/ui/app/pages/first-time-flow/create-password/new-account/new-account.component.js @@ -2,7 +2,7 @@ import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; import Button from '../../../../components/ui/button'; import { - INITIALIZE_SEED_PHRASE_ROUTE, + INITIALIZE_SEED_PHRASE_INTRO_ROUTE, INITIALIZE_SELECT_ACTION_ROUTE, } from '../../../../helpers/constants/routes'; import TextField from '../../../../components/ui/text-field'; @@ -108,7 +108,7 @@ export default class NewAccount extends PureComponent { }, }); - history.push(INITIALIZE_SEED_PHRASE_ROUTE); + history.push(INITIALIZE_SEED_PHRASE_INTRO_ROUTE); } catch (error) { this.setState({ passwordError: error.message }); } diff --git a/ui/app/pages/first-time-flow/first-time-flow.component.js b/ui/app/pages/first-time-flow/first-time-flow.component.js index 5094fb2ba..974c37020 100644 --- a/ui/app/pages/first-time-flow/first-time-flow.component.js +++ b/ui/app/pages/first-time-flow/first-time-flow.component.js @@ -12,6 +12,7 @@ import { INITIALIZE_END_OF_FLOW_ROUTE, INITIALIZE_METAMETRICS_OPT_IN_ROUTE, INITIALIZE_BACKUP_SEED_PHRASE_ROUTE, + INITIALIZE_SEED_PHRASE_INTRO_ROUTE, } from '../../helpers/constants/routes'; import FirstTimeFlowSwitch from './first-time-flow-switch'; import Welcome from './welcome'; @@ -125,6 +126,16 @@ export default class FirstTimeFlow extends PureComponent { /> )} /> + ( + + )} + /> ( diff --git a/ui/app/pages/first-time-flow/index.scss b/ui/app/pages/first-time-flow/index.scss index 994d218d3..34a294019 100644 --- a/ui/app/pages/first-time-flow/index.scss +++ b/ui/app/pages/first-time-flow/index.scss @@ -143,3 +143,13 @@ color: $primary-blue; } } + +.first-time-flow__wrapper.intro { + @media screen and (min-width: $break-large) { + max-width: 1010px; + } + + @media screen and (max-width: 1010px) { + padding: 0 20px; + } +} diff --git a/ui/app/pages/first-time-flow/seed-phrase/index.scss b/ui/app/pages/first-time-flow/seed-phrase/index.scss index b4a57912d..15f801632 100644 --- a/ui/app/pages/first-time-flow/seed-phrase/index.scss +++ b/ui/app/pages/first-time-flow/seed-phrase/index.scss @@ -1,5 +1,6 @@ @import 'confirm-seed-phrase/index'; @import 'reveal-seed-phrase/index'; +@import 'seed-phrase-intro/index'; .seed-phrase { &__sections { diff --git a/ui/app/pages/first-time-flow/seed-phrase/seed-phrase-intro/index.js b/ui/app/pages/first-time-flow/seed-phrase/seed-phrase-intro/index.js new file mode 100644 index 000000000..c9d6bdbd6 --- /dev/null +++ b/ui/app/pages/first-time-flow/seed-phrase/seed-phrase-intro/index.js @@ -0,0 +1 @@ +export { default } from './seed-phrase-intro.component'; diff --git a/ui/app/pages/first-time-flow/seed-phrase/seed-phrase-intro/index.scss b/ui/app/pages/first-time-flow/seed-phrase/seed-phrase-intro/index.scss new file mode 100644 index 000000000..f0aeb8979 --- /dev/null +++ b/ui/app/pages/first-time-flow/seed-phrase/seed-phrase-intro/index.scss @@ -0,0 +1,44 @@ +.seed-phrase-intro { + &__sections { + display: flex; + + @media screen and (min-width: $break-large) { + flex-direction: row; + } + + @media screen and (max-width: 970px) { + flex-direction: column; + } + } + + &__left { + flex: 3; + min-width: 0; + } + + &__right { + flex: 1; + min-width: 0; + + @media screen and (max-width: 970px) { + margin-top: 24px; + } + } + + video { + border-radius: 8px; + + @media screen and (max-width: 970px) { + width: 95%; + } + } + + &__copy { + max-width: 696px; + } + + &__sidebar_list { + list-style: disc; + padding-left: 20px; + } +} diff --git a/ui/app/pages/first-time-flow/seed-phrase/seed-phrase-intro/seed-phrase-intro.component.js b/ui/app/pages/first-time-flow/seed-phrase/seed-phrase-intro/seed-phrase-intro.component.js new file mode 100644 index 000000000..304339967 --- /dev/null +++ b/ui/app/pages/first-time-flow/seed-phrase/seed-phrase-intro/seed-phrase-intro.component.js @@ -0,0 +1,121 @@ +import React from 'react'; +import { useHistory } from 'react-router-dom'; +import { useI18nContext } from '../../../../hooks/useI18nContext'; +// Components +import Box from '../../../../components/ui/box'; +import Button from '../../../../components/ui/button'; +import Typography from '../../../../components/ui/typography'; +import { + BLOCK_SIZES, + COLORS, + TYPOGRAPHY, + FONT_WEIGHT, + SIZES, + BORDER_STYLE, +} from '../../../../helpers/constants/design-system'; +// Routes +import { INITIALIZE_SEED_PHRASE_ROUTE } from '../../../../helpers/constants/routes'; + +export default function SeedPhraseIntro() { + const t = useI18nContext(); + const history = useHistory(); + + const handleNextStep = () => { + history.push(INITIALIZE_SEED_PHRASE_ROUTE); + }; + + return ( +
+
+
+ + {t('seedPhraseIntroTitle')} + + + {t('seedPhraseIntroTitleCopy')} + + + + + + + +
+
+ + + + {t('seedPhraseIntroSidebarTitleOne')} + + {t('seedPhraseIntroSidebarCopyOne')} + + + + {t('seedPhraseIntroSidebarTitleTwo')} + +
    +
  • {t('seedPhraseIntroSidebarBulletOne')}
  • +
  • {t('seedPhraseIntroSidebarBulletTwo')}
  • +
  • {t('seedPhraseIntroSidebarBulletThree')}
  • +
  • {t('seedPhraseIntroSidebarBulletFour')}
  • +
+
+ + + {t('seedPhraseIntroSidebarTitleThree')} + + {t('seedPhraseIntroSidebarCopyTwo')} + + + {t('seedPhraseIntroSidebarCopyThree')} + +
+
+
+
+ ); +} diff --git a/ui/app/pages/first-time-flow/seed-phrase/seed-phrase.component.js b/ui/app/pages/first-time-flow/seed-phrase/seed-phrase.component.js index 97d27998a..612e86c77 100644 --- a/ui/app/pages/first-time-flow/seed-phrase/seed-phrase.component.js +++ b/ui/app/pages/first-time-flow/seed-phrase/seed-phrase.component.js @@ -7,11 +7,13 @@ import { INITIALIZE_SEED_PHRASE_ROUTE, INITIALIZE_CONFIRM_SEED_PHRASE_ROUTE, INITIALIZE_BACKUP_SEED_PHRASE_ROUTE, + INITIALIZE_SEED_PHRASE_INTRO_ROUTE, DEFAULT_ROUTE, } from '../../../helpers/constants/routes'; import MetaFoxLogo from '../../../components/ui/metafox-logo'; import ConfirmSeedPhrase from './confirm-seed-phrase'; import RevealSeedPhrase from './reveal-seed-phrase'; +import SeedPhraseIntro from './seed-phrase-intro'; export default class SeedPhrase extends PureComponent { static propTypes = { @@ -39,12 +41,15 @@ export default class SeedPhrase extends PureComponent { } render() { - const { seedPhrase } = this.props; + const { seedPhrase, history } = this.props; const { verifiedSeedPhrase } = this.state; + const pathname = history?.location?.pathname; + const introClass = + pathname === INITIALIZE_SEED_PHRASE_INTRO_ROUTE ? 'intro' : ''; return ( -
+
)} /> + ( + + )} + />