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 filefeature/default_network_editable
parent
189e126f61
commit
3eff478775
After Width: | Height: | Size: 1018 B |
@ -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 |
@ -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')) |
||||
}) |
||||
}) |
||||
}) |
File diff suppressed because one or more lines are too long
@ -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}` |
||||
}) |
||||
}) |
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) |
@ -0,0 +1 @@ |
||||
export { default } from './backup-notification.container' |
@ -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; |
||||
} |
||||
} |
@ -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' |
||||
|
@ -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' |
||||
|
@ -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) |
Loading…
Reference in new issue