aboutsummaryrefslogtreecommitdiffstats
path: root/ui/app/components/pages
diff options
context:
space:
mode:
Diffstat (limited to 'ui/app/components/pages')
-rw-r--r--ui/app/components/pages/add-token.js431
-rw-r--r--ui/app/components/pages/authenticated.js34
-rw-r--r--ui/app/components/pages/create-account/import-account/index.js96
-rw-r--r--ui/app/components/pages/create-account/import-account/json.js141
-rw-r--r--ui/app/components/pages/create-account/import-account/private-key.js95
-rw-r--r--ui/app/components/pages/create-account/import-account/seed.js35
-rw-r--r--ui/app/components/pages/create-account/index.js81
-rw-r--r--ui/app/components/pages/create-account/new-account.js103
-rw-r--r--ui/app/components/pages/home.js332
-rw-r--r--ui/app/components/pages/initialized.js25
-rw-r--r--ui/app/components/pages/keychains/restore-vault.js177
-rw-r--r--ui/app/components/pages/keychains/reveal-seed.js195
-rw-r--r--ui/app/components/pages/metamask-route.js28
-rw-r--r--ui/app/components/pages/notice.js203
-rw-r--r--ui/app/components/pages/settings/index.js59
-rw-r--r--ui/app/components/pages/settings/info.js112
-rw-r--r--ui/app/components/pages/settings/settings.js367
-rw-r--r--ui/app/components/pages/unlock.js193
18 files changed, 2707 insertions, 0 deletions
diff --git a/ui/app/components/pages/add-token.js b/ui/app/components/pages/add-token.js
new file mode 100644
index 000000000..566e42450
--- /dev/null
+++ b/ui/app/components/pages/add-token.js
@@ -0,0 +1,431 @@
+const inherits = require('util').inherits
+const Component = require('react').Component
+const classnames = require('classnames')
+const h = require('react-hyperscript')
+const PropTypes = require('prop-types')
+const connect = require('react-redux').connect
+const R = require('ramda')
+const Fuse = require('fuse.js')
+const contractMap = require('eth-contract-metadata')
+const TokenBalance = require('../../components/token-balance')
+const Identicon = require('../../components/identicon')
+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 },
+ ],
+})
+const actions = require('../../actions')
+const ethUtil = require('ethereumjs-util')
+const { tokenInfoGetter } = require('../../token-util')
+const { DEFAULT_ROUTE } = require('../../routes')
+
+const emptyAddr = '0x0000000000000000000000000000000000000000'
+
+AddTokenScreen.contextTypes = {
+ t: PropTypes.func,
+}
+
+module.exports = connect(mapStateToProps, mapDispatchToProps)(AddTokenScreen)
+
+
+function mapStateToProps (state) {
+ const { identities, tokens } = state.metamask
+ return {
+ identities,
+ tokens,
+ }
+}
+
+function mapDispatchToProps (dispatch) {
+ return {
+ addTokens: tokens => dispatch(actions.addTokens(tokens)),
+ }
+}
+
+inherits(AddTokenScreen, Component)
+function AddTokenScreen () {
+ this.state = {
+ isShowingConfirmation: false,
+ isShowingInfoBox: true,
+ customAddress: '',
+ customSymbol: '',
+ customDecimals: '',
+ searchQuery: '',
+ selectedTokens: {},
+ errors: {},
+ autoFilled: false,
+ displayedTab: 'SEARCH',
+ }
+ this.tokenAddressDidChange = this.tokenAddressDidChange.bind(this)
+ this.tokenSymbolDidChange = this.tokenSymbolDidChange.bind(this)
+ this.tokenDecimalsDidChange = this.tokenDecimalsDidChange.bind(this)
+ this.onNext = this.onNext.bind(this)
+ Component.call(this)
+}
+
+AddTokenScreen.prototype.componentWillMount = function () {
+ this.tokenInfoGetter = tokenInfoGetter()
+}
+
+AddTokenScreen.prototype.toggleToken = function (address, token) {
+ const { selectedTokens = {}, errors } = this.state
+ const selectedTokensCopy = { ...selectedTokens }
+
+ if (address in selectedTokensCopy) {
+ delete selectedTokensCopy[address]
+ } else {
+ selectedTokensCopy[address] = token
+ }
+
+ this.setState({
+ selectedTokens: selectedTokensCopy,
+ errors: {
+ ...errors,
+ tokenSelector: null,
+ },
+ })
+}
+
+AddTokenScreen.prototype.onNext = function () {
+ const { isValid, errors } = this.validate()
+
+ return !isValid
+ ? this.setState({ errors })
+ : this.setState({ isShowingConfirmation: true })
+}
+
+AddTokenScreen.prototype.tokenAddressDidChange = function (e) {
+ const customAddress = e.target.value.trim()
+ this.setState({ customAddress })
+ if (ethUtil.isValidAddress(customAddress) && customAddress !== emptyAddr) {
+ this.attemptToAutoFillTokenParams(customAddress)
+ } else {
+ this.setState({
+ customSymbol: '',
+ customDecimals: 0,
+ })
+ }
+}
+
+AddTokenScreen.prototype.tokenSymbolDidChange = function (e) {
+ const customSymbol = e.target.value.trim()
+ this.setState({ customSymbol })
+}
+
+AddTokenScreen.prototype.tokenDecimalsDidChange = function (e) {
+ const customDecimals = e.target.value.trim()
+ this.setState({ customDecimals })
+}
+
+AddTokenScreen.prototype.checkExistingAddresses = function (address) {
+ if (!address) return false
+ const tokensList = this.props.tokens
+ const matchesAddress = existingToken => {
+ return existingToken.address.toLowerCase() === address.toLowerCase()
+ }
+
+ return R.any(matchesAddress)(tokensList)
+}
+
+AddTokenScreen.prototype.validate = function () {
+ const errors = {}
+ const identitiesList = Object.keys(this.props.identities)
+ const { customAddress, customSymbol, customDecimals, selectedTokens } = this.state
+ const standardAddress = ethUtil.addHexPrefix(customAddress).toLowerCase()
+
+ if (customAddress) {
+ const validAddress = ethUtil.isValidAddress(customAddress)
+ if (!validAddress) {
+ errors.customAddress = this.context.t('invalidAddress')
+ }
+
+ const validDecimals = customDecimals !== null
+ && customDecimals !== ''
+ && customDecimals >= 0
+ && customDecimals < 36
+ if (!validDecimals) {
+ errors.customDecimals = this.context.t('decimalsMustZerotoTen')
+ }
+
+ const symbolLen = customSymbol.trim().length
+ const validSymbol = symbolLen > 0 && symbolLen < 10
+ if (!validSymbol) {
+ errors.customSymbol = this.context.t('symbolBetweenZeroTen')
+ }
+
+ const ownAddress = identitiesList.includes(standardAddress)
+ if (ownAddress) {
+ errors.customAddress = this.context.t('personalAddressDetected')
+ }
+
+ const tokenAlreadyAdded = this.checkExistingAddresses(customAddress)
+ if (tokenAlreadyAdded) {
+ errors.customAddress = this.context.t('tokenAlreadyAdded')
+ }
+ } else if (
+ Object.entries(selectedTokens)
+ .reduce((isEmpty, [ symbol, isSelected ]) => (
+ isEmpty && !isSelected
+ ), true)
+ ) {
+ errors.tokenSelector = this.context.t('mustSelectOne')
+ }
+
+ return {
+ isValid: !Object.keys(errors).length,
+ errors,
+ }
+}
+
+AddTokenScreen.prototype.attemptToAutoFillTokenParams = async function (address) {
+ const { symbol, decimals } = await this.tokenInfoGetter(address)
+ if (symbol && decimals) {
+ this.setState({
+ customSymbol: symbol,
+ customDecimals: decimals.toString(),
+ autoFilled: true,
+ })
+ }
+}
+
+AddTokenScreen.prototype.renderCustomForm = function () {
+ const { autoFilled, customAddress, customSymbol, customDecimals, errors } = this.state
+
+ return (
+ h('div.add-token__add-custom-form', [
+ h('div', {
+ className: classnames('add-token__add-custom-field', {
+ 'add-token__add-custom-field--error': errors.customAddress,
+ }),
+ }, [
+ h('div.add-token__add-custom-label', this.context.t('tokenAddress')),
+ h('input.add-token__add-custom-input', {
+ type: 'text',
+ onChange: this.tokenAddressDidChange,
+ value: customAddress,
+ }),
+ h('div.add-token__add-custom-error-message', errors.customAddress),
+ ]),
+ h('div', {
+ className: classnames('add-token__add-custom-field', {
+ 'add-token__add-custom-field--error': errors.customSymbol,
+ }),
+ }, [
+ h('div.add-token__add-custom-label', this.context.t('tokenSymbol')),
+ h('input.add-token__add-custom-input', {
+ type: 'text',
+ onChange: this.tokenSymbolDidChange,
+ value: customSymbol,
+ disabled: autoFilled,
+ }),
+ h('div.add-token__add-custom-error-message', errors.customSymbol),
+ ]),
+ h('div', {
+ className: classnames('add-token__add-custom-field', {
+ 'add-token__add-custom-field--error': errors.customDecimals,
+ }),
+ }, [
+ h('div.add-token__add-custom-label', this.context.t('decimal')),
+ h('input.add-token__add-custom-input', {
+ type: 'number',
+ onChange: this.tokenDecimalsDidChange,
+ value: customDecimals,
+ disabled: autoFilled,
+ }),
+ h('div.add-token__add-custom-error-message', errors.customDecimals),
+ ]),
+ ])
+ )
+}
+
+AddTokenScreen.prototype.renderTokenList = function () {
+ const { searchQuery = '', selectedTokens } = this.state
+ const fuseSearchResult = fuse.search(searchQuery)
+ const addressSearchResult = contractList.filter(token => {
+ return token.address.toLowerCase() === searchQuery.toLowerCase()
+ })
+ const results = [...addressSearchResult, ...fuseSearchResult]
+
+ return h('div', [
+ results.length > 0 && h('div.add-token__token-icons-title', this.context.t('popularTokens')),
+ h('div.add-token__token-icons-container', Array(6).fill(undefined)
+ .map((_, i) => {
+ const { logo, symbol, name, address } = results[i] || {}
+ const tokenAlreadyAdded = this.checkExistingAddresses(address)
+ return Boolean(logo || symbol || name) && (
+ h('div.add-token__token-wrapper', {
+ className: classnames({
+ 'add-token__token-wrapper--selected': selectedTokens[address],
+ 'add-token__token-wrapper--disabled': tokenAlreadyAdded,
+ }),
+ onClick: () => !tokenAlreadyAdded && this.toggleToken(address, results[i]),
+ }, [
+ h('div.add-token__token-icon', {
+ style: {
+ backgroundImage: logo && `url(images/contract/${logo})`,
+ },
+ }),
+ h('div.add-token__token-data', [
+ h('div.add-token__token-symbol', symbol),
+ h('div.add-token__token-name', name),
+ ]),
+ // tokenAlreadyAdded && (
+ // h('div.add-token__token-message', 'Already added')
+ // ),
+ ])
+ )
+ })),
+ ])
+}
+
+AddTokenScreen.prototype.renderConfirmation = function () {
+ const {
+ customAddress: address,
+ customSymbol: symbol,
+ customDecimals: decimals,
+ selectedTokens,
+ } = this.state
+
+ const { addTokens, history } = this.props
+
+ const customToken = {
+ address,
+ symbol,
+ decimals,
+ }
+
+ const tokens = address && symbol && decimals
+ ? { ...selectedTokens, [address]: customToken }
+ : selectedTokens
+
+ return (
+ h('div.add-token', [
+ h('div.add-token__wrapper', [
+ h('div.add-token__content-container.add-token__confirmation-content', [
+ h('div.add-token__description.add-token__confirmation-description', this.context.t('balances')),
+ h('div.add-token__confirmation-token-list',
+ Object.entries(tokens)
+ .map(([ address, token ]) => (
+ h('span.add-token__confirmation-token-list-item', [
+ h(Identicon, {
+ className: 'add-token__confirmation-token-icon',
+ diameter: 75,
+ address,
+ }),
+ h(TokenBalance, { token }),
+ ])
+ ))
+ ),
+ ]),
+ ]),
+ h('div.add-token__buttons', [
+ h('button.btn-secondary--lg.add-token__cancel-button', {
+ onClick: () => this.setState({ isShowingConfirmation: false }),
+ }, this.context.t('back')),
+ h('button.btn-primary--lg', {
+ onClick: () => addTokens(tokens).then(() => history.push(DEFAULT_ROUTE)),
+ }, this.context.t('addTokens')),
+ ]),
+ ])
+ )
+}
+
+AddTokenScreen.prototype.displayTab = function (selectedTab) {
+ this.setState({ displayedTab: selectedTab })
+}
+
+AddTokenScreen.prototype.renderTabs = function () {
+ const { isShowingInfoBox, displayedTab, errors } = this.state
+
+ return displayedTab === 'CUSTOM_TOKEN'
+ ? this.renderCustomForm()
+ : h('div', [
+ h('div.add-token__wrapper', [
+ h('div.add-token__content-container', [
+ isShowingInfoBox && h('div.add-token__info-box', [
+ h('div.add-token__info-box__close', {
+ onClick: () => this.setState({ isShowingInfoBox: false }),
+ }),
+ h('div.add-token__info-box__title', this.context.t('whatsThis')),
+ h('div.add-token__info-box__copy', this.context.t('keepTrackTokens')),
+ h('a.add-token__info-box__copy--blue', {
+ href: 'http://metamask.helpscoutdocs.com/article/16-managing-erc20-tokens',
+ target: '_blank',
+ }, this.context.t('learnMore')),
+ ]),
+ h('div.add-token__input-container', [
+ h('input.add-token__input', {
+ type: 'text',
+ placeholder: this.context.t('searchTokens'),
+ onChange: e => this.setState({ searchQuery: e.target.value }),
+ }),
+ h('div.add-token__search-input-error-message', errors.tokenSelector),
+ ]),
+ this.renderTokenList(),
+ ]),
+ ]),
+ ])
+}
+
+AddTokenScreen.prototype.render = function () {
+ const {
+ isShowingConfirmation,
+ displayedTab,
+ } = this.state
+ const { history } = this.props
+
+ return h('div.add-token', [
+ h('div.add-token__header', [
+ h('div.add-token__header__cancel', {
+ onClick: () => history.push(DEFAULT_ROUTE),
+ }, [
+ h('i.fa.fa-angle-left.fa-lg'),
+ h('span', this.context.t('cancel')),
+ ]),
+ h('div.add-token__header__title', this.context.t('addTokens')),
+ isShowingConfirmation && h('div.add-token__header__subtitle', this.context.t('likeToAddTokens')),
+ !isShowingConfirmation && h('div.add-token__header__tabs', [
+
+ h('div.add-token__header__tabs__tab', {
+ className: classnames('add-token__header__tabs__tab', {
+ 'add-token__header__tabs__selected': displayedTab === 'SEARCH',
+ 'add-token__header__tabs__unselected': displayedTab !== 'SEARCH',
+ }),
+ onClick: () => this.displayTab('SEARCH'),
+ }, this.context.t('search')),
+
+ h('div.add-token__header__tabs__tab', {
+ className: classnames('add-token__header__tabs__tab', {
+ 'add-token__header__tabs__selected': displayedTab === 'CUSTOM_TOKEN',
+ 'add-token__header__tabs__unselected': displayedTab !== 'CUSTOM_TOKEN',
+ }),
+ onClick: () => this.displayTab('CUSTOM_TOKEN'),
+ }, this.context.t('customToken')),
+
+ ]),
+ ]),
+
+ isShowingConfirmation
+ ? this.renderConfirmation()
+ : this.renderTabs(),
+
+ !isShowingConfirmation && h('div.add-token__buttons', [
+ h('button.btn-secondary--lg.add-token__cancel-button', {
+ onClick: () => history.push(DEFAULT_ROUTE),
+ }, this.context.t('cancel')),
+ h('button.btn-primary--lg.add-token__confirm-button', {
+ onClick: this.onNext,
+ }, this.context.t('next')),
+ ]),
+ ])
+}
diff --git a/ui/app/components/pages/authenticated.js b/ui/app/components/pages/authenticated.js
new file mode 100644
index 000000000..1f6b0be49
--- /dev/null
+++ b/ui/app/components/pages/authenticated.js
@@ -0,0 +1,34 @@
+const { connect } = require('react-redux')
+const PropTypes = require('prop-types')
+const { Redirect } = require('react-router-dom')
+const h = require('react-hyperscript')
+const MetamaskRoute = require('./metamask-route')
+const { UNLOCK_ROUTE, INITIALIZE_ROUTE } = require('../../routes')
+
+const Authenticated = props => {
+ const { isUnlocked, isInitialized } = props
+
+ switch (true) {
+ case isUnlocked && isInitialized:
+ return h(MetamaskRoute, { ...props })
+ case !isInitialized:
+ return h(Redirect, { to: { pathname: INITIALIZE_ROUTE } })
+ default:
+ return h(Redirect, { to: { pathname: UNLOCK_ROUTE } })
+ }
+}
+
+Authenticated.propTypes = {
+ isUnlocked: PropTypes.bool,
+ isInitialized: PropTypes.bool,
+}
+
+const mapStateToProps = state => {
+ const { metamask: { isUnlocked, isInitialized } } = state
+ return {
+ isUnlocked,
+ isInitialized,
+ }
+}
+
+module.exports = connect(mapStateToProps)(Authenticated)
diff --git a/ui/app/components/pages/create-account/import-account/index.js b/ui/app/components/pages/create-account/import-account/index.js
new file mode 100644
index 000000000..52d3dcde9
--- /dev/null
+++ b/ui/app/components/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.helpscoutdocs.com/article/17-what-are-loose-accounts',
+ })
+ },
+ }, 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/components/pages/create-account/import-account/json.js b/ui/app/components/pages/create-account/import-account/json.js
new file mode 100644
index 000000000..946907a47
--- /dev/null
+++ b/ui/app/components/pages/create-account/import-account/json.js
@@ -0,0 +1,141 @@
+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('../../../../actions')
+const FileInput = require('react-simple-file-input').default
+const { DEFAULT_ROUTE } = require('../../../../routes')
+const HELP_LINK = 'https://support.metamask.io/kb/article/7-importing-accounts'
+
+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.btn-secondary.new-account-create-form__button', {
+ onClick: () => this.props.history.push(DEFAULT_ROUTE),
+ }, [
+ this.context.t('cancel'),
+ ]),
+
+ h('button.btn-primary.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 state = this.state
+
+ if (!state) {
+ const message = this.context.t('validFileImport')
+ return this.props.displayWarning(message)
+ }
+
+ const { fileContents } = state
+
+ if (!fileContents) {
+ const message = this.context.t('needImportFile')
+ return this.props.displayWarning(message)
+ }
+
+ const passwordInput = document.getElementById('json-password-box')
+ const password = passwordInput.value
+
+ if (!password) {
+ const message = this.context.t('needImportPassword')
+ return this.props.displayWarning(message)
+ }
+
+ this.props.importNewJsonAccount([ fileContents, password ])
+ }
+}
+
+JsonImportSubview.propTypes = {
+ error: PropTypes.string,
+ goHome: PropTypes.func,
+ displayWarning: PropTypes.func,
+ importNewJsonAccount: PropTypes.func,
+ history: PropTypes.object,
+ t: PropTypes.func,
+}
+
+const mapStateToProps = state => {
+ return {
+ error: state.appState.warning,
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ goHome: () => dispatch(actions.goHome()),
+ displayWarning: warning => dispatch(actions.displayWarning(warning)),
+ importNewJsonAccount: options => dispatch(actions.importNewAccount('JSON File', options)),
+ }
+}
+
+JsonImportSubview.contextTypes = {
+ t: PropTypes.func,
+}
+
+module.exports = compose(
+ withRouter,
+ connect(mapStateToProps, mapDispatchToProps)
+)(JsonImportSubview)
diff --git a/ui/app/components/pages/create-account/import-account/private-key.js b/ui/app/components/pages/create-account/import-account/private-key.js
new file mode 100644
index 000000000..c77612ea4
--- /dev/null
+++ b/ui/app/components/pages/create-account/import-account/private-key.js
@@ -0,0 +1,95 @@
+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('../../../../actions')
+const { DEFAULT_ROUTE } = require('../../../../routes')
+
+PrivateKeyImportView.contextTypes = {
+ t: PropTypes.func,
+}
+
+module.exports = compose(
+ withRouter,
+ connect(mapStateToProps, mapDispatchToProps)
+)(PrivateKeyImportView)
+
+
+function mapStateToProps (state) {
+ return {
+ error: state.appState.warning,
+ }
+}
+
+function mapDispatchToProps (dispatch) {
+ return {
+ importNewAccount: (strategy, [ privateKey ]) => {
+ return dispatch(actions.importNewAccount(strategy, [ privateKey ]))
+ },
+ displayWarning: () => dispatch(actions.displayWarning(null)),
+ }
+}
+
+inherits(PrivateKeyImportView, Component)
+function PrivateKeyImportView () {
+ this.createKeyringOnEnter = this.createKeyringOnEnter.bind(this)
+ Component.call(this)
+}
+
+PrivateKeyImportView.prototype.render = function () {
+ const { error } = 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.btn-secondary--lg.new-account-create-form__button', {
+ onClick: () => this.props.history.push(DEFAULT_ROUTE),
+ }, [
+ this.context.t('cancel'),
+ ]),
+
+ h('button.btn-primary--lg.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 } = this.props
+
+ importNewAccount('Private Key', [ privateKey ])
+ .then(() => history.push(DEFAULT_ROUTE))
+}
diff --git a/ui/app/components/pages/create-account/import-account/seed.js b/ui/app/components/pages/create-account/import-account/seed.js
new file mode 100644
index 000000000..d98909baa
--- /dev/null
+++ b/ui/app/components/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/components/pages/create-account/index.js b/ui/app/components/pages/create-account/index.js
new file mode 100644
index 000000000..0962477d8
--- /dev/null
+++ b/ui/app/components/pages/create-account/index.js
@@ -0,0 +1,81 @@
+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('../../../actions')
+const { getCurrentViewContext } = require('../../../selectors')
+const classnames = require('classnames')
+const NewAccountCreateForm = require('./new-account')
+const NewAccountImportForm = require('./import-account')
+const { NEW_ACCOUNT_ROUTE, IMPORT_ACCOUNT_ROUTE } = require('../../../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),
+ }, '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),
+ }, 'Import'),
+ ])
+ }
+
+ render () {
+ return h('div.new-account', {}, [
+ h('div.new-account__header', [
+ h('div.new-account__title', 'New Account'),
+ 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,
+ }),
+ ]),
+ ]),
+ ])
+ }
+}
+
+CreateAccountPage.propTypes = {
+ location: PropTypes.object,
+ history: PropTypes.object,
+}
+
+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()),
+ saveAccountLabel: (address, label) => dispatch(actions.saveAccountLabel(address, label)),
+})
+
+module.exports = connect(mapStateToProps, mapDispatchToProps)(CreateAccountPage)
diff --git a/ui/app/components/pages/create-account/new-account.js b/ui/app/components/pages/create-account/new-account.js
new file mode 100644
index 000000000..40fa584be
--- /dev/null
+++ b/ui/app/components/pages/create-account/new-account.js
@@ -0,0 +1,103 @@
+const { Component } = require('react')
+const PropTypes = require('prop-types')
+const h = require('react-hyperscript')
+const connect = require('react-redux').connect
+const actions = require('../../../actions')
+const { DEFAULT_ROUTE } = require('../../../routes')
+
+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.btn-secondary--lg.new-account-create-form__button', {
+ onClick: () => history.push(DEFAULT_ROUTE),
+ }, [
+ this.context.t('cancel'),
+ ]),
+
+ h('button.btn-primary--lg.new-account-create-form__button', {
+ onClick: () => {
+ createAccount(newAccountName || defaultAccountName)
+ .then(() => history.push(DEFAULT_ROUTE))
+ },
+ }, [
+ this.context.t('create'),
+ ]),
+
+ ]),
+
+ ])
+ }
+}
+
+NewAccountCreateForm.propTypes = {
+ hideModal: PropTypes.func,
+ showImportPage: 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.saveAccountLabel(newAccountAddress, newAccountName))
+ }
+ })
+ },
+ showImportPage: () => dispatch(actions.showImportPage()),
+ }
+}
+
+NewAccountCreateForm.contextTypes = {
+ t: PropTypes.func,
+}
+
+module.exports = connect(mapStateToProps, mapDispatchToProps)(NewAccountCreateForm)
+
diff --git a/ui/app/components/pages/home.js b/ui/app/components/pages/home.js
new file mode 100644
index 000000000..7857a2a99
--- /dev/null
+++ b/ui/app/components/pages/home.js
@@ -0,0 +1,332 @@
+const { Component } = require('react')
+const PropTypes = require('prop-types')
+const connect = require('../../metamask-connect')
+const { Redirect, withRouter } = require('react-router-dom')
+const { compose } = require('recompose')
+const h = require('react-hyperscript')
+const actions = require('../../actions')
+
+// init
+const NewKeyChainScreen = require('../../new-keychain')
+// mascara
+const MascaraBuyEtherScreen = require('../../../../mascara/src/app/first-time/buy-ether-screen').default
+
+// accounts
+const MainContainer = require('../../main-container')
+
+// other views
+const BuyView = require('../../components/buy-button-subview')
+const QrView = require('../../components/qr-code')
+
+// Routes
+const {
+ REVEAL_SEED_ROUTE,
+ RESTORE_VAULT_ROUTE,
+ CONFIRM_TRANSACTION_ROUTE,
+ NOTICE_ROUTE,
+} = require('../../routes')
+
+class Home extends Component {
+ componentDidMount () {
+ const {
+ history,
+ unapprovedTxs = {},
+ unapprovedMsgCount = 0,
+ unapprovedPersonalMsgCount = 0,
+ unapprovedTypedMessagesCount = 0,
+ } = this.props
+
+ // unapprovedTxs and unapproved messages
+ if (Object.keys(unapprovedTxs).length ||
+ unapprovedTypedMessagesCount + unapprovedMsgCount + unapprovedPersonalMsgCount > 0) {
+ history.push(CONFIRM_TRANSACTION_ROUTE)
+ }
+ }
+
+ render () {
+ log.debug('rendering primary')
+ const {
+ noActiveNotices,
+ lostAccounts,
+ forgottenPassword,
+ currentView,
+ activeAddress,
+ seedWords,
+ } = this.props
+
+ // notices
+ if (!noActiveNotices || (lostAccounts && lostAccounts.length > 0)) {
+ return h(Redirect, {
+ to: {
+ pathname: NOTICE_ROUTE,
+ },
+ })
+ }
+
+ // seed words
+ if (seedWords) {
+ log.debug('rendering seed words')
+ return h(Redirect, {
+ to: {
+ pathname: REVEAL_SEED_ROUTE,
+ },
+ })
+ }
+
+ if (forgottenPassword) {
+ log.debug('rendering restore vault screen')
+ return h(Redirect, {
+ to: {
+ pathname: RESTORE_VAULT_ROUTE,
+ },
+ })
+ }
+
+ // if (!props.noActiveNotices) {
+ // log.debug('rendering notice screen for unread notices.')
+ // return h(NoticeScreen, {
+ // notice: props.lastUnreadNotice,
+ // key: 'NoticeScreen',
+ // onConfirm: () => props.dispatch(actions.markNoticeRead(props.lastUnreadNotice)),
+ // })
+ // } else if (props.lostAccounts && props.lostAccounts.length > 0) {
+ // log.debug('rendering notice screen for lost accounts view.')
+ // return h(NoticeScreen, {
+ // notice: generateLostAccountsNotice(props.lostAccounts),
+ // key: 'LostAccountsNotice',
+ // onConfirm: () => props.dispatch(actions.markAccountsFound()),
+ // })
+ // }
+
+ // if (props.seedWords) {
+ // log.debug('rendering seed words')
+ // return h(HDCreateVaultComplete, {key: 'HDCreateVaultComplete'})
+ // }
+
+ // show initialize screen
+ // if (!isInitialized || forgottenPassword) {
+ // // show current view
+ // log.debug('rendering an initialize screen')
+ // // switch (props.currentView.name) {
+
+ // // case 'restoreVault':
+ // // log.debug('rendering restore vault screen')
+ // // return h(HDRestoreVaultScreen, {key: 'HDRestoreVaultScreen'})
+
+ // // default:
+ // // log.debug('rendering menu screen')
+ // // return h(InitializeScreen, {key: 'menuScreenInit'})
+ // // }
+ // }
+
+ // // show unlock screen
+ // if (!props.isUnlocked) {
+ // return h(MainContainer, {
+ // currentViewName: props.currentView.name,
+ // isUnlocked: props.isUnlocked,
+ // })
+ // }
+
+ // show current view
+ switch (currentView.name) {
+
+ case 'accountDetail':
+ log.debug('rendering main container')
+ return h(MainContainer, {key: 'account-detail'})
+
+ // case 'sendTransaction':
+ // log.debug('rendering send tx screen')
+
+ // // Going to leave this here until we are ready to delete SendTransactionScreen v1
+ // // const SendComponentToRender = checkFeatureToggle('send-v2')
+ // // ? SendTransactionScreen2
+ // // : SendTransactionScreen
+
+ // return h(SendTransactionScreen2, {key: 'send-transaction'})
+
+ // case 'sendToken':
+ // log.debug('rendering send token screen')
+
+ // // Going to leave this here until we are ready to delete SendTransactionScreen v1
+ // // const SendTokenComponentToRender = checkFeatureToggle('send-v2')
+ // // ? SendTransactionScreen2
+ // // : SendTokenScreen
+
+ // return h(SendTransactionScreen2, {key: 'sendToken'})
+
+ case 'newKeychain':
+ log.debug('rendering new keychain screen')
+ return h(NewKeyChainScreen, {key: 'new-keychain'})
+
+ // case 'confTx':
+ // log.debug('rendering confirm tx screen')
+ // return h(Redirect, {
+ // to: {
+ // pathname: CONFIRM_TRANSACTION_ROUTE,
+ // },
+ // })
+ // return h(ConfirmTxScreen, {key: 'confirm-tx'})
+
+ // case 'add-token':
+ // log.debug('rendering add-token screen from unlock screen.')
+ // return h(AddTokenScreen, {key: 'add-token'})
+
+ // case 'config':
+ // log.debug('rendering config screen')
+ // return h(Settings, {key: 'config'})
+
+ // case 'import-menu':
+ // log.debug('rendering import screen')
+ // return h(Import, {key: 'import-menu'})
+
+ // case 'reveal-seed-conf':
+ // log.debug('rendering reveal seed confirmation screen')
+ // return h(RevealSeedConfirmation, {key: 'reveal-seed-conf'})
+
+ // case 'info':
+ // log.debug('rendering info screen')
+ // return h(Settings, {key: 'info', tab: 'info'})
+
+ case 'buyEth':
+ log.debug('rendering buy ether screen')
+ return h(BuyView, {key: 'buyEthView'})
+
+ case 'onboardingBuyEth':
+ log.debug('rendering onboarding buy ether screen')
+ return h(MascaraBuyEtherScreen, {key: 'buyEthView'})
+
+ case 'qr':
+ log.debug('rendering show qr screen')
+ return h('div', {
+ style: {
+ position: 'absolute',
+ height: '100%',
+ top: '0px',
+ left: '0px',
+ },
+ }, [
+ h('i.fa.fa-arrow-left.fa-lg.cursor-pointer.color-orange', {
+ onClick: () => this.props.dispatch(actions.backToAccountDetail(activeAddress)),
+ style: {
+ marginLeft: '10px',
+ marginTop: '50px',
+ },
+ }),
+ h('div', {
+ style: {
+ position: 'absolute',
+ left: '44px',
+ width: '285px',
+ },
+ }, [
+ h(QrView, {key: 'qr'}),
+ ]),
+ ])
+
+ default:
+ log.debug('rendering default, account detail screen')
+ return h(MainContainer, {key: 'account-detail'})
+ }
+ }
+}
+
+Home.propTypes = {
+ currentCurrency: PropTypes.string,
+ isLoading: PropTypes.bool,
+ loadingMessage: PropTypes.string,
+ network: PropTypes.string,
+ provider: PropTypes.object,
+ frequentRpcList: PropTypes.array,
+ currentView: PropTypes.object,
+ sidebarOpen: PropTypes.bool,
+ isMascara: PropTypes.bool,
+ isOnboarding: PropTypes.bool,
+ isUnlocked: PropTypes.bool,
+ networkDropdownOpen: PropTypes.bool,
+ history: PropTypes.object,
+ dispatch: 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,
+ unapprovedMsgCount: PropTypes.number,
+ unapprovedPersonalMsgCount: PropTypes.number,
+ unapprovedTypedMessagesCount: PropTypes.number,
+ welcomeScreenSeen: PropTypes.bool,
+ isPopup: PropTypes.bool,
+ isMouseUser: PropTypes.bool,
+ t: PropTypes.func,
+}
+
+function mapStateToProps (state) {
+ const { appState, metamask } = state
+ const {
+ networkDropdownOpen,
+ sidebarOpen,
+ isLoading,
+ loadingMessage,
+ } = appState
+
+ const {
+ accounts,
+ address,
+ isInitialized,
+ noActiveNotices,
+ seedWords,
+ unapprovedTxs,
+ lastUnreadNotice,
+ lostAccounts,
+ unapprovedMsgCount,
+ unapprovedPersonalMsgCount,
+ unapprovedTypedMessagesCount,
+ } = metamask
+ const selected = address || Object.keys(accounts)[0]
+
+ return {
+ // state from plugin
+ networkDropdownOpen,
+ sidebarOpen,
+ isLoading,
+ loadingMessage,
+ noActiveNotices,
+ isInitialized,
+ isUnlocked: state.metamask.isUnlocked,
+ selectedAddress: state.metamask.selectedAddress,
+ currentView: state.appState.currentView,
+ activeAddress: state.appState.activeAddress,
+ transForward: state.appState.transForward,
+ isMascara: state.metamask.isMascara,
+ isOnboarding: Boolean(!noActiveNotices || seedWords || !isInitialized),
+ isPopup: state.metamask.isPopup,
+ seedWords: state.metamask.seedWords,
+ unapprovedTxs,
+ unapprovedMsgs: state.metamask.unapprovedMsgs,
+ unapprovedMsgCount,
+ unapprovedPersonalMsgCount,
+ unapprovedTypedMessagesCount,
+ menuOpen: state.appState.menuOpen,
+ network: state.metamask.network,
+ provider: state.metamask.provider,
+ forgottenPassword: state.appState.forgottenPassword,
+ lastUnreadNotice,
+ lostAccounts,
+ frequentRpcList: state.metamask.frequentRpcList || [],
+ currentCurrency: state.metamask.currentCurrency,
+ isMouseUser: state.appState.isMouseUser,
+ isRevealingSeedWords: state.metamask.isRevealingSeedWords,
+ Qr: state.appState.Qr,
+ welcomeScreenSeen: state.metamask.welcomeScreenSeen,
+
+ // state needed to get account dropdown temporarily rendering from app bar
+ selected,
+ }
+}
+
+module.exports = compose(
+ withRouter,
+ connect(mapStateToProps)
+)(Home)
diff --git a/ui/app/components/pages/initialized.js b/ui/app/components/pages/initialized.js
new file mode 100644
index 000000000..3adf67b28
--- /dev/null
+++ b/ui/app/components/pages/initialized.js
@@ -0,0 +1,25 @@
+const { connect } = require('react-redux')
+const PropTypes = require('prop-types')
+const { Redirect } = require('react-router-dom')
+const h = require('react-hyperscript')
+const { INITIALIZE_ROUTE } = require('../../routes')
+const MetamaskRoute = require('./metamask-route')
+
+const Initialized = props => {
+ return props.isInitialized
+ ? h(MetamaskRoute, { ...props })
+ : h(Redirect, { to: { pathname: INITIALIZE_ROUTE } })
+}
+
+Initialized.propTypes = {
+ isInitialized: PropTypes.bool,
+}
+
+const mapStateToProps = state => {
+ const { metamask: { isInitialized } } = state
+ return {
+ isInitialized,
+ }
+}
+
+module.exports = connect(mapStateToProps)(Initialized)
diff --git a/ui/app/components/pages/keychains/restore-vault.js b/ui/app/components/pages/keychains/restore-vault.js
new file mode 100644
index 000000000..d57894e00
--- /dev/null
+++ b/ui/app/components/pages/keychains/restore-vault.js
@@ -0,0 +1,177 @@
+const { withRouter } = require('react-router-dom')
+const PropTypes = require('prop-types')
+const { compose } = require('recompose')
+const PersistentForm = require('../../../../lib/persistent-form')
+const connect = require('../../../metamask-connect')
+const h = require('react-hyperscript')
+const { createNewVaultAndRestore, unMarkPasswordForgotten } = require('../../../actions')
+const { DEFAULT_ROUTE } = require('../../../routes')
+
+class RestoreVaultPage extends PersistentForm {
+ constructor (props) {
+ super(props)
+
+ this.state = {
+ error: null,
+ }
+ }
+
+ createOnEnter (event) {
+ if (event.key === 'Enter') {
+ this.createNewVaultAndRestore()
+ }
+ }
+
+ cancel () {
+ this.props.unMarkPasswordForgotten()
+ .then(this.props.history.push(DEFAULT_ROUTE))
+ }
+
+ createNewVaultAndRestore () {
+ this.setState({ error: null })
+
+ // check password
+ var passwordBox = document.getElementById('password-box')
+ var password = passwordBox.value
+ var passwordConfirmBox = document.getElementById('password-box-confirm')
+ var passwordConfirm = passwordConfirmBox.value
+
+ if (password.length < 8) {
+ this.setState({ error: 'Password not long enough' })
+ return
+ }
+
+ if (password !== passwordConfirm) {
+ this.setState({ error: 'Passwords don\'t match' })
+ return
+ }
+
+ // check seed
+ var seedBox = document.querySelector('textarea.twelve-word-phrase')
+ var seed = seedBox.value.trim()
+ if (seed.split(' ').length !== 12) {
+ this.setState({ error: 'Seed phrases are 12 words long' })
+ return
+ }
+
+ // submit
+ this.props.createNewVaultAndRestore(password, seed)
+ .then(() => this.props.history.push(DEFAULT_ROUTE))
+ .catch(({ message }) => {
+ this.setState({ error: message })
+ log.error(message)
+ })
+ }
+
+ render () {
+ const { error } = this.state
+ this.persistentFormParentId = 'restore-vault-form'
+
+ return (
+ h('.initialize-screen.flex-column.flex-center.flex-grow', [
+
+ h('h3.flex-center.text-transform-uppercase', {
+ style: {
+ background: '#EBEBEB',
+ color: '#AEAEAE',
+ marginBottom: 24,
+ width: '100%',
+ fontSize: '20px',
+ padding: 6,
+ },
+ }, [
+ this.props.t('restoreVault'),
+ ]),
+
+ // wallet seed entry
+ h('h3', 'Wallet Seed'),
+ h('textarea.twelve-word-phrase.letter-spacey', {
+ dataset: {
+ persistentFormId: 'wallet-seed',
+ },
+ placeholder: this.props.t('secretPhrase'),
+ }),
+
+ // password
+ h('input.large-input.letter-spacey', {
+ type: 'password',
+ id: 'password-box',
+ placeholder: this.props.t('newPassword8Chars'),
+ dataset: {
+ persistentFormId: 'password',
+ },
+ style: {
+ width: 260,
+ marginTop: 12,
+ },
+ }),
+
+ // confirm password
+ h('input.large-input.letter-spacey', {
+ type: 'password',
+ id: 'password-box-confirm',
+ placeholder: this.props.t('confirmPassword'),
+ onKeyPress: this.createOnEnter.bind(this),
+ dataset: {
+ persistentFormId: 'password-confirmation',
+ },
+ style: {
+ width: 260,
+ marginTop: 16,
+ },
+ }),
+
+ error && (
+ h('span.error.in-progress-notification', error)
+ ),
+
+ // submit
+ h('.flex-row.flex-space-between', {
+ style: {
+ marginTop: 30,
+ width: '50%',
+ },
+ }, [
+
+ // cancel
+ h('button.primary', {
+ onClick: () => this.cancel(),
+ }, this.props.t('cancel')),
+
+ // submit
+ h('button.primary', {
+ onClick: this.createNewVaultAndRestore.bind(this),
+ }, this.props.t('ok')),
+
+ ]),
+ ])
+ )
+ }
+}
+
+RestoreVaultPage.propTypes = {
+ history: PropTypes.object,
+}
+
+const mapStateToProps = state => {
+ const { appState: { warning, forgottenPassword } } = state
+
+ return {
+ warning,
+ forgottenPassword,
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ createNewVaultAndRestore: (password, seed) => {
+ return dispatch(createNewVaultAndRestore(password, seed))
+ },
+ unMarkPasswordForgotten: () => dispatch(unMarkPasswordForgotten()),
+ }
+}
+
+module.exports = compose(
+ withRouter,
+ connect(mapStateToProps, mapDispatchToProps)
+)(RestoreVaultPage)
diff --git a/ui/app/components/pages/keychains/reveal-seed.js b/ui/app/components/pages/keychains/reveal-seed.js
new file mode 100644
index 000000000..247f3c8e2
--- /dev/null
+++ b/ui/app/components/pages/keychains/reveal-seed.js
@@ -0,0 +1,195 @@
+const { Component } = require('react')
+const { connect } = require('react-redux')
+const PropTypes = require('prop-types')
+const h = require('react-hyperscript')
+const { exportAsFile } = require('../../../util')
+const { requestRevealSeed, confirmSeedWords } = require('../../../actions')
+const { DEFAULT_ROUTE } = require('../../../routes')
+
+class RevealSeedPage extends Component {
+ componentDidMount () {
+ const passwordBox = document.getElementById('password-box')
+ if (passwordBox) {
+ passwordBox.focus()
+ }
+ }
+
+ checkConfirmation (event) {
+ if (event.key === 'Enter') {
+ event.preventDefault()
+ this.revealSeedWords()
+ }
+ }
+
+ revealSeedWords () {
+ const password = document.getElementById('password-box').value
+ this.props.requestRevealSeed(password)
+ }
+
+ renderSeed () {
+ const { seedWords, confirmSeedWords, history } = this.props
+
+ return (
+ h('.initialize-screen.flex-column.flex-center.flex-grow', [
+
+ h('h3.flex-center.text-transform-uppercase', {
+ style: {
+ background: '#EBEBEB',
+ color: '#AEAEAE',
+ marginTop: 36,
+ marginBottom: 8,
+ width: '100%',
+ fontSize: '20px',
+ padding: 6,
+ },
+ }, [
+ 'Vault Created',
+ ]),
+
+ h('div', {
+ style: {
+ fontSize: '1em',
+ marginTop: '10px',
+ textAlign: 'center',
+ },
+ }, [
+ h('span.error', 'These 12 words are the only way to restore your MetaMask accounts.\nSave them somewhere safe and secret.'),
+ ]),
+
+ h('textarea.twelve-word-phrase', {
+ readOnly: true,
+ value: seedWords,
+ }),
+
+ h('button.primary', {
+ onClick: () => confirmSeedWords().then(() => history.push(DEFAULT_ROUTE)),
+ style: {
+ margin: '24px',
+ fontSize: '0.9em',
+ marginBottom: '10px',
+ },
+ }, 'I\'ve copied it somewhere safe'),
+
+ h('button.primary', {
+ onClick: () => exportAsFile(`MetaMask Seed Words`, seedWords),
+ style: {
+ margin: '10px',
+ fontSize: '0.9em',
+ },
+ }, 'Save Seed Words As File'),
+ ])
+ )
+ }
+
+ renderConfirmation () {
+ const { history, warning, inProgress } = this.props
+
+ return (
+ h('.initialize-screen.flex-column.flex-center.flex-grow', {
+ style: { maxWidth: '420px' },
+ }, [
+
+ h('h3.flex-center.text-transform-uppercase', {
+ style: {
+ background: '#EBEBEB',
+ color: '#AEAEAE',
+ marginBottom: 24,
+ width: '100%',
+ fontSize: '20px',
+ padding: 6,
+ },
+ }, [
+ 'Reveal Seed Words',
+ ]),
+
+ h('.div', {
+ style: {
+ display: 'flex',
+ flexDirection: 'column',
+ padding: '20px',
+ justifyContent: 'center',
+ },
+ }, [
+
+ h('h4', 'Do not recover your seed words in a public place! These words can be used to steal all your accounts.'),
+
+ // confirmation
+ h('input.large-input.letter-spacey', {
+ type: 'password',
+ id: 'password-box',
+ placeholder: 'Enter your password to confirm',
+ onKeyPress: this.checkConfirmation.bind(this),
+ style: {
+ width: 260,
+ marginTop: '12px',
+ },
+ }),
+
+ h('.flex-row.flex-start', {
+ style: {
+ marginTop: 30,
+ width: '50%',
+ },
+ }, [
+ // cancel
+ h('button.primary', {
+ onClick: () => history.push(DEFAULT_ROUTE),
+ }, 'CANCEL'),
+
+ // submit
+ h('button.primary', {
+ style: { marginLeft: '10px' },
+ onClick: this.revealSeedWords.bind(this),
+ }, 'OK'),
+
+ ]),
+
+ warning && (
+ h('span.error', {
+ style: {
+ margin: '20px',
+ },
+ }, warning.split('-'))
+ ),
+
+ inProgress && (
+ h('span.in-progress-notification', 'Generating Seed...')
+ ),
+ ]),
+ ])
+ )
+ }
+
+ render () {
+ return this.props.seedWords
+ ? this.renderSeed()
+ : this.renderConfirmation()
+ }
+}
+
+RevealSeedPage.propTypes = {
+ requestRevealSeed: PropTypes.func,
+ confirmSeedWords: PropTypes.func,
+ seedWords: PropTypes.string,
+ inProgress: PropTypes.bool,
+ history: PropTypes.object,
+ warning: PropTypes.string,
+}
+
+const mapStateToProps = state => {
+ const { appState: { warning }, metamask: { seedWords } } = state
+
+ return {
+ warning,
+ seedWords,
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ requestRevealSeed: password => dispatch(requestRevealSeed(password)),
+ confirmSeedWords: () => dispatch(confirmSeedWords()),
+ }
+}
+
+module.exports = connect(mapStateToProps, mapDispatchToProps)(RevealSeedPage)
diff --git a/ui/app/components/pages/metamask-route.js b/ui/app/components/pages/metamask-route.js
new file mode 100644
index 000000000..23c5b5199
--- /dev/null
+++ b/ui/app/components/pages/metamask-route.js
@@ -0,0 +1,28 @@
+const { connect } = require('react-redux')
+const PropTypes = require('prop-types')
+const { Route } = require('react-router-dom')
+const h = require('react-hyperscript')
+
+const MetamaskRoute = ({ component, mascaraComponent, isMascara, ...props }) => {
+ return (
+ h(Route, {
+ ...props,
+ component: isMascara && mascaraComponent ? mascaraComponent : component,
+ })
+ )
+}
+
+MetamaskRoute.propTypes = {
+ component: PropTypes.func,
+ mascaraComponent: PropTypes.func,
+ isMascara: PropTypes.bool,
+}
+
+const mapStateToProps = state => {
+ const { metamask: { isMascara } } = state
+ return {
+ isMascara,
+ }
+}
+
+module.exports = connect(mapStateToProps)(MetamaskRoute)
diff --git a/ui/app/components/pages/notice.js b/ui/app/components/pages/notice.js
new file mode 100644
index 000000000..2329a9147
--- /dev/null
+++ b/ui/app/components/pages/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('../../actions')
+const { DEFAULT_ROUTE } = require('../../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, lastUnreadNotice, lostAccounts } = metamask
+
+ return {
+ noActiveNotices,
+ lastUnreadNotice,
+ lostAccounts,
+ }
+}
+
+Notice.propTypes = {
+ notice: PropTypes.object,
+ onConfirm: PropTypes.func,
+ history: PropTypes.object,
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ markNoticeRead: lastUnreadNotice => dispatch(actions.markNoticeRead(lastUnreadNotice)),
+ markAccountsFound: () => dispatch(actions.markAccountsFound()),
+ }
+}
+
+const mergeProps = (stateProps, dispatchProps, ownProps) => {
+ const { noActiveNotices, lastUnreadNotice, lostAccounts } = stateProps
+ const { markNoticeRead, markAccountsFound } = dispatchProps
+
+ let notice
+ let onConfirm
+
+ if (!noActiveNotices) {
+ notice = lastUnreadNotice
+ onConfirm = () => markNoticeRead(lastUnreadNotice)
+ } 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/components/pages/settings/index.js b/ui/app/components/pages/settings/index.js
new file mode 100644
index 000000000..384ae4b41
--- /dev/null
+++ b/ui/app/components/pages/settings/index.js
@@ -0,0 +1,59 @@
+const { Component } = require('react')
+const { Switch, Route, matchPath } = require('react-router-dom')
+const PropTypes = require('prop-types')
+const h = require('react-hyperscript')
+const TabBar = require('../../tab-bar')
+const Settings = require('./settings')
+const Info = require('./info')
+const { DEFAULT_ROUTE, SETTINGS_ROUTE, INFO_ROUTE } = require('../../../routes')
+
+class Config extends Component {
+ renderTabs () {
+ const { history, location } = this.props
+
+ return h('div.settings__tabs', [
+ h(TabBar, {
+ tabs: [
+ { content: 'Settings', key: SETTINGS_ROUTE },
+ { content: 'Info', key: INFO_ROUTE },
+ ],
+ isActive: key => matchPath(location.pathname, { path: key, exact: true }),
+ onSelect: key => history.push(key),
+ }),
+ ])
+ }
+
+ render () {
+ const { history } = this.props
+
+ return (
+ h('.main-container.settings', {}, [
+ h('.settings__header', [
+ h('div.settings__close-button', {
+ onClick: () => history.push(DEFAULT_ROUTE),
+ }),
+ this.renderTabs(),
+ ]),
+ h(Switch, [
+ h(Route, {
+ exact: true,
+ path: INFO_ROUTE,
+ component: Info,
+ }),
+ h(Route, {
+ exact: true,
+ path: SETTINGS_ROUTE,
+ component: Settings,
+ }),
+ ]),
+ ])
+ )
+ }
+}
+
+Config.propTypes = {
+ location: PropTypes.object,
+ history: PropTypes.object,
+}
+
+module.exports = Config
diff --git a/ui/app/components/pages/settings/info.js b/ui/app/components/pages/settings/info.js
new file mode 100644
index 000000000..eb9be66e6
--- /dev/null
+++ b/ui/app/components/pages/settings/info.js
@@ -0,0 +1,112 @@
+const { Component } = require('react')
+const PropTypes = require('prop-types')
+const h = require('react-hyperscript')
+
+class Info extends Component {
+ renderLogo () {
+ return (
+ h('div.settings__info-logo-wrapper', [
+ h('img.settings__info-logo', { src: 'images/info-logo.png' }),
+ ])
+ )
+ }
+
+ renderInfoLinks () {
+ return (
+ h('div.settings__content-item.settings__content-item--without-height', [
+ h('div.settings__info-link-header', this.context.t('links')),
+ h('div.settings__info-link-item', [
+ h('a', {
+ href: 'https://metamask.io/privacy.html',
+ target: '_blank',
+ }, [
+ h('span.settings__info-link', this.context.t('privacyMsg')),
+ ]),
+ ]),
+ h('div.settings__info-link-item', [
+ h('a', {
+ href: 'https://metamask.io/terms.html',
+ target: '_blank',
+ }, [
+ h('span.settings__info-link', this.context.t('terms')),
+ ]),
+ ]),
+ h('div.settings__info-link-item', [
+ h('a', {
+ href: 'https://metamask.io/attributions.html',
+ target: '_blank',
+ }, [
+ h('span.settings__info-link', this.context.t('attributions')),
+ ]),
+ ]),
+ h('hr.settings__info-separator'),
+ h('div.settings__info-link-item', [
+ h('a', {
+ href: 'https://support.metamask.io',
+ target: '_blank',
+ }, [
+ h('span.settings__info-link', this.context.t('supportCenter')),
+ ]),
+ ]),
+ h('div.settings__info-link-item', [
+ h('a', {
+ href: 'https://metamask.io/',
+ target: '_blank',
+ }, [
+ h('span.settings__info-link', this.context.t('visitWebSite')),
+ ]),
+ ]),
+ h('div.settings__info-link-item', [
+ h('a', {
+ target: '_blank',
+ href: 'mailto:help@metamask.io?subject=Feedback',
+ }, [
+ h('span.settings__info-link', this.context.t('emailUs')),
+ ]),
+ ]),
+ ])
+ )
+ }
+
+ render () {
+ return (
+ h('div.settings__content', [
+ h('div.settings__content-row', [
+ h('div.settings__content-item.settings__content-item--without-height', [
+ this.renderLogo(),
+ h('div.settings__info-item', [
+ h('div.settings__info-version-header', 'MetaMask Version'),
+ h('div.settings__info-version-number', '4.0.0'),
+ ]),
+ h('div.settings__info-item', [
+ h(
+ 'div.settings__info-about',
+ this.context.t('builtInCalifornia')
+ ),
+ ]),
+ ]),
+ this.renderInfoLinks(),
+ ]),
+ ])
+ )
+ }
+}
+
+Info.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,
+ t: PropTypes.func,
+}
+
+Info.contextTypes = {
+ t: PropTypes.func,
+}
+
+module.exports = Info
diff --git a/ui/app/components/pages/settings/settings.js b/ui/app/components/pages/settings/settings.js
new file mode 100644
index 000000000..05a7379fb
--- /dev/null
+++ b/ui/app/components/pages/settings/settings.js
@@ -0,0 +1,367 @@
+const { Component } = require('react')
+const { withRouter } = require('react-router-dom')
+const { compose } = require('recompose')
+const PropTypes = require('prop-types')
+const h = require('react-hyperscript')
+const connect = require('react-redux').connect
+const actions = require('../../../actions')
+const infuraCurrencies = require('../../../infura-conversion.json')
+const validUrl = require('valid-url')
+const { exportAsFile } = require('../../../util')
+const SimpleDropdown = require('../../dropdowns/simple-dropdown')
+const ToggleButton = require('react-toggle-button')
+const { REVEAL_SEED_ROUTE } = require('../../../routes')
+const locales = require('../../../../../app/_locales/index.json')
+const { OLD_UI_NETWORK_TYPE } = require('../../../../../app/scripts/config').enums
+
+const getInfuraCurrencyOptions = () => {
+ const sortedCurrencies = infuraCurrencies.objects.sort((a, b) => {
+ return a.quote.name.toLocaleLowerCase().localeCompare(b.quote.name.toLocaleLowerCase())
+ })
+
+ return sortedCurrencies.map(({ quote: { code, name } }) => {
+ return {
+ displayValue: `${code.toUpperCase()} - ${name}`,
+ key: code,
+ value: code,
+ }
+ })
+}
+
+const getLocaleOptions = () => {
+ return locales.map((locale) => {
+ return {
+ displayValue: `${locale.name}`,
+ key: locale.code,
+ value: locale.code,
+ }
+ })
+}
+
+class Settings extends Component {
+ constructor (props) {
+ super(props)
+
+ this.state = {
+ newRpc: '',
+ }
+ }
+
+ renderBlockieOptIn () {
+ const { metamask: { useBlockie }, setUseBlockie } = this.props
+
+ return h('div.settings__content-row', [
+ h('div.settings__content-item', [
+ h('span', this.context.t('blockiesIdenticon')),
+ ]),
+ h('div.settings__content-item', [
+ h('div.settings__content-item-col', [
+ h(ToggleButton, {
+ value: useBlockie,
+ onToggle: (value) => setUseBlockie(!value),
+ activeLabel: '',
+ inactiveLabel: '',
+ }),
+ ]),
+ ]),
+ ])
+ }
+
+ renderCurrentConversion () {
+ const { metamask: { currentCurrency, conversionDate }, setCurrentCurrency } = this.props
+
+ return h('div.settings__content-row', [
+ h('div.settings__content-item', [
+ h('span', this.context.t('currentConversion')),
+ h('span.settings__content-description', `Updated ${Date(conversionDate)}`),
+ ]),
+ h('div.settings__content-item', [
+ h('div.settings__content-item-col', [
+ h(SimpleDropdown, {
+ placeholder: this.context.t('selectCurrency'),
+ options: getInfuraCurrencyOptions(),
+ selectedOption: currentCurrency,
+ onSelect: newCurrency => setCurrentCurrency(newCurrency),
+ }),
+ ]),
+ ]),
+ ])
+ }
+
+ renderCurrentLocale () {
+ const { updateCurrentLocale, currentLocale } = this.props
+ const currentLocaleMeta = locales.find(locale => locale.code === currentLocale)
+ const currentLocaleName = currentLocaleMeta ? currentLocaleMeta.name : ''
+
+ return h('div.settings__content-row', [
+ h('div.settings__content-item', [
+ h('span', 'Current Language'),
+ h('span.settings__content-description', `${currentLocaleName}`),
+ ]),
+ h('div.settings__content-item', [
+ h('div.settings__content-item-col', [
+ h(SimpleDropdown, {
+ placeholder: 'Select Locale',
+ options: getLocaleOptions(),
+ selectedOption: currentLocale,
+ onSelect: async (newLocale) => {
+ updateCurrentLocale(newLocale)
+ },
+ }),
+ ]),
+ ]),
+ ])
+ }
+
+ renderCurrentProvider () {
+ const { metamask: { provider = {} } } = this.props
+ let title, value, color
+
+ switch (provider.type) {
+
+ case 'mainnet':
+ title = this.context.t('currentNetwork')
+ value = this.context.t('mainnet')
+ color = '#038789'
+ break
+
+ case 'ropsten':
+ title = this.context.t('currentNetwork')
+ value = this.context.t('ropsten')
+ color = '#e91550'
+ break
+
+ case 'kovan':
+ title = this.context.t('currentNetwork')
+ value = this.context.t('kovan')
+ color = '#690496'
+ break
+
+ case 'rinkeby':
+ title = this.context.t('currentNetwork')
+ value = this.context.t('rinkeby')
+ color = '#ebb33f'
+ break
+
+ default:
+ title = this.context.t('currentRpc')
+ value = provider.rpcTarget
+ }
+
+ return h('div.settings__content-row', [
+ h('div.settings__content-item', title),
+ h('div.settings__content-item', [
+ h('div.settings__content-item-col', [
+ h('div.settings__provider-wrapper', [
+ h('div.settings__provider-icon', { style: { background: color } }),
+ h('div', value),
+ ]),
+ ]),
+ ]),
+ ])
+ }
+
+ renderNewRpcUrl () {
+ return (
+ h('div.settings__content-row', [
+ h('div.settings__content-item', [
+ h('span', this.context.t('newRPC')),
+ ]),
+ h('div.settings__content-item', [
+ h('div.settings__content-item-col', [
+ h('input.settings__input', {
+ placeholder: this.context.t('newRPC'),
+ onChange: event => this.setState({ newRpc: event.target.value }),
+ onKeyPress: event => {
+ if (event.key === 'Enter') {
+ this.validateRpc(this.state.newRpc)
+ }
+ },
+ }),
+ h('div.settings__rpc-save-button', {
+ onClick: event => {
+ event.preventDefault()
+ this.validateRpc(this.state.newRpc)
+ },
+ }, this.context.t('save')),
+ ]),
+ ]),
+ ])
+ )
+ }
+
+ validateRpc (newRpc) {
+ const { setRpcTarget, displayWarning } = this.props
+
+ if (validUrl.isWebUri(newRpc)) {
+ setRpcTarget(newRpc)
+ } else {
+ const appendedRpc = `http://${newRpc}`
+
+ if (validUrl.isWebUri(appendedRpc)) {
+ displayWarning(this.context.t('uriErrorMsg'))
+ } else {
+ displayWarning(this.context.t('invalidRPC'))
+ }
+ }
+ }
+
+ renderStateLogs () {
+ return (
+ h('div.settings__content-row', [
+ h('div.settings__content-item', [
+ h('div', this.context.t('stateLogs')),
+ h(
+ 'div.settings__content-description',
+ this.context.t('stateLogsDescription')
+ ),
+ ]),
+ h('div.settings__content-item', [
+ h('div.settings__content-item-col', [
+ h('button.btn-primary--lg.settings__button', {
+ onClick (event) {
+ window.logStateString((err, result) => {
+ if (err) {
+ this.state.dispatch(actions.displayWarning(this.context.t('stateLogError')))
+ } else {
+ exportAsFile('MetaMask State Logs.json', result)
+ }
+ })
+ },
+ }, this.context.t('downloadStateLogs')),
+ ]),
+ ]),
+ ])
+ )
+ }
+
+ renderSeedWords () {
+ const { history } = this.props
+
+ return (
+ h('div.settings__content-row', [
+ h('div.settings__content-item', this.context.t('revealSeedWords')),
+ h('div.settings__content-item', [
+ h('div.settings__content-item-col', [
+ h('button.btn-primary--lg.settings__button--red', {
+ onClick: event => {
+ event.preventDefault()
+ history.push(REVEAL_SEED_ROUTE)
+ },
+ }, this.context.t('revealSeedWords')),
+ ]),
+ ]),
+ ])
+ )
+ }
+
+ renderOldUI () {
+ const { setFeatureFlagToBeta } = this.props
+
+ return (
+ h('div.settings__content-row', [
+ h('div.settings__content-item', this.context.t('useOldUI')),
+ h('div.settings__content-item', [
+ h('div.settings__content-item-col', [
+ h('button.btn-primary--lg.settings__button--orange', {
+ onClick (event) {
+ event.preventDefault()
+ setFeatureFlagToBeta()
+ },
+ }, this.context.t('useOldUI')),
+ ]),
+ ]),
+ ])
+ )
+ }
+
+ renderResetAccount () {
+ const { showResetAccountConfirmationModal } = this.props
+
+ return h('div.settings__content-row', [
+ h('div.settings__content-item', this.context.t('resetAccount')),
+ h('div.settings__content-item', [
+ h('div.settings__content-item-col', [
+ h('button.btn-primary--lg.settings__button--orange', {
+ onClick (event) {
+ event.preventDefault()
+ showResetAccountConfirmationModal()
+ },
+ }, this.context.t('resetAccount')),
+ ]),
+ ]),
+ ])
+ }
+
+ render () {
+ const { warning, isMascara } = this.props
+
+ return (
+ h('div.settings__content', [
+ warning && h('div.settings__error', warning),
+ this.renderCurrentConversion(),
+ this.renderCurrentLocale(),
+ // this.renderCurrentProvider(),
+ this.renderNewRpcUrl(),
+ this.renderStateLogs(),
+ this.renderSeedWords(),
+ !isMascara && this.renderOldUI(),
+ this.renderResetAccount(),
+ this.renderBlockieOptIn(),
+ ])
+ )
+ }
+}
+
+Settings.propTypes = {
+ metamask: PropTypes.object,
+ setUseBlockie: PropTypes.func,
+ setCurrentCurrency: PropTypes.func,
+ setRpcTarget: PropTypes.func,
+ displayWarning: PropTypes.func,
+ revealSeedConfirmation: PropTypes.func,
+ setFeatureFlagToBeta: PropTypes.func,
+ showResetAccountConfirmationModal: PropTypes.func,
+ warning: PropTypes.string,
+ history: PropTypes.object,
+ isMascara: PropTypes.bool,
+ updateCurrentLocale: PropTypes.func,
+ currentLocale: PropTypes.string,
+ t: PropTypes.func,
+}
+
+const mapStateToProps = state => {
+ return {
+ metamask: state.metamask,
+ warning: state.appState.warning,
+ isMascara: state.metamask.isMascara,
+ currentLocale: state.metamask.currentLocale,
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ setCurrentCurrency: currency => dispatch(actions.setCurrentCurrency(currency)),
+ setRpcTarget: newRpc => dispatch(actions.setRpcTarget(newRpc)),
+ displayWarning: warning => dispatch(actions.displayWarning(warning)),
+ revealSeedConfirmation: () => dispatch(actions.revealSeedConfirmation()),
+ setUseBlockie: value => dispatch(actions.setUseBlockie(value)),
+ updateCurrentLocale: key => dispatch(actions.updateCurrentLocale(key)),
+ setFeatureFlagToBeta: () => {
+ return dispatch(actions.setFeatureFlag('betaUI', false, 'OLD_UI_NOTIFICATION_MODAL'))
+ .then(() => dispatch(actions.setNetworkEndpoints(OLD_UI_NETWORK_TYPE)))
+ },
+ showResetAccountConfirmationModal: () => {
+ return dispatch(actions.showModal({ name: 'CONFIRM_RESET_ACCOUNT' }))
+ },
+ }
+}
+
+Settings.contextTypes = {
+ t: PropTypes.func,
+}
+
+module.exports = compose(
+ withRouter,
+ connect(mapStateToProps, mapDispatchToProps)
+)(Settings)
diff --git a/ui/app/components/pages/unlock.js b/ui/app/components/pages/unlock.js
new file mode 100644
index 000000000..b3320da21
--- /dev/null
+++ b/ui/app/components/pages/unlock.js
@@ -0,0 +1,193 @@
+const { Component } = require('react')
+const PropTypes = require('prop-types')
+const connect = require('../../metamask-connect')
+const h = require('react-hyperscript')
+const { withRouter } = require('react-router-dom')
+const { compose } = require('recompose')
+const {
+ tryUnlockMetamask,
+ forgotPassword,
+ markPasswordForgotten,
+ setNetworkEndpoints,
+ setFeatureFlag,
+} = require('../../actions')
+const environmentType = require('../../../../app/scripts/lib/environment-type')
+const getCaretCoordinates = require('textarea-caret')
+const EventEmitter = require('events').EventEmitter
+const Mascot = require('../mascot')
+const { OLD_UI_NETWORK_TYPE } = require('../../../../app/scripts/config').enums
+const { DEFAULT_ROUTE, RESTORE_VAULT_ROUTE } = require('../../routes')
+
+class UnlockScreen extends Component {
+ constructor (props) {
+ super(props)
+
+ this.state = {
+ error: null,
+ }
+
+ this.animationEventEmitter = new EventEmitter()
+ }
+
+ componentWillMount () {
+ const { isUnlocked, history } = this.props
+
+ if (isUnlocked) {
+ history.push(DEFAULT_ROUTE)
+ }
+ }
+
+ componentDidMount () {
+ const passwordBox = document.getElementById('password-box')
+
+ if (passwordBox) {
+ passwordBox.focus()
+ }
+ }
+
+ tryUnlockMetamask (password) {
+ const { tryUnlockMetamask, history } = this.props
+ tryUnlockMetamask(password)
+ .then(() => history.push(DEFAULT_ROUTE))
+ .catch(({ message }) => this.setState({ error: message }))
+ }
+
+ onSubmit (event) {
+ const input = document.getElementById('password-box')
+ const password = input.value
+ this.tryUnlockMetamask(password)
+ }
+
+ onKeyPress (event) {
+ if (event.key === 'Enter') {
+ this.submitPassword(event)
+ }
+ }
+
+ submitPassword (event) {
+ var element = event.target
+ var password = element.value
+ // reset input
+ element.value = ''
+ this.tryUnlockMetamask(password)
+ }
+
+ inputChanged (event) {
+ // tell mascot to look at page action
+ var element = event.target
+ var boundingRect = element.getBoundingClientRect()
+ var coordinates = getCaretCoordinates(element, element.selectionEnd)
+ this.animationEventEmitter.emit('point', {
+ x: boundingRect.left + coordinates.left - element.scrollLeft,
+ y: boundingRect.top + coordinates.top - element.scrollTop,
+ })
+ }
+
+ render () {
+ const { error } = this.state
+ return (
+ h('.unlock-screen', [
+
+ h(Mascot, {
+ animationEventEmitter: this.animationEventEmitter,
+ }),
+
+ h('h1', {
+ style: {
+ fontSize: '1.4em',
+ textTransform: 'uppercase',
+ color: '#7F8082',
+ },
+ }, this.props.t('appName')),
+
+ h('input.large-input', {
+ type: 'password',
+ id: 'password-box',
+ placeholder: 'enter password',
+ style: {
+ background: 'white',
+ },
+ onKeyPress: this.onKeyPress.bind(this),
+ onInput: this.inputChanged.bind(this),
+ }),
+
+ h('.error', {
+ style: {
+ display: error ? 'block' : 'none',
+ padding: '0 20px',
+ textAlign: 'center',
+ },
+ }, error),
+
+ h('button.primary.cursor-pointer', {
+ onClick: this.onSubmit.bind(this),
+ style: {
+ margin: 10,
+ },
+ }, this.props.t('login')),
+
+ h('p.pointer', {
+ onClick: () => {
+ this.props.markPasswordForgotten()
+ this.props.history.push(RESTORE_VAULT_ROUTE)
+
+ if (environmentType() === 'popup') {
+ global.platform.openExtensionInBrowser()
+ }
+ },
+ style: {
+ fontSize: '0.8em',
+ color: 'rgb(247, 134, 28)',
+ textDecoration: 'underline',
+ },
+ }, this.props.t('restoreFromSeed')),
+
+ h('p.pointer', {
+ onClick: () => {
+ this.props.useOldInterface()
+ .then(() => this.props.setNetworkEndpoints(OLD_UI_NETWORK_TYPE))
+ },
+ style: {
+ fontSize: '0.8em',
+ color: '#aeaeae',
+ textDecoration: 'underline',
+ marginTop: '32px',
+ },
+ }, this.props.t('classicInterface')),
+ ])
+ )
+ }
+}
+
+UnlockScreen.propTypes = {
+ forgotPassword: PropTypes.func,
+ tryUnlockMetamask: PropTypes.func,
+ markPasswordForgotten: PropTypes.func,
+ history: PropTypes.object,
+ isUnlocked: PropTypes.bool,
+ t: PropTypes.func,
+ useOldInterface: PropTypes.func,
+ setNetworkEndpoints: PropTypes.func,
+}
+
+const mapStateToProps = state => {
+ const { metamask: { isUnlocked } } = state
+ return {
+ isUnlocked,
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ forgotPassword: () => dispatch(forgotPassword()),
+ tryUnlockMetamask: password => dispatch(tryUnlockMetamask(password)),
+ markPasswordForgotten: () => dispatch(markPasswordForgotten()),
+ useOldInterface: () => dispatch(setFeatureFlag('betaUI', false, 'OLD_UI_NOTIFICATION_MODAL')),
+ setNetworkEndpoints: type => dispatch(setNetworkEndpoints(type)),
+ }
+}
+
+module.exports = compose(
+ withRouter,
+ connect(mapStateToProps, mapDispatchToProps)
+)(UnlockScreen)