aboutsummaryrefslogtreecommitdiffstats
path: root/ui/app/pages
diff options
context:
space:
mode:
Diffstat (limited to 'ui/app/pages')
-rw-r--r--ui/app/pages/add-token/add-token.component.js335
-rw-r--r--ui/app/pages/add-token/add-token.container.js22
-rw-r--r--ui/app/pages/add-token/index.js2
-rw-r--r--ui/app/pages/add-token/index.scss45
-rw-r--r--ui/app/pages/add-token/token-list/index.js2
-rw-r--r--ui/app/pages/add-token/token-list/index.scss65
-rw-r--r--ui/app/pages/add-token/token-list/token-list-placeholder/index.js2
-rw-r--r--ui/app/pages/add-token/token-list/token-list-placeholder/index.scss23
-rw-r--r--ui/app/pages/add-token/token-list/token-list-placeholder/token-list-placeholder.component.js27
-rw-r--r--ui/app/pages/add-token/token-list/token-list.component.js60
-rw-r--r--ui/app/pages/add-token/token-list/token-list.container.js11
-rw-r--r--ui/app/pages/add-token/token-search/index.js2
-rw-r--r--ui/app/pages/add-token/token-search/token-search.component.js85
-rw-r--r--ui/app/pages/add-token/util.js13
-rw-r--r--ui/app/pages/confirm-add-suggested-token/confirm-add-suggested-token.component.js122
-rw-r--r--ui/app/pages/confirm-add-suggested-token/confirm-add-suggested-token.container.js29
-rw-r--r--ui/app/pages/confirm-add-suggested-token/index.js2
-rw-r--r--ui/app/pages/confirm-add-token/confirm-add-token.component.js117
-rw-r--r--ui/app/pages/confirm-add-token/confirm-add-token.container.js20
-rw-r--r--ui/app/pages/confirm-add-token/index.js2
-rw-r--r--ui/app/pages/confirm-add-token/index.scss69
-rw-r--r--ui/app/pages/confirm-approve/confirm-approve.component.js21
-rw-r--r--ui/app/pages/confirm-approve/confirm-approve.container.js15
-rw-r--r--ui/app/pages/confirm-approve/index.js1
-rw-r--r--ui/app/pages/confirm-deploy-contract/confirm-deploy-contract.component.js64
-rw-r--r--ui/app/pages/confirm-deploy-contract/confirm-deploy-contract.container.js12
-rw-r--r--ui/app/pages/confirm-deploy-contract/index.js1
-rw-r--r--ui/app/pages/confirm-send-ether/confirm-send-ether.component.js39
-rw-r--r--ui/app/pages/confirm-send-ether/confirm-send-ether.container.js45
-rw-r--r--ui/app/pages/confirm-send-ether/index.js1
-rw-r--r--ui/app/pages/confirm-send-token/confirm-send-token.component.js29
-rw-r--r--ui/app/pages/confirm-send-token/confirm-send-token.container.js52
-rw-r--r--ui/app/pages/confirm-send-token/index.js1
-rw-r--r--ui/app/pages/confirm-token-transaction-base/confirm-token-transaction-base.component.js119
-rw-r--r--ui/app/pages/confirm-token-transaction-base/confirm-token-transaction-base.container.js34
-rw-r--r--ui/app/pages/confirm-token-transaction-base/index.js2
-rw-r--r--ui/app/pages/confirm-transaction-base/confirm-transaction-base.component.js574
-rw-r--r--ui/app/pages/confirm-transaction-base/confirm-transaction-base.container.js242
-rw-r--r--ui/app/pages/confirm-transaction-base/index.js1
-rw-r--r--ui/app/pages/confirm-transaction-base/tests/confirm-transaction-base.component.test.js14
-rw-r--r--ui/app/pages/confirm-transaction-switch/confirm-transaction-switch.component.js92
-rw-r--r--ui/app/pages/confirm-transaction-switch/confirm-transaction-switch.container.js22
-rw-r--r--ui/app/pages/confirm-transaction-switch/confirm-transaction-switch.util.js4
-rw-r--r--ui/app/pages/confirm-transaction-switch/index.js2
-rw-r--r--ui/app/pages/confirm-transaction/conf-tx.js225
-rw-r--r--ui/app/pages/confirm-transaction/confirm-transaction.component.js160
-rw-r--r--ui/app/pages/confirm-transaction/confirm-transaction.container.js37
-rw-r--r--ui/app/pages/confirm-transaction/index.js2
-rw-r--r--ui/app/pages/create-account/connect-hardware/account-list.js205
-rw-r--r--ui/app/pages/create-account/connect-hardware/connect-screen.js197
-rw-r--r--ui/app/pages/create-account/connect-hardware/index.js293
-rw-r--r--ui/app/pages/create-account/import-account/index.js96
-rw-r--r--ui/app/pages/create-account/import-account/json.js170
-rw-r--r--ui/app/pages/create-account/import-account/private-key.js128
-rw-r--r--ui/app/pages/create-account/import-account/seed.js35
-rw-r--r--ui/app/pages/create-account/index.js113
-rw-r--r--ui/app/pages/create-account/new-account.js130
-rw-r--r--ui/app/pages/first-time-flow/create-password/create-password.component.js71
-rw-r--r--ui/app/pages/first-time-flow/create-password/create-password.container.js12
-rw-r--r--ui/app/pages/first-time-flow/create-password/import-with-seed-phrase/import-with-seed-phrase.component.js256
-rw-r--r--ui/app/pages/first-time-flow/create-password/import-with-seed-phrase/index.js1
-rw-r--r--ui/app/pages/first-time-flow/create-password/index.js1
-rw-r--r--ui/app/pages/first-time-flow/create-password/new-account/index.js1
-rw-r--r--ui/app/pages/first-time-flow/create-password/new-account/new-account.component.js225
-rw-r--r--ui/app/pages/first-time-flow/create-password/unique-image/index.js1
-rw-r--r--ui/app/pages/first-time-flow/create-password/unique-image/unique-image.component.js55
-rw-r--r--ui/app/pages/first-time-flow/create-password/unique-image/unique-image.container.js12
-rw-r--r--ui/app/pages/first-time-flow/end-of-flow/end-of-flow.component.js93
-rw-r--r--ui/app/pages/first-time-flow/end-of-flow/end-of-flow.container.js25
-rw-r--r--ui/app/pages/first-time-flow/end-of-flow/index.js1
-rw-r--r--ui/app/pages/first-time-flow/end-of-flow/index.scss53
-rw-r--r--ui/app/pages/first-time-flow/first-time-flow-switch/first-time-flow-switch.component.js57
-rw-r--r--ui/app/pages/first-time-flow/first-time-flow-switch/first-time-flow-switch.container.js20
-rw-r--r--ui/app/pages/first-time-flow/first-time-flow-switch/index.js1
-rw-r--r--ui/app/pages/first-time-flow/first-time-flow.component.js152
-rw-r--r--ui/app/pages/first-time-flow/first-time-flow.container.js31
-rw-r--r--ui/app/pages/first-time-flow/first-time-flow.selectors.js26
-rw-r--r--ui/app/pages/first-time-flow/index.js1
-rw-r--r--ui/app/pages/first-time-flow/index.scss159
-rw-r--r--ui/app/pages/first-time-flow/metametrics-opt-in/index.js1
-rw-r--r--ui/app/pages/first-time-flow/metametrics-opt-in/index.scss136
-rw-r--r--ui/app/pages/first-time-flow/metametrics-opt-in/metametrics-opt-in.component.js169
-rw-r--r--ui/app/pages/first-time-flow/metametrics-opt-in/metametrics-opt-in.container.js27
-rw-r--r--ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/confirm-seed-phrase.component.js155
-rw-r--r--ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/confirm-seed-phrase.state.js41
-rw-r--r--ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/index.js1
-rw-r--r--ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/index.scss48
-rw-r--r--ui/app/pages/first-time-flow/seed-phrase/index.js1
-rw-r--r--ui/app/pages/first-time-flow/seed-phrase/index.scss40
-rw-r--r--ui/app/pages/first-time-flow/seed-phrase/reveal-seed-phrase/index.js1
-rw-r--r--ui/app/pages/first-time-flow/seed-phrase/reveal-seed-phrase/index.scss57
-rw-r--r--ui/app/pages/first-time-flow/seed-phrase/reveal-seed-phrase/reveal-seed-phrase.component.js143
-rw-r--r--ui/app/pages/first-time-flow/seed-phrase/seed-phrase.component.js70
-rw-r--r--ui/app/pages/first-time-flow/select-action/index.js1
-rw-r--r--ui/app/pages/first-time-flow/select-action/index.scss88
-rw-r--r--ui/app/pages/first-time-flow/select-action/select-action.component.js112
-rw-r--r--ui/app/pages/first-time-flow/select-action/select-action.container.js23
-rw-r--r--ui/app/pages/first-time-flow/welcome/index.js1
-rw-r--r--ui/app/pages/first-time-flow/welcome/index.scss42
-rw-r--r--ui/app/pages/first-time-flow/welcome/welcome.component.js69
-rw-r--r--ui/app/pages/first-time-flow/welcome/welcome.container.js26
-rw-r--r--ui/app/pages/home/home.component.js77
-rw-r--r--ui/app/pages/home/home.container.js32
-rw-r--r--ui/app/pages/home/index.js1
-rw-r--r--ui/app/pages/index.js31
-rw-r--r--ui/app/pages/index.scss11
-rw-r--r--ui/app/pages/keychains/index.scss197
-rw-r--r--ui/app/pages/keychains/restore-vault.js197
-rw-r--r--ui/app/pages/keychains/reveal-seed.js177
-rw-r--r--ui/app/pages/lock/index.js1
-rw-r--r--ui/app/pages/lock/lock.component.js26
-rw-r--r--ui/app/pages/lock/lock.container.js24
-rw-r--r--ui/app/pages/mobile-sync/index.js387
-rw-r--r--ui/app/pages/notice/notice.js203
-rw-r--r--ui/app/pages/provider-approval/index.js1
-rw-r--r--ui/app/pages/provider-approval/provider-approval.component.js29
-rw-r--r--ui/app/pages/provider-approval/provider-approval.container.js12
-rw-r--r--ui/app/pages/routes/index.js441
-rw-r--r--ui/app/pages/settings/index.js1
-rw-r--r--ui/app/pages/settings/index.scss80
-rw-r--r--ui/app/pages/settings/info-tab/index.js1
-rw-r--r--ui/app/pages/settings/info-tab/index.scss56
-rw-r--r--ui/app/pages/settings/info-tab/info-tab.component.js136
-rw-r--r--ui/app/pages/settings/settings-tab/index.js1
-rw-r--r--ui/app/pages/settings/settings-tab/index.scss69
-rw-r--r--ui/app/pages/settings/settings-tab/settings-tab.component.js674
-rw-r--r--ui/app/pages/settings/settings-tab/settings-tab.container.js81
-rw-r--r--ui/app/pages/settings/settings.component.js54
-rw-r--r--ui/app/pages/unlock-page/index.js2
-rw-r--r--ui/app/pages/unlock-page/index.scss51
-rw-r--r--ui/app/pages/unlock-page/unlock-page.component.js191
-rw-r--r--ui/app/pages/unlock-page/unlock-page.container.js64
132 files changed, 10047 insertions, 0 deletions
diff --git a/ui/app/pages/add-token/add-token.component.js b/ui/app/pages/add-token/add-token.component.js
new file mode 100644
index 000000000..40c1ff7fd
--- /dev/null
+++ b/ui/app/pages/add-token/add-token.component.js
@@ -0,0 +1,335 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import ethUtil from 'ethereumjs-util'
+import { checkExistingAddresses } from './util'
+import { tokenInfoGetter } from '../../helpers/utils/token-util'
+import { DEFAULT_ROUTE, CONFIRM_ADD_TOKEN_ROUTE } from '../../helpers/constants/routes'
+import TextField from '../../components/ui/text-field'
+import TokenList from './token-list'
+import TokenSearch from './token-search'
+import PageContainer from '../../components/ui/page-container'
+import { Tabs, Tab } from '../../components/ui/tabs'
+
+const emptyAddr = '0x0000000000000000000000000000000000000000'
+const SEARCH_TAB = 'SEARCH'
+const CUSTOM_TOKEN_TAB = 'CUSTOM_TOKEN'
+
+class AddToken extends Component {
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ static propTypes = {
+ history: PropTypes.object,
+ setPendingTokens: PropTypes.func,
+ pendingTokens: PropTypes.object,
+ clearPendingTokens: PropTypes.func,
+ tokens: PropTypes.array,
+ identities: PropTypes.object,
+ }
+
+ constructor (props) {
+ super(props)
+
+ this.state = {
+ customAddress: '',
+ customSymbol: '',
+ customDecimals: 0,
+ searchResults: [],
+ selectedTokens: {},
+ tokenSelectorError: null,
+ customAddressError: null,
+ customSymbolError: null,
+ customDecimalsError: null,
+ autoFilled: false,
+ displayedTab: SEARCH_TAB,
+ forceEditSymbol: false,
+ }
+ }
+
+ componentDidMount () {
+ this.tokenInfoGetter = tokenInfoGetter()
+ const { pendingTokens = {} } = this.props
+ const pendingTokenKeys = Object.keys(pendingTokens)
+
+ if (pendingTokenKeys.length > 0) {
+ let selectedTokens = {}
+ let customToken = {}
+
+ pendingTokenKeys.forEach(tokenAddress => {
+ const token = pendingTokens[tokenAddress]
+ const { isCustom } = token
+
+ if (isCustom) {
+ customToken = { ...token }
+ } else {
+ selectedTokens = { ...selectedTokens, [tokenAddress]: { ...token } }
+ }
+ })
+
+ const {
+ address: customAddress = '',
+ symbol: customSymbol = '',
+ decimals: customDecimals = 0,
+ } = customToken
+
+ const displayedTab = Object.keys(selectedTokens).length > 0 ? SEARCH_TAB : CUSTOM_TOKEN_TAB
+ this.setState({ selectedTokens, customAddress, customSymbol, customDecimals, displayedTab })
+ }
+ }
+
+ handleToggleToken (token) {
+ const { address } = token
+ const { selectedTokens = {} } = this.state
+ const selectedTokensCopy = { ...selectedTokens }
+
+ if (address in selectedTokensCopy) {
+ delete selectedTokensCopy[address]
+ } else {
+ selectedTokensCopy[address] = token
+ }
+
+ this.setState({
+ selectedTokens: selectedTokensCopy,
+ tokenSelectorError: null,
+ })
+ }
+
+ hasError () {
+ const {
+ tokenSelectorError,
+ customAddressError,
+ customSymbolError,
+ customDecimalsError,
+ } = this.state
+
+ return tokenSelectorError || customAddressError || customSymbolError || customDecimalsError
+ }
+
+ hasSelected () {
+ const { customAddress = '', selectedTokens = {} } = this.state
+ return customAddress || Object.keys(selectedTokens).length > 0
+ }
+
+ handleNext () {
+ if (this.hasError()) {
+ return
+ }
+
+ if (!this.hasSelected()) {
+ this.setState({ tokenSelectorError: this.context.t('mustSelectOne') })
+ return
+ }
+
+ const { setPendingTokens, history } = this.props
+ const {
+ customAddress: address,
+ customSymbol: symbol,
+ customDecimals: decimals,
+ selectedTokens,
+ } = this.state
+
+ const customToken = {
+ address,
+ symbol,
+ decimals,
+ }
+
+ setPendingTokens({ customToken, selectedTokens })
+ history.push(CONFIRM_ADD_TOKEN_ROUTE)
+ }
+
+ async attemptToAutoFillTokenParams (address) {
+ const { symbol = '', decimals = 0 } = await this.tokenInfoGetter(address)
+
+ const autoFilled = Boolean(symbol && decimals)
+ this.setState({ autoFilled })
+ this.handleCustomSymbolChange(symbol || '')
+ this.handleCustomDecimalsChange(decimals)
+ }
+
+ handleCustomAddressChange (value) {
+ const customAddress = value.trim()
+ this.setState({
+ customAddress,
+ customAddressError: null,
+ tokenSelectorError: null,
+ autoFilled: false,
+ })
+
+ const isValidAddress = ethUtil.isValidAddress(customAddress)
+ const standardAddress = ethUtil.addHexPrefix(customAddress).toLowerCase()
+
+ switch (true) {
+ case !isValidAddress:
+ this.setState({
+ customAddressError: this.context.t('invalidAddress'),
+ customSymbol: '',
+ customDecimals: 0,
+ customSymbolError: null,
+ customDecimalsError: null,
+ })
+
+ break
+ case Boolean(this.props.identities[standardAddress]):
+ this.setState({
+ customAddressError: this.context.t('personalAddressDetected'),
+ })
+
+ break
+ case checkExistingAddresses(customAddress, this.props.tokens):
+ this.setState({
+ customAddressError: this.context.t('tokenAlreadyAdded'),
+ })
+
+ break
+ default:
+ if (customAddress !== emptyAddr) {
+ this.attemptToAutoFillTokenParams(customAddress)
+ }
+ }
+ }
+
+ handleCustomSymbolChange (value) {
+ const customSymbol = value.trim()
+ const symbolLength = customSymbol.length
+ let customSymbolError = null
+
+ if (symbolLength <= 0 || symbolLength >= 12) {
+ customSymbolError = this.context.t('symbolBetweenZeroTwelve')
+ }
+
+ this.setState({ customSymbol, customSymbolError })
+ }
+
+ handleCustomDecimalsChange (value) {
+ const customDecimals = value.trim()
+ const validDecimals = customDecimals !== null &&
+ customDecimals !== '' &&
+ customDecimals >= 0 &&
+ customDecimals <= 36
+ let customDecimalsError = null
+
+ if (!validDecimals) {
+ customDecimalsError = this.context.t('decimalsMustZerotoTen')
+ }
+
+ this.setState({ customDecimals, customDecimalsError })
+ }
+
+ renderCustomTokenForm () {
+ const {
+ customAddress,
+ customSymbol,
+ customDecimals,
+ customAddressError,
+ customSymbolError,
+ customDecimalsError,
+ autoFilled,
+ forceEditSymbol,
+ } = this.state
+
+ return (
+ <div className="add-token__custom-token-form">
+ <TextField
+ id="custom-address"
+ label={this.context.t('tokenContractAddress')}
+ type="text"
+ value={customAddress}
+ onChange={e => this.handleCustomAddressChange(e.target.value)}
+ error={customAddressError}
+ fullWidth
+ margin="normal"
+ />
+ <TextField
+ id="custom-symbol"
+ label={(
+ <div className="add-token__custom-symbol__label-wrapper">
+ <span className="add-token__custom-symbol__label">
+ {this.context.t('tokenSymbol')}
+ </span>
+ {(autoFilled && !forceEditSymbol) && (
+ <div
+ className="add-token__custom-symbol__edit"
+ onClick={() => this.setState({ forceEditSymbol: true })}
+ >
+ {this.context.t('edit')}
+ </div>
+ )}
+ </div>
+ )}
+ type="text"
+ value={customSymbol}
+ onChange={e => this.handleCustomSymbolChange(e.target.value)}
+ error={customSymbolError}
+ fullWidth
+ margin="normal"
+ disabled={autoFilled && !forceEditSymbol}
+ />
+ <TextField
+ id="custom-decimals"
+ label={this.context.t('decimal')}
+ type="number"
+ value={customDecimals}
+ onChange={e => this.handleCustomDecimalsChange(e.target.value)}
+ error={customDecimalsError}
+ fullWidth
+ margin="normal"
+ disabled={autoFilled}
+ />
+ </div>
+ )
+ }
+
+ renderSearchToken () {
+ const { tokenSelectorError, selectedTokens, searchResults } = this.state
+
+ return (
+ <div className="add-token__search-token">
+ <TokenSearch
+ onSearch={({ results = [] }) => this.setState({ searchResults: results })}
+ error={tokenSelectorError}
+ />
+ <div className="add-token__token-list">
+ <TokenList
+ results={searchResults}
+ selectedTokens={selectedTokens}
+ onToggleToken={token => this.handleToggleToken(token)}
+ />
+ </div>
+ </div>
+ )
+ }
+
+ renderTabs () {
+ return (
+ <Tabs>
+ <Tab name={this.context.t('search')}>
+ { this.renderSearchToken() }
+ </Tab>
+ <Tab name={this.context.t('customToken')}>
+ { this.renderCustomTokenForm() }
+ </Tab>
+ </Tabs>
+ )
+ }
+
+ render () {
+ const { history, clearPendingTokens } = this.props
+
+ return (
+ <PageContainer
+ title={this.context.t('addTokens')}
+ tabsComponent={this.renderTabs()}
+ onSubmit={() => this.handleNext()}
+ disabled={this.hasError() || !this.hasSelected()}
+ onCancel={() => {
+ clearPendingTokens()
+ history.push(DEFAULT_ROUTE)
+ }}
+ />
+ )
+ }
+}
+
+export default AddToken
diff --git a/ui/app/pages/add-token/add-token.container.js b/ui/app/pages/add-token/add-token.container.js
new file mode 100644
index 000000000..eee16dfc7
--- /dev/null
+++ b/ui/app/pages/add-token/add-token.container.js
@@ -0,0 +1,22 @@
+import { connect } from 'react-redux'
+import AddToken from './add-token.component'
+
+const { setPendingTokens, clearPendingTokens } = require('../../store/actions')
+
+const mapStateToProps = ({ metamask }) => {
+ const { identities, tokens, pendingTokens } = metamask
+ return {
+ identities,
+ tokens,
+ pendingTokens,
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ setPendingTokens: tokens => dispatch(setPendingTokens(tokens)),
+ clearPendingTokens: () => dispatch(clearPendingTokens()),
+ }
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(AddToken)
diff --git a/ui/app/pages/add-token/index.js b/ui/app/pages/add-token/index.js
new file mode 100644
index 000000000..3666cae82
--- /dev/null
+++ b/ui/app/pages/add-token/index.js
@@ -0,0 +1,2 @@
+import AddToken from './add-token.container'
+module.exports = AddToken
diff --git a/ui/app/pages/add-token/index.scss b/ui/app/pages/add-token/index.scss
new file mode 100644
index 000000000..ef6802f96
--- /dev/null
+++ b/ui/app/pages/add-token/index.scss
@@ -0,0 +1,45 @@
+@import 'token-list/index';
+
+.add-token {
+ &__custom-token-form {
+ padding: 8px 16px 16px;
+
+ input[type="number"]::-webkit-inner-spin-button {
+ -webkit-appearance: none;
+ display: none;
+ }
+
+ input[type="number"]:hover::-webkit-inner-spin-button {
+ -webkit-appearance: none;
+ display: none;
+ }
+ }
+
+ &__search-token {
+ padding: 16px;
+ }
+
+ &__token-list {
+ margin-top: 16px;
+ }
+
+ &__custom-symbol {
+
+ &__label-wrapper {
+ display: flex;
+ flex-flow: row nowrap;
+ }
+
+ &__label {
+ flex: 0 0 auto;
+ }
+
+ &__edit {
+ flex: 1 1 auto;
+ text-align: right;
+ color: $curious-blue;
+ padding-right: 4px;
+ cursor: pointer;
+ }
+ }
+}
diff --git a/ui/app/pages/add-token/token-list/index.js b/ui/app/pages/add-token/token-list/index.js
new file mode 100644
index 000000000..21dd5ac72
--- /dev/null
+++ b/ui/app/pages/add-token/token-list/index.js
@@ -0,0 +1,2 @@
+import TokenList from './token-list.container'
+module.exports = TokenList
diff --git a/ui/app/pages/add-token/token-list/index.scss b/ui/app/pages/add-token/token-list/index.scss
new file mode 100644
index 000000000..b7787a18e
--- /dev/null
+++ b/ui/app/pages/add-token/token-list/index.scss
@@ -0,0 +1,65 @@
+@import 'token-list-placeholder/index';
+
+.token-list {
+ &__title {
+ font-size: .75rem;
+ }
+
+ &__tokens-container {
+ display: flex;
+ flex-direction: column;
+ }
+
+ &__token {
+ transition: 200ms ease-in-out;
+ display: flex;
+ flex-flow: row nowrap;
+ align-items: center;
+ padding: 8px;
+ margin-top: 8px;
+ box-sizing: border-box;
+ border-radius: 10px;
+ cursor: pointer;
+ border: 2px solid transparent;
+ position: relative;
+
+ &:hover {
+ border: 2px solid rgba($malibu-blue, .5);
+ }
+
+ &--selected {
+ border: 2px solid $malibu-blue !important;
+ }
+
+ &--disabled {
+ opacity: .4;
+ pointer-events: none;
+ }
+ }
+
+ &__token-icon {
+ width: 48px;
+ height: 48px;
+ background-repeat: no-repeat;
+ background-size: contain;
+ background-position: center;
+ border-radius: 50%;
+ background-color: $white;
+ box-shadow: 0 2px 4px 0 rgba($black, .24);
+ margin-right: 12px;
+ flex: 0 0 auto;
+ }
+
+ &__token-data {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ min-width: 0;
+ }
+
+ &__token-name {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+}
diff --git a/ui/app/pages/add-token/token-list/token-list-placeholder/index.js b/ui/app/pages/add-token/token-list/token-list-placeholder/index.js
new file mode 100644
index 000000000..b82f45e93
--- /dev/null
+++ b/ui/app/pages/add-token/token-list/token-list-placeholder/index.js
@@ -0,0 +1,2 @@
+import TokenListPlaceholder from './token-list-placeholder.component'
+module.exports = TokenListPlaceholder
diff --git a/ui/app/pages/add-token/token-list/token-list-placeholder/index.scss b/ui/app/pages/add-token/token-list/token-list-placeholder/index.scss
new file mode 100644
index 000000000..cc495dfb0
--- /dev/null
+++ b/ui/app/pages/add-token/token-list/token-list-placeholder/index.scss
@@ -0,0 +1,23 @@
+.token-list-placeholder {
+ display: flex;
+ align-items: center;
+ padding-top: 36px;
+ flex-direction: column;
+ line-height: 22px;
+ opacity: .5;
+
+ &__text {
+ color: $silver-chalice;
+ width: 50%;
+ text-align: center;
+ margin-top: 8px;
+
+ @media screen and (max-width: 575px) {
+ width: 60%;
+ }
+ }
+
+ &__link {
+ color: $curious-blue;
+ }
+}
diff --git a/ui/app/pages/add-token/token-list/token-list-placeholder/token-list-placeholder.component.js b/ui/app/pages/add-token/token-list/token-list-placeholder/token-list-placeholder.component.js
new file mode 100644
index 000000000..20f550927
--- /dev/null
+++ b/ui/app/pages/add-token/token-list/token-list-placeholder/token-list-placeholder.component.js
@@ -0,0 +1,27 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+
+export default class TokenListPlaceholder extends Component {
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ render () {
+ return (
+ <div className="token-list-placeholder">
+ <img src="images/tokensearch.svg" />
+ <div className="token-list-placeholder__text">
+ { this.context.t('addAcquiredTokens') }
+ </div>
+ <a
+ className="token-list-placeholder__link"
+ href="https://metamask.zendesk.com/hc/en-us/articles/360015489031"
+ target="_blank"
+ rel="noopener noreferrer"
+ >
+ { this.context.t('learnMore') }
+ </a>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/pages/add-token/token-list/token-list.component.js b/ui/app/pages/add-token/token-list/token-list.component.js
new file mode 100644
index 000000000..724a68d6e
--- /dev/null
+++ b/ui/app/pages/add-token/token-list/token-list.component.js
@@ -0,0 +1,60 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import classnames from 'classnames'
+import { checkExistingAddresses } from '../util'
+import TokenListPlaceholder from './token-list-placeholder'
+
+export default class InfoBox extends Component {
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ static propTypes = {
+ tokens: PropTypes.array,
+ results: PropTypes.array,
+ selectedTokens: PropTypes.object,
+ onToggleToken: PropTypes.func,
+ }
+
+ render () {
+ const { results = [], selectedTokens = {}, onToggleToken, tokens = [] } = this.props
+
+ return results.length === 0
+ ? <TokenListPlaceholder />
+ : (
+ <div className="token-list">
+ <div className="token-list__title">
+ { this.context.t('searchResults') }
+ </div>
+ <div className="token-list__tokens-container">
+ {
+ Array(6).fill(undefined)
+ .map((_, i) => {
+ const { logo, symbol, name, address } = results[i] || {}
+ const tokenAlreadyAdded = checkExistingAddresses(address, tokens)
+
+ return Boolean(logo || symbol || name) && (
+ <div
+ className={classnames('token-list__token', {
+ 'token-list__token--selected': selectedTokens[address],
+ 'token-list__token--disabled': tokenAlreadyAdded,
+ })}
+ onClick={() => !tokenAlreadyAdded && onToggleToken(results[i])}
+ key={i}
+ >
+ <div
+ className="token-list__token-icon"
+ style={{ backgroundImage: logo && `url(images/contract/${logo})` }}>
+ </div>
+ <div className="token-list__token-data">
+ <span className="token-list__token-name">{ `${name} (${symbol})` }</span>
+ </div>
+ </div>
+ )
+ })
+ }
+ </div>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/pages/add-token/token-list/token-list.container.js b/ui/app/pages/add-token/token-list/token-list.container.js
new file mode 100644
index 000000000..cd7b07a37
--- /dev/null
+++ b/ui/app/pages/add-token/token-list/token-list.container.js
@@ -0,0 +1,11 @@
+import { connect } from 'react-redux'
+import TokenList from './token-list.component'
+
+const mapStateToProps = ({ metamask }) => {
+ const { tokens } = metamask
+ return {
+ tokens,
+ }
+}
+
+export default connect(mapStateToProps)(TokenList)
diff --git a/ui/app/pages/add-token/token-search/index.js b/ui/app/pages/add-token/token-search/index.js
new file mode 100644
index 000000000..acaa6b084
--- /dev/null
+++ b/ui/app/pages/add-token/token-search/index.js
@@ -0,0 +1,2 @@
+import TokenSearch from './token-search.component'
+module.exports = TokenSearch
diff --git a/ui/app/pages/add-token/token-search/token-search.component.js b/ui/app/pages/add-token/token-search/token-search.component.js
new file mode 100644
index 000000000..5542a19ff
--- /dev/null
+++ b/ui/app/pages/add-token/token-search/token-search.component.js
@@ -0,0 +1,85 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import contractMap from 'eth-contract-metadata'
+import Fuse from 'fuse.js'
+import InputAdornment from '@material-ui/core/InputAdornment'
+import TextField from '../../../components/ui/text-field'
+
+const contractList = Object.entries(contractMap)
+ .map(([ _, tokenData]) => tokenData)
+ .filter(tokenData => Boolean(tokenData.erc20))
+
+const fuse = new Fuse(contractList, {
+ shouldSort: true,
+ threshold: 0.45,
+ location: 0,
+ distance: 100,
+ maxPatternLength: 32,
+ minMatchCharLength: 1,
+ keys: [
+ { name: 'name', weight: 0.5 },
+ { name: 'symbol', weight: 0.5 },
+ ],
+})
+
+export default class TokenSearch extends Component {
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ static defaultProps = {
+ error: null,
+ }
+
+ static propTypes = {
+ onSearch: PropTypes.func,
+ error: PropTypes.string,
+ }
+
+ constructor (props) {
+ super(props)
+
+ this.state = {
+ searchQuery: '',
+ }
+ }
+
+ handleSearch (searchQuery) {
+ this.setState({ searchQuery })
+ const fuseSearchResult = fuse.search(searchQuery)
+ const addressSearchResult = contractList.filter(token => {
+ return token.address.toLowerCase() === searchQuery.toLowerCase()
+ })
+ const results = [...addressSearchResult, ...fuseSearchResult]
+ this.props.onSearch({ searchQuery, results })
+ }
+
+ renderAdornment () {
+ return (
+ <InputAdornment
+ position="start"
+ style={{ marginRight: '12px' }}
+ >
+ <img src="images/search.svg" />
+ </InputAdornment>
+ )
+ }
+
+ render () {
+ const { error } = this.props
+ const { searchQuery } = this.state
+
+ return (
+ <TextField
+ id="search-tokens"
+ placeholder={this.context.t('searchTokens')}
+ type="text"
+ value={searchQuery}
+ onChange={e => this.handleSearch(e.target.value)}
+ error={error}
+ fullWidth
+ startAdornment={this.renderAdornment()}
+ />
+ )
+ }
+}
diff --git a/ui/app/pages/add-token/util.js b/ui/app/pages/add-token/util.js
new file mode 100644
index 000000000..579c56cc0
--- /dev/null
+++ b/ui/app/pages/add-token/util.js
@@ -0,0 +1,13 @@
+import R from 'ramda'
+
+export function checkExistingAddresses (address, tokenList = []) {
+ if (!address) {
+ return false
+ }
+
+ const matchesAddress = existingToken => {
+ return existingToken.address.toLowerCase() === address.toLowerCase()
+ }
+
+ return R.any(matchesAddress)(tokenList)
+}
diff --git a/ui/app/pages/confirm-add-suggested-token/confirm-add-suggested-token.component.js b/ui/app/pages/confirm-add-suggested-token/confirm-add-suggested-token.component.js
new file mode 100644
index 000000000..7edb8f541
--- /dev/null
+++ b/ui/app/pages/confirm-add-suggested-token/confirm-add-suggested-token.component.js
@@ -0,0 +1,122 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import { DEFAULT_ROUTE } from '../../helpers/constants/routes'
+import Button from '../../components/ui/button'
+import Identicon from '../../components/ui/identicon'
+import TokenBalance from '../../components/ui/token-balance'
+
+export default class ConfirmAddSuggestedToken extends Component {
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ static propTypes = {
+ history: PropTypes.object,
+ clearPendingTokens: PropTypes.func,
+ addToken: PropTypes.func,
+ pendingTokens: PropTypes.object,
+ removeSuggestedTokens: PropTypes.func,
+ }
+
+ componentDidMount () {
+ const { pendingTokens = {}, history } = this.props
+
+ if (Object.keys(pendingTokens).length === 0) {
+ history.push(DEFAULT_ROUTE)
+ }
+ }
+
+ getTokenName (name, symbol) {
+ return typeof name === 'undefined'
+ ? symbol
+ : `${name} (${symbol})`
+ }
+
+ render () {
+ const { addToken, pendingTokens, removeSuggestedTokens, history } = this.props
+ const pendingTokenKey = Object.keys(pendingTokens)[0]
+ const pendingToken = pendingTokens[pendingTokenKey]
+
+ return (
+ <div className="page-container">
+ <div className="page-container__header">
+ <div className="page-container__title">
+ { this.context.t('addSuggestedTokens') }
+ </div>
+ <div className="page-container__subtitle">
+ { this.context.t('likeToAddTokens') }
+ </div>
+ </div>
+ <div className="page-container__content">
+ <div className="confirm-add-token">
+ <div className="confirm-add-token__header">
+ <div className="confirm-add-token__token">
+ { this.context.t('token') }
+ </div>
+ <div className="confirm-add-token__balance">
+ { this.context.t('balance') }
+ </div>
+ </div>
+ <div className="confirm-add-token__token-list">
+ {
+ Object.entries(pendingTokens)
+ .map(([ address, token ]) => {
+ const { name, symbol, image } = token
+
+ return (
+ <div
+ className="confirm-add-token__token-list-item"
+ key={address}
+ >
+ <div className="confirm-add-token__token confirm-add-token__data">
+ <Identicon
+ className="confirm-add-token__token-icon"
+ diameter={48}
+ address={address}
+ image={image}
+ />
+ <div className="confirm-add-token__name">
+ { this.getTokenName(name, symbol) }
+ </div>
+ </div>
+ <div className="confirm-add-token__balance">
+ <TokenBalance token={token} />
+ </div>
+ </div>
+ )
+ })
+ }
+ </div>
+ </div>
+ </div>
+ <div className="page-container__footer">
+ <header>
+ <Button
+ type="default"
+ large
+ className="page-container__footer-button"
+ onClick={() => {
+ removeSuggestedTokens()
+ .then(() => history.push(DEFAULT_ROUTE))
+ }}
+ >
+ { this.context.t('cancel') }
+ </Button>
+ <Button
+ type="primary"
+ large
+ className="page-container__footer-button"
+ onClick={() => {
+ addToken(pendingToken)
+ .then(() => removeSuggestedTokens())
+ .then(() => history.push(DEFAULT_ROUTE))
+ }}
+ >
+ { this.context.t('addToken') }
+ </Button>
+ </header>
+ </div>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/pages/confirm-add-suggested-token/confirm-add-suggested-token.container.js b/ui/app/pages/confirm-add-suggested-token/confirm-add-suggested-token.container.js
new file mode 100644
index 000000000..a90fe148f
--- /dev/null
+++ b/ui/app/pages/confirm-add-suggested-token/confirm-add-suggested-token.container.js
@@ -0,0 +1,29 @@
+import { connect } from 'react-redux'
+import { compose } from 'recompose'
+import ConfirmAddSuggestedToken from './confirm-add-suggested-token.component'
+import { withRouter } from 'react-router-dom'
+
+const extend = require('xtend')
+
+const { addToken, removeSuggestedTokens } = require('../../store/actions')
+
+const mapStateToProps = ({ metamask }) => {
+ const { pendingTokens, suggestedTokens } = metamask
+ const params = extend(pendingTokens, suggestedTokens)
+
+ return {
+ pendingTokens: params,
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ addToken: ({address, symbol, decimals, image}) => dispatch(addToken(address, symbol, decimals, image)),
+ removeSuggestedTokens: () => dispatch(removeSuggestedTokens()),
+ }
+}
+
+export default compose(
+ withRouter,
+ connect(mapStateToProps, mapDispatchToProps)
+)(ConfirmAddSuggestedToken)
diff --git a/ui/app/pages/confirm-add-suggested-token/index.js b/ui/app/pages/confirm-add-suggested-token/index.js
new file mode 100644
index 000000000..2ca56b43c
--- /dev/null
+++ b/ui/app/pages/confirm-add-suggested-token/index.js
@@ -0,0 +1,2 @@
+import ConfirmAddSuggestedToken from './confirm-add-suggested-token.container'
+module.exports = ConfirmAddSuggestedToken
diff --git a/ui/app/pages/confirm-add-token/confirm-add-token.component.js b/ui/app/pages/confirm-add-token/confirm-add-token.component.js
new file mode 100644
index 000000000..c0ec624ac
--- /dev/null
+++ b/ui/app/pages/confirm-add-token/confirm-add-token.component.js
@@ -0,0 +1,117 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import { DEFAULT_ROUTE, ADD_TOKEN_ROUTE } from '../../helpers/constants/routes'
+import Button from '../../components/ui/button'
+import Identicon from '../../components/ui/identicon'
+import TokenBalance from '../../components/ui/token-balance'
+
+export default class ConfirmAddToken extends Component {
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ static propTypes = {
+ history: PropTypes.object,
+ clearPendingTokens: PropTypes.func,
+ addTokens: PropTypes.func,
+ pendingTokens: PropTypes.object,
+ }
+
+ componentDidMount () {
+ const { pendingTokens = {}, history } = this.props
+
+ if (Object.keys(pendingTokens).length === 0) {
+ history.push(DEFAULT_ROUTE)
+ }
+ }
+
+ getTokenName (name, symbol) {
+ return typeof name === 'undefined'
+ ? symbol
+ : `${name} (${symbol})`
+ }
+
+ render () {
+ const { history, addTokens, clearPendingTokens, pendingTokens } = this.props
+
+ return (
+ <div className="page-container">
+ <div className="page-container__header">
+ <div className="page-container__title">
+ { this.context.t('addTokens') }
+ </div>
+ <div className="page-container__subtitle">
+ { this.context.t('likeToAddTokens') }
+ </div>
+ </div>
+ <div className="page-container__content">
+ <div className="confirm-add-token">
+ <div className="confirm-add-token__header">
+ <div className="confirm-add-token__token">
+ { this.context.t('token') }
+ </div>
+ <div className="confirm-add-token__balance">
+ { this.context.t('balance') }
+ </div>
+ </div>
+ <div className="confirm-add-token__token-list">
+ {
+ Object.entries(pendingTokens)
+ .map(([ address, token ]) => {
+ const { name, symbol } = token
+
+ return (
+ <div
+ className="confirm-add-token__token-list-item"
+ key={address}
+ >
+ <div className="confirm-add-token__token confirm-add-token__data">
+ <Identicon
+ className="confirm-add-token__token-icon"
+ diameter={48}
+ address={address}
+ />
+ <div className="confirm-add-token__name">
+ { this.getTokenName(name, symbol) }
+ </div>
+ </div>
+ <div className="confirm-add-token__balance">
+ <TokenBalance token={token} />
+ </div>
+ </div>
+ )
+ })
+ }
+ </div>
+ </div>
+ </div>
+ <div className="page-container__footer">
+ <header>
+ <Button
+ type="default"
+ large
+ className="page-container__footer-button"
+ onClick={() => history.push(ADD_TOKEN_ROUTE)}
+ >
+ { this.context.t('back') }
+ </Button>
+ <Button
+ type="primary"
+ large
+ className="page-container__footer-button"
+ onClick={() => {
+ addTokens(pendingTokens)
+ .then(() => {
+ clearPendingTokens()
+ history.push(DEFAULT_ROUTE)
+ })
+ }}
+ >
+ { this.context.t('addTokens') }
+ </Button>
+ </header>
+ </div>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/pages/confirm-add-token/confirm-add-token.container.js b/ui/app/pages/confirm-add-token/confirm-add-token.container.js
new file mode 100644
index 000000000..961626177
--- /dev/null
+++ b/ui/app/pages/confirm-add-token/confirm-add-token.container.js
@@ -0,0 +1,20 @@
+import { connect } from 'react-redux'
+import ConfirmAddToken from './confirm-add-token.component'
+
+const { addTokens, clearPendingTokens } = require('../../store/actions')
+
+const mapStateToProps = ({ metamask }) => {
+ const { pendingTokens } = metamask
+ return {
+ pendingTokens,
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ addTokens: tokens => dispatch(addTokens(tokens)),
+ clearPendingTokens: () => dispatch(clearPendingTokens()),
+ }
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(ConfirmAddToken)
diff --git a/ui/app/pages/confirm-add-token/index.js b/ui/app/pages/confirm-add-token/index.js
new file mode 100644
index 000000000..b7decabec
--- /dev/null
+++ b/ui/app/pages/confirm-add-token/index.js
@@ -0,0 +1,2 @@
+import ConfirmAddToken from './confirm-add-token.container'
+module.exports = ConfirmAddToken
diff --git a/ui/app/pages/confirm-add-token/index.scss b/ui/app/pages/confirm-add-token/index.scss
new file mode 100644
index 000000000..66146cf78
--- /dev/null
+++ b/ui/app/pages/confirm-add-token/index.scss
@@ -0,0 +1,69 @@
+.confirm-add-token {
+ padding: 16px;
+
+ &__header {
+ font-size: .75rem;
+ display: flex;
+ }
+
+ &__token {
+ flex: 1;
+ min-width: 0;
+ }
+
+ &__balance {
+ flex: 0 0 30%;
+ min-width: 0;
+ }
+
+ &__token-list {
+ display: flex;
+ flex-flow: column nowrap;
+
+ .token-balance {
+ display: flex;
+ flex-flow: row nowrap;
+ align-items: flex-start;
+
+ &__amount {
+ color: $scorpion;
+ font-size: 43px;
+ line-height: 43px;
+ margin-right: 8px;
+ }
+
+ &__symbol {
+ color: $scorpion;
+ font-size: 16px;
+ font-weight: 400;
+ line-height: 24px;
+ }
+ }
+ }
+
+ &__token-list-item {
+ display: flex;
+ flex-flow: row nowrap;
+ align-items: center;
+ margin-top: 8px;
+ box-sizing: border-box;
+ }
+
+ &__data {
+ display: flex;
+ align-items: center;
+ padding: 8px;
+ }
+
+ &__name {
+ min-width: 0;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ &__token-icon {
+ margin-right: 12px;
+ flex: 0 0 auto;
+ }
+}
diff --git a/ui/app/pages/confirm-approve/confirm-approve.component.js b/ui/app/pages/confirm-approve/confirm-approve.component.js
new file mode 100644
index 000000000..b71eaa1d4
--- /dev/null
+++ b/ui/app/pages/confirm-approve/confirm-approve.component.js
@@ -0,0 +1,21 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import ConfirmTokenTransactionBase from '../confirm-token-transaction-base'
+
+export default class ConfirmApprove extends Component {
+ static propTypes = {
+ tokenAmount: PropTypes.number,
+ tokenSymbol: PropTypes.string,
+ }
+
+ render () {
+ const { tokenAmount, tokenSymbol } = this.props
+
+ return (
+ <ConfirmTokenTransactionBase
+ tokenAmount={tokenAmount}
+ warning={`By approving this action, you grant permission for this contract to spend up to ${tokenAmount} of your ${tokenSymbol}.`}
+ />
+ )
+ }
+}
diff --git a/ui/app/pages/confirm-approve/confirm-approve.container.js b/ui/app/pages/confirm-approve/confirm-approve.container.js
new file mode 100644
index 000000000..5f8bb8f0b
--- /dev/null
+++ b/ui/app/pages/confirm-approve/confirm-approve.container.js
@@ -0,0 +1,15 @@
+import { connect } from 'react-redux'
+import ConfirmApprove from './confirm-approve.component'
+import { approveTokenAmountAndToAddressSelector } from '../../selectors/confirm-transaction'
+
+const mapStateToProps = state => {
+ const { confirmTransaction: { tokenProps: { tokenSymbol } = {} } } = state
+ const { tokenAmount } = approveTokenAmountAndToAddressSelector(state)
+
+ return {
+ tokenAmount,
+ tokenSymbol,
+ }
+}
+
+export default connect(mapStateToProps)(ConfirmApprove)
diff --git a/ui/app/pages/confirm-approve/index.js b/ui/app/pages/confirm-approve/index.js
new file mode 100644
index 000000000..791297be7
--- /dev/null
+++ b/ui/app/pages/confirm-approve/index.js
@@ -0,0 +1 @@
+export { default } from './confirm-approve.container'
diff --git a/ui/app/pages/confirm-deploy-contract/confirm-deploy-contract.component.js b/ui/app/pages/confirm-deploy-contract/confirm-deploy-contract.component.js
new file mode 100644
index 000000000..9bc0daab9
--- /dev/null
+++ b/ui/app/pages/confirm-deploy-contract/confirm-deploy-contract.component.js
@@ -0,0 +1,64 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import ethUtil from 'ethereumjs-util'
+import ConfirmTransactionBase from '../confirm-transaction-base'
+
+export default class ConfirmDeployContract extends Component {
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ static propTypes = {
+ txData: PropTypes.object,
+ }
+
+ renderData () {
+ const { t } = this.context
+ const {
+ txData: {
+ origin,
+ txParams: {
+ data,
+ } = {},
+ } = {},
+ } = this.props
+
+ return (
+ <div className="confirm-page-container-content__data">
+ <div className="confirm-page-container-content__data-box">
+ <div className="confirm-page-container-content__data-field">
+ <div className="confirm-page-container-content__data-field-label">
+ { `${t('origin')}:` }
+ </div>
+ <div>
+ { origin }
+ </div>
+ </div>
+ <div className="confirm-page-container-content__data-field">
+ <div className="confirm-page-container-content__data-field-label">
+ { `${t('bytes')}:` }
+ </div>
+ <div>
+ { ethUtil.toBuffer(data).length }
+ </div>
+ </div>
+ </div>
+ <div className="confirm-page-container-content__data-box-label">
+ { `${t('hexData')}:` }
+ </div>
+ <div className="confirm-page-container-content__data-box">
+ { data }
+ </div>
+ </div>
+ )
+ }
+
+ render () {
+ return (
+ <ConfirmTransactionBase
+ action={this.context.t('contractDeployment')}
+ dataComponent={this.renderData()}
+ />
+ )
+ }
+}
diff --git a/ui/app/pages/confirm-deploy-contract/confirm-deploy-contract.container.js b/ui/app/pages/confirm-deploy-contract/confirm-deploy-contract.container.js
new file mode 100644
index 000000000..336ee83ea
--- /dev/null
+++ b/ui/app/pages/confirm-deploy-contract/confirm-deploy-contract.container.js
@@ -0,0 +1,12 @@
+import { connect } from 'react-redux'
+import ConfirmDeployContract from './confirm-deploy-contract.component'
+
+const mapStateToProps = state => {
+ const { confirmTransaction: { txData } = {} } = state
+
+ return {
+ txData,
+ }
+}
+
+export default connect(mapStateToProps)(ConfirmDeployContract)
diff --git a/ui/app/pages/confirm-deploy-contract/index.js b/ui/app/pages/confirm-deploy-contract/index.js
new file mode 100644
index 000000000..c4fb01b52
--- /dev/null
+++ b/ui/app/pages/confirm-deploy-contract/index.js
@@ -0,0 +1 @@
+export { default } from './confirm-deploy-contract.container'
diff --git a/ui/app/pages/confirm-send-ether/confirm-send-ether.component.js b/ui/app/pages/confirm-send-ether/confirm-send-ether.component.js
new file mode 100644
index 000000000..8daad675e
--- /dev/null
+++ b/ui/app/pages/confirm-send-ether/confirm-send-ether.component.js
@@ -0,0 +1,39 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import ConfirmTransactionBase from '../confirm-transaction-base'
+import { SEND_ROUTE } from '../../helpers/constants/routes'
+
+export default class ConfirmSendEther extends Component {
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ static propTypes = {
+ editTransaction: PropTypes.func,
+ history: PropTypes.object,
+ txParams: PropTypes.object,
+ }
+
+ handleEdit ({ txData }) {
+ const { editTransaction, history } = this.props
+ editTransaction(txData)
+ history.push(SEND_ROUTE)
+ }
+
+ shouldHideData () {
+ const { txParams = {} } = this.props
+ return !txParams.data
+ }
+
+ render () {
+ const hideData = this.shouldHideData()
+
+ return (
+ <ConfirmTransactionBase
+ action={this.context.t('confirm')}
+ hideData={hideData}
+ onEdit={confirmTransactionData => this.handleEdit(confirmTransactionData)}
+ />
+ )
+ }
+}
diff --git a/ui/app/pages/confirm-send-ether/confirm-send-ether.container.js b/ui/app/pages/confirm-send-ether/confirm-send-ether.container.js
new file mode 100644
index 000000000..713da702d
--- /dev/null
+++ b/ui/app/pages/confirm-send-ether/confirm-send-ether.container.js
@@ -0,0 +1,45 @@
+import { connect } from 'react-redux'
+import { compose } from 'recompose'
+import { withRouter } from 'react-router-dom'
+import { updateSend } from '../../store/actions'
+import { clearConfirmTransaction } from '../../ducks/confirm-transaction/confirm-transaction.duck'
+import ConfirmSendEther from './confirm-send-ether.component'
+
+const mapStateToProps = state => {
+ const { confirmTransaction: { txData: { txParams } = {} } } = state
+
+ return {
+ txParams,
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ editTransaction: txData => {
+ const { id, txParams } = txData
+ const {
+ gas: gasLimit,
+ gasPrice,
+ to,
+ value: amount,
+ } = txParams
+
+ dispatch(updateSend({
+ gasLimit,
+ gasPrice,
+ gasTotal: null,
+ to,
+ amount,
+ errors: { to: null, amount: null },
+ editingTransactionId: id && id.toString(),
+ }))
+
+ dispatch(clearConfirmTransaction())
+ },
+ }
+}
+
+export default compose(
+ withRouter,
+ connect(mapStateToProps, mapDispatchToProps)
+)(ConfirmSendEther)
diff --git a/ui/app/pages/confirm-send-ether/index.js b/ui/app/pages/confirm-send-ether/index.js
new file mode 100644
index 000000000..2d5767c39
--- /dev/null
+++ b/ui/app/pages/confirm-send-ether/index.js
@@ -0,0 +1 @@
+export { default } from './confirm-send-ether.container'
diff --git a/ui/app/pages/confirm-send-token/confirm-send-token.component.js b/ui/app/pages/confirm-send-token/confirm-send-token.component.js
new file mode 100644
index 000000000..7f3b1c082
--- /dev/null
+++ b/ui/app/pages/confirm-send-token/confirm-send-token.component.js
@@ -0,0 +1,29 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import ConfirmTokenTransactionBase from '../confirm-token-transaction-base'
+import { SEND_ROUTE } from '../../helpers/constants/routes'
+
+export default class ConfirmSendToken extends Component {
+ static propTypes = {
+ history: PropTypes.object,
+ editTransaction: PropTypes.func,
+ tokenAmount: PropTypes.number,
+ }
+
+ handleEdit (confirmTransactionData) {
+ const { editTransaction, history } = this.props
+ editTransaction(confirmTransactionData)
+ history.push(SEND_ROUTE)
+ }
+
+ render () {
+ const { tokenAmount } = this.props
+
+ return (
+ <ConfirmTokenTransactionBase
+ onEdit={confirmTransactionData => this.handleEdit(confirmTransactionData)}
+ tokenAmount={tokenAmount}
+ />
+ )
+ }
+}
diff --git a/ui/app/pages/confirm-send-token/confirm-send-token.container.js b/ui/app/pages/confirm-send-token/confirm-send-token.container.js
new file mode 100644
index 000000000..db9b08c48
--- /dev/null
+++ b/ui/app/pages/confirm-send-token/confirm-send-token.container.js
@@ -0,0 +1,52 @@
+import { connect } from 'react-redux'
+import { compose } from 'recompose'
+import { withRouter } from 'react-router-dom'
+import ConfirmSendToken from './confirm-send-token.component'
+import { clearConfirmTransaction } from '../../ducks/confirm-transaction/confirm-transaction.duck'
+import { setSelectedToken, updateSend, showSendTokenPage } from '../../store/actions'
+import { conversionUtil } from '../../helpers/utils/conversion-util'
+import { sendTokenTokenAmountAndToAddressSelector } from '../../selectors/confirm-transaction'
+
+const mapStateToProps = state => {
+ const { tokenAmount } = sendTokenTokenAmountAndToAddressSelector(state)
+
+ return {
+ tokenAmount,
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ editTransaction: ({ txData, tokenData, tokenProps }) => {
+ const { txParams: { to: tokenAddress, gas: gasLimit, gasPrice } = {}, id } = txData
+ const { params = [] } = tokenData
+ const { value: to } = params[0] || {}
+ const { value: tokenAmountInDec } = params[1] || {}
+ const tokenAmountInHex = conversionUtil(tokenAmountInDec, {
+ fromNumericBase: 'dec',
+ toNumericBase: 'hex',
+ })
+ dispatch(setSelectedToken(tokenAddress))
+ dispatch(updateSend({
+ gasLimit,
+ gasPrice,
+ gasTotal: null,
+ to,
+ amount: tokenAmountInHex,
+ errors: { to: null, amount: null },
+ editingTransactionId: id && id.toString(),
+ token: {
+ ...tokenProps,
+ address: tokenAddress,
+ },
+ }))
+ dispatch(clearConfirmTransaction())
+ dispatch(showSendTokenPage())
+ },
+ }
+}
+
+export default compose(
+ withRouter,
+ connect(mapStateToProps, mapDispatchToProps)
+)(ConfirmSendToken)
diff --git a/ui/app/pages/confirm-send-token/index.js b/ui/app/pages/confirm-send-token/index.js
new file mode 100644
index 000000000..409b6ef3d
--- /dev/null
+++ b/ui/app/pages/confirm-send-token/index.js
@@ -0,0 +1 @@
+export { default } from './confirm-send-token.container'
diff --git a/ui/app/pages/confirm-token-transaction-base/confirm-token-transaction-base.component.js b/ui/app/pages/confirm-token-transaction-base/confirm-token-transaction-base.component.js
new file mode 100644
index 000000000..dbda3c1dc
--- /dev/null
+++ b/ui/app/pages/confirm-token-transaction-base/confirm-token-transaction-base.component.js
@@ -0,0 +1,119 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import ConfirmTransactionBase from '../confirm-transaction-base'
+import UserPreferencedCurrencyDisplay from '../../components/app/user-preferenced-currency-display'
+import {
+ formatCurrency,
+ convertTokenToFiat,
+ addFiat,
+ roundExponential,
+} from '../../helpers/utils/confirm-tx.util'
+import { getWeiHexFromDecimalValue } from '../../helpers/utils/conversions.util'
+import { ETH, PRIMARY } from '../../helpers/constants/common'
+
+export default class ConfirmTokenTransactionBase extends Component {
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ static propTypes = {
+ tokenAddress: PropTypes.string,
+ toAddress: PropTypes.string,
+ tokenAmount: PropTypes.number,
+ tokenSymbol: PropTypes.string,
+ fiatTransactionTotal: PropTypes.string,
+ ethTransactionTotal: PropTypes.string,
+ contractExchangeRate: PropTypes.number,
+ conversionRate: PropTypes.number,
+ currentCurrency: PropTypes.string,
+ }
+
+ getFiatTransactionAmount () {
+ const { tokenAmount, currentCurrency, conversionRate, contractExchangeRate } = this.props
+
+ return convertTokenToFiat({
+ value: tokenAmount,
+ toCurrency: currentCurrency,
+ conversionRate,
+ contractExchangeRate,
+ })
+ }
+
+ renderSubtitleComponent () {
+ const { contractExchangeRate, tokenAmount } = this.props
+
+ const decimalEthValue = (tokenAmount * contractExchangeRate) || 0
+ const hexWeiValue = getWeiHexFromDecimalValue({
+ value: decimalEthValue,
+ fromCurrency: ETH,
+ fromDenomination: ETH,
+ })
+
+ return typeof contractExchangeRate === 'undefined'
+ ? (
+ <span>
+ { this.context.t('noConversionRateAvailable') }
+ </span>
+ ) : (
+ <UserPreferencedCurrencyDisplay
+ value={hexWeiValue}
+ type={PRIMARY}
+ showEthLogo
+ hideLabel
+ />
+ )
+ }
+
+ renderPrimaryTotalTextOverride () {
+ const { tokenAmount, tokenSymbol, ethTransactionTotal } = this.props
+ const tokensText = `${tokenAmount} ${tokenSymbol}`
+
+ return (
+ <div>
+ <span>{ `${tokensText} + ` }</span>
+ <img
+ src="/images/eth.svg"
+ height="18"
+ />
+ <span>{ ethTransactionTotal }</span>
+ </div>
+ )
+ }
+
+ getSecondaryTotalTextOverride () {
+ const { fiatTransactionTotal, currentCurrency, contractExchangeRate } = this.props
+
+ if (typeof contractExchangeRate === 'undefined') {
+ return formatCurrency(fiatTransactionTotal, currentCurrency)
+ } else {
+ const fiatTransactionAmount = this.getFiatTransactionAmount()
+ const fiatTotal = addFiat(fiatTransactionAmount, fiatTransactionTotal)
+ const roundedFiatTotal = roundExponential(fiatTotal)
+ return formatCurrency(roundedFiatTotal, currentCurrency)
+ }
+ }
+
+ render () {
+ const {
+ toAddress,
+ tokenAddress,
+ tokenSymbol,
+ tokenAmount,
+ ...restProps
+ } = this.props
+
+ const tokensText = `${tokenAmount} ${tokenSymbol}`
+
+ return (
+ <ConfirmTransactionBase
+ toAddress={toAddress}
+ identiconAddress={tokenAddress}
+ title={tokensText}
+ subtitleComponent={this.renderSubtitleComponent()}
+ primaryTotalTextOverride={this.renderPrimaryTotalTextOverride()}
+ secondaryTotalTextOverride={this.getSecondaryTotalTextOverride()}
+ {...restProps}
+ />
+ )
+ }
+}
diff --git a/ui/app/pages/confirm-token-transaction-base/confirm-token-transaction-base.container.js b/ui/app/pages/confirm-token-transaction-base/confirm-token-transaction-base.container.js
new file mode 100644
index 000000000..f5f30a460
--- /dev/null
+++ b/ui/app/pages/confirm-token-transaction-base/confirm-token-transaction-base.container.js
@@ -0,0 +1,34 @@
+import { connect } from 'react-redux'
+import ConfirmTokenTransactionBase from './confirm-token-transaction-base.component'
+import {
+ tokenAmountAndToAddressSelector,
+ contractExchangeRateSelector,
+} from '../../selectors/confirm-transaction'
+
+const mapStateToProps = (state, ownProps) => {
+ const { tokenAmount: ownTokenAmount } = ownProps
+ const { confirmTransaction, metamask: { currentCurrency, conversionRate } } = state
+ const {
+ txData: { txParams: { to: tokenAddress } = {} } = {},
+ tokenProps: { tokenSymbol } = {},
+ fiatTransactionTotal,
+ ethTransactionTotal,
+ } = confirmTransaction
+
+ const { tokenAmount, toAddress } = tokenAmountAndToAddressSelector(state)
+ const contractExchangeRate = contractExchangeRateSelector(state)
+
+ return {
+ toAddress,
+ tokenAddress,
+ tokenAmount: typeof ownTokenAmount !== 'undefined' ? ownTokenAmount : tokenAmount,
+ tokenSymbol,
+ currentCurrency,
+ conversionRate,
+ contractExchangeRate,
+ fiatTransactionTotal,
+ ethTransactionTotal,
+ }
+}
+
+export default connect(mapStateToProps)(ConfirmTokenTransactionBase)
diff --git a/ui/app/pages/confirm-token-transaction-base/index.js b/ui/app/pages/confirm-token-transaction-base/index.js
new file mode 100644
index 000000000..e15c5d56b
--- /dev/null
+++ b/ui/app/pages/confirm-token-transaction-base/index.js
@@ -0,0 +1,2 @@
+export { default } from './confirm-token-transaction-base.container'
+export { default as ConfirmTokenTransactionBase } from './confirm-token-transaction-base.component'
diff --git a/ui/app/pages/confirm-transaction-base/confirm-transaction-base.component.js b/ui/app/pages/confirm-transaction-base/confirm-transaction-base.component.js
new file mode 100644
index 000000000..1da9c34bd
--- /dev/null
+++ b/ui/app/pages/confirm-transaction-base/confirm-transaction-base.component.js
@@ -0,0 +1,574 @@
+import ethUtil from 'ethereumjs-util'
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import ConfirmPageContainer, { ConfirmDetailRow } from '../../components/app/confirm-page-container'
+import { isBalanceSufficient } from '../../components/app/send/send.utils'
+import { DEFAULT_ROUTE, CONFIRM_TRANSACTION_ROUTE } from '../../helpers/constants/routes'
+import {
+ INSUFFICIENT_FUNDS_ERROR_KEY,
+ TRANSACTION_ERROR_KEY,
+} from '../../helpers/constants/error-keys'
+import { CONFIRMED_STATUS, DROPPED_STATUS } from '../../helpers/constants/transactions'
+import UserPreferencedCurrencyDisplay from '../../components/app/user-preferenced-currency-display'
+import { PRIMARY, SECONDARY } from '../../helpers/constants/common'
+import AdvancedGasInputs from '../../components/app/gas-customization/advanced-gas-inputs'
+
+export default class ConfirmTransactionBase extends Component {
+ static contextTypes = {
+ t: PropTypes.func,
+ metricsEvent: PropTypes.func,
+ }
+
+ static propTypes = {
+ // react-router props
+ match: PropTypes.object,
+ history: PropTypes.object,
+ // Redux props
+ balance: PropTypes.string,
+ cancelTransaction: PropTypes.func,
+ cancelAllTransactions: PropTypes.func,
+ clearConfirmTransaction: PropTypes.func,
+ clearSend: PropTypes.func,
+ conversionRate: PropTypes.number,
+ currentCurrency: PropTypes.string,
+ editTransaction: PropTypes.func,
+ ethTransactionAmount: PropTypes.string,
+ ethTransactionFee: PropTypes.string,
+ ethTransactionTotal: PropTypes.string,
+ fiatTransactionAmount: PropTypes.string,
+ fiatTransactionFee: PropTypes.string,
+ fiatTransactionTotal: PropTypes.string,
+ fromAddress: PropTypes.string,
+ fromName: PropTypes.string,
+ hexTransactionAmount: PropTypes.string,
+ hexTransactionFee: PropTypes.string,
+ hexTransactionTotal: PropTypes.string,
+ isTxReprice: PropTypes.bool,
+ methodData: PropTypes.object,
+ nonce: PropTypes.string,
+ assetImage: PropTypes.string,
+ sendTransaction: PropTypes.func,
+ showCustomizeGasModal: PropTypes.func,
+ showTransactionConfirmedModal: PropTypes.func,
+ showRejectTransactionsConfirmationModal: PropTypes.func,
+ toAddress: PropTypes.string,
+ tokenData: PropTypes.object,
+ tokenProps: PropTypes.object,
+ toName: PropTypes.string,
+ transactionStatus: PropTypes.string,
+ txData: PropTypes.object,
+ unapprovedTxCount: PropTypes.number,
+ currentNetworkUnapprovedTxs: PropTypes.object,
+ updateGasAndCalculate: PropTypes.func,
+ customGas: PropTypes.object,
+ // Component props
+ action: PropTypes.string,
+ contentComponent: PropTypes.node,
+ dataComponent: PropTypes.node,
+ detailsComponent: PropTypes.node,
+ errorKey: PropTypes.string,
+ errorMessage: PropTypes.string,
+ primaryTotalTextOverride: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
+ secondaryTotalTextOverride: PropTypes.string,
+ hideData: PropTypes.bool,
+ hideDetails: PropTypes.bool,
+ hideSubtitle: PropTypes.bool,
+ identiconAddress: PropTypes.string,
+ onCancel: PropTypes.func,
+ onEdit: PropTypes.func,
+ onEditGas: PropTypes.func,
+ onSubmit: PropTypes.func,
+ setMetaMetricsSendCount: PropTypes.func,
+ metaMetricsSendCount: PropTypes.number,
+ subtitle: PropTypes.string,
+ subtitleComponent: PropTypes.node,
+ summaryComponent: PropTypes.node,
+ title: PropTypes.string,
+ titleComponent: PropTypes.node,
+ valid: PropTypes.bool,
+ warning: PropTypes.string,
+ advancedInlineGasShown: PropTypes.bool,
+ insufficientBalance: PropTypes.bool,
+ hideFiatConversion: PropTypes.bool,
+ }
+
+ state = {
+ submitting: false,
+ submitError: null,
+ }
+
+ componentDidUpdate () {
+ const {
+ transactionStatus,
+ showTransactionConfirmedModal,
+ history,
+ clearConfirmTransaction,
+ } = this.props
+
+ if (transactionStatus === DROPPED_STATUS || transactionStatus === CONFIRMED_STATUS) {
+ showTransactionConfirmedModal({
+ onSubmit: () => {
+ clearConfirmTransaction()
+ history.push(DEFAULT_ROUTE)
+ },
+ })
+
+ return
+ }
+ }
+
+ getErrorKey () {
+ const {
+ balance,
+ conversionRate,
+ hexTransactionFee,
+ txData: {
+ simulationFails,
+ txParams: {
+ value: amount,
+ } = {},
+ } = {},
+ } = this.props
+
+ const insufficientBalance = balance && !isBalanceSufficient({
+ amount,
+ gasTotal: hexTransactionFee || '0x0',
+ balance,
+ conversionRate,
+ })
+
+ if (insufficientBalance) {
+ return {
+ valid: false,
+ errorKey: INSUFFICIENT_FUNDS_ERROR_KEY,
+ }
+ }
+
+ if (simulationFails) {
+ return {
+ valid: true,
+ errorKey: simulationFails.errorKey ? simulationFails.errorKey : TRANSACTION_ERROR_KEY,
+ }
+ }
+
+ return {
+ valid: true,
+ }
+ }
+
+ handleEditGas () {
+ const { onEditGas, showCustomizeGasModal, action, txData: { origin }, methodData = {} } = this.props
+
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Transactions',
+ action: 'Confirm Screen',
+ name: 'User clicks "Edit" on gas',
+ },
+ customVariables: {
+ recipientKnown: null,
+ functionType: action || getMethodName(methodData.name) || this.context.t('contractInteraction'),
+ origin,
+ },
+ })
+
+ if (onEditGas) {
+ onEditGas()
+ } else {
+ showCustomizeGasModal()
+ }
+ }
+
+ renderDetails () {
+ const {
+ detailsComponent,
+ primaryTotalTextOverride,
+ secondaryTotalTextOverride,
+ hexTransactionFee,
+ hexTransactionTotal,
+ hideDetails,
+ advancedInlineGasShown,
+ customGas,
+ insufficientBalance,
+ updateGasAndCalculate,
+ hideFiatConversion,
+ } = this.props
+
+ if (hideDetails) {
+ return null
+ }
+
+ return (
+ detailsComponent || (
+ <div className="confirm-page-container-content__details">
+ <div className="confirm-page-container-content__gas-fee">
+ <ConfirmDetailRow
+ label="Gas Fee"
+ value={hexTransactionFee}
+ headerText="Edit"
+ headerTextClassName="confirm-detail-row__header-text--edit"
+ onHeaderClick={() => this.handleEditGas()}
+ secondaryText={hideFiatConversion ? this.context.t('noConversionRateAvailable') : ''}
+ />
+ {advancedInlineGasShown
+ ? <AdvancedGasInputs
+ updateCustomGasPrice={newGasPrice => updateGasAndCalculate({ ...customGas, gasPrice: newGasPrice })}
+ updateCustomGasLimit={newGasLimit => updateGasAndCalculate({ ...customGas, gasLimit: newGasLimit })}
+ customGasPrice={customGas.gasPrice}
+ customGasLimit={customGas.gasLimit}
+ insufficientBalance={insufficientBalance}
+ customPriceIsSafe={true}
+ isSpeedUp={false}
+ />
+ : null
+ }
+ </div>
+ <div>
+ <ConfirmDetailRow
+ label="Total"
+ value={hexTransactionTotal}
+ primaryText={primaryTotalTextOverride}
+ secondaryText={hideFiatConversion ? this.context.t('noConversionRateAvailable') : secondaryTotalTextOverride}
+ headerText="Amount + Gas Fee"
+ headerTextClassName="confirm-detail-row__header-text--total"
+ primaryValueTextColor="#2f9ae0"
+ />
+ </div>
+ </div>
+ )
+ )
+ }
+
+ renderData () {
+ const { t } = this.context
+ const {
+ txData: {
+ txParams: {
+ data,
+ } = {},
+ } = {},
+ methodData: {
+ name,
+ params,
+ } = {},
+ hideData,
+ dataComponent,
+ } = this.props
+
+ if (hideData) {
+ return null
+ }
+
+ return dataComponent || (
+ <div className="confirm-page-container-content__data">
+ <div className="confirm-page-container-content__data-box-label">
+ {`${t('functionType')}:`}
+ <span className="confirm-page-container-content__function-type">
+ { name || t('notFound') }
+ </span>
+ </div>
+ {
+ params && (
+ <div className="confirm-page-container-content__data-box">
+ <div className="confirm-page-container-content__data-field-label">
+ { `${t('parameters')}:` }
+ </div>
+ <div>
+ <pre>{ JSON.stringify(params, null, 2) }</pre>
+ </div>
+ </div>
+ )
+ }
+ <div className="confirm-page-container-content__data-box-label">
+ {`${t('hexData')}: ${ethUtil.toBuffer(data).length} bytes`}
+ </div>
+ <div className="confirm-page-container-content__data-box">
+ { data }
+ </div>
+ </div>
+ )
+ }
+
+ handleEdit () {
+ const { txData, tokenData, tokenProps, onEdit, action, txData: { origin }, methodData = {} } = this.props
+
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Transactions',
+ action: 'Confirm Screen',
+ name: 'Edit Transaction',
+ },
+ customVariables: {
+ recipientKnown: null,
+ functionType: action || getMethodName(methodData.name) || this.context.t('contractInteraction'),
+ origin,
+ },
+ })
+
+ onEdit({ txData, tokenData, tokenProps })
+ }
+
+ handleCancelAll () {
+ const {
+ cancelAllTransactions,
+ clearConfirmTransaction,
+ history,
+ showRejectTransactionsConfirmationModal,
+ unapprovedTxCount,
+ } = this.props
+
+ showRejectTransactionsConfirmationModal({
+ unapprovedTxCount,
+ async onSubmit () {
+ await cancelAllTransactions()
+ clearConfirmTransaction()
+ history.push(DEFAULT_ROUTE)
+ },
+ })
+ }
+
+ handleCancel () {
+ const { metricsEvent } = this.context
+ const { onCancel, txData, cancelTransaction, history, clearConfirmTransaction, action, txData: { origin }, methodData = {} } = this.props
+
+ if (onCancel) {
+ metricsEvent({
+ eventOpts: {
+ category: 'Transactions',
+ action: 'Confirm Screen',
+ name: 'Cancel',
+ },
+ customVariables: {
+ recipientKnown: null,
+ functionType: action || getMethodName(methodData.name) || this.context.t('contractInteraction'),
+ origin,
+ },
+ })
+ onCancel(txData)
+ } else {
+ cancelTransaction(txData)
+ .then(() => {
+ clearConfirmTransaction()
+ history.push(DEFAULT_ROUTE)
+ })
+ }
+ }
+
+ handleSubmit () {
+ const { metricsEvent } = this.context
+ const { txData: { origin }, sendTransaction, clearConfirmTransaction, txData, history, onSubmit, action, metaMetricsSendCount = 0, setMetaMetricsSendCount, methodData = {} } = this.props
+ const { submitting } = this.state
+
+ if (submitting) {
+ return
+ }
+
+ this.setState({
+ submitting: true,
+ submitError: null,
+ }, () => {
+ metricsEvent({
+ eventOpts: {
+ category: 'Transactions',
+ action: 'Confirm Screen',
+ name: 'Transaction Completed',
+ },
+ customVariables: {
+ recipientKnown: null,
+ functionType: action || getMethodName(methodData.name) || this.context.t('contractInteraction'),
+ origin,
+ },
+ })
+
+ setMetaMetricsSendCount(metaMetricsSendCount + 1)
+ .then(() => {
+ if (onSubmit) {
+ Promise.resolve(onSubmit(txData))
+ .then(() => {
+ this.setState({
+ submitting: false,
+ })
+ })
+ } else {
+ sendTransaction(txData)
+ .then(() => {
+ clearConfirmTransaction()
+ this.setState({
+ submitting: false,
+ }, () => {
+ history.push(DEFAULT_ROUTE)
+ })
+ })
+ .catch(error => {
+ this.setState({
+ submitting: false,
+ submitError: error.message,
+ })
+ })
+ }
+ })
+ })
+ }
+
+ renderTitleComponent () {
+ const { title, titleComponent, hexTransactionAmount } = this.props
+
+ // Title string passed in by props takes priority
+ if (title) {
+ return null
+ }
+
+ return titleComponent || (
+ <UserPreferencedCurrencyDisplay
+ value={hexTransactionAmount}
+ type={PRIMARY}
+ showEthLogo
+ ethLogoHeight="26"
+ hideLabel
+ />
+ )
+ }
+
+ renderSubtitleComponent () {
+ const { subtitle, subtitleComponent, hexTransactionAmount } = this.props
+
+ // Subtitle string passed in by props takes priority
+ if (subtitle) {
+ return null
+ }
+
+ return subtitleComponent || (
+ <UserPreferencedCurrencyDisplay
+ value={hexTransactionAmount}
+ type={SECONDARY}
+ showEthLogo
+ hideLabel
+ />
+ )
+ }
+
+ handleNextTx (txId) {
+ const { history, clearConfirmTransaction } = this.props
+ if (txId) {
+ clearConfirmTransaction()
+ history.push(`${CONFIRM_TRANSACTION_ROUTE}/${txId}`)
+ }
+ }
+
+ getNavigateTxData () {
+ const { currentNetworkUnapprovedTxs, txData: { id } = {} } = this.props
+ const enumUnapprovedTxs = Object.keys(currentNetworkUnapprovedTxs).reverse()
+ const currentPosition = enumUnapprovedTxs.indexOf(id.toString())
+
+ return {
+ totalTx: enumUnapprovedTxs.length,
+ positionOfCurrentTx: currentPosition + 1,
+ nextTxId: enumUnapprovedTxs[currentPosition + 1],
+ prevTxId: enumUnapprovedTxs[currentPosition - 1],
+ showNavigation: enumUnapprovedTxs.length > 1,
+ firstTx: enumUnapprovedTxs[0],
+ lastTx: enumUnapprovedTxs[enumUnapprovedTxs.length - 1],
+ ofText: this.context.t('ofTextNofM'),
+ requestsWaitingText: this.context.t('requestsAwaitingAcknowledgement'),
+ }
+ }
+
+ componentDidMount () {
+ const { txData: { origin } = {} } = this.props
+ const { metricsEvent } = this.context
+ metricsEvent({
+ eventOpts: {
+ category: 'Transactions',
+ action: 'Confirm Screen',
+ name: 'Confirm: Started',
+ },
+ customVariables: {
+ origin,
+ },
+ })
+ }
+
+ render () {
+ const {
+ isTxReprice,
+ fromName,
+ fromAddress,
+ toName,
+ toAddress,
+ methodData,
+ valid: propsValid = true,
+ errorMessage,
+ errorKey: propsErrorKey,
+ action,
+ title,
+ subtitle,
+ hideSubtitle,
+ identiconAddress,
+ summaryComponent,
+ contentComponent,
+ onEdit,
+ nonce,
+ assetImage,
+ warning,
+ unapprovedTxCount,
+ } = this.props
+ const { submitting, submitError } = this.state
+
+ const { name } = methodData
+ const { valid, errorKey } = this.getErrorKey()
+ const { totalTx, positionOfCurrentTx, nextTxId, prevTxId, showNavigation, firstTx, lastTx, ofText, requestsWaitingText } = this.getNavigateTxData()
+
+ return (
+ <ConfirmPageContainer
+ fromName={fromName}
+ fromAddress={fromAddress}
+ toName={toName}
+ toAddress={toAddress}
+ showEdit={onEdit && !isTxReprice}
+ action={action || getMethodName(name) || this.context.t('contractInteraction')}
+ title={title}
+ titleComponent={this.renderTitleComponent()}
+ subtitle={subtitle}
+ subtitleComponent={this.renderSubtitleComponent()}
+ hideSubtitle={hideSubtitle}
+ summaryComponent={summaryComponent}
+ detailsComponent={this.renderDetails()}
+ dataComponent={this.renderData()}
+ contentComponent={contentComponent}
+ nonce={nonce}
+ unapprovedTxCount={unapprovedTxCount}
+ assetImage={assetImage}
+ identiconAddress={identiconAddress}
+ errorMessage={errorMessage || submitError}
+ errorKey={propsErrorKey || errorKey}
+ warning={warning}
+ totalTx={totalTx}
+ positionOfCurrentTx={positionOfCurrentTx}
+ nextTxId={nextTxId}
+ prevTxId={prevTxId}
+ showNavigation={showNavigation}
+ onNextTx={(txId) => this.handleNextTx(txId)}
+ firstTx={firstTx}
+ lastTx={lastTx}
+ ofText={ofText}
+ requestsWaitingText={requestsWaitingText}
+ disabled={!propsValid || !valid || submitting}
+ onEdit={() => this.handleEdit()}
+ onCancelAll={() => this.handleCancelAll()}
+ onCancel={() => this.handleCancel()}
+ onSubmit={() => this.handleSubmit()}
+ />
+ )
+ }
+}
+
+export function getMethodName (camelCase) {
+ if (!camelCase || typeof camelCase !== 'string') {
+ return ''
+ }
+
+ return camelCase
+ .replace(/([a-z])([A-Z])/g, '$1 $2')
+ .replace(/([A-Z])([a-z])/g, ' $1$2')
+ .replace(/ +/g, ' ')
+}
diff --git a/ui/app/pages/confirm-transaction-base/confirm-transaction-base.container.js b/ui/app/pages/confirm-transaction-base/confirm-transaction-base.container.js
new file mode 100644
index 000000000..83543f1a4
--- /dev/null
+++ b/ui/app/pages/confirm-transaction-base/confirm-transaction-base.container.js
@@ -0,0 +1,242 @@
+import { connect } from 'react-redux'
+import { compose } from 'recompose'
+import { withRouter } from 'react-router-dom'
+import R from 'ramda'
+import contractMap from 'eth-contract-metadata'
+import ConfirmTransactionBase from './confirm-transaction-base.component'
+import {
+ clearConfirmTransaction,
+ updateGasAndCalculate,
+} from '../../ducks/confirm-transaction/confirm-transaction.duck'
+import { clearSend, cancelTx, cancelTxs, updateAndApproveTx, showModal, setMetaMetricsSendCount } from '../../store/actions'
+import {
+ INSUFFICIENT_FUNDS_ERROR_KEY,
+ GAS_LIMIT_TOO_LOW_ERROR_KEY,
+} from '../../helpers/constants/error-keys'
+import { getHexGasTotal } from '../../helpers/utils/confirm-tx.util'
+import { isBalanceSufficient, calcGasTotal } from '../../components/app/send/send.utils'
+import { conversionGreaterThan } from '../../helpers/utils/conversion-util'
+import { MIN_GAS_LIMIT_DEC } from '../../components/app/send/send.constants'
+import { checksumAddress, addressSlicer, valuesFor } from '../../helpers/utils/util'
+import {getMetaMaskAccounts, getAdvancedInlineGasShown, preferencesSelector, getIsMainnet} from '../../selectors/selectors'
+
+const casedContractMap = Object.keys(contractMap).reduce((acc, base) => {
+ return {
+ ...acc,
+ [base.toLowerCase()]: contractMap[base],
+ }
+}, {})
+
+const mapStateToProps = (state, props) => {
+ const { toAddress: propsToAddress } = props
+ const { showFiatInTestnets } = preferencesSelector(state)
+ const isMainnet = getIsMainnet(state)
+ const { confirmTransaction, metamask, gas } = state
+ const {
+ ethTransactionAmount,
+ ethTransactionFee,
+ ethTransactionTotal,
+ fiatTransactionAmount,
+ fiatTransactionFee,
+ fiatTransactionTotal,
+ hexTransactionAmount,
+ hexTransactionFee,
+ hexTransactionTotal,
+ tokenData,
+ methodData,
+ txData,
+ tokenProps,
+ nonce,
+ } = confirmTransaction
+ const { txParams = {}, lastGasPrice, id: transactionId } = txData
+ const {
+ from: fromAddress,
+ to: txParamsToAddress,
+ gasPrice,
+ gas: gasLimit,
+ value: amount,
+ } = txParams
+ const accounts = getMetaMaskAccounts(state)
+ const {
+ conversionRate,
+ identities,
+ currentCurrency,
+ selectedAddress,
+ selectedAddressTxList,
+ assetImages,
+ network,
+ unapprovedTxs,
+ metaMetricsSendCount,
+ } = metamask
+ const assetImage = assetImages[txParamsToAddress]
+
+ const {
+ customGasLimit,
+ customGasPrice,
+ } = gas
+
+ const { balance } = accounts[selectedAddress]
+ const { name: fromName } = identities[selectedAddress]
+ const toAddress = propsToAddress || txParamsToAddress
+ const toName = identities[toAddress]
+ ? identities[toAddress].name
+ : (
+ casedContractMap[toAddress]
+ ? casedContractMap[toAddress].name
+ : addressSlicer(checksumAddress(toAddress))
+ )
+
+ const isTxReprice = Boolean(lastGasPrice)
+
+ const transaction = R.find(({ id }) => id === transactionId)(selectedAddressTxList)
+ const transactionStatus = transaction ? transaction.status : ''
+
+ const currentNetworkUnapprovedTxs = R.filter(
+ ({ metamaskNetworkId }) => metamaskNetworkId === network,
+ unapprovedTxs,
+ )
+ const unapprovedTxCount = valuesFor(currentNetworkUnapprovedTxs).length
+
+ const insufficientBalance = !isBalanceSufficient({
+ amount,
+ gasTotal: calcGasTotal(gasLimit, gasPrice),
+ balance,
+ conversionRate,
+ })
+
+ return {
+ balance,
+ fromAddress,
+ fromName,
+ toAddress,
+ toName,
+ ethTransactionAmount,
+ ethTransactionFee,
+ ethTransactionTotal,
+ fiatTransactionAmount,
+ fiatTransactionFee,
+ fiatTransactionTotal,
+ hexTransactionAmount,
+ hexTransactionFee,
+ hexTransactionTotal,
+ txData,
+ tokenData,
+ methodData,
+ tokenProps,
+ isTxReprice,
+ currentCurrency,
+ conversionRate,
+ transactionStatus,
+ nonce,
+ assetImage,
+ unapprovedTxs,
+ unapprovedTxCount,
+ currentNetworkUnapprovedTxs,
+ customGas: {
+ gasLimit: customGasLimit || gasLimit,
+ gasPrice: customGasPrice || gasPrice,
+ },
+ advancedInlineGasShown: getAdvancedInlineGasShown(state),
+ insufficientBalance,
+ hideSubtitle: (!isMainnet && !showFiatInTestnets),
+ hideFiatConversion: (!isMainnet && !showFiatInTestnets),
+ metaMetricsSendCount,
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ clearConfirmTransaction: () => dispatch(clearConfirmTransaction()),
+ clearSend: () => dispatch(clearSend()),
+ showTransactionConfirmedModal: ({ onSubmit }) => {
+ return dispatch(showModal({ name: 'TRANSACTION_CONFIRMED', onSubmit }))
+ },
+ showCustomizeGasModal: ({ txData, onSubmit, validate }) => {
+ return dispatch(showModal({ name: 'CUSTOMIZE_GAS', txData, onSubmit, validate }))
+ },
+ updateGasAndCalculate: ({ gasLimit, gasPrice }) => {
+ return dispatch(updateGasAndCalculate({ gasLimit, gasPrice }))
+ },
+ showRejectTransactionsConfirmationModal: ({ onSubmit, unapprovedTxCount }) => {
+ return dispatch(showModal({ name: 'REJECT_TRANSACTIONS', onSubmit, unapprovedTxCount }))
+ },
+ cancelTransaction: ({ id }) => dispatch(cancelTx({ id })),
+ cancelAllTransactions: (txList) => dispatch(cancelTxs(txList)),
+ sendTransaction: txData => dispatch(updateAndApproveTx(txData)),
+ setMetaMetricsSendCount: val => dispatch(setMetaMetricsSendCount(val)),
+ }
+}
+
+const getValidateEditGas = ({ balance, conversionRate, txData }) => {
+ const { txParams: { value: amount } = {} } = txData
+
+ return ({ gasLimit, gasPrice }) => {
+ const gasTotal = getHexGasTotal({ gasLimit, gasPrice })
+ const hasSufficientBalance = isBalanceSufficient({
+ amount,
+ gasTotal,
+ balance,
+ conversionRate,
+ })
+
+ if (!hasSufficientBalance) {
+ return {
+ valid: false,
+ errorKey: INSUFFICIENT_FUNDS_ERROR_KEY,
+ }
+ }
+
+ const gasLimitTooLow = gasLimit && conversionGreaterThan(
+ {
+ value: MIN_GAS_LIMIT_DEC,
+ fromNumericBase: 'dec',
+ conversionRate,
+ },
+ {
+ value: gasLimit,
+ fromNumericBase: 'hex',
+ },
+ )
+
+ if (gasLimitTooLow) {
+ return {
+ valid: false,
+ errorKey: GAS_LIMIT_TOO_LOW_ERROR_KEY,
+ }
+ }
+
+ return {
+ valid: true,
+ }
+ }
+}
+
+const mergeProps = (stateProps, dispatchProps, ownProps) => {
+ const { balance, conversionRate, txData, unapprovedTxs } = stateProps
+ const {
+ cancelAllTransactions: dispatchCancelAllTransactions,
+ showCustomizeGasModal: dispatchShowCustomizeGasModal,
+ updateGasAndCalculate: dispatchUpdateGasAndCalculate,
+ ...otherDispatchProps
+ } = dispatchProps
+
+ const validateEditGas = getValidateEditGas({ balance, conversionRate, txData })
+
+ return {
+ ...stateProps,
+ ...otherDispatchProps,
+ ...ownProps,
+ showCustomizeGasModal: () => dispatchShowCustomizeGasModal({
+ txData,
+ onSubmit: customGas => dispatchUpdateGasAndCalculate(customGas),
+ validate: validateEditGas,
+ }),
+ cancelAllTransactions: () => dispatchCancelAllTransactions(valuesFor(unapprovedTxs)),
+ updateGasAndCalculate: dispatchUpdateGasAndCalculate,
+ }
+}
+
+export default compose(
+ withRouter,
+ connect(mapStateToProps, mapDispatchToProps, mergeProps)
+)(ConfirmTransactionBase)
diff --git a/ui/app/pages/confirm-transaction-base/index.js b/ui/app/pages/confirm-transaction-base/index.js
new file mode 100644
index 000000000..9996e9aeb
--- /dev/null
+++ b/ui/app/pages/confirm-transaction-base/index.js
@@ -0,0 +1 @@
+export { default } from './confirm-transaction-base.container'
diff --git a/ui/app/pages/confirm-transaction-base/tests/confirm-transaction-base.component.test.js b/ui/app/pages/confirm-transaction-base/tests/confirm-transaction-base.component.test.js
new file mode 100644
index 000000000..8ca7ca4e7
--- /dev/null
+++ b/ui/app/pages/confirm-transaction-base/tests/confirm-transaction-base.component.test.js
@@ -0,0 +1,14 @@
+import assert from 'assert'
+import { getMethodName } from '../confirm-transaction-base.component'
+
+describe('ConfirmTransactionBase Component', () => {
+ describe('getMethodName', () => {
+ it('should get correct method names', () => {
+ assert.equal(getMethodName(undefined), '')
+ assert.equal(getMethodName({}), '')
+ assert.equal(getMethodName('confirm'), 'confirm')
+ assert.equal(getMethodName('balanceOf'), 'balance Of')
+ assert.equal(getMethodName('ethToTokenSwapInput'), 'eth To Token Swap Input')
+ })
+ })
+})
diff --git a/ui/app/pages/confirm-transaction-switch/confirm-transaction-switch.component.js b/ui/app/pages/confirm-transaction-switch/confirm-transaction-switch.component.js
new file mode 100644
index 000000000..cd471b822
--- /dev/null
+++ b/ui/app/pages/confirm-transaction-switch/confirm-transaction-switch.component.js
@@ -0,0 +1,92 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import { Redirect } from 'react-router-dom'
+import Loading from '../../components/ui/loading-screen'
+import {
+ CONFIRM_TRANSACTION_ROUTE,
+ CONFIRM_DEPLOY_CONTRACT_PATH,
+ CONFIRM_SEND_ETHER_PATH,
+ CONFIRM_SEND_TOKEN_PATH,
+ CONFIRM_APPROVE_PATH,
+ CONFIRM_TRANSFER_FROM_PATH,
+ CONFIRM_TOKEN_METHOD_PATH,
+ SIGNATURE_REQUEST_PATH,
+} from '../../helpers/constants/routes'
+import { isConfirmDeployContract } from '../../helpers/utils/transactions.util'
+import {
+ TOKEN_METHOD_TRANSFER,
+ TOKEN_METHOD_APPROVE,
+ TOKEN_METHOD_TRANSFER_FROM,
+} from '../../helpers/constants/transactions'
+
+export default class ConfirmTransactionSwitch extends Component {
+ static propTypes = {
+ txData: PropTypes.object,
+ methodData: PropTypes.object,
+ fetchingData: PropTypes.bool,
+ isEtherTransaction: PropTypes.bool,
+ }
+
+ redirectToTransaction () {
+ const {
+ txData,
+ methodData: { name },
+ fetchingData,
+ isEtherTransaction,
+ } = this.props
+ const { id, txParams: { data } = {} } = txData
+
+ if (fetchingData) {
+ return <Loading />
+ }
+
+ if (isConfirmDeployContract(txData)) {
+ const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_DEPLOY_CONTRACT_PATH}`
+ return <Redirect to={{ pathname }} />
+ }
+
+ if (isEtherTransaction) {
+ const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_SEND_ETHER_PATH}`
+ return <Redirect to={{ pathname }} />
+ }
+
+ if (data) {
+ const methodName = name && name.toLowerCase()
+
+ switch (methodName) {
+ case TOKEN_METHOD_TRANSFER: {
+ const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_SEND_TOKEN_PATH}`
+ return <Redirect to={{ pathname }} />
+ }
+ case TOKEN_METHOD_APPROVE: {
+ const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_APPROVE_PATH}`
+ return <Redirect to={{ pathname }} />
+ }
+ case TOKEN_METHOD_TRANSFER_FROM: {
+ const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_TRANSFER_FROM_PATH}`
+ return <Redirect to={{ pathname }} />
+ }
+ default: {
+ const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_TOKEN_METHOD_PATH}`
+ return <Redirect to={{ pathname }} />
+ }
+ }
+ }
+
+ const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_SEND_ETHER_PATH}`
+ return <Redirect to={{ pathname }} />
+ }
+
+ render () {
+ const { txData } = this.props
+
+ if (txData.txParams) {
+ return this.redirectToTransaction()
+ } else if (txData.msgParams) {
+ const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${txData.id}${SIGNATURE_REQUEST_PATH}`
+ return <Redirect to={{ pathname }} />
+ }
+
+ return <Loading />
+ }
+}
diff --git a/ui/app/pages/confirm-transaction-switch/confirm-transaction-switch.container.js b/ui/app/pages/confirm-transaction-switch/confirm-transaction-switch.container.js
new file mode 100644
index 000000000..7f2c36af2
--- /dev/null
+++ b/ui/app/pages/confirm-transaction-switch/confirm-transaction-switch.container.js
@@ -0,0 +1,22 @@
+import { connect } from 'react-redux'
+import ConfirmTransactionSwitch from './confirm-transaction-switch.component'
+
+const mapStateToProps = state => {
+ const {
+ confirmTransaction: {
+ txData,
+ methodData,
+ fetchingData,
+ toSmartContract,
+ },
+ } = state
+
+ return {
+ txData,
+ methodData,
+ fetchingData,
+ isEtherTransaction: !toSmartContract,
+ }
+}
+
+export default connect(mapStateToProps)(ConfirmTransactionSwitch)
diff --git a/ui/app/pages/confirm-transaction-switch/confirm-transaction-switch.util.js b/ui/app/pages/confirm-transaction-switch/confirm-transaction-switch.util.js
new file mode 100644
index 000000000..536aa5212
--- /dev/null
+++ b/ui/app/pages/confirm-transaction-switch/confirm-transaction-switch.util.js
@@ -0,0 +1,4 @@
+export function isConfirmDeployContract (txData = {}) {
+ const { txParams = {} } = txData
+ return !txParams.to
+}
diff --git a/ui/app/pages/confirm-transaction-switch/index.js b/ui/app/pages/confirm-transaction-switch/index.js
new file mode 100644
index 000000000..c288acb1a
--- /dev/null
+++ b/ui/app/pages/confirm-transaction-switch/index.js
@@ -0,0 +1,2 @@
+import ConfirmTransactionSwitch from './confirm-transaction-switch.container'
+module.exports = ConfirmTransactionSwitch
diff --git a/ui/app/pages/confirm-transaction/conf-tx.js b/ui/app/pages/confirm-transaction/conf-tx.js
new file mode 100644
index 000000000..f9af6624e
--- /dev/null
+++ b/ui/app/pages/confirm-transaction/conf-tx.js
@@ -0,0 +1,225 @@
+const inherits = require('util').inherits
+const Component = require('react').Component
+const h = require('react-hyperscript')
+const connect = require('react-redux').connect
+const { withRouter } = require('react-router-dom')
+const { compose } = require('recompose')
+const actions = require('../../store/actions')
+const txHelper = require('../../../lib/tx-helper')
+const log = require('loglevel')
+const R = require('ramda')
+
+const SignatureRequest = require('../../components/app/signature-request')
+const Loading = require('../../components/ui/loading-screen')
+const { DEFAULT_ROUTE } = require('../../helpers/constants/routes')
+const { getMetaMaskAccounts } = require('../../selectors/selectors')
+
+module.exports = compose(
+ withRouter,
+ connect(mapStateToProps)
+)(ConfirmTxScreen)
+
+function mapStateToProps (state) {
+ const { metamask } = state
+ const {
+ unapprovedMsgCount,
+ unapprovedPersonalMsgCount,
+ unapprovedTypedMessagesCount,
+ } = metamask
+
+ return {
+ identities: state.metamask.identities,
+ accounts: getMetaMaskAccounts(state),
+ selectedAddress: state.metamask.selectedAddress,
+ unapprovedTxs: state.metamask.unapprovedTxs,
+ unapprovedMsgs: state.metamask.unapprovedMsgs,
+ unapprovedPersonalMsgs: state.metamask.unapprovedPersonalMsgs,
+ unapprovedTypedMessages: state.metamask.unapprovedTypedMessages,
+ index: state.appState.currentView.context,
+ warning: state.appState.warning,
+ network: state.metamask.network,
+ provider: state.metamask.provider,
+ conversionRate: state.metamask.conversionRate,
+ currentCurrency: state.metamask.currentCurrency,
+ blockGasLimit: state.metamask.currentBlockGasLimit,
+ computedBalances: state.metamask.computedBalances,
+ unapprovedMsgCount,
+ unapprovedPersonalMsgCount,
+ unapprovedTypedMessagesCount,
+ send: state.metamask.send,
+ selectedAddressTxList: state.metamask.selectedAddressTxList,
+ }
+}
+
+inherits(ConfirmTxScreen, Component)
+function ConfirmTxScreen () {
+ Component.call(this)
+}
+
+ConfirmTxScreen.prototype.getUnapprovedMessagesTotal = function () {
+ const {
+ unapprovedMsgCount = 0,
+ unapprovedPersonalMsgCount = 0,
+ unapprovedTypedMessagesCount = 0,
+ } = this.props
+
+ return unapprovedTypedMessagesCount + unapprovedMsgCount + unapprovedPersonalMsgCount
+}
+
+ConfirmTxScreen.prototype.componentDidMount = function () {
+ const {
+ unapprovedTxs = {},
+ network,
+ send,
+ } = this.props
+ const unconfTxList = txHelper(unapprovedTxs, {}, {}, {}, network)
+
+ if (unconfTxList.length === 0 && !send.to && this.getUnapprovedMessagesTotal() === 0) {
+ this.props.history.push(DEFAULT_ROUTE)
+ }
+}
+
+ConfirmTxScreen.prototype.componentDidUpdate = function (prevProps) {
+ const {
+ unapprovedTxs = {},
+ network,
+ selectedAddressTxList,
+ send,
+ history,
+ match: { params: { id: transactionId } = {} },
+ } = this.props
+
+ let prevTx
+
+ if (transactionId) {
+ prevTx = R.find(({ id }) => id + '' === transactionId)(selectedAddressTxList)
+ } else {
+ const { index: prevIndex, unapprovedTxs: prevUnapprovedTxs } = prevProps
+ const prevUnconfTxList = txHelper(prevUnapprovedTxs, {}, {}, {}, network)
+ const prevTxData = prevUnconfTxList[prevIndex] || {}
+ prevTx = selectedAddressTxList.find(({ id }) => id === prevTxData.id) || {}
+ }
+
+ const unconfTxList = txHelper(unapprovedTxs, {}, {}, {}, network)
+
+ if (prevTx && prevTx.status === 'dropped') {
+ this.props.dispatch(actions.showModal({
+ name: 'TRANSACTION_CONFIRMED',
+ onSubmit: () => history.push(DEFAULT_ROUTE),
+ }))
+
+ return
+ }
+
+ if (unconfTxList.length === 0 && !send.to && this.getUnapprovedMessagesTotal() === 0) {
+ this.props.history.push(DEFAULT_ROUTE)
+ }
+}
+
+ConfirmTxScreen.prototype.getTxData = function () {
+ const {
+ network,
+ index,
+ unapprovedTxs,
+ unapprovedMsgs,
+ unapprovedPersonalMsgs,
+ unapprovedTypedMessages,
+ match: { params: { id: transactionId } = {} },
+ } = this.props
+
+ const unconfTxList = txHelper(
+ unapprovedTxs,
+ unapprovedMsgs,
+ unapprovedPersonalMsgs,
+ unapprovedTypedMessages,
+ network
+ )
+
+ log.info(`rendering a combined ${unconfTxList.length} unconf msgs & txs`)
+
+ return transactionId
+ ? R.find(({ id }) => id + '' === transactionId)(unconfTxList)
+ : unconfTxList[index]
+}
+
+ConfirmTxScreen.prototype.render = function () {
+ const props = this.props
+ const {
+ currentCurrency,
+ conversionRate,
+ blockGasLimit,
+ } = props
+
+ var txData = this.getTxData() || {}
+ const { msgParams } = txData
+ log.debug('msgParams detected, rendering pending msg')
+
+ return msgParams
+ ? h(SignatureRequest, {
+ // Properties
+ txData: txData,
+ key: txData.id,
+ selectedAddress: props.selectedAddress,
+ accounts: props.accounts,
+ identities: props.identities,
+ conversionRate,
+ currentCurrency,
+ blockGasLimit,
+ // Actions
+ signMessage: this.signMessage.bind(this, txData),
+ signPersonalMessage: this.signPersonalMessage.bind(this, txData),
+ signTypedMessage: this.signTypedMessage.bind(this, txData),
+ cancelMessage: this.cancelMessage.bind(this, txData),
+ cancelPersonalMessage: this.cancelPersonalMessage.bind(this, txData),
+ cancelTypedMessage: this.cancelTypedMessage.bind(this, txData),
+ })
+ : h(Loading)
+}
+
+ConfirmTxScreen.prototype.signMessage = function (msgData, event) {
+ log.info('conf-tx.js: signing message')
+ var params = msgData.msgParams
+ params.metamaskId = msgData.id
+ this.stopPropagation(event)
+ return this.props.dispatch(actions.signMsg(params))
+}
+
+ConfirmTxScreen.prototype.stopPropagation = function (event) {
+ if (event.stopPropagation) {
+ event.stopPropagation()
+ }
+}
+
+ConfirmTxScreen.prototype.signPersonalMessage = function (msgData, event) {
+ log.info('conf-tx.js: signing personal message')
+ var params = msgData.msgParams
+ params.metamaskId = msgData.id
+ this.stopPropagation(event)
+ return this.props.dispatch(actions.signPersonalMsg(params))
+}
+
+ConfirmTxScreen.prototype.signTypedMessage = function (msgData, event) {
+ log.info('conf-tx.js: signing typed message')
+ var params = msgData.msgParams
+ params.metamaskId = msgData.id
+ this.stopPropagation(event)
+ return this.props.dispatch(actions.signTypedMsg(params))
+}
+
+ConfirmTxScreen.prototype.cancelMessage = function (msgData, event) {
+ log.info('canceling message')
+ this.stopPropagation(event)
+ return this.props.dispatch(actions.cancelMsg(msgData))
+}
+
+ConfirmTxScreen.prototype.cancelPersonalMessage = function (msgData, event) {
+ log.info('canceling personal message')
+ this.stopPropagation(event)
+ return this.props.dispatch(actions.cancelPersonalMsg(msgData))
+}
+
+ConfirmTxScreen.prototype.cancelTypedMessage = function (msgData, event) {
+ log.info('canceling typed message')
+ this.stopPropagation(event)
+ return this.props.dispatch(actions.cancelTypedMsg(msgData))
+}
diff --git a/ui/app/pages/confirm-transaction/confirm-transaction.component.js b/ui/app/pages/confirm-transaction/confirm-transaction.component.js
new file mode 100644
index 000000000..35b8dc5aa
--- /dev/null
+++ b/ui/app/pages/confirm-transaction/confirm-transaction.component.js
@@ -0,0 +1,160 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import { Switch, Route } from 'react-router-dom'
+import Loading from '../../components/ui/loading-screen'
+import ConfirmTransactionSwitch from '../confirm-transaction-switch'
+import ConfirmTransactionBase from '../confirm-transaction-base'
+import ConfirmSendEther from '../confirm-send-ether'
+import ConfirmSendToken from '../confirm-send-token'
+import ConfirmDeployContract from '../confirm-deploy-contract'
+import ConfirmApprove from '../confirm-approve'
+import ConfirmTokenTransactionBase from '../confirm-token-transaction-base'
+import ConfTx from './conf-tx'
+import {
+ DEFAULT_ROUTE,
+ CONFIRM_TRANSACTION_ROUTE,
+ CONFIRM_DEPLOY_CONTRACT_PATH,
+ CONFIRM_SEND_ETHER_PATH,
+ CONFIRM_SEND_TOKEN_PATH,
+ CONFIRM_APPROVE_PATH,
+ CONFIRM_TRANSFER_FROM_PATH,
+ CONFIRM_TOKEN_METHOD_PATH,
+ SIGNATURE_REQUEST_PATH,
+} from '../../helpers/constants/routes'
+
+export default class ConfirmTransaction extends Component {
+ static propTypes = {
+ history: PropTypes.object.isRequired,
+ totalUnapprovedCount: PropTypes.number.isRequired,
+ match: PropTypes.object,
+ send: PropTypes.object,
+ unconfirmedTransactions: PropTypes.array,
+ setTransactionToConfirm: PropTypes.func,
+ confirmTransaction: PropTypes.object,
+ clearConfirmTransaction: PropTypes.func,
+ fetchBasicGasAndTimeEstimates: PropTypes.func,
+ }
+
+ getParamsTransactionId () {
+ const { match: { params: { id } = {} } } = this.props
+ return id || null
+ }
+
+ componentDidMount () {
+ const {
+ totalUnapprovedCount = 0,
+ send = {},
+ history,
+ confirmTransaction: { txData: { id: transactionId } = {} },
+ fetchBasicGasAndTimeEstimates,
+ } = this.props
+
+ if (!totalUnapprovedCount && !send.to) {
+ history.replace(DEFAULT_ROUTE)
+ return
+ }
+
+ if (!transactionId) {
+ fetchBasicGasAndTimeEstimates()
+ this.setTransactionToConfirm()
+ }
+ }
+
+ componentDidUpdate () {
+ const {
+ setTransactionToConfirm,
+ confirmTransaction: { txData: { id: transactionId } = {} },
+ clearConfirmTransaction,
+ } = this.props
+ const paramsTransactionId = this.getParamsTransactionId()
+
+ if (paramsTransactionId && transactionId && paramsTransactionId !== transactionId + '') {
+ clearConfirmTransaction()
+ setTransactionToConfirm(paramsTransactionId)
+ return
+ }
+
+ if (!transactionId) {
+ this.setTransactionToConfirm()
+ }
+ }
+
+ setTransactionToConfirm () {
+ const {
+ history,
+ unconfirmedTransactions,
+ setTransactionToConfirm,
+ } = this.props
+ const paramsTransactionId = this.getParamsTransactionId()
+
+ if (paramsTransactionId) {
+ // Check to make sure params ID is valid
+ const tx = unconfirmedTransactions.find(({ id }) => id + '' === paramsTransactionId)
+
+ if (!tx) {
+ history.replace(DEFAULT_ROUTE)
+ } else {
+ setTransactionToConfirm(paramsTransactionId)
+ }
+ } else if (unconfirmedTransactions.length) {
+ const totalUnconfirmed = unconfirmedTransactions.length
+ const transaction = unconfirmedTransactions[totalUnconfirmed - 1]
+ const { id: transactionId, loadingDefaults } = transaction
+
+ if (!loadingDefaults) {
+ setTransactionToConfirm(transactionId)
+ }
+ }
+ }
+
+ render () {
+ const { confirmTransaction: { txData: { id } } = {} } = this.props
+ const paramsTransactionId = this.getParamsTransactionId()
+
+ // Show routes when state.confirmTransaction has been set and when either the ID in the params
+ // isn't specified or is specified and matches the ID in state.confirmTransaction in order to
+ // support URLs of /confirm-transaction or /confirm-transaction/<transactionId>
+ return id && (!paramsTransactionId || paramsTransactionId === id + '')
+ ? (
+ <Switch>
+ <Route
+ exact
+ path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${CONFIRM_DEPLOY_CONTRACT_PATH}`}
+ component={ConfirmDeployContract}
+ />
+ <Route
+ exact
+ path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${CONFIRM_TOKEN_METHOD_PATH}`}
+ component={ConfirmTransactionBase}
+ />
+ <Route
+ exact
+ path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${CONFIRM_SEND_ETHER_PATH}`}
+ component={ConfirmSendEther}
+ />
+ <Route
+ exact
+ path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${CONFIRM_SEND_TOKEN_PATH}`}
+ component={ConfirmSendToken}
+ />
+ <Route
+ exact
+ path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${CONFIRM_APPROVE_PATH}`}
+ component={ConfirmApprove}
+ />
+ <Route
+ exact
+ path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${CONFIRM_TRANSFER_FROM_PATH}`}
+ component={ConfirmTokenTransactionBase}
+ />
+ <Route
+ exact
+ path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${SIGNATURE_REQUEST_PATH}`}
+ component={ConfTx}
+ />
+ <Route path="*" component={ConfirmTransactionSwitch} />
+ </Switch>
+ )
+ : <Loading />
+ }
+}
diff --git a/ui/app/pages/confirm-transaction/confirm-transaction.container.js b/ui/app/pages/confirm-transaction/confirm-transaction.container.js
new file mode 100644
index 000000000..2dd5e833e
--- /dev/null
+++ b/ui/app/pages/confirm-transaction/confirm-transaction.container.js
@@ -0,0 +1,37 @@
+import { connect } from 'react-redux'
+import { compose } from 'recompose'
+import { withRouter } from 'react-router-dom'
+import {
+ setTransactionToConfirm,
+ clearConfirmTransaction,
+} from '../../ducks/confirm-transaction/confirm-transaction.duck'
+import {
+ fetchBasicGasAndTimeEstimates,
+} from '../../ducks/gas/gas.duck'
+import ConfirmTransaction from './confirm-transaction.component'
+import { getTotalUnapprovedCount } from '../../selectors/selectors'
+import { unconfirmedTransactionsListSelector } from '../../selectors/confirm-transaction'
+
+const mapStateToProps = state => {
+ const { metamask: { send }, confirmTransaction } = state
+
+ return {
+ totalUnapprovedCount: getTotalUnapprovedCount(state),
+ send,
+ confirmTransaction,
+ unconfirmedTransactions: unconfirmedTransactionsListSelector(state),
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ setTransactionToConfirm: transactionId => dispatch(setTransactionToConfirm(transactionId)),
+ clearConfirmTransaction: () => dispatch(clearConfirmTransaction()),
+ fetchBasicGasAndTimeEstimates: () => dispatch(fetchBasicGasAndTimeEstimates()),
+ }
+}
+
+export default compose(
+ withRouter,
+ connect(mapStateToProps, mapDispatchToProps),
+)(ConfirmTransaction)
diff --git a/ui/app/pages/confirm-transaction/index.js b/ui/app/pages/confirm-transaction/index.js
new file mode 100644
index 000000000..4bf42d85c
--- /dev/null
+++ b/ui/app/pages/confirm-transaction/index.js
@@ -0,0 +1,2 @@
+import ConfirmTransaction from './confirm-transaction.container'
+module.exports = ConfirmTransaction
diff --git a/ui/app/pages/create-account/connect-hardware/account-list.js b/ui/app/pages/create-account/connect-hardware/account-list.js
new file mode 100644
index 000000000..617fb8833
--- /dev/null
+++ b/ui/app/pages/create-account/connect-hardware/account-list.js
@@ -0,0 +1,205 @@
+const { Component } = require('react')
+const PropTypes = require('prop-types')
+const h = require('react-hyperscript')
+const genAccountLink = require('../../../../lib/account-link.js')
+const Select = require('react-select').default
+import Button from '../../../components/ui/button'
+
+class AccountList extends Component {
+ constructor (props, context) {
+ super(props)
+ }
+
+ getHdPaths () {
+ return [
+ {
+ label: `Ledger Live`,
+ value: `m/44'/60'/0'/0/0`,
+ },
+ {
+ label: `Legacy (MEW / MyCrypto)`,
+ value: `m/44'/60'/0'`,
+ },
+ ]
+ }
+
+ goToNextPage = () => {
+ // If we have < 5 accounts, it's restricted by BIP-44
+ if (this.props.accounts.length === 5) {
+ this.props.getPage(this.props.device, 1, this.props.selectedPath)
+ } else {
+ this.props.onAccountRestriction()
+ }
+ }
+
+ goToPreviousPage = () => {
+ this.props.getPage(this.props.device, -1, this.props.selectedPath)
+ }
+
+ renderHdPathSelector () {
+ const { onPathChange, selectedPath } = this.props
+
+ const options = this.getHdPaths()
+ return h('div', [
+ h('h3.hw-connect__hdPath__title', {}, this.context.t('selectHdPath')),
+ h('p.hw-connect__msg', {}, this.context.t('selectPathHelp')),
+ h('div.hw-connect__hdPath', [
+ h(Select, {
+ className: 'hw-connect__hdPath__select',
+ name: 'hd-path-select',
+ clearable: false,
+ value: selectedPath,
+ options,
+ onChange: (opt) => {
+ onPathChange(opt.value)
+ },
+ }),
+ ]),
+ ])
+ }
+
+ capitalizeDevice (device) {
+ return device.slice(0, 1).toUpperCase() + device.slice(1)
+ }
+
+ renderHeader () {
+ const { device } = this.props
+ return (
+ h('div.hw-connect', [
+
+ h('h3.hw-connect__unlock-title', {}, `${this.context.t('unlock')} ${this.capitalizeDevice(device)}`),
+
+ device.toLowerCase() === 'ledger' ? this.renderHdPathSelector() : null,
+
+ h('h3.hw-connect__hdPath__title', {}, this.context.t('selectAnAccount')),
+ h('p.hw-connect__msg', {}, this.context.t('selectAnAccountHelp')),
+ ])
+ )
+ }
+
+ renderAccounts () {
+ return h('div.hw-account-list', [
+ this.props.accounts.map((a, i) => {
+
+ return h('div.hw-account-list__item', { key: a.address }, [
+ h('div.hw-account-list__item__radio', [
+ h('input', {
+ type: 'radio',
+ name: 'selectedAccount',
+ id: `address-${i}`,
+ value: a.index,
+ onChange: (e) => this.props.onAccountChange(e.target.value),
+ checked: this.props.selectedAccount === a.index.toString(),
+ }),
+ h(
+ 'label.hw-account-list__item__label',
+ {
+ htmlFor: `address-${i}`,
+ },
+ [
+ h('span.hw-account-list__item__index', a.index + 1),
+ `${a.address.slice(0, 4)}...${a.address.slice(-4)}`,
+ h('span.hw-account-list__item__balance', `${a.balance}`),
+ ]),
+ ]),
+ h(
+ 'a.hw-account-list__item__link',
+ {
+ href: genAccountLink(a.address, this.props.network),
+ target: '_blank',
+ title: this.context.t('etherscanView'),
+ },
+ h('img', { src: 'images/popout.svg' })
+ ),
+ ])
+ }),
+ ])
+ }
+
+ renderPagination () {
+ return h('div.hw-list-pagination', [
+ h(
+ 'button.hw-list-pagination__button',
+ {
+ onClick: this.goToPreviousPage,
+ },
+ `< ${this.context.t('prev')}`
+ ),
+
+ h(
+ 'button.hw-list-pagination__button',
+ {
+ onClick: this.goToNextPage,
+ },
+ `${this.context.t('next')} >`
+ ),
+ ])
+ }
+
+ renderButtons () {
+ const disabled = this.props.selectedAccount === null
+ const buttonProps = {}
+ if (disabled) {
+ buttonProps.disabled = true
+ }
+
+ return h('div.new-account-connect-form__buttons', {}, [
+ h(Button, {
+ type: 'default',
+ large: true,
+ className: 'new-account-connect-form__button',
+ onClick: this.props.onCancel.bind(this),
+ }, [this.context.t('cancel')]),
+
+ h(Button, {
+ type: 'confirm',
+ large: true,
+ className: 'new-account-connect-form__button unlock',
+ disabled,
+ onClick: this.props.onUnlockAccount.bind(this, this.props.device),
+ }, [this.context.t('unlock')]),
+ ])
+ }
+
+ renderForgetDevice () {
+ return h('div.hw-forget-device-container', {}, [
+ h('a', {
+ onClick: this.props.onForgetDevice.bind(this, this.props.device),
+ }, this.context.t('forgetDevice')),
+ ])
+ }
+
+ render () {
+ return h('div.new-account-connect-form.account-list', {}, [
+ this.renderHeader(),
+ this.renderAccounts(),
+ this.renderPagination(),
+ this.renderButtons(),
+ this.renderForgetDevice(),
+ ])
+ }
+
+}
+
+
+AccountList.propTypes = {
+ onPathChange: PropTypes.func.isRequired,
+ selectedPath: PropTypes.string.isRequired,
+ device: PropTypes.string.isRequired,
+ accounts: PropTypes.array.isRequired,
+ onAccountChange: PropTypes.func.isRequired,
+ onForgetDevice: PropTypes.func.isRequired,
+ getPage: PropTypes.func.isRequired,
+ network: PropTypes.string,
+ selectedAccount: PropTypes.string,
+ history: PropTypes.object,
+ onUnlockAccount: PropTypes.func,
+ onCancel: PropTypes.func,
+ onAccountRestriction: PropTypes.func,
+}
+
+AccountList.contextTypes = {
+ t: PropTypes.func,
+}
+
+module.exports = AccountList
diff --git a/ui/app/pages/create-account/connect-hardware/connect-screen.js b/ui/app/pages/create-account/connect-hardware/connect-screen.js
new file mode 100644
index 000000000..7e9dee970
--- /dev/null
+++ b/ui/app/pages/create-account/connect-hardware/connect-screen.js
@@ -0,0 +1,197 @@
+const { Component } = require('react')
+const PropTypes = require('prop-types')
+const h = require('react-hyperscript')
+import Button from '../../../components/ui/button'
+
+class ConnectScreen extends Component {
+ constructor (props, context) {
+ super(props)
+ this.state = {
+ selectedDevice: null,
+ }
+ }
+
+ connect = () => {
+ if (this.state.selectedDevice) {
+ this.props.connectToHardwareWallet(this.state.selectedDevice)
+ }
+ return null
+ }
+
+ renderConnectToTrezorButton () {
+ return h(
+ `button.hw-connect__btn${this.state.selectedDevice === 'trezor' ? '.selected' : ''}`,
+ { onClick: _ => this.setState({selectedDevice: 'trezor'}) },
+ h('img.hw-connect__btn__img', {
+ src: 'images/trezor-logo.svg',
+ })
+ )
+ }
+
+ renderConnectToLedgerButton () {
+ return h(
+ `button.hw-connect__btn${this.state.selectedDevice === 'ledger' ? '.selected' : ''}`,
+ { onClick: _ => this.setState({selectedDevice: 'ledger'}) },
+ h('img.hw-connect__btn__img', {
+ src: 'images/ledger-logo.svg',
+ })
+ )
+ }
+
+ renderButtons () {
+ return (
+ h('div', {}, [
+ h('div.hw-connect__btn-wrapper', {}, [
+ this.renderConnectToLedgerButton(),
+ this.renderConnectToTrezorButton(),
+ ]),
+ h(Button, {
+ type: 'confirm',
+ large: true,
+ className: 'hw-connect__connect-btn',
+ onClick: this.connect,
+ disabled: !this.state.selectedDevice,
+ }, this.context.t('connect')),
+ ])
+ )
+ }
+
+ renderUnsupportedBrowser () {
+ return (
+ h('div.new-account-connect-form.unsupported-browser', {}, [
+ h('div.hw-connect', [
+ h('h3.hw-connect__title', {}, this.context.t('browserNotSupported')),
+ h('p.hw-connect__msg', {}, this.context.t('chromeRequiredForHardwareWallets')),
+ ]),
+ h(Button, {
+ type: 'primary',
+ large: true,
+ onClick: () => global.platform.openWindow({
+ url: 'https://google.com/chrome',
+ }),
+ }, this.context.t('downloadGoogleChrome')),
+ ])
+ )
+ }
+
+ renderHeader () {
+ return (
+ h('div.hw-connect__header', {}, [
+ h('h3.hw-connect__header__title', {}, this.context.t(`hardwareWallets`)),
+ h('p.hw-connect__header__msg', {}, this.context.t(`hardwareWalletsMsg`)),
+ ])
+ )
+ }
+
+ getAffiliateLinks () {
+ const links = {
+ trezor: `<a class='hw-connect__get-hw__link' href='https://shop.trezor.io/?a=metamask' target='_blank'>Trezor</a>`,
+ ledger: `<a class='hw-connect__get-hw__link' href='https://www.ledger.com/products/ledger-nano-s?r=17c4991a03fa&tracker=MY_TRACKER' target='_blank'>Ledger</a>`,
+ }
+
+ const text = this.context.t('orderOneHere')
+ const response = text.replace('Trezor', links.trezor).replace('Ledger', links.ledger)
+
+ return h('div.hw-connect__get-hw__msg', { dangerouslySetInnerHTML: {__html: response }})
+ }
+
+ renderTrezorAffiliateLink () {
+ return h('div.hw-connect__get-hw', {}, [
+ h('p.hw-connect__get-hw__msg', {}, this.context.t(`dontHaveAHardwareWallet`)),
+ this.getAffiliateLinks(),
+ ])
+ }
+
+
+ scrollToTutorial = (e) => {
+ if (this.referenceNode) this.referenceNode.scrollIntoView({behavior: 'smooth'})
+ }
+
+ renderLearnMore () {
+ return (
+ h('p.hw-connect__learn-more', {
+ onClick: this.scrollToTutorial,
+ }, [
+ this.context.t('learnMore'),
+ h('img.hw-connect__learn-more__arrow', { src: 'images/caret-right.svg'}),
+ ])
+ )
+ }
+
+ renderTutorialSteps () {
+ const steps = [
+ {
+ asset: 'hardware-wallet-step-1',
+ dimensions: {width: '225px', height: '75px'},
+ },
+ {
+ asset: 'hardware-wallet-step-2',
+ dimensions: {width: '300px', height: '100px'},
+ },
+ {
+ asset: 'hardware-wallet-step-3',
+ dimensions: {width: '120px', height: '90px'},
+ },
+ ]
+
+ return h('.hw-tutorial', {
+ ref: node => { this.referenceNode = node },
+ },
+ steps.map((step, i) => (
+ h('div.hw-connect', {}, [
+ h('h3.hw-connect__title', {}, this.context.t(`step${i + 1}HardwareWallet`)),
+ h('p.hw-connect__msg', {}, this.context.t(`step${i + 1}HardwareWalletMsg`)),
+ h('img.hw-connect__step-asset', { src: `images/${step.asset}.svg`, ...step.dimensions }),
+ ])
+ ))
+ )
+ }
+
+ renderFooter () {
+ return (
+ h('div.hw-connect__footer', {}, [
+ h('h3.hw-connect__footer__title', {}, this.context.t(`readyToConnect`)),
+ this.renderButtons(),
+ h('p.hw-connect__footer__msg', {}, [
+ this.context.t(`havingTroubleConnecting`),
+ h('a.hw-connect__footer__link', {
+ href: 'https://support.metamask.io/',
+ target: '_blank',
+ }, this.context.t('getHelp')),
+ ]),
+ ])
+ )
+ }
+
+ renderConnectScreen () {
+ return (
+ h('div.new-account-connect-form', {}, [
+ this.renderHeader(),
+ this.renderButtons(),
+ this.renderTrezorAffiliateLink(),
+ this.renderLearnMore(),
+ this.renderTutorialSteps(),
+ this.renderFooter(),
+ ])
+ )
+ }
+
+ render () {
+ if (this.props.browserSupported) {
+ return this.renderConnectScreen()
+ }
+ return this.renderUnsupportedBrowser()
+ }
+}
+
+ConnectScreen.propTypes = {
+ connectToHardwareWallet: PropTypes.func.isRequired,
+ browserSupported: PropTypes.bool.isRequired,
+}
+
+ConnectScreen.contextTypes = {
+ t: PropTypes.func,
+}
+
+module.exports = ConnectScreen
+
diff --git a/ui/app/pages/create-account/connect-hardware/index.js b/ui/app/pages/create-account/connect-hardware/index.js
new file mode 100644
index 000000000..1398fa680
--- /dev/null
+++ b/ui/app/pages/create-account/connect-hardware/index.js
@@ -0,0 +1,293 @@
+const { Component } = require('react')
+const PropTypes = require('prop-types')
+const h = require('react-hyperscript')
+const connect = require('react-redux').connect
+const actions = require('../../../store/actions')
+const { getMetaMaskAccounts } = require('../../../selectors/selectors')
+const ConnectScreen = require('./connect-screen')
+const AccountList = require('./account-list')
+const { DEFAULT_ROUTE } = require('../../../helpers/constants/routes')
+const { formatBalance } = require('../../../helpers/utils/util')
+const { getPlatform } = require('../../../../../app/scripts/lib/util')
+const { PLATFORM_FIREFOX } = require('../../../../../app/scripts/lib/enums')
+
+class ConnectHardwareForm extends Component {
+ constructor (props, context) {
+ super(props)
+ this.state = {
+ error: null,
+ selectedAccount: null,
+ accounts: [],
+ browserSupported: true,
+ unlocked: false,
+ device: null,
+ }
+ }
+
+ componentWillReceiveProps (nextProps) {
+ const { accounts } = nextProps
+ const newAccounts = this.state.accounts.map(a => {
+ const normalizedAddress = a.address.toLowerCase()
+ const balanceValue = accounts[normalizedAddress] && accounts[normalizedAddress].balance || null
+ a.balance = balanceValue ? formatBalance(balanceValue, 6) : '...'
+ return a
+ })
+ this.setState({accounts: newAccounts})
+ }
+
+
+ componentDidMount () {
+ this.checkIfUnlocked()
+ }
+
+ async checkIfUnlocked () {
+ ['trezor', 'ledger'].forEach(async device => {
+ const unlocked = await this.props.checkHardwareStatus(device, this.props.defaultHdPaths[device])
+ if (unlocked) {
+ this.setState({unlocked: true})
+ this.getPage(device, 0, this.props.defaultHdPaths[device])
+ }
+ })
+ }
+
+ connectToHardwareWallet = (device) => {
+ // Ledger hardware wallets are not supported on firefox
+ if (getPlatform() === PLATFORM_FIREFOX && device === 'ledger') {
+ this.setState({ browserSupported: false, error: null})
+ return null
+ }
+
+ if (this.state.accounts.length) {
+ return null
+ }
+
+ // Default values
+ this.getPage(device, 0, this.props.defaultHdPaths[device])
+ }
+
+ onPathChange = (path) => {
+ this.props.setHardwareWalletDefaultHdPath({device: this.state.device, path})
+ this.getPage(this.state.device, 0, path)
+ }
+
+ onAccountChange = (account) => {
+ this.setState({selectedAccount: account.toString(), error: null})
+ }
+
+ onAccountRestriction = () => {
+ this.setState({error: this.context.t('ledgerAccountRestriction') })
+ }
+
+ showTemporaryAlert () {
+ this.props.showAlert(this.context.t('hardwareWalletConnected'))
+ // Autohide the alert after 5 seconds
+ setTimeout(_ => {
+ this.props.hideAlert()
+ }, 5000)
+ }
+
+ getPage = (device, page, hdPath) => {
+ this.props
+ .connectHardware(device, page, hdPath)
+ .then(accounts => {
+ if (accounts.length) {
+
+ // If we just loaded the accounts for the first time
+ // (device previously locked) show the global alert
+ if (this.state.accounts.length === 0 && !this.state.unlocked) {
+ this.showTemporaryAlert()
+ }
+
+ const newState = { unlocked: true, device, error: null }
+ // Default to the first account
+ if (this.state.selectedAccount === null) {
+ accounts.forEach((a, i) => {
+ if (a.address.toLowerCase() === this.props.address) {
+ newState.selectedAccount = a.index.toString()
+ }
+ })
+ // If the page doesn't contain the selected account, let's deselect it
+ } else if (!accounts.filter(a => a.index.toString() === this.state.selectedAccount).length) {
+ newState.selectedAccount = null
+ }
+
+
+ // Map accounts with balances
+ newState.accounts = accounts.map(account => {
+ const normalizedAddress = account.address.toLowerCase()
+ const balanceValue = this.props.accounts[normalizedAddress] && this.props.accounts[normalizedAddress].balance || null
+ account.balance = balanceValue ? formatBalance(balanceValue, 6) : '...'
+ return account
+ })
+
+ this.setState(newState)
+ }
+ })
+ .catch(e => {
+ if (e === 'Window blocked') {
+ this.setState({ browserSupported: false, error: null})
+ } else if (e !== 'Window closed' && e !== 'Popup closed') {
+ this.setState({ error: e.toString() })
+ }
+ })
+ }
+
+ onForgetDevice = (device) => {
+ this.props.forgetDevice(device)
+ .then(_ => {
+ this.setState({
+ error: null,
+ selectedAccount: null,
+ accounts: [],
+ unlocked: false,
+ })
+ }).catch(e => {
+ this.setState({ error: e.toString() })
+ })
+ }
+
+ onUnlockAccount = (device) => {
+
+ if (this.state.selectedAccount === null) {
+ this.setState({ error: this.context.t('accountSelectionRequired') })
+ }
+
+ this.props.unlockHardwareWalletAccount(this.state.selectedAccount, device)
+ .then(_ => {
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Accounts',
+ action: 'Connected Hardware Wallet',
+ name: 'Connected Account with: ' + device,
+ },
+ })
+ this.props.history.push(DEFAULT_ROUTE)
+ }).catch(e => {
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Accounts',
+ action: 'Connected Hardware Wallet',
+ name: 'Error connecting hardware wallet',
+ },
+ customVariables: {
+ error: e.toString(),
+ },
+ })
+ this.setState({ error: e.toString() })
+ })
+ }
+
+ onCancel = () => {
+ this.props.history.push(DEFAULT_ROUTE)
+ }
+
+ renderError () {
+ return this.state.error
+ ? h('span.error', { style: { margin: '20px 20px 10px', display: 'block', textAlign: 'center' } }, this.state.error)
+ : null
+ }
+
+ renderContent () {
+ if (!this.state.accounts.length) {
+ return h(ConnectScreen, {
+ connectToHardwareWallet: this.connectToHardwareWallet,
+ browserSupported: this.state.browserSupported,
+ })
+ }
+
+ return h(AccountList, {
+ onPathChange: this.onPathChange,
+ selectedPath: this.props.defaultHdPaths[this.state.device],
+ device: this.state.device,
+ accounts: this.state.accounts,
+ selectedAccount: this.state.selectedAccount,
+ onAccountChange: this.onAccountChange,
+ network: this.props.network,
+ getPage: this.getPage,
+ history: this.props.history,
+ onUnlockAccount: this.onUnlockAccount,
+ onForgetDevice: this.onForgetDevice,
+ onCancel: this.onCancel,
+ onAccountRestriction: this.onAccountRestriction,
+ })
+ }
+
+ render () {
+ return h('div', [
+ this.renderError(),
+ this.renderContent(),
+ ])
+ }
+}
+
+ConnectHardwareForm.propTypes = {
+ hideModal: PropTypes.func,
+ showImportPage: PropTypes.func,
+ showConnectPage: PropTypes.func,
+ connectHardware: PropTypes.func,
+ checkHardwareStatus: PropTypes.func,
+ forgetDevice: PropTypes.func,
+ showAlert: PropTypes.func,
+ hideAlert: PropTypes.func,
+ unlockHardwareWalletAccount: PropTypes.func,
+ setHardwareWalletDefaultHdPath: PropTypes.func,
+ numberOfExistingAccounts: PropTypes.number,
+ history: PropTypes.object,
+ t: PropTypes.func,
+ network: PropTypes.string,
+ accounts: PropTypes.object,
+ address: PropTypes.string,
+ defaultHdPaths: PropTypes.object,
+}
+
+const mapStateToProps = state => {
+ const {
+ metamask: { network, selectedAddress, identities = {} },
+ } = state
+ const accounts = getMetaMaskAccounts(state)
+ const numberOfExistingAccounts = Object.keys(identities).length
+ const {
+ appState: { defaultHdPaths },
+ } = state
+
+ return {
+ network,
+ accounts,
+ address: selectedAddress,
+ numberOfExistingAccounts,
+ defaultHdPaths,
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ setHardwareWalletDefaultHdPath: ({device, path}) => {
+ return dispatch(actions.setHardwareWalletDefaultHdPath({device, path}))
+ },
+ connectHardware: (deviceName, page, hdPath) => {
+ return dispatch(actions.connectHardware(deviceName, page, hdPath))
+ },
+ checkHardwareStatus: (deviceName, hdPath) => {
+ return dispatch(actions.checkHardwareStatus(deviceName, hdPath))
+ },
+ forgetDevice: (deviceName) => {
+ return dispatch(actions.forgetDevice(deviceName))
+ },
+ unlockHardwareWalletAccount: (index, deviceName, hdPath) => {
+ return dispatch(actions.unlockHardwareWalletAccount(index, deviceName, hdPath))
+ },
+ showImportPage: () => dispatch(actions.showImportPage()),
+ showConnectPage: () => dispatch(actions.showConnectPage()),
+ showAlert: (msg) => dispatch(actions.showAlert(msg)),
+ hideAlert: () => dispatch(actions.hideAlert()),
+ }
+}
+
+ConnectHardwareForm.contextTypes = {
+ t: PropTypes.func,
+ metricsEvent: PropTypes.func,
+}
+
+module.exports = connect(mapStateToProps, mapDispatchToProps)(
+ ConnectHardwareForm
+)
diff --git a/ui/app/pages/create-account/import-account/index.js b/ui/app/pages/create-account/import-account/index.js
new file mode 100644
index 000000000..48d8f8838
--- /dev/null
+++ b/ui/app/pages/create-account/import-account/index.js
@@ -0,0 +1,96 @@
+const inherits = require('util').inherits
+const Component = require('react').Component
+const h = require('react-hyperscript')
+const PropTypes = require('prop-types')
+const connect = require('react-redux').connect
+import Select from 'react-select'
+
+// Subviews
+const JsonImportView = require('./json.js')
+const PrivateKeyImportView = require('./private-key.js')
+
+
+AccountImportSubview.contextTypes = {
+ t: PropTypes.func,
+}
+
+module.exports = connect()(AccountImportSubview)
+
+
+inherits(AccountImportSubview, Component)
+function AccountImportSubview () {
+ Component.call(this)
+}
+
+AccountImportSubview.prototype.getMenuItemTexts = function () {
+ return [
+ this.context.t('privateKey'),
+ this.context.t('jsonFile'),
+ ]
+}
+
+AccountImportSubview.prototype.render = function () {
+ const state = this.state || {}
+ const menuItems = this.getMenuItemTexts()
+ const { type } = state
+
+ return (
+ h('div.new-account-import-form', [
+
+ h('.new-account-import-disclaimer', [
+ h('span', this.context.t('importAccountMsg')),
+ h('span', {
+ style: {
+ cursor: 'pointer',
+ textDecoration: 'underline',
+ },
+ onClick: () => {
+ global.platform.openWindow({
+ url: 'https://metamask.zendesk.com/hc/en-us/articles/360015289932',
+ })
+ },
+ }, this.context.t('here')),
+ ]),
+
+ h('div.new-account-import-form__select-section', [
+
+ h('div.new-account-import-form__select-label', this.context.t('selectType')),
+
+ h(Select, {
+ className: 'new-account-import-form__select',
+ name: 'import-type-select',
+ clearable: false,
+ value: type || menuItems[0],
+ options: menuItems.map((type) => {
+ return {
+ value: type,
+ label: type,
+ }
+ }),
+ onChange: (opt) => {
+ this.setState({ type: opt.value })
+ },
+ }),
+
+ ]),
+
+ this.renderImportView(),
+ ])
+ )
+}
+
+AccountImportSubview.prototype.renderImportView = function () {
+ const state = this.state || {}
+ const { type } = state
+ const menuItems = this.getMenuItemTexts()
+ const current = type || menuItems[0]
+
+ switch (current) {
+ case this.context.t('privateKey'):
+ return h(PrivateKeyImportView)
+ case this.context.t('jsonFile'):
+ return h(JsonImportView)
+ default:
+ return h(JsonImportView)
+ }
+}
diff --git a/ui/app/pages/create-account/import-account/json.js b/ui/app/pages/create-account/import-account/json.js
new file mode 100644
index 000000000..17bef763c
--- /dev/null
+++ b/ui/app/pages/create-account/import-account/json.js
@@ -0,0 +1,170 @@
+const Component = require('react').Component
+const PropTypes = require('prop-types')
+const h = require('react-hyperscript')
+const { withRouter } = require('react-router-dom')
+const { compose } = require('recompose')
+const connect = require('react-redux').connect
+const actions = require('../../../store/actions')
+const FileInput = require('react-simple-file-input').default
+const { DEFAULT_ROUTE } = require('../../../helpers/constants/routes')
+const { getMetaMaskAccounts } = require('../../../selectors/selectors')
+const HELP_LINK = 'https://support.metamask.io/kb/article/7-importing-accounts'
+import Button from '../../../components/ui/button'
+
+class JsonImportSubview extends Component {
+ constructor (props) {
+ super(props)
+
+ this.state = {
+ file: null,
+ fileContents: '',
+ }
+ }
+
+ render () {
+ const { error } = this.props
+
+ return (
+ h('div.new-account-import-form__json', [
+
+ h('p', this.context.t('usedByClients')),
+ h('a.warning', {
+ href: HELP_LINK,
+ target: '_blank',
+ }, this.context.t('fileImportFail')),
+
+ h(FileInput, {
+ readAs: 'text',
+ onLoad: this.onLoad.bind(this),
+ style: {
+ margin: '20px 0px 12px 34%',
+ fontSize: '15px',
+ display: 'flex',
+ justifyContent: 'center',
+ },
+ }),
+
+ h('input.new-account-import-form__input-password', {
+ type: 'password',
+ placeholder: this.context.t('enterPassword'),
+ id: 'json-password-box',
+ onKeyPress: this.createKeyringOnEnter.bind(this),
+ }),
+
+ h('div.new-account-create-form__buttons', {}, [
+
+ h(Button, {
+ type: 'default',
+ large: true,
+ className: 'new-account-create-form__button',
+ onClick: () => this.props.history.push(DEFAULT_ROUTE),
+ }, [this.context.t('cancel')]),
+
+ h(Button, {
+ type: 'primary',
+ large: true,
+ className: 'new-account-create-form__button',
+ onClick: () => this.createNewKeychain(),
+ }, [this.context.t('import')]),
+
+ ]),
+
+ error ? h('span.error', error) : null,
+ ])
+ )
+ }
+
+ onLoad (event, file) {
+ this.setState({file: file, fileContents: event.target.result})
+ }
+
+ createKeyringOnEnter (event) {
+ if (event.key === 'Enter') {
+ event.preventDefault()
+ this.createNewKeychain()
+ }
+ }
+
+ createNewKeychain () {
+ const { firstAddress, displayWarning, importNewJsonAccount, setSelectedAddress, history } = this.props
+ const state = this.state
+
+ if (!state) {
+ const message = this.context.t('validFileImport')
+ return displayWarning(message)
+ }
+
+ const { fileContents } = state
+
+ if (!fileContents) {
+ const message = this.context.t('needImportFile')
+ return displayWarning(message)
+ }
+
+ const passwordInput = document.getElementById('json-password-box')
+ const password = passwordInput.value
+
+ importNewJsonAccount([ fileContents, password ])
+ .then(({ selectedAddress }) => {
+ if (selectedAddress) {
+ history.push(DEFAULT_ROUTE)
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Accounts',
+ action: 'Import Account',
+ name: 'Imported Account with JSON',
+ },
+ })
+ displayWarning(null)
+ } else {
+ displayWarning('Error importing account.')
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Accounts',
+ action: 'Import Account',
+ name: 'Error importing JSON',
+ },
+ })
+ setSelectedAddress(firstAddress)
+ }
+ })
+ .catch(err => err && displayWarning(err.message || err))
+ }
+}
+
+JsonImportSubview.propTypes = {
+ error: PropTypes.string,
+ goHome: PropTypes.func,
+ displayWarning: PropTypes.func,
+ firstAddress: PropTypes.string,
+ importNewJsonAccount: PropTypes.func,
+ history: PropTypes.object,
+ setSelectedAddress: PropTypes.func,
+ t: PropTypes.func,
+}
+
+const mapStateToProps = state => {
+ return {
+ error: state.appState.warning,
+ firstAddress: Object.keys(getMetaMaskAccounts(state))[0],
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ goHome: () => dispatch(actions.goHome()),
+ displayWarning: warning => dispatch(actions.displayWarning(warning)),
+ importNewJsonAccount: options => dispatch(actions.importNewAccount('JSON File', options)),
+ setSelectedAddress: (address) => dispatch(actions.setSelectedAddress(address)),
+ }
+}
+
+JsonImportSubview.contextTypes = {
+ t: PropTypes.func,
+ metricsEvent: PropTypes.func,
+}
+
+module.exports = compose(
+ withRouter,
+ connect(mapStateToProps, mapDispatchToProps)
+)(JsonImportSubview)
diff --git a/ui/app/pages/create-account/import-account/private-key.js b/ui/app/pages/create-account/import-account/private-key.js
new file mode 100644
index 000000000..450614e87
--- /dev/null
+++ b/ui/app/pages/create-account/import-account/private-key.js
@@ -0,0 +1,128 @@
+const inherits = require('util').inherits
+const Component = require('react').Component
+const h = require('react-hyperscript')
+const { withRouter } = require('react-router-dom')
+const { compose } = require('recompose')
+const PropTypes = require('prop-types')
+const connect = require('react-redux').connect
+const actions = require('../../../store/actions')
+const { DEFAULT_ROUTE } = require('../../../helpers/constants/routes')
+const { getMetaMaskAccounts } = require('../../../selectors/selectors')
+import Button from '../../../components/ui/button'
+
+PrivateKeyImportView.contextTypes = {
+ t: PropTypes.func,
+ metricsEvent: PropTypes.func,
+}
+
+module.exports = compose(
+ withRouter,
+ connect(mapStateToProps, mapDispatchToProps)
+)(PrivateKeyImportView)
+
+
+function mapStateToProps (state) {
+ return {
+ error: state.appState.warning,
+ firstAddress: Object.keys(getMetaMaskAccounts(state))[0],
+ }
+}
+
+function mapDispatchToProps (dispatch) {
+ return {
+ importNewAccount: (strategy, [ privateKey ]) => {
+ return dispatch(actions.importNewAccount(strategy, [ privateKey ]))
+ },
+ displayWarning: (message) => dispatch(actions.displayWarning(message || null)),
+ setSelectedAddress: (address) => dispatch(actions.setSelectedAddress(address)),
+ }
+}
+
+inherits(PrivateKeyImportView, Component)
+function PrivateKeyImportView () {
+ this.createKeyringOnEnter = this.createKeyringOnEnter.bind(this)
+ Component.call(this)
+}
+
+PrivateKeyImportView.prototype.render = function () {
+ const { error, displayWarning } = this.props
+
+ return (
+ h('div.new-account-import-form__private-key', [
+
+ h('span.new-account-create-form__instruction', this.context.t('pastePrivateKey')),
+
+ h('div.new-account-import-form__private-key-password-container', [
+
+ h('input.new-account-import-form__input-password', {
+ type: 'password',
+ id: 'private-key-box',
+ onKeyPress: e => this.createKeyringOnEnter(e),
+ }),
+
+ ]),
+
+ h('div.new-account-import-form__buttons', {}, [
+
+ h(Button, {
+ type: 'default',
+ large: true,
+ className: 'new-account-create-form__button',
+ onClick: () => {
+ displayWarning(null)
+ this.props.history.push(DEFAULT_ROUTE)
+ },
+ }, [this.context.t('cancel')]),
+
+ h(Button, {
+ type: 'primary',
+ large: true,
+ className: 'new-account-create-form__button',
+ onClick: () => this.createNewKeychain(),
+ }, [this.context.t('import')]),
+
+ ]),
+
+ error ? h('span.error', error) : null,
+ ])
+ )
+}
+
+PrivateKeyImportView.prototype.createKeyringOnEnter = function (event) {
+ if (event.key === 'Enter') {
+ event.preventDefault()
+ this.createNewKeychain()
+ }
+}
+
+PrivateKeyImportView.prototype.createNewKeychain = function () {
+ const input = document.getElementById('private-key-box')
+ const privateKey = input.value
+ const { importNewAccount, history, displayWarning, setSelectedAddress, firstAddress } = this.props
+
+ importNewAccount('Private Key', [ privateKey ])
+ .then(({ selectedAddress }) => {
+ if (selectedAddress) {
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Accounts',
+ action: 'Import Account',
+ name: 'Imported Account with Private Key',
+ },
+ })
+ history.push(DEFAULT_ROUTE)
+ displayWarning(null)
+ } else {
+ displayWarning('Error importing account.')
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Accounts',
+ action: 'Import Account',
+ name: 'Error importing with Private Key',
+ },
+ })
+ setSelectedAddress(firstAddress)
+ }
+ })
+ .catch(err => err && displayWarning(err.message || err))
+}
diff --git a/ui/app/pages/create-account/import-account/seed.js b/ui/app/pages/create-account/import-account/seed.js
new file mode 100644
index 000000000..d98909baa
--- /dev/null
+++ b/ui/app/pages/create-account/import-account/seed.js
@@ -0,0 +1,35 @@
+const inherits = require('util').inherits
+const Component = require('react').Component
+const h = require('react-hyperscript')
+const PropTypes = require('prop-types')
+const connect = require('react-redux').connect
+
+SeedImportSubview.contextTypes = {
+ t: PropTypes.func,
+}
+
+module.exports = connect(mapStateToProps)(SeedImportSubview)
+
+
+function mapStateToProps (state) {
+ return {}
+}
+
+inherits(SeedImportSubview, Component)
+function SeedImportSubview () {
+ Component.call(this)
+}
+
+SeedImportSubview.prototype.render = function () {
+ return (
+ h('div', {
+ style: {
+ },
+ }, [
+ this.context.t('pasteSeed'),
+ h('textarea'),
+ h('br'),
+ h('button', this.context.t('submit')),
+ ])
+ )
+}
diff --git a/ui/app/pages/create-account/index.js b/ui/app/pages/create-account/index.js
new file mode 100644
index 000000000..ce84db028
--- /dev/null
+++ b/ui/app/pages/create-account/index.js
@@ -0,0 +1,113 @@
+const Component = require('react').Component
+const { Switch, Route, matchPath } = require('react-router-dom')
+const PropTypes = require('prop-types')
+const h = require('react-hyperscript')
+const connect = require('react-redux').connect
+const actions = require('../../store/actions')
+const { getCurrentViewContext } = require('../../selectors/selectors')
+const classnames = require('classnames')
+const NewAccountCreateForm = require('./new-account')
+const NewAccountImportForm = require('./import-account')
+const ConnectHardwareForm = require('./connect-hardware')
+const {
+ NEW_ACCOUNT_ROUTE,
+ IMPORT_ACCOUNT_ROUTE,
+ CONNECT_HARDWARE_ROUTE,
+} = require('../../helpers/constants/routes')
+
+class CreateAccountPage extends Component {
+ renderTabs () {
+ const { history, location } = this.props
+
+ return h('div.new-account__tabs', [
+ h('div.new-account__tabs__tab', {
+ className: classnames('new-account__tabs__tab', {
+ 'new-account__tabs__selected': matchPath(location.pathname, {
+ path: NEW_ACCOUNT_ROUTE, exact: true,
+ }),
+ }),
+ onClick: () => history.push(NEW_ACCOUNT_ROUTE),
+ }, [
+ this.context.t('create'),
+ ]),
+
+ h('div.new-account__tabs__tab', {
+ className: classnames('new-account__tabs__tab', {
+ 'new-account__tabs__selected': matchPath(location.pathname, {
+ path: IMPORT_ACCOUNT_ROUTE, exact: true,
+ }),
+ }),
+ onClick: () => history.push(IMPORT_ACCOUNT_ROUTE),
+ }, [
+ this.context.t('import'),
+ ]),
+ h(
+ 'div.new-account__tabs__tab',
+ {
+ className: classnames('new-account__tabs__tab', {
+ 'new-account__tabs__selected': matchPath(location.pathname, {
+ path: CONNECT_HARDWARE_ROUTE,
+ exact: true,
+ }),
+ }),
+ onClick: () => history.push(CONNECT_HARDWARE_ROUTE),
+ },
+ this.context.t('connect')
+ ),
+ ])
+ }
+
+ render () {
+ return h('div.new-account', {}, [
+ h('div.new-account__header', [
+ h('div.new-account__title', this.context.t('newAccount')),
+ this.renderTabs(),
+ ]),
+ h('div.new-account__form', [
+ h(Switch, [
+ h(Route, {
+ exact: true,
+ path: NEW_ACCOUNT_ROUTE,
+ component: NewAccountCreateForm,
+ }),
+ h(Route, {
+ exact: true,
+ path: IMPORT_ACCOUNT_ROUTE,
+ component: NewAccountImportForm,
+ }),
+ h(Route, {
+ exact: true,
+ path: CONNECT_HARDWARE_ROUTE,
+ component: ConnectHardwareForm,
+ }),
+ ]),
+ ]),
+ ])
+ }
+}
+
+CreateAccountPage.propTypes = {
+ location: PropTypes.object,
+ history: PropTypes.object,
+ t: PropTypes.func,
+}
+
+CreateAccountPage.contextTypes = {
+ t: PropTypes.func,
+}
+
+const mapStateToProps = state => ({
+ displayedForm: getCurrentViewContext(state),
+})
+
+const mapDispatchToProps = dispatch => ({
+ displayForm: form => dispatch(actions.setNewAccountForm(form)),
+ showQrView: (selected, identity) => dispatch(actions.showQrView(selected, identity)),
+ showExportPrivateKeyModal: () => {
+ dispatch(actions.showModal({ name: 'EXPORT_PRIVATE_KEY' }))
+ },
+ hideModal: () => dispatch(actions.hideModal()),
+ setAccountLabel: (address, label) => dispatch(actions.setAccountLabel(address, label)),
+})
+
+module.exports = connect(mapStateToProps, mapDispatchToProps)(CreateAccountPage)
diff --git a/ui/app/pages/create-account/new-account.js b/ui/app/pages/create-account/new-account.js
new file mode 100644
index 000000000..316fbe6f1
--- /dev/null
+++ b/ui/app/pages/create-account/new-account.js
@@ -0,0 +1,130 @@
+const { Component } = require('react')
+const PropTypes = require('prop-types')
+const h = require('react-hyperscript')
+const connect = require('react-redux').connect
+const actions = require('../../store/actions')
+const { DEFAULT_ROUTE } = require('../../helpers/constants/routes')
+import Button from '../../components/ui/button'
+
+class NewAccountCreateForm extends Component {
+ constructor (props, context) {
+ super(props)
+
+ const { numberOfExistingAccounts = 0 } = props
+ const newAccountNumber = numberOfExistingAccounts + 1
+
+ this.state = {
+ newAccountName: '',
+ defaultAccountName: context.t('newAccountNumberName', [newAccountNumber]),
+ }
+ }
+
+ render () {
+ const { newAccountName, defaultAccountName } = this.state
+ const { history, createAccount } = this.props
+
+ return h('div.new-account-create-form', [
+
+ h('div.new-account-create-form__input-label', {}, [
+ this.context.t('accountName'),
+ ]),
+
+ h('div.new-account-create-form__input-wrapper', {}, [
+ h('input.new-account-create-form__input', {
+ value: newAccountName,
+ placeholder: defaultAccountName,
+ onChange: event => this.setState({ newAccountName: event.target.value }),
+ }, []),
+ ]),
+
+ h('div.new-account-create-form__buttons', {}, [
+
+ h(Button, {
+ type: 'default',
+ large: true,
+ className: 'new-account-create-form__button',
+ onClick: () => history.push(DEFAULT_ROUTE),
+ }, [this.context.t('cancel')]),
+
+ h(Button, {
+ type: 'primary',
+ large: true,
+ className: 'new-account-create-form__button',
+ onClick: () => {
+ createAccount(newAccountName || defaultAccountName)
+ .then(() => {
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Accounts',
+ action: 'Add New Account',
+ name: 'Added New Account',
+ },
+ })
+ history.push(DEFAULT_ROUTE)
+ })
+ .catch((e) => {
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Accounts',
+ action: 'Add New Account',
+ name: 'Error',
+ },
+ customVariables: {
+ errorMessage: e.message,
+ },
+ })
+ })
+ },
+ }, [this.context.t('create')]),
+
+ ]),
+
+ ])
+ }
+}
+
+NewAccountCreateForm.propTypes = {
+ hideModal: PropTypes.func,
+ showImportPage: PropTypes.func,
+ showConnectPage: PropTypes.func,
+ createAccount: PropTypes.func,
+ numberOfExistingAccounts: PropTypes.number,
+ history: PropTypes.object,
+ t: PropTypes.func,
+}
+
+const mapStateToProps = state => {
+ const { metamask: { network, selectedAddress, identities = {} } } = state
+ const numberOfExistingAccounts = Object.keys(identities).length
+
+ return {
+ network,
+ address: selectedAddress,
+ numberOfExistingAccounts,
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ toCoinbase: address => dispatch(actions.buyEth({ network: '1', address, amount: 0 })),
+ hideModal: () => dispatch(actions.hideModal()),
+ createAccount: newAccountName => {
+ return dispatch(actions.addNewAccount())
+ .then(newAccountAddress => {
+ if (newAccountName) {
+ dispatch(actions.setAccountLabel(newAccountAddress, newAccountName))
+ }
+ })
+ },
+ showImportPage: () => dispatch(actions.showImportPage()),
+ showConnectPage: () => dispatch(actions.showConnectPage()),
+ }
+}
+
+NewAccountCreateForm.contextTypes = {
+ t: PropTypes.func,
+ metricsEvent: PropTypes.func,
+}
+
+module.exports = connect(mapStateToProps, mapDispatchToProps)(NewAccountCreateForm)
+
diff --git a/ui/app/pages/first-time-flow/create-password/create-password.component.js b/ui/app/pages/first-time-flow/create-password/create-password.component.js
new file mode 100644
index 000000000..5e67a2244
--- /dev/null
+++ b/ui/app/pages/first-time-flow/create-password/create-password.component.js
@@ -0,0 +1,71 @@
+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 {
+ INITIALIZE_CREATE_PASSWORD_ROUTE,
+ INITIALIZE_IMPORT_WITH_SEED_PHRASE_ROUTE,
+ INITIALIZE_SEED_PHRASE_ROUTE,
+} from '../../../helpers/constants/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_SEED_PHRASE_ROUTE)
+ }
+ }
+
+ render () {
+ const { onCreateNewAccount, onCreateNewAccountFromSeed } = this.props
+
+ return (
+ <div className="first-time-flow__wrapper">
+ <div className="app-header__logo-container">
+ <img
+ className="app-header__metafox-logo app-header__metafox-logo--horizontal"
+ src="/images/logo/metamask-logo-horizontal.svg"
+ height={30}
+ />
+ <img
+ className="app-header__metafox-logo app-header__metafox-logo--icon"
+ src="/images/logo/metamask-fox.svg"
+ height={42}
+ width={42}
+ />
+ </div>
+ <Switch>
+ <Route
+ exact
+ path={INITIALIZE_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>
+ )
+ }
+}
diff --git a/ui/app/pages/first-time-flow/create-password/create-password.container.js b/ui/app/pages/first-time-flow/create-password/create-password.container.js
new file mode 100644
index 000000000..89106f016
--- /dev/null
+++ b/ui/app/pages/first-time-flow/create-password/create-password.container.js
@@ -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)
diff --git a/ui/app/pages/first-time-flow/create-password/import-with-seed-phrase/import-with-seed-phrase.component.js b/ui/app/pages/first-time-flow/create-password/import-with-seed-phrase/import-with-seed-phrase.component.js
new file mode 100644
index 000000000..433dad6e2
--- /dev/null
+++ b/ui/app/pages/first-time-flow/create-password/import-with-seed-phrase/import-with-seed-phrase.component.js
@@ -0,0 +1,256 @@
+import {validateMnemonic} from 'bip39'
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import TextField from '../../../../components/ui/text-field'
+import Button from '../../../../components/ui/button'
+import {
+ INITIALIZE_SELECT_ACTION_ROUTE,
+ INITIALIZE_END_OF_FLOW_ROUTE,
+} from '../../../../helpers/constants/routes'
+
+export default class ImportWithSeedPhrase extends PureComponent {
+ static contextTypes = {
+ t: PropTypes.func,
+ metricsEvent: PropTypes.func,
+ }
+
+ static propTypes = {
+ history: PropTypes.object,
+ onSubmit: PropTypes.func.isRequired,
+ }
+
+ state = {
+ seedPhrase: '',
+ password: '',
+ confirmPassword: '',
+ seedPhraseError: '',
+ passwordError: '',
+ confirmPasswordError: '',
+ termsChecked: false,
+ }
+
+ parseSeedPhrase = (seedPhrase) => {
+ return seedPhrase
+ .trim()
+ .match(/\w+/g)
+ .join(' ')
+ }
+
+ handleSeedPhraseChange (seedPhrase) {
+ let seedPhraseError = ''
+
+ 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 { 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, this.parseSeedPhrase(seedPhrase))
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Onboarding',
+ action: 'Import Seed Phrase',
+ name: 'Import Complete',
+ },
+ })
+ history.push(INITIALIZE_END_OF_FLOW_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
+ }
+
+ toggleTermsCheck = () => {
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Onboarding',
+ action: 'Import Seed Phrase',
+ name: 'Check ToS',
+ },
+ })
+
+ this.setState((prevState) => ({
+ termsChecked: !prevState.termsChecked,
+ }))
+ }
+
+ render () {
+ const { t } = this.context
+ const { seedPhraseError, passwordError, confirmPasswordError, termsChecked } = this.state
+
+ return (
+ <form
+ className="first-time-flow__form"
+ onSubmit={this.handleImport}
+ >
+ <div className="first-time-flow__create-back">
+ <a
+ onClick={e => {
+ e.preventDefault()
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Onboarding',
+ action: 'Import Seed Phrase',
+ name: 'Go Back from Onboarding Import',
+ },
+ })
+ this.props.history.push(INITIALIZE_SELECT_ACTION_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
+ />
+ <div className="first-time-flow__checkbox-container" onClick={this.toggleTermsCheck}>
+ <div className="first-time-flow__checkbox">
+ {termsChecked ? <i className="fa fa-check fa-2x" /> : null}
+ </div>
+ <span className="first-time-flow__checkbox-label">
+ I have read and agree to the <a
+ href="https://metamask.io/terms.html"
+ target="_blank"
+ rel="noopener noreferrer"
+ >
+ <span className="first-time-flow__link-text">
+ { 'Terms of Use' }
+ </span>
+ </a>
+ </span>
+ </div>
+ <Button
+ type="confirm"
+ className="first-time-flow__button"
+ disabled={!this.isValid() || !termsChecked}
+ onClick={this.handleImport}
+ >
+ { t('import') }
+ </Button>
+ </form>
+ )
+ }
+}
diff --git a/ui/app/pages/first-time-flow/create-password/import-with-seed-phrase/index.js b/ui/app/pages/first-time-flow/create-password/import-with-seed-phrase/index.js
new file mode 100644
index 000000000..e5ff1fde5
--- /dev/null
+++ b/ui/app/pages/first-time-flow/create-password/import-with-seed-phrase/index.js
@@ -0,0 +1 @@
+export { default } from './import-with-seed-phrase.component'
diff --git a/ui/app/pages/first-time-flow/create-password/index.js b/ui/app/pages/first-time-flow/create-password/index.js
new file mode 100644
index 000000000..42e7436f9
--- /dev/null
+++ b/ui/app/pages/first-time-flow/create-password/index.js
@@ -0,0 +1 @@
+export { default } from './create-password.container'
diff --git a/ui/app/pages/first-time-flow/create-password/new-account/index.js b/ui/app/pages/first-time-flow/create-password/new-account/index.js
new file mode 100644
index 000000000..97db39cc3
--- /dev/null
+++ b/ui/app/pages/first-time-flow/create-password/new-account/index.js
@@ -0,0 +1 @@
+export { default } from './new-account.component'
diff --git a/ui/app/pages/first-time-flow/create-password/new-account/new-account.component.js b/ui/app/pages/first-time-flow/create-password/new-account/new-account.component.js
new file mode 100644
index 000000000..c040cff88
--- /dev/null
+++ b/ui/app/pages/first-time-flow/create-password/new-account/new-account.component.js
@@ -0,0 +1,225 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import Button from '../../../../components/ui/button'
+import {
+ INITIALIZE_SEED_PHRASE_ROUTE,
+ INITIALIZE_IMPORT_WITH_SEED_PHRASE_ROUTE,
+ INITIALIZE_SELECT_ACTION_ROUTE,
+} from '../../../../helpers/constants/routes'
+import TextField from '../../../../components/ui/text-field'
+
+export default class NewAccount extends PureComponent {
+ static contextTypes = {
+ metricsEvent: PropTypes.func,
+ t: PropTypes.func,
+ }
+
+ static propTypes = {
+ onSubmit: PropTypes.func.isRequired,
+ history: PropTypes.object.isRequired,
+ }
+
+ state = {
+ password: '',
+ confirmPassword: '',
+ passwordError: '',
+ confirmPasswordError: '',
+ termsChecked: false,
+ }
+
+ 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)
+
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Onboarding',
+ action: 'Create Password',
+ name: 'Submit Password',
+ },
+ })
+
+ history.push(INITIALIZE_SEED_PHRASE_ROUTE)
+ } catch (error) {
+ this.setState({ passwordError: error.message })
+ }
+ }
+
+ handleImportWithSeedPhrase = event => {
+ const { history } = this.props
+
+ event.preventDefault()
+ history.push(INITIALIZE_IMPORT_WITH_SEED_PHRASE_ROUTE)
+ }
+
+ toggleTermsCheck = () => {
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Onboarding',
+ action: 'Create Password',
+ name: 'Check ToS',
+ },
+ })
+
+ this.setState((prevState) => ({
+ termsChecked: !prevState.termsChecked,
+ }))
+ }
+
+ render () {
+ const { t } = this.context
+ const { password, confirmPassword, passwordError, confirmPasswordError, termsChecked } = this.state
+
+ return (
+ <div>
+ <div className="first-time-flow__create-back">
+ <a
+ onClick={e => {
+ e.preventDefault()
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Onboarding',
+ action: 'Create Password',
+ name: 'Go Back from Onboarding Create',
+ },
+ })
+ this.props.history.push(INITIALIZE_SELECT_ACTION_ROUTE)
+ }}
+ href="#"
+ >
+ {`< Back`}
+ </a>
+ </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
+ />
+ <div className="first-time-flow__checkbox-container" onClick={this.toggleTermsCheck}>
+ <div className="first-time-flow__checkbox">
+ {termsChecked ? <i className="fa fa-check fa-2x" /> : null}
+ </div>
+ <span className="first-time-flow__checkbox-label">
+ I have read and agree to the <a
+ href="https://metamask.io/terms.html"
+ target="_blank"
+ rel="noopener noreferrer"
+ >
+ <span className="first-time-flow__link-text">
+ { 'Terms of Use' }
+ </span>
+ </a>
+ </span>
+ </div>
+ <Button
+ type="confirm"
+ className="first-time-flow__button"
+ disabled={!this.isValid() || !termsChecked}
+ onClick={this.handleCreate}
+ >
+ { t('create') }
+ </Button>
+ </form>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/pages/first-time-flow/create-password/unique-image/index.js b/ui/app/pages/first-time-flow/create-password/unique-image/index.js
new file mode 100644
index 000000000..0e97bf755
--- /dev/null
+++ b/ui/app/pages/first-time-flow/create-password/unique-image/index.js
@@ -0,0 +1 @@
+export { default } from './unique-image.container'
diff --git a/ui/app/pages/first-time-flow/create-password/unique-image/unique-image.component.js b/ui/app/pages/first-time-flow/create-password/unique-image/unique-image.component.js
new file mode 100644
index 000000000..3434d117a
--- /dev/null
+++ b/ui/app/pages/first-time-flow/create-password/unique-image/unique-image.component.js
@@ -0,0 +1,55 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import Button from '../../../../components/ui/button'
+import { INITIALIZE_END_OF_FLOW_ROUTE } from '../../../../helpers/constants/routes'
+
+export default class UniqueImageScreen extends PureComponent {
+ static contextTypes = {
+ t: PropTypes.func,
+ metricsEvent: PropTypes.func,
+ }
+
+ static propTypes = {
+ history: PropTypes.object,
+ }
+
+ render () {
+ const { t } = this.context
+ const { history } = this.props
+
+ return (
+ <div>
+ <img
+ src="/images/sleuth.svg"
+ height={42}
+ width={42}
+ />
+ <div className="first-time-flow__header">
+ { t('protectYourKeys') }
+ </div>
+ <div className="first-time-flow__text-block">
+ { t('protectYourKeysMessage1') }
+ </div>
+ <div className="first-time-flow__text-block">
+ { t('protectYourKeysMessage2') }
+ </div>
+ <Button
+ type="confirm"
+ className="first-time-flow__button"
+ onClick={() => {
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Onboarding',
+ action: 'Agree to Phishing Warning',
+ name: 'Agree to Phishing Warning',
+ },
+ })
+ history.push(INITIALIZE_END_OF_FLOW_ROUTE)
+ }}
+ >
+ { t('next') }
+ </Button>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/pages/first-time-flow/create-password/unique-image/unique-image.container.js b/ui/app/pages/first-time-flow/create-password/unique-image/unique-image.container.js
new file mode 100644
index 000000000..34874aaec
--- /dev/null
+++ b/ui/app/pages/first-time-flow/create-password/unique-image/unique-image.container.js
@@ -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)
diff --git a/ui/app/pages/first-time-flow/end-of-flow/end-of-flow.component.js b/ui/app/pages/first-time-flow/end-of-flow/end-of-flow.component.js
new file mode 100644
index 000000000..c4292331b
--- /dev/null
+++ b/ui/app/pages/first-time-flow/end-of-flow/end-of-flow.component.js
@@ -0,0 +1,93 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import Button from '../../../components/ui/button'
+import { DEFAULT_ROUTE } from '../../../helpers/constants/routes'
+
+export default class EndOfFlowScreen extends PureComponent {
+ static contextTypes = {
+ t: PropTypes.func,
+ metricsEvent: PropTypes.func,
+ }
+
+ static propTypes = {
+ history: PropTypes.object,
+ completeOnboarding: PropTypes.func,
+ completionMetaMetricsName: PropTypes.string,
+ }
+
+ render () {
+ const { t } = this.context
+ const { history, completeOnboarding, completionMetaMetricsName } = this.props
+
+ return (
+ <div className="end-of-flow">
+ <div className="app-header__logo-container">
+ <img
+ className="app-header__metafox-logo app-header__metafox-logo--horizontal"
+ src="/images/logo/metamask-logo-horizontal.svg"
+ height={30}
+ />
+ <img
+ className="app-header__metafox-logo app-header__metafox-logo--icon"
+ src="/images/logo/metamask-fox.svg"
+ height={42}
+ width={42}
+ />
+ </div>
+ <div className="end-of-flow__emoji">🎉</div>
+ <div className="first-time-flow__header">
+ { t('congratulations') }
+ </div>
+ <div className="first-time-flow__text-block end-of-flow__text-1">
+ { t('endOfFlowMessage1') }
+ </div>
+ <div className="first-time-flow__text-block end-of-flow__text-2">
+ { t('endOfFlowMessage2') }
+ </div>
+ <div className="end-of-flow__text-3">
+ { '• ' + t('endOfFlowMessage3') }
+ </div>
+ <div className="end-of-flow__text-3">
+ { '• ' + t('endOfFlowMessage4') }
+ </div>
+ <div className="end-of-flow__text-3">
+ { '• ' + t('endOfFlowMessage5') }
+ </div>
+ <div className="end-of-flow__text-3">
+ { '• ' + t('endOfFlowMessage6') }
+ </div>
+ <div className="end-of-flow__text-3">
+ { '• ' + t('endOfFlowMessage7') }
+ </div>
+ <div className="first-time-flow__text-block end-of-flow__text-4">
+ *MetaMask cannot recover your seedphrase. <a
+ href="https://metamask.zendesk.com/hc/en-us/articles/360015489591-Basic-Safety-Tips"
+ target="_blank"
+ rel="noopener noreferrer"
+ >
+ <span className="first-time-flow__link-text">
+ Learn More
+ </span>
+ </a>.
+ </div>
+ <Button
+ type="confirm"
+ className="first-time-flow__button"
+ onClick={async () => {
+ await completeOnboarding()
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Onboarding',
+ action: 'Onboarding Complete',
+ name: completionMetaMetricsName,
+ },
+ })
+ history.push(DEFAULT_ROUTE)
+ }}
+ >
+ { 'All Done' }
+ </Button>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/pages/first-time-flow/end-of-flow/end-of-flow.container.js b/ui/app/pages/first-time-flow/end-of-flow/end-of-flow.container.js
new file mode 100644
index 000000000..38313806c
--- /dev/null
+++ b/ui/app/pages/first-time-flow/end-of-flow/end-of-flow.container.js
@@ -0,0 +1,25 @@
+import { connect } from 'react-redux'
+import EndOfFlow from './end-of-flow.component'
+import { setCompletedOnboarding } from '../../../store/actions'
+
+const firstTimeFlowTypeNameMap = {
+ create: 'New Wallet Created',
+ 'import': 'New Wallet Imported',
+}
+
+const mapStateToProps = ({ metamask }) => {
+ const { firstTimeFlowType } = metamask
+
+ return {
+ completionMetaMetricsName: firstTimeFlowTypeNameMap[firstTimeFlowType],
+ }
+}
+
+
+const mapDispatchToProps = dispatch => {
+ return {
+ completeOnboarding: () => dispatch(setCompletedOnboarding()),
+ }
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(EndOfFlow)
diff --git a/ui/app/pages/first-time-flow/end-of-flow/index.js b/ui/app/pages/first-time-flow/end-of-flow/index.js
new file mode 100644
index 000000000..b0643d155
--- /dev/null
+++ b/ui/app/pages/first-time-flow/end-of-flow/index.js
@@ -0,0 +1 @@
+export { default } from './end-of-flow.container'
diff --git a/ui/app/pages/first-time-flow/end-of-flow/index.scss b/ui/app/pages/first-time-flow/end-of-flow/index.scss
new file mode 100644
index 000000000..d7eb4513b
--- /dev/null
+++ b/ui/app/pages/first-time-flow/end-of-flow/index.scss
@@ -0,0 +1,53 @@
+.end-of-flow {
+ color: black;
+ font-family: Roboto;
+ font-style: normal;
+
+ .app-header__logo-container {
+ width: 742px;
+ margin-top: 3%;
+
+ @media screen and (max-width: $break-small) {
+ width: 100%;
+ }
+ }
+
+ &__text-1, &__text-3 {
+ font-weight: normal;
+ font-size: 16px;
+ margin-top: 18px;
+ }
+
+ &__text-2 {
+ font-weight: bold;
+ font-size: 16px;
+ margin-top: 26px;
+ }
+
+ &__text-3 {
+ margin-top: 2px;
+ margin-bottom: 2px;
+
+ @media screen and (max-width: $break-small) {
+ margin-bottom: 16px;
+ font-size: .875rem;
+ }
+ }
+
+ &__text-4 {
+ margin-top: 26px;
+ }
+
+ button {
+ width: 207px;
+ }
+
+ &__start-over-button {
+ width: 744px;
+ }
+
+ &__emoji {
+ font-size: 80px;
+ margin-top: 70px;
+ }
+} \ No newline at end of file
diff --git a/ui/app/pages/first-time-flow/first-time-flow-switch/first-time-flow-switch.component.js b/ui/app/pages/first-time-flow/first-time-flow-switch/first-time-flow-switch.component.js
new file mode 100644
index 000000000..4fd028482
--- /dev/null
+++ b/ui/app/pages/first-time-flow/first-time-flow-switch/first-time-flow-switch.component.js
@@ -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_UNLOCK_ROUTE,
+ INITIALIZE_SEED_PHRASE_ROUTE,
+ INITIALIZE_METAMETRICS_OPT_IN_ROUTE,
+} from '../../../helpers/constants/routes'
+
+export default class FirstTimeFlowSwitch extends PureComponent {
+ static propTypes = {
+ completedOnboarding: PropTypes.bool,
+ isInitialized: PropTypes.bool,
+ isUnlocked: PropTypes.bool,
+ seedPhrase: PropTypes.string,
+ optInMetaMetrics: PropTypes.bool,
+ }
+
+ render () {
+ const {
+ completedOnboarding,
+ isInitialized,
+ isUnlocked,
+ seedPhrase,
+ optInMetaMetrics,
+ } = 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 (seedPhrase) {
+ return <Redirect to={{ pathname: INITIALIZE_SEED_PHRASE_ROUTE }} />
+ }
+
+ if (optInMetaMetrics === null) {
+ return <Redirect to={{ pathname: INITIALIZE_WELCOME_ROUTE }} />
+ }
+
+ return <Redirect to={{ pathname: INITIALIZE_METAMETRICS_OPT_IN_ROUTE }} />
+ }
+}
diff --git a/ui/app/pages/first-time-flow/first-time-flow-switch/first-time-flow-switch.container.js b/ui/app/pages/first-time-flow/first-time-flow-switch/first-time-flow-switch.container.js
new file mode 100644
index 000000000..d68f7a153
--- /dev/null
+++ b/ui/app/pages/first-time-flow/first-time-flow-switch/first-time-flow-switch.container.js
@@ -0,0 +1,20 @@
+import { connect } from 'react-redux'
+import FirstTimeFlowSwitch from './first-time-flow-switch.component'
+
+const mapStateToProps = ({ metamask }) => {
+ const {
+ completedOnboarding,
+ isInitialized,
+ isUnlocked,
+ participateInMetaMetrics: optInMetaMetrics,
+ } = metamask
+
+ return {
+ completedOnboarding,
+ isInitialized,
+ isUnlocked,
+ optInMetaMetrics,
+ }
+}
+
+export default connect(mapStateToProps)(FirstTimeFlowSwitch)
diff --git a/ui/app/pages/first-time-flow/first-time-flow-switch/index.js b/ui/app/pages/first-time-flow/first-time-flow-switch/index.js
new file mode 100644
index 000000000..3647756ef
--- /dev/null
+++ b/ui/app/pages/first-time-flow/first-time-flow-switch/index.js
@@ -0,0 +1 @@
+export { default } from './first-time-flow-switch.container'
diff --git a/ui/app/pages/first-time-flow/first-time-flow.component.js b/ui/app/pages/first-time-flow/first-time-flow.component.js
new file mode 100644
index 000000000..bf6e80ca9
--- /dev/null
+++ b/ui/app/pages/first-time-flow/first-time-flow.component.js
@@ -0,0 +1,152 @@
+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 SelectAction from './select-action'
+import EndOfFlow from './end-of-flow'
+import Unlock from '../unlock-page'
+import CreatePassword from './create-password'
+import SeedPhrase from './seed-phrase'
+import MetaMetricsOptInScreen from './metametrics-opt-in'
+import {
+ DEFAULT_ROUTE,
+ INITIALIZE_WELCOME_ROUTE,
+ INITIALIZE_CREATE_PASSWORD_ROUTE,
+ INITIALIZE_SEED_PHRASE_ROUTE,
+ INITIALIZE_UNLOCK_ROUTE,
+ INITIALIZE_SELECT_ACTION_ROUTE,
+ INITIALIZE_END_OF_FLOW_ROUTE,
+ INITIALIZE_METAMETRICS_OPT_IN_ROUTE,
+} from '../../helpers/constants/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,
+ unlockAccount: PropTypes.func,
+ nextRoute: 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, nextRoute } = this.props
+
+ try {
+ const seedPhrase = await unlockAccount(password)
+ this.setState({ seedPhrase }, () => {
+ history.push(nextRoute)
+ })
+ } 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
+ path={INITIALIZE_CREATE_PASSWORD_ROUTE}
+ render={props => (
+ <CreatePassword
+ { ...props }
+ isImportedKeyring={isImportedKeyring}
+ onCreateNewAccount={this.handleCreateNewAccount}
+ onCreateNewAccountFromSeed={this.handleImportWithSeedPhrase}
+ />
+ )}
+ />
+ <Route
+ path={INITIALIZE_SELECT_ACTION_ROUTE}
+ component={SelectAction}
+ />
+ <Route
+ path={INITIALIZE_UNLOCK_ROUTE}
+ render={props => (
+ <Unlock
+ { ...props }
+ onSubmit={this.handleUnlock}
+ />
+ )}
+ />
+ <Route
+ exact
+ path={INITIALIZE_END_OF_FLOW_ROUTE}
+ component={EndOfFlow}
+ />
+ <Route
+ exact
+ path={INITIALIZE_WELCOME_ROUTE}
+ component={Welcome}
+ />
+ <Route
+ exact
+ path={INITIALIZE_METAMETRICS_OPT_IN_ROUTE}
+ component={MetaMetricsOptInScreen}
+ />
+ <Route
+ exact
+ path="*"
+ component={FirstTimeFlowSwitch}
+ />
+ </Switch>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/pages/first-time-flow/first-time-flow.container.js b/ui/app/pages/first-time-flow/first-time-flow.container.js
new file mode 100644
index 000000000..16025a489
--- /dev/null
+++ b/ui/app/pages/first-time-flow/first-time-flow.container.js
@@ -0,0 +1,31 @@
+import { connect } from 'react-redux'
+import FirstTimeFlow from './first-time-flow.component'
+import { getFirstTimeFlowTypeRoute } from './first-time-flow.selectors'
+import {
+ createNewVaultAndGetSeedPhrase,
+ createNewVaultAndRestore,
+ unlockAndGetSeedPhrase,
+} from '../../store/actions'
+
+const mapStateToProps = state => {
+ const { metamask: { completedOnboarding, isInitialized, isUnlocked } } = state
+
+ return {
+ completedOnboarding,
+ isInitialized,
+ isUnlocked,
+ nextRoute: getFirstTimeFlowTypeRoute(state),
+ }
+}
+
+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)
diff --git a/ui/app/pages/first-time-flow/first-time-flow.selectors.js b/ui/app/pages/first-time-flow/first-time-flow.selectors.js
new file mode 100644
index 000000000..e6cd5a84a
--- /dev/null
+++ b/ui/app/pages/first-time-flow/first-time-flow.selectors.js
@@ -0,0 +1,26 @@
+import {
+ INITIALIZE_CREATE_PASSWORD_ROUTE,
+ INITIALIZE_IMPORT_WITH_SEED_PHRASE_ROUTE,
+ DEFAULT_ROUTE,
+} from '../../helpers/constants/routes'
+
+const selectors = {
+ getFirstTimeFlowTypeRoute,
+}
+
+module.exports = selectors
+
+function getFirstTimeFlowTypeRoute (state) {
+ const { firstTimeFlowType } = state.metamask
+
+ let nextRoute
+ if (firstTimeFlowType === 'create') {
+ nextRoute = INITIALIZE_CREATE_PASSWORD_ROUTE
+ } else if (firstTimeFlowType === 'import') {
+ nextRoute = INITIALIZE_IMPORT_WITH_SEED_PHRASE_ROUTE
+ } else {
+ nextRoute = DEFAULT_ROUTE
+ }
+
+ return nextRoute
+}
diff --git a/ui/app/pages/first-time-flow/index.js b/ui/app/pages/first-time-flow/index.js
new file mode 100644
index 000000000..5db42437c
--- /dev/null
+++ b/ui/app/pages/first-time-flow/index.js
@@ -0,0 +1 @@
+export { default } from './first-time-flow.container'
diff --git a/ui/app/pages/first-time-flow/index.scss b/ui/app/pages/first-time-flow/index.scss
new file mode 100644
index 000000000..6c65cfdae
--- /dev/null
+++ b/ui/app/pages/first-time-flow/index.scss
@@ -0,0 +1,159 @@
+@import 'welcome/index';
+
+@import 'select-action/index';
+
+@import 'seed-phrase/index';
+
+@import 'end-of-flow/index';
+
+@import 'metametrics-opt-in/index';
+
+
+.first-time-flow {
+ width: 100%;
+ background-color: $white;
+ display: flex;
+ justify-content: center;
+
+ &__wrapper {
+ @media screen and (min-width: $break-large) {
+ max-width: 742px;
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ margin-top: 2%;
+ }
+
+ .app-header__metafox-logo {
+ margin-bottom: 40px;
+ }
+ }
+
+ &__form {
+ display: flex;
+ flex-direction: column;
+ }
+
+ &__create-back {
+ margin-bottom: 16px;
+ }
+
+ &__header {
+ font-size: 2.5rem;
+ margin-bottom: 24px;
+ color: black;
+ }
+
+ &__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;
+ color: black;
+
+ @media screen and (max-width: $break-small) {
+ margin-bottom: 16px;
+ font-size: .875rem;
+ }
+ }
+
+ &__button {
+ margin: 35px 0 14px;
+ width: 140px;
+ height: 44px;
+ }
+
+ &__checkbox-container {
+ display: flex;
+ align-items: center;
+ margin-top: 24px;
+ }
+
+ &__checkbox {
+ background: #FFFFFF;
+ border: 1px solid #CDCDCD;
+ box-sizing: border-box;
+ height: 34px;
+ width: 34px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ &:hover {
+ border: 1.5px solid #2f9ae0;
+ }
+
+ .fa-check {
+ color: #2f9ae0
+ }
+ }
+
+ &__checkbox-label {
+ font-family: Roboto;
+ font-style: normal;
+ font-weight: normal;
+ line-height: normal;
+ font-size: 18px;
+ color: #939090;
+ margin-left: 18px;
+ }
+
+ &__link-text {
+ color: $curious-blue;
+ }
+}
diff --git a/ui/app/pages/first-time-flow/metametrics-opt-in/index.js b/ui/app/pages/first-time-flow/metametrics-opt-in/index.js
new file mode 100644
index 000000000..4bc2fc3a7
--- /dev/null
+++ b/ui/app/pages/first-time-flow/metametrics-opt-in/index.js
@@ -0,0 +1 @@
+export { default } from './metametrics-opt-in.container'
diff --git a/ui/app/pages/first-time-flow/metametrics-opt-in/index.scss b/ui/app/pages/first-time-flow/metametrics-opt-in/index.scss
new file mode 100644
index 000000000..6c2e37785
--- /dev/null
+++ b/ui/app/pages/first-time-flow/metametrics-opt-in/index.scss
@@ -0,0 +1,136 @@
+.metametrics-opt-in {
+ position: relative;
+ width: 100%;
+
+ a {
+ color: #2f9ae0bf;
+ }
+
+ &__main {
+ display: flex;
+ flex-direction: column;
+ margin-left: 26.26%;
+ margin-right: 28%;
+ color: black;
+
+ @media screen and (max-width: 575px) {
+ justify-content: center;
+ margin-left: 2%;
+ margin-right: 0%;
+ }
+
+ .app-header__logo-container {
+ margin-top: 3%;
+ }
+ }
+
+ &__title {
+ position: relative;
+ margin-top: 20px;
+
+ font-family: Roboto;
+ font-style: normal;
+ font-weight: normal;
+ line-height: normal;
+ font-size: 42px;
+ }
+
+ &__body-graphic {
+ margin-top: 25px;
+
+ .fa-bar-chart {
+ color: #C4C4C4;
+ }
+ }
+
+ &__description {
+ font-family: Roboto;
+ font-style: normal;
+ font-weight: normal;
+ line-height: 21px;
+ font-size: 16px;
+ margin-top: 12px;
+ }
+
+ &__committments {
+ display: flex;
+ flex-direction: column;
+ }
+
+ &__content {
+ overflow-y: scroll;
+ flex: 1;
+ }
+
+ &__row {
+ display: flex;
+ margin-top: 8px;
+
+ .fa-check {
+ margin-right: 12px;
+ color: #1ACC56;
+ }
+
+ .fa-times {
+ margin-right: 12px;
+ color: #D0021B;
+ }
+ }
+
+ &__bold {
+ font-weight: bold;
+ }
+
+ &__break-row {
+ margin-top: 30px;
+ }
+
+ &__body {
+ position: relative;
+ display: flex;
+ max-width: 730px;
+ flex-direction: column;
+ }
+
+ &__body-text {
+ max-width: 548px;
+ margin-left: 16px;
+ margin-right: 16px;
+ }
+
+ &__bottom-text {
+ margin-top: 10px;
+ color: #9a9a9a;
+ }
+
+ &__content {
+ overflow-y: auto;
+ }
+
+ &__footer {
+ margin-top: 26px;
+
+ @media screen and (max-width: 575px) {
+ margin-top: 10px;
+ justify-content: center;
+ margin-left: 2%;
+ max-height: 520px;
+ }
+
+ .page-container__footer {
+ border-top: none;
+ max-width: 535px;
+ margin-bottom: 15px;
+
+ button {
+ height: 44px;
+ min-height: 44px;
+ margin-right: 16px;
+ }
+
+ header {
+ padding: 0px;
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/ui/app/pages/first-time-flow/metametrics-opt-in/metametrics-opt-in.component.js b/ui/app/pages/first-time-flow/metametrics-opt-in/metametrics-opt-in.component.js
new file mode 100644
index 000000000..19c668278
--- /dev/null
+++ b/ui/app/pages/first-time-flow/metametrics-opt-in/metametrics-opt-in.component.js
@@ -0,0 +1,169 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import PageContainerFooter from '../../../components/ui/page-container/page-container-footer'
+
+export default class MetaMetricsOptIn extends Component {
+ static propTypes = {
+ history: PropTypes.object,
+ setParticipateInMetaMetrics: PropTypes.func,
+ nextRoute: PropTypes.string,
+ firstTimeSelectionMetaMetricsName: PropTypes.string,
+ participateInMetaMetrics: PropTypes.bool,
+ }
+
+ static contextTypes = {
+ metricsEvent: PropTypes.func,
+ }
+
+ render () {
+ const { metricsEvent } = this.context
+ const {
+ nextRoute,
+ history,
+ setParticipateInMetaMetrics,
+ firstTimeSelectionMetaMetricsName,
+ participateInMetaMetrics,
+ } = this.props
+
+ return (
+ <div className="metametrics-opt-in">
+ <div className="metametrics-opt-in__main">
+ <div className="app-header__logo-container">
+ <img
+ className="app-header__metafox-logo app-header__metafox-logo--horizontal"
+ src="/images/logo/metamask-logo-horizontal.svg"
+ height={30}
+ />
+ <img
+ className="app-header__metafox-logo app-header__metafox-logo--icon"
+ src="/images/logo/metamask-fox.svg"
+ height={42}
+ width={42}
+ />
+ </div>
+ <div className="metametrics-opt-in__body-graphic">
+ <img src="images/metrics-chart.svg" />
+ </div>
+ <div className="metametrics-opt-in__title">Help Us Improve MetaMask</div>
+ <div className="metametrics-opt-in__body">
+ <div className="metametrics-opt-in__description">
+ MetaMask would like to gather usage data to better understand how our users interact with the extension. This data
+ will be used to continually improve the usability and user experience of our product and the Ethereum ecosystem.
+ </div>
+ <div className="metametrics-opt-in__description">
+ MetaMask will..
+ </div>
+
+ <div className="metametrics-opt-in__committments">
+ <div className="metametrics-opt-in__row">
+ <i className="fa fa-check" />
+ <div className="metametrics-opt-in__row-description">
+ Always allow you to opt-out via Settings
+ </div>
+ </div>
+ <div className="metametrics-opt-in__row">
+ <i className="fa fa-check" />
+ <div className="metametrics-opt-in__row-description">
+ Send anonymized click & pageview events
+ </div>
+ </div>
+ <div className="metametrics-opt-in__row">
+ <i className="fa fa-check" />
+ <div className="metametrics-opt-in__row-description">
+ Maintain a public aggregate dashboard to educate the community
+ </div>
+ </div>
+ <div className="metametrics-opt-in__row metametrics-opt-in__break-row">
+ <i className="fa fa-times" />
+ <div className="metametrics-opt-in__row-description">
+ <span className="metametrics-opt-in__bold">Never</span> collect keys, addresses, transactions, balances, hashes, or any personal information
+ </div>
+ </div>
+ <div className="metametrics-opt-in__row">
+ <i className="fa fa-times" />
+ <div className="metametrics-opt-in__row-description">
+ <span className="metametrics-opt-in__bold">Never</span> collect your full IP address
+ </div>
+ </div>
+ <div className="metametrics-opt-in__row">
+ <i className="fa fa-times" />
+ <div className="metametrics-opt-in__row-description">
+ <span className="metametrics-opt-in__bold">Never</span> sell data for profit. Ever!
+ </div>
+ </div>
+ </div>
+ </div>
+ <div className="metametrics-opt-in__footer">
+ <PageContainerFooter
+ onCancel={() => {
+ setParticipateInMetaMetrics(false)
+ .then(() => {
+ const promise = participateInMetaMetrics !== false
+ ? metricsEvent({
+ eventOpts: {
+ category: 'Onboarding',
+ action: 'Metrics Option',
+ name: 'Metrics Opt Out',
+ },
+ isOptIn: true,
+ })
+ : Promise.resolve()
+
+ promise
+ .then(() => {
+ history.push(nextRoute)
+ })
+ })
+ }}
+ cancelText={'No Thanks'}
+ hideCancel={false}
+ onSubmit={() => {
+ setParticipateInMetaMetrics(true)
+ .then(([participateStatus, metaMetricsId]) => {
+ const promise = participateInMetaMetrics !== true
+ ? metricsEvent({
+ eventOpts: {
+ category: 'Onboarding',
+ action: 'Metrics Option',
+ name: 'Metrics Opt In',
+ },
+ isOptIn: true,
+ })
+ : Promise.resolve()
+
+ promise
+ .then(() => {
+ return metricsEvent({
+ eventOpts: {
+ category: 'Onboarding',
+ action: 'Import or Create',
+ name: firstTimeSelectionMetaMetricsName,
+ },
+ isOptIn: true,
+ metaMetricsId,
+ })
+ })
+ .then(() => {
+ history.push(nextRoute)
+ })
+ })
+ }}
+ submitText={'I agree'}
+ submitButtonType={'confirm'}
+ disabled={false}
+ />
+ <div className="metametrics-opt-in__bottom-text">
+ This data is aggregated and is therefore anonymous for the purposes of General Data Protection Regulation (EU) 2016/679. For more information in relation to our privacy practices, please see our <a
+ href="https://metamask.io/privacy.html"
+ target="_blank"
+ rel="noopener noreferrer"
+ >
+ Privacy Policy here
+ </a>.
+ </div>
+ </div>
+ </div>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/pages/first-time-flow/metametrics-opt-in/metametrics-opt-in.container.js b/ui/app/pages/first-time-flow/metametrics-opt-in/metametrics-opt-in.container.js
new file mode 100644
index 000000000..2566a2a56
--- /dev/null
+++ b/ui/app/pages/first-time-flow/metametrics-opt-in/metametrics-opt-in.container.js
@@ -0,0 +1,27 @@
+import { connect } from 'react-redux'
+import MetaMetricsOptIn from './metametrics-opt-in.component'
+import { setParticipateInMetaMetrics } from '../../../store/actions'
+import { getFirstTimeFlowTypeRoute } from '../first-time-flow.selectors'
+
+const firstTimeFlowTypeNameMap = {
+ create: 'Selected Create New Wallet',
+ 'import': 'Selected Import Wallet',
+}
+
+const mapStateToProps = (state) => {
+ const { firstTimeFlowType, participateInMetaMetrics } = state.metamask
+
+ return {
+ nextRoute: getFirstTimeFlowTypeRoute(state),
+ firstTimeSelectionMetaMetricsName: firstTimeFlowTypeNameMap[firstTimeFlowType],
+ participateInMetaMetrics,
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ setParticipateInMetaMetrics: (val) => dispatch(setParticipateInMetaMetrics(val)),
+ }
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(MetaMetricsOptIn)
diff --git a/ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/confirm-seed-phrase.component.js b/ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/confirm-seed-phrase.component.js
new file mode 100644
index 000000000..59b4f73a6
--- /dev/null
+++ b/ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/confirm-seed-phrase.component.js
@@ -0,0 +1,155 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import classnames from 'classnames'
+import shuffle from 'lodash.shuffle'
+import Button from '../../../../components/ui/button'
+import {
+ INITIALIZE_END_OF_FLOW_ROUTE,
+ INITIALIZE_SEED_PHRASE_ROUTE,
+} from '../../../../helpers/constants/routes'
+import { exportAsFile } from '../../../../helpers/utils/util'
+import { selectSeedWord, deselectSeedWord } from './confirm-seed-phrase.state'
+
+export default class ConfirmSeedPhrase extends PureComponent {
+ static contextTypes = {
+ metricsEvent: PropTypes.func,
+ t: PropTypes.func,
+ }
+
+ static defaultProps = {
+ seedPhrase: '',
+ }
+
+ static propTypes = {
+ history: PropTypes.object,
+ onSubmit: 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 { history } = this.props
+
+ if (!this.isValid()) {
+ return
+ }
+
+ try {
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Onboarding',
+ action: 'Seed Phrase Setup',
+ name: 'Verify Complete',
+ },
+ })
+ history.push(INITIALIZE_END_OF_FLOW_ROUTE)
+ } 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 { history } = this.props
+ const { selectedSeedWords, shuffledSeedWords, selectedSeedWordsHash } = this.state
+
+ return (
+ <div className="confirm-seed-phrase">
+ <div className="confirm-seed-phrase__back-button">
+ <a
+ onClick={e => {
+ e.preventDefault()
+ history.push(INITIALIZE_SEED_PHRASE_ROUTE)
+ }}
+ href="#"
+ >
+ {`< Back`}
+ </a>
+ </div>
+ <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="confirm"
+ className="first-time-flow__button"
+ onClick={this.handleSubmit}
+ disabled={!this.isValid()}
+ >
+ { t('confirm') }
+ </Button>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/confirm-seed-phrase.state.js b/ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/confirm-seed-phrase.state.js
new file mode 100644
index 000000000..f2476fc5c
--- /dev/null
+++ b/ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/confirm-seed-phrase.state.js
@@ -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,
+ }
+ }
+}
diff --git a/ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/index.js b/ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/index.js
new file mode 100644
index 000000000..c7b511503
--- /dev/null
+++ b/ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/index.js
@@ -0,0 +1 @@
+export { default } from './confirm-seed-phrase.component'
diff --git a/ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/index.scss b/ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/index.scss
new file mode 100644
index 000000000..93137618c
--- /dev/null
+++ b/ui/app/pages/first-time-flow/seed-phrase/confirm-seed-phrase/index.scss
@@ -0,0 +1,48 @@
+.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;
+ }
+ }
+
+ button {
+ margin-top: 0xp;
+ }
+}
diff --git a/ui/app/pages/first-time-flow/seed-phrase/index.js b/ui/app/pages/first-time-flow/seed-phrase/index.js
new file mode 100644
index 000000000..185b3f089
--- /dev/null
+++ b/ui/app/pages/first-time-flow/seed-phrase/index.js
@@ -0,0 +1 @@
+export { default } from './seed-phrase.component'
diff --git a/ui/app/pages/first-time-flow/seed-phrase/index.scss b/ui/app/pages/first-time-flow/seed-phrase/index.scss
new file mode 100644
index 000000000..24da45ded
--- /dev/null
+++ b/ui/app/pages/first-time-flow/seed-phrase/index.scss
@@ -0,0 +1,40 @@
+@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: 81px;
+ }
+
+ @media screen and (max-width: $break-small) {
+ margin-top: 24px;
+ }
+
+ .first-time-flow__text-block {
+ color: #5A5A5A;
+ }
+ }
+}
diff --git a/ui/app/pages/first-time-flow/seed-phrase/reveal-seed-phrase/index.js b/ui/app/pages/first-time-flow/seed-phrase/reveal-seed-phrase/index.js
new file mode 100644
index 000000000..4a1b191b5
--- /dev/null
+++ b/ui/app/pages/first-time-flow/seed-phrase/reveal-seed-phrase/index.js
@@ -0,0 +1 @@
+export { default } from './reveal-seed-phrase.component'
diff --git a/ui/app/pages/first-time-flow/seed-phrase/reveal-seed-phrase/index.scss b/ui/app/pages/first-time-flow/seed-phrase/reveal-seed-phrase/index.scss
new file mode 100644
index 000000000..8a47447ed
--- /dev/null
+++ b/ui/app/pages/first-time-flow/seed-phrase/reveal-seed-phrase/index.scss
@@ -0,0 +1,57 @@
+.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;
+ }
+
+ button {
+ margin-top: 0xp;
+ }
+}
diff --git a/ui/app/pages/first-time-flow/seed-phrase/reveal-seed-phrase/reveal-seed-phrase.component.js b/ui/app/pages/first-time-flow/seed-phrase/reveal-seed-phrase/reveal-seed-phrase.component.js
new file mode 100644
index 000000000..ee352d74e
--- /dev/null
+++ b/ui/app/pages/first-time-flow/seed-phrase/reveal-seed-phrase/reveal-seed-phrase.component.js
@@ -0,0 +1,143 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import classnames from 'classnames'
+import LockIcon from '../../../../components/ui/lock-icon'
+import Button from '../../../../components/ui/button'
+import { INITIALIZE_CONFIRM_SEED_PHRASE_ROUTE } from '../../../../helpers/constants/routes'
+import { exportAsFile } from '../../../../helpers/utils/util'
+
+export default class RevealSeedPhrase extends PureComponent {
+ static contextTypes = {
+ t: PropTypes.func,
+ metricsEvent: PropTypes.func,
+ }
+
+ static propTypes = {
+ 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
+
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Onboarding',
+ action: 'Seed Phrase Setup',
+ name: 'Advance to Verify',
+ },
+ })
+
+ 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.context.metricsEvent({
+ eventOpts: {
+ category: 'Onboarding',
+ action: 'Seed Phrase Setup',
+ name: 'Revealed Words',
+ },
+ })
+ 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 { isShowingSeedPhrase } = this.state
+
+ return (
+ <div className="reveal-seed-phrase">
+ <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="confirm"
+ className="first-time-flow__button"
+ onClick={this.handleNext}
+ disabled={!isShowingSeedPhrase}
+ >
+ { t('next') }
+ </Button>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/pages/first-time-flow/seed-phrase/seed-phrase.component.js b/ui/app/pages/first-time-flow/seed-phrase/seed-phrase.component.js
new file mode 100644
index 000000000..9a9f84049
--- /dev/null
+++ b/ui/app/pages/first-time-flow/seed-phrase/seed-phrase.component.js
@@ -0,0 +1,70 @@
+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 '../../../helpers/constants/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 { seedPhrase } = this.props
+
+ return (
+ <div className="first-time-flow__wrapper">
+ <div className="app-header__logo-container">
+ <img
+ className="app-header__metafox-logo app-header__metafox-logo--horizontal"
+ src="/images/logo/metamask-logo-horizontal.svg"
+ height={30}
+ />
+ <img
+ className="app-header__metafox-logo app-header__metafox-logo--icon"
+ src="/images/logo/metamask-fox.svg"
+ height={42}
+ width={42}
+ />
+ </div>
+ <Switch>
+ <Route
+ exact
+ path={INITIALIZE_CONFIRM_SEED_PHRASE_ROUTE}
+ render={props => (
+ <ConfirmSeedPhrase
+ { ...props }
+ seedPhrase={seedPhrase}
+ />
+ )}
+ />
+ <Route
+ exact
+ path={INITIALIZE_SEED_PHRASE_ROUTE}
+ render={props => (
+ <RevealSeedPhrase
+ { ...props }
+ seedPhrase={seedPhrase}
+ />
+ )}
+ />
+ </Switch>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/pages/first-time-flow/select-action/index.js b/ui/app/pages/first-time-flow/select-action/index.js
new file mode 100644
index 000000000..4fbe1823b
--- /dev/null
+++ b/ui/app/pages/first-time-flow/select-action/index.js
@@ -0,0 +1 @@
+export { default } from './select-action.container'
diff --git a/ui/app/pages/first-time-flow/select-action/index.scss b/ui/app/pages/first-time-flow/select-action/index.scss
new file mode 100644
index 000000000..e1b22d05b
--- /dev/null
+++ b/ui/app/pages/first-time-flow/select-action/index.scss
@@ -0,0 +1,88 @@
+.select-action {
+ .app-header__logo-container {
+ width: 742px;
+ margin-top: 3%;
+ }
+
+ &__body {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ }
+
+ &__body-header {
+ font-family: Roboto;
+ font-style: normal;
+ font-weight: normal;
+ line-height: 39px;
+ font-size: 28px;
+ text-align: center;
+ margin-top: 65px;
+ color: black;
+ }
+
+ &__select-buttons {
+ display: flex;
+ flex-direction: row;
+ margin-top: 40px;
+ }
+
+ &__select-button {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: space-evenly;
+ width: 388px;
+ height: 278px;
+
+ border: 1px solid #D8D8D8;
+ box-sizing: border-box;
+ border-radius: 10px;
+ margin-left: 22px;
+
+ .first-time-flow__button {
+ max-width: 221px;
+ height: 44px;
+ }
+ }
+
+ &__button-symbol {
+ color: #C4C4C4;
+ margin-top: 41px;
+ }
+
+ &__button-content {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ height: 144px;
+ }
+
+ &__button-text-big {
+ font-family: Roboto;
+ font-style: normal;
+ font-weight: normal;
+ line-height: 28px;
+ font-size: 20px;
+ color: #000000;
+ margin-top: 12px;
+ text-align: center;
+ }
+
+ &__button-text-small {
+ font-family: Roboto;
+ font-style: normal;
+ font-weight: normal;
+ line-height: 20px;
+ font-size: 14px;
+ color: #7A7A7B;
+ margin-top: 10px;
+ text-align: center;
+ }
+
+ button {
+ font-weight: 500;
+ width: 221px;
+ }
+} \ No newline at end of file
diff --git a/ui/app/pages/first-time-flow/select-action/select-action.component.js b/ui/app/pages/first-time-flow/select-action/select-action.component.js
new file mode 100644
index 000000000..b25a15514
--- /dev/null
+++ b/ui/app/pages/first-time-flow/select-action/select-action.component.js
@@ -0,0 +1,112 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import Button from '../../../components/ui/button'
+import {
+ INITIALIZE_METAMETRICS_OPT_IN_ROUTE,
+} from '../../../helpers/constants/routes'
+
+export default class SelectAction extends PureComponent {
+ static propTypes = {
+ history: PropTypes.object,
+ isInitialized: PropTypes.bool,
+ setFirstTimeFlowType: PropTypes.func,
+ nextRoute: PropTypes.string,
+ }
+
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ componentDidMount () {
+ const { history, isInitialized, nextRoute } = this.props
+
+ if (isInitialized) {
+ history.push(nextRoute)
+ }
+ }
+
+ handleCreate = () => {
+ this.props.setFirstTimeFlowType('create')
+ this.props.history.push(INITIALIZE_METAMETRICS_OPT_IN_ROUTE)
+ }
+
+ handleImport = () => {
+ this.props.setFirstTimeFlowType('import')
+ this.props.history.push(INITIALIZE_METAMETRICS_OPT_IN_ROUTE)
+ }
+
+ render () {
+ const { t } = this.context
+
+ return (
+ <div className="select-action">
+ <div className="app-header__logo-container">
+ <img
+ className="app-header__metafox-logo app-header__metafox-logo--horizontal"
+ src="/images/logo/metamask-logo-horizontal.svg"
+ height={30}
+ />
+ <img
+ className="app-header__metafox-logo app-header__metafox-logo--icon"
+ src="/images/logo/metamask-fox.svg"
+ height={42}
+ width={42}
+ />
+ </div>
+
+ <div className="select-action__wrapper">
+
+
+ <div className="select-action__body">
+ <div className="select-action__body-header">
+ { t('newToMetaMask') }
+ </div>
+ <div className="select-action__select-buttons">
+ <div className="select-action__select-button">
+ <div className="select-action__button-content">
+ <div className="select-action__button-symbol">
+ <img src="/images/download-alt.svg" />
+ </div>
+ <div className="select-action__button-text-big">
+ { t('noAlreadyHaveSeed') }
+ </div>
+ <div className="select-action__button-text-small">
+ { t('importYourExisting') }
+ </div>
+ </div>
+ <Button
+ type="primary"
+ className="first-time-flow__button"
+ onClick={this.handleImport}
+ >
+ { t('importWallet') }
+ </Button>
+ </div>
+ <div className="select-action__select-button">
+ <div className="select-action__button-content">
+ <div className="select-action__button-symbol">
+ <img src="/images/thin-plus.svg" />
+ </div>
+ <div className="select-action__button-text-big">
+ { t('letsGoSetUp') }
+ </div>
+ <div className="select-action__button-text-small">
+ { t('thisWillCreate') }
+ </div>
+ </div>
+ <Button
+ type="confirm"
+ className="first-time-flow__button"
+ onClick={this.handleCreate}
+ >
+ { t('createAWallet') }
+ </Button>
+ </div>
+ </div>
+ </div>
+
+ </div>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/pages/first-time-flow/select-action/select-action.container.js b/ui/app/pages/first-time-flow/select-action/select-action.container.js
new file mode 100644
index 000000000..9dc988430
--- /dev/null
+++ b/ui/app/pages/first-time-flow/select-action/select-action.container.js
@@ -0,0 +1,23 @@
+import { connect } from 'react-redux'
+import { withRouter } from 'react-router-dom'
+import { compose } from 'recompose'
+import { setFirstTimeFlowType } from '../../../store/actions'
+import { getFirstTimeFlowTypeRoute } from '../first-time-flow.selectors'
+import Welcome from './select-action.component'
+
+const mapStateToProps = (state) => {
+ return {
+ nextRoute: getFirstTimeFlowTypeRoute(state),
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ setFirstTimeFlowType: type => dispatch(setFirstTimeFlowType(type)),
+ }
+}
+
+export default compose(
+ withRouter,
+ connect(mapStateToProps, mapDispatchToProps)
+)(Welcome)
diff --git a/ui/app/pages/first-time-flow/welcome/index.js b/ui/app/pages/first-time-flow/welcome/index.js
new file mode 100644
index 000000000..8abeddaa1
--- /dev/null
+++ b/ui/app/pages/first-time-flow/welcome/index.js
@@ -0,0 +1 @@
+export { default } from './welcome.container'
diff --git a/ui/app/pages/first-time-flow/welcome/index.scss b/ui/app/pages/first-time-flow/welcome/index.scss
new file mode 100644
index 000000000..3b5071480
--- /dev/null
+++ b/ui/app/pages/first-time-flow/welcome/index.scss
@@ -0,0 +1,42 @@
+.welcome-page {
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-start;
+ align-items: center;
+ max-width: 442px;
+ padding: 0 18px;
+ color: black;
+
+ &__wrapper {
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+ align-items: flex-start;
+ height: 100%;
+ margin-top: 110px;
+ }
+
+ &__header {
+ font-size: 28px;
+ margin-bottom: 22px;
+ margin-top: 50px;
+ }
+
+ &__description {
+ text-align: center;
+
+ div {
+ font-size: 16px;
+ }
+
+ @media screen and (max-width: 575px) {
+ font-size: .9rem;
+ }
+ }
+
+ .first-time-flow__button {
+ width: 184px;
+ font-weight: 500;
+ margin-top: 44px;
+ }
+}
diff --git a/ui/app/pages/first-time-flow/welcome/welcome.component.js b/ui/app/pages/first-time-flow/welcome/welcome.component.js
new file mode 100644
index 000000000..3b8d6eb17
--- /dev/null
+++ b/ui/app/pages/first-time-flow/welcome/welcome.component.js
@@ -0,0 +1,69 @@
+import EventEmitter from 'events'
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import Mascot from '../../../components/ui/mascot'
+import Button from '../../../components/ui/button'
+import { INITIALIZE_CREATE_PASSWORD_ROUTE, INITIALIZE_SELECT_ACTION_ROUTE } from '../../../helpers/constants/routes'
+
+export default class Welcome extends PureComponent {
+ static propTypes = {
+ history: PropTypes.object,
+ isInitialized: PropTypes.bool,
+ participateInMetaMetrics: PropTypes.bool,
+ welcomeScreenSeen: PropTypes.bool,
+ }
+
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ constructor (props) {
+ super(props)
+
+ this.animationEventEmitter = new EventEmitter()
+ }
+
+ componentDidMount () {
+ const { history, participateInMetaMetrics, welcomeScreenSeen } = this.props
+
+ if (welcomeScreenSeen && participateInMetaMetrics !== null) {
+ history.push(INITIALIZE_CREATE_PASSWORD_ROUTE)
+ } else if (welcomeScreenSeen) {
+ history.push(INITIALIZE_SELECT_ACTION_ROUTE)
+ }
+ }
+
+ handleContinue = () => {
+ this.props.history.push(INITIALIZE_SELECT_ACTION_ROUTE)
+ }
+
+ render () {
+ const { t } = this.context
+
+ return (
+ <div className="welcome-page__wrapper">
+ <div className="welcome-page">
+ <Mascot
+ animationEventEmitter={this.animationEventEmitter}
+ width="125"
+ height="125"
+ />
+ <div className="welcome-page__header">
+ { t('welcome') }
+ </div>
+ <div className="welcome-page__description">
+ <div>{ t('metamaskDescription') }</div>
+ <div>{ t('happyToSeeYou') }</div>
+ </div>
+ <Button
+ type="confirm"
+ className="first-time-flow__button"
+ onClick={this.handleContinue}
+ >
+ { t('getStarted') }
+ </Button>
+ </div>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/pages/first-time-flow/welcome/welcome.container.js b/ui/app/pages/first-time-flow/welcome/welcome.container.js
new file mode 100644
index 000000000..ce4b2b471
--- /dev/null
+++ b/ui/app/pages/first-time-flow/welcome/welcome.container.js
@@ -0,0 +1,26 @@
+import { connect } from 'react-redux'
+import { withRouter } from 'react-router-dom'
+import { compose } from 'recompose'
+import { closeWelcomeScreen } from '../../../store/actions'
+import Welcome from './welcome.component'
+
+const mapStateToProps = ({ metamask }) => {
+ const { welcomeScreenSeen, isInitialized, participateInMetaMetrics } = metamask
+
+ return {
+ welcomeScreenSeen,
+ isInitialized,
+ participateInMetaMetrics,
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ closeWelcomeScreen: () => dispatch(closeWelcomeScreen()),
+ }
+}
+
+export default compose(
+ withRouter,
+ connect(mapStateToProps, mapDispatchToProps)
+)(Welcome)
diff --git a/ui/app/pages/home/home.component.js b/ui/app/pages/home/home.component.js
new file mode 100644
index 000000000..29d93a9fa
--- /dev/null
+++ b/ui/app/pages/home/home.component.js
@@ -0,0 +1,77 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import Media from 'react-media'
+import { Redirect } from 'react-router-dom'
+import WalletView from '../../components/app/wallet-view'
+import TransactionView from '../../components/app/transaction-view'
+import ProviderApproval from '../provider-approval'
+
+import {
+ INITIALIZE_SEED_PHRASE_ROUTE,
+ RESTORE_VAULT_ROUTE,
+ CONFIRM_TRANSACTION_ROUTE,
+ CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE,
+} from '../../helpers/constants/routes'
+
+export default class Home extends PureComponent {
+ static propTypes = {
+ history: PropTypes.object,
+ forgottenPassword: PropTypes.bool,
+ seedWords: PropTypes.string,
+ suggestedTokens: PropTypes.object,
+ unconfirmedTransactionsCount: PropTypes.number,
+ providerRequests: PropTypes.array,
+ }
+
+ componentDidMount () {
+ const {
+ history,
+ suggestedTokens = {},
+ unconfirmedTransactionsCount = 0,
+ } = this.props
+
+ // suggested new tokens
+ if (Object.keys(suggestedTokens).length > 0) {
+ history.push(CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE)
+ }
+
+ if (unconfirmedTransactionsCount > 0) {
+ history.push(CONFIRM_TRANSACTION_ROUTE)
+ }
+ }
+
+ render () {
+ const {
+ forgottenPassword,
+ seedWords,
+ providerRequests,
+ } = this.props
+
+ // seed words
+ if (seedWords) {
+ return <Redirect to={{ pathname: INITIALIZE_SEED_PHRASE_ROUTE }}/>
+ }
+
+ if (forgottenPassword) {
+ return <Redirect to={{ pathname: RESTORE_VAULT_ROUTE }} />
+ }
+
+ if (providerRequests && providerRequests.length > 0) {
+ return (
+ <ProviderApproval providerRequest={providerRequests[0]} />
+ )
+ }
+
+ return (
+ <div className="main-container">
+ <div className="account-and-transaction-details">
+ <Media
+ query="(min-width: 576px)"
+ render={() => <WalletView />}
+ />
+ <TransactionView />
+ </div>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/pages/home/home.container.js b/ui/app/pages/home/home.container.js
new file mode 100644
index 000000000..02ec4b9c6
--- /dev/null
+++ b/ui/app/pages/home/home.container.js
@@ -0,0 +1,32 @@
+import Home from './home.component'
+import { compose } from 'recompose'
+import { connect } from 'react-redux'
+import { withRouter } from 'react-router-dom'
+import { unconfirmedTransactionsCountSelector } from '../../selectors/confirm-transaction'
+
+const mapStateToProps = state => {
+ const { metamask, appState } = state
+ const {
+ noActiveNotices,
+ lostAccounts,
+ seedWords,
+ suggestedTokens,
+ providerRequests,
+ } = metamask
+ const { forgottenPassword } = appState
+
+ return {
+ noActiveNotices,
+ lostAccounts,
+ forgottenPassword,
+ seedWords,
+ suggestedTokens,
+ unconfirmedTransactionsCount: unconfirmedTransactionsCountSelector(state),
+ providerRequests,
+ }
+}
+
+export default compose(
+ withRouter,
+ connect(mapStateToProps)
+)(Home)
diff --git a/ui/app/pages/home/index.js b/ui/app/pages/home/index.js
new file mode 100644
index 000000000..4474ba5b8
--- /dev/null
+++ b/ui/app/pages/home/index.js
@@ -0,0 +1 @@
+export { default } from './home.container'
diff --git a/ui/app/pages/index.js b/ui/app/pages/index.js
new file mode 100644
index 000000000..56fc4af04
--- /dev/null
+++ b/ui/app/pages/index.js
@@ -0,0 +1,31 @@
+import React, { Component } from 'react'
+const PropTypes = require('prop-types')
+const { Provider } = require('react-redux')
+const { HashRouter } = require('react-router-dom')
+const Routes = require('./routes')
+const I18nProvider = require('../helpers/higher-order-components/i18n-provider')
+const MetaMetricsProvider = require('../helpers/higher-order-components/metametrics/metametrics.provider')
+
+class Index extends Component {
+ render () {
+ const { store } = this.props
+
+ return (
+ <Provider store={store}>
+ <HashRouter hashType="noslash">
+ <MetaMetricsProvider>
+ <I18nProvider>
+ <Routes />
+ </I18nProvider>
+ </MetaMetricsProvider>
+ </HashRouter>
+ </Provider>
+ )
+ }
+}
+
+Index.propTypes = {
+ store: PropTypes.object,
+}
+
+module.exports = Index
diff --git a/ui/app/pages/index.scss b/ui/app/pages/index.scss
new file mode 100644
index 000000000..cb9f0d80c
--- /dev/null
+++ b/ui/app/pages/index.scss
@@ -0,0 +1,11 @@
+@import 'unlock-page/index';
+
+@import 'add-token/index';
+
+@import 'confirm-add-token/index';
+
+@import 'settings/index';
+
+@import 'first-time-flow/index';
+
+@import 'keychains/index';
diff --git a/ui/app/pages/keychains/index.scss b/ui/app/pages/keychains/index.scss
new file mode 100644
index 000000000..868185419
--- /dev/null
+++ b/ui/app/pages/keychains/index.scss
@@ -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;
+}
diff --git a/ui/app/pages/keychains/restore-vault.js b/ui/app/pages/keychains/restore-vault.js
new file mode 100644
index 000000000..574949258
--- /dev/null
+++ b/ui/app/pages/keychains/restore-vault.js
@@ -0,0 +1,197 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import {connect} from 'react-redux'
+import {
+ createNewVaultAndRestore,
+ unMarkPasswordForgotten,
+} from '../../store/actions'
+import { DEFAULT_ROUTE } from '../../helpers/constants/routes'
+import TextField from '../../components/ui/text-field'
+import Button from '../../components/ui/button'
+
+class RestoreVaultPage extends Component {
+ static contextTypes = {
+ t: PropTypes.func,
+ metricsEvent: 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
+ .match(/\w+/g)
+ .join(' ')
+ }
+
+ handleSeedPhraseChange (seedPhrase) {
+ let seedPhraseError = null
+
+ if (seedPhrase && this.parseSeedPhrase(seedPhrase).split(' ').length !== 12) {
+ seedPhraseError = this.context.t('seedPhraseReq')
+ }
+
+ 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(() => {
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Retention',
+ action: 'userEntersSeedPhrase',
+ name: 'onboardingRestoredVault',
+ },
+ })
+ history.push(DEFAULT_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">
+ { this.context.t('restoreAccountWithSeed') }
+ </div>
+ <div className="import-account__selector-label">
+ { this.context.t('secretPhrase') }
+ </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={this.context.t('separateEachWord')}
+ />
+ </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
+ type="first-time"
+ className="first-time-flow__button"
+ onClick={() => !disabled && this.onClick()}
+ disabled={disabled}
+ >
+ {this.context.t('restore')}
+ </Button>
+ </div>
+ </div>
+ </div>
+ )
+ }
+}
+
+export default connect(
+ ({ appState: { warning, isLoading } }) => ({ warning, isLoading }),
+ dispatch => ({
+ leaveImportSeedScreenState: () => {
+ dispatch(unMarkPasswordForgotten())
+ },
+ createNewVaultAndRestore: (pw, seed) => dispatch(createNewVaultAndRestore(pw, seed)),
+ })
+)(RestoreVaultPage)
diff --git a/ui/app/pages/keychains/reveal-seed.js b/ui/app/pages/keychains/reveal-seed.js
new file mode 100644
index 000000000..edc9db5a0
--- /dev/null
+++ b/ui/app/pages/keychains/reveal-seed.js
@@ -0,0 +1,177 @@
+const { Component } = require('react')
+const { connect } = require('react-redux')
+const PropTypes = require('prop-types')
+const h = require('react-hyperscript')
+const classnames = require('classnames')
+
+const { requestRevealSeedWords } = require('../../store/actions')
+const { DEFAULT_ROUTE } = require('../../helpers/constants/routes')
+const ExportTextContainer = require('../../components/ui/export-text-container')
+
+import Button from '../../components/ui/button'
+
+const PASSWORD_PROMPT_SCREEN = 'PASSWORD_PROMPT_SCREEN'
+const REVEAL_SEED_SCREEN = 'REVEAL_SEED_SCREEN'
+
+class RevealSeedPage extends Component {
+ constructor (props) {
+ super(props)
+
+ this.state = {
+ screen: PASSWORD_PROMPT_SCREEN,
+ password: '',
+ seedWords: null,
+ error: null,
+ }
+ }
+
+ componentDidMount () {
+ const passwordBox = document.getElementById('password-box')
+ if (passwordBox) {
+ passwordBox.focus()
+ }
+ }
+
+ handleSubmit (event) {
+ event.preventDefault()
+ this.setState({ seedWords: null, error: null })
+ this.props.requestRevealSeedWords(this.state.password)
+ .then(seedWords => this.setState({ seedWords, screen: REVEAL_SEED_SCREEN }))
+ .catch(error => this.setState({ error: error.message }))
+ }
+
+ renderWarning () {
+ return (
+ h('.page-container__warning-container', [
+ h('img.page-container__warning-icon', {
+ src: 'images/warning.svg',
+ }),
+ h('.page-container__warning-message', [
+ h('.page-container__warning-title', [this.context.t('revealSeedWordsWarningTitle')]),
+ h('div', [this.context.t('revealSeedWordsWarning')]),
+ ]),
+ ])
+ )
+ }
+
+ renderContent () {
+ return this.state.screen === PASSWORD_PROMPT_SCREEN
+ ? this.renderPasswordPromptContent()
+ : this.renderRevealSeedContent()
+ }
+
+ renderPasswordPromptContent () {
+ const { t } = this.context
+
+ return (
+ h('form', {
+ onSubmit: event => this.handleSubmit(event),
+ }, [
+ h('label.input-label', {
+ htmlFor: 'password-box',
+ }, t('enterPasswordContinue')),
+ h('.input-group', [
+ h('input.form-control', {
+ type: 'password',
+ placeholder: t('password'),
+ id: 'password-box',
+ value: this.state.password,
+ onChange: event => this.setState({ password: event.target.value }),
+ className: classnames({ 'form-control--error': this.state.error }),
+ }),
+ ]),
+ this.state.error && h('.reveal-seed__error', this.state.error),
+ ])
+ )
+ }
+
+ renderRevealSeedContent () {
+ const { t } = this.context
+
+ return (
+ h('div', [
+ h('label.reveal-seed__label', t('yourPrivateSeedPhrase')),
+ h(ExportTextContainer, {
+ text: this.state.seedWords,
+ filename: t('metamaskSeedWords'),
+ }),
+ ])
+ )
+ }
+
+ renderFooter () {
+ return this.state.screen === PASSWORD_PROMPT_SCREEN
+ ? this.renderPasswordPromptFooter()
+ : this.renderRevealSeedFooter()
+ }
+
+ renderPasswordPromptFooter () {
+ return (
+ h('.page-container__footer', [
+ h('header', [
+ h(Button, {
+ type: 'default',
+ large: true,
+ className: 'page-container__footer-button',
+ onClick: () => this.props.history.push(DEFAULT_ROUTE),
+ }, this.context.t('cancel')),
+ h(Button, {
+ type: 'primary',
+ large: true,
+ className: 'page-container__footer-button',
+ onClick: event => this.handleSubmit(event),
+ disabled: this.state.password === '',
+ }, this.context.t('next')),
+ ]),
+ ])
+ )
+ }
+
+ renderRevealSeedFooter () {
+ return (
+ h('.page-container__footer', [
+ h(Button, {
+ type: 'default',
+ large: true,
+ className: 'page-container__footer-button',
+ onClick: () => this.props.history.push(DEFAULT_ROUTE),
+ }, this.context.t('close')),
+ ])
+ )
+ }
+
+ render () {
+ return (
+ h('.page-container', [
+ h('.page-container__header', [
+ h('.page-container__title', this.context.t('revealSeedWordsTitle')),
+ h('.page-container__subtitle', this.context.t('revealSeedWordsDescription')),
+ ]),
+ h('.page-container__content', [
+ this.renderWarning(),
+ h('.reveal-seed__content', [
+ this.renderContent(),
+ ]),
+ ]),
+ this.renderFooter(),
+ ])
+ )
+ }
+}
+
+RevealSeedPage.propTypes = {
+ requestRevealSeedWords: PropTypes.func,
+ history: PropTypes.object,
+}
+
+RevealSeedPage.contextTypes = {
+ t: PropTypes.func,
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ requestRevealSeedWords: password => dispatch(requestRevealSeedWords(password)),
+ }
+}
+
+module.exports = connect(null, mapDispatchToProps)(RevealSeedPage)
diff --git a/ui/app/pages/lock/index.js b/ui/app/pages/lock/index.js
new file mode 100644
index 000000000..7bfe2a61f
--- /dev/null
+++ b/ui/app/pages/lock/index.js
@@ -0,0 +1 @@
+export { default } from './lock.container'
diff --git a/ui/app/pages/lock/lock.component.js b/ui/app/pages/lock/lock.component.js
new file mode 100644
index 000000000..1145158c5
--- /dev/null
+++ b/ui/app/pages/lock/lock.component.js
@@ -0,0 +1,26 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import Loading from '../../components/ui/loading-screen'
+import { DEFAULT_ROUTE } from '../../helpers/constants/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 />
+ }
+}
diff --git a/ui/app/pages/lock/lock.container.js b/ui/app/pages/lock/lock.container.js
new file mode 100644
index 000000000..6a20b6ed1
--- /dev/null
+++ b/ui/app/pages/lock/lock.container.js
@@ -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 '../../store/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)
diff --git a/ui/app/pages/mobile-sync/index.js b/ui/app/pages/mobile-sync/index.js
new file mode 100644
index 000000000..0938ad103
--- /dev/null
+++ b/ui/app/pages/mobile-sync/index.js
@@ -0,0 +1,387 @@
+const { Component } = require('react')
+const { connect } = require('react-redux')
+const PropTypes = require('prop-types')
+const h = require('react-hyperscript')
+const classnames = require('classnames')
+const PubNub = require('pubnub')
+
+const { requestRevealSeedWords, fetchInfoToSync } = require('../../store/actions')
+const { DEFAULT_ROUTE } = require('../../helpers/constants/routes')
+const actions = require('../../store/actions')
+
+const qrCode = require('qrcode-generator')
+
+import Button from '../../components/ui/button'
+import LoadingScreen from '../../components/ui/loading-screen'
+
+const PASSWORD_PROMPT_SCREEN = 'PASSWORD_PROMPT_SCREEN'
+const REVEAL_SEED_SCREEN = 'REVEAL_SEED_SCREEN'
+
+class MobileSyncPage extends Component {
+ static propTypes = {
+ history: PropTypes.object,
+ selectedAddress: PropTypes.string,
+ displayWarning: PropTypes.func,
+ fetchInfoToSync: PropTypes.func,
+ requestRevealSeedWords: PropTypes.func,
+ }
+
+ constructor (props) {
+ super(props)
+
+ this.state = {
+ screen: PASSWORD_PROMPT_SCREEN,
+ password: '',
+ seedWords: null,
+ error: null,
+ syncing: false,
+ completed: false,
+ }
+
+ this.syncing = false
+ }
+
+ componentDidMount () {
+ const passwordBox = document.getElementById('password-box')
+ if (passwordBox) {
+ passwordBox.focus()
+ }
+ }
+
+ handleSubmit (event) {
+ event.preventDefault()
+ this.setState({ seedWords: null, error: null })
+ this.props.requestRevealSeedWords(this.state.password)
+ .then(seedWords => {
+ this.generateCipherKeyAndChannelName()
+ this.setState({ seedWords, screen: REVEAL_SEED_SCREEN })
+ this.initWebsockets()
+ })
+ .catch(error => this.setState({ error: error.message }))
+ }
+
+ generateCipherKeyAndChannelName () {
+ this.cipherKey = `${this.props.selectedAddress.substr(-4)}-${PubNub.generateUUID()}`
+ this.channelName = `mm-${PubNub.generateUUID()}`
+ }
+
+ initWebsockets () {
+ this.pubnub = new PubNub({
+ subscribeKey: process.env.PUBNUB_SUB_KEY,
+ publishKey: process.env.PUBNUB_PUB_KEY,
+ cipherKey: this.cipherKey,
+ ssl: true,
+ })
+
+ this.pubnubListener = this.pubnub.addListener({
+ message: (data) => {
+ const {channel, message} = data
+ // handle message
+ if (channel !== this.channelName || !message) {
+ return false
+ }
+
+ if (message.event === 'start-sync') {
+ this.startSyncing()
+ } else if (message.event === 'end-sync') {
+ this.disconnectWebsockets()
+ this.setState({syncing: false, completed: true})
+ }
+ },
+ })
+
+ this.pubnub.subscribe({
+ channels: [this.channelName],
+ withPresence: false,
+ })
+
+ }
+
+ disconnectWebsockets () {
+ if (this.pubnub && this.pubnubListener) {
+ this.pubnub.disconnect(this.pubnubListener)
+ }
+ }
+
+ // Calculating a PubNub Message Payload Size.
+ calculatePayloadSize (channel, message) {
+ return encodeURIComponent(
+ channel + JSON.stringify(message)
+ ).length + 100
+ }
+
+ chunkString (str, size) {
+ const numChunks = Math.ceil(str.length / size)
+ const chunks = new Array(numChunks)
+ for (let i = 0, o = 0; i < numChunks; ++i, o += size) {
+ chunks[i] = str.substr(o, size)
+ }
+ return chunks
+ }
+
+ notifyError (errorMsg) {
+ return new Promise((resolve, reject) => {
+ this.pubnub.publish(
+ {
+ message: {
+ event: 'error-sync',
+ data: errorMsg,
+ },
+ channel: this.channelName,
+ sendByPost: false, // true to send via post
+ storeInHistory: false,
+ },
+ (status, response) => {
+ if (!status.error) {
+ resolve()
+ } else {
+ reject(response)
+ }
+ })
+ })
+ }
+
+ async startSyncing () {
+ if (this.syncing) return false
+ this.syncing = true
+ this.setState({syncing: true})
+
+ const { accounts, network, preferences, transactions } = await this.props.fetchInfoToSync()
+
+ const allDataStr = JSON.stringify({
+ accounts,
+ network,
+ preferences,
+ transactions,
+ udata: {
+ pwd: this.state.password,
+ seed: this.state.seedWords,
+ },
+ })
+
+ const chunks = this.chunkString(allDataStr, 17000)
+ const totalChunks = chunks.length
+ try {
+ for (let i = 0; i < totalChunks; i++) {
+ await this.sendMessage(chunks[i], i + 1, totalChunks)
+ }
+ } catch (e) {
+ this.props.displayWarning('Sync failed :(')
+ this.setState({syncing: false})
+ this.syncing = false
+ this.notifyError(e.toString())
+ }
+ }
+
+ sendMessage (data, pkg, count) {
+ return new Promise((resolve, reject) => {
+ this.pubnub.publish(
+ {
+ message: {
+ event: 'syncing-data',
+ data,
+ totalPkg: count,
+ currentPkg: pkg,
+ },
+ channel: this.channelName,
+ sendByPost: false, // true to send via post
+ storeInHistory: false,
+ },
+ (status, response) => {
+ if (!status.error) {
+ resolve()
+ } else {
+ reject(response)
+ }
+ }
+ )
+ })
+ }
+
+
+ componentWillUnmount () {
+ this.disconnectWebsockets()
+ }
+
+ renderWarning (text) {
+ return (
+ h('.page-container__warning-container', [
+ h('.page-container__warning-message', [
+ h('div', [text]),
+ ]),
+ ])
+ )
+ }
+
+ renderContent () {
+ const { t } = this.context
+
+ if (this.state.syncing) {
+ return h(LoadingScreen, {loadingMessage: 'Sync in progress'})
+ }
+
+ if (this.state.completed) {
+ return h('div.reveal-seed__content', {},
+ h('label.reveal-seed__label', {
+ style: {
+ width: '100%',
+ textAlign: 'center',
+ },
+ }, t('syncWithMobileComplete')),
+ )
+ }
+
+ return this.state.screen === PASSWORD_PROMPT_SCREEN
+ ? h('div', {}, [
+ this.renderWarning(this.context.t('mobileSyncText')),
+ h('.reveal-seed__content', [
+ this.renderPasswordPromptContent(),
+ ]),
+ ])
+ : h('div', {}, [
+ this.renderWarning(this.context.t('syncWithMobileBeCareful')),
+ h('.reveal-seed__content', [ this.renderRevealSeedContent() ]),
+ ])
+ }
+
+ renderPasswordPromptContent () {
+ const { t } = this.context
+
+ return (
+ h('form', {
+ onSubmit: event => this.handleSubmit(event),
+ }, [
+ h('label.input-label', {
+ htmlFor: 'password-box',
+ }, t('enterPasswordContinue')),
+ h('.input-group', [
+ h('input.form-control', {
+ type: 'password',
+ placeholder: t('password'),
+ id: 'password-box',
+ value: this.state.password,
+ onChange: event => this.setState({ password: event.target.value }),
+ className: classnames({ 'form-control--error': this.state.error }),
+ }),
+ ]),
+ this.state.error && h('.reveal-seed__error', this.state.error),
+ ])
+ )
+ }
+
+ renderRevealSeedContent () {
+
+ const qrImage = qrCode(0, 'M')
+ qrImage.addData(`metamask-sync:${this.channelName}|@|${this.cipherKey}`)
+ qrImage.make()
+
+ const { t } = this.context
+ return (
+ h('div', [
+ h('label.reveal-seed__label', {
+ style: {
+ width: '100%',
+ textAlign: 'center',
+ },
+ }, t('syncWithMobileScanThisCode')),
+ h('.div.qr-wrapper', {
+ style: {
+ display: 'flex',
+ justifyContent: 'center',
+ },
+ dangerouslySetInnerHTML: {
+ __html: qrImage.createTableTag(4),
+ },
+ }),
+ ])
+ )
+ }
+
+ renderFooter () {
+ return this.state.screen === PASSWORD_PROMPT_SCREEN
+ ? this.renderPasswordPromptFooter()
+ : this.renderRevealSeedFooter()
+ }
+
+ renderPasswordPromptFooter () {
+ return (
+ h('div.new-account-import-form__buttons', {style: {padding: 30}}, [
+
+ h(Button, {
+ type: 'default',
+ large: true,
+ className: 'new-account-create-form__button',
+ onClick: () => this.props.history.push(DEFAULT_ROUTE),
+ }, this.context.t('cancel')),
+
+ h(Button, {
+ type: 'primary',
+ large: true,
+ className: 'new-account-create-form__button',
+ onClick: event => this.handleSubmit(event),
+ disabled: this.state.password === '',
+ }, this.context.t('next')),
+ ])
+ )
+ }
+
+ renderRevealSeedFooter () {
+ return (
+ h('.page-container__footer', {style: {padding: 30}}, [
+ h(Button, {
+ type: 'default',
+ large: true,
+ className: 'page-container__footer-button',
+ onClick: () => this.props.history.push(DEFAULT_ROUTE),
+ }, this.context.t('close')),
+ ])
+ )
+ }
+
+ render () {
+ return (
+ h('.page-container', [
+ h('.page-container__header', [
+ h('.page-container__title', this.context.t('syncWithMobileTitle')),
+ this.state.screen === PASSWORD_PROMPT_SCREEN ? h('.page-container__subtitle', this.context.t('syncWithMobileDesc')) : null,
+ this.state.screen === PASSWORD_PROMPT_SCREEN ? h('.page-container__subtitle', this.context.t('syncWithMobileDescNewUsers')) : null,
+ ]),
+ h('.page-container__content', [
+ this.renderContent(),
+ ]),
+ this.renderFooter(),
+ ])
+ )
+ }
+}
+
+MobileSyncPage.propTypes = {
+ requestRevealSeedWords: PropTypes.func,
+ fetchInfoToSync: PropTypes.func,
+ history: PropTypes.object,
+}
+
+MobileSyncPage.contextTypes = {
+ t: PropTypes.func,
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ requestRevealSeedWords: password => dispatch(requestRevealSeedWords(password)),
+ fetchInfoToSync: () => dispatch(fetchInfoToSync()),
+ displayWarning: (message) => dispatch(actions.displayWarning(message || null)),
+ }
+
+}
+
+const mapStateToProps = state => {
+ const {
+ metamask: { selectedAddress },
+ } = state
+
+ return {
+ selectedAddress,
+ }
+}
+
+module.exports = connect(mapStateToProps, mapDispatchToProps)(MobileSyncPage)
diff --git a/ui/app/pages/notice/notice.js b/ui/app/pages/notice/notice.js
new file mode 100644
index 000000000..d8274dfcb
--- /dev/null
+++ b/ui/app/pages/notice/notice.js
@@ -0,0 +1,203 @@
+const { Component } = require('react')
+const h = require('react-hyperscript')
+const { connect } = require('react-redux')
+const PropTypes = require('prop-types')
+const ReactMarkdown = require('react-markdown')
+const linker = require('extension-link-enabler')
+const generateLostAccountsNotice = require('../../../lib/lost-accounts-notice')
+const findDOMNode = require('react-dom').findDOMNode
+const actions = require('../../store/actions')
+const { DEFAULT_ROUTE } = require('../../helpers/constants/routes')
+
+class Notice extends Component {
+ constructor (props) {
+ super(props)
+
+ this.state = {
+ disclaimerDisabled: true,
+ }
+ }
+
+ componentWillMount () {
+ if (!this.props.notice) {
+ this.props.history.push(DEFAULT_ROUTE)
+ }
+ }
+
+ componentDidMount () {
+ // eslint-disable-next-line react/no-find-dom-node
+ var node = findDOMNode(this)
+ linker.setupListener(node)
+ if (document.getElementsByClassName('notice-box')[0].clientHeight < 310) {
+ this.setState({ disclaimerDisabled: false })
+ }
+ }
+
+ componentWillReceiveProps (nextProps) {
+ if (!nextProps.notice) {
+ this.props.history.push(DEFAULT_ROUTE)
+ }
+ }
+
+ componentWillUnmount () {
+ // eslint-disable-next-line react/no-find-dom-node
+ var node = findDOMNode(this)
+ linker.teardownListener(node)
+ }
+
+ handleAccept () {
+ this.setState({ disclaimerDisabled: true })
+ this.props.onConfirm()
+ }
+
+ render () {
+ const { notice = {} } = this.props
+ const { title, date, body } = notice
+ const { disclaimerDisabled } = this.state
+
+ return (
+ h('.flex-column.flex-center.flex-grow', {
+ style: {
+ width: '100%',
+ },
+ }, [
+ h('h3.flex-center.text-transform-uppercase.terms-header', {
+ style: {
+ background: '#EBEBEB',
+ color: '#AEAEAE',
+ width: '100%',
+ fontSize: '20px',
+ textAlign: 'center',
+ padding: 6,
+ },
+ }, [
+ title,
+ ]),
+
+ h('h5.flex-center.text-transform-uppercase.terms-header', {
+ style: {
+ background: '#EBEBEB',
+ color: '#AEAEAE',
+ marginBottom: 24,
+ width: '100%',
+ fontSize: '20px',
+ textAlign: 'center',
+ padding: 6,
+ },
+ }, [
+ date,
+ ]),
+
+ h('style', `
+
+ .markdown {
+ overflow-x: hidden;
+ }
+
+ .markdown h1, .markdown h2, .markdown h3 {
+ margin: 10px 0;
+ font-weight: bold;
+ }
+
+ .markdown strong {
+ font-weight: bold;
+ }
+ .markdown em {
+ font-style: italic;
+ }
+
+ .markdown p {
+ margin: 10px 0;
+ }
+
+ .markdown a {
+ color: #df6b0e;
+ }
+
+ `),
+
+ h('div.markdown', {
+ onScroll: (e) => {
+ var object = e.currentTarget
+ if (object.offsetHeight + object.scrollTop + 100 >= object.scrollHeight) {
+ this.setState({ disclaimerDisabled: false })
+ }
+ },
+ style: {
+ background: 'rgb(235, 235, 235)',
+ height: '310px',
+ padding: '6px',
+ width: '90%',
+ overflowY: 'scroll',
+ scroll: 'auto',
+ },
+ }, [
+ h(ReactMarkdown, {
+ className: 'notice-box',
+ source: body,
+ skipHtml: true,
+ }),
+ ]),
+
+ h('button.primary', {
+ disabled: disclaimerDisabled,
+ onClick: () => this.handleAccept(),
+ style: {
+ marginTop: '18px',
+ },
+ }, 'Accept'),
+ ])
+ )
+ }
+
+}
+
+const mapStateToProps = state => {
+ const { metamask } = state
+ const { noActiveNotices, nextUnreadNotice, lostAccounts } = metamask
+
+ return {
+ noActiveNotices,
+ nextUnreadNotice,
+ lostAccounts,
+ }
+}
+
+Notice.propTypes = {
+ notice: PropTypes.object,
+ onConfirm: PropTypes.func,
+ history: PropTypes.object,
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ markNoticeRead: nextUnreadNotice => dispatch(actions.markNoticeRead(nextUnreadNotice)),
+ markAccountsFound: () => dispatch(actions.markAccountsFound()),
+ }
+}
+
+const mergeProps = (stateProps, dispatchProps, ownProps) => {
+ const { noActiveNotices, nextUnreadNotice, lostAccounts } = stateProps
+ const { markNoticeRead, markAccountsFound } = dispatchProps
+
+ let notice
+ let onConfirm
+
+ if (!noActiveNotices) {
+ notice = nextUnreadNotice
+ onConfirm = () => markNoticeRead(nextUnreadNotice)
+ } else if (lostAccounts && lostAccounts.length > 0) {
+ notice = generateLostAccountsNotice(lostAccounts)
+ onConfirm = () => markAccountsFound()
+ }
+
+ return {
+ ...stateProps,
+ ...dispatchProps,
+ ...ownProps,
+ notice,
+ onConfirm,
+ }
+}
+
+module.exports = connect(mapStateToProps, mapDispatchToProps, mergeProps)(Notice)
diff --git a/ui/app/pages/provider-approval/index.js b/ui/app/pages/provider-approval/index.js
new file mode 100644
index 000000000..4162f3155
--- /dev/null
+++ b/ui/app/pages/provider-approval/index.js
@@ -0,0 +1 @@
+export { default } from './provider-approval.container'
diff --git a/ui/app/pages/provider-approval/provider-approval.component.js b/ui/app/pages/provider-approval/provider-approval.component.js
new file mode 100644
index 000000000..1f1d68da7
--- /dev/null
+++ b/ui/app/pages/provider-approval/provider-approval.component.js
@@ -0,0 +1,29 @@
+import PropTypes from 'prop-types'
+import React, { Component } from 'react'
+import ProviderPageContainer from '../../components/app/provider-page-container'
+
+export default class ProviderApproval extends Component {
+ static propTypes = {
+ approveProviderRequest: PropTypes.func.isRequired,
+ providerRequest: PropTypes.object.isRequired,
+ rejectProviderRequest: PropTypes.func.isRequired,
+ };
+
+ static contextTypes = {
+ t: PropTypes.func,
+ };
+
+ render () {
+ const { approveProviderRequest, providerRequest, rejectProviderRequest } = this.props
+ return (
+ <ProviderPageContainer
+ approveProviderRequest={approveProviderRequest}
+ origin={providerRequest.origin}
+ tabID={providerRequest.tabID}
+ rejectProviderRequest={rejectProviderRequest}
+ siteImage={providerRequest.siteImage}
+ siteTitle={providerRequest.siteTitle}
+ />
+ )
+ }
+}
diff --git a/ui/app/pages/provider-approval/provider-approval.container.js b/ui/app/pages/provider-approval/provider-approval.container.js
new file mode 100644
index 000000000..d53c0ae4d
--- /dev/null
+++ b/ui/app/pages/provider-approval/provider-approval.container.js
@@ -0,0 +1,12 @@
+import { connect } from 'react-redux'
+import ProviderApproval from './provider-approval.component'
+import { approveProviderRequest, rejectProviderRequest } from '../../store/actions'
+
+function mapDispatchToProps (dispatch) {
+ return {
+ approveProviderRequest: tabID => dispatch(approveProviderRequest(tabID)),
+ rejectProviderRequest: tabID => dispatch(rejectProviderRequest(tabID)),
+ }
+}
+
+export default connect(null, mapDispatchToProps)(ProviderApproval)
diff --git a/ui/app/pages/routes/index.js b/ui/app/pages/routes/index.js
new file mode 100644
index 000000000..460cec958
--- /dev/null
+++ b/ui/app/pages/routes/index.js
@@ -0,0 +1,441 @@
+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 '../../store/actions'
+import log from 'loglevel'
+import { getMetaMaskAccounts, getNetworkIdentifier } from '../../selectors/selectors'
+
+// init
+import FirstTimeFlow from '../first-time-flow'
+// accounts
+const SendTransactionScreen = require('../../components/app/send/send.container')
+const ConfirmTransaction = require('../confirm-transaction')
+
+// slideout menu
+const Sidebar = require('../../components/app/sidebars').default
+const { WALLET_VIEW_SIDEBAR } = require('../../components/app/sidebars/sidebar.constants')
+
+// other views
+import Home from '../home'
+import Settings from '../settings'
+import Authenticated from '../../helpers/higher-order-components/authenticated'
+import Initialized from '../../helpers/higher-order-components/initialized'
+import Lock from '../lock'
+import UiMigrationAnnouncement from '../../components/app/ui-migration-annoucement'
+const RestoreVaultPage = require('../keychains/restore-vault').default
+const RevealSeedConfirmation = require('../keychains/reveal-seed')
+const MobileSyncPage = require('../mobile-sync')
+const AddTokenPage = require('../add-token')
+const ConfirmAddTokenPage = require('../confirm-add-token')
+const ConfirmAddSuggestedTokenPage = require('../confirm-add-suggested-token')
+const CreateAccountPage = require('../create-account')
+const NoticeScreen = require('../notice/notice')
+
+const Loading = require('../../components/ui/loading-screen')
+const LoadingNetwork = require('../../components/app/loading-network-screen').default
+const NetworkDropdown = require('../../components/app/dropdowns/network-dropdown')
+import AccountMenu from '../../components/app/account-menu'
+
+// Global Modals
+const Modal = require('../../components/app/modals').Modal
+// Global Alert
+const Alert = require('../../components/ui/alert')
+
+import AppHeader from '../../components/app/app-header'
+import UnlockPage from '../unlock-page'
+
+import {
+ submittedPendingTransactionsSelector,
+} from '../../selectors/transactions'
+
+// Routes
+import {
+ DEFAULT_ROUTE,
+ LOCK_ROUTE,
+ UNLOCK_ROUTE,
+ SETTINGS_ROUTE,
+ REVEAL_SEED_ROUTE,
+ MOBILE_SYNC_ROUTE,
+ RESTORE_VAULT_ROUTE,
+ ADD_TOKEN_ROUTE,
+ CONFIRM_ADD_TOKEN_ROUTE,
+ CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE,
+ NEW_ACCOUNT_ROUTE,
+ SEND_ROUTE,
+ CONFIRM_TRANSACTION_ROUTE,
+ INITIALIZE_ROUTE,
+ INITIALIZE_UNLOCK_ROUTE,
+ NOTICE_ROUTE,
+} from '../../helpers/constants/routes'
+
+// enums
+import {
+ ENVIRONMENT_TYPE_NOTIFICATION,
+ ENVIRONMENT_TYPE_POPUP,
+} from '../../../../app/scripts/lib/enums'
+
+class Routes extends Component {
+ componentWillMount () {
+ const { currentCurrency, setCurrentCurrencyToUSD } = this.props
+
+ if (!currentCurrency) {
+ setCurrentCurrencyToUSD()
+ }
+
+ this.props.history.listen((locationObj, action) => {
+ if (action === 'PUSH') {
+ const url = `&url=${encodeURIComponent('http://www.metamask.io/metametrics' + locationObj.pathname)}`
+ this.context.metricsEvent({}, {
+ currentPath: '',
+ pathname: locationObj.pathname,
+ url,
+ pageOpts: {
+ hideDimensions: true,
+ },
+ })
+ }
+ })
+ }
+
+ renderRoutes () {
+ return (
+ <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={MOBILE_SYNC_ROUTE} component={MobileSyncPage} 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,
+ provider,
+ frequentRpcListDetail,
+ currentView,
+ setMouseUserState,
+ sidebar,
+ submittedPendingTransactions,
+ } = this.props
+ const isLoadingNetwork = network === 'loading' && currentView.name !== 'config'
+ const loadMessage = loadingMessage || isLoadingNetwork ?
+ this.getConnectingLabel(loadingMessage) : null
+ log.debug('Main ui render function')
+
+ const sidebarOnOverlayClose = sidebarType === WALLET_VIEW_SIDEBAR
+ ? () => {
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Navigation',
+ action: 'Wallet Sidebar',
+ name: 'Closed Sidebare Via Overlay',
+ },
+ })
+ }
+ : null
+
+ const {
+ isOpen: sidebarIsOpen,
+ transitionName: sidebarTransitionName,
+ type: sidebarType,
+ props,
+ } = sidebar
+ const { transaction: sidebarTransaction } = props || {}
+
+ return (
+ <div
+ className="app"
+ onClick={() => setMouseUserState(true)}
+ onKeyDown={e => {
+ if (e.keyCode === 9) {
+ setMouseUserState(false)
+ }
+ }}
+ >
+ <UiMigrationAnnouncement />
+ <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}
+ onOverlayClose={sidebarOnOverlayClose}
+ />
+ <NetworkDropdown
+ provider={provider}
+ frequentRpcListDetail={frequentRpcListDetail}
+ />
+ <AccountMenu />
+ <div className="main-container-wrapper">
+ { isLoading && <Loading loadingMessage={loadMessage} /> }
+ { !isLoading && isLoadingNetwork && <LoadingNetwork /> }
+ { this.renderRoutes() }
+ </div>
+ </div>
+ )
+ }
+
+ toggleMetamaskActive () {
+ if (!this.props.isUnlocked) {
+ // currently inactive: redirect to password box
+ var passwordBox = document.querySelector('input[type=password]')
+ if (!passwordBox) return
+ passwordBox.focus()
+ } else {
+ // currently active: deactivate
+ this.props.dispatch(actions.lockMetamask(false))
+ }
+ }
+
+ getConnectingLabel = function (loadingMessage) {
+ if (loadingMessage) {
+ return loadingMessage
+ }
+ const { provider, providerId } = this.props
+ const providerName = provider.type
+
+ let name
+
+ if (providerName === 'mainnet') {
+ name = this.context.t('connectingToMainnet')
+ } else if (providerName === 'ropsten') {
+ name = this.context.t('connectingToRopsten')
+ } else if (providerName === 'kovan') {
+ name = this.context.t('connectingToKovan')
+ } else if (providerName === 'rinkeby') {
+ name = this.context.t('connectingToRinkeby')
+ } else {
+ name = this.context.t('connectingTo', [providerId])
+ }
+
+ return name
+ }
+
+ getNetworkName () {
+ const { provider } = this.props
+ const providerName = provider.type
+
+ let name
+
+ if (providerName === 'mainnet') {
+ name = this.context.t('mainnet')
+ } else if (providerName === 'ropsten') {
+ name = this.context.t('ropsten')
+ } else if (providerName === 'kovan') {
+ name = this.context.t('kovan')
+ } else if (providerName === 'rinkeby') {
+ name = this.context.t('rinkeby')
+ } else {
+ name = this.context.t('unknownNetwork')
+ }
+
+ return name
+ }
+}
+
+Routes.propTypes = {
+ currentCurrency: PropTypes.string,
+ setCurrentCurrencyToUSD: PropTypes.func,
+ isLoading: PropTypes.bool,
+ loadingMessage: PropTypes.string,
+ alertMessage: PropTypes.string,
+ network: PropTypes.string,
+ provider: PropTypes.object,
+ frequentRpcListDetail: PropTypes.array,
+ currentView: PropTypes.object,
+ sidebar: PropTypes.object,
+ alertOpen: PropTypes.bool,
+ hideSidebar: PropTypes.func,
+ isOnboarding: PropTypes.bool,
+ isUnlocked: PropTypes.bool,
+ networkDropdownOpen: PropTypes.bool,
+ showNetworkDropdown: PropTypes.func,
+ hideNetworkDropdown: PropTypes.func,
+ history: PropTypes.object,
+ location: PropTypes.object,
+ dispatch: PropTypes.func,
+ toggleAccountMenu: PropTypes.func,
+ selectedAddress: PropTypes.string,
+ noActiveNotices: PropTypes.bool,
+ lostAccounts: PropTypes.array,
+ isInitialized: PropTypes.bool,
+ forgottenPassword: PropTypes.bool,
+ activeAddress: PropTypes.string,
+ unapprovedTxs: PropTypes.object,
+ seedWords: PropTypes.string,
+ submittedPendingTransactions: PropTypes.array,
+ unapprovedMsgCount: PropTypes.number,
+ unapprovedPersonalMsgCount: PropTypes.number,
+ unapprovedTypedMessagesCount: PropTypes.number,
+ welcomeScreenSeen: PropTypes.bool,
+ isPopup: PropTypes.bool,
+ isMouseUser: PropTypes.bool,
+ setMouseUserState: PropTypes.func,
+ t: PropTypes.func,
+ providerId: PropTypes.string,
+ providerRequests: PropTypes.array,
+}
+
+function mapStateToProps (state) {
+ const { appState, metamask } = state
+ const {
+ networkDropdownOpen,
+ sidebar,
+ alertOpen,
+ alertMessage,
+ isLoading,
+ loadingMessage,
+ } = appState
+
+ const accounts = getMetaMaskAccounts(state)
+
+ const {
+ identities,
+ address,
+ keyrings,
+ isInitialized,
+ noActiveNotices,
+ seedWords,
+ unapprovedTxs,
+ nextUnreadNotice,
+ lostAccounts,
+ unapprovedMsgCount,
+ unapprovedPersonalMsgCount,
+ unapprovedTypedMessagesCount,
+ providerRequests,
+ } = metamask
+ const selected = address || Object.keys(accounts)[0]
+
+ return {
+ // state from plugin
+ networkDropdownOpen,
+ sidebar,
+ alertOpen,
+ alertMessage,
+ isLoading,
+ loadingMessage,
+ noActiveNotices,
+ isInitialized,
+ isUnlocked: state.metamask.isUnlocked,
+ selectedAddress: state.metamask.selectedAddress,
+ currentView: state.appState.currentView,
+ activeAddress: state.appState.activeAddress,
+ transForward: state.appState.transForward,
+ isOnboarding: Boolean(!noActiveNotices || seedWords || !isInitialized),
+ isPopup: state.metamask.isPopup,
+ seedWords: state.metamask.seedWords,
+ submittedPendingTransactions: submittedPendingTransactionsSelector(state),
+ unapprovedTxs,
+ unapprovedMsgs: state.metamask.unapprovedMsgs,
+ unapprovedMsgCount,
+ unapprovedPersonalMsgCount,
+ unapprovedTypedMessagesCount,
+ menuOpen: state.appState.menuOpen,
+ network: state.metamask.network,
+ provider: state.metamask.provider,
+ forgottenPassword: state.appState.forgottenPassword,
+ nextUnreadNotice,
+ lostAccounts,
+ frequentRpcListDetail: state.metamask.frequentRpcListDetail || [],
+ currentCurrency: state.metamask.currentCurrency,
+ isMouseUser: state.appState.isMouseUser,
+ isRevealingSeedWords: state.metamask.isRevealingSeedWords,
+ Qr: state.appState.Qr,
+ welcomeScreenSeen: state.metamask.welcomeScreenSeen,
+ providerId: getNetworkIdentifier(state),
+
+ // state needed to get account dropdown temporarily rendering from app bar
+ identities,
+ selected,
+ keyrings,
+ providerRequests,
+ }
+}
+
+function mapDispatchToProps (dispatch, ownProps) {
+ return {
+ dispatch,
+ hideSidebar: () => dispatch(actions.hideSidebar()),
+ showNetworkDropdown: () => dispatch(actions.showNetworkDropdown()),
+ hideNetworkDropdown: () => dispatch(actions.hideNetworkDropdown()),
+ setCurrentCurrencyToUSD: () => dispatch(actions.setCurrentCurrency('usd')),
+ toggleAccountMenu: () => dispatch(actions.toggleAccountMenu()),
+ setMouseUserState: (isMouseUser) => dispatch(actions.setMouseUserState(isMouseUser)),
+ }
+}
+
+Routes.contextTypes = {
+ t: PropTypes.func,
+ metricsEvent: PropTypes.func,
+}
+
+module.exports = compose(
+ withRouter,
+ connect(mapStateToProps, mapDispatchToProps)
+)(Routes)
diff --git a/ui/app/pages/settings/index.js b/ui/app/pages/settings/index.js
new file mode 100644
index 000000000..44a9ffa63
--- /dev/null
+++ b/ui/app/pages/settings/index.js
@@ -0,0 +1 @@
+export { default } from './settings.component'
diff --git a/ui/app/pages/settings/index.scss b/ui/app/pages/settings/index.scss
new file mode 100644
index 000000000..0e8482c63
--- /dev/null
+++ b/ui/app/pages/settings/index.scss
@@ -0,0 +1,80 @@
+@import 'info-tab/index';
+
+@import 'settings-tab/index';
+
+.settings-page {
+ position: relative;
+ background: $white;
+ display: flex;
+ flex-flow: column nowrap;
+
+ &__header {
+ padding: 25px 25px 0;
+ }
+
+ &__close-button::after {
+ content: '\00D7';
+ font-size: 40px;
+ color: $dusty-gray;
+ position: absolute;
+ top: 25px;
+ right: 30px;
+ cursor: pointer;
+ }
+
+ &__content {
+ padding: 25px;
+ height: auto;
+ overflow: auto;
+ }
+
+ &__content-row {
+ display: flex;
+ flex-direction: row;
+ padding: 10px 0 20px;
+
+ @media screen and (max-width: 575px) {
+ flex-direction: column;
+ padding: 10px 0;
+ }
+ }
+
+ &__content-item {
+ flex: 1;
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+ padding: 0 5px;
+ min-height: 71px;
+
+ @media screen and (max-width: 575px) {
+ height: initial;
+ padding: 5px 0;
+ }
+
+ &--without-height {
+ height: initial;
+ }
+ }
+
+ &__content-label {
+ text-transform: capitalize;
+ }
+
+ &__content-description {
+ font-size: 14px;
+ color: $dusty-gray;
+ padding-top: 5px;
+ }
+
+ &__content-item-col {
+ max-width: 300px;
+ display: flex;
+ flex-direction: column;
+
+ @media screen and (max-width: 575px) {
+ max-width: 100%;
+ width: 100%;
+ }
+ }
+}
diff --git a/ui/app/pages/settings/info-tab/index.js b/ui/app/pages/settings/info-tab/index.js
new file mode 100644
index 000000000..7556a258d
--- /dev/null
+++ b/ui/app/pages/settings/info-tab/index.js
@@ -0,0 +1 @@
+export { default } from './info-tab.component'
diff --git a/ui/app/pages/settings/info-tab/index.scss b/ui/app/pages/settings/info-tab/index.scss
new file mode 100644
index 000000000..43ad6f652
--- /dev/null
+++ b/ui/app/pages/settings/info-tab/index.scss
@@ -0,0 +1,56 @@
+.info-tab {
+ &__logo-wrapper {
+ height: 80px;
+ margin-bottom: 20px;
+ }
+
+ &__logo {
+ max-height: 100%;
+ max-width: 100%;
+ }
+
+ &__item {
+ padding: 10px 0;
+ }
+
+ &__link-header {
+ padding-bottom: 15px;
+
+ @media screen and (max-width: 575px) {
+ padding-bottom: 5px;
+ }
+ }
+
+ &__link-item {
+ padding: 15px 0;
+
+ @media screen and (max-width: 575px) {
+ padding: 5px 0;
+ }
+ }
+
+ &__link-text {
+ color: $curious-blue;
+ }
+
+ &__version-number {
+ padding-top: 5px;
+ font-size: 13px;
+ color: $dusty-gray;
+ }
+
+ &__separator {
+ margin: 15px 0;
+ width: 80px;
+ border-color: $alto;
+ border: none;
+ height: 1px;
+ background-color: $alto;
+ color: $alto;
+ }
+
+ &__about {
+ color: $dusty-gray;
+ margin-bottom: 15px;
+ }
+}
diff --git a/ui/app/pages/settings/info-tab/info-tab.component.js b/ui/app/pages/settings/info-tab/info-tab.component.js
new file mode 100644
index 000000000..72f7d835e
--- /dev/null
+++ b/ui/app/pages/settings/info-tab/info-tab.component.js
@@ -0,0 +1,136 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+
+export default class InfoTab extends PureComponent {
+ state = {
+ version: global.platform.getVersion(),
+ }
+
+ static propTypes = {
+ tab: PropTypes.string,
+ metamask: PropTypes.object,
+ setCurrentCurrency: PropTypes.func,
+ setRpcTarget: PropTypes.func,
+ displayWarning: PropTypes.func,
+ revealSeedConfirmation: PropTypes.func,
+ warning: PropTypes.string,
+ location: PropTypes.object,
+ history: PropTypes.object,
+ }
+
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ renderInfoLinks () {
+ const { t } = this.context
+
+ return (
+ <div className="settings-page__content-item settings-page__content-item--without-height">
+ <div className="info-tab__link-header">
+ { t('links') }
+ </div>
+ <div className="info-tab__link-item">
+ <a
+ href="https://metamask.io/privacy.html"
+ target="_blank"
+ rel="noopener noreferrer"
+ >
+ <span className="info-tab__link-text">
+ { t('privacyMsg') }
+ </span>
+ </a>
+ </div>
+ <div className="info-tab__link-item">
+ <a
+ href="https://metamask.io/terms.html"
+ target="_blank"
+ rel="noopener noreferrer"
+ >
+ <span className="info-tab__link-text">
+ { t('terms') }
+ </span>
+ </a>
+ </div>
+ <div className="info-tab__link-item">
+ <a
+ href="https://metamask.io/attributions.html"
+ target="_blank"
+ rel="noopener noreferrer"
+ >
+ <span className="info-tab__link-text">
+ { t('attributions') }
+ </span>
+ </a>
+ </div>
+ <hr className="info-tab__separator" />
+ <div className="info-tab__link-item">
+ <a
+ href="https://support.metamask.io"
+ target="_blank"
+ rel="noopener noreferrer"
+ >
+ <span className="info-tab__link-text">
+ { t('supportCenter') }
+ </span>
+ </a>
+ </div>
+ <div className="info-tab__link-item">
+ <a
+ href="https://metamask.io/"
+ target="_blank"
+ rel="noopener noreferrer"
+ >
+ <span className="info-tab__link-text">
+ { t('visitWebSite') }
+ </span>
+ </a>
+ </div>
+ <div className="info-tab__link-item">
+ <a
+ href="mailto:help@metamask.io?subject=Feedback"
+ target="_blank"
+ rel="noopener noreferrer"
+ >
+ <span className="info-tab__link-text">
+ { t('emailUs') }
+ </span>
+ </a>
+ </div>
+ </div>
+ )
+ }
+
+ render () {
+ const { t } = this.context
+
+ return (
+ <div className="settings-page__content">
+ <div className="settings-page__content-row">
+ <div className="settings-page__content-item settings-page__content-item--without-height">
+ <div className="info-tab__logo-wrapper">
+ <img
+ src="images/info-logo.png"
+ className="info-tab__logo"
+ />
+ </div>
+ <div className="info-tab__item">
+ <div className="info-tab__version-header">
+ { t('metamaskVersion') }
+ </div>
+ <div className="info-tab__version-number">
+ { this.state.version }
+ </div>
+ </div>
+ <div className="info-tab__item">
+ <div className="info-tab__about">
+ { t('builtInCalifornia') }
+ </div>
+ </div>
+ </div>
+ { this.renderInfoLinks() }
+ </div>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/pages/settings/settings-tab/index.js b/ui/app/pages/settings/settings-tab/index.js
new file mode 100644
index 000000000..9fdaafd3f
--- /dev/null
+++ b/ui/app/pages/settings/settings-tab/index.js
@@ -0,0 +1 @@
+export { default } from './settings-tab.container'
diff --git a/ui/app/pages/settings/settings-tab/index.scss b/ui/app/pages/settings/settings-tab/index.scss
new file mode 100644
index 000000000..ef32b0e4c
--- /dev/null
+++ b/ui/app/pages/settings/settings-tab/index.scss
@@ -0,0 +1,69 @@
+.settings-tab {
+ &__error {
+ padding-bottom: 20px;
+ text-align: center;
+ color: $crimson;
+ }
+
+ &__advanced-link {
+ color: $curious-blue;
+ padding-left: 5px;
+ }
+
+ &__rpc-save-button {
+ align-self: flex-end;
+ padding: 5px;
+ text-transform: uppercase;
+ color: $dusty-gray;
+ cursor: pointer;
+ width: 25%;
+ min-width: 80px;
+ height: 33px;
+ }
+
+ &__button--red {
+ border-color: lighten($monzo, 10%);
+ color: $monzo;
+
+ &:active {
+ background: lighten($monzo, 55%);
+ border-color: $monzo;
+ }
+
+ &:hover {
+ border-color: $monzo;
+ }
+ }
+
+ &__button--orange {
+ border-color: lighten($ecstasy, 20%);
+ color: $ecstasy;
+
+ &:active {
+ background: lighten($ecstasy, 40%);
+ border-color: $ecstasy;
+ }
+
+ &:hover {
+ border-color: $ecstasy;
+ }
+ }
+
+ &__radio-buttons {
+ display: flex;
+ align-items: center;
+ }
+
+ &__radio-button {
+ display: flex;
+ align-items: center;
+
+ &:not(:last-child) {
+ margin-right: 16px;
+ }
+ }
+
+ &__radio-label {
+ padding-left: 4px;
+ }
+}
diff --git a/ui/app/pages/settings/settings-tab/settings-tab.component.js b/ui/app/pages/settings/settings-tab/settings-tab.component.js
new file mode 100644
index 000000000..f69c21e82
--- /dev/null
+++ b/ui/app/pages/settings/settings-tab/settings-tab.component.js
@@ -0,0 +1,674 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import infuraCurrencies from '../../../helpers/constants/infura-conversion.json'
+import validUrl from 'valid-url'
+import { exportAsFile } from '../../../helpers/utils/util'
+import SimpleDropdown from '../../../components/app/dropdowns/simple-dropdown'
+import ToggleButton from 'react-toggle-button'
+import { REVEAL_SEED_ROUTE, MOBILE_SYNC_ROUTE } from '../../../helpers/constants/routes'
+import locales from '../../../../../app/_locales/index.json'
+import TextField from '../../../components/ui/text-field'
+import Button from '../../../components/ui/button'
+
+const sortedCurrencies = infuraCurrencies.objects.sort((a, b) => {
+ return a.quote.name.toLocaleLowerCase().localeCompare(b.quote.name.toLocaleLowerCase())
+})
+
+const infuraCurrencyOptions = sortedCurrencies.map(({ quote: { code, name } }) => {
+ return {
+ displayValue: `${code.toUpperCase()} - ${name}`,
+ key: code,
+ value: code,
+ }
+})
+
+const localeOptions = locales.map(locale => {
+ return {
+ displayValue: `${locale.name}`,
+ key: locale.code,
+ value: locale.code,
+ }
+})
+
+export default class SettingsTab extends PureComponent {
+ static contextTypes = {
+ t: PropTypes.func,
+ metricsEvent: PropTypes.func,
+ }
+
+ static propTypes = {
+ metamask: PropTypes.object,
+ setUseBlockie: PropTypes.func,
+ setHexDataFeatureFlag: PropTypes.func,
+ setPrivacyMode: PropTypes.func,
+ privacyMode: PropTypes.bool,
+ setCurrentCurrency: PropTypes.func,
+ setRpcTarget: PropTypes.func,
+ delRpcTarget: PropTypes.func,
+ displayWarning: PropTypes.func,
+ revealSeedConfirmation: PropTypes.func,
+ setFeatureFlagToBeta: PropTypes.func,
+ showClearApprovalModal: PropTypes.func,
+ showResetAccountConfirmationModal: PropTypes.func,
+ warning: PropTypes.string,
+ history: PropTypes.object,
+ updateCurrentLocale: PropTypes.func,
+ currentLocale: PropTypes.string,
+ useBlockie: PropTypes.bool,
+ sendHexData: PropTypes.bool,
+ currentCurrency: PropTypes.string,
+ conversionDate: PropTypes.number,
+ nativeCurrency: PropTypes.string,
+ useNativeCurrencyAsPrimaryCurrency: PropTypes.bool,
+ setUseNativeCurrencyAsPrimaryCurrencyPreference: PropTypes.func,
+ setAdvancedInlineGasFeatureFlag: PropTypes.func,
+ advancedInlineGas: PropTypes.bool,
+ showFiatInTestnets: PropTypes.bool,
+ setShowFiatConversionOnTestnetsPreference: PropTypes.func.isRequired,
+ participateInMetaMetrics: PropTypes.bool,
+ setParticipateInMetaMetrics: PropTypes.func,
+ }
+
+ state = {
+ newRpc: '',
+ chainId: '',
+ showOptions: false,
+ ticker: '',
+ nickname: '',
+ }
+
+ renderCurrentConversion () {
+ const { t } = this.context
+ const { currentCurrency, conversionDate, setCurrentCurrency } = this.props
+
+ return (
+ <div className="settings-page__content-row">
+ <div className="settings-page__content-item">
+ <span>{ t('currencyConversion') }</span>
+ <span className="settings-page__content-description">
+ { t('updatedWithDate', [Date(conversionDate)]) }
+ </span>
+ </div>
+ <div className="settings-page__content-item">
+ <div className="settings-page__content-item-col">
+ <SimpleDropdown
+ placeholder={t('selectCurrency')}
+ options={infuraCurrencyOptions}
+ selectedOption={currentCurrency}
+ onSelect={newCurrency => setCurrentCurrency(newCurrency)}
+ />
+ </div>
+ </div>
+ </div>
+ )
+ }
+
+ renderCurrentLocale () {
+ const { t } = this.context
+ const { updateCurrentLocale, currentLocale } = this.props
+ const currentLocaleMeta = locales.find(locale => locale.code === currentLocale)
+ const currentLocaleName = currentLocaleMeta ? currentLocaleMeta.name : ''
+
+ return (
+ <div className="settings-page__content-row">
+ <div className="settings-page__content-item">
+ <span className="settings-page__content-label">
+ { t('currentLanguage') }
+ </span>
+ <span className="settings-page__content-description">
+ { currentLocaleName }
+ </span>
+ </div>
+ <div className="settings-page__content-item">
+ <div className="settings-page__content-item-col">
+ <SimpleDropdown
+ placeholder={t('selectLocale')}
+ options={localeOptions}
+ selectedOption={currentLocale}
+ onSelect={async newLocale => updateCurrentLocale(newLocale)}
+ />
+ </div>
+ </div>
+ </div>
+ )
+ }
+
+ renderNewRpcUrl () {
+ const { t } = this.context
+ const { newRpc, chainId, ticker, nickname } = this.state
+
+ return (
+ <div className="settings-page__content-row">
+ <div className="settings-page__content-item">
+ <span>{ t('newNetwork') }</span>
+ </div>
+ <div className="settings-page__content-item">
+ <div className="settings-page__content-item-col">
+ <TextField
+ type="text"
+ id="new-rpc"
+ placeholder={t('rpcURL')}
+ value={newRpc}
+ onChange={e => this.setState({ newRpc: e.target.value })}
+ onKeyPress={e => {
+ if (e.key === 'Enter') {
+ this.validateRpc(newRpc, chainId, ticker, nickname)
+ }
+ }}
+ fullWidth
+ margin="dense"
+ />
+ <TextField
+ type="text"
+ id="chainid"
+ placeholder={t('optionalChainId')}
+ value={chainId}
+ onChange={e => this.setState({ chainId: e.target.value })}
+ onKeyPress={e => {
+ if (e.key === 'Enter') {
+ this.validateRpc(newRpc, chainId, ticker, nickname)
+ }
+ }}
+ style={{
+ display: this.state.showOptions ? null : 'none',
+ }}
+ fullWidth
+ margin="dense"
+ />
+ <TextField
+ type="text"
+ id="ticker"
+ placeholder={t('optionalSymbol')}
+ value={ticker}
+ onChange={e => this.setState({ ticker: e.target.value })}
+ onKeyPress={e => {
+ if (e.key === 'Enter') {
+ this.validateRpc(newRpc, chainId, ticker, nickname)
+ }
+ }}
+ style={{
+ display: this.state.showOptions ? null : 'none',
+ }}
+ fullWidth
+ margin="dense"
+ />
+ <TextField
+ type="text"
+ id="nickname"
+ placeholder={t('optionalNickname')}
+ value={nickname}
+ onChange={e => this.setState({ nickname: e.target.value })}
+ onKeyPress={e => {
+ if (e.key === 'Enter') {
+ this.validateRpc(newRpc, chainId, ticker, nickname)
+ }
+ }}
+ style={{
+ display: this.state.showOptions ? null : 'none',
+ }}
+ fullWidth
+ margin="dense"
+ />
+ <div className="flex-row flex-align-center space-between">
+ <span className="settings-tab__advanced-link"
+ onClick={e => {
+ e.preventDefault()
+ this.setState({ showOptions: !this.state.showOptions })
+ }}
+ >
+ { t(this.state.showOptions ? 'hideAdvancedOptions' : 'showAdvancedOptions') }
+ </span>
+ <button
+ className="button btn-primary settings-tab__rpc-save-button"
+ onClick={e => {
+ e.preventDefault()
+ this.validateRpc(newRpc, chainId, ticker, nickname)
+ }}
+ >
+ { t('save') }
+ </button>
+ </div>
+ </div>
+ </div>
+ </div>
+ )
+ }
+
+ validateRpc (newRpc, chainId, ticker = 'ETH', nickname) {
+ const { setRpcTarget, displayWarning } = this.props
+ if (validUrl.isWebUri(newRpc)) {
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Settings',
+ action: 'Custom RPC',
+ name: 'Success',
+ },
+ customVariables: {
+ networkId: newRpc,
+ chainId,
+ },
+ })
+ if (!!chainId && Number.isNaN(parseInt(chainId))) {
+ return displayWarning(`${this.context.t('invalidInput')} chainId`)
+ }
+
+ setRpcTarget(newRpc, chainId, ticker, nickname)
+ } else {
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Settings',
+ action: 'Custom RPC',
+ name: 'Error',
+ },
+ customVariables: {
+ networkId: newRpc,
+ chainId,
+ },
+ })
+ const appendedRpc = `http://${newRpc}`
+
+ if (validUrl.isWebUri(appendedRpc)) {
+ displayWarning(this.context.t('uriErrorMsg'))
+ } else {
+ displayWarning(this.context.t('invalidRPC'))
+ }
+ }
+ }
+
+ renderStateLogs () {
+ const { t } = this.context
+ const { displayWarning } = this.props
+
+ return (
+ <div className="settings-page__content-row">
+ <div className="settings-page__content-item">
+ <span>{ t('stateLogs') }</span>
+ <span className="settings-page__content-description">
+ { t('stateLogsDescription') }
+ </span>
+ </div>
+ <div className="settings-page__content-item">
+ <div className="settings-page__content-item-col">
+ <Button
+ type="primary"
+ large
+ onClick={() => {
+ window.logStateString((err, result) => {
+ if (err) {
+ displayWarning(t('stateLogError'))
+ } else {
+ exportAsFile('MetaMask State Logs.json', result)
+ }
+ })
+ }}
+ >
+ { t('downloadStateLogs') }
+ </Button>
+ </div>
+ </div>
+ </div>
+ )
+ }
+
+ renderClearApproval () {
+ const { t } = this.context
+ const { showClearApprovalModal } = this.props
+ return (
+ <div className="settings-page__content-row">
+ <div className="settings-page__content-item">
+ <span>{ t('approvalData') }</span>
+ <span className="settings-page__content-description">
+ { t('approvalDataDescription') }
+ </span>
+ </div>
+ <div className="settings-page__content-item">
+ <div className="settings-page__content-item-col">
+ <Button
+ type="secondary"
+ large
+ className="settings-tab__button--orange"
+ onClick={event => {
+ event.preventDefault()
+ showClearApprovalModal()
+ }}
+ >
+ { t('clearApprovalData') }
+ </Button>
+ </div>
+ </div>
+ </div>
+ )
+ }
+
+ renderSeedWords () {
+ const { t } = this.context
+ const { history } = this.props
+
+ return (
+ <div className="settings-page__content-row">
+ <div className="settings-page__content-item">
+ <span>{ t('revealSeedWords') }</span>
+ </div>
+ <div className="settings-page__content-item">
+ <div className="settings-page__content-item-col">
+ <Button
+ type="secondary"
+ large
+ onClick={event => {
+ event.preventDefault()
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Settings',
+ action: 'Reveal Seed Phrase',
+ name: 'Reveal Seed Phrase',
+ },
+ })
+ history.push(REVEAL_SEED_ROUTE)
+ }}
+ >
+ { t('revealSeedWords') }
+ </Button>
+ </div>
+ </div>
+ </div>
+ )
+ }
+
+
+ renderMobileSync () {
+ const { t } = this.context
+ const { history } = this.props
+
+ return (
+ <div className="settings-page__content-row">
+ <div className="settings-page__content-item">
+ <span>{ t('syncWithMobile') }</span>
+ </div>
+ <div className="settings-page__content-item">
+ <div className="settings-page__content-item-col">
+ <Button
+ type="primary"
+ large
+ onClick={event => {
+ event.preventDefault()
+ history.push(MOBILE_SYNC_ROUTE)
+ }}
+ >
+ { t('syncWithMobile') }
+ </Button>
+ </div>
+ </div>
+ </div>
+ )
+ }
+
+
+ renderResetAccount () {
+ const { t } = this.context
+ const { showResetAccountConfirmationModal } = this.props
+
+ return (
+ <div className="settings-page__content-row">
+ <div className="settings-page__content-item">
+ <span>{ t('resetAccount') }</span>
+ </div>
+ <div className="settings-page__content-item">
+ <div className="settings-page__content-item-col">
+ <Button
+ type="secondary"
+ large
+ className="settings-tab__button--orange"
+ onClick={event => {
+ event.preventDefault()
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Settings',
+ action: 'Reset Account',
+ name: 'Reset Account',
+ },
+ })
+ showResetAccountConfirmationModal()
+ }}
+ >
+ { t('resetAccount') }
+ </Button>
+ </div>
+ </div>
+ </div>
+ )
+ }
+
+ renderBlockieOptIn () {
+ const { useBlockie, setUseBlockie } = this.props
+
+ return (
+ <div className="settings-page__content-row">
+ <div className="settings-page__content-item">
+ <span>{ this.context.t('blockiesIdenticon') }</span>
+ </div>
+ <div className="settings-page__content-item">
+ <div className="settings-page__content-item-col">
+ <ToggleButton
+ value={useBlockie}
+ onToggle={value => setUseBlockie(!value)}
+ activeLabel=""
+ inactiveLabel=""
+ />
+ </div>
+ </div>
+ </div>
+ )
+ }
+
+ renderHexDataOptIn () {
+ const { t } = this.context
+ const { sendHexData, setHexDataFeatureFlag } = this.props
+
+ return (
+ <div className="settings-page__content-row">
+ <div className="settings-page__content-item">
+ <span>{ t('showHexData') }</span>
+ <div className="settings-page__content-description">
+ { t('showHexDataDescription') }
+ </div>
+ </div>
+ <div className="settings-page__content-item">
+ <div className="settings-page__content-item-col">
+ <ToggleButton
+ value={sendHexData}
+ onToggle={value => setHexDataFeatureFlag(!value)}
+ activeLabel=""
+ inactiveLabel=""
+ />
+ </div>
+ </div>
+ </div>
+ )
+ }
+
+ renderAdvancedGasInputInline () {
+ const { t } = this.context
+ const { advancedInlineGas, setAdvancedInlineGasFeatureFlag } = this.props
+
+ return (
+ <div className="settings-page__content-row">
+ <div className="settings-page__content-item">
+ <span>{ t('showAdvancedGasInline') }</span>
+ <div className="settings-page__content-description">
+ { t('showAdvancedGasInlineDescription') }
+ </div>
+ </div>
+ <div className="settings-page__content-item">
+ <div className="settings-page__content-item-col">
+ <ToggleButton
+ value={advancedInlineGas}
+ onToggle={value => setAdvancedInlineGasFeatureFlag(!value)}
+ activeLabel=""
+ inactiveLabel=""
+ />
+ </div>
+ </div>
+ </div>
+ )
+ }
+
+ renderUsePrimaryCurrencyOptions () {
+ const { t } = this.context
+ const {
+ nativeCurrency,
+ setUseNativeCurrencyAsPrimaryCurrencyPreference,
+ useNativeCurrencyAsPrimaryCurrency,
+ } = this.props
+
+ return (
+ <div className="settings-page__content-row">
+ <div className="settings-page__content-item">
+ <span>{ t('primaryCurrencySetting') }</span>
+ <div className="settings-page__content-description">
+ { t('primaryCurrencySettingDescription') }
+ </div>
+ </div>
+ <div className="settings-page__content-item">
+ <div className="settings-page__content-item-col">
+ <div className="settings-tab__radio-buttons">
+ <div className="settings-tab__radio-button">
+ <input
+ type="radio"
+ id="native-primary-currency"
+ onChange={() => setUseNativeCurrencyAsPrimaryCurrencyPreference(true)}
+ checked={Boolean(useNativeCurrencyAsPrimaryCurrency)}
+ />
+ <label
+ htmlFor="native-primary-currency"
+ className="settings-tab__radio-label"
+ >
+ { nativeCurrency }
+ </label>
+ </div>
+ <div className="settings-tab__radio-button">
+ <input
+ type="radio"
+ id="fiat-primary-currency"
+ onChange={() => setUseNativeCurrencyAsPrimaryCurrencyPreference(false)}
+ checked={!useNativeCurrencyAsPrimaryCurrency}
+ />
+ <label
+ htmlFor="fiat-primary-currency"
+ className="settings-tab__radio-label"
+ >
+ { t('fiat') }
+ </label>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ )
+ }
+
+ renderShowConversionInTestnets () {
+ const { t } = this.context
+ const {
+ showFiatInTestnets,
+ setShowFiatConversionOnTestnetsPreference,
+ } = this.props
+
+ return (
+ <div className="settings-page__content-row">
+ <div className="settings-page__content-item">
+ <span>{ t('showFiatConversionInTestnets') }</span>
+ <div className="settings-page__content-description">
+ { t('showFiatConversionInTestnetsDescription') }
+ </div>
+ </div>
+ <div className="settings-page__content-item">
+ <div className="settings-page__content-item-col">
+ <ToggleButton
+ value={showFiatInTestnets}
+ onToggle={value => setShowFiatConversionOnTestnetsPreference(!value)}
+ activeLabel=""
+ inactiveLabel=""
+ />
+ </div>
+ </div>
+ </div>
+ )
+ }
+
+ renderPrivacyOptIn () {
+ const { t } = this.context
+ const { privacyMode, setPrivacyMode } = this.props
+
+ return (
+ <div className="settings-page__content-row">
+ <div className="settings-page__content-item">
+ <span>{ t('privacyMode') }</span>
+ <div className="settings-page__content-description">
+ { t('privacyModeDescription') }
+ </div>
+ </div>
+ <div className="settings-page__content-item">
+ <div className="settings-page__content-item-col">
+ <ToggleButton
+ value={privacyMode}
+ onToggle={value => setPrivacyMode(!value)}
+ activeLabel=""
+ inactiveLabel=""
+ />
+ </div>
+ </div>
+ </div>
+ )
+ }
+
+ renderMetaMetricsOptIn () {
+ const { t } = this.context
+ const { participateInMetaMetrics, setParticipateInMetaMetrics } = this.props
+
+ return (
+ <div className="settings-page__content-row">
+ <div className="settings-page__content-item">
+ <span>{ t('participateInMetaMetrics') }</span>
+ <div className="settings-page__content-description">
+ <span>{ t('participateInMetaMetricsDescription') }</span>
+ </div>
+ </div>
+ <div className="settings-page__content-item">
+ <div className="settings-page__content-item-col">
+ <ToggleButton
+ value={participateInMetaMetrics}
+ onToggle={value => setParticipateInMetaMetrics(!value)}
+ activeLabel=""
+ inactiveLabel=""
+ />
+ </div>
+ </div>
+ </div>
+ )
+ }
+
+ render () {
+ const { warning } = this.props
+
+ return (
+ <div className="settings-page__content">
+ { warning && <div className="settings-tab__error">{ warning }</div> }
+ { this.renderCurrentConversion() }
+ { this.renderUsePrimaryCurrencyOptions() }
+ { this.renderShowConversionInTestnets() }
+ { this.renderCurrentLocale() }
+ { this.renderNewRpcUrl() }
+ { this.renderStateLogs() }
+ { this.renderSeedWords() }
+ { this.renderResetAccount() }
+ { this.renderClearApproval() }
+ { this.renderPrivacyOptIn() }
+ { this.renderHexDataOptIn() }
+ { this.renderAdvancedGasInputInline() }
+ { this.renderBlockieOptIn() }
+ { this.renderMobileSync() }
+ { this.renderMetaMetricsOptIn() }
+ </div>
+ )
+ }
+}
diff --git a/ui/app/pages/settings/settings-tab/settings-tab.container.js b/ui/app/pages/settings/settings-tab/settings-tab.container.js
new file mode 100644
index 000000000..3ae4985d7
--- /dev/null
+++ b/ui/app/pages/settings/settings-tab/settings-tab.container.js
@@ -0,0 +1,81 @@
+import SettingsTab from './settings-tab.component'
+import { compose } from 'recompose'
+import { connect } from 'react-redux'
+import { withRouter } from 'react-router-dom'
+import {
+ setCurrentCurrency,
+ updateAndSetCustomRpc,
+ displayWarning,
+ revealSeedConfirmation,
+ setUseBlockie,
+ updateCurrentLocale,
+ setFeatureFlag,
+ showModal,
+ setUseNativeCurrencyAsPrimaryCurrencyPreference,
+ setShowFiatConversionOnTestnetsPreference,
+ setParticipateInMetaMetrics,
+} from '../../../store/actions'
+import { preferencesSelector } from '../../../selectors/selectors'
+
+const mapStateToProps = state => {
+ const { appState: { warning }, metamask } = state
+ const {
+ currentCurrency,
+ conversionDate,
+ nativeCurrency,
+ useBlockie,
+ featureFlags: {
+ sendHexData,
+ privacyMode,
+ advancedInlineGas,
+ } = {},
+ provider = {},
+ currentLocale,
+ participateInMetaMetrics,
+ } = metamask
+ const { useNativeCurrencyAsPrimaryCurrency, showFiatInTestnets } = preferencesSelector(state)
+
+ return {
+ warning,
+ currentLocale,
+ currentCurrency,
+ conversionDate,
+ nativeCurrency,
+ useBlockie,
+ sendHexData,
+ advancedInlineGas,
+ privacyMode,
+ provider,
+ useNativeCurrencyAsPrimaryCurrency,
+ showFiatInTestnets,
+ participateInMetaMetrics,
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ setCurrentCurrency: currency => dispatch(setCurrentCurrency(currency)),
+ setRpcTarget: (newRpc, chainId, ticker, nickname) => dispatch(updateAndSetCustomRpc(newRpc, chainId, ticker, nickname)),
+ displayWarning: warning => dispatch(displayWarning(warning)),
+ revealSeedConfirmation: () => dispatch(revealSeedConfirmation()),
+ setUseBlockie: value => dispatch(setUseBlockie(value)),
+ updateCurrentLocale: key => dispatch(updateCurrentLocale(key)),
+ setHexDataFeatureFlag: shouldShow => dispatch(setFeatureFlag('sendHexData', shouldShow)),
+ setAdvancedInlineGasFeatureFlag: shouldShow => dispatch(setFeatureFlag('advancedInlineGas', shouldShow)),
+ setPrivacyMode: enabled => dispatch(setFeatureFlag('privacyMode', enabled)),
+ showResetAccountConfirmationModal: () => dispatch(showModal({ name: 'CONFIRM_RESET_ACCOUNT' })),
+ setUseNativeCurrencyAsPrimaryCurrencyPreference: value => {
+ return dispatch(setUseNativeCurrencyAsPrimaryCurrencyPreference(value))
+ },
+ setShowFiatConversionOnTestnetsPreference: value => {
+ return dispatch(setShowFiatConversionOnTestnetsPreference(value))
+ },
+ showClearApprovalModal: () => dispatch(showModal({ name: 'CLEAR_APPROVED_ORIGINS' })),
+ setParticipateInMetaMetrics: (val) => dispatch(setParticipateInMetaMetrics(val)),
+ }
+}
+
+export default compose(
+ withRouter,
+ connect(mapStateToProps, mapDispatchToProps)
+)(SettingsTab)
diff --git a/ui/app/pages/settings/settings.component.js b/ui/app/pages/settings/settings.component.js
new file mode 100644
index 000000000..d67d3fcfe
--- /dev/null
+++ b/ui/app/pages/settings/settings.component.js
@@ -0,0 +1,54 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import { Switch, Route, matchPath } from 'react-router-dom'
+import TabBar from '../../components/app/tab-bar'
+import SettingsTab from './settings-tab'
+import InfoTab from './info-tab'
+import { DEFAULT_ROUTE, SETTINGS_ROUTE, INFO_ROUTE } from '../../helpers/constants/routes'
+
+export default class SettingsPage extends PureComponent {
+ static propTypes = {
+ location: PropTypes.object,
+ history: PropTypes.object,
+ t: PropTypes.func,
+ }
+
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ render () {
+ const { history, location } = this.props
+
+ return (
+ <div className="main-container settings-page">
+ <div className="settings-page__header">
+ <div
+ className="settings-page__close-button"
+ onClick={() => history.push(DEFAULT_ROUTE)}
+ />
+ <TabBar
+ tabs={[
+ { content: this.context.t('settings'), key: SETTINGS_ROUTE },
+ { content: this.context.t('info'), key: INFO_ROUTE },
+ ]}
+ isActive={key => matchPath(location.pathname, { path: key, exact: true })}
+ onSelect={key => history.push(key)}
+ />
+ </div>
+ <Switch>
+ <Route
+ exact
+ path={INFO_ROUTE}
+ component={InfoTab}
+ />
+ <Route
+ exact
+ path={SETTINGS_ROUTE}
+ component={SettingsTab}
+ />
+ </Switch>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/pages/unlock-page/index.js b/ui/app/pages/unlock-page/index.js
new file mode 100644
index 000000000..be80cde4f
--- /dev/null
+++ b/ui/app/pages/unlock-page/index.js
@@ -0,0 +1,2 @@
+import UnlockPage from './unlock-page.container'
+module.exports = UnlockPage
diff --git a/ui/app/pages/unlock-page/index.scss b/ui/app/pages/unlock-page/index.scss
new file mode 100644
index 000000000..3d44bd037
--- /dev/null
+++ b/ui/app/pages/unlock-page/index.scss
@@ -0,0 +1,51 @@
+.unlock-page {
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-start;
+ align-items: center;
+ width: 357px;
+ padding: 30px;
+ font-weight: 400;
+ color: $silver-chalice;
+
+ &__container {
+ background: $white;
+ display: flex;
+ align-self: stretch;
+ justify-content: center;
+ flex: 1 0 auto;
+ }
+
+ &__mascot-container {
+ margin-top: 24px;
+ }
+
+ &__title {
+ margin-top: 5px;
+ font-size: 2rem;
+ font-weight: 800;
+ color: $tundora;
+ }
+
+ &__form {
+ width: 100%;
+ margin: 56px 0 8px;
+ }
+
+ &__links {
+ margin-top: 25px;
+ width: 100%;
+ }
+
+ &__link {
+ cursor: pointer;
+
+ &--import {
+ color: $ecstasy;
+ }
+
+ &--use-classic {
+ margin-top: 10px;
+ }
+ }
+}
diff --git a/ui/app/pages/unlock-page/unlock-page.component.js b/ui/app/pages/unlock-page/unlock-page.component.js
new file mode 100644
index 000000000..3aeb2a59b
--- /dev/null
+++ b/ui/app/pages/unlock-page/unlock-page.component.js
@@ -0,0 +1,191 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import Button from '@material-ui/core/Button'
+import TextField from '../../components/ui/text-field'
+import getCaretCoordinates from 'textarea-caret'
+import { EventEmitter } from 'events'
+import Mascot from '../../components/ui/mascot'
+import { DEFAULT_ROUTE } from '../../helpers/constants/routes'
+
+export default class UnlockPage extends Component {
+ static contextTypes = {
+ metricsEvent: PropTypes.func,
+ t: PropTypes.func,
+ }
+
+ static propTypes = {
+ history: PropTypes.object,
+ isUnlocked: PropTypes.bool,
+ onImport: PropTypes.func,
+ onRestore: PropTypes.func,
+ onSubmit: PropTypes.func,
+ forceUpdateMetamaskState: PropTypes.func,
+ showOptInModal: PropTypes.func,
+ }
+
+ constructor (props) {
+ super(props)
+
+ this.state = {
+ password: '',
+ error: null,
+ }
+
+ this.submitting = false
+ this.animationEventEmitter = new EventEmitter()
+ }
+
+ componentWillMount () {
+ const { isUnlocked, history } = this.props
+
+ if (isUnlocked) {
+ history.push(DEFAULT_ROUTE)
+ }
+ }
+
+ handleSubmit = async event => {
+ event.preventDefault()
+ event.stopPropagation()
+
+ const { password } = this.state
+ const { onSubmit, forceUpdateMetamaskState, showOptInModal } = this.props
+
+ if (password === '' || this.submitting) {
+ return
+ }
+
+ this.setState({ error: null })
+ this.submitting = true
+
+ try {
+ await onSubmit(password)
+ const newState = await forceUpdateMetamaskState()
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Navigation',
+ action: 'Unlock',
+ name: 'Success',
+ },
+ isNewVisit: true,
+ })
+
+ if (newState.participateInMetaMetrics === null || newState.participateInMetaMetrics === undefined) {
+ showOptInModal()
+ }
+ } catch ({ message }) {
+ if (message === 'Incorrect password') {
+ const newState = await forceUpdateMetamaskState()
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Navigation',
+ action: 'Unlock',
+ name: 'Incorrect Passowrd',
+ },
+ customVariables: {
+ numberOfTokens: newState.tokens.length,
+ numberOfAccounts: Object.keys(newState.accounts).length,
+ },
+ })
+ }
+
+ this.setState({ error: message })
+ this.submitting = false
+ }
+ }
+
+ handleInputChange ({ target }) {
+ this.setState({ password: target.value, error: null })
+
+ // tell mascot to look at page action
+ const element = target
+ const boundingRect = element.getBoundingClientRect()
+ const coordinates = getCaretCoordinates(element, element.selectionEnd)
+ this.animationEventEmitter.emit('point', {
+ x: boundingRect.left + coordinates.left - element.scrollLeft,
+ y: boundingRect.top + coordinates.top - element.scrollTop,
+ })
+ }
+
+ renderSubmitButton () {
+ const style = {
+ backgroundColor: '#f7861c',
+ color: 'white',
+ marginTop: '20px',
+ height: '60px',
+ fontWeight: '400',
+ boxShadow: 'none',
+ borderRadius: '4px',
+ }
+
+ return (
+ <Button
+ type="submit"
+ style={style}
+ disabled={!this.state.password}
+ fullWidth
+ variant="raised"
+ size="large"
+ onClick={this.handleSubmit}
+ disableRipple
+ >
+ { this.context.t('login') }
+ </Button>
+ )
+ }
+
+ render () {
+ const { password, error } = this.state
+ const { t } = this.context
+ const { onImport, onRestore } = this.props
+
+ return (
+ <div className="unlock-page__container">
+ <div className="unlock-page">
+ <div className="unlock-page__mascot-container">
+ <Mascot
+ animationEventEmitter={this.animationEventEmitter}
+ width="120"
+ height="120"
+ />
+ </div>
+ <h1 className="unlock-page__title">
+ { t('welcomeBack') }
+ </h1>
+ <div>{ t('unlockMessage') }</div>
+ <form
+ className="unlock-page__form"
+ onSubmit={this.handleSubmit}
+ >
+ <TextField
+ id="password"
+ label={t('password')}
+ type="password"
+ value={password}
+ onChange={event => this.handleInputChange(event)}
+ error={error}
+ autoFocus
+ autoComplete="current-password"
+ material
+ fullWidth
+ />
+ </form>
+ { this.renderSubmitButton() }
+ <div className="unlock-page__links">
+ <div
+ className="unlock-page__link"
+ onClick={() => onRestore()}
+ >
+ { t('restoreFromSeed') }
+ </div>
+ <div
+ className="unlock-page__link unlock-page__link--import"
+ onClick={() => onImport()}
+ >
+ { t('importUsingSeed') }
+ </div>
+ </div>
+ </div>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/pages/unlock-page/unlock-page.container.js b/ui/app/pages/unlock-page/unlock-page.container.js
new file mode 100644
index 000000000..bd43666fc
--- /dev/null
+++ b/ui/app/pages/unlock-page/unlock-page.container.js
@@ -0,0 +1,64 @@
+import { connect } from 'react-redux'
+import { withRouter } from 'react-router-dom'
+import { compose } from 'recompose'
+import { getEnvironmentType } from '../../../../app/scripts/lib/util'
+import { ENVIRONMENT_TYPE_POPUP } from '../../../../app/scripts/lib/enums'
+import { DEFAULT_ROUTE, RESTORE_VAULT_ROUTE } from '../../helpers/constants/routes'
+import {
+ tryUnlockMetamask,
+ forgotPassword,
+ markPasswordForgotten,
+ forceUpdateMetamaskState,
+ showModal,
+} from '../../store/actions'
+import UnlockPage from './unlock-page.component'
+
+const mapStateToProps = state => {
+ const { metamask: { isUnlocked } } = state
+ return {
+ isUnlocked,
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ forgotPassword: () => dispatch(forgotPassword()),
+ tryUnlockMetamask: password => dispatch(tryUnlockMetamask(password)),
+ markPasswordForgotten: () => dispatch(markPasswordForgotten()),
+ forceUpdateMetamaskState: () => forceUpdateMetamaskState(dispatch),
+ showOptInModal: () => dispatch(showModal({ name: 'METAMETRICS_OPT_IN_MODAL' })),
+ }
+}
+
+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, mergeProps)
+)(UnlockPage)