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

First time flow updates (#6192)

* Action select step of onboarding flow added.

* Update navigation on create and import password screens.

* Adds terms of service checkbox to create and import account screens.

* Add security warning to jazzicon intro step

* Update and streamline unique image to confirm seed steps of first time flow.

* UI touch ups to welcome screen.

* UI touch up on select action page

* Fix first time import flow.

* Add end of flow screen to first time flow

* Replace unique image screen with updated fishing warning screen.

* Update e2e tests for onboarding flow changes.

* Add required translations to onboarding flow.

* Update design of select action screen to emphasize create new wallet option.

* Clean up onboarding flow code.

* Remove notice related code from first-time-flow directory.

* Use updater function argument in new-account.component
This commit is contained in:
Dan J Miller 2019-02-27 11:16:41 -03:30 committed by Whymarrh Whitby
parent a2320c76fe
commit cb2698d20e
41 changed files with 752 additions and 517 deletions

View File

@ -92,6 +92,15 @@
"advanced": {
"message": "Advanced"
},
"agreeTermsOfService": {
"message": "I agree to the Terms of Service"
},
"allDone": {
"message": "All Done"
},
"alreadyHaveSeedPhrase": {
"message": "No, I already have a seed phrase"
},
"amount": {
"message": "Amount"
},
@ -239,6 +248,9 @@
"confirmTransaction": {
"message": "Confirm Transaction"
},
"congratulations": {
"message": "Congratulations"
},
"connectHardwareWallet": {
"message": "Connect Hardware Wallet"
},
@ -326,6 +338,9 @@
"createAccount": {
"message": "Create Account"
},
"createAWallet": {
"message": "Create a Wallet"
},
"createDen": {
"message": "Create"
},
@ -448,6 +463,24 @@
"encryptNewDen": {
"message": "Encrypt your new DEN"
},
"endOfFlowMessage1": {
"message": "You passed the test - keep your seedphrase safe, it's your responsibility!"
},
"endOfFlowMessage2": {
"message": "Tips on storing it safely"
},
"endOfFlowMessage3": {
"message": "Save a backup in multiple places"
},
"endOfFlowMessage4": {
"message": "Never tell anyone"
},
"endOfFlowMessage5": {
"message": "If you need to back your seed phrase again, you can find it in Settings -> Security."
},
"endOfFlowMessage6": {
"message": "MetaMask cannot recover your seedphrase. Learn more."
},
"ensNameNotFound": {
"message": "ENS name not found"
},
@ -581,10 +614,16 @@
"getHelp": {
"message": "Get Help."
},
"getStarted": {
"message": "Get Started"
},
"greaterThanMin": {
"message": "must be greater than or equal to $1.",
"description": "helper for inputting hex as decimal input"
},
"happyToSeeYou": {
"message": "Were happy to see you."
},
"hardware": {
"message": "hardware"
},
@ -647,6 +686,9 @@
"importDen": {
"message": "Import Existing DEN"
},
"importWallet": {
"message": "Import Wallet"
},
"imported": {
"message": "Imported",
"description": "status showing that an account has been fully loaded into the keyring"
@ -731,6 +773,9 @@
"message": "must be less than or equal to $1.",
"description": "helper for inputting hex as decimal input"
},
"letsGoSetUp": {
"message": "Yes, lets get set up!"
},
"likeToAddTokens": {
"message": "Would you like to add these tokens?"
},
@ -777,7 +822,7 @@
"message": "Message"
},
"metamaskDescription": {
"message": "MetaMask is a secure identity vault for Ethereum."
"message": "Connecting you to Ethereum and the Decentralized Web."
},
"metamaskSeedWords": {
"message": "MetaMask Seed Words"
@ -848,6 +893,21 @@
"newNetwork": {
"message": "New Network"
},
"newToMetaMask": {
"message": "New to MetaMask?"
},
"noAlreadyHaveSeed": {
"message": "No, I already have a seed phrase"
},
"protectYourKeys": {
"message": "Protect Your Keys!"
},
"protectYourKeysMessage1": {
"message": "Be careful with your seed phrase — there have been reports of websites that attempt to imitate MetaMask. MetaMask will never ask for your seed phrase!"
},
"protectYourKeysMessage2": {
"message": "Keep your phrase safe. If you see something fishy, or youre uncertain about a website, email support@metamask.io"
},
"rpcURL": {
"message": "New RPC URL"
},
@ -1609,6 +1669,9 @@
"yourUniqueAccountImageDescription2": {
"message": "Youll see this image everytime you need to confirm a transaction."
},
"yourUniqueAccountImageDescription3": {
"message": "MetaMask will never ask for your seed phrase!"
},
"zeroGasPriceOnSpeedUpError": {
"message":"Zero gas price on speed up"
}

View File

@ -0,0 +1,5 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="40" y="40" width="40" height="4.57143" rx="2.28571" transform="rotate(-180 40 40)" fill="#979797"/>
<rect x="22.5641" y="21.7144" width="4.10256" height="21.7143" rx="2.05128" transform="rotate(-180 22.5641 21.7144)" fill="#979797"/>
<path d="M20.5 30L7.07661 12L33.9234 12L20.5 30Z" fill="#979797"/>
</svg>

After

Width:  |  Height:  |  Size: 413 B

1
app/images/sleuth.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><path fill="#66757F" d="M33 36v-1c0-3.313-2.687-6-6-6H9c-3.313 0-6 2.687-6 6v1h30z"/><path fill="#EF9645" d="M12 27.482C13.672 29.057 15.746 30 18 30s4.327-.944 6-2.518V26H12v1.482z"/><path fill="#66757F" d="M26.75 20.435c1.188.208 2.619.129 2.416.917-.479 1.854-2.604 1.167-2.979 1.188-.375.02.563-2.105.563-2.105z"/><path fill="#292F33" d="M27.062 20.645c1.875.25 2.541.416 1.166.958-.772.305-2.243 4.803-3.331 4.118-1.087-.685 2.165-5.076 2.165-5.076z"/><path fill="#66757F" d="M9.255 20.435c-1.188.208-2.619.129-2.416.917.479 1.854 2.604 1.167 2.979 1.188.375.02-.563-2.105-.563-2.105z"/><path fill="#292F33" d="M8.943 20.645c-1.875.25-2.541.416-1.166.958.772.305 2.243 4.803 3.331 4.118 1.088-.685-2.165-5.076-2.165-5.076z"/><path fill="#FFAC33" d="M8.055 11.031c-1.953 0-2.305 3.164-.664 3.594 0 0-1.367 3.32 1.953 3.32-.547-1.68-1.562-4.414-.781-6.406m19.38-.508c1.953 0 2.305 3.164.664 3.594 0 0 1.367 3.32-1.953 3.32.547-1.68 1.562-4.414.781-6.406"/><ellipse fill="#FFDC5D" cx="18" cy="15.5" rx="10" ry="12.5"/><path fill="#662113" d="M14 17c-.552 0-1-.448-1-1v-1c0-.552.448-1 1-1s1 .448 1 1v1c0 .552-.448 1-1 1zm8 0c-.553 0-1-.448-1-1v-1c0-.552.447-1 1-1s1 .448 1 1v1c0 .552-.447 1-1 1z"/><path fill="#C1694F" d="M19 20.5c0 .276-.224.5-.5.5h-1c-.276 0-.5-.224-.5-.5s.224-.5.5-.5h1c.276 0 .5.224.5.5z"/><path fill-rule="evenodd" clip-rule="evenodd" fill="#292F33" d="M7.657 14.788c.148.147.888.591 1.036 1.034.148.443.445 2.954 1.333 3.693.916.762 4.37.478 5.032.149 1.48-.738 1.662-2.798 1.924-3.842.148-.591 1.036-.591 1.036-.591s.888 0 1.036.591c.262 1.044.444 3.104 1.924 3.841.662.33 4.116.614 5.034-.147.887-.739 1.183-3.25 1.331-3.694.146-.443.888-.886 1.035-1.034.148-.148.148-.739 0-.887-.296-.295-3.788-.559-7.548-.148-.75.082-1.035.295-2.812.295-1.776 0-2.062-.214-2.812-.295-3.759-.411-7.252-.148-7.548.148-.149.148-.149.74-.001.887z"/><path fill="#66757F" d="M7.858 8.395S9.217-.506 13.79.023c3.512.406 4.89.825 7.833.097 1.947-.482 4.065 1.136 5.342 4.379.816 2.068 1.224 4.041 1.224 4.041s3.938-.385 4.165 1.732c.228 2.117-4.354 4.716-15.889 4.716C10 14.987 3.33 12.63 3.013 10.657c-.317-1.973 4.845-2.262 4.845-2.262z"/><path fill="#292F33" d="M8.125 7.15s-.27 1.104-.406 1.871c-.136.768.226 1.296 2.705 1.824 3.287.7 10.679.692 15.058-.383 1.759-.432 2.886-.72 2.751-1.583-.167-1.068-.196-1.066-.541-2.208 0 0-1.477.502-3.427.96-2.66.624-9.964.911-13.481.144-1.874-.41-2.659-.625-2.659-.625zm-.136 13.953c-.354.145 2.921 1.378 7.48 1.458 4.771.084 6.234.39 5.146 1.459-1.146 1.125-.852 2.894-.771 3.418.081.524 2.047 1.916 2.208 2.56.161.645-1.229 5.961-1.229 5.961l-8.729-.252c-2.565-8.844-2.883-8.501-4.105-13.604-.241-1.008 0-1 0-1z"/><path fill="#66757F" d="M6.989 21.144c-.354.146 2.921 1.378 7.48 1.458 4.771.084 6.234.39 5.146 1.459-1.146 1.125-.664 2.894-.583 3.418.081.524 1.859 1.916 2.021 2.561.16.644-1.231 5.96-1.231 5.96l-8.729-.252c-2.565-8.844-2.883-8.501-4.105-13.604-.24-1.008.001-1 .001-1z"/><path fill="#292F33" d="M28.052 21.103c.354.145-2.921 1.378-7.479 1.458-4.771.084-6.234.39-5.146 1.459 1.146 1.125 2.976 2.892 2.896 3.416-.081.524-4.172 1.918-4.333 2.562-.161.645 1.229 5.961 1.229 5.961l8.729-.252c2.565-8.844 2.883-8.501 4.104-13.604.241-1.008 0-1 0-1z"/><path fill="#66757F" d="M28.958 21.103c.354.145-2.921 1.378-7.479 1.458-4.771.084-6.234.39-5.146 1.459 1.146 1.125 2.977 2.892 2.896 3.416-.081.524-4.172 1.918-4.333 2.562-.161.645 1.229 5.961 1.229 5.961l8.657.01c2.565-8.844 2.955-8.763 4.177-13.866.24-1.008-.001-1-.001-1z"/></svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

4
app/images/thin-plus.svg Normal file
View File

@ -0,0 +1,4 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect y="18" width="40" height="4" rx="2" fill="#979797"/>
<rect x="18" width="4" height="40" rx="2" fill="#979797"/>
</svg>

After

Width:  |  Height:  |  Size: 221 B

View File

@ -65,13 +65,16 @@ describe('Using MetaMask with an existing account', function () {
beforeEach(async function () {
await driver.executeScript(
'window.origFetch = window.fetch.bind(window);' +
'window.fetch = ' +
'(...args) => { ' +
'if (args[0] === "https://ethgasstation.info/json/ethgasAPI.json") { return ' +
'Promise.resolve({ json: () => Promise.resolve(JSON.parse(\'' + fetchMockResponses.ethGasBasic + '\')) }); } else if ' +
'(args[0] === "https://ethgasstation.info/json/predictTable.json") { return ' +
'Promise.resolve({ json: () => Promise.resolve(JSON.parse(\'' + fetchMockResponses.ethGasPredictTable + '\')) }); } ' +
'return window.fetch(...args); }'
'Promise.resolve({ json: () => Promise.resolve(JSON.parse(\'' + fetchMockResponses.ethGasPredictTable + '\')) }); } else if ' +
'(args[0] === "https://dev.blockscale.net/api/gasexpress.json") { return ' +
'Promise.resolve({ json: () => Promise.resolve(JSON.parse(\'' + fetchMockResponses.gasExpress + '\')) }); } ' +
'return window.origFetch(...args); }'
)
})
@ -95,16 +98,19 @@ describe('Using MetaMask with an existing account', function () {
describe('First time flow starting from an existing seed phrase', () => {
it('clicks the continue button on the welcome screen', async () => {
const welcomeScreenBtn = await findElement(driver, By.css('.welcome-page .first-time-flow__button'))
await findElement(driver, By.css('.welcome-page__header'))
const welcomeScreenBtn = await findElement(driver, By.css('.first-time-flow__button'))
welcomeScreenBtn.click()
await delay(largeDelayMs)
})
it('imports a seed phrase', async () => {
const [seedPhrase] = await findElements(driver, By.xpath(`//a[contains(text(), 'Import with seed phrase')]`))
await seedPhrase.click()
await delay(regularDelayMs)
it('clicks the "Import Wallet" option', async () => {
const customRpcButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Import Wallet')]`))
customRpcButton.click()
await delay(largeDelayMs)
})
it('imports a seed phrase', async () => {
const [seedTextArea] = await findElements(driver, By.css('textarea.first-time-flow__textarea'))
await seedTextArea.sendKeys(testSeedPhrase)
await delay(regularDelayMs)
@ -114,39 +120,25 @@ describe('Using MetaMask with an existing account', function () {
const [confirmPassword] = await findElements(driver, By.id('confirm-password'))
confirmPassword.sendKeys('correct horse battery staple')
const tosCheckBox = await findElement(driver, By.css('.first-time-flow__checkbox'))
await tosCheckBox.click()
const [importButton] = await findElements(driver, By.xpath(`//button[contains(text(), 'Import')]`))
await importButton.click()
await delay(regularDelayMs)
})
it('clicks through the ToS', async () => {
// terms of use
await findElement(driver, By.css('.first-time-flow__markdown'))
const canClickThrough = await driver.findElement(By.css('button.first-time-flow__button')).isEnabled()
assert.equal(canClickThrough, false, 'disabled continue button')
const bottomOfTos = await findElement(driver, By.linkText('Attributions'))
await driver.executeScript('arguments[0].scrollIntoView(true)', bottomOfTos)
await delay(regularDelayMs)
const acceptTos = await findElement(driver, By.css('button.first-time-flow__button'))
driver.wait(until.elementIsEnabled(acceptTos))
await acceptTos.click()
await delay(regularDelayMs)
})
it('clicks through the privacy notice', async () => {
// privacy notice
it('clicks through the security warning screen', async () => {
await findElement(driver, By.xpath(`//div[contains(text(), 'Protect Your Keys!')]`))
const nextScreen = await findElement(driver, By.css('button.first-time-flow__button'))
await nextScreen.click()
await delay(regularDelayMs)
})
it('clicks through the phishing notice', async () => {
// phishing notice
const noticeElement = await driver.findElement(By.css('.first-time-flow__markdown'))
await driver.executeScript('arguments[0].scrollTop = arguments[0].scrollHeight', noticeElement)
await delay(regularDelayMs)
const nextScreen = await findElement(driver, By.css('button.first-time-flow__button'))
await nextScreen.click()
it('clicks through the success screen', async () => {
await findElement(driver, By.xpath(`//div[contains(text(), 'Congratulations')]`))
const doneButton = await findElement(driver, By.css('button.first-time-flow__button'))
await doneButton.click()
await delay(regularDelayMs)
})
})

View File

@ -80,135 +80,112 @@ describe('MetaMask', function () {
})
describe('Going through the first time flow', () => {
it('clicks the continue button on the welcome screen', async () => {
const welcomeScreenBtn = await findElement(driver, By.css('.welcome-page .first-time-flow__button'))
welcomeScreenBtn.click()
await delay(largeDelayMs)
})
it('clicks the continue button on the welcome screen', async () => {
await findElement(driver, By.css('.welcome-page__header'))
const welcomeScreenBtn = await findElement(driver, By.css('.first-time-flow__button'))
welcomeScreenBtn.click()
await delay(largeDelayMs)
})
it('accepts a secure password', async () => {
const passwordBox = await findElement(driver, By.css('.first-time-flow__form #create-password'))
const passwordBoxConfirm = await findElement(driver, By.css('.first-time-flow__form #confirm-password'))
const button = await findElement(driver, By.css('.first-time-flow__form button'))
it('clicks the "Create New Wallet" option', async () => {
const customRpcButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Create a Wallet')]`))
customRpcButton.click()
await delay(largeDelayMs)
})
await passwordBox.sendKeys('correct horse battery staple')
await passwordBoxConfirm.sendKeys('correct horse battery staple')
await button.click()
await delay(regularDelayMs)
})
it('accepts a secure password', async () => {
const passwordBox = await findElement(driver, By.css('.first-time-flow__form #create-password'))
const passwordBoxConfirm = await findElement(driver, By.css('.first-time-flow__form #confirm-password'))
const button = await findElement(driver, By.css('.first-time-flow__form button'))
it('clicks through the unique image screen', async () => {
await findElement(driver, By.css('.first-time-flow__unique-image'))
const nextScreen = await findElement(driver, By.css('button.first-time-flow__button'))
await nextScreen.click()
await delay(regularDelayMs)
})
await passwordBox.sendKeys('correct horse battery staple')
await passwordBoxConfirm.sendKeys('correct horse battery staple')
it('clicks through the ToS', async () => {
// terms of use
await findElement(driver, By.css('.first-time-flow__markdown'))
const canClickThrough = await driver.findElement(By.css('button.first-time-flow__button')).isEnabled()
assert.equal(canClickThrough, false, 'disabled continue button')
const bottomOfTos = await findElement(driver, By.linkText('Attributions'))
await driver.executeScript('arguments[0].scrollIntoView(true)', bottomOfTos)
await delay(regularDelayMs)
const acceptTos = await findElement(driver, By.css('button.first-time-flow__button'))
driver.wait(until.elementIsEnabled(acceptTos))
await acceptTos.click()
await delay(regularDelayMs)
})
const tosCheckBox = await findElement(driver, By.css('.first-time-flow__checkbox'))
await tosCheckBox.click()
it('clicks through the privacy notice', async () => {
// privacy notice
const nextScreen = await findElement(driver, By.css('button.first-time-flow__button'))
await nextScreen.click()
await delay(regularDelayMs)
})
await button.click()
await delay(regularDelayMs)
})
it('clicks through the phishing notice', async () => {
// phishing notice
const noticeElement = await driver.findElement(By.css('.first-time-flow__markdown'))
await driver.executeScript('arguments[0].scrollTop = arguments[0].scrollHeight', noticeElement)
await delay(regularDelayMs)
const nextScreen = await findElement(driver, By.css('button.first-time-flow__button'))
await nextScreen.click()
await delay(regularDelayMs)
})
it('clicks through the security warning screen', async () => {
await findElement(driver, By.xpath(`//div[contains(text(), 'Protect Your Keys!')]`))
const nextScreen = await findElement(driver, By.css('button.first-time-flow__button'))
await nextScreen.click()
await delay(regularDelayMs)
})
let seedPhrase
let seedPhrase
it('reveals the seed phrase', async () => {
const byRevealButton = By.css('.reveal-seed-phrase__secret-blocker .reveal-seed-phrase__reveal-button')
await driver.wait(until.elementLocated(byRevealButton, 10000))
const revealSeedPhraseButton = await findElement(driver, byRevealButton, 10000)
await revealSeedPhraseButton.click()
await delay(regularDelayMs)
it('reveals the seed phrase', async () => {
const byRevealButton = By.css('.reveal-seed-phrase__secret-blocker .reveal-seed-phrase__reveal-button')
await driver.wait(until.elementLocated(byRevealButton, 10000))
const revealSeedPhraseButton = await findElement(driver, byRevealButton, 10000)
await revealSeedPhraseButton.click()
await delay(regularDelayMs)
seedPhrase = await driver.findElement(By.css('.reveal-seed-phrase__secret-words')).getText()
assert.equal(seedPhrase.split(' ').length, 12)
await delay(regularDelayMs)
seedPhrase = await driver.findElement(By.css('.reveal-seed-phrase__secret-words')).getText()
assert.equal(seedPhrase.split(' ').length, 12)
await delay(regularDelayMs)
const nextScreen = await findElement(driver, By.css('button.first-time-flow__button'))
await nextScreen.click()
await delay(regularDelayMs)
})
const nextScreen = await findElement(driver, By.css('button.first-time-flow__button'))
await nextScreen.click()
await delay(regularDelayMs)
})
async function clickWordAndWait (word) {
const xpath = `//div[contains(@class, 'confirm-seed-phrase__seed-word--shuffled') and not(contains(@class, 'confirm-seed-phrase__seed-word--selected')) and contains(text(), '${word}')]`
const word0 = await findElement(driver, By.xpath(xpath), 10000)
async function clickWordAndWait (word) {
const xpath = `//div[contains(@class, 'confirm-seed-phrase__seed-word--shuffled') and not(contains(@class, 'confirm-seed-phrase__seed-word--selected')) and contains(text(), '${word}')]`
const word0 = await findElement(driver, By.xpath(xpath), 10000)
await word0.click()
await delay(tinyDelayMs)
}
await word0.click()
await delay(tinyDelayMs)
}
async function retypeSeedPhrase (words, wasReloaded, count = 0) {
try {
if (wasReloaded) {
const byRevealButton = By.css('.reveal-seed-phrase__secret-blocker .reveal-seed-phrase__reveal-button')
await driver.wait(until.elementLocated(byRevealButton, 10000))
const revealSeedPhraseButton = await findElement(driver, byRevealButton, 10000)
await revealSeedPhraseButton.click()
await delay(regularDelayMs)
async function retypeSeedPhrase (words, wasReloaded, count = 0) {
try {
if (wasReloaded) {
const byRevealButton = By.css('.reveal-seed-phrase__secret-blocker .reveal-seed-phrase__reveal-button')
await driver.wait(until.elementLocated(byRevealButton, 10000))
const revealSeedPhraseButton = await findElement(driver, byRevealButton, 10000)
await revealSeedPhraseButton.click()
await delay(regularDelayMs)
const nextScreen = await findElement(driver, By.css('button.first-time-flow__button'))
await nextScreen.click()
await delay(regularDelayMs)
}
const nextScreen = await findElement(driver, By.css('button.first-time-flow__button'))
await nextScreen.click()
await delay(regularDelayMs)
}
for (let i = 0; i < 12; i++) {
await clickWordAndWait(words[i])
}
} catch (e) {
if (count > 2) {
throw e
} else {
await loadExtension(driver, extensionId)
await retypeSeedPhrase(words, true, count + 1)
for (let i = 0; i < 12; i++) {
await clickWordAndWait(words[i])
}
} catch (e) {
if (count > 2) {
throw e
} else {
await loadExtension(driver, extensionId)
await retypeSeedPhrase(words, true, count + 1)
}
}
}
}
it('can retype the seed phrase', async () => {
const words = seedPhrase.split(' ')
it('can retype the seed phrase', async () => {
const words = seedPhrase.split(' ')
await retypeSeedPhrase(words)
await delay(regularDelayMs)
await retypeSeedPhrase(words)
const confirm = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`))
await confirm.click()
await delay(regularDelayMs)
const confirm = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`))
await confirm.click()
await delay(regularDelayMs)
})
it('clicks through the success screen', async () => {
await findElement(driver, By.xpath(`//div[contains(text(), 'Congratulations')]`))
const doneButton = await findElement(driver, By.css('button.first-time-flow__button'))
await doneButton.click()
await delay(regularDelayMs)
})
})
it('clicks through the deposit modal', async () => {
const byBuyModal = By.css('span .modal')
const buyModal = await driver.wait(until.elementLocated(byBuyModal))
const closeModal = await findElement(driver, By.css('.page-container__header-close'))
await closeModal.click()
await driver.wait(until.stalenessOf(buyModal))
await delay(regularDelayMs)
})
})
describe('Show account information', () => {
it('show account details dropdown menu', async () => {
await driver.findElement(By.css('div.menu-bar__open-in-browser')).click()

View File

@ -102,11 +102,18 @@ describe('MetaMask', function () {
describe('Going through the first time flow', () => {
it('clicks the continue button on the welcome screen', async () => {
const welcomeScreenBtn = await findElement(driver, By.css('.welcome-page .first-time-flow__button'))
await findElement(driver, By.css('.welcome-page__header'))
const welcomeScreenBtn = await findElement(driver, By.css('.first-time-flow__button'))
welcomeScreenBtn.click()
await delay(largeDelayMs)
})
it('clicks the "Create New Wallet" option', async () => {
const customRpcButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Create a Wallet')]`))
customRpcButton.click()
await delay(largeDelayMs)
})
it('accepts a secure password', async () => {
const passwordBox = await findElement(driver, By.css('.first-time-flow__form #create-password'))
const passwordBoxConfirm = await findElement(driver, By.css('.first-time-flow__form #confirm-password'))
@ -114,43 +121,16 @@ describe('MetaMask', function () {
await passwordBox.sendKeys('correct horse battery staple')
await passwordBoxConfirm.sendKeys('correct horse battery staple')
const tosCheckBox = await findElement(driver, By.css('.first-time-flow__checkbox'))
await tosCheckBox.click()
await button.click()
await delay(regularDelayMs)
})
it('clicks through the unique image screen', async () => {
await findElement(driver, By.css('.first-time-flow__unique-image'))
const nextScreen = await findElement(driver, By.css('button.first-time-flow__button'))
await nextScreen.click()
await delay(regularDelayMs)
})
it('clicks through the ToS', async () => {
// terms of use
await findElement(driver, By.css('.first-time-flow__markdown'))
const canClickThrough = await driver.findElement(By.css('button.first-time-flow__button')).isEnabled()
assert.equal(canClickThrough, false, 'disabled continue button')
const bottomOfTos = await findElement(driver, By.linkText('Attributions'))
await driver.executeScript('arguments[0].scrollIntoView(true)', bottomOfTos)
await delay(regularDelayMs)
const acceptTos = await findElement(driver, By.css('button.first-time-flow__button'))
driver.wait(until.elementIsEnabled(acceptTos))
await acceptTos.click()
await delay(regularDelayMs)
})
it('clicks through the privacy notice', async () => {
// privacy notice
const nextScreen = await findElement(driver, By.css('button.first-time-flow__button'))
await nextScreen.click()
await delay(regularDelayMs)
})
it('clicks through the phishing notice', async () => {
// phishing notice
const noticeElement = await driver.findElement(By.css('.first-time-flow__markdown'))
await driver.executeScript('arguments[0].scrollTop = arguments[0].scrollHeight', noticeElement)
await delay(regularDelayMs)
it('clicks through the security warning screen', async () => {
await findElement(driver, By.xpath(`//div[contains(text(), 'Protect Your Keys!')]`))
const nextScreen = await findElement(driver, By.css('button.first-time-flow__button'))
await nextScreen.click()
await delay(regularDelayMs)
@ -219,12 +199,10 @@ describe('MetaMask', function () {
await delay(regularDelayMs)
})
it('clicks through the deposit modal', async () => {
const byBuyModal = By.css('span .modal')
const buyModal = await driver.wait(until.elementLocated(byBuyModal))
const closeModal = await findElement(driver, By.css('.page-container__header-close'))
await closeModal.click()
await driver.wait(until.stalenessOf(buyModal))
it('clicks through the success screen', async () => {
await findElement(driver, By.xpath(`//div[contains(text(), 'Congratulations')]`))
const doneButton = await findElement(driver, By.css('button.first-time-flow__button'))
await doneButton.click()
await delay(regularDelayMs)
})
})

View File

@ -8,13 +8,13 @@ import {
INITIALIZE_CREATE_PASSWORD_ROUTE,
INITIALIZE_IMPORT_WITH_SEED_PHRASE_ROUTE,
INITIALIZE_UNIQUE_IMAGE_ROUTE,
INITIALIZE_NOTICE_ROUTE,
} from '../../../../routes'
export default class CreatePassword extends PureComponent {
static propTypes = {
history: PropTypes.object,
isInitialized: PropTypes.bool,
isImportedKeyring: PropTypes.bool,
onCreateNewAccount: PropTypes.func,
onCreateNewAccountFromSeed: PropTypes.func,
}
@ -23,17 +23,38 @@ export default class CreatePassword extends PureComponent {
const { isInitialized, history } = this.props
if (isInitialized) {
history.push(INITIALIZE_NOTICE_ROUTE)
history.push(INITIALIZE_UNIQUE_IMAGE_ROUTE)
}
}
render () {
const { onCreateNewAccount, onCreateNewAccountFromSeed } = this.props
const { onCreateNewAccount, onCreateNewAccountFromSeed, isImportedKeyring } = this.props
return (
<div className="first-time-flow__wrapper">
<div className="app-header__logo-container">
<img
className="app-header__metafox-logo app-header__metafox-logo--horizontal"
src="/images/logo/metamask-logo-horizontal.svg"
height={30}
/>
<img
className="app-header__metafox-logo app-header__metafox-logo--icon"
src="/images/logo/metamask-fox.svg"
height={42}
width={42}
/>
</div>
<Switch>
<Route exact path={INITIALIZE_UNIQUE_IMAGE_ROUTE} component={UniqueImage} />
<Route exact
path={INITIALIZE_UNIQUE_IMAGE_ROUTE}
render={props => (
<UniqueImage
{ ...props }
isImportedKeyring={isImportedKeyring}
/>
)}
/>
<Route
exact
path={INITIALIZE_IMPORT_WITH_SEED_PHRASE_ROUTE}

View File

@ -3,10 +3,9 @@ import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import TextField from '../../../../text-field'
import Button from '../../../../button'
import Breadcrumbs from '../../../../breadcrumbs'
import {
INITIALIZE_CREATE_PASSWORD_ROUTE,
INITIALIZE_NOTICE_ROUTE,
INITIALIZE_SELECT_ACTION_ROUTE,
INITIALIZE_UNIQUE_IMAGE_ROUTE,
} from '../../../../../routes'
export default class ImportWithSeedPhrase extends PureComponent {
@ -26,6 +25,7 @@ export default class ImportWithSeedPhrase extends PureComponent {
seedPhraseError: '',
passwordError: '',
confirmPasswordError: '',
termsChecked: false,
}
parseSeedPhrase = (seedPhrase) => {
@ -104,7 +104,7 @@ export default class ImportWithSeedPhrase extends PureComponent {
try {
await onSubmit(password, seedPhrase)
history.push(INITIALIZE_NOTICE_ROUTE)
history.push(INITIALIZE_UNIQUE_IMAGE_ROUTE)
} catch (error) {
this.setState({ seedPhraseError: error.message })
}
@ -131,20 +131,26 @@ export default class ImportWithSeedPhrase extends PureComponent {
return !passwordError && !confirmPasswordError && !seedPhraseError
}
toggleTermsCheck = () => {
this.setState((prevState) => ({
termsChecked: !prevState.termsChecked,
}))
}
render () {
const { t } = this.context
const { seedPhraseError, passwordError, confirmPasswordError } = this.state
const { seedPhraseError, passwordError, confirmPasswordError, termsChecked } = this.state
return (
<form
className="first-time-flow__form"
onSubmit={this.handleImport}
>
<div>
<div className="first-time-flow__create-back">
<a
onClick={e => {
e.preventDefault()
this.props.history.push(INITIALIZE_CREATE_PASSWORD_ROUTE)
this.props.history.push(INITIALIZE_SELECT_ACTION_ROUTE)
}}
href="#"
>
@ -197,19 +203,22 @@ export default class ImportWithSeedPhrase extends PureComponent {
margin="normal"
largeLabel
/>
<div className="first-time-flow__checkbox-container" onClick={this.toggleTermsCheck}>
<div className="first-time-flow__checkbox">
{termsChecked ? <i className="fa fa-check fa-2x" /> : null}
</div>
<span className="first-time-flow__checkbox-label">
{ t('agreeTermsOfService') }
</span>
</div>
<Button
type="first-time"
type="confirm"
className="first-time-flow__button"
disabled={!this.isValid()}
disabled={!this.isValid() || !termsChecked}
onClick={this.handleImport}
>
{ t('import') }
</Button>
<Breadcrumbs
className="first-time-flow__breadcrumbs"
total={2}
currentIndex={0}
/>
</form>
)
}

View File

@ -1,10 +1,10 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import Breadcrumbs from '../../../../breadcrumbs'
import Button from '../../../../button'
import {
INITIALIZE_UNIQUE_IMAGE_ROUTE,
INITIALIZE_IMPORT_WITH_SEED_PHRASE_ROUTE,
INITIALIZE_SELECT_ACTION_ROUTE,
} from '../../../../../routes'
import TextField from '../../../../text-field'
@ -23,6 +23,7 @@ export default class NewAccount extends PureComponent {
confirmPassword: '',
passwordError: '',
confirmPasswordError: '',
termsChecked: false,
}
isValid () {
@ -111,12 +112,29 @@ export default class NewAccount extends PureComponent {
history.push(INITIALIZE_IMPORT_WITH_SEED_PHRASE_ROUTE)
}
toggleTermsCheck = () => {
this.setState((prevState) => ({
termsChecked: !prevState.termsChecked,
}))
}
render () {
const { t } = this.context
const { password, confirmPassword, passwordError, confirmPasswordError } = this.state
const { password, confirmPassword, passwordError, confirmPasswordError, termsChecked } = this.state
return (
<div>
<div className="first-time-flow__create-back">
<a
onClick={e => {
e.preventDefault()
this.props.history.push(INITIALIZE_SELECT_ACTION_ROUTE)
}}
href="#"
>
{`< Back`}
</a>
</div>
<div className="first-time-flow__header">
{ t('createPassword') }
</div>
@ -151,27 +169,23 @@ export default class NewAccount extends PureComponent {
fullWidth
largeLabel
/>
<div className="first-time-flow__checkbox-container" onClick={this.toggleTermsCheck}>
<div className="first-time-flow__checkbox">
{termsChecked ? <i className="fa fa-check fa-2x" /> : null}
</div>
<span className="first-time-flow__checkbox-label">
I agree to the Terms Of Service
</span>
</div>
<Button
type="first-time"
type="confirm"
className="first-time-flow__button"
disabled={!this.isValid()}
disabled={!this.isValid() || !termsChecked}
onClick={this.handleCreate}
>
{ t('create') }
</Button>
</form>
<a
href=""
className="first-time-flow__link create-password__import-link"
onClick={this.handleImportWithSeedPhrase}
>
{ t('importWithSeedPhrase') }
</a>
<Breadcrumbs
className="first-time-flow__breadcrumbs"
total={3}
currentIndex={0}
/>
</div>
)
}

View File

@ -1,9 +1,7 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import Identicon from '../../../../identicon'
import Breadcrumbs from '../../../../breadcrumbs'
import Button from '../../../../button'
import { INITIALIZE_NOTICE_ROUTE } from '../../../../../routes'
import { INITIALIZE_SEED_PHRASE_ROUTE, INITIALIZE_END_OF_FLOW_ROUTE } from '../../../../../routes'
export default class UniqueImageScreen extends PureComponent {
static contextTypes = {
@ -11,42 +9,43 @@ export default class UniqueImageScreen extends PureComponent {
}
static propTypes = {
address: PropTypes.string,
history: PropTypes.object,
isImportedKeyring: PropTypes.bool,
}
render () {
const { t } = this.context
const { address, history } = this.props
const { history, isImportedKeyring } = this.props
return (
<div>
<Identicon
className="first-time-flow__unique-image"
address={address}
diameter={70}
<img
src="/images/sleuth.svg"
height={42}
width={42}
/>
<div className="first-time-flow__header">
{ t('yourUniqueAccountImage') }
{ t('protectYourKeys') }
</div>
<div className="first-time-flow__text-block">
{ t('yourUniqueAccountImageDescription1') }
{ t('protectYourKeysMessage1') }
</div>
<div className="first-time-flow__text-block">
{ t('yourUniqueAccountImageDescription2') }
{ t('protectYourKeysMessage2') }
</div>
<Button
type="first-time"
type="confirm"
className="first-time-flow__button"
onClick={() => history.push(INITIALIZE_NOTICE_ROUTE)}
onClick={() => {
if (isImportedKeyring) {
history.push(INITIALIZE_END_OF_FLOW_ROUTE)
} else {
history.push(INITIALIZE_SEED_PHRASE_ROUTE)
}
}}
>
{ t('next') }
</Button>
<Breadcrumbs
className="first-time-flow__breadcrumbs"
total={3}
currentIndex={0}
/>
</div>
)
}

View File

@ -0,0 +1,70 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import Button from '../../../button'
import { DEFAULT_ROUTE } from '../../../../routes'
export default class EndOfFlowScreen extends PureComponent {
static contextTypes = {
t: PropTypes.func,
}
static propTypes = {
history: PropTypes.object,
completeOnboarding: PropTypes.func,
}
render () {
const { t } = this.context
const { history, completeOnboarding } = this.props
return (
<div className="end-of-flow">
<div className="app-header__logo-container">
<img
className="app-header__metafox-logo app-header__metafox-logo--horizontal"
src="/images/logo/metamask-logo-horizontal.svg"
height={30}
/>
<img
className="app-header__metafox-logo app-header__metafox-logo--icon"
src="/images/logo/metamask-fox.svg"
height={42}
width={42}
/>
</div>
<div className="end-of-flow__emoji">🎉</div>
<div className="first-time-flow__header">
{ t('congratulations') }
</div>
<div className="first-time-flow__text-block end-of-flow__text-1">
{ t('endOfFlowMessage1') }
</div>
<div className="first-time-flow__text-block end-of-flow__text-2">
{ t('endOfFlowMessage2') }
</div>
<div className="first-time-flow__text-block end-of-flow__text-3">
{ '• ' + t('endOfFlowMessage3') }
</div>
<div className="first-time-flow__text-block end-of-flow__text-4">
{ '• ' + t('endOfFlowMessage4') }
</div>
<div className="first-time-flow__text-block end-of-flow__text-3">
{ t('endOfFlowMessage5') }
</div>
<div className="first-time-flow__text-block end-of-flow__text-3">
{ '*' + t('endOfFlowMessage6') }
</div>
<Button
type="confirm"
className="first-time-flow__button"
onClick={async () => {
await completeOnboarding()
history.push(DEFAULT_ROUTE)
}}
>
{ 'All Done' }
</Button>
</div>
)
}
}

View File

@ -0,0 +1,11 @@
import { connect } from 'react-redux'
import EndOfFlow from './end-of-flow.component'
import { setCompletedOnboarding } from '../../../../actions'
const mapDispatchToProps = dispatch => {
return {
completeOnboarding: () => dispatch(setCompletedOnboarding()),
}
}
export default connect(null, mapDispatchToProps)(EndOfFlow)

View File

@ -0,0 +1 @@
export { default } from './end-of-flow.container'

View File

@ -0,0 +1,47 @@
.end-of-flow {
color: black;
font-family: Roboto;
font-style: normal;
.app-header__logo-container {
width: 742px;
margin-top: 3%;
@media screen and (max-width: $break-small) {
width: 100%;
}
}
&__text-1, &__text-3 {
font-weight: normal;
font-size: 16px;
margin-top: 18px;
}
&__text-2 {
font-weight: bold;
font-size: 16px;
margin-top: 26px;
}
&__text-3 {
margin-top: 26px;
}
&__text-3 {
margin-top: 2px;
}
button {
width: 207px;
}
&__start-over-button {
width: 744px;
}
&__emoji {
font-size: 80px;
margin-top: 70px;
}
}

View File

@ -5,7 +5,6 @@ import {
DEFAULT_ROUTE,
LOCK_ROUTE,
INITIALIZE_WELCOME_ROUTE,
INITIALIZE_NOTICE_ROUTE,
INITIALIZE_UNLOCK_ROUTE,
INITIALIZE_SEED_PHRASE_ROUTE,
} from '../../../../routes'
@ -15,7 +14,6 @@ export default class FirstTimeFlowSwitch extends PureComponent {
completedOnboarding: PropTypes.bool,
isInitialized: PropTypes.bool,
isUnlocked: PropTypes.bool,
noActiveNotices: PropTypes.bool,
seedPhrase: PropTypes.string,
}
@ -24,7 +22,6 @@ export default class FirstTimeFlowSwitch extends PureComponent {
completedOnboarding,
isInitialized,
isUnlocked,
noActiveNotices,
seedPhrase,
} = this.props
@ -44,10 +41,6 @@ export default class FirstTimeFlowSwitch extends PureComponent {
return <Redirect to={{ pathname: INITIALIZE_UNLOCK_ROUTE }} />
}
if (!noActiveNotices) {
return <Redirect to={{ pathname: INITIALIZE_NOTICE_ROUTE }} />
}
if (seedPhrase) {
return <Redirect to={{ pathname: INITIALIZE_SEED_PHRASE_ROUTE }} />
}

View File

@ -6,14 +6,12 @@ const mapStateToProps = ({ metamask }) => {
completedOnboarding,
isInitialized,
isUnlocked,
noActiveNotices,
} = metamask
return {
completedOnboarding,
isInitialized,
isUnlocked,
noActiveNotices,
}
}

View File

@ -3,17 +3,19 @@ import PropTypes from 'prop-types'
import { Switch, Route } from 'react-router-dom'
import FirstTimeFlowSwitch from './first-time-flow-switch'
import Welcome from './welcome'
import SelectAction from './select-action'
import EndOfFlow from './end-of-flow'
import Unlock from '../unlock-page'
import CreatePassword from './create-password'
import Notices from './notices'
import SeedPhrase from './seed-phrase'
import {
DEFAULT_ROUTE,
INITIALIZE_WELCOME_ROUTE,
INITIALIZE_CREATE_PASSWORD_ROUTE,
INITIALIZE_NOTICE_ROUTE,
INITIALIZE_SEED_PHRASE_ROUTE,
INITIALIZE_UNLOCK_ROUTE,
INITIALIZE_SELECT_ACTION_ROUTE,
INITIALIZE_END_OF_FLOW_ROUTE,
} from '../../../routes'
export default class FirstTimeFlow extends PureComponent {
@ -24,7 +26,6 @@ export default class FirstTimeFlow extends PureComponent {
history: PropTypes.object,
isInitialized: PropTypes.bool,
isUnlocked: PropTypes.bool,
noActiveNotices: PropTypes.bool,
unlockAccount: PropTypes.func,
}
@ -70,14 +71,12 @@ export default class FirstTimeFlow extends PureComponent {
}
handleUnlock = async password => {
const { unlockAccount, history, noActiveNotices } = this.props
const { unlockAccount, history } = this.props
try {
const seedPhrase = await unlockAccount(password)
this.setState({ seedPhrase }, () => {
noActiveNotices
? history.push(INITIALIZE_SEED_PHRASE_ROUTE)
: history.push(INITIALIZE_NOTICE_ROUTE)
history.push(INITIALIZE_SEED_PHRASE_ROUTE)
})
} catch (error) {
throw new Error(error.message)
@ -99,26 +98,21 @@ export default class FirstTimeFlow extends PureComponent {
/>
)}
/>
<Route
exact
path={INITIALIZE_NOTICE_ROUTE}
render={props => (
<Notices
{ ...props }
isImportedKeyring={isImportedKeyring}
/>
)}
/>
<Route
path={INITIALIZE_CREATE_PASSWORD_ROUTE}
render={props => (
<CreatePassword
{ ...props }
isImportedKeyring={isImportedKeyring}
onCreateNewAccount={this.handleCreateNewAccount}
onCreateNewAccountFromSeed={this.handleImportWithSeedPhrase}
/>
)}
/>
<Route
path={INITIALIZE_SELECT_ACTION_ROUTE}
component={SelectAction}
/>
<Route
path={INITIALIZE_UNLOCK_ROUTE}
render={props => (
@ -128,6 +122,11 @@ export default class FirstTimeFlow extends PureComponent {
/>
)}
/>
<Route
exact
path={INITIALIZE_END_OF_FLOW_ROUTE}
component={EndOfFlow}
/>
<Route
exact
path={INITIALIZE_WELCOME_ROUTE}

View File

@ -7,13 +7,12 @@ import {
} from '../../../actions'
const mapStateToProps = state => {
const { metamask: { completedOnboarding, isInitialized, isUnlocked, noActiveNotices } } = state
const { metamask: { completedOnboarding, isInitialized, isUnlocked } } = state
return {
completedOnboarding,
isInitialized,
isUnlocked,
noActiveNotices,
}
}

View File

@ -1,18 +1,28 @@
@import './welcome/index';
@import './select-action/index';
@import './seed-phrase/index';
@import './end-of-flow/index';
.first-time-flow {
width: 100%;
background-color: $white;
display: flex;
justify-content: center;
&__wrapper {
@media screen and (min-width: $break-large) {
padding: 60px 275px 0 275px;
max-width: 742px;
display: flex;
flex-direction: column;
width: 100%;
margin-top: 2%;
}
@media screen and (max-width: 1100px) {
padding: 36px;
.app-header__metafox-logo {
margin-bottom: 40px;
}
}
@ -21,9 +31,14 @@
flex-direction: column;
}
&__create-back {
margin-bottom: 16px;
}
&__header {
font-size: 2.5rem;
margin-bottom: 24px;
color: black;
}
&__subheader {
@ -86,6 +101,7 @@
&__text-block {
margin-bottom: 24px;
color: black;
@media screen and (max-width: $break-small) {
margin-bottom: 16px;
@ -95,5 +111,42 @@
&__button {
margin: 35px 0 14px;
width: 140px;
height: 44px;
}
&__checkbox-container {
display: flex;
align-items: center;
margin-top: 24px;
}
&__checkbox {
background: #FFFFFF;
border: 1px solid #CDCDCD;
box-sizing: border-box;
height: 34px;
width: 34px;
display: flex;
justify-content: center;
align-items: center;
&:hover {
border: 1.5px solid #2f9ae0;
}
.fa-check {
color: #2f9ae0
}
}
&__checkbox-label {
font-family: Roboto;
font-style: normal;
font-weight: normal;
line-height: normal;
font-size: 18px;
color: #939090;
margin-left: 18px;
}
}

View File

@ -1 +0,0 @@
export { default } from './notices.container'

View File

@ -1,124 +0,0 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import Markdown from 'react-markdown'
import debounce from 'lodash.debounce'
import Button from '../../../button'
import Identicon from '../../../identicon'
import Breadcrumbs from '../../../breadcrumbs'
import { DEFAULT_ROUTE, INITIALIZE_SEED_PHRASE_ROUTE } from '../../../../routes'
export default class Notices extends PureComponent {
static contextTypes = {
t: PropTypes.func,
}
static propTypes = {
address: PropTypes.string.isRequired,
completeOnboarding: PropTypes.func,
history: PropTypes.object,
isImportedKeyring: PropTypes.bool,
markNoticeRead: PropTypes.func,
nextUnreadNotice: PropTypes.shape({
title: PropTypes.string,
date: PropTypes.string,
body: PropTypes.string,
}),
noActiveNotices: PropTypes.bool,
}
static defaultProps = {
nextUnreadNotice: {},
}
state = {
atBottom: false,
}
componentDidMount () {
const { noActiveNotices, history } = this.props
if (noActiveNotices) {
history.push(INITIALIZE_SEED_PHRASE_ROUTE)
}
this.onScroll()
}
acceptTerms = async () => {
const {
completeOnboarding,
history,
isImportedKeyring,
markNoticeRead,
nextUnreadNotice,
} = this.props
const hasActiveNotices = await markNoticeRead(nextUnreadNotice)
if (!hasActiveNotices) {
if (isImportedKeyring) {
await completeOnboarding()
history.push(DEFAULT_ROUTE)
} else {
history.push(INITIALIZE_SEED_PHRASE_ROUTE)
}
} else {
this.setState({ atBottom: false }, () => this.onScroll())
}
}
onScroll = debounce(() => {
if (this.state.atBottom) {
return
}
const target = document.querySelector('.first-time-flow__markdown')
if (target) {
const { scrollTop, offsetHeight, scrollHeight } = target
const atBottom = scrollTop + offsetHeight >= scrollHeight
this.setState({ atBottom })
}
}, 25)
render () {
const { t } = this.context
const { isImportedKeyring, address, nextUnreadNotice: { title, body } } = this.props
const { atBottom } = this.state
return (
<div
className="first-time-flow__wrapper"
onScroll={this.onScroll}
>
<Identicon
className="first-time-flow__unique-image"
address={address}
diameter={70}
/>
<div className="first-time-flow__header">
{ title }
</div>
<Markdown
className="first-time-flow__markdown"
source={body}
skipHtml
/>
<Button
type="first-time"
className="first-time-flow__button"
onClick={atBottom && this.acceptTerms}
disabled={!atBottom}
>
{ t('accept') }
</Button>
<Breadcrumbs
className="first-time-flow__breadcrumbs"
total={isImportedKeyring ? 2 : 3}
currentIndex={1}
/>
</div>
)
}
}

View File

@ -1,27 +0,0 @@
import { connect } from 'react-redux'
import { withRouter } from 'react-router-dom'
import { compose } from 'recompose'
import { markNoticeRead, setCompletedOnboarding } from '../../../../actions'
import Notices from './notices.component'
const mapStateToProps = ({ metamask }) => {
const { selectedAddress, nextUnreadNotice, noActiveNotices } = metamask
return {
address: selectedAddress,
nextUnreadNotice,
noActiveNotices,
}
}
const mapDispatchToProps = dispatch => {
return {
markNoticeRead: notice => dispatch(markNoticeRead(notice)),
completeOnboarding: () => dispatch(setCompletedOnboarding()),
}
}
export default compose(
withRouter,
connect(mapStateToProps, mapDispatchToProps)
)(Notices)

View File

@ -2,10 +2,8 @@ import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
import shuffle from 'lodash.shuffle'
import Identicon from '../../../../identicon'
import Button from '../../../../button'
import Breadcrumbs from '../../../../breadcrumbs'
import { DEFAULT_ROUTE, INITIALIZE_SEED_PHRASE_ROUTE } from '../../../../../routes'
import { INITIALIZE_END_OF_FLOW_ROUTE, INITIALIZE_SEED_PHRASE_ROUTE } from '../../../../../routes'
import { exportAsFile } from '../../../../../../app/util'
import { selectSeedWord, deselectSeedWord } from './confirm-seed-phrase.state'
@ -19,11 +17,8 @@ export default class ConfirmSeedPhrase extends PureComponent {
}
static propTypes = {
address: PropTypes.string,
completeOnboarding: PropTypes.func,
history: PropTypes.object,
onSubmit: PropTypes.func,
openBuyEtherModal: PropTypes.func,
seedPhrase: PropTypes.string,
}
@ -45,16 +40,14 @@ export default class ConfirmSeedPhrase extends PureComponent {
}
handleSubmit = async () => {
const { completeOnboarding, history, openBuyEtherModal } = this.props
const { history } = this.props
if (!this.isValid()) {
return
}
try {
await completeOnboarding()
history.push(DEFAULT_ROUTE)
openBuyEtherModal()
history.push(INITIALIZE_END_OF_FLOW_ROUTE)
} catch (error) {
console.error(error.message)
}
@ -76,11 +69,11 @@ export default class ConfirmSeedPhrase extends PureComponent {
render () {
const { t } = this.context
const { address, history } = this.props
const { history } = this.props
const { selectedSeedWords, shuffledSeedWords, selectedSeedWordsHash } = this.state
return (
<div>
<div className="confirm-seed-phrase">
<div className="confirm-seed-phrase__back-button">
<a
onClick={e => {
@ -92,11 +85,6 @@ export default class ConfirmSeedPhrase extends PureComponent {
{`< Back`}
</a>
</div>
<Identicon
className="first-time-flow__unique-image"
address={address}
diameter={70}
/>
<div className="first-time-flow__header">
{ t('confirmSecretBackupPhrase') }
</div>
@ -143,18 +131,13 @@ export default class ConfirmSeedPhrase extends PureComponent {
}
</div>
<Button
type="first-time"
type="confirm"
className="first-time-flow__button"
onClick={this.handleSubmit}
disabled={!this.isValid()}
>
{ t('confirm') }
</Button>
<Breadcrumbs
className="first-time-flow__breadcrumbs"
total={3}
currentIndex={2}
/>
</div>
)
}

View File

@ -1,12 +0,0 @@
import { connect } from 'react-redux'
import ConfirmSeedPhrase from './confirm-seed-phrase.component'
import { setCompletedOnboarding, showModal } from '../../../../../actions'
const mapDispatchToProps = dispatch => {
return {
completeOnboarding: () => dispatch(setCompletedOnboarding()),
openBuyEtherModal: () => dispatch(showModal({ name: 'DEPOSIT_ETHER'})),
}
}
export default connect(null, mapDispatchToProps)(ConfirmSeedPhrase)

View File

@ -1 +1 @@
export { default } from './confirm-seed-phrase.container'
export { default } from './confirm-seed-phrase.component'

View File

@ -41,4 +41,8 @@
padding: 6px 18px;
}
}
button {
margin-top: 0xp;
}
}

View File

@ -1 +1 @@
export { default } from './seed-phrase.container'
export { default } from './seed-phrase.component'

View File

@ -26,11 +26,15 @@
min-width: 0;
@media screen and (min-width: $break-large) {
margin-left: 48px;
margin-left: 81px;
}
@media screen and (max-width: $break-small) {
margin-top: 24px;
}
.first-time-flow__text-block {
color: #5A5A5A;
}
}
}

View File

@ -50,4 +50,8 @@
cursor: pointer;
font-weight: 500;
}
button {
margin-top: 0xp;
}
}

View File

@ -1,10 +1,8 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
import Identicon from '../../../../identicon'
import LockIcon from '../../../../lock-icon'
import Button from '../../../../button'
import Breadcrumbs from '../../../../breadcrumbs'
import { INITIALIZE_CONFIRM_SEED_PHRASE_ROUTE } from '../../../../../routes'
import { exportAsFile } from '../../../../../../app/util'
@ -14,7 +12,6 @@ export default class RevealSeedPhrase extends PureComponent {
}
static propTypes = {
address: PropTypes.string,
history: PropTypes.object,
seedPhrase: PropTypes.string,
}
@ -75,16 +72,10 @@ export default class RevealSeedPhrase extends PureComponent {
render () {
const { t } = this.context
const { address } = this.props
const { isShowingSeedPhrase } = this.state
return (
<div>
<Identicon
className="first-time-flow__unique-image"
address={address}
diameter={70}
/>
<div className="reveal-seed-phrase">
<div className="seed-phrase__sections">
<div className="seed-phrase__main">
<div className="first-time-flow__header">
@ -121,18 +112,13 @@ export default class RevealSeedPhrase extends PureComponent {
</div>
</div>
<Button
type="first-time"
type="confirm"
className="first-time-flow__button"
onClick={this.handleNext}
disabled={!isShowingSeedPhrase}
>
{ t('next') }
</Button>
<Breadcrumbs
className="first-time-flow__breadcrumbs"
total={3}
currentIndex={2}
/>
</div>
)
}

View File

@ -25,10 +25,23 @@ export default class SeedPhrase extends PureComponent {
}
render () {
const { address, seedPhrase } = this.props
const { seedPhrase } = this.props
return (
<div className="first-time-flow__wrapper">
<div className="app-header__logo-container">
<img
className="app-header__metafox-logo app-header__metafox-logo--horizontal"
src="/images/logo/metamask-logo-horizontal.svg"
height={30}
/>
<img
className="app-header__metafox-logo app-header__metafox-logo--icon"
src="/images/logo/metamask-fox.svg"
height={42}
width={42}
/>
</div>
<Switch>
<Route
exact
@ -36,7 +49,6 @@ export default class SeedPhrase extends PureComponent {
render={props => (
<ConfirmSeedPhrase
{ ...props }
address={address}
seedPhrase={seedPhrase}
/>
)}
@ -47,7 +59,6 @@ export default class SeedPhrase extends PureComponent {
render={props => (
<RevealSeedPhrase
{ ...props }
address={address}
seedPhrase={seedPhrase}
/>
)}

View File

@ -1,12 +0,0 @@
import { connect } from 'react-redux'
import SeedPhrase from './seed-phrase.component'
const mapStateToProps = state => {
const { metamask: { selectedAddress } } = state
return {
address: selectedAddress,
}
}
export default connect(mapStateToProps)(SeedPhrase)

View File

@ -0,0 +1 @@
export { default } from './select-action.component'

View File

@ -0,0 +1,87 @@
.select-action {
.app-header__logo-container {
width: 742px;
margin-top: 3%;
}
&__body {
display: flex;
flex-direction: column;
align-items: center;
}
&__body-header {
font-family: Roboto;
font-style: normal;
font-weight: normal;
line-height: 39px;
font-size: 28px;
text-align: center;
margin-top: 65px;
color: black;
}
&__select-buttons {
display: flex;
flex-direction: row;
margin-top: 40px;
}
&__select-button {
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-evenly;
width: 269px;
height: 278px;
border: 1px solid #D8D8D8;
box-sizing: border-box;
border-radius: 10px;
margin-left: 22px;
.first-time-flow__button {
max-width: 221px;
height: 44px;
}
}
&__button-symbol {
color: #C4C4C4;
margin-top: 41px;
}
&__button-content {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 144px;
}
&__button-text-big {
font-family: Roboto;
font-style: normal;
font-weight: normal;
line-height: 28px;
font-size: 20px;
color: #000000;
margin-top: 12px;
text-align: center;
}
&__button-text-small {
font-family: Roboto;
font-style: normal;
font-weight: normal;
line-height: 20px;
font-size: 14px;
color: #7A7A7B;
margin-top: 10px;
}
button {
font-weight: 500;
width: 221px;
}
}

View File

@ -0,0 +1,104 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import Button from '../../../button'
import {
INITIALIZE_CREATE_PASSWORD_ROUTE,
INITIALIZE_IMPORT_WITH_SEED_PHRASE_ROUTE,
INITIALIZE_UNIQUE_IMAGE_ROUTE,
} from '../../../../routes'
export default class SelectAction extends PureComponent {
static propTypes = {
history: PropTypes.object,
isInitialized: PropTypes.bool,
}
static contextTypes = {
t: PropTypes.func,
}
componentDidMount () {
const { history, isInitialized } = this.props
if (isInitialized) {
history.push(INITIALIZE_UNIQUE_IMAGE_ROUTE)
}
}
handleCreate = () => {
this.props.history.push(INITIALIZE_CREATE_PASSWORD_ROUTE)
}
handleImport = () => {
this.props.history.push(INITIALIZE_IMPORT_WITH_SEED_PHRASE_ROUTE)
}
render () {
const { t } = this.context
return (
<div className="select-action">
<div className="app-header__logo-container">
<img
className="app-header__metafox-logo app-header__metafox-logo--horizontal"
src="/images/logo/metamask-logo-horizontal.svg"
height={30}
/>
<img
className="app-header__metafox-logo app-header__metafox-logo--icon"
src="/images/logo/metamask-fox.svg"
height={42}
width={42}
/>
</div>
<div className="select-action__wrapper">
<div className="select-action__body">
<div className="select-action__body-header">
{ t('newToMetaMask') }
</div>
<div className="select-action__select-buttons">
<div className="select-action__select-button">
<div className="select-action__button-content">
<div className="select-action__button-symbol">
<img src="/images/download-alt.svg" />
</div>
<div className="select-action__button-text-big">
{ t('noAlreadyHaveSeed') }
</div>
</div>
<Button
type="primary"
className="first-time-flow__button"
onClick={this.handleImport}
>
{ t('importWallet') }
</Button>
</div>
<div className="select-action__select-button">
<div className="select-action__button-content">
<div className="select-action__button-symbol">
<img src="/images/thin-plus.svg" />
</div>
<div className="select-action__button-text-big">
{ t('letsGoSetUp') }
</div>
</div>
<Button
type="confirm"
className="first-time-flow__button"
onClick={this.handleCreate}
>
{ t('createAWallet') }
</Button>
</div>
</div>
</div>
</div>
</div>
)
}
}

View File

@ -1,43 +1,42 @@
.welcome-page {
display: flex;
flex-direction: column;
justify-content: center;
justify-content: flex-start;
align-items: center;
width: 400px;
max-width: 442px;
padding: 0 18px;
color: black;
&__wrapper {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
align-items: flex-start;
height: 100%;
margin-top: 110px;
}
&__header {
font-size: 1.5rem;
margin-bottom: 14px;
font-size: 28px;
margin-bottom: 22px;
margin-top: 50px;
}
&__description {
text-align: center;
div {
font-size: 16px;
}
@media screen and (max-width: 575px) {
font-size: .9rem;
}
}
&__button {
height: 54px;
width: 198px;
font-family: Roboto;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, .14);
color: $white;
font-size: 1.25rem;
.first-time-flow__button {
width: 184px;
font-weight: 500;
text-transform: uppercase;
margin: 35px 0 14px;
transition: 200ms ease-in-out;
background-color: rgba(247, 134, 28, .9);
margin-top: 44px;
}
}

View File

@ -3,7 +3,7 @@ import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import Mascot from '../../../mascot'
import Button from '../../../button'
import { INITIALIZE_CREATE_PASSWORD_ROUTE, INITIALIZE_NOTICE_ROUTE } from '../../../../routes'
import { INITIALIZE_SELECT_ACTION_ROUTE, INITIALIZE_UNIQUE_IMAGE_ROUTE } from '../../../../routes'
export default class Welcome extends PureComponent {
static propTypes = {
@ -25,12 +25,12 @@ export default class Welcome extends PureComponent {
const { history, isInitialized } = this.props
if (isInitialized) {
history.push(INITIALIZE_NOTICE_ROUTE)
history.push(INITIALIZE_UNIQUE_IMAGE_ROUTE)
}
}
handleContinue = () => {
this.props.history.push(INITIALIZE_CREATE_PASSWORD_ROUTE)
this.props.history.push(INITIALIZE_SELECT_ACTION_ROUTE)
}
render () {
@ -41,22 +41,22 @@ export default class Welcome extends PureComponent {
<div className="welcome-page">
<Mascot
animationEventEmitter={this.animationEventEmitter}
width="225"
height="225"
width="125"
height="125"
/>
<div className="welcome-page__header">
{ t('welcome') }
</div>
<div className="welcome-page__description">
<div>{ t('metamaskDescription') }</div>
<div>{ t('holdEther') }</div>
<div>{ t('happyToSeeYou') }</div>
</div>
<Button
type="first-time"
type="confirm"
className="first-time-flow__button"
onClick={this.handleContinue}
>
{ t('continue') }
{ t('getStarted') }
</Button>
</div>
</div>

View File

@ -10,15 +10,12 @@ import {
INITIALIZE_SEED_PHRASE_ROUTE,
RESTORE_VAULT_ROUTE,
CONFIRM_TRANSACTION_ROUTE,
NOTICE_ROUTE,
CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE,
} from '../../../routes'
export default class Home extends PureComponent {
static propTypes = {
history: PropTypes.object,
noActiveNotices: PropTypes.bool,
lostAccounts: PropTypes.array,
forgottenPassword: PropTypes.bool,
seedWords: PropTypes.string,
suggestedTokens: PropTypes.object,
@ -45,18 +42,11 @@ export default class Home extends PureComponent {
render () {
const {
noActiveNotices,
lostAccounts,
forgottenPassword,
seedWords,
providerRequests,
} = this.props
// notices
if (!noActiveNotices || (lostAccounts && lostAccounts.length > 0)) {
return <Redirect to={{ pathname: NOTICE_ROUTE }} />
}
// seed words
if (seedWords) {
return <Redirect to={{ pathname: INITIALIZE_SEED_PHRASE_ROUTE }}/>

View File

@ -25,7 +25,9 @@ const INITIALIZE_IMPORT_ACCOUNT_ROUTE = '/initialize/create-password/import-acco
const INITIALIZE_IMPORT_WITH_SEED_PHRASE_ROUTE = '/initialize/create-password/import-with-seed-phrase'
const INITIALIZE_UNIQUE_IMAGE_ROUTE = '/initialize/create-password/unique-image'
const INITIALIZE_NOTICE_ROUTE = '/initialize/notice'
const INITIALIZE_SELECT_ACTION_ROUTE = '/initialize/select-action'
const INITIALIZE_SEED_PHRASE_ROUTE = '/initialize/seed-phrase'
const INITIALIZE_END_OF_FLOW_ROUTE = '/initialize/end-of-flow'
const INITIALIZE_CONFIRM_SEED_PHRASE_ROUTE = '/initialize/seed-phrase/confirm'
const CONFIRM_TRANSACTION_ROUTE = '/confirm-transaction'
@ -64,8 +66,10 @@ module.exports = {
INITIALIZE_IMPORT_WITH_SEED_PHRASE_ROUTE,
INITIALIZE_UNIQUE_IMAGE_ROUTE,
INITIALIZE_NOTICE_ROUTE,
INITIALIZE_SELECT_ACTION_ROUTE,
INITIALIZE_SEED_PHRASE_ROUTE,
INITIALIZE_CONFIRM_SEED_PHRASE_ROUTE,
INITIALIZE_END_OF_FLOW_ROUTE,
CONFIRM_TRANSACTION_ROUTE,
CONFIRM_SEND_ETHER_PATH,
CONFIRM_SEND_TOKEN_PATH,