mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-11-22 01:47:00 +01:00
Refactor first time flow, remove seed phrase from state (#5994)
* Refactor and fix styling for first time flow. Remove seed phrase from persisted metamask state * Fix linting and tests * Fix translations, initialization notice routing * Fix drizzle tests * Fix e2e tests * Fix integration tests * Fix styling * Fix migration naming from 030 to 031 * Open extension in browser when user has not completed onboarding
This commit is contained in:
parent
69fcfa427b
commit
fba17d77de
@ -206,6 +206,9 @@
|
||||
"clickToAdd": {
|
||||
"message": "Click on $1 to add them to your account"
|
||||
},
|
||||
"clickToRevealSeed": {
|
||||
"message": "Click here to reveal secret words"
|
||||
},
|
||||
"close": {
|
||||
"message": "Close"
|
||||
},
|
||||
@ -227,6 +230,9 @@
|
||||
"confirmPassword": {
|
||||
"message": "Confirm Password"
|
||||
},
|
||||
"confirmSecretBackupPhrase": {
|
||||
"message": "Confirm your Secret Backup Phrase"
|
||||
},
|
||||
"confirmTransaction": {
|
||||
"message": "Confirm Transaction"
|
||||
},
|
||||
@ -314,6 +320,9 @@
|
||||
"createDen": {
|
||||
"message": "Create"
|
||||
},
|
||||
"createPassword": {
|
||||
"message": "Create Password"
|
||||
},
|
||||
"crypto": {
|
||||
"message": "Crypto",
|
||||
"description": "Exchange type (cryptocurrencies)"
|
||||
@ -403,6 +412,9 @@
|
||||
"downloadGoogleChrome": {
|
||||
"message": "Download Google Chrome"
|
||||
},
|
||||
"downloadSecretBackup": {
|
||||
"message": "Download this Secret Backup Phrase and keep it stored safely on an external encrypted hard drive or storage medium."
|
||||
},
|
||||
"downloadStateLogs": {
|
||||
"message": "Download State Logs"
|
||||
},
|
||||
@ -611,6 +623,9 @@
|
||||
"importAccountMsg": {
|
||||
"message": " Imported accounts will not be associated with your originally created MetaMask account seedphrase. Learn more about imported accounts "
|
||||
},
|
||||
"importAccountSeedPhrase": {
|
||||
"message": "Import an Account with Seed Phrase"
|
||||
},
|
||||
"importAnAccount": {
|
||||
"message": "Import an account"
|
||||
},
|
||||
@ -624,6 +639,9 @@
|
||||
"importUsingSeed": {
|
||||
"message": "Import using account seed phrase"
|
||||
},
|
||||
"importWithSeedPhrase": {
|
||||
"message": "Import with seed phrase"
|
||||
},
|
||||
"info": {
|
||||
"message": "Info"
|
||||
},
|
||||
@ -731,6 +749,9 @@
|
||||
"mainnet": {
|
||||
"message": "Main Ethereum Network"
|
||||
},
|
||||
"memorizePhrase": {
|
||||
"message": "Memorize this phrase."
|
||||
},
|
||||
"menu": {
|
||||
"message": "Menu"
|
||||
},
|
||||
@ -1096,12 +1117,24 @@
|
||||
"searchResults": {
|
||||
"message": "Search Results"
|
||||
},
|
||||
"secretBackupPhrase": {
|
||||
"message": "Secret Backup Phrase"
|
||||
},
|
||||
"secretBackupPhraseDescription": {
|
||||
"message": "Your secret backup phrase makes it easy to back up and restore your account."
|
||||
},
|
||||
"secretBackupPhraseWarning": {
|
||||
"message": "WARNING: Never disclose your backup phrase. Anyone with this phrase can take your Ether forever."
|
||||
},
|
||||
"secretPhrase": {
|
||||
"message": "Enter your secret twelve word phrase here to restore your vault."
|
||||
},
|
||||
"secondsShorthand": {
|
||||
"message": "Sec"
|
||||
},
|
||||
"seedPhrasePlaceholder": {
|
||||
"message": "Separate each word with a single space"
|
||||
},
|
||||
"seedPhraseReq": {
|
||||
"message": "Seed phrases are 12 words long"
|
||||
},
|
||||
@ -1111,6 +1144,9 @@
|
||||
"selectCurrency": {
|
||||
"message": "Select Currency"
|
||||
},
|
||||
"selectEachPhrase": {
|
||||
"message": "Please select each phrase in order to make sure it is correct."
|
||||
},
|
||||
"selectLocale": {
|
||||
"message": "Select Locale"
|
||||
},
|
||||
@ -1258,6 +1294,9 @@
|
||||
"step3HardwareWalletMsg": {
|
||||
"message": "Use your hardware account like you would with any Ethereum account. Log in to dApps, send Eth, buy and store ERC20 tokens and Non-Fungible tokens like CryptoKitties."
|
||||
},
|
||||
"storePhrase": {
|
||||
"message": "Store this phrase in a password manager like 1Password."
|
||||
},
|
||||
"submit": {
|
||||
"message": "Submit"
|
||||
},
|
||||
@ -1279,6 +1318,9 @@
|
||||
"testFaucet": {
|
||||
"message": "Test Faucet"
|
||||
},
|
||||
"tips": {
|
||||
"message": "Tips"
|
||||
},
|
||||
"to": {
|
||||
"message": "To"
|
||||
},
|
||||
@ -1477,6 +1519,9 @@
|
||||
"whatsThis": {
|
||||
"message": "What's this?"
|
||||
},
|
||||
"writePhrase": {
|
||||
"message": "Write this phrase on a piece of paper and store in a secure location. If you want even more security, write it down on multiple pieces of paper and store each in 2 - 3 different locations."
|
||||
},
|
||||
"yesLetsTry": {
|
||||
"message": "Yes, let's try"
|
||||
},
|
||||
@ -1492,6 +1537,15 @@
|
||||
"yourPrivateSeedPhrase": {
|
||||
"message": "Your private seed phrase"
|
||||
},
|
||||
"yourUniqueAccountImage": {
|
||||
"message": "Your unique account image"
|
||||
},
|
||||
"yourUniqueAccountImageDescription1": {
|
||||
"message": "This image was programmatically generated for you by your new account number."
|
||||
},
|
||||
"yourUniqueAccountImageDescription2": {
|
||||
"message": "You’ll see this image everytime you need to confirm a transaction."
|
||||
},
|
||||
"zeroGasPriceOnSpeedUpError": {
|
||||
"message":"Zero gas price on speed up"
|
||||
}
|
||||
|
@ -46,6 +46,7 @@ class PreferencesController {
|
||||
preferences: {
|
||||
useNativeCurrencyAsPrimaryCurrency: true,
|
||||
},
|
||||
completedOnboarding: false,
|
||||
}, opts.initState)
|
||||
|
||||
this.diagnostics = opts.diagnostics
|
||||
@ -516,6 +517,15 @@ class PreferencesController {
|
||||
return this.store.getState().preferences
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the completedOnboarding state to true, indicating that the user has completed the
|
||||
* onboarding process.
|
||||
*/
|
||||
completeOnboarding () {
|
||||
this.store.updateState({ completedOnboarding: true })
|
||||
return Promise.resolve(true)
|
||||
}
|
||||
|
||||
//
|
||||
// PRIVATE METHODS
|
||||
//
|
||||
|
@ -425,6 +425,7 @@ module.exports = class MetamaskController extends EventEmitter {
|
||||
setAccountLabel: nodeify(preferencesController.setAccountLabel, preferencesController),
|
||||
setFeatureFlag: nodeify(preferencesController.setFeatureFlag, preferencesController),
|
||||
setPreference: nodeify(preferencesController.setPreference, preferencesController),
|
||||
completeOnboarding: nodeify(preferencesController.completeOnboarding, preferencesController),
|
||||
addKnownMethodData: nodeify(preferencesController.addKnownMethodData, preferencesController),
|
||||
|
||||
// BlacklistController
|
||||
|
31
app/scripts/migrations/031.js
Normal file
31
app/scripts/migrations/031.js
Normal file
@ -0,0 +1,31 @@
|
||||
// next version number
|
||||
const version = 31
|
||||
const clone = require('clone')
|
||||
|
||||
/*
|
||||
* The purpose of this migration is to properly set the completedOnboarding flag baesd on the state
|
||||
* of the KeyringController.
|
||||
*/
|
||||
module.exports = {
|
||||
version,
|
||||
|
||||
migrate: async function (originalVersionedData) {
|
||||
const versionedData = clone(originalVersionedData)
|
||||
versionedData.meta.version = version
|
||||
const state = versionedData.data
|
||||
const newState = transformState(state)
|
||||
versionedData.data = newState
|
||||
return versionedData
|
||||
},
|
||||
}
|
||||
|
||||
function transformState (state) {
|
||||
const { KeyringController, PreferencesController } = state
|
||||
|
||||
if (KeyringController && PreferencesController) {
|
||||
const { vault } = KeyringController
|
||||
PreferencesController.completedOnboarding = Boolean(vault)
|
||||
}
|
||||
|
||||
return state
|
||||
}
|
@ -41,4 +41,5 @@ module.exports = [
|
||||
require('./028'),
|
||||
require('./029'),
|
||||
require('./030'),
|
||||
require('./031'),
|
||||
]
|
||||
|
@ -5,7 +5,7 @@ const {getShouldUseNewUi} = require('../../ui/app/selectors')
|
||||
const startPopup = require('./popup-core')
|
||||
const PortStream = require('extension-port-stream')
|
||||
const { getEnvironmentType } = require('./lib/util')
|
||||
const { ENVIRONMENT_TYPE_NOTIFICATION } = require('./lib/enums')
|
||||
const { ENVIRONMENT_TYPE_NOTIFICATION, ENVIRONMENT_TYPE_FULLSCREEN } = require('./lib/enums')
|
||||
const extension = require('extensionizer')
|
||||
const ExtensionPlatform = require('./platforms/extension')
|
||||
const NotificationManager = require('./lib/notification-manager')
|
||||
@ -49,7 +49,14 @@ async function start () {
|
||||
if (err) return displayCriticalError(err)
|
||||
|
||||
const state = store.getState()
|
||||
let betaUIState = Boolean(state.featureFlags && state.featureFlags.betaUI)
|
||||
const { metamask: { completedOnboarding, featureFlags } = {} } = state
|
||||
|
||||
if (!completedOnboarding && windowType !== ENVIRONMENT_TYPE_FULLSCREEN) {
|
||||
global.platform.openExtensionInBrowser()
|
||||
return
|
||||
}
|
||||
|
||||
let betaUIState = Boolean(featureFlags && featureFlags.betaUI)
|
||||
const useBetaCss = getShouldUseNewUi(state)
|
||||
|
||||
let css = useBetaCss ? NewMetaMaskUiCss() : OldMetaMaskUiCss()
|
||||
|
@ -1,5 +1,6 @@
|
||||
{
|
||||
"metamask": {
|
||||
"completedOnboarding": true,
|
||||
"isInitialized": true,
|
||||
"isUnlocked": true,
|
||||
"featureFlags": {"betaUI": true},
|
||||
|
@ -1,5 +1,6 @@
|
||||
{
|
||||
"metamask": {
|
||||
"completedOnboarding": true,
|
||||
"isInitialized": true,
|
||||
"isUnlocked": true,
|
||||
"featureFlags": {"betaUI": true},
|
||||
|
@ -1,5 +1,6 @@
|
||||
{
|
||||
"metamask": {
|
||||
"completedOnboarding": true,
|
||||
"isInitialized": true,
|
||||
"isUnlocked": true,
|
||||
"featureFlags": {"betaUI": true},
|
||||
|
@ -1,5 +1,6 @@
|
||||
{
|
||||
"metamask": {
|
||||
"completedOnboarding": true,
|
||||
"isInitialized": true,
|
||||
"isUnlocked": true,
|
||||
"featureFlags": {"betaUI": true},
|
||||
|
@ -1,5 +1,6 @@
|
||||
{
|
||||
"metamask": {
|
||||
"completedOnboarding": true,
|
||||
"isInitialized": true,
|
||||
"isUnlocked": true,
|
||||
"featureFlags": {"betaUI": true},
|
||||
|
@ -1,26 +0,0 @@
|
||||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
export default class Breadcrumbs extends Component {
|
||||
|
||||
static propTypes = {
|
||||
total: PropTypes.number,
|
||||
currentIndex: PropTypes.number,
|
||||
};
|
||||
|
||||
render () {
|
||||
const {total, currentIndex} = this.props
|
||||
return (
|
||||
<div className="breadcrumbs">
|
||||
{Array(total).fill().map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="breadcrumb"
|
||||
style={{backgroundColor: i === currentIndex ? '#D8D8D8' : '#FFFFFF'}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
@ -1,200 +0,0 @@
|
||||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import classnames from 'classnames'
|
||||
import {connect} from 'react-redux'
|
||||
import {qrcode} from 'qrcode-npm'
|
||||
import copyToClipboard from 'copy-to-clipboard'
|
||||
import ShapeShiftForm from '../shapeshift-form'
|
||||
import Identicon from '../../../../ui/app/components/identicon'
|
||||
import {buyEth, showAccountDetail} from '../../../../ui/app/actions'
|
||||
|
||||
class BuyEtherScreen extends Component {
|
||||
static OPTION_VALUES = {
|
||||
COINBASE: 'coinbase',
|
||||
SHAPESHIFT: 'shapeshift',
|
||||
QR_CODE: 'qr_code',
|
||||
};
|
||||
|
||||
static OPTIONS = [
|
||||
{
|
||||
name: 'Direct Deposit',
|
||||
value: BuyEtherScreen.OPTION_VALUES.QR_CODE,
|
||||
},
|
||||
{
|
||||
name: 'Buy with Dollars',
|
||||
value: BuyEtherScreen.OPTION_VALUES.COINBASE,
|
||||
},
|
||||
{
|
||||
name: 'Buy with Cryptos',
|
||||
value: BuyEtherScreen.OPTION_VALUES.SHAPESHIFT,
|
||||
},
|
||||
];
|
||||
|
||||
static propTypes = {
|
||||
address: PropTypes.string,
|
||||
goToCoinbase: PropTypes.func.isRequired,
|
||||
showAccountDetail: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
state = {
|
||||
selectedOption: BuyEtherScreen.OPTION_VALUES.QR_CODE,
|
||||
justCopied: false,
|
||||
}
|
||||
|
||||
copyToClipboard = () => {
|
||||
const { address } = this.props
|
||||
|
||||
this.setState({ justCopied: true }, () => copyToClipboard(address))
|
||||
|
||||
setTimeout(() => this.setState({ justCopied: false }), 1000)
|
||||
}
|
||||
|
||||
renderSkip () {
|
||||
const {showAccountDetail, address} = this.props
|
||||
|
||||
return (
|
||||
<div
|
||||
className="buy-ether__do-it-later"
|
||||
onClick={() => showAccountDetail(address)}
|
||||
>
|
||||
Do it later
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderCoinbaseLogo () {
|
||||
return (
|
||||
<svg width="140px" height="49px" viewBox="0 0 579 126" version="1.1">
|
||||
<g id="Page-1" stroke="none" strokeWidth={1} fill="none" fillRule="evenodd">
|
||||
<g id="Imported-Layers" fill="#0081C9">
|
||||
<path d="M37.752,125.873 C18.824,125.873 0.369,112.307 0.369,81.549 C0.369,50.79 18.824,37.382 37.752,37.382 C47.059,37.382 54.315,39.749 59.52,43.219 L53.841,55.68 C50.371,53.156 45.166,51.579 39.961,51.579 C28.604,51.579 18.193,60.57 18.193,81.391 C18.193,102.212 28.919,111.361 39.961,111.361 C45.166,111.361 50.371,109.783 53.841,107.26 L59.52,120.036 C54.157,123.664 47.059,125.873 37.752,125.873" id="Fill-1" />
|
||||
<path d="M102.898,125.873 C78.765,125.873 65.515,106.786 65.515,81.549 C65.515,56.311 78.765,37.382 102.898,37.382 C127.032,37.382 140.282,56.311 140.282,81.549 C140.282,106.786 127.032,125.873 102.898,125.873 L102.898,125.873 Z M102.898,51.105 C89.491,51.105 82.866,63.093 82.866,81.391 C82.866,99.688 89.491,111.834 102.898,111.834 C116.306,111.834 122.931,99.688 122.931,81.391 C122.931,63.093 116.306,51.105 102.898,51.105 L102.898,51.105 Z" id="Fill-2" />
|
||||
<path d="M163.468,23.659 C157.79,23.659 153.215,19.243 153.215,13.88 C153.215,8.517 157.79,4.1 163.468,4.1 C169.146,4.1 173.721,8.517 173.721,13.88 C173.721,19.243 169.146,23.659 163.468,23.659 L163.468,23.659 Z M154.793,39.118 L172.144,39.118 L172.144,124.138 L154.793,124.138 L154.793,39.118 Z" id="Fill-3" />
|
||||
<path d="M240.443,124.137 L240.443,67.352 C240.443,57.415 234.449,51.263 222.619,51.263 C216.31,51.263 210.473,52.367 207.003,53.787 L207.003,124.137 L189.81,124.137 L189.81,43.376 C198.328,39.906 209.212,37.382 222.461,37.382 C246.28,37.382 257.794,47.793 257.794,65.775 L257.794,124.137 L240.443,124.137" id="Fill-4" />
|
||||
<path d="M303.536,125.873 C292.494,125.873 281.611,123.191 274.986,119.879 L274.986,0.314 L292.179,0.314 L292.179,41.326 C296.28,39.433 302.905,37.856 308.741,37.856 C330.667,37.856 345.494,53.629 345.494,79.656 C345.494,111.676 328.931,125.873 303.536,125.873 L303.536,125.873 Z M305.744,51.263 C301.012,51.263 295.491,52.367 292.179,54.103 L292.179,109.941 C294.703,111.045 299.593,112.149 304.482,112.149 C318.205,112.149 328.301,102.685 328.301,80.918 C328.301,62.305 319.467,51.263 305.744,51.263 L305.744,51.263 Z" id="Fill-5" />
|
||||
<path d="M392.341,125.873 C367.892,125.873 355.589,115.935 355.589,99.215 C355.589,75.555 380.826,71.296 406.537,69.876 L406.537,64.513 C406.537,53.787 399.439,50.001 388.555,50.001 C380.511,50.001 370.731,52.525 365.053,55.207 L360.636,43.376 C367.419,40.379 378.933,37.382 390.29,37.382 C410.638,37.382 422.942,45.269 422.942,66.248 L422.942,119.879 C416.79,123.191 404.329,125.873 392.341,125.873 L392.341,125.873 Z M406.537,81.391 C389.186,82.337 371.835,83.757 371.835,98.9 C371.835,107.89 378.776,113.411 391.868,113.411 C397.389,113.411 403.856,112.465 406.537,111.203 L406.537,81.391 L406.537,81.391 Z" id="Fill-6" />
|
||||
<path d="M461.743,125.873 C451.806,125.873 441.395,123.191 435.244,119.879 L441.08,106.629 C445.496,109.31 454.803,112.149 461.27,112.149 C470.576,112.149 476.728,107.575 476.728,100.477 C476.728,92.748 470.261,89.751 461.586,86.596 C450.228,82.337 437.452,77.132 437.452,61.201 C437.452,47.162 448.336,37.382 467.264,37.382 C477.517,37.382 486.035,39.906 492.029,43.376 L486.665,55.364 C482.88,52.998 475.309,50.317 469.157,50.317 C460.166,50.317 455.118,55.049 455.118,61.201 C455.118,68.93 461.428,71.611 469.788,74.766 C481.618,79.183 494.71,84.072 494.71,100.635 C494.71,115.935 483.038,125.873 461.743,125.873" id="Fill-7" />
|
||||
<path d="M578.625,81.233 L522.155,89.12 C523.89,104.42 533.828,112.149 548.182,112.149 C556.699,112.149 565.848,110.099 571.684,106.944 L576.732,119.879 C570.107,123.349 558.75,125.873 547.078,125.873 C520.262,125.873 505.277,108.679 505.277,81.549 C505.277,55.522 519.789,37.382 543.607,37.382 C565.69,37.382 578.782,51.894 578.782,74.766 C578.782,76.816 578.782,79.025 578.625,81.233 L578.625,81.233 Z M543.292,50.001 C530.042,50.001 521.367,60.097 521.051,77.763 L562.22,72.084 C562.062,57.257 554.649,50.001 543.292,50.001 L543.292,50.001 Z" id="Fill-8" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
renderCoinbaseForm () {
|
||||
const {goToCoinbase, address} = this.props
|
||||
|
||||
return (
|
||||
<div className="buy-ether__action-content-wrapper">
|
||||
<div>{this.renderCoinbaseLogo()}</div>
|
||||
<div className="buy-ether__body-text">Coinbase is the world’s most popular way to buy and sell bitcoin, ethereum, and litecoin.</div>
|
||||
<a className="first-time-flow__link buy-ether__faq-link">What is Ethereum?</a>
|
||||
<div className="buy-ether__buttons">
|
||||
<button
|
||||
className="first-time-flow__button"
|
||||
onClick={() => goToCoinbase(address)}
|
||||
>
|
||||
Buy
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderContent () {
|
||||
const { OPTION_VALUES } = BuyEtherScreen
|
||||
const { address } = this.props
|
||||
const { justCopied } = this.state
|
||||
const qrImage = qrcode(4, 'M')
|
||||
qrImage.addData(address)
|
||||
qrImage.make()
|
||||
|
||||
switch (this.state.selectedOption) {
|
||||
case OPTION_VALUES.COINBASE:
|
||||
return this.renderCoinbaseForm()
|
||||
case OPTION_VALUES.SHAPESHIFT:
|
||||
return (
|
||||
<div className="buy-ether__action-content-wrapper">
|
||||
<div className="shapeshift-logo" />
|
||||
<div className="buy-ether__body-text">
|
||||
Trade any leading blockchain asset for any other. Protection by Design. No Account Needed.
|
||||
</div>
|
||||
<ShapeShiftForm btnClass="first-time-flow__button" />
|
||||
</div>
|
||||
)
|
||||
case OPTION_VALUES.QR_CODE:
|
||||
return (
|
||||
<div className="buy-ether__action-content-wrapper">
|
||||
<div dangerouslySetInnerHTML={{ __html: qrImage.createTableTag(4) }} />
|
||||
<div className="buy-ether__body-text">Deposit Ether directly into your account.</div>
|
||||
<div className="buy-ether__small-body-text">(This is the account address that MetaMask created for you to recieve funds.)</div>
|
||||
<div className="buy-ether__buttons">
|
||||
<button
|
||||
className="first-time-flow__button"
|
||||
onClick={this.copyToClipboard}
|
||||
disabled={justCopied}
|
||||
>
|
||||
{ justCopied ? 'Copied' : 'Copy' }
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { OPTIONS } = BuyEtherScreen
|
||||
const { selectedOption } = this.state
|
||||
|
||||
return (
|
||||
<div className="buy-ether">
|
||||
<Identicon address={this.props.address} diameter={70} />
|
||||
<div className="buy-ether__title">Deposit Ether</div>
|
||||
<div className="buy-ether__body-text">
|
||||
MetaMask works best if you have Ether in your account to pay for transaction gas fees and more. To get Ether, choose from one of these methods.
|
||||
</div>
|
||||
<div className="buy-ether__content-wrapper">
|
||||
<div className="buy-ether__content-headline-wrapper">
|
||||
<div className="buy-ether__content-headline">Deposit Options</div>
|
||||
{this.renderSkip()}
|
||||
</div>
|
||||
<div className="buy-ether__content">
|
||||
<div className="buy-ether__side-panel">
|
||||
{OPTIONS.map(({ name, value }) => (
|
||||
<div
|
||||
key={value}
|
||||
className={classnames('buy-ether__side-panel-item', {
|
||||
'buy-ether__side-panel-item--selected': value === selectedOption,
|
||||
})}
|
||||
onClick={() => this.setState({ selectedOption: value })}
|
||||
>
|
||||
<div className="buy-ether__side-panel-item-name">{name}</div>
|
||||
{value === selectedOption && (
|
||||
<svg viewBox="0 0 574 1024" id="si-ant-right" width="15px" height="15px">
|
||||
<path d="M10 9Q0 19 0 32t10 23l482 457L10 969Q0 979 0 992t10 23q10 9 24 9t24-9l506-480q10-10 10-23t-10-23L58 9Q48 0 34 0T10 9z" />
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="buy-ether__action-content">
|
||||
{this.renderContent()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
({ metamask: { selectedAddress } }) => ({
|
||||
address: selectedAddress,
|
||||
}),
|
||||
dispatch => ({
|
||||
goToCoinbase: address => dispatch(buyEth({ network: '1', address, amount: 0 })),
|
||||
showAccountDetail: address => dispatch(showAccountDetail(address)),
|
||||
})
|
||||
)(BuyEtherScreen)
|
@ -1,162 +0,0 @@
|
||||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { connect } from 'react-redux'
|
||||
import { withRouter } from 'react-router-dom'
|
||||
import classnames from 'classnames'
|
||||
import shuffle from 'lodash.shuffle'
|
||||
import { compose } from 'recompose'
|
||||
import Identicon from '../../../../ui/app/components/identicon'
|
||||
import { confirmSeedWords, showModal } from '../../../../ui/app/actions'
|
||||
import Breadcrumbs from './breadcrumbs'
|
||||
import LoadingScreen from './loading-screen'
|
||||
import { DEFAULT_ROUTE, INITIALIZE_BACKUP_PHRASE_ROUTE } from '../../../../ui/app/routes'
|
||||
|
||||
class ConfirmSeedScreen extends Component {
|
||||
static propTypes = {
|
||||
isLoading: PropTypes.bool,
|
||||
address: PropTypes.string,
|
||||
seedWords: PropTypes.string,
|
||||
confirmSeedWords: PropTypes.func,
|
||||
history: PropTypes.object,
|
||||
openBuyEtherModal: PropTypes.func,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
seedWords: '',
|
||||
}
|
||||
|
||||
constructor (props) {
|
||||
super(props)
|
||||
const { seedWords } = props
|
||||
this.state = {
|
||||
selectedSeeds: [],
|
||||
shuffledSeeds: seedWords && shuffle(seedWords.split(' ')) || [],
|
||||
}
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
const { seedWords, history } = this.props
|
||||
|
||||
if (!seedWords) {
|
||||
history.push(DEFAULT_ROUTE)
|
||||
}
|
||||
}
|
||||
|
||||
handleClick () {
|
||||
const { confirmSeedWords, history, openBuyEtherModal } = this.props
|
||||
|
||||
confirmSeedWords()
|
||||
.then(() => {
|
||||
history.push(DEFAULT_ROUTE)
|
||||
openBuyEtherModal()
|
||||
})
|
||||
}
|
||||
|
||||
render () {
|
||||
const { seedWords, history } = this.props
|
||||
const { selectedSeeds, shuffledSeeds } = this.state
|
||||
const isValid = seedWords === selectedSeeds.map(([_, seed]) => seed).join(' ')
|
||||
|
||||
return (
|
||||
<div className="first-time-flow">
|
||||
{
|
||||
this.props.isLoading
|
||||
? <LoadingScreen loadingMessage="Creating your new account" />
|
||||
: (
|
||||
<div className="first-view-main-wrapper">
|
||||
<div className="first-view-main">
|
||||
<div className="backup-phrase">
|
||||
<a
|
||||
className="backup-phrase__back-button"
|
||||
onClick={e => {
|
||||
e.preventDefault()
|
||||
history.push(INITIALIZE_BACKUP_PHRASE_ROUTE)
|
||||
}}
|
||||
href="#"
|
||||
>
|
||||
{`< Back`}
|
||||
</a>
|
||||
<Identicon address={this.props.address} diameter={70} />
|
||||
<div className="backup-phrase__content-wrapper">
|
||||
<div>
|
||||
<div className="backup-phrase__title">
|
||||
Confirm your Secret Backup Phrase
|
||||
</div>
|
||||
<div className="backup-phrase__body-text">
|
||||
Please select each phrase in order to make sure it is correct.
|
||||
</div>
|
||||
<div className="backup-phrase__confirm-secret">
|
||||
{selectedSeeds.map(([_, word], i) => (
|
||||
<button
|
||||
key={i}
|
||||
className="backup-phrase__confirm-seed-option"
|
||||
>
|
||||
{word}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="backup-phrase__confirm-seed-options">
|
||||
{shuffledSeeds.map((word, i) => {
|
||||
const isSelected = selectedSeeds
|
||||
.filter(([index, seed]) => seed === word && index === i)
|
||||
.length
|
||||
|
||||
return (
|
||||
<button
|
||||
key={i}
|
||||
className={classnames('backup-phrase__confirm-seed-option', {
|
||||
'backup-phrase__confirm-seed-option--selected': isSelected,
|
||||
'backup-phrase__confirm-seed-option--unselected': !isSelected,
|
||||
})}
|
||||
onClick={() => {
|
||||
if (!isSelected) {
|
||||
this.setState({
|
||||
selectedSeeds: [...selectedSeeds, [i, word]],
|
||||
})
|
||||
} else {
|
||||
this.setState({
|
||||
selectedSeeds: selectedSeeds
|
||||
.filter(([index, seed]) => !(seed === word && index === i)),
|
||||
})
|
||||
}
|
||||
}}
|
||||
>
|
||||
{word}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<button
|
||||
className="first-time-flow__button"
|
||||
onClick={() => isValid && this.handleClick()}
|
||||
disabled={!isValid}
|
||||
>
|
||||
Confirm
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<Breadcrumbs total={3} currentIndex={1} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default compose(
|
||||
withRouter,
|
||||
connect(
|
||||
({ metamask: { selectedAddress, seedWords }, appState: { isLoading } }) => ({
|
||||
seedWords,
|
||||
isLoading,
|
||||
address: selectedAddress,
|
||||
}),
|
||||
dispatch => ({
|
||||
confirmSeedWords: () => dispatch(confirmSeedWords()),
|
||||
openBuyEtherModal: () => dispatch(showModal({ name: 'DEPOSIT_ETHER'})),
|
||||
})
|
||||
)
|
||||
)(ConfirmSeedScreen)
|
@ -1,221 +0,0 @@
|
||||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import {connect} from 'react-redux'
|
||||
import { withRouter } from 'react-router-dom'
|
||||
import { compose } from 'recompose'
|
||||
import { createNewVaultAndKeychain } from '../../../../ui/app/actions'
|
||||
import Breadcrumbs from './breadcrumbs'
|
||||
import EventEmitter from 'events'
|
||||
import Mascot from '../../../../ui/app/components/mascot'
|
||||
import classnames from 'classnames'
|
||||
import {
|
||||
INITIALIZE_UNIQUE_IMAGE_ROUTE,
|
||||
INITIALIZE_IMPORT_WITH_SEED_PHRASE_ROUTE,
|
||||
INITIALIZE_NOTICE_ROUTE,
|
||||
} from '../../../../ui/app/routes'
|
||||
import TextField from '../../../../ui/app/components/text-field'
|
||||
|
||||
class CreatePasswordScreen extends Component {
|
||||
static contextTypes = {
|
||||
t: PropTypes.func,
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
isLoading: PropTypes.bool.isRequired,
|
||||
createAccount: PropTypes.func.isRequired,
|
||||
history: PropTypes.object.isRequired,
|
||||
isInitialized: PropTypes.bool,
|
||||
isUnlocked: PropTypes.bool,
|
||||
isMascara: PropTypes.bool.isRequired,
|
||||
}
|
||||
|
||||
state = {
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
passwordError: null,
|
||||
confirmPasswordError: null,
|
||||
}
|
||||
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this.animationEventEmitter = new EventEmitter()
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
const { isInitialized, history } = this.props
|
||||
|
||||
if (isInitialized) {
|
||||
history.push(INITIALIZE_NOTICE_ROUTE)
|
||||
}
|
||||
}
|
||||
|
||||
isValid () {
|
||||
const { password, confirmPassword } = this.state
|
||||
|
||||
if (!password || !confirmPassword) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (password.length < 8) {
|
||||
return false
|
||||
}
|
||||
|
||||
return password === confirmPassword
|
||||
}
|
||||
|
||||
createAccount = (event) => {
|
||||
event.preventDefault()
|
||||
|
||||
if (!this.isValid()) {
|
||||
return
|
||||
}
|
||||
|
||||
const { password } = this.state
|
||||
const { createAccount, history } = this.props
|
||||
|
||||
this.setState({ isLoading: true })
|
||||
createAccount(password)
|
||||
.then(() => history.push(INITIALIZE_UNIQUE_IMAGE_ROUTE))
|
||||
}
|
||||
|
||||
handlePasswordChange (password) {
|
||||
const { confirmPassword } = this.state
|
||||
let confirmPasswordError = null
|
||||
let passwordError = null
|
||||
|
||||
if (password && password.length < 8) {
|
||||
passwordError = this.context.t('passwordNotLongEnough')
|
||||
}
|
||||
|
||||
if (confirmPassword && password !== confirmPassword) {
|
||||
confirmPasswordError = this.context.t('passwordsDontMatch')
|
||||
}
|
||||
|
||||
this.setState({ password, passwordError, confirmPasswordError })
|
||||
}
|
||||
|
||||
handleConfirmPasswordChange (confirmPassword) {
|
||||
const { password } = this.state
|
||||
let confirmPasswordError = null
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
confirmPasswordError = this.context.t('passwordsDontMatch')
|
||||
}
|
||||
|
||||
this.setState({ confirmPassword, confirmPasswordError })
|
||||
}
|
||||
|
||||
render () {
|
||||
const { history, isMascara } = this.props
|
||||
const { passwordError, confirmPasswordError } = this.state
|
||||
const { t } = this.context
|
||||
|
||||
return (
|
||||
<div className={classnames({ 'first-view-main-wrapper': !isMascara })}>
|
||||
<div className={classnames({
|
||||
'first-view-main': !isMascara,
|
||||
'first-view-main__mascara': isMascara,
|
||||
})}>
|
||||
{isMascara && <div className="mascara-info first-view-phone-invisible">
|
||||
<Mascot
|
||||
animationEventEmitter={this.animationEventEmitter}
|
||||
width="225"
|
||||
height="225"
|
||||
/>
|
||||
<div className="info">
|
||||
MetaMask is a secure identity vault for Ethereum.
|
||||
</div>
|
||||
<div className="info">
|
||||
It allows you to hold ether & tokens, and interact with decentralized applications.
|
||||
</div>
|
||||
</div>}
|
||||
<form className="create-password">
|
||||
<div className="create-password__title">
|
||||
Create Password
|
||||
</div>
|
||||
<TextField
|
||||
id="create-password"
|
||||
label={t('newPassword')}
|
||||
type="password"
|
||||
className="first-time-flow__input"
|
||||
value={this.state.password}
|
||||
onChange={event => this.handlePasswordChange(event.target.value)}
|
||||
error={passwordError}
|
||||
autoFocus
|
||||
autoComplete="new-password"
|
||||
margin="normal"
|
||||
fullWidth
|
||||
largeLabel
|
||||
/>
|
||||
<TextField
|
||||
id="confirm-password"
|
||||
label={t('confirmPassword')}
|
||||
type="password"
|
||||
className="first-time-flow__input"
|
||||
value={this.state.confirmPassword}
|
||||
onChange={event => this.handleConfirmPasswordChange(event.target.value)}
|
||||
error={confirmPasswordError}
|
||||
autoComplete="confirm-password"
|
||||
margin="normal"
|
||||
fullWidth
|
||||
largeLabel
|
||||
/>
|
||||
<button
|
||||
className="first-time-flow__button"
|
||||
disabled={!this.isValid()}
|
||||
onClick={this.createAccount}
|
||||
>
|
||||
Create
|
||||
</button>
|
||||
<a
|
||||
href=""
|
||||
className="first-time-flow__link create-password__import-link"
|
||||
onClick={e => {
|
||||
e.preventDefault()
|
||||
history.push(INITIALIZE_IMPORT_WITH_SEED_PHRASE_ROUTE)
|
||||
}}
|
||||
>
|
||||
Import with seed phrase
|
||||
</a>
|
||||
{ /* }
|
||||
<a
|
||||
href=""
|
||||
className="first-time-flow__link create-password__import-link"
|
||||
onClick={e => {
|
||||
e.preventDefault()
|
||||
history.push(INITIALIZE_IMPORT_ACCOUNT_ROUTE)
|
||||
}}
|
||||
>
|
||||
Import an account
|
||||
</a>
|
||||
{ */ }
|
||||
<Breadcrumbs total={3} currentIndex={0} />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = ({ metamask, appState }) => {
|
||||
const { isInitialized, isUnlocked, isMascara, noActiveNotices } = metamask
|
||||
const { isLoading } = appState
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
isInitialized,
|
||||
isUnlocked,
|
||||
isMascara,
|
||||
noActiveNotices,
|
||||
}
|
||||
}
|
||||
|
||||
export default compose(
|
||||
withRouter,
|
||||
connect(
|
||||
mapStateToProps,
|
||||
dispatch => ({
|
||||
createAccount: password => dispatch(createNewVaultAndKeychain(password)),
|
||||
})
|
||||
)
|
||||
)(CreatePasswordScreen)
|
@ -1,208 +0,0 @@
|
||||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import {connect} from 'react-redux'
|
||||
import classnames from 'classnames'
|
||||
import LoadingScreen from './loading-screen'
|
||||
import {importNewAccount, hideWarning} from '../../../../ui/app/actions'
|
||||
|
||||
const Input = ({ label, placeholder, onChange, errorMessage, type = 'text' }) => (
|
||||
<div className="import-account__input-wrapper">
|
||||
<div className="import-account__input-label">{label}</div>
|
||||
<input
|
||||
type={type}
|
||||
placeholder={placeholder}
|
||||
className={classnames('first-time-flow__input import-account__input', {
|
||||
'first-time-flow__input--error': errorMessage,
|
||||
})}
|
||||
onChange={onChange}
|
||||
/>
|
||||
<div className="import-account__input-error-message">{errorMessage}</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
Input.prototype.propTypes = {
|
||||
label: PropTypes.string.isRequired,
|
||||
placeholder: PropTypes.string.isRequired,
|
||||
type: PropTypes.string.isRequired,
|
||||
errorMessage: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
class ImportAccountScreen extends Component {
|
||||
static OPTIONS = {
|
||||
PRIVATE_KEY: 'private_key',
|
||||
JSON_FILE: 'json_file',
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
warning: PropTypes.string,
|
||||
back: PropTypes.func.isRequired,
|
||||
next: PropTypes.func.isRequired,
|
||||
importNewAccount: PropTypes.func.isRequired,
|
||||
hideWarning: PropTypes.func.isRequired,
|
||||
isLoading: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
state = {
|
||||
selectedOption: ImportAccountScreen.OPTIONS.PRIVATE_KEY,
|
||||
privateKey: '',
|
||||
jsonFile: {},
|
||||
}
|
||||
|
||||
isValid () {
|
||||
const { OPTIONS } = ImportAccountScreen
|
||||
const { privateKey, jsonFile, password } = this.state
|
||||
|
||||
switch (this.state.selectedOption) {
|
||||
case OPTIONS.JSON_FILE:
|
||||
return Boolean(jsonFile && password)
|
||||
case OPTIONS.PRIVATE_KEY:
|
||||
default:
|
||||
return Boolean(privateKey)
|
||||
}
|
||||
}
|
||||
|
||||
onClick = () => {
|
||||
const { OPTIONS } = ImportAccountScreen
|
||||
const { importNewAccount, next } = this.props
|
||||
const { privateKey, jsonFile, password } = this.state
|
||||
|
||||
switch (this.state.selectedOption) {
|
||||
case OPTIONS.JSON_FILE:
|
||||
return importNewAccount('JSON File', [ jsonFile, password ])
|
||||
// JS runtime requires caught rejections but failures are handled by Redux
|
||||
.catch()
|
||||
.then(next)
|
||||
case OPTIONS.PRIVATE_KEY:
|
||||
default:
|
||||
return importNewAccount('Private Key', [ privateKey ])
|
||||
// JS runtime requires caught rejections but failures are handled by Redux
|
||||
.catch()
|
||||
.then(next)
|
||||
}
|
||||
}
|
||||
|
||||
renderPrivateKey () {
|
||||
return Input({
|
||||
label: 'Add Private Key String',
|
||||
placeholder: 'Enter private key',
|
||||
onChange: e => this.setState({ privateKey: e.target.value }),
|
||||
errorMessage: this.props.warning && 'Something went wrong. Please make sure your private key is correct.',
|
||||
})
|
||||
}
|
||||
|
||||
renderJsonFile () {
|
||||
const { jsonFile: { name } } = this.state
|
||||
const { warning } = this.props
|
||||
|
||||
return (
|
||||
<div className="">
|
||||
<div className="import-account__input-wrapper">
|
||||
<div className="import-account__input-label">Upload File</div>
|
||||
<div className="import-account__file-picker-wrapper">
|
||||
<input
|
||||
type="file"
|
||||
id="file"
|
||||
className="import-account__file-input"
|
||||
onChange={e => this.setState({ jsonFile: e.target.files[0] })}
|
||||
/>
|
||||
<label
|
||||
htmlFor="file"
|
||||
className={classnames('import-account__file-input-label', {
|
||||
'import-account__file-input-label--error': warning,
|
||||
})}
|
||||
>
|
||||
Choose File
|
||||
</label>
|
||||
<div className="import-account__file-name">{name}</div>
|
||||
</div>
|
||||
<div className="import-account__input-error-message">
|
||||
{warning && 'Something went wrong. Please make sure your JSON file is properly formatted.'}
|
||||
</div>
|
||||
</div>
|
||||
{Input({
|
||||
label: 'Enter Password',
|
||||
placeholder: 'Enter Password',
|
||||
type: 'password',
|
||||
onChange: e => this.setState({ password: e.target.value }),
|
||||
errorMessage: warning && 'Please make sure your password is correct.',
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderContent () {
|
||||
const { OPTIONS } = ImportAccountScreen
|
||||
|
||||
switch (this.state.selectedOption) {
|
||||
case OPTIONS.JSON_FILE:
|
||||
return this.renderJsonFile()
|
||||
case OPTIONS.PRIVATE_KEY:
|
||||
default:
|
||||
return this.renderPrivateKey()
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { OPTIONS } = ImportAccountScreen
|
||||
const { selectedOption } = this.state
|
||||
|
||||
return this.props.isLoading
|
||||
? <LoadingScreen loadingMessage="Creating your new account" />
|
||||
: (
|
||||
<div className="import-account">
|
||||
<a
|
||||
className="import-account__back-button"
|
||||
onClick={e => {
|
||||
e.preventDefault()
|
||||
this.props.back()
|
||||
}}
|
||||
href="#"
|
||||
>
|
||||
{`< Back`}
|
||||
</a>
|
||||
<div className="import-account__title">
|
||||
Import an Account
|
||||
</div>
|
||||
<div className="import-account__selector-label">
|
||||
How would you like to import your account?
|
||||
</div>
|
||||
<select
|
||||
className="import-account__dropdown"
|
||||
value={selectedOption}
|
||||
onChange={e => {
|
||||
this.setState({ selectedOption: e.target.value })
|
||||
this.props.hideWarning()
|
||||
}}
|
||||
>
|
||||
<option value={OPTIONS.PRIVATE_KEY}>Private Key</option>
|
||||
<option value={OPTIONS.JSON_FILE}>JSON File</option>
|
||||
</select>
|
||||
{this.renderContent()}
|
||||
<button
|
||||
className="first-time-flow__button"
|
||||
disabled={!this.isValid()}
|
||||
onClick={this.onClick}
|
||||
>
|
||||
Import
|
||||
</button>
|
||||
<a
|
||||
href="https://github.com/MetaMask/faq/blob/master/README.md#q-i-cant-use-the-import-feature-for-uploading-a-json-file-the-window-keeps-closing-when-i-try-to-select-a-file"
|
||||
className="first-time-flow__link import-account__faq-link"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
File import not working?
|
||||
</a>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
({ appState: { isLoading, warning } }) => ({ isLoading, warning }),
|
||||
dispatch => ({
|
||||
importNewAccount: (strategy, args) => dispatch(importNewAccount(strategy, args)),
|
||||
hideWarning: () => dispatch(hideWarning()),
|
||||
})
|
||||
)(ImportAccountScreen)
|
@ -1,192 +0,0 @@
|
||||
import {validateMnemonic} from 'bip39'
|
||||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import {connect} from 'react-redux'
|
||||
import {
|
||||
createNewVaultAndRestore,
|
||||
unMarkPasswordForgotten,
|
||||
} from '../../../../ui/app/actions'
|
||||
import { INITIALIZE_NOTICE_ROUTE } from '../../../../ui/app/routes'
|
||||
import TextField from '../../../../ui/app/components/text-field'
|
||||
|
||||
class ImportSeedPhraseScreen extends Component {
|
||||
static contextTypes = {
|
||||
t: PropTypes.func,
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
warning: PropTypes.string,
|
||||
createNewVaultAndRestore: PropTypes.func.isRequired,
|
||||
leaveImportSeedScreenState: PropTypes.func,
|
||||
history: PropTypes.object,
|
||||
isLoading: PropTypes.bool,
|
||||
};
|
||||
|
||||
state = {
|
||||
seedPhrase: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
seedPhraseError: null,
|
||||
passwordError: null,
|
||||
confirmPasswordError: null,
|
||||
}
|
||||
|
||||
parseSeedPhrase = (seedPhrase) => {
|
||||
return seedPhrase
|
||||
.trim()
|
||||
.match(/\w+/g)
|
||||
.join(' ')
|
||||
}
|
||||
|
||||
handleSeedPhraseChange (seedPhrase) {
|
||||
let seedPhraseError = null
|
||||
|
||||
if (seedPhrase) {
|
||||
const parsedSeedPhrase = this.parseSeedPhrase(seedPhrase)
|
||||
if (parsedSeedPhrase.split(' ').length !== 12) {
|
||||
seedPhraseError = this.context.t('seedPhraseReq')
|
||||
} else if (!validateMnemonic(parsedSeedPhrase)) {
|
||||
seedPhraseError = this.context.t('invalidSeedPhrase')
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({ seedPhrase, seedPhraseError })
|
||||
}
|
||||
|
||||
handlePasswordChange (password) {
|
||||
const { confirmPassword } = this.state
|
||||
let confirmPasswordError = null
|
||||
let passwordError = null
|
||||
|
||||
if (password && password.length < 8) {
|
||||
passwordError = this.context.t('passwordNotLongEnough')
|
||||
}
|
||||
|
||||
if (confirmPassword && password !== confirmPassword) {
|
||||
confirmPasswordError = this.context.t('passwordsDontMatch')
|
||||
}
|
||||
|
||||
this.setState({ password, passwordError, confirmPasswordError })
|
||||
}
|
||||
|
||||
handleConfirmPasswordChange (confirmPassword) {
|
||||
const { password } = this.state
|
||||
let confirmPasswordError = null
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
confirmPasswordError = this.context.t('passwordsDontMatch')
|
||||
}
|
||||
|
||||
this.setState({ confirmPassword, confirmPasswordError })
|
||||
}
|
||||
|
||||
onClick = () => {
|
||||
const { password, seedPhrase } = this.state
|
||||
const {
|
||||
createNewVaultAndRestore,
|
||||
leaveImportSeedScreenState,
|
||||
history,
|
||||
} = this.props
|
||||
|
||||
leaveImportSeedScreenState()
|
||||
createNewVaultAndRestore(password, this.parseSeedPhrase(seedPhrase))
|
||||
.then(() => history.push(INITIALIZE_NOTICE_ROUTE))
|
||||
}
|
||||
|
||||
hasError () {
|
||||
const { passwordError, confirmPasswordError, seedPhraseError } = this.state
|
||||
return passwordError || confirmPasswordError || seedPhraseError
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
seedPhrase,
|
||||
password,
|
||||
confirmPassword,
|
||||
seedPhraseError,
|
||||
passwordError,
|
||||
confirmPasswordError,
|
||||
} = this.state
|
||||
const { t } = this.context
|
||||
const { isLoading } = this.props
|
||||
const disabled = !seedPhrase || !password || !confirmPassword || isLoading || this.hasError()
|
||||
|
||||
return (
|
||||
<div className="first-view-main-wrapper">
|
||||
<div className="first-view-main">
|
||||
<div className="import-account">
|
||||
<a
|
||||
className="import-account__back-button"
|
||||
onClick={e => {
|
||||
e.preventDefault()
|
||||
this.props.history.goBack()
|
||||
}}
|
||||
href="#"
|
||||
>
|
||||
{`< Back`}
|
||||
</a>
|
||||
<div className="import-account__title">
|
||||
Import an Account with Seed Phrase
|
||||
</div>
|
||||
<div className="import-account__selector-label">
|
||||
Enter your secret twelve word phrase here to restore your vault.
|
||||
</div>
|
||||
<div className="import-account__input-wrapper">
|
||||
<label className="import-account__input-label">Wallet Seed</label>
|
||||
<textarea
|
||||
className="import-account__secret-phrase"
|
||||
onChange={e => this.handleSeedPhraseChange(e.target.value)}
|
||||
value={this.state.seedPhrase}
|
||||
placeholder="Separate each word with a single space"
|
||||
/>
|
||||
</div>
|
||||
<span className="error">
|
||||
{ seedPhraseError }
|
||||
</span>
|
||||
<TextField
|
||||
id="password"
|
||||
label={t('newPassword')}
|
||||
type="password"
|
||||
className="first-time-flow__input"
|
||||
value={this.state.password}
|
||||
onChange={event => this.handlePasswordChange(event.target.value)}
|
||||
error={passwordError}
|
||||
autoComplete="new-password"
|
||||
margin="normal"
|
||||
largeLabel
|
||||
/>
|
||||
<TextField
|
||||
id="confirm-password"
|
||||
label={t('confirmPassword')}
|
||||
type="password"
|
||||
className="first-time-flow__input"
|
||||
value={this.state.confirmPassword}
|
||||
onChange={event => this.handleConfirmPasswordChange(event.target.value)}
|
||||
error={confirmPasswordError}
|
||||
autoComplete="confirm-password"
|
||||
margin="normal"
|
||||
largeLabel
|
||||
/>
|
||||
<button
|
||||
className="first-time-flow__button"
|
||||
onClick={() => !disabled && this.onClick()}
|
||||
disabled={disabled}
|
||||
>
|
||||
Import
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
({ appState: { warning, isLoading } }) => ({ warning, isLoading }),
|
||||
dispatch => ({
|
||||
leaveImportSeedScreenState: () => {
|
||||
dispatch(unMarkPasswordForgotten())
|
||||
},
|
||||
createNewVaultAndRestore: (pw, seed) => dispatch(createNewVaultAndRestore(pw, seed)),
|
||||
})
|
||||
)(ImportSeedPhraseScreen)
|
File diff suppressed because one or more lines are too long
@ -1,99 +0,0 @@
|
||||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import {connect} from 'react-redux'
|
||||
import { withRouter, Switch, Route } from 'react-router-dom'
|
||||
import { compose } from 'recompose'
|
||||
|
||||
import CreatePasswordScreen from './create-password-screen'
|
||||
import UniqueImageScreen from './unique-image-screen'
|
||||
import NoticeScreen from './notice-screen'
|
||||
import BackupPhraseScreen from './seed-screen'
|
||||
import ImportAccountScreen from './import-account-screen'
|
||||
import ImportSeedPhraseScreen from './import-seed-phrase-screen'
|
||||
import ConfirmSeed from './confirm-seed-screen'
|
||||
import {
|
||||
INITIALIZE_ROUTE,
|
||||
INITIALIZE_IMPORT_ACCOUNT_ROUTE,
|
||||
INITIALIZE_UNIQUE_IMAGE_ROUTE,
|
||||
INITIALIZE_IMPORT_WITH_SEED_PHRASE_ROUTE,
|
||||
INITIALIZE_NOTICE_ROUTE,
|
||||
INITIALIZE_BACKUP_PHRASE_ROUTE,
|
||||
INITIALIZE_CONFIRM_SEED_ROUTE,
|
||||
INITIALIZE_CREATE_PASSWORD_ROUTE,
|
||||
} from '../../../../ui/app/routes'
|
||||
import WelcomeScreen from '../../../../ui/app/welcome-screen'
|
||||
|
||||
class FirstTimeFlow extends Component {
|
||||
|
||||
static propTypes = {
|
||||
isInitialized: PropTypes.bool,
|
||||
seedWords: PropTypes.string,
|
||||
address: PropTypes.string,
|
||||
noActiveNotices: PropTypes.bool,
|
||||
goToBuyEtherView: PropTypes.func,
|
||||
isUnlocked: PropTypes.bool,
|
||||
history: PropTypes.object,
|
||||
welcomeScreenSeen: PropTypes.bool,
|
||||
isPopup: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
isInitialized: false,
|
||||
seedWords: '',
|
||||
noActiveNotices: false,
|
||||
};
|
||||
|
||||
render () {
|
||||
return (
|
||||
<div className="flex-column flex-grow">
|
||||
<div className="first-time-flow">
|
||||
<Switch>
|
||||
<Route exact path={INITIALIZE_IMPORT_ACCOUNT_ROUTE} component={ImportAccountScreen} />
|
||||
<Route
|
||||
exact
|
||||
path={INITIALIZE_IMPORT_WITH_SEED_PHRASE_ROUTE}
|
||||
component={ImportSeedPhraseScreen}
|
||||
/>
|
||||
<Route exact path={INITIALIZE_UNIQUE_IMAGE_ROUTE} component={UniqueImageScreen} />
|
||||
<Route exact path={INITIALIZE_NOTICE_ROUTE} component={NoticeScreen} />
|
||||
<Route exact path={INITIALIZE_BACKUP_PHRASE_ROUTE} component={BackupPhraseScreen} />
|
||||
<Route exact path={INITIALIZE_CONFIRM_SEED_ROUTE} component={ConfirmSeed} />
|
||||
<Route exact path={INITIALIZE_CREATE_PASSWORD_ROUTE} component={CreatePasswordScreen} />
|
||||
<Route exact path={INITIALIZE_ROUTE} component={WelcomeScreen} />
|
||||
</Switch>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = ({ metamask }) => {
|
||||
const {
|
||||
isInitialized,
|
||||
seedWords,
|
||||
noActiveNotices,
|
||||
selectedAddress,
|
||||
forgottenPassword,
|
||||
isMascara,
|
||||
isUnlocked,
|
||||
welcomeScreenSeen,
|
||||
isPopup,
|
||||
} = metamask
|
||||
|
||||
return {
|
||||
isMascara,
|
||||
isInitialized,
|
||||
seedWords,
|
||||
noActiveNotices,
|
||||
address: selectedAddress,
|
||||
forgottenPassword,
|
||||
isUnlocked,
|
||||
welcomeScreenSeen,
|
||||
isPopup,
|
||||
}
|
||||
}
|
||||
|
||||
export default compose(
|
||||
withRouter,
|
||||
connect(mapStateToProps)
|
||||
)(FirstTimeFlow)
|
@ -1,17 +0,0 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import Spinner from './spinner'
|
||||
|
||||
export default function LoadingScreen ({ className = '', loadingMessage }) {
|
||||
return (
|
||||
<div className={`${className} loading-screen`}>
|
||||
<Spinner color="#1B344D" />
|
||||
<div className="loading-screen__message">{loadingMessage}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
LoadingScreen.propTypes = {
|
||||
className: PropTypes.string,
|
||||
loadingMessage: PropTypes.string,
|
||||
}
|
@ -1,135 +0,0 @@
|
||||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import Markdown from 'react-markdown'
|
||||
import { connect } from 'react-redux'
|
||||
import { withRouter } from 'react-router-dom'
|
||||
import { compose } from 'recompose'
|
||||
import debounce from 'lodash.debounce'
|
||||
import { markNoticeRead } from '../../../../ui/app/actions'
|
||||
import Identicon from '../../../../ui/app/components/identicon'
|
||||
import Breadcrumbs from './breadcrumbs'
|
||||
import { INITIALIZE_BACKUP_PHRASE_ROUTE } from '../../../../ui/app/routes'
|
||||
import LoadingScreen from './loading-screen'
|
||||
|
||||
class NoticeScreen extends Component {
|
||||
static propTypes = {
|
||||
address: PropTypes.string.isRequired,
|
||||
nextUnreadNotice: PropTypes.shape({
|
||||
title: PropTypes.string,
|
||||
date: PropTypes.string,
|
||||
body: PropTypes.string,
|
||||
}),
|
||||
location: PropTypes.shape({
|
||||
state: PropTypes.shape({
|
||||
next: PropTypes.func.isRequired,
|
||||
}),
|
||||
}),
|
||||
markNoticeRead: PropTypes.func,
|
||||
history: PropTypes.object,
|
||||
isLoading: PropTypes.bool,
|
||||
noActiveNotices: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
nextUnreadNotice: {},
|
||||
};
|
||||
|
||||
state = {
|
||||
atBottom: false,
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
if (this.props.noActiveNotices) {
|
||||
this.props.history.push(INITIALIZE_BACKUP_PHRASE_ROUTE)
|
||||
}
|
||||
|
||||
this.onScroll()
|
||||
}
|
||||
|
||||
acceptTerms = () => {
|
||||
const { markNoticeRead, nextUnreadNotice, history } = this.props
|
||||
markNoticeRead(nextUnreadNotice)
|
||||
.then(hasActiveNotices => {
|
||||
if (!hasActiveNotices) {
|
||||
history.push(INITIALIZE_BACKUP_PHRASE_ROUTE)
|
||||
} else {
|
||||
this.setState({ atBottom: false })
|
||||
this.onScroll()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onScroll = debounce(() => {
|
||||
if (this.state.atBottom) return
|
||||
|
||||
const target = document.querySelector('.tou__body')
|
||||
const {scrollTop, offsetHeight, scrollHeight} = target
|
||||
const atBottom = scrollTop + offsetHeight >= scrollHeight
|
||||
|
||||
this.setState({atBottom: atBottom})
|
||||
}, 25)
|
||||
|
||||
render () {
|
||||
const {
|
||||
address,
|
||||
nextUnreadNotice: { title, body },
|
||||
isLoading,
|
||||
} = this.props
|
||||
const { atBottom } = this.state
|
||||
|
||||
return (
|
||||
isLoading
|
||||
? <LoadingScreen />
|
||||
: (
|
||||
<div className="first-time-flow">
|
||||
<div className="first-view-main-wrapper">
|
||||
<div className="first-view-main">
|
||||
<div
|
||||
className="tou"
|
||||
onScroll={this.onScroll}
|
||||
>
|
||||
<Identicon address={address} diameter={70} />
|
||||
<div className="tou__title">{title}</div>
|
||||
<Markdown
|
||||
className="tou__body markdown"
|
||||
source={body}
|
||||
skipHtml
|
||||
/>
|
||||
<button
|
||||
className="first-time-flow__button"
|
||||
onClick={atBottom && this.acceptTerms}
|
||||
disabled={!atBottom}
|
||||
>
|
||||
Accept
|
||||
</button>
|
||||
<Breadcrumbs total={3} currentIndex={2} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = ({ metamask, appState }) => {
|
||||
const { selectedAddress, nextUnreadNotice, noActiveNotices } = metamask
|
||||
const { isLoading } = appState
|
||||
|
||||
return {
|
||||
address: selectedAddress,
|
||||
nextUnreadNotice,
|
||||
noActiveNotices,
|
||||
isLoading,
|
||||
}
|
||||
}
|
||||
|
||||
export default compose(
|
||||
withRouter,
|
||||
connect(
|
||||
mapStateToProps,
|
||||
dispatch => ({
|
||||
markNoticeRead: notice => dispatch(markNoticeRead(notice)),
|
||||
})
|
||||
)
|
||||
)(NoticeScreen)
|
@ -1,176 +0,0 @@
|
||||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { connect } from 'react-redux'
|
||||
import classnames from 'classnames'
|
||||
import { withRouter } from 'react-router-dom'
|
||||
import { compose } from 'recompose'
|
||||
import Identicon from '../../../../ui/app/components/identicon'
|
||||
import {exportAsFile} from '../../../../ui/app/util'
|
||||
import Breadcrumbs from './breadcrumbs'
|
||||
import LoadingScreen from './loading-screen'
|
||||
import { DEFAULT_ROUTE, INITIALIZE_CONFIRM_SEED_ROUTE } from '../../../../ui/app/routes'
|
||||
|
||||
const LockIcon = props => (
|
||||
<svg
|
||||
version="1.1"
|
||||
id="Capa_1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||
x="0px"
|
||||
y="0px"
|
||||
width="401.998px"
|
||||
height="401.998px"
|
||||
viewBox="0 0 401.998 401.998"
|
||||
style={{enableBackground: 'new 0 0 401.998 401.998'}}
|
||||
xmlSpace="preserve"
|
||||
{...props}
|
||||
>
|
||||
<g>
|
||||
<path
|
||||
d="M357.45,190.721c-5.331-5.33-11.8-7.993-19.417-7.993h-9.131v-54.821c0-35.022-12.559-65.093-37.685-90.218
|
||||
C266.093,12.563,236.025,0,200.998,0c-35.026,0-65.1,12.563-90.222,37.688C85.65,62.814,73.091,92.884,73.091,127.907v54.821
|
||||
h-9.135c-7.611,0-14.084,2.663-19.414,7.993c-5.33,5.326-7.994,11.799-7.994,19.417V374.59c0,7.611,2.665,14.086,7.994,19.417
|
||||
c5.33,5.325,11.803,7.991,19.414,7.991H338.04c7.617,0,14.085-2.663,19.417-7.991c5.325-5.331,7.994-11.806,7.994-19.417V210.135
|
||||
C365.455,202.523,362.782,196.051,357.45,190.721z M274.087,182.728H127.909v-54.821c0-20.175,7.139-37.402,21.414-51.675
|
||||
c14.277-14.275,31.501-21.411,51.678-21.411c20.179,0,37.399,7.135,51.677,21.411c14.271,14.272,21.409,31.5,21.409,51.675V182.728
|
||||
z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
|
||||
class BackupPhraseScreen extends Component {
|
||||
static propTypes = {
|
||||
isLoading: PropTypes.bool.isRequired,
|
||||
address: PropTypes.string.isRequired,
|
||||
seedWords: PropTypes.string,
|
||||
history: PropTypes.object,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
seedWords: '',
|
||||
}
|
||||
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
isShowingSecret: false,
|
||||
}
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
const { seedWords, history } = this.props
|
||||
|
||||
if (!seedWords) {
|
||||
history.push(DEFAULT_ROUTE)
|
||||
}
|
||||
}
|
||||
|
||||
exportSeedWords = () => {
|
||||
const { seedWords } = this.props
|
||||
|
||||
exportAsFile('MetaMask Secret Backup Phrase', seedWords, 'text/plain')
|
||||
}
|
||||
|
||||
renderSecretWordsContainer () {
|
||||
const { isShowingSecret } = this.state
|
||||
|
||||
return (
|
||||
<div className="backup-phrase__secret">
|
||||
<div className={classnames('backup-phrase__secret-words', {
|
||||
'backup-phrase__secret-words--hidden': !isShowingSecret,
|
||||
})}>
|
||||
{this.props.seedWords}
|
||||
</div>
|
||||
{!isShowingSecret && (
|
||||
<div
|
||||
className="backup-phrase__secret-blocker"
|
||||
onClick={() => this.setState({ isShowingSecret: true })}
|
||||
>
|
||||
<LockIcon width="28px" height="35px" fill="#FFFFFF" />
|
||||
<div
|
||||
className="backup-phrase__reveal-button"
|
||||
>
|
||||
Click here to reveal secret words
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderSecretScreen () {
|
||||
const { isShowingSecret } = this.state
|
||||
const { history } = this.props
|
||||
|
||||
return (
|
||||
<div className="backup-phrase__content-wrapper">
|
||||
<div className="backup-phrase__phrase">
|
||||
<div className="backup-phrase__title">Secret Backup Phrase</div>
|
||||
<div className="backup-phrase__body-text">
|
||||
Your secret backup phrase makes it easy to back up and restore your account.
|
||||
</div>
|
||||
<div className="backup-phrase__body-text">
|
||||
WARNING: Never disclose your backup phrase. Anyone with this phrase can take your Ether forever.
|
||||
</div>
|
||||
{this.renderSecretWordsContainer()}
|
||||
</div>
|
||||
<div className="backup-phrase__tips">
|
||||
<div className="backup-phrase__tips-text">Tips:</div>
|
||||
<div className="backup-phrase__tips-text">
|
||||
Store this phrase in a password manager like 1Password.
|
||||
</div>
|
||||
<div className="backup-phrase__tips-text">
|
||||
Write this phrase on a piece of paper and store in a secure location. If you want even more security, write it down on multiple pieces of paper and store each in 2 - 3 different locations.
|
||||
</div>
|
||||
<div className="backup-phrase__tips-text">
|
||||
Memorize this phrase.
|
||||
</div>
|
||||
<div className="backup-phrase__tips-text">
|
||||
<strong>
|
||||
<a className="backup-phrase__tips-text--link backup-phrase__tips-text--strong" onClick={this.exportSeedWords}>
|
||||
Download this Secret Backup Phrase
|
||||
</a>
|
||||
</strong> and keep it stored safely on an external encrypted hard drive or storage medium.
|
||||
</div>
|
||||
</div>
|
||||
<div className="backup-phrase__next-button">
|
||||
<button
|
||||
className="first-time-flow__button"
|
||||
onClick={() => isShowingSecret && history.push(INITIALIZE_CONFIRM_SEED_ROUTE)}
|
||||
disabled={!isShowingSecret}
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
<Breadcrumbs total={3} currentIndex={1} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
render () {
|
||||
return this.props.isLoading
|
||||
? <LoadingScreen loadingMessage="Creating your new account" />
|
||||
: (
|
||||
<div className="first-view-main-wrapper">
|
||||
<div className="first-view-main">
|
||||
<div className="backup-phrase">
|
||||
<Identicon address={this.props.address} diameter={70} />
|
||||
{this.renderSecretScreen()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default compose(
|
||||
withRouter,
|
||||
connect(
|
||||
({ metamask: { selectedAddress, seedWords }, appState: { isLoading } }) => ({
|
||||
seedWords,
|
||||
isLoading,
|
||||
address: selectedAddress,
|
||||
})
|
||||
)
|
||||
)(BackupPhraseScreen)
|
@ -1,70 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function Spinner({ className = '', color = "#000000" }) {
|
||||
return (
|
||||
<div className={`spinner ${className}`}>
|
||||
<svg className="lds-spinner" width="100%" height="100%" xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid" style={{background: 'none'}}>
|
||||
<g transform="rotate(0 50 50)">
|
||||
<rect x={47} y={16} rx={0} ry={0} width={6} height={20} fill={color}>
|
||||
<animate attributeName="opacity" values="1;0" dur="1s" begin="-0.9166666666666666s" repeatCount="indefinite" />
|
||||
</rect>
|
||||
</g>
|
||||
<g transform="rotate(30 50 50)">
|
||||
<rect x={47} y={16} rx={0} ry={0} width={6} height={20} fill={color}>
|
||||
<animate attributeName="opacity" values="1;0" dur="1s" begin="-0.8333333333333334s" repeatCount="indefinite" />
|
||||
</rect>
|
||||
</g>
|
||||
<g transform="rotate(60 50 50)">
|
||||
<rect x={47} y={16} rx={0} ry={0} width={6} height={20} fill={color}>
|
||||
<animate attributeName="opacity" values="1;0" dur="1s" begin="-0.75s" repeatCount="indefinite" />
|
||||
</rect>
|
||||
</g>
|
||||
<g transform="rotate(90 50 50)">
|
||||
<rect x={47} y={16} rx={0} ry={0} width={6} height={20} fill={color}>
|
||||
<animate attributeName="opacity" values="1;0" dur="1s" begin="-0.6666666666666666s" repeatCount="indefinite" />
|
||||
</rect>
|
||||
</g>
|
||||
<g transform="rotate(120 50 50)">
|
||||
<rect x={47} y={16} rx={0} ry={0} width={6} height={20} fill={color}>
|
||||
<animate attributeName="opacity" values="1;0" dur="1s" begin="-0.5833333333333334s" repeatCount="indefinite" />
|
||||
</rect>
|
||||
</g>
|
||||
<g transform="rotate(150 50 50)">
|
||||
<rect x={47} y={16} rx={0} ry={0} width={6} height={20} fill={color}>
|
||||
<animate attributeName="opacity" values="1;0" dur="1s" begin="-0.5s" repeatCount="indefinite" />
|
||||
</rect>
|
||||
</g>
|
||||
<g transform="rotate(180 50 50)">
|
||||
<rect x={47} y={16} rx={0} ry={0} width={6} height={20} fill={color}>
|
||||
<animate attributeName="opacity" values="1;0" dur="1s" begin="-0.4166666666666667s" repeatCount="indefinite" />
|
||||
</rect>
|
||||
</g>
|
||||
<g transform="rotate(210 50 50)">
|
||||
<rect x={47} y={16} rx={0} ry={0} width={6} height={20} fill={color}>
|
||||
<animate attributeName="opacity" values="1;0" dur="1s" begin="-0.3333333333333333s" repeatCount="indefinite" />
|
||||
</rect>
|
||||
</g>
|
||||
<g transform="rotate(240 50 50)">
|
||||
<rect x={47} y={16} rx={0} ry={0} width={6} height={20} fill={color}>
|
||||
<animate attributeName="opacity" values="1;0" dur="1s" begin="-0.25s" repeatCount="indefinite" />
|
||||
</rect>
|
||||
</g>
|
||||
<g transform="rotate(270 50 50)">
|
||||
<rect x={47} y={16} rx={0} ry={0} width={6} height={20} fill={color}>
|
||||
<animate attributeName="opacity" values="1;0" dur="1s" begin="-0.16666666666666666s" repeatCount="indefinite" />
|
||||
</rect>
|
||||
</g>
|
||||
<g transform="rotate(300 50 50)">
|
||||
<rect x={47} y={16} rx={0} ry={0} width={6} height={20} fill={color}>
|
||||
<animate attributeName="opacity" values="1;0" dur="1s" begin="-0.08333333333333333s" repeatCount="indefinite" />
|
||||
</rect>
|
||||
</g>
|
||||
<g transform="rotate(330 50 50)">
|
||||
<rect x={47} y={16} rx={0} ry={0} width={6} height={20} fill={color}>
|
||||
<animate attributeName="opacity" values="1;0" dur="1s" begin="0s" repeatCount="indefinite" />
|
||||
</rect>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,50 +0,0 @@
|
||||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { withRouter } from 'react-router-dom'
|
||||
import { compose } from 'recompose'
|
||||
import {connect} from 'react-redux'
|
||||
import Identicon from '../../../../ui/app/components/identicon'
|
||||
import Breadcrumbs from './breadcrumbs'
|
||||
import { INITIALIZE_NOTICE_ROUTE } from '../../../../ui/app/routes'
|
||||
|
||||
class UniqueImageScreen extends Component {
|
||||
static propTypes = {
|
||||
address: PropTypes.string,
|
||||
history: PropTypes.object,
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<div className="first-view-main-wrapper">
|
||||
<div className="first-view-main">
|
||||
<div className="unique-image">
|
||||
<Identicon address={this.props.address} diameter={70} />
|
||||
<div className="unique-image__title">Your unique account image</div>
|
||||
<div className="unique-image__body-text">
|
||||
This image was programmatically generated for you by your new account number.
|
||||
</div>
|
||||
<div className="unique-image__body-text">
|
||||
You’ll see this image everytime you need to confirm a transaction.
|
||||
</div>
|
||||
<button
|
||||
className="first-time-flow__button"
|
||||
onClick={() => this.props.history.push(INITIALIZE_NOTICE_ROUTE)}
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
<Breadcrumbs total={3} currentIndex={1} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default compose(
|
||||
withRouter,
|
||||
connect(
|
||||
({ metamask: { selectedAddress } }) => ({
|
||||
address: selectedAddress,
|
||||
})
|
||||
)
|
||||
)(UniqueImageScreen)
|
@ -5,8 +5,7 @@ const h = require('react-hyperscript')
|
||||
const actions = require('../../ui/app/actions')
|
||||
const log = require('loglevel')
|
||||
// mascara
|
||||
const MascaraFirstTime = require('../../mascara/src/app/first-time').default
|
||||
const MascaraBuyEtherScreen = require('../../mascara/src/app/first-time/buy-ether-screen').default
|
||||
const FirstTime = require('../../ui/app/components/pages/first-time-flow').default
|
||||
// init
|
||||
const InitializeMenuScreen = require('./first-time/init-menu')
|
||||
const NewKeyChainScreen = require('./new-keychain')
|
||||
@ -153,7 +152,7 @@ App.prototype.renderPrimary = function () {
|
||||
const {isMascara, isOnboarding, providerRequests} = props
|
||||
|
||||
if (isMascara && isOnboarding) {
|
||||
return h(MascaraFirstTime)
|
||||
return h(FirstTime)
|
||||
}
|
||||
|
||||
// notices
|
||||
@ -270,10 +269,6 @@ App.prototype.renderPrimary = function () {
|
||||
log.debug('rendering buy ether screen')
|
||||
return h(BuyView, {key: 'buyEthView'})
|
||||
|
||||
case 'onboardingBuyEth':
|
||||
log.debug('rendering onboarding buy ether screen')
|
||||
return h(MascaraBuyEtherScreen, {key: 'buyEthView'})
|
||||
|
||||
case 'qr':
|
||||
log.debug('rendering show qr screen')
|
||||
return h(AccountQrScreen, {
|
||||
|
@ -9,7 +9,6 @@ var cssFiles = {
|
||||
'lib.css': fs.readFileSync(path.join(__dirname, '/app/css/lib.css'), 'utf8'),
|
||||
'index.css': fs.readFileSync(path.join(__dirname, '/app/css/index.css'), 'utf8'),
|
||||
'transitions.css': fs.readFileSync(path.join(__dirname, '/app/css/transitions.css'), 'utf8'),
|
||||
'first-time.css': fs.readFileSync(path.join(__dirname, '../mascara/src/app/first-time/index.css'), 'utf8'),
|
||||
'react-tooltip-component.css': fs.readFileSync(path.join(__dirname, '..', 'node_modules', 'react-tooltip-component', 'dist', 'react-tooltip-component.css'), 'utf8'),
|
||||
'react-css': fs.readFileSync(path.join(__dirname, '..', 'node_modules', 'react-select', 'dist', 'react-select.css'), 'utf8'),
|
||||
}
|
||||
|
@ -112,7 +112,7 @@ describe('MetaMask', function () {
|
||||
await loadExtension(driver, extensionId)
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const continueBtn = await findElement(driver, By.css('.welcome-screen__button'))
|
||||
const continueBtn = await findElement(driver, By.css('.first-time-flow__button'))
|
||||
await continueBtn.click()
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
@ -120,9 +120,9 @@ describe('MetaMask', function () {
|
||||
|
||||
describe('Going through the first time flow', () => {
|
||||
it('accepts a secure password', async () => {
|
||||
const passwordBox = await findElement(driver, By.css('.create-password #create-password'))
|
||||
const passwordBoxConfirm = await findElement(driver, By.css('.create-password #confirm-password'))
|
||||
const button = await findElement(driver, By.css('.create-password button'))
|
||||
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')
|
||||
@ -131,19 +131,21 @@ describe('MetaMask', function () {
|
||||
})
|
||||
|
||||
it('clicks through the unique image screen', async () => {
|
||||
const nextScreen = await findElement(driver, By.css('.unique-image button'))
|
||||
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
|
||||
const canClickThrough = await driver.findElement(By.css('.tou button')).isEnabled()
|
||||
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('.tou button'))
|
||||
const acceptTos = await findElement(driver, By.css('button.first-time-flow__button'))
|
||||
driver.wait(until.elementIsEnabled(acceptTos))
|
||||
await acceptTos.click()
|
||||
await delay(regularDelayMs)
|
||||
@ -151,17 +153,17 @@ describe('MetaMask', function () {
|
||||
|
||||
it('clicks through the privacy notice', async () => {
|
||||
// privacy notice
|
||||
const nextScreen = await findElement(driver, By.css('.tou button'))
|
||||
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('.markdown'))
|
||||
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('.tou button'))
|
||||
const nextScreen = await findElement(driver, By.css('button.first-time-flow__button'))
|
||||
await nextScreen.click()
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
@ -169,24 +171,23 @@ describe('MetaMask', function () {
|
||||
let seedPhrase
|
||||
|
||||
it('reveals the seed phrase', async () => {
|
||||
const byRevealButton = By.css('.backup-phrase__secret-blocker .backup-phrase__reveal-button')
|
||||
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('.backup-phrase__secret-words')).getText()
|
||||
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('.backup-phrase button'))
|
||||
const nextScreen = await findElement(driver, By.css('button.first-time-flow__button'))
|
||||
await nextScreen.click()
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
|
||||
async function clickWordAndWait (word) {
|
||||
const xpathClass = 'backup-phrase__confirm-seed-option backup-phrase__confirm-seed-option--unselected'
|
||||
const xpath = `//button[@class='${xpathClass}' and contains(text(), '${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()
|
||||
@ -196,13 +197,13 @@ describe('MetaMask', function () {
|
||||
async function retypeSeedPhrase (words, wasReloaded, count = 0) {
|
||||
try {
|
||||
if (wasReloaded) {
|
||||
const byRevealButton = By.css('.backup-phrase__secret-blocker .backup-phrase__reveal-button')
|
||||
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('.backup-phrase button'))
|
||||
const nextScreen = await findElement(driver, By.css('button.first-time-flow__button'))
|
||||
await nextScreen.click()
|
||||
await delay(regularDelayMs)
|
||||
}
|
||||
|
@ -95,7 +95,7 @@ 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-screen__button'))
|
||||
const welcomeScreenBtn = await findElement(driver, By.css('.welcome-page .first-time-flow__button'))
|
||||
welcomeScreenBtn.click()
|
||||
await delay(largeDelayMs)
|
||||
})
|
||||
@ -105,7 +105,7 @@ describe('Using MetaMask with an existing account', function () {
|
||||
await seedPhrase.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const [seedTextArea] = await findElements(driver, By.css('textarea.import-account__secret-phrase'))
|
||||
const [seedTextArea] = await findElements(driver, By.css('textarea.first-time-flow__textarea'))
|
||||
await seedTextArea.sendKeys(testSeedPhrase)
|
||||
await delay(regularDelayMs)
|
||||
|
||||
@ -121,30 +121,31 @@ describe('Using MetaMask with an existing account', function () {
|
||||
|
||||
it('clicks through the ToS', async () => {
|
||||
// terms of use
|
||||
await delay(largeDelayMs)
|
||||
const canClickThrough = await driver.findElement(By.css('.tou button')).isEnabled()
|
||||
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('.tou button'))
|
||||
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('.tou button'))
|
||||
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('.markdown'))
|
||||
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('.tou button'))
|
||||
const nextScreen = await findElement(driver, By.css('button.first-time-flow__button'))
|
||||
await nextScreen.click()
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
|
@ -81,15 +81,15 @@ 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-screen__button'))
|
||||
const welcomeScreenBtn = await findElement(driver, By.css('.welcome-page .first-time-flow__button'))
|
||||
welcomeScreenBtn.click()
|
||||
await delay(largeDelayMs)
|
||||
})
|
||||
|
||||
it('accepts a secure password', async () => {
|
||||
const passwordBox = await findElement(driver, By.css('.create-password #create-password'))
|
||||
const passwordBoxConfirm = await findElement(driver, By.css('.create-password #confirm-password'))
|
||||
const button = await findElement(driver, By.css('.create-password button'))
|
||||
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')
|
||||
@ -98,19 +98,21 @@ describe('MetaMask', function () {
|
||||
})
|
||||
|
||||
it('clicks through the unique image screen', async () => {
|
||||
const nextScreen = await findElement(driver, By.css('.unique-image button'))
|
||||
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
|
||||
const canClickThrough = await driver.findElement(By.css('.tou button')).isEnabled()
|
||||
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('.tou button'))
|
||||
const acceptTos = await findElement(driver, By.css('button.first-time-flow__button'))
|
||||
driver.wait(until.elementIsEnabled(acceptTos))
|
||||
await acceptTos.click()
|
||||
await delay(regularDelayMs)
|
||||
@ -118,17 +120,17 @@ describe('MetaMask', function () {
|
||||
|
||||
it('clicks through the privacy notice', async () => {
|
||||
// privacy notice
|
||||
const nextScreen = await findElement(driver, By.css('.tou button'))
|
||||
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('.markdown'))
|
||||
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('.tou button'))
|
||||
const nextScreen = await findElement(driver, By.css('button.first-time-flow__button'))
|
||||
await nextScreen.click()
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
@ -136,24 +138,23 @@ describe('MetaMask', function () {
|
||||
let seedPhrase
|
||||
|
||||
it('reveals the seed phrase', async () => {
|
||||
const byRevealButton = By.css('.backup-phrase__secret-blocker .backup-phrase__reveal-button')
|
||||
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('.backup-phrase__secret-words')).getText()
|
||||
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('.backup-phrase button'))
|
||||
const nextScreen = await findElement(driver, By.css('button.first-time-flow__button'))
|
||||
await nextScreen.click()
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
|
||||
async function clickWordAndWait (word) {
|
||||
const xpathClass = 'backup-phrase__confirm-seed-option backup-phrase__confirm-seed-option--unselected'
|
||||
const xpath = `//button[@class='${xpathClass}' and contains(text(), '${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()
|
||||
@ -163,13 +164,13 @@ describe('MetaMask', function () {
|
||||
async function retypeSeedPhrase (words, wasReloaded, count = 0) {
|
||||
try {
|
||||
if (wasReloaded) {
|
||||
const byRevealButton = By.css('.backup-phrase__secret-blocker .backup-phrase__reveal-button')
|
||||
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('.backup-phrase button'))
|
||||
const nextScreen = await findElement(driver, By.css('button.first-time-flow__button'))
|
||||
await nextScreen.click()
|
||||
await delay(regularDelayMs)
|
||||
}
|
||||
@ -191,6 +192,7 @@ describe('MetaMask', function () {
|
||||
const words = seedPhrase.split(' ')
|
||||
|
||||
await retypeSeedPhrase(words)
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const confirm = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`))
|
||||
await confirm.click()
|
||||
|
@ -102,15 +102,15 @@ 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-screen__button'))
|
||||
const welcomeScreenBtn = await findElement(driver, By.css('.welcome-page .first-time-flow__button'))
|
||||
welcomeScreenBtn.click()
|
||||
await delay(largeDelayMs)
|
||||
})
|
||||
|
||||
it('accepts a secure password', async () => {
|
||||
const passwordBox = await findElement(driver, By.css('.create-password #create-password'))
|
||||
const passwordBoxConfirm = await findElement(driver, By.css('.create-password #confirm-password'))
|
||||
const button = await findElement(driver, By.css('.create-password button'))
|
||||
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')
|
||||
@ -119,19 +119,21 @@ describe('MetaMask', function () {
|
||||
})
|
||||
|
||||
it('clicks through the unique image screen', async () => {
|
||||
const nextScreen = await findElement(driver, By.css('.unique-image button'))
|
||||
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
|
||||
const canClickThrough = await driver.findElement(By.css('.tou button')).isEnabled()
|
||||
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('.tou button'))
|
||||
const acceptTos = await findElement(driver, By.css('button.first-time-flow__button'))
|
||||
driver.wait(until.elementIsEnabled(acceptTos))
|
||||
await acceptTos.click()
|
||||
await delay(regularDelayMs)
|
||||
@ -139,17 +141,17 @@ describe('MetaMask', function () {
|
||||
|
||||
it('clicks through the privacy notice', async () => {
|
||||
// privacy notice
|
||||
const nextScreen = await findElement(driver, By.css('.tou button'))
|
||||
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('.markdown'))
|
||||
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('.tou button'))
|
||||
const nextScreen = await findElement(driver, By.css('button.first-time-flow__button'))
|
||||
await nextScreen.click()
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
@ -157,24 +159,23 @@ describe('MetaMask', function () {
|
||||
let seedPhrase
|
||||
|
||||
it('reveals the seed phrase', async () => {
|
||||
const byRevealButton = By.css('.backup-phrase__secret-blocker .backup-phrase__reveal-button')
|
||||
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('.backup-phrase__secret-words')).getText()
|
||||
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('.backup-phrase button'))
|
||||
const nextScreen = await findElement(driver, By.css('button.first-time-flow__button'))
|
||||
await nextScreen.click()
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
|
||||
async function clickWordAndWait (word) {
|
||||
const xpathClass = 'backup-phrase__confirm-seed-option backup-phrase__confirm-seed-option--unselected'
|
||||
const xpath = `//button[@class='${xpathClass}' and contains(text(), '${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()
|
||||
@ -184,13 +185,13 @@ describe('MetaMask', function () {
|
||||
async function retypeSeedPhrase (words, wasReloaded, count = 0) {
|
||||
try {
|
||||
if (wasReloaded) {
|
||||
const byRevealButton = By.css('.backup-phrase__secret-blocker .backup-phrase__reveal-button')
|
||||
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('.backup-phrase button'))
|
||||
const nextScreen = await findElement(driver, By.css('button.first-time-flow__button'))
|
||||
await nextScreen.click()
|
||||
await delay(regularDelayMs)
|
||||
}
|
||||
|
@ -85,7 +85,7 @@ function buildFirefoxWebdriver (opts = {}) {
|
||||
|
||||
async function getExtensionIdChrome (driver) {
|
||||
await driver.get('chrome://extensions')
|
||||
const extensionId = await driver.executeScript('return document.querySelector("extensions-manager").shadowRoot.querySelector("extensions-view-manager extensions-item-list").shadowRoot.querySelector("extensions-item:nth-child(2)").getAttribute("id")')
|
||||
const extensionId = await driver.executeScript('return document.querySelector("extensions-manager").shadowRoot.querySelector("extensions-item-list").shadowRoot.querySelector("extensions-item:nth-child(2)").getAttribute("id")')
|
||||
return extensionId
|
||||
}
|
||||
|
||||
|
56
test/unit/migrations/031-test.js
Normal file
56
test/unit/migrations/031-test.js
Normal file
@ -0,0 +1,56 @@
|
||||
const assert = require('assert')
|
||||
const migration31 = require('../../../app/scripts/migrations/031')
|
||||
|
||||
describe('migration #31', () => {
|
||||
it('should set completedOnboarding to true if vault exists', done => {
|
||||
const oldStorage = {
|
||||
'meta': {},
|
||||
'data': {
|
||||
'PreferencesController': {
|
||||
'tokens': [{address: '0xa', symbol: 'A', decimals: 4}, {address: '0xb', symbol: 'B', decimals: 4}],
|
||||
'identities': {
|
||||
'0x6d14': {},
|
||||
'0x3695': {},
|
||||
},
|
||||
},
|
||||
'KeyringController': {
|
||||
'vault': {
|
||||
'data': 'test0',
|
||||
'iv': 'test1',
|
||||
'salt': 'test2',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
migration31.migrate(oldStorage)
|
||||
.then(newStorage => {
|
||||
assert.equal(newStorage.data.PreferencesController.completedOnboarding, true)
|
||||
done()
|
||||
})
|
||||
.catch(done)
|
||||
})
|
||||
|
||||
it('should set completedOnboarding to false if vault does not exist', done => {
|
||||
const oldStorage = {
|
||||
'meta': {},
|
||||
'data': {
|
||||
'PreferencesController': {
|
||||
'tokens': [{address: '0xa', symbol: 'A', decimals: 4}, {address: '0xb', symbol: 'B', decimals: 4}],
|
||||
'identities': {
|
||||
'0x6d14': {},
|
||||
'0x3695': {},
|
||||
},
|
||||
},
|
||||
'KeyringController': {},
|
||||
},
|
||||
}
|
||||
|
||||
migration31.migrate(oldStorage)
|
||||
.then(newStorage => {
|
||||
assert.equal(newStorage.data.PreferencesController.completedOnboarding, false)
|
||||
done()
|
||||
})
|
||||
.catch(done)
|
||||
})
|
||||
})
|
@ -198,7 +198,7 @@ describe('Actions', () => {
|
||||
createNewVaultAndRestoreSpy = sinon.spy(background, 'createNewVaultAndRestore')
|
||||
clearSeedWordCacheSpy = sinon.spy(background, 'clearSeedWordCache')
|
||||
return store.dispatch(actions.createNewVaultAndRestore())
|
||||
.then(() => {
|
||||
.catch(() => {
|
||||
assert(clearSeedWordCacheSpy.calledOnce)
|
||||
assert(createNewVaultAndRestoreSpy.calledOnce)
|
||||
})
|
||||
@ -218,7 +218,7 @@ describe('Actions', () => {
|
||||
})
|
||||
|
||||
return store.dispatch(actions.createNewVaultAndRestore())
|
||||
.then(() => {
|
||||
.catch(() => {
|
||||
assert.deepEqual(store.getActions(), expectedActions)
|
||||
})
|
||||
})
|
||||
@ -240,7 +240,7 @@ describe('Actions', () => {
|
||||
})
|
||||
|
||||
return store.dispatch(actions.createNewVaultAndRestore())
|
||||
.then(() => {
|
||||
.catch(() => {
|
||||
assert.deepEqual(store.getActions(), expectedActions)
|
||||
})
|
||||
})
|
||||
|
@ -85,6 +85,8 @@ var actions = {
|
||||
createNewVaultAndKeychain: createNewVaultAndKeychain,
|
||||
createNewVaultAndRestore: createNewVaultAndRestore,
|
||||
createNewVaultInProgress: createNewVaultInProgress,
|
||||
createNewVaultAndGetSeedPhrase,
|
||||
unlockAndGetSeedPhrase,
|
||||
addNewKeyring,
|
||||
importNewAccount,
|
||||
addNewAccount,
|
||||
@ -312,6 +314,11 @@ var actions = {
|
||||
UPDATE_PREFERENCES: 'UPDATE_PREFERENCES',
|
||||
setUseNativeCurrencyAsPrimaryCurrencyPreference,
|
||||
|
||||
// Onboarding
|
||||
setCompletedOnboarding,
|
||||
completeOnboarding,
|
||||
COMPLETE_ONBOARDING: 'COMPLETE_ONBOARDING',
|
||||
|
||||
setMouseUserState,
|
||||
SET_MOUSE_USER_STATE: 'SET_MOUSE_USER_STATE',
|
||||
|
||||
@ -451,6 +458,7 @@ function createNewVaultAndRestore (password, seed) {
|
||||
.catch(err => {
|
||||
dispatch(actions.displayWarning(err.message))
|
||||
dispatch(actions.hideLoadingIndication())
|
||||
return Promise.reject(err)
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -485,12 +493,71 @@ function createNewVaultAndKeychain (password) {
|
||||
}
|
||||
}
|
||||
|
||||
function createNewVaultAndGetSeedPhrase (password) {
|
||||
return async dispatch => {
|
||||
dispatch(actions.showLoadingIndication())
|
||||
|
||||
try {
|
||||
await createNewVault(password)
|
||||
const seedWords = await verifySeedPhrase()
|
||||
dispatch(actions.hideLoadingIndication())
|
||||
return seedWords
|
||||
} catch (error) {
|
||||
dispatch(actions.hideLoadingIndication())
|
||||
dispatch(actions.displayWarning(error.message))
|
||||
throw new Error(error.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function unlockAndGetSeedPhrase (password) {
|
||||
return async dispatch => {
|
||||
dispatch(actions.showLoadingIndication())
|
||||
|
||||
try {
|
||||
await submitPassword(password)
|
||||
const seedWords = await verifySeedPhrase()
|
||||
await forceUpdateMetamaskState(dispatch)
|
||||
dispatch(actions.hideLoadingIndication())
|
||||
return seedWords
|
||||
} catch (error) {
|
||||
dispatch(actions.hideLoadingIndication())
|
||||
dispatch(actions.displayWarning(error.message))
|
||||
throw new Error(error.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function revealSeedConfirmation () {
|
||||
return {
|
||||
type: this.REVEAL_SEED_CONFIRMATION,
|
||||
}
|
||||
}
|
||||
|
||||
function submitPassword (password) {
|
||||
return new Promise((resolve, reject) => {
|
||||
background.submitPassword(password, error => {
|
||||
if (error) {
|
||||
return reject(error)
|
||||
}
|
||||
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function createNewVault (password) {
|
||||
return new Promise((resolve, reject) => {
|
||||
background.createNewVaultAndKeychain(password, error => {
|
||||
if (error) {
|
||||
return reject(error)
|
||||
}
|
||||
|
||||
resolve(true)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function verifyPassword (password) {
|
||||
return new Promise((resolve, reject) => {
|
||||
background.submitPassword(password, error => {
|
||||
@ -2356,6 +2423,31 @@ function setUseNativeCurrencyAsPrimaryCurrencyPreference (value) {
|
||||
return setPreference('useNativeCurrencyAsPrimaryCurrency', value)
|
||||
}
|
||||
|
||||
function setCompletedOnboarding () {
|
||||
return dispatch => {
|
||||
dispatch(actions.showLoadingIndication())
|
||||
return new Promise((resolve, reject) => {
|
||||
background.completeOnboarding(err => {
|
||||
dispatch(actions.hideLoadingIndication())
|
||||
|
||||
if (err) {
|
||||
dispatch(actions.displayWarning(err.message))
|
||||
return reject(err)
|
||||
}
|
||||
|
||||
dispatch(actions.completeOnboarding())
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function completeOnboarding () {
|
||||
return {
|
||||
type: actions.COMPLETE_ONBOARDING,
|
||||
}
|
||||
}
|
||||
|
||||
function setNetworkNonce (networkNonce) {
|
||||
return {
|
||||
type: actions.SET_NETWORK_NONCE,
|
||||
|
199
ui/app/app.js
199
ui/app/app.js
@ -1,16 +1,14 @@
|
||||
const { Component } = require('react')
|
||||
const PropTypes = require('prop-types')
|
||||
const connect = require('react-redux').connect
|
||||
const { Route, Switch, withRouter } = require('react-router-dom')
|
||||
const { compose } = require('recompose')
|
||||
const h = require('react-hyperscript')
|
||||
const actions = require('./actions')
|
||||
const classnames = require('classnames')
|
||||
const log = require('loglevel')
|
||||
const { getMetaMaskAccounts, getNetworkIdentifier } = require('./selectors')
|
||||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { connect } from 'react-redux'
|
||||
import { Route, Switch, withRouter, matchPath } from 'react-router-dom'
|
||||
import { compose } from 'recompose'
|
||||
import actions from './actions'
|
||||
import log from 'loglevel'
|
||||
import { getMetaMaskAccounts, getNetworkIdentifier } from './selectors'
|
||||
|
||||
// init
|
||||
const InitializeScreen = require('../../mascara/src/app/first-time').default
|
||||
import FirstTimeFlow from './components/pages/first-time-flow'
|
||||
// accounts
|
||||
const SendTransactionScreen = require('./components/send/send.container')
|
||||
const ConfirmTransaction = require('./components/pages/confirm-transaction')
|
||||
@ -21,8 +19,9 @@ const Sidebar = require('./components/sidebars').default
|
||||
// other views
|
||||
import Home from './components/pages/home'
|
||||
import Settings from './components/pages/settings'
|
||||
const Authenticated = require('./components/pages/authenticated')
|
||||
const Initialized = require('./components/pages/initialized')
|
||||
import Authenticated from './higher-order-components/authenticated'
|
||||
import Initialized from './higher-order-components/initialized'
|
||||
import Lock from './components/pages/lock'
|
||||
const RestoreVaultPage = require('./components/pages/keychains/restore-vault').default
|
||||
const RevealSeedConfirmation = require('./components/pages/keychains/reveal-seed')
|
||||
const AddTokenPage = require('./components/pages/add-token')
|
||||
@ -49,8 +48,9 @@ import {
|
||||
} from './selectors/transactions'
|
||||
|
||||
// Routes
|
||||
const {
|
||||
import {
|
||||
DEFAULT_ROUTE,
|
||||
LOCK_ROUTE,
|
||||
UNLOCK_ROUTE,
|
||||
SETTINGS_ROUTE,
|
||||
REVEAL_SEED_ROUTE,
|
||||
@ -62,8 +62,15 @@ const {
|
||||
SEND_ROUTE,
|
||||
CONFIRM_TRANSACTION_ROUTE,
|
||||
INITIALIZE_ROUTE,
|
||||
INITIALIZE_UNLOCK_ROUTE,
|
||||
NOTICE_ROUTE,
|
||||
} = require('./routes')
|
||||
} from './routes'
|
||||
|
||||
// enums
|
||||
import {
|
||||
ENVIRONMENT_TYPE_NOTIFICATION,
|
||||
ENVIRONMENT_TYPE_POPUP,
|
||||
} from '../../app/scripts/lib/enums'
|
||||
|
||||
class App extends Component {
|
||||
componentWillMount () {
|
||||
@ -75,37 +82,67 @@ class App extends Component {
|
||||
}
|
||||
|
||||
renderRoutes () {
|
||||
const exact = true
|
||||
|
||||
return (
|
||||
h(Switch, [
|
||||
h(Route, { path: INITIALIZE_ROUTE, component: InitializeScreen }),
|
||||
h(Initialized, { path: UNLOCK_ROUTE, exact, component: UnlockPage }),
|
||||
h(Initialized, { path: RESTORE_VAULT_ROUTE, exact, component: RestoreVaultPage }),
|
||||
h(Authenticated, { path: REVEAL_SEED_ROUTE, exact, component: RevealSeedConfirmation }),
|
||||
h(Authenticated, { path: SETTINGS_ROUTE, component: Settings }),
|
||||
h(Authenticated, { path: NOTICE_ROUTE, exact, component: NoticeScreen }),
|
||||
h(Authenticated, {
|
||||
path: `${CONFIRM_TRANSACTION_ROUTE}/:id?`,
|
||||
component: ConfirmTransaction,
|
||||
}),
|
||||
h(Authenticated, { path: SEND_ROUTE, exact, component: SendTransactionScreen }),
|
||||
h(Authenticated, { path: ADD_TOKEN_ROUTE, exact, component: AddTokenPage }),
|
||||
h(Authenticated, { path: CONFIRM_ADD_TOKEN_ROUTE, exact, component: ConfirmAddTokenPage }),
|
||||
h(Authenticated, { path: CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE, exact, component: ConfirmAddSuggestedTokenPage }),
|
||||
h(Authenticated, { path: NEW_ACCOUNT_ROUTE, component: CreateAccountPage }),
|
||||
h(Authenticated, { path: DEFAULT_ROUTE, exact, component: Home }),
|
||||
])
|
||||
<Switch>
|
||||
<Route path={LOCK_ROUTE} component={Lock} exact />
|
||||
<Route path={INITIALIZE_ROUTE} component={FirstTimeFlow} />
|
||||
<Initialized path={UNLOCK_ROUTE} component={UnlockPage} exact />
|
||||
<Initialized path={RESTORE_VAULT_ROUTE} component={RestoreVaultPage} exact />
|
||||
<Authenticated path={REVEAL_SEED_ROUTE} component={RevealSeedConfirmation} exact />
|
||||
<Authenticated path={SETTINGS_ROUTE} component={Settings} />
|
||||
<Authenticated path={NOTICE_ROUTE} component={NoticeScreen} exact />
|
||||
<Authenticated path={`${CONFIRM_TRANSACTION_ROUTE}/:id?`} component={ConfirmTransaction} />
|
||||
<Authenticated path={SEND_ROUTE} component={SendTransactionScreen} exact />
|
||||
<Authenticated path={ADD_TOKEN_ROUTE} component={AddTokenPage} exact />
|
||||
<Authenticated path={CONFIRM_ADD_TOKEN_ROUTE} component={ConfirmAddTokenPage} exact />
|
||||
<Authenticated path={CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE} component={ConfirmAddSuggestedTokenPage} exact />
|
||||
<Authenticated path={NEW_ACCOUNT_ROUTE} component={CreateAccountPage} />
|
||||
<Authenticated path={DEFAULT_ROUTE} component={Home} exact />
|
||||
</Switch>
|
||||
)
|
||||
}
|
||||
|
||||
onInitializationUnlockPage () {
|
||||
const { location } = this.props
|
||||
return Boolean(matchPath(location.pathname, { path: INITIALIZE_UNLOCK_ROUTE, exact: true }))
|
||||
}
|
||||
|
||||
onConfirmPage () {
|
||||
const { location } = this.props
|
||||
return Boolean(matchPath(location.pathname, { path: CONFIRM_TRANSACTION_ROUTE, exact: false }))
|
||||
}
|
||||
|
||||
hasProviderRequests () {
|
||||
const { providerRequests } = this.props
|
||||
return Array.isArray(providerRequests) && providerRequests.length > 0
|
||||
}
|
||||
|
||||
hideAppHeader () {
|
||||
const { location } = this.props
|
||||
|
||||
const isInitializing = Boolean(matchPath(location.pathname, {
|
||||
path: INITIALIZE_ROUTE, exact: false,
|
||||
}))
|
||||
|
||||
if (isInitializing && !this.onInitializationUnlockPage()) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (window.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_NOTIFICATION) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (window.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_POPUP) {
|
||||
return this.onConfirmPage() || this.hasProviderRequests()
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
isLoading,
|
||||
alertMessage,
|
||||
loadingMessage,
|
||||
network,
|
||||
isMouseUser,
|
||||
provider,
|
||||
frequentRpcListDetail,
|
||||
currentView,
|
||||
@ -127,58 +164,47 @@ class App extends Component {
|
||||
const { transaction: sidebarTransaction } = props || {}
|
||||
|
||||
return (
|
||||
h('.flex-column.full-height', {
|
||||
className: classnames({ 'mouse-user-styles': isMouseUser }),
|
||||
style: {
|
||||
overflowX: 'hidden',
|
||||
position: 'relative',
|
||||
alignItems: 'center',
|
||||
},
|
||||
tabIndex: '0',
|
||||
onClick: () => setMouseUserState(true),
|
||||
onKeyDown: (e) => {
|
||||
<div
|
||||
className="app"
|
||||
onClick={() => setMouseUserState(true)}
|
||||
onKeyDown={e => {
|
||||
if (e.keyCode === 9) {
|
||||
setMouseUserState(false)
|
||||
}
|
||||
},
|
||||
}, [
|
||||
|
||||
// global modal
|
||||
h(Modal, {}, []),
|
||||
|
||||
// global alert
|
||||
h(Alert, {visible: this.props.alertOpen, msg: alertMessage}),
|
||||
|
||||
h(AppHeader),
|
||||
|
||||
// sidebar
|
||||
h(Sidebar, {
|
||||
sidebarOpen: sidebarIsOpen,
|
||||
sidebarShouldClose: sidebarTransaction && !submittedPendingTransactions.find(({ id }) => id === sidebarTransaction.id),
|
||||
hideSidebar: this.props.hideSidebar,
|
||||
transitionName: sidebarTransitionName,
|
||||
type: sidebarType,
|
||||
sidebarProps: sidebar.props,
|
||||
}),
|
||||
|
||||
// network dropdown
|
||||
h(NetworkDropdown, {
|
||||
provider,
|
||||
frequentRpcListDetail,
|
||||
}, []),
|
||||
|
||||
h(AccountMenu),
|
||||
|
||||
h('div.main-container-wrapper', [
|
||||
isLoading && h(Loading, {
|
||||
loadingMessage: loadMessage,
|
||||
}),
|
||||
!isLoading && isLoadingNetwork && h(LoadingNetwork),
|
||||
|
||||
// content
|
||||
this.renderRoutes(),
|
||||
]),
|
||||
])
|
||||
}}
|
||||
>
|
||||
<Modal />
|
||||
<Alert
|
||||
visible={this.props.alertOpen}
|
||||
msg={alertMessage}
|
||||
/>
|
||||
{
|
||||
!this.hideAppHeader() && (
|
||||
<AppHeader
|
||||
hideNetworkIndicator={this.onInitializationUnlockPage()}
|
||||
disabled={this.onConfirmPage()}
|
||||
/>
|
||||
)
|
||||
}
|
||||
<Sidebar
|
||||
sidebarOpen={sidebarIsOpen}
|
||||
sidebarShouldClose={sidebarTransaction && !submittedPendingTransactions.find(({ id }) => id === sidebarTransaction.id)}
|
||||
hideSidebar={this.props.hideSidebar}
|
||||
transitionName={sidebarTransitionName}
|
||||
type={sidebarType}
|
||||
sidebarProps={sidebar.props}
|
||||
/>
|
||||
<NetworkDropdown
|
||||
provider={provider}
|
||||
frequentRpcListDetail={frequentRpcListDetail}
|
||||
/>
|
||||
<AccountMenu />
|
||||
<div className="main-container-wrapper">
|
||||
{ isLoading && <Loading loadingMessage={loadMessage} /> }
|
||||
{ !isLoading && isLoadingNetwork && <LoadingNetwork /> }
|
||||
{ this.renderRoutes() }
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -282,6 +308,7 @@ App.propTypes = {
|
||||
setMouseUserState: PropTypes.func,
|
||||
t: PropTypes.func,
|
||||
providerId: PropTypes.string,
|
||||
providerRequests: PropTypes.array,
|
||||
}
|
||||
|
||||
function mapStateToProps (state) {
|
||||
@ -310,6 +337,7 @@ function mapStateToProps (state) {
|
||||
unapprovedMsgCount,
|
||||
unapprovedPersonalMsgCount,
|
||||
unapprovedTypedMessagesCount,
|
||||
providerRequests,
|
||||
} = metamask
|
||||
const selected = address || Object.keys(accounts)[0]
|
||||
|
||||
@ -357,6 +385,7 @@ function mapStateToProps (state) {
|
||||
identities,
|
||||
selected,
|
||||
keyrings,
|
||||
providerRequests,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,20 +1,13 @@
|
||||
import React, { PureComponent } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import classnames from 'classnames'
|
||||
import { matchPath } from 'react-router-dom'
|
||||
import Identicon from '../identicon'
|
||||
|
||||
const {
|
||||
ENVIRONMENT_TYPE_NOTIFICATION,
|
||||
ENVIRONMENT_TYPE_POPUP,
|
||||
} = require('../../../../app/scripts/lib/enums')
|
||||
const { DEFAULT_ROUTE, INITIALIZE_ROUTE, CONFIRM_TRANSACTION_ROUTE } = require('../../routes')
|
||||
import { DEFAULT_ROUTE } from '../../routes'
|
||||
const NetworkIndicator = require('../network')
|
||||
|
||||
export default class AppHeader extends PureComponent {
|
||||
static propTypes = {
|
||||
history: PropTypes.object,
|
||||
location: PropTypes.object,
|
||||
network: PropTypes.string,
|
||||
provider: PropTypes.object,
|
||||
networkDropdownOpen: PropTypes.bool,
|
||||
@ -23,7 +16,8 @@ export default class AppHeader extends PureComponent {
|
||||
toggleAccountMenu: PropTypes.func,
|
||||
selectedAddress: PropTypes.string,
|
||||
isUnlocked: PropTypes.bool,
|
||||
providerRequests: PropTypes.array,
|
||||
hideNetworkIndicator: PropTypes.bool,
|
||||
disabled: PropTypes.bool,
|
||||
}
|
||||
|
||||
static contextTypes = {
|
||||
@ -41,34 +35,15 @@ export default class AppHeader extends PureComponent {
|
||||
: hideNetworkDropdown()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not the user is in the middle of a confirmation prompt
|
||||
*
|
||||
* This accounts for both tx confirmations as well as provider approvals
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isConfirming () {
|
||||
const { location, providerRequests } = this.props
|
||||
const confirmTxRouteMatch = matchPath(location.pathname, {
|
||||
exact: false,
|
||||
path: CONFIRM_TRANSACTION_ROUTE,
|
||||
})
|
||||
const isConfirmingTx = Boolean(confirmTxRouteMatch)
|
||||
const hasPendingProviderApprovals = Array.isArray(providerRequests) && providerRequests.length > 0
|
||||
|
||||
return isConfirmingTx || hasPendingProviderApprovals
|
||||
}
|
||||
|
||||
renderAccountMenu () {
|
||||
const { isUnlocked, toggleAccountMenu, selectedAddress } = this.props
|
||||
const { isUnlocked, toggleAccountMenu, selectedAddress, disabled } = this.props
|
||||
|
||||
return isUnlocked && (
|
||||
<div
|
||||
className={classnames('account-menu__icon', {
|
||||
'account-menu__icon--disabled': this.isConfirming(),
|
||||
'account-menu__icon--disabled': disabled,
|
||||
})}
|
||||
onClick={() => this.isConfirming() || toggleAccountMenu()}
|
||||
onClick={() => disabled || toggleAccountMenu()}
|
||||
>
|
||||
<Identicon
|
||||
address={selectedAddress}
|
||||
@ -78,38 +53,16 @@ export default class AppHeader extends PureComponent {
|
||||
)
|
||||
}
|
||||
|
||||
hideAppHeader () {
|
||||
const { location } = this.props
|
||||
|
||||
const isInitializing = Boolean(matchPath(location.pathname, {
|
||||
path: INITIALIZE_ROUTE, exact: false,
|
||||
}))
|
||||
|
||||
if (isInitializing) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (window.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_NOTIFICATION) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (window.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_POPUP && this.isConfirming()) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
history,
|
||||
network,
|
||||
provider,
|
||||
history,
|
||||
isUnlocked,
|
||||
hideNetworkIndicator,
|
||||
disabled,
|
||||
} = this.props
|
||||
|
||||
if (this.hideAppHeader()) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classnames('app-header', { 'app-header--back-drop': isUnlocked })}>
|
||||
@ -131,14 +84,18 @@ export default class AppHeader extends PureComponent {
|
||||
/>
|
||||
</div>
|
||||
<div className="app-header__account-menu-container">
|
||||
<div className="app-header__network-component-wrapper">
|
||||
<NetworkIndicator
|
||||
network={network}
|
||||
provider={provider}
|
||||
onClick={event => this.handleNetworkIndicatorClick(event)}
|
||||
disabled={this.isConfirming()}
|
||||
/>
|
||||
</div>
|
||||
{
|
||||
!hideNetworkIndicator && (
|
||||
<div className="app-header__network-component-wrapper">
|
||||
<NetworkIndicator
|
||||
network={network}
|
||||
provider={provider}
|
||||
onClick={event => this.handleNetworkIndicatorClick(event)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{ this.renderAccountMenu() }
|
||||
</div>
|
||||
</div>
|
||||
|
@ -11,7 +11,6 @@ const mapStateToProps = state => {
|
||||
const {
|
||||
network,
|
||||
provider,
|
||||
providerRequests,
|
||||
selectedAddress,
|
||||
isUnlocked,
|
||||
} = metamask
|
||||
@ -20,7 +19,6 @@ const mapStateToProps = state => {
|
||||
networkDropdownOpen,
|
||||
network,
|
||||
provider,
|
||||
providerRequests,
|
||||
selectedAddress,
|
||||
isUnlocked,
|
||||
}
|
||||
|
29
ui/app/components/breadcrumbs/breadcrumbs.component.js
Normal file
29
ui/app/components/breadcrumbs/breadcrumbs.component.js
Normal file
@ -0,0 +1,29 @@
|
||||
import React, { PureComponent } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import classnames from 'classnames'
|
||||
|
||||
export default class Breadcrumbs extends PureComponent {
|
||||
static propTypes = {
|
||||
className: PropTypes.string,
|
||||
currentIndex: PropTypes.number,
|
||||
total: PropTypes.number,
|
||||
}
|
||||
|
||||
render () {
|
||||
const { className, currentIndex, total } = this.props
|
||||
|
||||
return (
|
||||
<div className={classnames('breadcrumbs', className)}>
|
||||
{
|
||||
Array(total).fill().map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="breadcrumb"
|
||||
style={{backgroundColor: i === currentIndex ? '#D8D8D8' : '#FFFFFF'}}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
1
ui/app/components/breadcrumbs/index.js
Normal file
1
ui/app/components/breadcrumbs/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './breadcrumbs.component'
|
15
ui/app/components/breadcrumbs/index.scss
Normal file
15
ui/app/components/breadcrumbs/index.scss
Normal file
@ -0,0 +1,15 @@
|
||||
.breadcrumbs {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
border: 1px solid #979797;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.breadcrumb + .breadcrumb {
|
||||
margin-left: 10px;
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
import React from 'react'
|
||||
import assert from 'assert'
|
||||
import { shallow } from 'enzyme'
|
||||
import Breadcrumbs from '../breadcrumbs.component'
|
||||
|
||||
describe('Breadcrumbs Component', () => {
|
||||
it('should render with the correct colors', () => {
|
||||
const wrapper = shallow(
|
||||
<Breadcrumbs
|
||||
currentIndex={1}
|
||||
total={3}
|
||||
/>
|
||||
)
|
||||
|
||||
assert.ok(wrapper)
|
||||
assert.equal(wrapper.find('.breadcrumbs').length, 1)
|
||||
assert.equal(wrapper.find('.breadcrumb').length, 3)
|
||||
assert.equal(wrapper.find('.breadcrumb').at(0).props().style['backgroundColor'], '#FFFFFF')
|
||||
assert.equal(wrapper.find('.breadcrumb').at(1).props().style['backgroundColor'], '#D8D8D8')
|
||||
assert.equal(wrapper.find('.breadcrumb').at(2).props().style['backgroundColor'], '#FFFFFF')
|
||||
})
|
||||
})
|
@ -8,6 +8,7 @@ const CLASSNAME_SECONDARY = 'btn-secondary'
|
||||
const CLASSNAME_CONFIRM = 'btn-confirm'
|
||||
const CLASSNAME_RAISED = 'btn-raised'
|
||||
const CLASSNAME_LARGE = 'btn--large'
|
||||
const CLASSNAME_FIRST_TIME = 'btn--first-time'
|
||||
|
||||
const typeHash = {
|
||||
default: CLASSNAME_DEFAULT,
|
||||
@ -15,6 +16,7 @@ const typeHash = {
|
||||
secondary: CLASSNAME_SECONDARY,
|
||||
confirm: CLASSNAME_CONFIRM,
|
||||
raised: CLASSNAME_RAISED,
|
||||
'first-time': CLASSNAME_FIRST_TIME,
|
||||
}
|
||||
|
||||
export default class Button extends Component {
|
||||
|
@ -4,6 +4,8 @@
|
||||
|
||||
@import './app-header/index';
|
||||
|
||||
@import './breadcrumbs/index';
|
||||
|
||||
@import './button-group/index';
|
||||
|
||||
@import './card/index';
|
||||
|
1
ui/app/components/lock-icon/index.js
Normal file
1
ui/app/components/lock-icon/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './lock-icon.component'
|
32
ui/app/components/lock-icon/lock-icon.component.js
Normal file
32
ui/app/components/lock-icon/lock-icon.component.js
Normal file
@ -0,0 +1,32 @@
|
||||
import React from 'react'
|
||||
|
||||
export default function LockIcon (props) {
|
||||
return (
|
||||
<svg
|
||||
version="1.1"
|
||||
id="Capa_1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||
x="0px"
|
||||
y="0px"
|
||||
width="401.998px"
|
||||
height="401.998px"
|
||||
viewBox="0 0 401.998 401.998"
|
||||
style={{enableBackground: 'new 0 0 401.998 401.998'}}
|
||||
xmlSpace="preserve"
|
||||
{...props}
|
||||
>
|
||||
<g>
|
||||
<path
|
||||
d="M357.45,190.721c-5.331-5.33-11.8-7.993-19.417-7.993h-9.131v-54.821c0-35.022-12.559-65.093-37.685-90.218
|
||||
C266.093,12.563,236.025,0,200.998,0c-35.026,0-65.1,12.563-90.222,37.688C85.65,62.814,73.091,92.884,73.091,127.907v54.821
|
||||
h-9.135c-7.611,0-14.084,2.663-19.414,7.993c-5.33,5.326-7.994,11.799-7.994,19.417V374.59c0,7.611,2.665,14.086,7.994,19.417
|
||||
c5.33,5.325,11.803,7.991,19.414,7.991H338.04c7.617,0,14.085-2.663,19.417-7.991c5.325-5.331,7.994-11.806,7.994-19.417V210.135
|
||||
C365.455,202.523,362.782,196.051,357.45,190.721z M274.087,182.728H127.909v-54.821c0-20.175,7.139-37.402,21.414-51.675
|
||||
c14.277-14.275,31.501-21.411,51.678-21.411c20.179,0,37.399,7.135,51.677,21.411c14.271,14.272,21.409,31.5,21.409,51.675V182.728
|
||||
z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
@ -122,7 +122,8 @@ const MODALS = {
|
||||
display: 'flex',
|
||||
},
|
||||
laptopModalStyle: {
|
||||
width: '850px',
|
||||
width: 'initial',
|
||||
maxWidth: '850px',
|
||||
top: 'calc(10% + 10px)',
|
||||
left: '0',
|
||||
right: '0',
|
||||
|
@ -42,6 +42,12 @@
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
&__bottom {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&__footer {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
|
@ -1,34 +0,0 @@
|
||||
const { connect } = require('react-redux')
|
||||
const PropTypes = require('prop-types')
|
||||
const { Redirect } = require('react-router-dom')
|
||||
const h = require('react-hyperscript')
|
||||
const MetamaskRoute = require('./metamask-route')
|
||||
const { UNLOCK_ROUTE, INITIALIZE_ROUTE } = require('../../routes')
|
||||
|
||||
const Authenticated = props => {
|
||||
const { isUnlocked, isInitialized } = props
|
||||
|
||||
switch (true) {
|
||||
case isUnlocked && isInitialized:
|
||||
return h(MetamaskRoute, { ...props })
|
||||
case !isInitialized:
|
||||
return h(Redirect, { to: { pathname: INITIALIZE_ROUTE } })
|
||||
default:
|
||||
return h(Redirect, { to: { pathname: UNLOCK_ROUTE } })
|
||||
}
|
||||
}
|
||||
|
||||
Authenticated.propTypes = {
|
||||
isUnlocked: PropTypes.bool,
|
||||
isInitialized: PropTypes.bool,
|
||||
}
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const { metamask: { isUnlocked, isInitialized } } = state
|
||||
return {
|
||||
isUnlocked,
|
||||
isInitialized,
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = connect(mapStateToProps)(Authenticated)
|
@ -0,0 +1,61 @@
|
||||
import React, { PureComponent } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Switch, Route } from 'react-router-dom'
|
||||
import NewAccount from './new-account'
|
||||
import ImportWithSeedPhrase from './import-with-seed-phrase'
|
||||
import UniqueImage from './unique-image'
|
||||
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,
|
||||
onCreateNewAccount: PropTypes.func,
|
||||
onCreateNewAccountFromSeed: PropTypes.func,
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
const { isInitialized, history } = this.props
|
||||
|
||||
if (isInitialized) {
|
||||
history.push(INITIALIZE_NOTICE_ROUTE)
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { onCreateNewAccount, onCreateNewAccountFromSeed } = this.props
|
||||
|
||||
return (
|
||||
<div className="first-time-flow__wrapper">
|
||||
<Switch>
|
||||
<Route exact path={INITIALIZE_UNIQUE_IMAGE_ROUTE} component={UniqueImage} />
|
||||
<Route
|
||||
exact
|
||||
path={INITIALIZE_IMPORT_WITH_SEED_PHRASE_ROUTE}
|
||||
render={props => (
|
||||
<ImportWithSeedPhrase
|
||||
{ ...props }
|
||||
onSubmit={onCreateNewAccountFromSeed}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={INITIALIZE_CREATE_PASSWORD_ROUTE}
|
||||
render={props => (
|
||||
<NewAccount
|
||||
{ ...props }
|
||||
onSubmit={onCreateNewAccount}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Switch>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
import { connect } from 'react-redux'
|
||||
import CreatePassword from './create-password.component'
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const { metamask: { isInitialized } } = state
|
||||
|
||||
return {
|
||||
isInitialized,
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(CreatePassword)
|
@ -0,0 +1,214 @@
|
||||
import {validateMnemonic} from 'bip39'
|
||||
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,
|
||||
} from '../../../../../routes'
|
||||
|
||||
export default class ImportWithSeedPhrase extends PureComponent {
|
||||
static contextTypes = {
|
||||
t: PropTypes.func,
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
history: PropTypes.object,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
state = {
|
||||
seedPhrase: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
seedPhraseError: '',
|
||||
passwordError: '',
|
||||
confirmPasswordError: '',
|
||||
}
|
||||
|
||||
parseSeedPhrase = (seedPhrase) => {
|
||||
return seedPhrase
|
||||
.match(/\w+/g)
|
||||
.join(' ')
|
||||
}
|
||||
|
||||
handleSeedPhraseChange (seedPhrase) {
|
||||
let seedPhraseError = ''
|
||||
|
||||
if (seedPhrase) {
|
||||
if (this.parseSeedPhrase(seedPhrase).split(' ').length !== 12) {
|
||||
seedPhraseError = this.context.t('seedPhraseReq')
|
||||
} else if (!validateMnemonic(seedPhrase)) {
|
||||
seedPhraseError = this.context.t('invalidSeedPhrase')
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({ seedPhrase, seedPhraseError })
|
||||
}
|
||||
|
||||
handlePasswordChange (password) {
|
||||
const { t } = this.context
|
||||
|
||||
this.setState(state => {
|
||||
const { confirmPassword } = state
|
||||
let confirmPasswordError = ''
|
||||
let passwordError = ''
|
||||
|
||||
if (password && password.length < 8) {
|
||||
passwordError = t('passwordNotLongEnough')
|
||||
}
|
||||
|
||||
if (confirmPassword && password !== confirmPassword) {
|
||||
confirmPasswordError = t('passwordsDontMatch')
|
||||
}
|
||||
|
||||
return {
|
||||
password,
|
||||
passwordError,
|
||||
confirmPasswordError,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
handleConfirmPasswordChange (confirmPassword) {
|
||||
const { t } = this.context
|
||||
|
||||
this.setState(state => {
|
||||
const { password } = state
|
||||
let confirmPasswordError = ''
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
confirmPasswordError = t('passwordsDontMatch')
|
||||
}
|
||||
|
||||
return {
|
||||
confirmPassword,
|
||||
confirmPasswordError,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
handleImport = async event => {
|
||||
event.preventDefault()
|
||||
|
||||
if (!this.isValid()) {
|
||||
return
|
||||
}
|
||||
|
||||
const { password, seedPhrase } = this.state
|
||||
const { history, onSubmit } = this.props
|
||||
|
||||
try {
|
||||
await onSubmit(password, seedPhrase)
|
||||
history.push(INITIALIZE_NOTICE_ROUTE)
|
||||
} catch (error) {
|
||||
this.setState({ seedPhraseError: error.message })
|
||||
}
|
||||
}
|
||||
|
||||
isValid () {
|
||||
const {
|
||||
seedPhrase,
|
||||
password,
|
||||
confirmPassword,
|
||||
passwordError,
|
||||
confirmPasswordError,
|
||||
seedPhraseError,
|
||||
} = this.state
|
||||
|
||||
if (!password || !confirmPassword || !seedPhrase || password !== confirmPassword) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (password.length < 8) {
|
||||
return false
|
||||
}
|
||||
|
||||
return !passwordError && !confirmPasswordError && !seedPhraseError
|
||||
}
|
||||
|
||||
render () {
|
||||
const { t } = this.context
|
||||
const { seedPhraseError, passwordError, confirmPasswordError } = this.state
|
||||
|
||||
return (
|
||||
<form
|
||||
className="first-time-flow__form"
|
||||
onSubmit={this.handleImport}
|
||||
>
|
||||
<div>
|
||||
<a
|
||||
onClick={e => {
|
||||
e.preventDefault()
|
||||
this.props.history.push(INITIALIZE_CREATE_PASSWORD_ROUTE)
|
||||
}}
|
||||
href="#"
|
||||
>
|
||||
{`< Back`}
|
||||
</a>
|
||||
</div>
|
||||
<div className="first-time-flow__header">
|
||||
{ t('importAccountSeedPhrase') }
|
||||
</div>
|
||||
<div className="first-time-flow__text-block">
|
||||
{ t('secretPhrase') }
|
||||
</div>
|
||||
<div className="first-time-flow__textarea-wrapper">
|
||||
<label>{ t('walletSeed') }</label>
|
||||
<textarea
|
||||
className="first-time-flow__textarea"
|
||||
onChange={e => this.handleSeedPhraseChange(e.target.value)}
|
||||
value={this.state.seedPhrase}
|
||||
placeholder={t('seedPhrasePlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
{
|
||||
seedPhraseError && (
|
||||
<span className="error">
|
||||
{ seedPhraseError }
|
||||
</span>
|
||||
)
|
||||
}
|
||||
<TextField
|
||||
id="password"
|
||||
label={t('newPassword')}
|
||||
type="password"
|
||||
className="first-time-flow__input"
|
||||
value={this.state.password}
|
||||
onChange={event => this.handlePasswordChange(event.target.value)}
|
||||
error={passwordError}
|
||||
autoComplete="new-password"
|
||||
margin="normal"
|
||||
largeLabel
|
||||
/>
|
||||
<TextField
|
||||
id="confirm-password"
|
||||
label={t('confirmPassword')}
|
||||
type="password"
|
||||
className="first-time-flow__input"
|
||||
value={this.state.confirmPassword}
|
||||
onChange={event => this.handleConfirmPasswordChange(event.target.value)}
|
||||
error={confirmPasswordError}
|
||||
autoComplete="confirm-password"
|
||||
margin="normal"
|
||||
largeLabel
|
||||
/>
|
||||
<Button
|
||||
type="first-time"
|
||||
className="first-time-flow__button"
|
||||
disabled={!this.isValid()}
|
||||
onClick={this.handleImport}
|
||||
>
|
||||
{ t('import') }
|
||||
</Button>
|
||||
<Breadcrumbs
|
||||
className="first-time-flow__breadcrumbs"
|
||||
total={2}
|
||||
currentIndex={0}
|
||||
/>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
export { default } from './import-with-seed-phrase.component'
|
@ -0,0 +1 @@
|
||||
export { default } from './create-password.container'
|
@ -0,0 +1 @@
|
||||
export { default } from './new-account.component'
|
@ -0,0 +1,178 @@
|
||||
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,
|
||||
} from '../../../../../routes'
|
||||
import TextField from '../../../../text-field'
|
||||
|
||||
export default class NewAccount extends PureComponent {
|
||||
static contextTypes = {
|
||||
t: PropTypes.func,
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
history: PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
state = {
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
passwordError: '',
|
||||
confirmPasswordError: '',
|
||||
}
|
||||
|
||||
isValid () {
|
||||
const {
|
||||
password,
|
||||
confirmPassword,
|
||||
passwordError,
|
||||
confirmPasswordError,
|
||||
} = this.state
|
||||
|
||||
if (!password || !confirmPassword || password !== confirmPassword) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (password.length < 8) {
|
||||
return false
|
||||
}
|
||||
|
||||
return !passwordError && !confirmPasswordError
|
||||
}
|
||||
|
||||
handlePasswordChange (password) {
|
||||
const { t } = this.context
|
||||
|
||||
this.setState(state => {
|
||||
const { confirmPassword } = state
|
||||
let passwordError = ''
|
||||
let confirmPasswordError = ''
|
||||
|
||||
if (password && password.length < 8) {
|
||||
passwordError = t('passwordNotLongEnough')
|
||||
}
|
||||
|
||||
if (confirmPassword && password !== confirmPassword) {
|
||||
confirmPasswordError = t('passwordsDontMatch')
|
||||
}
|
||||
|
||||
return {
|
||||
password,
|
||||
passwordError,
|
||||
confirmPasswordError,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
handleConfirmPasswordChange (confirmPassword) {
|
||||
const { t } = this.context
|
||||
|
||||
this.setState(state => {
|
||||
const { password } = state
|
||||
let confirmPasswordError = ''
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
confirmPasswordError = t('passwordsDontMatch')
|
||||
}
|
||||
|
||||
return {
|
||||
confirmPassword,
|
||||
confirmPasswordError,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
handleCreate = async event => {
|
||||
event.preventDefault()
|
||||
|
||||
if (!this.isValid()) {
|
||||
return
|
||||
}
|
||||
|
||||
const { password } = this.state
|
||||
const { onSubmit, history } = this.props
|
||||
|
||||
try {
|
||||
await onSubmit(password)
|
||||
history.push(INITIALIZE_UNIQUE_IMAGE_ROUTE)
|
||||
} catch (error) {
|
||||
this.setState({ passwordError: error.message })
|
||||
}
|
||||
}
|
||||
|
||||
handleImportWithSeedPhrase = event => {
|
||||
const { history } = this.props
|
||||
|
||||
event.preventDefault()
|
||||
history.push(INITIALIZE_IMPORT_WITH_SEED_PHRASE_ROUTE)
|
||||
}
|
||||
|
||||
render () {
|
||||
const { t } = this.context
|
||||
const { password, confirmPassword, passwordError, confirmPasswordError } = this.state
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="first-time-flow__header">
|
||||
{ t('createPassword') }
|
||||
</div>
|
||||
<form
|
||||
className="first-time-flow__form"
|
||||
onSubmit={this.handleCreate}
|
||||
>
|
||||
<TextField
|
||||
id="create-password"
|
||||
label={t('newPassword')}
|
||||
type="password"
|
||||
className="first-time-flow__input"
|
||||
value={password}
|
||||
onChange={event => this.handlePasswordChange(event.target.value)}
|
||||
error={passwordError}
|
||||
autoFocus
|
||||
autoComplete="new-password"
|
||||
margin="normal"
|
||||
fullWidth
|
||||
largeLabel
|
||||
/>
|
||||
<TextField
|
||||
id="confirm-password"
|
||||
label={t('confirmPassword')}
|
||||
type="password"
|
||||
className="first-time-flow__input"
|
||||
value={confirmPassword}
|
||||
onChange={event => this.handleConfirmPasswordChange(event.target.value)}
|
||||
error={confirmPasswordError}
|
||||
autoComplete="confirm-password"
|
||||
margin="normal"
|
||||
fullWidth
|
||||
largeLabel
|
||||
/>
|
||||
<Button
|
||||
type="first-time"
|
||||
className="first-time-flow__button"
|
||||
disabled={!this.isValid()}
|
||||
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>
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
export { default } from './unique-image.container'
|
@ -0,0 +1,53 @@
|
||||
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'
|
||||
|
||||
export default class UniqueImageScreen extends PureComponent {
|
||||
static contextTypes = {
|
||||
t: PropTypes.func,
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
address: PropTypes.string,
|
||||
history: PropTypes.object,
|
||||
}
|
||||
|
||||
render () {
|
||||
const { t } = this.context
|
||||
const { address, history } = this.props
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Identicon
|
||||
className="first-time-flow__unique-image"
|
||||
address={address}
|
||||
diameter={70}
|
||||
/>
|
||||
<div className="first-time-flow__header">
|
||||
{ t('yourUniqueAccountImage') }
|
||||
</div>
|
||||
<div className="first-time-flow__text-block">
|
||||
{ t('yourUniqueAccountImageDescription1') }
|
||||
</div>
|
||||
<div className="first-time-flow__text-block">
|
||||
{ t('yourUniqueAccountImageDescription2') }
|
||||
</div>
|
||||
<Button
|
||||
type="first-time"
|
||||
className="first-time-flow__button"
|
||||
onClick={() => history.push(INITIALIZE_NOTICE_ROUTE)}
|
||||
>
|
||||
{ t('next') }
|
||||
</Button>
|
||||
<Breadcrumbs
|
||||
className="first-time-flow__breadcrumbs"
|
||||
total={3}
|
||||
currentIndex={0}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
import { connect } from 'react-redux'
|
||||
import UniqueImage from './unique-image.component'
|
||||
|
||||
const mapStateToProps = ({ metamask }) => {
|
||||
const { selectedAddress } = metamask
|
||||
|
||||
return {
|
||||
address: selectedAddress,
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(UniqueImage)
|
@ -0,0 +1,57 @@
|
||||
import React, { PureComponent } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Redirect } from 'react-router-dom'
|
||||
import {
|
||||
DEFAULT_ROUTE,
|
||||
LOCK_ROUTE,
|
||||
INITIALIZE_WELCOME_ROUTE,
|
||||
INITIALIZE_NOTICE_ROUTE,
|
||||
INITIALIZE_UNLOCK_ROUTE,
|
||||
INITIALIZE_SEED_PHRASE_ROUTE,
|
||||
} from '../../../../routes'
|
||||
|
||||
export default class FirstTimeFlowSwitch extends PureComponent {
|
||||
static propTypes = {
|
||||
completedOnboarding: PropTypes.bool,
|
||||
isInitialized: PropTypes.bool,
|
||||
isUnlocked: PropTypes.bool,
|
||||
noActiveNotices: PropTypes.bool,
|
||||
seedPhrase: PropTypes.string,
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
completedOnboarding,
|
||||
isInitialized,
|
||||
isUnlocked,
|
||||
noActiveNotices,
|
||||
seedPhrase,
|
||||
} = this.props
|
||||
|
||||
if (completedOnboarding) {
|
||||
return <Redirect to={{ pathname: DEFAULT_ROUTE }} />
|
||||
}
|
||||
|
||||
if (isUnlocked && !seedPhrase) {
|
||||
return <Redirect to={{ pathname: LOCK_ROUTE }} />
|
||||
}
|
||||
|
||||
if (!isInitialized) {
|
||||
return <Redirect to={{ pathname: INITIALIZE_WELCOME_ROUTE }} />
|
||||
}
|
||||
|
||||
if (!isUnlocked) {
|
||||
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 }} />
|
||||
}
|
||||
|
||||
return <Redirect to={{ pathname: INITIALIZE_WELCOME_ROUTE }} />
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
import { connect } from 'react-redux'
|
||||
import FirstTimeFlowSwitch from './first-time-flow-switch.component'
|
||||
|
||||
const mapStateToProps = ({ metamask }) => {
|
||||
const {
|
||||
completedOnboarding,
|
||||
isInitialized,
|
||||
isUnlocked,
|
||||
noActiveNotices,
|
||||
} = metamask
|
||||
|
||||
return {
|
||||
completedOnboarding,
|
||||
isInitialized,
|
||||
isUnlocked,
|
||||
noActiveNotices,
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(FirstTimeFlowSwitch)
|
@ -0,0 +1 @@
|
||||
export { default } from './first-time-flow-switch.container'
|
@ -0,0 +1,145 @@
|
||||
import React, { PureComponent } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Switch, Route } from 'react-router-dom'
|
||||
import FirstTimeFlowSwitch from './first-time-flow-switch'
|
||||
import Welcome from './welcome'
|
||||
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,
|
||||
} from '../../../routes'
|
||||
|
||||
export default class FirstTimeFlow extends PureComponent {
|
||||
static propTypes = {
|
||||
completedOnboarding: PropTypes.bool,
|
||||
createNewAccount: PropTypes.func,
|
||||
createNewAccountFromSeed: PropTypes.func,
|
||||
history: PropTypes.object,
|
||||
isInitialized: PropTypes.bool,
|
||||
isUnlocked: PropTypes.bool,
|
||||
noActiveNotices: PropTypes.bool,
|
||||
unlockAccount: PropTypes.func,
|
||||
}
|
||||
|
||||
state = {
|
||||
seedPhrase: '',
|
||||
isImportedKeyring: false,
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
const { completedOnboarding, history, isInitialized, isUnlocked } = this.props
|
||||
|
||||
if (completedOnboarding) {
|
||||
history.push(DEFAULT_ROUTE)
|
||||
return
|
||||
}
|
||||
|
||||
if (isInitialized && !isUnlocked) {
|
||||
history.push(INITIALIZE_UNLOCK_ROUTE)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
handleCreateNewAccount = async password => {
|
||||
const { createNewAccount } = this.props
|
||||
|
||||
try {
|
||||
const seedPhrase = await createNewAccount(password)
|
||||
this.setState({ seedPhrase })
|
||||
} catch (error) {
|
||||
throw new Error(error.message)
|
||||
}
|
||||
}
|
||||
|
||||
handleImportWithSeedPhrase = async (password, seedPhrase) => {
|
||||
const { createNewAccountFromSeed } = this.props
|
||||
|
||||
try {
|
||||
await createNewAccountFromSeed(password, seedPhrase)
|
||||
this.setState({ isImportedKeyring: true })
|
||||
} catch (error) {
|
||||
throw new Error(error.message)
|
||||
}
|
||||
}
|
||||
|
||||
handleUnlock = async password => {
|
||||
const { unlockAccount, history, noActiveNotices } = this.props
|
||||
|
||||
try {
|
||||
const seedPhrase = await unlockAccount(password)
|
||||
this.setState({ seedPhrase }, () => {
|
||||
noActiveNotices
|
||||
? history.push(INITIALIZE_SEED_PHRASE_ROUTE)
|
||||
: history.push(INITIALIZE_NOTICE_ROUTE)
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(error.message)
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { seedPhrase, isImportedKeyring } = this.state
|
||||
|
||||
return (
|
||||
<div className="first-time-flow">
|
||||
<Switch>
|
||||
<Route
|
||||
path={INITIALIZE_SEED_PHRASE_ROUTE}
|
||||
render={props => (
|
||||
<SeedPhrase
|
||||
{ ...props }
|
||||
seedPhrase={seedPhrase}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={INITIALIZE_NOTICE_ROUTE}
|
||||
render={props => (
|
||||
<Notices
|
||||
{ ...props }
|
||||
isImportedKeyring={isImportedKeyring}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
path={INITIALIZE_CREATE_PASSWORD_ROUTE}
|
||||
render={props => (
|
||||
<CreatePassword
|
||||
{ ...props }
|
||||
onCreateNewAccount={this.handleCreateNewAccount}
|
||||
onCreateNewAccountFromSeed={this.handleImportWithSeedPhrase}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
path={INITIALIZE_UNLOCK_ROUTE}
|
||||
render={props => (
|
||||
<Unlock
|
||||
{ ...props }
|
||||
onSubmit={this.handleUnlock}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={INITIALIZE_WELCOME_ROUTE}
|
||||
component={Welcome}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="*"
|
||||
component={FirstTimeFlowSwitch}
|
||||
/>
|
||||
</Switch>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
import { connect } from 'react-redux'
|
||||
import FirstTimeFlow from './first-time-flow.component'
|
||||
import {
|
||||
createNewVaultAndGetSeedPhrase,
|
||||
createNewVaultAndRestore,
|
||||
unlockAndGetSeedPhrase,
|
||||
} from '../../../actions'
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const { metamask: { completedOnboarding, isInitialized, isUnlocked, noActiveNotices } } = state
|
||||
|
||||
return {
|
||||
completedOnboarding,
|
||||
isInitialized,
|
||||
isUnlocked,
|
||||
noActiveNotices,
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
createNewAccount: password => dispatch(createNewVaultAndGetSeedPhrase(password)),
|
||||
createNewAccountFromSeed: (password, seedPhrase) => {
|
||||
return dispatch(createNewVaultAndRestore(password, seedPhrase))
|
||||
},
|
||||
unlockAccount: password => dispatch(unlockAndGetSeedPhrase(password)),
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(FirstTimeFlow)
|
1
ui/app/components/pages/first-time-flow/index.js
Normal file
1
ui/app/components/pages/first-time-flow/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './first-time-flow.container'
|
99
ui/app/components/pages/first-time-flow/index.scss
Normal file
99
ui/app/components/pages/first-time-flow/index.scss
Normal file
@ -0,0 +1,99 @@
|
||||
@import './welcome/index';
|
||||
|
||||
@import './seed-phrase/index';
|
||||
|
||||
.first-time-flow {
|
||||
width: 100%;
|
||||
background-color: $white;
|
||||
|
||||
&__wrapper {
|
||||
@media screen and (min-width: $break-large) {
|
||||
padding: 60px 275px 0 275px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1100px) {
|
||||
padding: 36px;
|
||||
}
|
||||
}
|
||||
|
||||
&__form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&__header {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
&__subheader {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
&__input {
|
||||
max-width: 350px;
|
||||
}
|
||||
|
||||
&__textarea-wrapper {
|
||||
margin-bottom: 8px;
|
||||
display: inline-flex;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
flex-direction: column;
|
||||
max-width: 350px;
|
||||
}
|
||||
|
||||
&__textarea-label {
|
||||
margin-bottom: 9px;
|
||||
color: #1B344D;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
&__textarea {
|
||||
font-size: 1rem;
|
||||
font-family: Roboto;
|
||||
height: 190px;
|
||||
border: 1px solid #CDCDCD;
|
||||
border-radius: 6px;
|
||||
background-color: #FFFFFF;
|
||||
padding: 16px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
&__breadcrumbs {
|
||||
margin: 36px 0;
|
||||
}
|
||||
|
||||
&__unique-image {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
&__markdown {
|
||||
border: 1px solid #979797;
|
||||
border-radius: 8px;
|
||||
background-color: $white;
|
||||
height: 200px;
|
||||
overflow-y: auto;
|
||||
color: #757575;
|
||||
font-size: .75rem;
|
||||
line-height: 15px;
|
||||
text-align: justify;
|
||||
margin: 0;
|
||||
padding: 16px 20px;
|
||||
height: 30vh;
|
||||
}
|
||||
|
||||
&__text-block {
|
||||
margin-bottom: 24px;
|
||||
|
||||
@media screen and (max-width: $break-small) {
|
||||
margin-bottom: 16px;
|
||||
font-size: .875rem;
|
||||
}
|
||||
}
|
||||
|
||||
&__button {
|
||||
margin: 35px 0 14px;
|
||||
}
|
||||
}
|
1
ui/app/components/pages/first-time-flow/notices/index.js
Normal file
1
ui/app/components/pages/first-time-flow/notices/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './notices.container'
|
@ -0,0 +1,124 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
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)
|
@ -0,0 +1,161 @@
|
||||
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 { exportAsFile } from '../../../../../../app/util'
|
||||
import { selectSeedWord, deselectSeedWord } from './confirm-seed-phrase.state'
|
||||
|
||||
export default class ConfirmSeedPhrase extends PureComponent {
|
||||
static contextTypes = {
|
||||
t: PropTypes.func,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
seedPhrase: '',
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
address: PropTypes.string,
|
||||
completeOnboarding: PropTypes.func,
|
||||
history: PropTypes.object,
|
||||
onSubmit: PropTypes.func,
|
||||
openBuyEtherModal: PropTypes.func,
|
||||
seedPhrase: PropTypes.string,
|
||||
}
|
||||
|
||||
state = {
|
||||
selectedSeedWords: [],
|
||||
shuffledSeedWords: [],
|
||||
// Hash of shuffledSeedWords index {Number} to selectedSeedWords index {Number}
|
||||
selectedSeedWordsHash: {},
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
const { seedPhrase = '' } = this.props
|
||||
const shuffledSeedWords = shuffle(seedPhrase.split(' ')) || []
|
||||
this.setState({ shuffledSeedWords })
|
||||
}
|
||||
|
||||
handleExport = () => {
|
||||
exportAsFile('MetaMask Secret Backup Phrase', this.props.seedPhrase, 'text/plain')
|
||||
}
|
||||
|
||||
handleSubmit = async () => {
|
||||
const { completeOnboarding, history, openBuyEtherModal } = this.props
|
||||
|
||||
if (!this.isValid()) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await completeOnboarding()
|
||||
history.push(DEFAULT_ROUTE)
|
||||
openBuyEtherModal()
|
||||
} catch (error) {
|
||||
console.error(error.message)
|
||||
}
|
||||
}
|
||||
|
||||
handleSelectSeedWord = (word, shuffledIndex) => {
|
||||
this.setState(selectSeedWord(word, shuffledIndex))
|
||||
}
|
||||
|
||||
handleDeselectSeedWord = shuffledIndex => {
|
||||
this.setState(deselectSeedWord(shuffledIndex))
|
||||
}
|
||||
|
||||
isValid () {
|
||||
const { seedPhrase } = this.props
|
||||
const { selectedSeedWords } = this.state
|
||||
return seedPhrase === selectedSeedWords.join(' ')
|
||||
}
|
||||
|
||||
render () {
|
||||
const { t } = this.context
|
||||
const { address, history } = this.props
|
||||
const { selectedSeedWords, shuffledSeedWords, selectedSeedWordsHash } = this.state
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="confirm-seed-phrase__back-button">
|
||||
<a
|
||||
onClick={e => {
|
||||
e.preventDefault()
|
||||
history.push(INITIALIZE_SEED_PHRASE_ROUTE)
|
||||
}}
|
||||
href="#"
|
||||
>
|
||||
{`< Back`}
|
||||
</a>
|
||||
</div>
|
||||
<Identicon
|
||||
className="first-time-flow__unique-image"
|
||||
address={address}
|
||||
diameter={70}
|
||||
/>
|
||||
<div className="first-time-flow__header">
|
||||
{ t('confirmSecretBackupPhrase') }
|
||||
</div>
|
||||
<div className="first-time-flow__text-block">
|
||||
{ t('selectEachPhrase') }
|
||||
</div>
|
||||
<div className="confirm-seed-phrase__selected-seed-words">
|
||||
{
|
||||
selectedSeedWords.map((word, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="confirm-seed-phrase__seed-word"
|
||||
>
|
||||
{ word }
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
<div className="confirm-seed-phrase__shuffled-seed-words">
|
||||
{
|
||||
shuffledSeedWords.map((word, index) => {
|
||||
const isSelected = index in selectedSeedWordsHash
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className={classnames(
|
||||
'confirm-seed-phrase__seed-word',
|
||||
'confirm-seed-phrase__seed-word--shuffled',
|
||||
{ 'confirm-seed-phrase__seed-word--selected': isSelected }
|
||||
)}
|
||||
onClick={() => {
|
||||
if (!isSelected) {
|
||||
this.handleSelectSeedWord(word, index)
|
||||
} else {
|
||||
this.handleDeselectSeedWord(index)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{ word }
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
</div>
|
||||
<Button
|
||||
type="first-time"
|
||||
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>
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
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)
|
@ -0,0 +1,41 @@
|
||||
export function selectSeedWord (word, shuffledIndex) {
|
||||
return function update (state) {
|
||||
const { selectedSeedWords, selectedSeedWordsHash } = state
|
||||
const nextSelectedIndex = selectedSeedWords.length
|
||||
|
||||
return {
|
||||
selectedSeedWords: [ ...selectedSeedWords, word ],
|
||||
selectedSeedWordsHash: { ...selectedSeedWordsHash, [shuffledIndex]: nextSelectedIndex },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function deselectSeedWord (shuffledIndex) {
|
||||
return function update (state) {
|
||||
const {
|
||||
selectedSeedWords: prevSelectedSeedWords,
|
||||
selectedSeedWordsHash: prevSelectedSeedWordsHash,
|
||||
} = state
|
||||
|
||||
const selectedSeedWords = [...prevSelectedSeedWords]
|
||||
const indexToRemove = prevSelectedSeedWordsHash[shuffledIndex]
|
||||
selectedSeedWords.splice(indexToRemove, 1)
|
||||
const selectedSeedWordsHash = Object.keys(prevSelectedSeedWordsHash).reduce((acc, index) => {
|
||||
const output = { ...acc }
|
||||
const selectedSeedWordIndex = prevSelectedSeedWordsHash[index]
|
||||
|
||||
if (selectedSeedWordIndex < indexToRemove) {
|
||||
output[index] = selectedSeedWordIndex
|
||||
} else if (selectedSeedWordIndex > indexToRemove) {
|
||||
output[index] = selectedSeedWordIndex - 1
|
||||
}
|
||||
|
||||
return output
|
||||
}, {})
|
||||
|
||||
return {
|
||||
selectedSeedWords,
|
||||
selectedSeedWordsHash,
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
export { default } from './confirm-seed-phrase.container'
|
@ -0,0 +1,44 @@
|
||||
.confirm-seed-phrase {
|
||||
&__back-button {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
&__selected-seed-words {
|
||||
min-height: 190px;
|
||||
max-width: 496px;
|
||||
border: 1px solid #CDCDCD;
|
||||
border-radius: 6px;
|
||||
background-color: $white;
|
||||
margin: 24px 0 36px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
&__shuffled-seed-words {
|
||||
max-width: 496px;
|
||||
}
|
||||
|
||||
&__seed-word {
|
||||
display: inline-block;
|
||||
color: #5B5D67;
|
||||
background-color: #E7E7E7;
|
||||
padding: 8px 18px;
|
||||
min-width: 64px;
|
||||
margin: 4px;
|
||||
text-align: center;
|
||||
|
||||
&--selected {
|
||||
background-color: #85D1CC;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
&--shuffled {
|
||||
cursor: pointer;
|
||||
margin: 6px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 575px) {
|
||||
font-size: .875rem;
|
||||
padding: 6px 18px;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
export { default } from './seed-phrase.container'
|
@ -0,0 +1,36 @@
|
||||
@import './confirm-seed-phrase/index';
|
||||
|
||||
@import './reveal-seed-phrase/index';
|
||||
|
||||
.seed-phrase {
|
||||
|
||||
&__sections {
|
||||
display: flex;
|
||||
|
||||
@media screen and (min-width: $break-large) {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
@media screen and (max-width: $break-small) {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
&__main {
|
||||
flex: 3;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__side {
|
||||
flex: 2;
|
||||
min-width: 0;
|
||||
|
||||
@media screen and (min-width: $break-large) {
|
||||
margin-left: 48px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: $break-small) {
|
||||
margin-top: 24px;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
export { default } from './reveal-seed-phrase.component'
|
@ -0,0 +1,53 @@
|
||||
.reveal-seed-phrase {
|
||||
&__secret {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
border: 1px solid #CDCDCD;
|
||||
border-radius: 6px;
|
||||
background-color: $white;
|
||||
padding: 18px;
|
||||
margin-top: 36px;
|
||||
max-width: 350px;
|
||||
}
|
||||
|
||||
&__secret-words {
|
||||
width: 310px;
|
||||
font-size: 1.25rem;
|
||||
text-align: center;
|
||||
|
||||
&--hidden {
|
||||
filter: blur(5px);
|
||||
}
|
||||
}
|
||||
|
||||
&__secret-blocker {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-color: rgba(0,0,0,0.6);
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 8px 0 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&__reveal-button {
|
||||
color: $white;
|
||||
font-size: .75rem;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
margin-top: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&__export-text {
|
||||
color: $curious-blue;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
@ -0,0 +1,139 @@
|
||||
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'
|
||||
|
||||
export default class RevealSeedPhrase extends PureComponent {
|
||||
static contextTypes = {
|
||||
t: PropTypes.func,
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
address: PropTypes.string,
|
||||
history: PropTypes.object,
|
||||
seedPhrase: PropTypes.string,
|
||||
}
|
||||
|
||||
state = {
|
||||
isShowingSeedPhrase: false,
|
||||
}
|
||||
|
||||
handleExport = () => {
|
||||
exportAsFile('MetaMask Secret Backup Phrase', this.props.seedPhrase, 'text/plain')
|
||||
}
|
||||
|
||||
handleNext = event => {
|
||||
event.preventDefault()
|
||||
const { isShowingSeedPhrase } = this.state
|
||||
const { history } = this.props
|
||||
|
||||
if (!isShowingSeedPhrase) {
|
||||
return
|
||||
}
|
||||
|
||||
history.push(INITIALIZE_CONFIRM_SEED_PHRASE_ROUTE)
|
||||
}
|
||||
|
||||
renderSecretWordsContainer () {
|
||||
const { t } = this.context
|
||||
const { seedPhrase } = this.props
|
||||
const { isShowingSeedPhrase } = this.state
|
||||
|
||||
return (
|
||||
<div className="reveal-seed-phrase__secret">
|
||||
<div className={classnames(
|
||||
'reveal-seed-phrase__secret-words',
|
||||
{ 'reveal-seed-phrase__secret-words--hidden': !isShowingSeedPhrase }
|
||||
)}>
|
||||
{ seedPhrase }
|
||||
</div>
|
||||
{
|
||||
!isShowingSeedPhrase && (
|
||||
<div
|
||||
className="reveal-seed-phrase__secret-blocker"
|
||||
onClick={() => this.setState({ isShowingSeedPhrase: true })}
|
||||
>
|
||||
<LockIcon
|
||||
width="28px"
|
||||
height="35px"
|
||||
fill="#FFFFFF"
|
||||
/>
|
||||
<div className="reveal-seed-phrase__reveal-button">
|
||||
{ t('clickToRevealSeed') }
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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="seed-phrase__sections">
|
||||
<div className="seed-phrase__main">
|
||||
<div className="first-time-flow__header">
|
||||
{ t('secretBackupPhrase') }
|
||||
</div>
|
||||
<div className="first-time-flow__text-block">
|
||||
{ t('secretBackupPhraseDescription') }
|
||||
</div>
|
||||
<div className="first-time-flow__text-block">
|
||||
{ t('secretBackupPhraseWarning') }
|
||||
</div>
|
||||
{ this.renderSecretWordsContainer() }
|
||||
</div>
|
||||
<div className="seed-phrase__side">
|
||||
<div className="first-time-flow__text-block">
|
||||
{ `${t('tips')}:` }
|
||||
</div>
|
||||
<div className="first-time-flow__text-block">
|
||||
{ t('storePhrase') }
|
||||
</div>
|
||||
<div className="first-time-flow__text-block">
|
||||
{ t('writePhrase') }
|
||||
</div>
|
||||
<div className="first-time-flow__text-block">
|
||||
{ t('memorizePhrase') }
|
||||
</div>
|
||||
<div className="first-time-flow__text-block">
|
||||
<a
|
||||
className="reveal-seed-phrase__export-text"
|
||||
onClick={this.handleExport}>
|
||||
{ t('downloadSecretBackup') }
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
type="first-time"
|
||||
className="first-time-flow__button"
|
||||
onClick={this.handleNext}
|
||||
disabled={!isShowingSeedPhrase}
|
||||
>
|
||||
{ t('next') }
|
||||
</Button>
|
||||
<Breadcrumbs
|
||||
className="first-time-flow__breadcrumbs"
|
||||
total={3}
|
||||
currentIndex={2}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
import React, { PureComponent } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Switch, Route } from 'react-router-dom'
|
||||
import RevealSeedPhrase from './reveal-seed-phrase'
|
||||
import ConfirmSeedPhrase from './confirm-seed-phrase'
|
||||
import {
|
||||
INITIALIZE_SEED_PHRASE_ROUTE,
|
||||
INITIALIZE_CONFIRM_SEED_PHRASE_ROUTE,
|
||||
DEFAULT_ROUTE,
|
||||
} from '../../../../routes'
|
||||
|
||||
export default class SeedPhrase extends PureComponent {
|
||||
static propTypes = {
|
||||
address: PropTypes.string,
|
||||
history: PropTypes.object,
|
||||
seedPhrase: PropTypes.string,
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
const { seedPhrase, history } = this.props
|
||||
|
||||
if (!seedPhrase) {
|
||||
history.push(DEFAULT_ROUTE)
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { address, seedPhrase } = this.props
|
||||
|
||||
return (
|
||||
<div className="first-time-flow__wrapper">
|
||||
<Switch>
|
||||
<Route
|
||||
exact
|
||||
path={INITIALIZE_CONFIRM_SEED_PHRASE_ROUTE}
|
||||
render={props => (
|
||||
<ConfirmSeedPhrase
|
||||
{ ...props }
|
||||
address={address}
|
||||
seedPhrase={seedPhrase}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={INITIALIZE_SEED_PHRASE_ROUTE}
|
||||
render={props => (
|
||||
<RevealSeedPhrase
|
||||
{ ...props }
|
||||
address={address}
|
||||
seedPhrase={seedPhrase}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Switch>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
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)
|
1
ui/app/components/pages/first-time-flow/welcome/index.js
Normal file
1
ui/app/components/pages/first-time-flow/welcome/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './welcome.container'
|
43
ui/app/components/pages/first-time-flow/welcome/index.scss
Normal file
43
ui/app/components/pages/first-time-flow/welcome/index.scss
Normal file
@ -0,0 +1,43 @@
|
||||
.welcome-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 400px;
|
||||
padding: 0 18px;
|
||||
|
||||
&__wrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&__header {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
&__description {
|
||||
text-align: center;
|
||||
|
||||
@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;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
margin: 35px 0 14px;
|
||||
transition: 200ms ease-in-out;
|
||||
background-color: rgba(247, 134, 28, .9);
|
||||
}
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
import EventEmitter from 'events'
|
||||
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'
|
||||
|
||||
export default class Welcome extends PureComponent {
|
||||
static propTypes = {
|
||||
history: PropTypes.object,
|
||||
isInitialized: PropTypes.bool,
|
||||
}
|
||||
|
||||
static contextTypes = {
|
||||
t: PropTypes.func,
|
||||
}
|
||||
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.animationEventEmitter = new EventEmitter()
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
const { history, isInitialized } = this.props
|
||||
|
||||
if (isInitialized) {
|
||||
history.push(INITIALIZE_NOTICE_ROUTE)
|
||||
}
|
||||
}
|
||||
|
||||
handleContinue = () => {
|
||||
this.props.history.push(INITIALIZE_CREATE_PASSWORD_ROUTE)
|
||||
}
|
||||
|
||||
render () {
|
||||
const { t } = this.context
|
||||
|
||||
return (
|
||||
<div className="welcome-page__wrapper">
|
||||
<div className="welcome-page">
|
||||
<Mascot
|
||||
animationEventEmitter={this.animationEventEmitter}
|
||||
width="225"
|
||||
height="225"
|
||||
/>
|
||||
<div className="welcome-page__header">
|
||||
{ t('welcome') }
|
||||
</div>
|
||||
<div className="welcome-page__description">
|
||||
<div>{ t('metamaskDescription') }</div>
|
||||
<div>{ t('holdEther') }</div>
|
||||
</div>
|
||||
<Button
|
||||
type="first-time"
|
||||
className="first-time-flow__button"
|
||||
onClick={this.handleContinue}
|
||||
>
|
||||
{ t('continue') }
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
import { connect } from 'react-redux'
|
||||
import { withRouter } from 'react-router-dom'
|
||||
import { compose } from 'recompose'
|
||||
import { closeWelcomeScreen } from '../../../../actions'
|
||||
import Welcome from './welcome.component'
|
||||
|
||||
const mapStateToProps = ({ metamask }) => {
|
||||
const { welcomeScreenSeen, isInitialized } = metamask
|
||||
|
||||
return {
|
||||
welcomeScreenSeen,
|
||||
isInitialized,
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
closeWelcomeScreen: () => dispatch(closeWelcomeScreen()),
|
||||
}
|
||||
}
|
||||
|
||||
export default compose(
|
||||
withRouter,
|
||||
connect(mapStateToProps, mapDispatchToProps)
|
||||
)(Welcome)
|
@ -7,7 +7,7 @@ import TransactionView from '../../transaction-view'
|
||||
import ProviderApproval from '../provider-approval'
|
||||
|
||||
import {
|
||||
INITIALIZE_BACKUP_PHRASE_ROUTE,
|
||||
INITIALIZE_SEED_PHRASE_ROUTE,
|
||||
RESTORE_VAULT_ROUTE,
|
||||
CONFIRM_TRANSACTION_ROUTE,
|
||||
NOTICE_ROUTE,
|
||||
@ -59,7 +59,7 @@ export default class Home extends PureComponent {
|
||||
|
||||
// seed words
|
||||
if (seedWords) {
|
||||
return <Redirect to={{ pathname: INITIALIZE_BACKUP_PHRASE_ROUTE }}/>
|
||||
return <Redirect to={{ pathname: INITIALIZE_SEED_PHRASE_ROUTE }}/>
|
||||
}
|
||||
|
||||
if (forgottenPassword) {
|
||||
|
@ -5,3 +5,7 @@
|
||||
@import './confirm-add-token/index';
|
||||
|
||||
@import './settings/index';
|
||||
|
||||
@import './first-time-flow/index';
|
||||
|
||||
@import './keychains/index';
|
||||
|
@ -1,25 +0,0 @@
|
||||
const { connect } = require('react-redux')
|
||||
const PropTypes = require('prop-types')
|
||||
const { Redirect } = require('react-router-dom')
|
||||
const h = require('react-hyperscript')
|
||||
const { INITIALIZE_ROUTE } = require('../../routes')
|
||||
const MetamaskRoute = require('./metamask-route')
|
||||
|
||||
const Initialized = props => {
|
||||
return props.isInitialized
|
||||
? h(MetamaskRoute, { ...props })
|
||||
: h(Redirect, { to: { pathname: INITIALIZE_ROUTE } })
|
||||
}
|
||||
|
||||
Initialized.propTypes = {
|
||||
isInitialized: PropTypes.bool,
|
||||
}
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const { metamask: { isInitialized } } = state
|
||||
return {
|
||||
isInitialized,
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = connect(mapStateToProps)(Initialized)
|
197
ui/app/components/pages/keychains/index.scss
Normal file
197
ui/app/components/pages/keychains/index.scss
Normal file
@ -0,0 +1,197 @@
|
||||
.first-view-main-wrapper {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.first-view-main {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1281px) {
|
||||
.first-view-main {
|
||||
width: 62vw;
|
||||
}
|
||||
}
|
||||
|
||||
.import-account {
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
margin: 60px 0 30px 0;
|
||||
position: relative;
|
||||
max-width: initial;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 575px) {
|
||||
.import-account{
|
||||
margin: 24px;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
width: calc(100vw - 80px);
|
||||
}
|
||||
|
||||
.import-account__title {
|
||||
width: initial !important;
|
||||
}
|
||||
|
||||
.first-view-main {
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.first-view-phone-invisible {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.first-time-flow__input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.import-account__secret-phrase {
|
||||
width: initial !important;
|
||||
height: initial !important;
|
||||
min-height: 190px;
|
||||
}
|
||||
}
|
||||
|
||||
.import-account__title {
|
||||
color: #1B344D;
|
||||
font-size: 40px;
|
||||
line-height: 51px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.import-account__back-button {
|
||||
margin-bottom: 18px;
|
||||
color: #22232c;
|
||||
font-size: 16px;
|
||||
line-height: 21px;
|
||||
position: absolute;
|
||||
top: -25px;
|
||||
}
|
||||
|
||||
.import-account__secret-phrase {
|
||||
height: 190px;
|
||||
width: 495px;
|
||||
border: 1px solid #CDCDCD;
|
||||
border-radius: 6px;
|
||||
background-color: #FFFFFF;
|
||||
padding: 17px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.import-account__secret-phrase::placeholder {
|
||||
color: #9B9B9B;
|
||||
font-weight: 200;
|
||||
}
|
||||
|
||||
.import-account__faq-link {
|
||||
font-size: 18px;
|
||||
line-height: 23px;
|
||||
font-family: Roboto;
|
||||
}
|
||||
|
||||
.import-account__selector-label {
|
||||
color: #1B344D;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.import-account__dropdown {
|
||||
width: 325px;
|
||||
border: 1px solid #CDCDCD;
|
||||
border-radius: 4px;
|
||||
background-color: #FFFFFF;
|
||||
margin-top: 14px;
|
||||
color: #5B5D67;
|
||||
font-family: Roboto;
|
||||
font-size: 18px;
|
||||
line-height: 23px;
|
||||
padding: 14px 21px;
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.import-account__description-text {
|
||||
color: #757575;
|
||||
font-size: 18px;
|
||||
line-height: 23px;
|
||||
margin-top: 21px;
|
||||
font-family: Roboto;
|
||||
}
|
||||
|
||||
.import-account__input-wrapper {
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.import-account__input-error-message {
|
||||
margin-top: 10px;
|
||||
width: 422px;
|
||||
color: #FF001F;
|
||||
font-size: 16px;
|
||||
line-height: 21px;
|
||||
}
|
||||
|
||||
.import-account__input-label {
|
||||
margin-bottom: 9px;
|
||||
color: #1B344D;
|
||||
font-size: 18px;
|
||||
line-height: 23px;
|
||||
}
|
||||
|
||||
.import-account__input-label__disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.import-account__input {
|
||||
width: 350px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 575px) {
|
||||
.import-account__input {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.import-account__file-input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.import-account__file-input-label {
|
||||
height: 53px;
|
||||
width: 148px;
|
||||
border: 1px solid #1B344D;
|
||||
border-radius: 4px;
|
||||
color: #1B344D;
|
||||
font-family: Roboto;
|
||||
font-size: 18px;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.import-account__file-picker-wrapper {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.import-account__file-name {
|
||||
color: #000000;
|
||||
font-family: Roboto;
|
||||
font-size: 18px;
|
||||
line-height: 23px;
|
||||
margin-left: 22px;
|
||||
}
|
@ -7,6 +7,7 @@ import {
|
||||
} from '../../../actions'
|
||||
import { DEFAULT_ROUTE } from '../../../routes'
|
||||
import TextField from '../../text-field'
|
||||
import Button from '../../button'
|
||||
|
||||
class RestoreVaultPage extends Component {
|
||||
static contextTypes = {
|
||||
@ -160,13 +161,14 @@ class RestoreVaultPage extends Component {
|
||||
margin="normal"
|
||||
largeLabel
|
||||
/>
|
||||
<button
|
||||
<Button
|
||||
type="first-time"
|
||||
className="first-time-flow__button"
|
||||
onClick={() => !disabled && this.onClick()}
|
||||
disabled={disabled}
|
||||
>
|
||||
{this.context.t('restore')}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
1
ui/app/components/pages/lock/index.js
Normal file
1
ui/app/components/pages/lock/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './lock.container'
|
26
ui/app/components/pages/lock/lock.component.js
Normal file
26
ui/app/components/pages/lock/lock.component.js
Normal file
@ -0,0 +1,26 @@
|
||||
import React, { PureComponent } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import Loading from '../../loading-screen'
|
||||
import { DEFAULT_ROUTE } from '../../../routes'
|
||||
|
||||
export default class Lock extends PureComponent {
|
||||
static propTypes = {
|
||||
history: PropTypes.object,
|
||||
isUnlocked: PropTypes.bool,
|
||||
lockMetamask: PropTypes.func,
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
const { lockMetamask, isUnlocked, history } = this.props
|
||||
|
||||
if (isUnlocked) {
|
||||
lockMetamask().then(() => history.push(DEFAULT_ROUTE))
|
||||
} else {
|
||||
history.replace(DEFAULT_ROUTE)
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
return <Loading />
|
||||
}
|
||||
}
|
24
ui/app/components/pages/lock/lock.container.js
Normal file
24
ui/app/components/pages/lock/lock.container.js
Normal file
@ -0,0 +1,24 @@
|
||||
import Lock from './lock.component'
|
||||
import { compose } from 'recompose'
|
||||
import { connect } from 'react-redux'
|
||||
import { withRouter } from 'react-router-dom'
|
||||
import { lockMetamask } from '../../../actions'
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const { metamask: { isUnlocked } } = state
|
||||
|
||||
return {
|
||||
isUnlocked,
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
lockMetamask: () => dispatch(lockMetamask()),
|
||||
}
|
||||
}
|
||||
|
||||
export default compose(
|
||||
withRouter,
|
||||
connect(mapStateToProps, mapDispatchToProps)
|
||||
)(Lock)
|
@ -1,28 +0,0 @@
|
||||
const { connect } = require('react-redux')
|
||||
const PropTypes = require('prop-types')
|
||||
const { Route } = require('react-router-dom')
|
||||
const h = require('react-hyperscript')
|
||||
|
||||
const MetamaskRoute = ({ component, mascaraComponent, isMascara, ...props }) => {
|
||||
return (
|
||||
h(Route, {
|
||||
...props,
|
||||
component: isMascara && mascaraComponent ? mascaraComponent : component,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
MetamaskRoute.propTypes = {
|
||||
component: PropTypes.func,
|
||||
mascaraComponent: PropTypes.func,
|
||||
isMascara: PropTypes.bool,
|
||||
}
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const { metamask: { isMascara } } = state
|
||||
return {
|
||||
isMascara,
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = connect(mapStateToProps)(MetamaskRoute)
|
@ -14,7 +14,6 @@
|
||||
align-self: stretch;
|
||||
justify-content: center;
|
||||
flex: 1 0 auto;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
&__mascot-container {
|
||||
|
@ -2,12 +2,10 @@ import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import Button from '@material-ui/core/Button'
|
||||
import TextField from '../../text-field'
|
||||
import { ENVIRONMENT_TYPE_POPUP } from '../../../../../app/scripts/lib/enums'
|
||||
import { getEnvironmentType } from '../../../../../app/scripts/lib/util'
|
||||
import getCaretCoordinates from 'textarea-caret'
|
||||
import { EventEmitter } from 'events'
|
||||
import Mascot from '../../mascot'
|
||||
import { DEFAULT_ROUTE, RESTORE_VAULT_ROUTE } from '../../../routes'
|
||||
import { DEFAULT_ROUTE } from '../../../routes'
|
||||
|
||||
export default class UnlockPage extends Component {
|
||||
static contextTypes = {
|
||||
@ -15,12 +13,11 @@ export default class UnlockPage extends Component {
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
forgotPassword: PropTypes.func,
|
||||
tryUnlockMetamask: PropTypes.func,
|
||||
markPasswordForgotten: PropTypes.func,
|
||||
history: PropTypes.object,
|
||||
isUnlocked: PropTypes.bool,
|
||||
useOldInterface: PropTypes.func,
|
||||
onImport: PropTypes.func,
|
||||
onRestore: PropTypes.func,
|
||||
onSubmit: PropTypes.func,
|
||||
}
|
||||
|
||||
constructor (props) {
|
||||
@ -43,12 +40,12 @@ export default class UnlockPage extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
async handleSubmit (event) {
|
||||
handleSubmit = async event => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
const { password } = this.state
|
||||
const { tryUnlockMetamask, history } = this.props
|
||||
const { onSubmit } = this.props
|
||||
|
||||
if (password === '' || this.submitting) {
|
||||
return
|
||||
@ -58,9 +55,7 @@ export default class UnlockPage extends Component {
|
||||
this.submitting = true
|
||||
|
||||
try {
|
||||
await tryUnlockMetamask(password)
|
||||
this.submitting = false
|
||||
history.push(DEFAULT_ROUTE)
|
||||
await onSubmit(password)
|
||||
} catch ({ message }) {
|
||||
this.setState({ error: message })
|
||||
this.submitting = false
|
||||
@ -99,7 +94,7 @@ export default class UnlockPage extends Component {
|
||||
fullWidth
|
||||
variant="raised"
|
||||
size="large"
|
||||
onClick={event => this.handleSubmit(event)}
|
||||
onClick={this.handleSubmit}
|
||||
disableRipple
|
||||
>
|
||||
{ this.context.t('login') }
|
||||
@ -110,7 +105,7 @@ export default class UnlockPage extends Component {
|
||||
render () {
|
||||
const { password, error } = this.state
|
||||
const { t } = this.context
|
||||
const { markPasswordForgotten, history } = this.props
|
||||
const { onImport, onRestore } = this.props
|
||||
|
||||
return (
|
||||
<div className="unlock-page__container">
|
||||
@ -128,7 +123,7 @@ export default class UnlockPage extends Component {
|
||||
<div>{ t('unlockMessage') }</div>
|
||||
<form
|
||||
className="unlock-page__form"
|
||||
onSubmit={event => this.handleSubmit(event)}
|
||||
onSubmit={this.handleSubmit}
|
||||
>
|
||||
<TextField
|
||||
id="password"
|
||||
@ -147,27 +142,13 @@ export default class UnlockPage extends Component {
|
||||
<div className="unlock-page__links">
|
||||
<div
|
||||
className="unlock-page__link"
|
||||
onClick={() => {
|
||||
markPasswordForgotten()
|
||||
history.push(RESTORE_VAULT_ROUTE)
|
||||
|
||||
if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_POPUP) {
|
||||
global.platform.openExtensionInBrowser()
|
||||
}
|
||||
}}
|
||||
onClick={() => onRestore()}
|
||||
>
|
||||
{ t('restoreFromSeed') }
|
||||
</div>
|
||||
<div
|
||||
className="unlock-page__link unlock-page__link--import"
|
||||
onClick={() => {
|
||||
markPasswordForgotten()
|
||||
history.push(RESTORE_VAULT_ROUTE)
|
||||
|
||||
if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_POPUP) {
|
||||
global.platform.openExtensionInBrowser()
|
||||
}
|
||||
}}
|
||||
onClick={() => onImport()}
|
||||
>
|
||||
{ t('importUsingSeed') }
|
||||
</div>
|
||||
|
@ -1,13 +1,14 @@
|
||||
import { connect } from 'react-redux'
|
||||
import { withRouter } from 'react-router-dom'
|
||||
import { compose } from 'recompose'
|
||||
|
||||
const {
|
||||
import { getEnvironmentType } from '../../../../../app/scripts/lib/util'
|
||||
import { ENVIRONMENT_TYPE_POPUP } from '../../../../../app/scripts/lib/enums'
|
||||
import { DEFAULT_ROUTE, RESTORE_VAULT_ROUTE } from '../../../routes'
|
||||
import {
|
||||
tryUnlockMetamask,
|
||||
forgotPassword,
|
||||
markPasswordForgotten,
|
||||
} = require('../../../actions')
|
||||
|
||||
} from '../../../actions'
|
||||
import UnlockPage from './unlock-page.component'
|
||||
|
||||
const mapStateToProps = state => {
|
||||
@ -25,7 +26,35 @@ const mapDispatchToProps = dispatch => {
|
||||
}
|
||||
}
|
||||
|
||||
const mergeProps = (stateProps, dispatchProps, ownProps) => {
|
||||
const { markPasswordForgotten, tryUnlockMetamask, ...restDispatchProps } = dispatchProps
|
||||
const { history, onSubmit: ownPropsSubmit, ...restOwnProps } = ownProps
|
||||
|
||||
const onImport = () => {
|
||||
markPasswordForgotten()
|
||||
history.push(RESTORE_VAULT_ROUTE)
|
||||
|
||||
if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_POPUP) {
|
||||
global.platform.openExtensionInBrowser()
|
||||
}
|
||||
}
|
||||
|
||||
const onSubmit = async password => {
|
||||
await tryUnlockMetamask(password)
|
||||
history.push(DEFAULT_ROUTE)
|
||||
}
|
||||
|
||||
return {
|
||||
...stateProps,
|
||||
...restDispatchProps,
|
||||
...restOwnProps,
|
||||
onImport,
|
||||
onRestore: onImport,
|
||||
onSubmit: ownPropsSubmit || onSubmit,
|
||||
}
|
||||
}
|
||||
|
||||
export default compose(
|
||||
withRouter,
|
||||
connect(mapStateToProps, mapDispatchToProps)
|
||||
connect(mapStateToProps, mapDispatchToProps, mergeProps)
|
||||
)(UnlockPage)
|
||||
|
@ -32,6 +32,7 @@
|
||||
@media screen and (max-width: $break-small) {
|
||||
font-size: 1.75rem;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -11,8 +11,6 @@
|
||||
|
||||
@import './itcss/generic/index.scss';
|
||||
|
||||
@import './itcss/base/index.scss';
|
||||
|
||||
@import './itcss/objects/index.scss';
|
||||
|
||||
@import './itcss/components/index.scss';
|
||||
|
@ -1,7 +0,0 @@
|
||||
// Base
|
||||
|
||||
.mouse-user-styles {
|
||||
button:focus {
|
||||
outline: 0;
|
||||
}
|
||||
}
|
@ -87,6 +87,18 @@
|
||||
min-width: initial;
|
||||
}
|
||||
|
||||
.btn--first-time {
|
||||
height: 54px;
|
||||
width: 198px;
|
||||
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, .14);
|
||||
color: $white;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 500;
|
||||
transition: 200ms ease-in-out;
|
||||
background-color: rgba(247, 134, 28, .9);
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.btn--large {
|
||||
min-height: 54px;
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user