1
0
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:
Alexander Tseung 2019-01-23 07:25:34 -08:00 committed by Whymarrh Whitby
parent 69fcfa427b
commit fba17d77de
114 changed files with 2722 additions and 3002 deletions

View File

@ -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": "Youll see this image everytime you need to confirm a transaction."
},
"zeroGasPriceOnSpeedUpError": {
"message":"Zero gas price on speed up"
}

View File

@ -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
//

View File

@ -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

View 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
}

View File

@ -41,4 +41,5 @@ module.exports = [
require('./028'),
require('./029'),
require('./030'),
require('./031'),
]

View File

@ -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()

View File

@ -1,5 +1,6 @@
{
"metamask": {
"completedOnboarding": true,
"isInitialized": true,
"isUnlocked": true,
"featureFlags": {"betaUI": true},

View File

@ -1,5 +1,6 @@
{
"metamask": {
"completedOnboarding": true,
"isInitialized": true,
"isUnlocked": true,
"featureFlags": {"betaUI": true},

View File

@ -1,5 +1,6 @@
{
"metamask": {
"completedOnboarding": true,
"isInitialized": true,
"isUnlocked": true,
"featureFlags": {"betaUI": true},

View File

@ -1,5 +1,6 @@
{
"metamask": {
"completedOnboarding": true,
"isInitialized": true,
"isUnlocked": true,
"featureFlags": {"betaUI": true},

View File

@ -1,5 +1,6 @@
{
"metamask": {
"completedOnboarding": true,
"isInitialized": true,
"isUnlocked": true,
"featureFlags": {"betaUI": true},

View File

@ -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>
)
}
}

View File

@ -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 worlds 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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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,
}

View File

@ -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)

View File

@ -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)

View File

@ -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>
);
}

View File

@ -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">
Youll 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)

View File

@ -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, {

View File

@ -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'),
}

View File

@ -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)
}

View File

@ -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)
})

View File

@ -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()

View File

@ -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)
}

View File

@ -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
}

View 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)
})
})

View File

@ -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)
})
})

View File

@ -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,

View File

@ -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,
}
}

View File

@ -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>

View File

@ -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,
}

View 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>
)
}
}

View File

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

View 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;
}

View File

@ -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')
})
})

View File

@ -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 {

View File

@ -4,6 +4,8 @@
@import './app-header/index';
@import './breadcrumbs/index';
@import './button-group/index';
@import './card/index';

View File

@ -0,0 +1 @@
export { default } from './lock-icon.component'

View 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>
)
}

View File

@ -122,7 +122,8 @@ const MODALS = {
display: 'flex',
},
laptopModalStyle: {
width: '850px',
width: 'initial',
maxWidth: '850px',
top: 'calc(10% + 10px)',
left: '0',
right: '0',

View File

@ -42,6 +42,12 @@
justify-content: space-between;
}
&__bottom {
flex: 1;
display: flex;
flex-direction: column;
}
&__footer {
display: flex;
flex-flow: column;

View File

@ -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)

View File

@ -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>
)
}
}

View File

@ -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)

View File

@ -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>
)
}
}

View File

@ -0,0 +1 @@
export { default } from './import-with-seed-phrase.component'

View File

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

View File

@ -0,0 +1 @@
export { default } from './new-account.component'

View File

@ -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>
)
}
}

View File

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

View File

@ -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>
)
}
}

View File

@ -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)

View File

@ -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 }} />
}
}

View File

@ -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)

View File

@ -0,0 +1 @@
export { default } from './first-time-flow-switch.container'

View File

@ -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>
)
}
}

View File

@ -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)

View File

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

View 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;
}
}

View File

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

View File

@ -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>
)
}
}

View File

@ -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)

View File

@ -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>
)
}
}

View File

@ -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)

View File

@ -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,
}
}
}

View File

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

View File

@ -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;
}
}
}

View File

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

View File

@ -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;
}
}
}

View File

@ -0,0 +1 @@
export { default } from './reveal-seed-phrase.component'

View File

@ -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;
}
}

View File

@ -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>
)
}
}

View File

@ -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>
)
}
}

View File

@ -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)

View File

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

View 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);
}
}

View File

@ -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>
)
}
}

View File

@ -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)

View File

@ -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) {

View File

@ -5,3 +5,7 @@
@import './confirm-add-token/index';
@import './settings/index';
@import './first-time-flow/index';
@import './keychains/index';

View File

@ -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)

View 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;
}

View File

@ -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>

View File

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

View 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 />
}
}

View 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)

View File

@ -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)

View File

@ -14,7 +14,6 @@
align-self: stretch;
justify-content: center;
flex: 1 0 auto;
height: 100vh;
}
&__mascot-container {

View File

@ -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>

View File

@ -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)

View File

@ -32,6 +32,7 @@
@media screen and (max-width: $break-small) {
font-size: 1.75rem;
width: 100%;
justify-content: center;
}
}

View File

@ -11,8 +11,6 @@
@import './itcss/generic/index.scss';
@import './itcss/base/index.scss';
@import './itcss/objects/index.scss';
@import './itcss/components/index.scss';

View File

@ -1,7 +0,0 @@
// Base
.mouse-user-styles {
button:focus {
outline: 0;
}
}

View File

@ -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