mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-11-22 01:47:00 +01:00
I5849 incremental account security (#6874)
* Implements ability to defer seed phrase backup to later * Adds incremental-security.spec.js, including test dapp that sends signed tx with stand alone localhost provider * Update metamask-responsive-ui for incremental account security changes * Update backup-notification style and fix responsiveness of seed phrase screen * Remove uneeded files from send-eth-with-private-key-test/ * Apply linguist flags in .gitattributes for send-eth-with-private-key-test/ethereumjs-tx.js * Improve docs in controllers/onboarding.js * Clean up metamask-extension/test/e2e/send-eth-with-private-key-test/index.html * Remove unnecessary newlines in a couple first-time-flow/ files * Fix import of backup-notification in home.component * Fix git attrs file
This commit is contained in:
parent
189e126f61
commit
3eff478775
2
.gitattributes
vendored
2
.gitattributes
vendored
@ -4,3 +4,5 @@ CHANGELOG.md merge=union
|
||||
# we're using the dependencies we expect to be using
|
||||
package-lock.json linguist-generated=false
|
||||
yarn.lock linguist-generated=false
|
||||
|
||||
test/e2e/send-eth-with-private-key-test/ethereumjs-tx.js linguist-vendored linguist-generated
|
||||
|
@ -208,6 +208,9 @@
|
||||
"backToAll": {
|
||||
"message": "Back to All"
|
||||
},
|
||||
"backupNow": {
|
||||
"message": "Backup now"
|
||||
},
|
||||
"balance": {
|
||||
"message": "Balance"
|
||||
},
|
||||
@ -1310,6 +1313,9 @@
|
||||
"deleteNetworkDescription": {
|
||||
"message": "Are you sure you want to delete this network?"
|
||||
},
|
||||
"remindMeLater": {
|
||||
"message": "Remind me later"
|
||||
},
|
||||
"restoreFromSeed": {
|
||||
"message": "Restore account?"
|
||||
},
|
||||
|
3
app/images/meta-shield.svg
Normal file
3
app/images/meta-shield.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="20" height="24" viewBox="0 0 20 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 9.71495V3.57158L10 0.872803L20 3.57158V9.71495C20 16.0442 15.7999 21.4041 10 23.2357C4.19995 21.4041 0 16.0442 0 9.71495ZM15.3879 10.7277L15.6232 10.898L15.0819 11.5366L15.9057 14.1001L15.1431 16.7158L12.4738 15.9779L11.956 16.4036L10.9014 17.1367H9.09833L8.04379 16.4036L7.52596 15.9779L4.85671 16.7158L4.09874 14.1001L4.91784 11.5366L4.37644 10.898L4.61188 10.7277L4.23524 10.3825L4.52242 10.1553L4.14578 9.8669L4.4 9.6777L3.99989 7.74783L4.59302 5.95527L8.41104 7.38838H11.5887L15.4067 5.95527L15.9999 7.74783L15.6045 9.6777L15.854 9.8669L15.4773 10.1553L15.7644 10.3825L15.3879 10.7277ZM7.38499 13.1652L8.47065 12.6641L8.92346 13.6225L7.38499 13.1652ZM10.7685 13.6225L11.2197 12.6641L12.307 13.1652L10.7685 13.6225ZM9.24343 14.9001H10.4439L10.6519 15.0662L10.7689 16.178L10.6607 16.0674H9.02677L8.92279 16.178L9.03541 15.0662L9.24343 14.9001Z" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1018 B |
43
app/scripts/controllers/onboarding.js
Normal file
43
app/scripts/controllers/onboarding.js
Normal file
@ -0,0 +1,43 @@
|
||||
const ObservableStore = require('obs-store')
|
||||
const extend = require('xtend')
|
||||
|
||||
/**
|
||||
* @typedef {Object} InitState
|
||||
* @property {Boolean} seedPhraseBackedUp Indicates whether the user has completed the seed phrase backup challenge
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} OnboardingOptions
|
||||
* @property {InitState} initState The initial controller state
|
||||
*/
|
||||
|
||||
/**
|
||||
* Controller responsible for maintaining
|
||||
* a cache of account balances in local storage
|
||||
*/
|
||||
class OnboardingController {
|
||||
/**
|
||||
* Creates a new controller instance
|
||||
*
|
||||
* @param {OnboardingOptions} [opts] Controller configuration parameters
|
||||
*/
|
||||
constructor (opts = {}) {
|
||||
const initState = extend({
|
||||
seedPhraseBackedUp: null,
|
||||
}, opts.initState)
|
||||
this.store = new ObservableStore(initState)
|
||||
}
|
||||
|
||||
setSeedPhraseBackedUp (newSeedPhraseBackUpState) {
|
||||
this.store.updateState({
|
||||
seedPhraseBackedUp: newSeedPhraseBackUpState,
|
||||
})
|
||||
}
|
||||
|
||||
getSeedPhraseBackedUp () {
|
||||
return this.store.getState().seedPhraseBackedUp
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = OnboardingController
|
@ -28,6 +28,7 @@ const PreferencesController = require('./controllers/preferences')
|
||||
const AppStateController = require('./controllers/app-state')
|
||||
const InfuraController = require('./controllers/infura')
|
||||
const CachedBalancesController = require('./controllers/cached-balances')
|
||||
const OnboardingController = require('./controllers/onboarding')
|
||||
const RecentBlocksController = require('./controllers/recent-blocks')
|
||||
const MessageManager = require('./lib/message-manager')
|
||||
const PersonalMessageManager = require('./lib/personal-message-manager')
|
||||
@ -158,6 +159,10 @@ module.exports = class MetamaskController extends EventEmitter {
|
||||
initState: initState.CachedBalancesController,
|
||||
})
|
||||
|
||||
this.onboardingController = new OnboardingController({
|
||||
initState: initState.OnboardingController,
|
||||
})
|
||||
|
||||
// ensure accountTracker updates balances after network change
|
||||
this.networkController.on('networkDidChange', () => {
|
||||
this.accountTracker._updateAccounts()
|
||||
@ -262,6 +267,7 @@ module.exports = class MetamaskController extends EventEmitter {
|
||||
NetworkController: this.networkController.store,
|
||||
InfuraController: this.infuraController.store,
|
||||
CachedBalancesController: this.cachedBalancesController.store,
|
||||
OnboardingController: this.onboardingController.store,
|
||||
})
|
||||
|
||||
this.memStore = new ComposableObservableStore(null, {
|
||||
@ -283,6 +289,7 @@ module.exports = class MetamaskController extends EventEmitter {
|
||||
ShapeshiftController: this.shapeshiftController,
|
||||
InfuraController: this.infuraController.store,
|
||||
ProviderApprovalController: this.providerApprovalController.store,
|
||||
OnboardingController: this.onboardingController.store,
|
||||
})
|
||||
this.memStore.subscribe(this.sendUpdate.bind(this))
|
||||
}
|
||||
@ -398,6 +405,7 @@ module.exports = class MetamaskController extends EventEmitter {
|
||||
const txController = this.txController
|
||||
const networkController = this.networkController
|
||||
const providerApprovalController = this.providerApprovalController
|
||||
const onboardingController = this.onboardingController
|
||||
|
||||
return {
|
||||
// etc
|
||||
@ -501,6 +509,9 @@ module.exports = class MetamaskController extends EventEmitter {
|
||||
rejectProviderRequestByOrigin: providerApprovalController.rejectProviderRequestByOrigin.bind(providerApprovalController),
|
||||
forceApproveProviderRequestByOrigin: providerApprovalController.forceApproveProviderRequestByOrigin.bind(providerApprovalController),
|
||||
clearApprovedOrigins: providerApprovalController.clearApprovedOrigins.bind(providerApprovalController),
|
||||
|
||||
// onboarding controller
|
||||
setSeedPhraseBackedUp: nodeify(onboardingController.setSeedPhraseBackedUp, onboardingController),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -13,6 +13,7 @@
|
||||
"dapp": "static-server test/e2e/contract-test --port 8080",
|
||||
"dapp-chain": "GANACHE_ARGS='-b 2' concurrently -k -n ganache,dapp -p '[{time}][{name}]' 'yarn ganache:start' 'sleep 5 && static-server test/e2e/contract-test --port 8080'",
|
||||
"watch:test:unit": "nodemon --exec \"yarn test:unit\" ./test ./app ./ui",
|
||||
"sendwithprivatedapp": "static-server test/e2e/send-eth-with-private-key-test --port 8080",
|
||||
"test:unit": "cross-env METAMASK_ENV=test mocha --exit --require test/setup.js --recursive \"test/unit/**/*.js\" \"ui/app/**/*.test.js\"",
|
||||
"test:single": "cross-env METAMASK_ENV=test mocha --require test/helper.js",
|
||||
"test:integration": "yarn test:integration:build && yarn test:flat",
|
||||
|
295
test/e2e/incremental-security.spec.js
Normal file
295
test/e2e/incremental-security.spec.js
Normal file
@ -0,0 +1,295 @@
|
||||
const path = require('path')
|
||||
const assert = require('assert')
|
||||
const webdriver = require('selenium-webdriver')
|
||||
const { By, until } = webdriver
|
||||
const {
|
||||
delay,
|
||||
buildChromeWebDriver,
|
||||
buildFirefoxWebdriver,
|
||||
installWebExt,
|
||||
getExtensionIdChrome,
|
||||
getExtensionIdFirefox,
|
||||
} = require('./func')
|
||||
const {
|
||||
assertElementNotPresent,
|
||||
checkBrowserForConsoleErrors,
|
||||
closeAllWindowHandlesExcept,
|
||||
findElement,
|
||||
findElements,
|
||||
loadExtension,
|
||||
openNewPage,
|
||||
verboseReportOnFailure,
|
||||
} = require('./helpers')
|
||||
const fetchMockResponses = require('./fetch-mocks.js')
|
||||
|
||||
describe('MetaMask', function () {
|
||||
let extensionId
|
||||
let driver
|
||||
let publicAddress
|
||||
|
||||
const tinyDelayMs = 200
|
||||
const regularDelayMs = tinyDelayMs * 2
|
||||
const largeDelayMs = regularDelayMs * 2
|
||||
|
||||
this.timeout(0)
|
||||
this.bail(true)
|
||||
|
||||
before(async function () {
|
||||
let extensionUrl
|
||||
switch (process.env.SELENIUM_BROWSER) {
|
||||
case 'chrome': {
|
||||
const extPath = path.resolve('dist/chrome')
|
||||
driver = buildChromeWebDriver(extPath)
|
||||
extensionId = await getExtensionIdChrome(driver)
|
||||
await delay(largeDelayMs)
|
||||
extensionUrl = `chrome-extension://${extensionId}/home.html`
|
||||
break
|
||||
}
|
||||
case 'firefox': {
|
||||
const extPath = path.resolve('dist/firefox')
|
||||
driver = buildFirefoxWebdriver()
|
||||
await installWebExt(driver, extPath)
|
||||
await delay(largeDelayMs)
|
||||
extensionId = await getExtensionIdFirefox(driver)
|
||||
extensionUrl = `moz-extension://${extensionId}/home.html`
|
||||
break
|
||||
}
|
||||
}
|
||||
// Depending on the state of the application built into the above directory (extPath) and the value of
|
||||
// METAMASK_DEBUG we will see different post-install behaviour and possibly some extra windows. Here we
|
||||
// are closing any extraneous windows to reset us to a single window before continuing.
|
||||
const [tab1] = await driver.getAllWindowHandles()
|
||||
await closeAllWindowHandlesExcept(driver, [tab1])
|
||||
await driver.switchTo().window(tab1)
|
||||
await driver.get(extensionUrl)
|
||||
})
|
||||
|
||||
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 + '\')) }); } else if ' +
|
||||
'(args[0].match(/chromeextensionmm/)) { return ' +
|
||||
'Promise.resolve({ json: () => Promise.resolve(JSON.parse(\'' + fetchMockResponses.metametrics + '\')) }); } 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); };' +
|
||||
'function cancelInfuraRequest(requestDetails) {' +
|
||||
'console.log("Canceling: " + requestDetails.url);' +
|
||||
'return {' +
|
||||
'cancel: true' +
|
||||
'};' +
|
||||
' }' +
|
||||
'window.chrome && window.chrome.webRequest && window.chrome.webRequest.onBeforeRequest.addListener(' +
|
||||
'cancelInfuraRequest,' +
|
||||
'{urls: ["https://*.infura.io/*"]},' +
|
||||
'["blocking"]' +
|
||||
');'
|
||||
)
|
||||
})
|
||||
|
||||
afterEach(async function () {
|
||||
if (process.env.SELENIUM_BROWSER === 'chrome') {
|
||||
const errors = await checkBrowserForConsoleErrors(driver)
|
||||
if (errors.length) {
|
||||
const errorReports = errors.map(err => err.message)
|
||||
const errorMessage = `Errors found in browser console:\n${errorReports.join('\n')}`
|
||||
console.error(new Error(errorMessage))
|
||||
}
|
||||
}
|
||||
if (this.currentTest.state === 'failed') {
|
||||
await verboseReportOnFailure(driver, this.currentTest)
|
||||
}
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await driver.quit()
|
||||
})
|
||||
|
||||
describe('Going through the first time flow, but skipping the seed phrase challenge', () => {
|
||||
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('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('clicks the "No thanks" option on the metametrics opt-in screen', async () => {
|
||||
const optOutButton = await findElement(driver, By.css('.btn-default'))
|
||||
optOutButton.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'))
|
||||
|
||||
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('skips the seed phrase challenge', async () => {
|
||||
const buttons = await findElements(driver, By.css('.first-time-flow__button'))
|
||||
await buttons[0].click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const detailsButton = await findElement(driver, By.css('.wallet-view__details-button'))
|
||||
await detailsButton.click()
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
|
||||
it('gets the current accounts address', async () => {
|
||||
const addressInput = await findElement(driver, By.css('.qr-ellip-address'))
|
||||
publicAddress = await addressInput.getAttribute('value')
|
||||
|
||||
const accountModal = await driver.findElement(By.css('span .modal'))
|
||||
|
||||
await driver.executeScript("document.querySelector('.account-modal-close').click()")
|
||||
|
||||
await driver.wait(until.stalenessOf(accountModal))
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
describe('send to current account from dapp with different provider', () => {
|
||||
let extension
|
||||
|
||||
it('switches to dapp screen', async () => {
|
||||
const windowHandles = await driver.getAllWindowHandles()
|
||||
extension = windowHandles[0]
|
||||
|
||||
await openNewPage(driver, 'http://127.0.0.1:8080/')
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
|
||||
it('sends eth to the current account', async () => {
|
||||
const addressInput = await findElement(driver, By.css('#address'))
|
||||
await addressInput.sendKeys(publicAddress)
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const sendButton = await findElement(driver, By.css('#send'))
|
||||
await sendButton.click()
|
||||
|
||||
const txStatus = await findElement(driver, By.css('#success'))
|
||||
await driver.wait(until.elementTextMatches(txStatus, /Success/), 15000)
|
||||
})
|
||||
|
||||
it('switches back to MetaMask', async () => {
|
||||
await driver.switchTo().window(extension)
|
||||
})
|
||||
|
||||
it('should have the correct amount of eth', async () => {
|
||||
const balances = await findElements(driver, By.css('.currency-display-component__text'))
|
||||
await driver.wait(until.elementTextMatches(balances[0], /1/), 15000)
|
||||
const balance = await balances[0].getText()
|
||||
|
||||
assert.equal(balance, '1')
|
||||
})
|
||||
})
|
||||
|
||||
describe('backs up the seed phrase', () => {
|
||||
it('should show a backup reminder', async () => {
|
||||
const backupReminder = await findElements(driver, By.css('.backup-notification'))
|
||||
assert.equal(backupReminder.length, 1)
|
||||
})
|
||||
|
||||
it('should take the user to the seedphrase backup screen', async () => {
|
||||
const backupButton = await findElement(driver, By.css('.backup-notification__submit-button'))
|
||||
await backupButton.click()
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
|
||||
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)
|
||||
|
||||
seedPhrase = await driver.findElement(By.css('.reveal-seed-phrase__secret-words')).getText()
|
||||
assert.equal(seedPhrase.split(' ').length, 12)
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const nextScreen = (await findElements(driver, By.css('button.first-time-flow__button')))[1]
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
it('can retype the seed phrase', async () => {
|
||||
const words = seedPhrase.split(' ')
|
||||
|
||||
await retypeSeedPhrase(words)
|
||||
|
||||
const confirm = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`))
|
||||
await confirm.click()
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
|
||||
it('should have the correct amount of eth', async () => {
|
||||
const balances = await findElements(driver, By.css('.currency-display-component__text'))
|
||||
await driver.wait(until.elementTextMatches(balances[0], /1/), 15000)
|
||||
const balance = await balances[0].getText()
|
||||
|
||||
assert.equal(balance, '1')
|
||||
})
|
||||
|
||||
it('should not show a backup reminder', async () => {
|
||||
await assertElementNotPresent(webdriver, driver, By.css('.backup-notification'))
|
||||
})
|
||||
})
|
||||
})
|
@ -156,7 +156,7 @@ describe('MetaMask', function () {
|
||||
assert.equal(seedPhrase.split(' ').length, 12)
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const nextScreen = await findElement(driver, By.css('button.first-time-flow__button'))
|
||||
const nextScreen = (await findElements(driver, By.css('button.first-time-flow__button')))[1]
|
||||
await nextScreen.click()
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
|
@ -161,7 +161,7 @@ describe('MetaMask', function () {
|
||||
assert.equal(seedPhrase.split(' ').length, 12)
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const nextScreen = await findElement(driver, By.css('button.first-time-flow__button'))
|
||||
const nextScreen = (await findElements(driver, By.css('button.first-time-flow__button')))[1]
|
||||
await nextScreen.click()
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
|
@ -6,7 +6,7 @@ set -u
|
||||
set -o pipefail
|
||||
|
||||
export PATH="$PATH:./node_modules/.bin"
|
||||
export GANACHE_ARGS='--quiet --blockTime 2'
|
||||
export GANACHE_ARGS='--blockTime 2 --quiet'
|
||||
|
||||
concurrently --kill-others \
|
||||
--names 'ganache,dapp,e2e' \
|
||||
@ -32,6 +32,7 @@ concurrently --kill-others \
|
||||
'yarn ganache:start' \
|
||||
'sleep 5 && mocha test/e2e/from-import-ui.spec'
|
||||
|
||||
|
||||
export GANACHE_ARGS="$GANACHE_ARGS --deterministic --account=0x53CB0AB5226EEBF4D872113D98332C1555DC304443BEE1CF759D15798D3C55A9,25000000000000000000"
|
||||
concurrently --kill-others \
|
||||
--names 'ganache,e2e' \
|
||||
@ -39,3 +40,12 @@ concurrently --kill-others \
|
||||
--success first \
|
||||
'npm run ganache:start' \
|
||||
'sleep 5 && mocha test/e2e/send-edit.spec'
|
||||
|
||||
export GANACHE_ARGS="$GANACHE_ARGS --deterministic --account=0x250F458997A364988956409A164BA4E16F0F99F916ACDD73ADCD3A1DE30CF8D1,0 --account=0x53CB0AB5226EEBF4D872113D98332C1555DC304443BEE1CF759D15798D3C55A9,25000000000000000000"
|
||||
concurrently --kill-others \
|
||||
--names 'ganache,sendwithprivatedapp,e2e' \
|
||||
--prefix '[{time}][{name}]' \
|
||||
--success first \
|
||||
'npm run ganache:start' \
|
||||
'npm run sendwithprivatedapp' \
|
||||
'sleep 5 && mocha test/e2e/incremental-security.spec'
|
||||
|
711
test/e2e/send-eth-with-private-key-test/ethereumjs-tx.js
generated
vendored
Normal file
711
test/e2e/send-eth-with-private-key-test/ethereumjs-tx.js
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
17
test/e2e/send-eth-with-private-key-test/index.html
Normal file
17
test/e2e/send-eth-with-private-key-test/index.html
Normal file
@ -0,0 +1,17 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>E2E Test Dapp</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="success"></div>
|
||||
<input id="address" />
|
||||
<button id="send">Send with private key</button>
|
||||
|
||||
|
||||
<script src="web3js.js"></script>
|
||||
<script src="ethereumjs-tx.js"></script>
|
||||
<script src="send-eth-with-private-key.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
@ -0,0 +1,28 @@
|
||||
/* eslint-disable */
|
||||
var Tx = ethereumjs.Tx
|
||||
var privateKey = ethereumjs.Buffer.Buffer.from('53CB0AB5226EEBF4D872113D98332C1555DC304443BEE1CF759D15798D3C55A9', 'hex')
|
||||
|
||||
const web3 = new Web3(new Web3.providers.HttpProvider(`http://localhost:8545`))
|
||||
|
||||
const sendButton = document.getElementById('send')
|
||||
|
||||
sendButton.addEventListener('click', function () {
|
||||
var rawTx = {
|
||||
nonce: '0x00',
|
||||
gasPrice: '0x09184e72a000',
|
||||
gasLimit: '0x22710',
|
||||
value: '0xde0b6b3a7640000',
|
||||
r: '0x25a1bc499cd8799a2ece0fcba0df6e666e54a6e2b4e18c09838e2b621c10db71',
|
||||
s: '0x6cf83e6e8f6e82a0a1d7bd10bc343fc0ae4b096c1701aa54e6389d447f98ac6f',
|
||||
v: '0x2d46',
|
||||
to: document.getElementById('address').value,
|
||||
}
|
||||
var tx = new Tx(rawTx);
|
||||
tx.sign(privateKey);
|
||||
|
||||
var serializedTx = tx.serialize();
|
||||
|
||||
web3.eth.sendSignedTransaction('0x' + serializedTx.toString('hex')).on('receipt', (transactionResult) => {
|
||||
document.getElementById('success').innerHTML = `Successfully sent transaction: ${transactionResult.transactionHash}`
|
||||
})
|
||||
})
|
2
test/e2e/send-eth-with-private-key-test/web3js.js
Normal file
2
test/e2e/send-eth-with-private-key-test/web3js.js
Normal file
File diff suppressed because one or more lines are too long
@ -0,0 +1,50 @@
|
||||
import React, { PureComponent } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import Button from '../../ui/button'
|
||||
import {
|
||||
INITIALIZE_SEED_PHRASE_ROUTE,
|
||||
} from '../../../helpers/constants/routes'
|
||||
|
||||
export default class BackupNotification extends PureComponent {
|
||||
static propTypes = {
|
||||
history: PropTypes.object,
|
||||
showSeedPhraseBackupAfterOnboarding: PropTypes.func,
|
||||
}
|
||||
|
||||
static contextTypes = {
|
||||
t: PropTypes.func,
|
||||
metricsEvent: PropTypes.func,
|
||||
}
|
||||
|
||||
handleSubmit = () => {
|
||||
const { history, showSeedPhraseBackupAfterOnboarding } = this.props
|
||||
showSeedPhraseBackupAfterOnboarding()
|
||||
history.push(INITIALIZE_SEED_PHRASE_ROUTE)
|
||||
}
|
||||
|
||||
render () {
|
||||
const { t } = this.context
|
||||
|
||||
return (
|
||||
<div className="backup-notification">
|
||||
<div className="backup-notification__header">
|
||||
<img
|
||||
className="backup-notification__icon"
|
||||
src="images/meta-shield.svg"
|
||||
/>
|
||||
<div className="backup-notification__text">Backup your Secret Recovery code to keep your wallet and funds secure.</div>
|
||||
<i className="fa fa-info-circle"></i>
|
||||
</div>
|
||||
<div className="backup-notification__buttons">
|
||||
<Button
|
||||
type="primary"
|
||||
className="backup-notification__submit-button"
|
||||
onClick={this.handleSubmit}
|
||||
>
|
||||
{ t('backupNow') }
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
import { connect } from 'react-redux'
|
||||
import { withRouter } from 'react-router-dom'
|
||||
import { compose } from 'recompose'
|
||||
import BackupNotification from './backup-notification.component'
|
||||
import { showSeedPhraseBackupAfterOnboarding } from '../../../store/actions'
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
showSeedPhraseBackupAfterOnboarding: () => dispatch(showSeedPhraseBackupAfterOnboarding()),
|
||||
}
|
||||
}
|
||||
|
||||
export default compose(
|
||||
withRouter,
|
||||
connect(null, mapDispatchToProps)
|
||||
)(BackupNotification)
|
1
ui/app/components/app/backup-notification/index.js
Normal file
1
ui/app/components/app/backup-notification/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './backup-notification.container'
|
75
ui/app/components/app/backup-notification/index.scss
Normal file
75
ui/app/components/app/backup-notification/index.scss
Normal file
@ -0,0 +1,75 @@
|
||||
.backup-notification {
|
||||
background: rgba(36, 41, 46, 0.9);
|
||||
box-shadow: 0px 2px 10px rgba(0, 0, 0, 0.12);
|
||||
border-radius: 8px;
|
||||
height: 116px;
|
||||
padding: 16px;
|
||||
margin: 8px;
|
||||
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
justify-content: space-between;
|
||||
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
&__text {
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
font-size: 12px;
|
||||
color: #FFFFFF;
|
||||
margin-left: 10px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.fa-info-circle {
|
||||
color: #6A737D;
|
||||
}
|
||||
|
||||
&__ignore-button {
|
||||
border: 2px solid #6A737D;
|
||||
box-sizing: border-box;
|
||||
border-radius: 6px;
|
||||
color: $white;
|
||||
background-color: rgba(36, 41, 46, 0.9);
|
||||
height: 34px;
|
||||
width: 155px;
|
||||
padding: 0;
|
||||
|
||||
&:hover {
|
||||
border-color: #6A737D;
|
||||
background-color: #6A737D;
|
||||
}
|
||||
}
|
||||
|
||||
&__submit-button {
|
||||
border: 2px solid #6A737D;
|
||||
box-sizing: border-box;
|
||||
border-radius: 6px;
|
||||
color: $white;
|
||||
background-color: rgba(36, 41, 46, 0.9);
|
||||
height: 34px;
|
||||
width: 155px;
|
||||
padding: 0;
|
||||
|
||||
&:hover {
|
||||
background-color: #3b4046;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color:#141618;
|
||||
}
|
||||
}
|
||||
|
||||
&__buttons {
|
||||
display: flex;
|
||||
width: 130px;
|
||||
align-self: flex-end;
|
||||
}
|
||||
}
|
@ -81,3 +81,5 @@
|
||||
@import '../ui/toggle-button/index';
|
||||
|
||||
@import 'home-notification/index';
|
||||
|
||||
@import 'backup-notification/index';
|
||||
|
@ -73,6 +73,7 @@ function reduceApp (state, action) {
|
||||
networksTabSelectedRpcUrl: '',
|
||||
networksTabIsInAddMode: false,
|
||||
loadingMethodData: false,
|
||||
showingSeedPhraseBackupAfterOnboarding: false,
|
||||
}, state.appState)
|
||||
|
||||
switch (action.type) {
|
||||
@ -756,6 +757,16 @@ function reduceApp (state, action) {
|
||||
loadingMethodData: false,
|
||||
})
|
||||
|
||||
case actions.SHOW_SEED_PHRASE_BACKUP_AFTER_ONBOARDING:
|
||||
return extend(appState, {
|
||||
showingSeedPhraseBackupAfterOnboarding: true,
|
||||
})
|
||||
|
||||
case actions.HIDE_SEED_PHRASE_BACKUP_AFTER_ONBOARDING:
|
||||
return extend(appState, {
|
||||
showingSeedPhraseBackupAfterOnboarding: false,
|
||||
})
|
||||
|
||||
|
||||
default:
|
||||
return appState
|
||||
|
@ -3,6 +3,7 @@ import Authenticated from './authenticated.component'
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const { metamask: { isUnlocked, completedOnboarding } } = state
|
||||
|
||||
return {
|
||||
isUnlocked,
|
||||
completedOnboarding,
|
||||
|
@ -17,6 +17,7 @@ export default class ImportWithSeedPhrase extends PureComponent {
|
||||
static propTypes = {
|
||||
history: PropTypes.object,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
setSeedPhraseBackedUp: PropTypes.func,
|
||||
}
|
||||
|
||||
state = {
|
||||
@ -126,7 +127,7 @@ export default class ImportWithSeedPhrase extends PureComponent {
|
||||
}
|
||||
|
||||
const { password, seedPhrase } = this.state
|
||||
const { history, onSubmit } = this.props
|
||||
const { history, onSubmit, setSeedPhraseBackedUp } = this.props
|
||||
|
||||
try {
|
||||
await onSubmit(password, this.parseSeedPhrase(seedPhrase))
|
||||
@ -137,7 +138,10 @@ export default class ImportWithSeedPhrase extends PureComponent {
|
||||
name: 'Import Complete',
|
||||
},
|
||||
})
|
||||
|
||||
setSeedPhraseBackedUp(true).then(() => {
|
||||
history.push(INITIALIZE_END_OF_FLOW_ROUTE)
|
||||
})
|
||||
} catch (error) {
|
||||
this.setState({ seedPhraseError: error.message })
|
||||
}
|
||||
|
@ -0,0 +1,13 @@
|
||||
import { connect } from 'react-redux'
|
||||
import ImportWithSeedPhrase from './import-with-seed-phrase.component'
|
||||
import {
|
||||
setSeedPhraseBackedUp,
|
||||
} from '../../../../store/actions'
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
setSeedPhraseBackedUp: (seedPhraseBackupState) => dispatch(setSeedPhraseBackedUp(seedPhraseBackupState)),
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(null, mapDispatchToProps)(ImportWithSeedPhrase)
|
@ -1 +1 @@
|
||||
export { default } from './import-with-seed-phrase.component'
|
||||
export { default } from './import-with-seed-phrase.container'
|
||||
|
@ -30,6 +30,9 @@ export default class FirstTimeFlow extends PureComponent {
|
||||
isUnlocked: PropTypes.bool,
|
||||
unlockAccount: PropTypes.func,
|
||||
nextRoute: PropTypes.string,
|
||||
showingSeedPhraseBackupAfterOnboarding: PropTypes.bool,
|
||||
seedPhraseBackedUp: PropTypes.bool,
|
||||
verifySeedPhrase: PropTypes.func,
|
||||
}
|
||||
|
||||
state = {
|
||||
@ -38,9 +41,16 @@ export default class FirstTimeFlow extends PureComponent {
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
const { completedOnboarding, history, isInitialized, isUnlocked } = this.props
|
||||
const {
|
||||
completedOnboarding,
|
||||
history,
|
||||
isInitialized,
|
||||
isUnlocked,
|
||||
showingSeedPhraseBackupAfterOnboarding,
|
||||
seedPhraseBackedUp,
|
||||
} = this.props
|
||||
|
||||
if (completedOnboarding) {
|
||||
if (completedOnboarding && (!showingSeedPhraseBackupAfterOnboarding || seedPhraseBackedUp)) {
|
||||
history.push(DEFAULT_ROUTE)
|
||||
return
|
||||
}
|
||||
@ -88,6 +98,7 @@ export default class FirstTimeFlow extends PureComponent {
|
||||
|
||||
render () {
|
||||
const { seedPhrase, isImportedKeyring } = this.state
|
||||
const { verifySeedPhrase } = this.props
|
||||
|
||||
return (
|
||||
<div className="first-time-flow">
|
||||
@ -98,6 +109,7 @@ export default class FirstTimeFlow extends PureComponent {
|
||||
<SeedPhrase
|
||||
{ ...props }
|
||||
seedPhrase={seedPhrase}
|
||||
verifySeedPhrase={verifySeedPhrase}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
@ -5,16 +5,19 @@ import {
|
||||
createNewVaultAndGetSeedPhrase,
|
||||
createNewVaultAndRestore,
|
||||
unlockAndGetSeedPhrase,
|
||||
verifySeedPhrase,
|
||||
} from '../../store/actions'
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const { metamask: { completedOnboarding, isInitialized, isUnlocked } } = state
|
||||
const { metamask: { completedOnboarding, isInitialized, isUnlocked, seedPhraseBackedUp }, appState: { showingSeedPhraseBackupAfterOnboarding } } = state
|
||||
|
||||
return {
|
||||
completedOnboarding,
|
||||
isInitialized,
|
||||
isUnlocked,
|
||||
nextRoute: getFirstTimeFlowTypeRoute(state),
|
||||
showingSeedPhraseBackupAfterOnboarding,
|
||||
seedPhraseBackedUp,
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,6 +28,7 @@ const mapDispatchToProps = dispatch => {
|
||||
return dispatch(createNewVaultAndRestore(password, seedPhrase))
|
||||
},
|
||||
unlockAccount: password => dispatch(unlockAndGetSeedPhrase(password)),
|
||||
verifySeedPhrase: () => verifySeedPhrase(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -26,6 +26,10 @@
|
||||
|
||||
.app-header__metafox-logo {
|
||||
margin-bottom: 40px;
|
||||
|
||||
@media screen and (max-width: $break-small) {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -6,6 +6,7 @@ import Button from '../../../../components/ui/button'
|
||||
import {
|
||||
INITIALIZE_END_OF_FLOW_ROUTE,
|
||||
INITIALIZE_SEED_PHRASE_ROUTE,
|
||||
DEFAULT_ROUTE,
|
||||
} from '../../../../helpers/constants/routes'
|
||||
import { exportAsFile } from '../../../../helpers/utils/util'
|
||||
import DraggableSeed from './draggable-seed.component'
|
||||
@ -88,7 +89,7 @@ export default class ConfirmSeedPhrase extends PureComponent {
|
||||
}
|
||||
|
||||
handleSubmit = async () => {
|
||||
const { history } = this.props
|
||||
const { history, setSeedPhraseBackedUp, showingSeedPhraseBackupAfterOnboarding, hideSeedPhraseBackupAfterOnboarding } = this.props
|
||||
|
||||
if (!this.isValid()) {
|
||||
return
|
||||
@ -102,7 +103,15 @@ export default class ConfirmSeedPhrase extends PureComponent {
|
||||
name: 'Verify Complete',
|
||||
},
|
||||
})
|
||||
|
||||
setSeedPhraseBackedUp(true).then(() => {
|
||||
if (showingSeedPhraseBackupAfterOnboarding) {
|
||||
hideSeedPhraseBackupAfterOnboarding()
|
||||
history.push(DEFAULT_ROUTE)
|
||||
} else {
|
||||
history.push(INITIALIZE_END_OF_FLOW_ROUTE)
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error.message)
|
||||
}
|
||||
|
@ -0,0 +1,23 @@
|
||||
import { connect } from 'react-redux'
|
||||
import ConfirmSeedPhrase from './confirm-seed-phrase.component'
|
||||
import {
|
||||
setSeedPhraseBackedUp,
|
||||
hideSeedPhraseBackupAfterOnboarding,
|
||||
} from '../../../../store/actions'
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const { appState: { showingSeedPhraseBackupAfterOnboarding } } = state
|
||||
|
||||
return {
|
||||
showingSeedPhraseBackupAfterOnboarding,
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
setSeedPhraseBackedUp: (seedPhraseBackupState) => dispatch(setSeedPhraseBackedUp(seedPhraseBackupState)),
|
||||
hideSeedPhraseBackupAfterOnboarding: () => dispatch(hideSeedPhraseBackupAfterOnboarding()),
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ConfirmSeedPhrase)
|
@ -1 +1 @@
|
||||
export { default } from './confirm-seed-phrase.component'
|
||||
export { default } from './confirm-seed-phrase.container'
|
||||
|
@ -1 +1 @@
|
||||
export { default } from './reveal-seed-phrase.component'
|
||||
export { default } from './reveal-seed-phrase.container'
|
||||
|
@ -1,4 +1,12 @@
|
||||
.reveal-seed-phrase {
|
||||
@media screen and (max-width: 576px) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 96%;
|
||||
margin-left: 2%;
|
||||
margin-right: 2%;
|
||||
}
|
||||
|
||||
&__secret {
|
||||
position: relative;
|
||||
display: flex;
|
||||
@ -54,4 +62,12 @@
|
||||
button {
|
||||
margin-top: 0xp;
|
||||
}
|
||||
|
||||
&__buttons {
|
||||
display: flex;
|
||||
|
||||
.first-time-flow__button:last-of-type {
|
||||
margin-left: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ import PropTypes from 'prop-types'
|
||||
import classnames from 'classnames'
|
||||
import LockIcon from '../../../../components/ui/lock-icon'
|
||||
import Button from '../../../../components/ui/button'
|
||||
import { INITIALIZE_CONFIRM_SEED_PHRASE_ROUTE } from '../../../../helpers/constants/routes'
|
||||
import { INITIALIZE_CONFIRM_SEED_PHRASE_ROUTE, DEFAULT_ROUTE } from '../../../../helpers/constants/routes'
|
||||
import { exportAsFile } from '../../../../helpers/utils/util'
|
||||
|
||||
export default class RevealSeedPhrase extends PureComponent {
|
||||
@ -15,6 +15,8 @@ export default class RevealSeedPhrase extends PureComponent {
|
||||
static propTypes = {
|
||||
history: PropTypes.object,
|
||||
seedPhrase: PropTypes.string,
|
||||
setSeedPhraseBackedUp: PropTypes.func,
|
||||
setCompletedOnboarding: PropTypes.func,
|
||||
}
|
||||
|
||||
state = {
|
||||
@ -45,6 +47,24 @@ export default class RevealSeedPhrase extends PureComponent {
|
||||
history.push(INITIALIZE_CONFIRM_SEED_PHRASE_ROUTE)
|
||||
}
|
||||
|
||||
handleSkip = event => {
|
||||
event.preventDefault()
|
||||
const { history, setSeedPhraseBackedUp, setCompletedOnboarding } = this.props
|
||||
|
||||
this.context.metricsEvent({
|
||||
eventOpts: {
|
||||
category: 'Onboarding',
|
||||
action: 'Seed Phrase Setup',
|
||||
name: 'Remind me later',
|
||||
},
|
||||
})
|
||||
|
||||
Promise.all([setCompletedOnboarding(), setSeedPhraseBackedUp(false)])
|
||||
.then(() => {
|
||||
history.push(DEFAULT_ROUTE)
|
||||
})
|
||||
}
|
||||
|
||||
renderSecretWordsContainer () {
|
||||
const { t } = this.context
|
||||
const { seedPhrase } = this.props
|
||||
@ -129,6 +149,14 @@ export default class RevealSeedPhrase extends PureComponent {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="reveal-seed-phrase__buttons">
|
||||
<Button
|
||||
type="secondary"
|
||||
className="first-time-flow__button"
|
||||
onClick={this.handleSkip}
|
||||
>
|
||||
{ t('remindMeLater') }
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
className="first-time-flow__button"
|
||||
@ -138,6 +166,7 @@ export default class RevealSeedPhrase extends PureComponent {
|
||||
{ t('next') }
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,15 @@
|
||||
import { connect } from 'react-redux'
|
||||
import RevealSeedPhrase from './reveal-seed-phrase.component'
|
||||
import {
|
||||
setCompletedOnboarding,
|
||||
setSeedPhraseBackedUp,
|
||||
} from '../../../../store/actions'
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
setSeedPhraseBackedUp: (seedPhraseBackupState) => dispatch(setSeedPhraseBackedUp(seedPhraseBackupState)),
|
||||
setCompletedOnboarding: () => dispatch(setCompletedOnboarding()),
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(null, mapDispatchToProps)(RevealSeedPhrase)
|
@ -17,18 +17,31 @@ export default class SeedPhrase extends PureComponent {
|
||||
address: PropTypes.string,
|
||||
history: PropTypes.object,
|
||||
seedPhrase: PropTypes.string,
|
||||
verifySeedPhrase: PropTypes.func,
|
||||
}
|
||||
|
||||
state = {
|
||||
verifiedSeedPhrase: '',
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
const { seedPhrase, history } = this.props
|
||||
const { seedPhrase, history, verifySeedPhrase } = this.props
|
||||
|
||||
if (!seedPhrase) {
|
||||
verifySeedPhrase()
|
||||
.then(verifiedSeedPhrase => {
|
||||
if (!verifiedSeedPhrase) {
|
||||
history.push(DEFAULT_ROUTE)
|
||||
} else {
|
||||
this.setState({ verifiedSeedPhrase })
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { seedPhrase } = this.props
|
||||
const { verifiedSeedPhrase } = this.state
|
||||
|
||||
return (
|
||||
<DragDropContextProvider backend={HTML5Backend}>
|
||||
@ -41,7 +54,7 @@ export default class SeedPhrase extends PureComponent {
|
||||
render={props => (
|
||||
<ConfirmSeedPhrase
|
||||
{ ...props }
|
||||
seedPhrase={seedPhrase}
|
||||
seedPhrase={seedPhrase || verifiedSeedPhrase}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@ -51,7 +64,7 @@ export default class SeedPhrase extends PureComponent {
|
||||
render={props => (
|
||||
<RevealSeedPhrase
|
||||
{ ...props }
|
||||
seedPhrase={seedPhrase}
|
||||
seedPhrase={seedPhrase || verifiedSeedPhrase}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
@ -131,7 +131,7 @@ describe('ConfirmSeedPhrase Component', () => {
|
||||
assert.deepEqual(root.state().pendingSeedIndices, [2, 0, 1])
|
||||
})
|
||||
|
||||
it('should submit correctly', () => {
|
||||
it('should submit correctly', async () => {
|
||||
const originalSeed = ['鼠', '牛', '虎', '兔', '龍', '蛇', '馬', '羊', '猴', '雞', '狗', '豬']
|
||||
const metricsEventSpy = sinon.spy()
|
||||
const pushSpy = sinon.spy()
|
||||
@ -139,6 +139,7 @@ describe('ConfirmSeedPhrase Component', () => {
|
||||
{
|
||||
seedPhrase: '鼠 牛 虎 兔 龍 蛇 馬 羊 猴 雞 狗 豬',
|
||||
history: { push: pushSpy },
|
||||
setSeedPhraseBackedUp: () => Promise.resolve(),
|
||||
},
|
||||
{
|
||||
metricsEvent: metricsEventSpy,
|
||||
@ -157,6 +158,9 @@ describe('ConfirmSeedPhrase Component', () => {
|
||||
root.update()
|
||||
|
||||
root.find('.first-time-flow__button').simulate('click')
|
||||
|
||||
await (new Promise(resolve => setTimeout(resolve, 100)))
|
||||
|
||||
assert.deepEqual(metricsEventSpy.args[0][0], {
|
||||
eventOpts: {
|
||||
category: 'Onboarding',
|
||||
|
@ -6,6 +6,7 @@ import HomeNotification from '../../components/app/home-notification'
|
||||
import WalletView from '../../components/app/wallet-view'
|
||||
import TransactionView from '../../components/app/transaction-view'
|
||||
import ProviderApproval from '../provider-approval'
|
||||
import BackupNotification from '../../components/app/backup-notification'
|
||||
|
||||
import {
|
||||
RESTORE_VAULT_ROUTE,
|
||||
@ -38,6 +39,7 @@ export default class Home extends PureComponent {
|
||||
unsetMigratedPrivacyMode: PropTypes.func,
|
||||
viewingUnconnectedDapp: PropTypes.bool.isRequired,
|
||||
forceApproveProviderRequestByOrigin: PropTypes.func,
|
||||
shouldShowSeedPhraseReminder: PropTypes.bool,
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
@ -74,6 +76,7 @@ export default class Home extends PureComponent {
|
||||
unsetMigratedPrivacyMode,
|
||||
viewingUnconnectedDapp,
|
||||
forceApproveProviderRequestByOrigin,
|
||||
shouldShowSeedPhraseReminder,
|
||||
} = this.props
|
||||
|
||||
if (forgottenPassword) {
|
||||
@ -85,7 +88,6 @@ export default class Home extends PureComponent {
|
||||
<ProviderApproval providerRequest={providerRequests[0]} />
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="main-container">
|
||||
<div className="account-and-transaction-details">
|
||||
@ -124,6 +126,11 @@ export default class Home extends PureComponent {
|
||||
)
|
||||
: null
|
||||
}
|
||||
{
|
||||
shouldShowSeedPhraseReminder
|
||||
? (<BackupNotification />)
|
||||
: null
|
||||
}
|
||||
</TransactionView>
|
||||
)
|
||||
: null }
|
||||
|
@ -3,6 +3,7 @@ import { compose } from 'recompose'
|
||||
import { connect } from 'react-redux'
|
||||
import { withRouter } from 'react-router-dom'
|
||||
import { unconfirmedTransactionsCountSelector } from '../../selectors/confirm-transaction'
|
||||
import { getCurrentEthBalance } from '../../selectors/selectors'
|
||||
import {
|
||||
forceApproveProviderRequestByOrigin,
|
||||
unsetMigratedPrivacyMode,
|
||||
@ -21,7 +22,9 @@ const mapStateToProps = state => {
|
||||
featureFlags: {
|
||||
privacyMode,
|
||||
} = {},
|
||||
seedPhraseBackedUp,
|
||||
} = metamask
|
||||
const accountBalance = getCurrentEthBalance(state)
|
||||
const { forgottenPassword } = appState
|
||||
|
||||
const isUnconnected = Boolean(activeTab && privacyMode && !approvedOrigins[activeTab.origin])
|
||||
@ -36,6 +39,7 @@ const mapStateToProps = state => {
|
||||
showPrivacyModeNotification: migratedPrivacyMode,
|
||||
activeTab,
|
||||
viewingUnconnectedDapp: isUnconnected && isPopup,
|
||||
shouldShowSeedPhraseReminder: parseInt(accountBalance, 16) > 0 && !seedPhraseBackedUp,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -126,6 +126,7 @@ export default class NetworksTab extends PureComponent {
|
||||
|
||||
renderNetworksList () {
|
||||
const { networksToRender, selectedNetwork, networkIsSelected, networksTabIsInAddMode, networkDefaultedToProvider } = this.props
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classnames('networks-tab__networks-list', {
|
||||
|
@ -375,6 +375,15 @@ var actions = {
|
||||
LOADING_TOKEN_PARAMS_STARTED: 'LOADING_TOKEN_PARAMS_STARTED',
|
||||
loadingTokenParamsFinished,
|
||||
LOADING_TOKEN_PARAMS_FINISHED: 'LOADING_TOKEN_PARAMS_FINISHED',
|
||||
|
||||
setSeedPhraseBackedUp,
|
||||
showSeedPhraseBackupAfterOnboarding,
|
||||
SHOW_SEED_PHRASE_BACKUP_AFTER_ONBOARDING: 'SHOW_SEED_PHRASE_BACKUP_AFTER_ONBOARDING',
|
||||
hideSeedPhraseBackupAfterOnboarding,
|
||||
HIDE_SEED_PHRASE_BACKUP_AFTER_ONBOARDING: 'HIDE_SEED_PHRASE_BACKUP_AFTER_ONBOARDING',
|
||||
|
||||
verifySeedPhrase,
|
||||
SET_SEED_PHRASE_BACKED_UP_TO_TRUE: 'SET_SEED_PHRASE_BACKED_UP_TO_TRUE',
|
||||
}
|
||||
|
||||
module.exports = actions
|
||||
@ -2772,3 +2781,30 @@ function unsetMigratedPrivacyMode () {
|
||||
background.unsetMigratedPrivacyMode()
|
||||
}
|
||||
}
|
||||
|
||||
function setSeedPhraseBackedUp (seedPhraseBackupState) {
|
||||
return (dispatch) => {
|
||||
log.debug(`background.setSeedPhraseBackedUp`)
|
||||
return new Promise((resolve, reject) => {
|
||||
background.setSeedPhraseBackedUp(seedPhraseBackupState, (err) => {
|
||||
if (err) {
|
||||
dispatch(actions.displayWarning(err.message))
|
||||
return reject(err)
|
||||
}
|
||||
return forceUpdateMetamaskState(dispatch).then(() => resolve())
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function showSeedPhraseBackupAfterOnboarding () {
|
||||
return {
|
||||
type: actions.SHOW_SEED_PHRASE_BACKUP_AFTER_ONBOARDING,
|
||||
}
|
||||
}
|
||||
|
||||
function hideSeedPhraseBackupAfterOnboarding () {
|
||||
return {
|
||||
type: actions.HIDE_SEED_PHRASE_BACKUP_AFTER_ONBOARDING,
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user