aboutsummaryrefslogtreecommitdiffstats
path: root/ui/app/components/pages
diff options
context:
space:
mode:
authorAlexander Tseung <alextsg@gmail.com>2017-11-29 12:24:35 +0800
committerAlexander Tseung <alextsg@gmail.com>2017-12-15 04:50:20 +0800
commite226b10a89d87af07c7c35ff1251a8264f3bb1b8 (patch)
tree2a1507463ac1074a39e4ebd3ae609be0a2962bc6 /ui/app/components/pages
parent339eb7d1a687f141e822c745c568063783d44f15 (diff)
downloadtangerine-wallet-browser-e226b10a89d87af07c7c35ff1251a8264f3bb1b8.tar
tangerine-wallet-browser-e226b10a89d87af07c7c35ff1251a8264f3bb1b8.tar.gz
tangerine-wallet-browser-e226b10a89d87af07c7c35ff1251a8264f3bb1b8.tar.bz2
tangerine-wallet-browser-e226b10a89d87af07c7c35ff1251a8264f3bb1b8.tar.lz
tangerine-wallet-browser-e226b10a89d87af07c7c35ff1251a8264f3bb1b8.tar.xz
tangerine-wallet-browser-e226b10a89d87af07c7c35ff1251a8264f3bb1b8.tar.zst
tangerine-wallet-browser-e226b10a89d87af07c7c35ff1251a8264f3bb1b8.zip
Add react-router to allow use of the browser back button
Diffstat (limited to 'ui/app/components/pages')
-rw-r--r--ui/app/components/pages/add-token.js359
-rw-r--r--ui/app/components/pages/authenticated.js42
-rw-r--r--ui/app/components/pages/import-account/index.js95
-rw-r--r--ui/app/components/pages/import-account/json.js100
-rw-r--r--ui/app/components/pages/import-account/private-key.js71
-rw-r--r--ui/app/components/pages/import-account/seed.js30
-rw-r--r--ui/app/components/pages/keychains/restore-vault.js167
-rw-r--r--ui/app/components/pages/keychains/reveal-seed.js192
-rw-r--r--ui/app/components/pages/notice.js219
-rw-r--r--ui/app/components/pages/settings/index.js59
-rw-r--r--ui/app/components/pages/settings/info.js108
-rw-r--r--ui/app/components/pages/settings/settings.js283
-rw-r--r--ui/app/components/pages/unauthenticated/unlock.js172
13 files changed, 1897 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..7f0c37475
--- /dev/null
+++ b/ui/app/components/pages/add-token.js
@@ -0,0 +1,359 @@
+const inherits = require('util').inherits
+const Component = require('react').Component
+const classnames = require('classnames')
+const h = require('react-hyperscript')
+const connect = require('react-redux').connect
+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: ['address', 'name', 'symbol'],
+})
+// const actions = require('./actions')
+const actions = require('../../actions')
+const ethUtil = require('ethereumjs-util')
+const { tokenInfoGetter } = require('../../token-util')
+const R = require('ramda')
+const { DEFAULT_ROUTE } = require('../../routes')
+
+const emptyAddr = '0x0000000000000000000000000000000000000000'
+
+module.exports = connect(mapStateToProps, mapDispatchToProps)(AddTokenScreen)
+
+function mapStateToProps (state) {
+ const { identities, tokens } = state.metamask
+ return {
+ identities,
+ tokens,
+ }
+}
+
+function mapDispatchToProps (dispatch) {
+ return {
+ goHome: () => dispatch(actions.goHome()),
+ addTokens: tokens => dispatch(actions.addTokens(tokens)),
+ }
+}
+
+inherits(AddTokenScreen, Component)
+function AddTokenScreen () {
+ this.state = {
+ isShowingConfirmation: false,
+ customAddress: '',
+ customSymbol: '',
+ customDecimals: 0,
+ searchQuery: '',
+ isCollapsed: true,
+ selectedTokens: {},
+ errors: {},
+ }
+ this.tokenAddressDidChange = this.tokenAddressDidChange.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 { [address]: selectedToken } = selectedTokens
+ this.setState({
+ selectedTokens: {
+ ...selectedTokens,
+ [address]: selectedToken ? null : token,
+ },
+ 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.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 = 'Address is invalid. '
+ }
+
+ const validDecimals = customDecimals >= 0 && customDecimals < 36
+ if (!validDecimals) {
+ errors.customDecimals = 'Decimals must be at least 0, and not over 36.'
+ }
+
+ const symbolLen = customSymbol.trim().length
+ const validSymbol = symbolLen > 0 && symbolLen < 10
+ if (!validSymbol) {
+ errors.customSymbol = 'Symbol must be between 0 and 10 characters.'
+ }
+
+ const ownAddress = identitiesList.includes(standardAddress)
+ if (ownAddress) {
+ errors.customAddress = 'Personal address detected. Input the token contract address.'
+ }
+
+ const tokenAlreadyAdded = this.checkExistingAddresses(customAddress)
+ if (tokenAlreadyAdded) {
+ errors.customAddress = 'Token has already been added.'
+ }
+ } else if (
+ Object.entries(selectedTokens)
+ .reduce((isEmpty, [ symbol, isSelected ]) => (
+ isEmpty && !isSelected
+ ), true)
+ ) {
+ errors.tokenSelector = 'Must select at least 1 token.'
+ }
+
+ 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(),
+ })
+ }
+}
+
+AddTokenScreen.prototype.renderCustomForm = function () {
+ const { customAddress, customSymbol, customDecimals, errors } = this.state
+
+ return !this.state.isCollapsed && (
+ 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', 'Token Address'),
+ 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', 'Token Symbol'),
+ h('input.add-token__add-custom-input', {
+ type: 'text',
+ value: customSymbol,
+ disabled: true,
+ }),
+ 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', 'Decimals of Precision'),
+ h('input.add-token__add-custom-input', {
+ type: 'number',
+ value: customDecimals,
+ disabled: true,
+ }),
+ h('div.add-token__add-custom-error-message', errors.customDecimals),
+ ]),
+ ])
+ )
+}
+
+AddTokenScreen.prototype.renderTokenList = function () {
+ const { searchQuery = '', selectedTokens } = this.state
+ const results = searchQuery
+ ? fuse.search(searchQuery) || []
+ : contractList
+
+ return 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: `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__title-container.add-token__confirmation-title', [
+ h('div.add-token__title', 'Add Token'),
+ h('div.add-token__description', 'Would you like to add these tokens?'),
+ ]),
+ h('div.add-token__content-container.add-token__confirmation-content', [
+ h('div.add-token__description.add-token__confirmation-description', 'Your 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', {
+ onClick: () => addTokens(tokens).then(() => history.push(DEFAULT_ROUTE)),
+ }, 'Add Tokens'),
+ h('button.btn-tertiary', {
+ onClick: () => this.setState({ isShowingConfirmation: false }),
+ }, 'Back'),
+ ]),
+ ])
+ )
+}
+
+AddTokenScreen.prototype.render = function () {
+ const { isCollapsed, errors, isShowingConfirmation } = this.state
+ const { history } = this.props
+
+ return isShowingConfirmation
+ ? this.renderConfirmation()
+ : (
+ h('div.add-token', [
+ h('div.add-token__wrapper', [
+ h('div.add-token__title-container', [
+ h('div.add-token__title', 'Add Token'),
+ h('div.add-token__description', 'Keep track of the tokens you’ve bought with your MetaMask account. If you bought tokens using a different account, those tokens will not appear here.'),
+ h('div.add-token__description', 'Search for tokens or select from our list of popular tokens.'),
+ ]),
+ h('div.add-token__content-container', [
+ h('div.add-token__input-container', [
+ h('input.add-token__input', {
+ type: 'text',
+ placeholder: 'Search',
+ onChange: e => this.setState({ searchQuery: e.target.value }),
+ }),
+ h('div.add-token__search-input-error-message', errors.tokenSelector),
+ ]),
+ h(
+ 'div.add-token__token-icons-container',
+ this.renderTokenList(),
+ ),
+ ]),
+ h('div.add-token__footers', [
+ h('div.add-token__add-custom', {
+ onClick: () => this.setState({ isCollapsed: !isCollapsed }),
+ }, [
+ 'Add custom token',
+ h(`i.fa.fa-angle-${isCollapsed ? 'down' : 'up'}`),
+ ]),
+ this.renderCustomForm(),
+ ]),
+ ]),
+ h('div.add-token__buttons', [
+ h('button.btn-secondary', {
+ onClick: this.onNext,
+ }, 'Next'),
+ h('button.btn-tertiary', {
+ onClick: () => history.goBack(),
+ }, 'Cancel'),
+ ]),
+ ])
+ )
+}
diff --git a/ui/app/components/pages/authenticated.js b/ui/app/components/pages/authenticated.js
new file mode 100644
index 000000000..78f3a4225
--- /dev/null
+++ b/ui/app/components/pages/authenticated.js
@@ -0,0 +1,42 @@
+const { connect } = require('react-redux')
+const PropTypes = require('prop-types')
+const { Redirect, Route } = require('react-router-dom')
+const h = require('react-hyperscript')
+const { UNLOCK_ROUTE, INITIALIZE_MENU_ROUTE } = require('../../routes')
+
+const Authenticated = ({ component: Component, isUnlocked, isInitialized, ...props }) => {
+
+ const render = props => {
+ switch (true) {
+ case isUnlocked:
+ return h(Component, { ...props })
+ case !isInitialized:
+ return h(Redirect, { to: { pathname: INITIALIZE_MENU_ROUTE } })
+ default:
+ return h(Redirect, { to: { pathname: UNLOCK_ROUTE } })
+ }
+ }
+
+ return (
+ h(Route, {
+ ...props,
+ render,
+ })
+ )
+}
+
+Authenticated.propTypes = {
+ component: PropTypes.func,
+ 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/import-account/index.js b/ui/app/components/pages/import-account/index.js
new file mode 100644
index 000000000..481ed6a4b
--- /dev/null
+++ b/ui/app/components/pages/import-account/index.js
@@ -0,0 +1,95 @@
+const Component = require('react').Component
+const h = require('react-hyperscript')
+const PropTypes = require('prop-types')
+import Select from 'react-select'
+
+// Subviews
+const JsonImportView = require('./json.js')
+const PrivateKeyImportView = require('./private-key.js')
+
+const PRIVATE_KEY_MENU_ITEM = 'Private Key'
+const JSON_FILE_MENU_ITEM = 'JSON File'
+
+class ImportAccount extends Component {
+ constructor (props) {
+ super(props)
+
+ this.state = {
+ current: PRIVATE_KEY_MENU_ITEM,
+ menuItems: [ PRIVATE_KEY_MENU_ITEM, JSON_FILE_MENU_ITEM ],
+ }
+ }
+
+ renderImportView () {
+ const { current } = this.state
+
+ switch (current) {
+ case 'Private Key':
+ return h(PrivateKeyImportView)
+ case 'JSON File':
+ return h(JsonImportView)
+ default:
+ return h(JsonImportView)
+ }
+ }
+
+ render () {
+ const { history } = this.props
+ const { current, menuItems } = this.state
+
+ return (
+ h('div.flex-center', {
+ style: {
+ flexDirection: 'column',
+ marginTop: '32px',
+ },
+ }, [
+ h('.section-title.flex-row.flex-center', [
+ h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', {
+ onClick: history.goBack,
+ }),
+ h('h2.page-subtitle', 'Import Accounts'),
+ ]),
+ h('div', {
+ style: {
+ padding: '10px 0',
+ width: '260px',
+ color: 'rgb(174, 174, 174)',
+ },
+ }, [
+
+ h('h3', { style: { padding: '3px' } }, 'SELECT TYPE'),
+
+ h('style', `
+ .has-value.Select--single > .Select-control .Select-value .Select-value-label, .Select-value-label {
+ color: rgb(174,174,174);
+ }
+ `),
+
+ h(Select, {
+ name: 'import-type-select',
+ clearable: false,
+ value: current,
+ options: menuItems.map(type => {
+ return {
+ value: type,
+ label: type,
+ }
+ }),
+ onChange: opt => {
+ this.setState({ current: opt.value })
+ },
+ }),
+ ]),
+
+ this.renderImportView(),
+ ])
+ )
+ }
+}
+
+ImportAccount.propTypes = {
+ history: PropTypes.object,
+}
+
+module.exports = ImportAccount
diff --git a/ui/app/components/pages/import-account/json.js b/ui/app/components/pages/import-account/json.js
new file mode 100644
index 000000000..63a44b4b7
--- /dev/null
+++ b/ui/app/components/pages/import-account/json.js
@@ -0,0 +1,100 @@
+const inherits = require('util').inherits
+const Component = require('react').Component
+const h = require('react-hyperscript')
+const connect = require('react-redux').connect
+const actions = require('../../../actions')
+const FileInput = require('react-simple-file-input').default
+
+const HELP_LINK = 'https://support.metamask.io/kb/article/7-importing-accounts'
+
+module.exports = connect(mapStateToProps)(JsonImportSubview)
+
+function mapStateToProps (state) {
+ return {
+ error: state.appState.warning,
+ }
+}
+
+inherits(JsonImportSubview, Component)
+function JsonImportSubview () {
+ Component.call(this)
+}
+
+JsonImportSubview.prototype.render = function () {
+ const { error } = this.props
+
+ return (
+ h('div', {
+ style: {
+ display: 'flex',
+ flexDirection: 'column',
+ alignItems: 'center',
+ padding: '5px 15px 0px 15px',
+ },
+ }, [
+
+ h('p', 'Used by a variety of different clients'),
+ h('a.warning', { href: HELP_LINK, target: '_blank' }, 'File import not working? Click here!'),
+
+ h(FileInput, {
+ readAs: 'text',
+ onLoad: this.onLoad.bind(this),
+ style: {
+ margin: '20px 0px 12px 20px',
+ fontSize: '15px',
+ },
+ }),
+
+ h('input.large-input.letter-spacey', {
+ type: 'password',
+ placeholder: 'Enter password',
+ id: 'json-password-box',
+ onKeyPress: this.createKeyringOnEnter.bind(this),
+ style: {
+ width: 260,
+ marginTop: 12,
+ },
+ }),
+
+ h('button.primary', {
+ onClick: this.createNewKeychain.bind(this),
+ style: {
+ margin: 12,
+ },
+ }, 'Import'),
+
+ error ? h('span.error', error) : null,
+ ])
+ )
+}
+
+JsonImportSubview.prototype.onLoad = function (event, file) {
+ this.setState({file: file, fileContents: event.target.result})
+}
+
+JsonImportSubview.prototype.createKeyringOnEnter = function (event) {
+ if (event.key === 'Enter') {
+ event.preventDefault()
+ this.createNewKeychain()
+ }
+}
+
+JsonImportSubview.prototype.createNewKeychain = function () {
+ const state = this.state
+ const { fileContents } = state
+
+ if (!fileContents) {
+ const message = 'You must select a file to import.'
+ return this.props.dispatch(actions.displayWarning(message))
+ }
+
+ const passwordInput = document.getElementById('json-password-box')
+ const password = passwordInput.value
+
+ if (!password) {
+ const message = 'You must enter a password for the selected file.'
+ return this.props.dispatch(actions.displayWarning(message))
+ }
+
+ this.props.dispatch(actions.importNewAccount('JSON File', [ fileContents, password ]))
+}
diff --git a/ui/app/components/pages/import-account/private-key.js b/ui/app/components/pages/import-account/private-key.js
new file mode 100644
index 000000000..217e6d9f9
--- /dev/null
+++ b/ui/app/components/pages/import-account/private-key.js
@@ -0,0 +1,71 @@
+const inherits = require('util').inherits
+const Component = require('react').Component
+const h = require('react-hyperscript')
+const connect = require('react-redux').connect
+const actions = require('../../../actions')
+
+module.exports = connect(mapStateToProps)(PrivateKeyImportView)
+
+function mapStateToProps (state) {
+ return {
+ error: state.appState.warning,
+ }
+}
+
+inherits(PrivateKeyImportView, Component)
+function PrivateKeyImportView () {
+ Component.call(this)
+}
+
+PrivateKeyImportView.prototype.componentWillUnmount = function () {
+ this.props.dispatch(actions.displayWarning(null))
+}
+
+PrivateKeyImportView.prototype.render = function () {
+ const { error } = this.props
+
+ return (
+ h('div', {
+ style: {
+ display: 'flex',
+ flexDirection: 'column',
+ alignItems: 'center',
+ padding: '5px 15px 0px 15px',
+ },
+ }, [
+ h('span', 'Paste your private key string here'),
+
+ h('input.large-input.letter-spacey', {
+ type: 'password',
+ id: 'private-key-box',
+ onKeyPress: this.createKeyringOnEnter.bind(this),
+ style: {
+ width: 260,
+ marginTop: 12,
+ },
+ }),
+
+ h('button.primary', {
+ onClick: this.createNewKeychain.bind(this),
+ style: {
+ margin: 12,
+ },
+ }, '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
+ this.props.dispatch(actions.importNewAccount('Private Key', [ privateKey ]))
+}
diff --git a/ui/app/components/pages/import-account/seed.js b/ui/app/components/pages/import-account/seed.js
new file mode 100644
index 000000000..b4a7c0afa
--- /dev/null
+++ b/ui/app/components/pages/import-account/seed.js
@@ -0,0 +1,30 @@
+const inherits = require('util').inherits
+const Component = require('react').Component
+const h = require('react-hyperscript')
+const connect = require('react-redux').connect
+
+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: {
+ },
+ }, [
+ `Paste your seed phrase here!`,
+ h('textarea'),
+ h('br'),
+ h('button', 'Submit'),
+ ])
+ )
+}
+
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..5573f2dd2
--- /dev/null
+++ b/ui/app/components/pages/keychains/restore-vault.js
@@ -0,0 +1,167 @@
+const { withRouter } = require('react-router-dom')
+const PropTypes = require('prop-types')
+const { compose } = require('recompose')
+const PersistentForm = require('../../../../lib/persistent-form')
+const { connect } = require('react-redux')
+const h = require('react-hyperscript')
+const { createNewVaultAndRestore } = 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()
+ }
+ }
+
+ 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(() => history.push(DEFAULT_ROUTE))
+ .catch(({ message }) => this.setState({ error: message }))
+ }
+
+ render () {
+ const { error } = this.state
+ const { history } = this.props
+ 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,
+ },
+ }, [
+ 'Restore Vault',
+ ]),
+
+ // wallet seed entry
+ h('h3', 'Wallet Seed'),
+ h('textarea.twelve-word-phrase.letter-spacey', {
+ dataset: {
+ persistentFormId: 'wallet-seed',
+ },
+ placeholder: 'Enter your secret twelve word phrase here to restore your vault.',
+ }),
+
+ // password
+ h('input.large-input.letter-spacey', {
+ type: 'password',
+ id: 'password-box',
+ placeholder: 'New Password (min 8 chars)',
+ dataset: {
+ persistentFormId: 'password',
+ },
+ style: {
+ width: 260,
+ marginTop: 12,
+ },
+ }),
+
+ // confirm password
+ h('input.large-input.letter-spacey', {
+ type: 'password',
+ id: 'password-box-confirm',
+ placeholder: 'Confirm Password',
+ 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: () => history.goBack() }, 'CANCEL'),
+
+ // submit
+ h('button.primary', {
+ onClick: this.createNewVaultAndRestore.bind(this),
+ }, '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))
+ },
+ }
+}
+
+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..dc2cfc23e
--- /dev/null
+++ b/ui/app/components/pages/keychains/reveal-seed.js
@@ -0,0 +1,192 @@
+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 () {
+ document.getElementById('password-box').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.goBack(),
+ }, '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/notice.js b/ui/app/components/pages/notice.js
new file mode 100644
index 000000000..8e68cd52b
--- /dev/null
+++ b/ui/app/components/pages/notice.js
@@ -0,0 +1,219 @@
+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 state = this.state || { disclaimerDisabled: true }
+ 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
+ // 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()),
+ // })
+ // }
+
+ 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..39e9b26ed
--- /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 { 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('/'),
+ }),
+ 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..d8155eb9b
--- /dev/null
+++ b/ui/app/components/pages/settings/info.js
@@ -0,0 +1,108 @@
+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', 'Links'),
+ h('div.settings__info-link-item', [
+ h('a', {
+ href: 'https://metamask.io/privacy.html',
+ target: '_blank',
+ }, [
+ h('span.settings__info-link', 'Privacy Policy'),
+ ]),
+ ]),
+ h('div.settings__info-link-item', [
+ h('a', {
+ href: 'https://metamask.io/terms.html',
+ target: '_blank',
+ }, [
+ h('span.settings__info-link', 'Terms of Use'),
+ ]),
+ ]),
+ h('div.settings__info-link-item', [
+ h('a', {
+ href: 'https://metamask.io/attributions.html',
+ target: '_blank',
+ }, [
+ h('span.settings__info-link', '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', 'Visit our Support Center'),
+ ]),
+ ]),
+ h('div.settings__info-link-item', [
+ h('a', {
+ href: 'https://metamask.io/',
+ target: '_blank',
+ }, [
+ h('span.settings__info-link', 'Visit our web site'),
+ ]),
+ ]),
+ h('div.settings__info-link-item', [
+ h('a', {
+ target: '_blank',
+ href: 'mailto:help@metamask.io?subject=Feedback',
+ }, [
+ h('span.settings__info-link', 'Email us!'),
+ ]),
+ ]),
+ ])
+ )
+ }
+
+ 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',
+ 'MetaMask is designed and built in California.'
+ ),
+ ]),
+ ]),
+ 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,
+ goHome: PropTypes.func,
+ location: PropTypes.object,
+ history: PropTypes.object,
+}
+
+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..537270dee
--- /dev/null
+++ b/ui/app/components/pages/settings/settings.js
@@ -0,0 +1,283 @@
+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')
+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 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,
+ }
+ })
+}
+
+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', 'Use Blockies Identicon'),
+ ]),
+ 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', 'Current Conversion'),
+ h('span.settings__content-description', `Updated ${Date(conversionDate)}`),
+ ]),
+ h('div.settings__content-item', [
+ h('div.settings__content-item-col', [
+ h(SimpleDropdown, {
+ placeholder: 'Select Currency',
+ options: getInfuraCurrencyOptions(),
+ selectedOption: currentCurrency,
+ onSelect: newCurrency => setCurrentCurrency(newCurrency),
+ }),
+ ]),
+ ]),
+ ])
+ }
+
+ renderCurrentProvider () {
+ const { metamask: { provider = {} } } = this.props
+ let title, value, color
+
+ switch (provider.type) {
+
+ case 'mainnet':
+ title = 'Current Network'
+ value = 'Main Ethereum Network'
+ color = '#038789'
+ break
+
+ case 'ropsten':
+ title = 'Current Network'
+ value = 'Ropsten Test Network'
+ color = '#e91550'
+ break
+
+ case 'kovan':
+ title = 'Current Network'
+ value = 'Kovan Test Network'
+ color = '#690496'
+ break
+
+ case 'rinkeby':
+ title = 'Current Network'
+ value = 'Rinkeby Test Network'
+ color = '#ebb33f'
+ break
+
+ default:
+ title = 'Current RPC'
+ 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', 'New RPC URL'),
+ ]),
+ h('div.settings__content-item', [
+ h('div.settings__content-item-col', [
+ h('input.settings__input', {
+ placeholder: 'New RPC URL',
+ 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)
+ },
+ }, 'Save'),
+ ]),
+ ]),
+ ])
+ )
+ }
+
+ validateRpc (newRpc) {
+ const { setRpcTarget, displayWarning } = this.props
+
+ if (validUrl.isWebUri(newRpc)) {
+ setRpcTarget(newRpc)
+ } else {
+ const appendedRpc = `http://${newRpc}`
+
+ if (validUrl.isWebUri(appendedRpc)) {
+ displayWarning('URIs require the appropriate HTTP/HTTPS prefix.')
+ } else {
+ displayWarning('Invalid RPC URI')
+ }
+ }
+ }
+
+ renderStateLogs () {
+ return (
+ h('div.settings__content-row', [
+ h('div.settings__content-item', [
+ h('div', 'State Logs'),
+ h(
+ 'div.settings__content-description',
+ 'State logs contain your public account addresses and sent transactions.'
+ ),
+ ]),
+ h('div.settings__content-item', [
+ h('div.settings__content-item-col', [
+ h('button.settings__clear-button', {
+ onClick (event) {
+ exportAsFile('MetaMask State Logs', window.logState())
+ },
+ }, 'Download State Logs'),
+ ]),
+ ]),
+ ])
+ )
+ }
+
+ renderSeedWords () {
+ const { history } = this.props
+
+ return (
+ h('div.settings__content-row', [
+ h('div.settings__content-item', 'Reveal Seed Words'),
+ h('div.settings__content-item', [
+ h('div.settings__content-item-col', [
+ h('button.settings__clear-button.settings__clear-button--red', {
+ onClick: () => history.push(REVEAL_SEED_ROUTE),
+ }, 'Reveal Seed Words'),
+ ]),
+ ]),
+ ])
+ )
+ }
+
+ renderOldUI () {
+ const { setFeatureFlagToBeta } = this.props
+
+ return (
+ h('div.settings__content-row', [
+ h('div.settings__content-item', 'Use old UI'),
+ h('div.settings__content-item', [
+ h('div.settings__content-item-col', [
+ h('button.settings__clear-button.settings__clear-button--orange', {
+ onClick (event) {
+ event.preventDefault()
+ setFeatureFlagToBeta()
+ },
+ }, 'Use old UI'),
+ ]),
+ ]),
+ ])
+ )
+ }
+
+ render () {
+ const { warning } = this.props
+
+ return (
+ h('div.settings__content', [
+ warning && h('div.settings__error', warning),
+ this.renderBlockieOptIn(),
+ this.renderCurrentConversion(),
+ // this.renderCurrentProvider(),
+ this.renderNewRpcUrl(),
+ this.renderStateLogs(),
+ this.renderSeedWords(),
+ this.renderOldUI(),
+ ])
+ )
+ }
+}
+
+Settings.propTypes = {
+ metamask: PropTypes.object,
+ setUseBlockie: PropTypes.func,
+ setCurrentCurrency: PropTypes.func,
+ setRpcTarget: PropTypes.func,
+ displayWarning: PropTypes.func,
+ revealSeedConfirmation: PropTypes.func,
+ setFeatureFlagToBeta: PropTypes.func,
+ warning: PropTypes.string,
+ history: PropTypes.object,
+}
+
+const mapStateToProps = state => {
+ return {
+ metamask: state.metamask,
+ warning: state.appState.warning,
+ }
+}
+
+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)),
+ setFeatureFlagToBeta: () => dispatch(actions.setFeatureFlag('betaUI', false)),
+ }
+}
+
+module.exports = compose(
+ withRouter,
+ connect(mapStateToProps, mapDispatchToProps)
+)(Settings)
diff --git a/ui/app/components/pages/unauthenticated/unlock.js b/ui/app/components/pages/unauthenticated/unlock.js
new file mode 100644
index 000000000..72f27c11d
--- /dev/null
+++ b/ui/app/components/pages/unauthenticated/unlock.js
@@ -0,0 +1,172 @@
+const { Component } = require('react')
+const PropTypes = require('prop-types')
+const { connect } = require('react-redux')
+const h = require('react-hyperscript')
+const { Redirect, withRouter } = require('react-router-dom')
+const { compose } = require('recompose')
+const { tryUnlockMetamask, forgotPassword } = require('../../../actions')
+const getCaretCoordinates = require('textarea-caret')
+const EventEmitter = require('events').EventEmitter
+const Mascot = require('../../mascot')
+const { DEFAULT_ROUTE, RESTORE_VAULT_ROUTE } = require('../../../routes')
+
+class UnlockScreen extends Component {
+ constructor (props) {
+ super(props)
+
+ this.state = {
+ error: null,
+ }
+
+ this.animationEventEmitter = new EventEmitter()
+ }
+
+ 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
+ const { isUnlocked, history } = this.props
+
+ if (isUnlocked) {
+ return (
+ h(Redirect, {
+ to: {
+ pathname: DEFAULT_ROUTE,
+ },
+ })
+ )
+ }
+
+ return (
+ h('.unlock-page.main-container', [
+ h('.flex-column', {
+ style: {
+ width: 'inherit',
+ },
+ }, [
+ h('.unlock-screen.flex-column.flex-center.flex-grow', [
+
+ h(Mascot, {
+ animationEventEmitter: this.animationEventEmitter,
+ }),
+
+ h('h1', {
+ style: {
+ fontSize: '1.4em',
+ textTransform: 'uppercase',
+ color: '#7F8082',
+ },
+ }, 'MetaMask'),
+
+ 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,
+ },
+ }, 'Unlock'),
+
+ h('.flex-row.flex-center.flex-grow', [
+ h('p.pointer', {
+ onClick: () => history.push(RESTORE_VAULT_ROUTE),
+ style: {
+ fontSize: '0.8em',
+ color: 'rgb(247, 134, 28)',
+ textDecoration: 'underline',
+ },
+ }, 'Restore from seed phrase'),
+ ]),
+ ]),
+ ]),
+ ])
+ )
+ }
+}
+
+UnlockScreen.propTypes = {
+ forgotPassword: PropTypes.func,
+ tryUnlockMetamask: PropTypes.func,
+ history: PropTypes.object,
+ isUnlocked: PropTypes.bool,
+}
+
+const mapStateToProps = state => {
+ const { metamask: { isUnlocked } } = state
+ return {
+ isUnlocked,
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ forgotPassword: () => dispatch(forgotPassword()),
+ tryUnlockMetamask: password => dispatch(tryUnlockMetamask(password)),
+ }
+}
+
+module.exports = compose(
+ withRouter,
+ connect(mapStateToProps, mapDispatchToProps)
+)(UnlockScreen)