aboutsummaryrefslogtreecommitdiffstats
path: root/ui/app/components
diff options
context:
space:
mode:
authorBruno Barbieri <bruno.barbieri@consensys.net>2018-07-20 03:00:59 +0800
committerGitHub <noreply@github.com>2018-07-20 03:00:59 +0800
commit9be2248d7accfc51fd21f31b16d08c18a468c0f7 (patch)
treed52384c0b0803e68188fe310a665cec2890b5680 /ui/app/components
parent3784a7e2c852974315cfe17a68673938cf24a7fa (diff)
parentdf19163bf9611d75aaf8ea6da52651dbba9a5e00 (diff)
downloadtangerine-wallet-browser-9be2248d7accfc51fd21f31b16d08c18a468c0f7.tar
tangerine-wallet-browser-9be2248d7accfc51fd21f31b16d08c18a468c0f7.tar.gz
tangerine-wallet-browser-9be2248d7accfc51fd21f31b16d08c18a468c0f7.tar.bz2
tangerine-wallet-browser-9be2248d7accfc51fd21f31b16d08c18a468c0f7.tar.lz
tangerine-wallet-browser-9be2248d7accfc51fd21f31b16d08c18a468c0f7.tar.xz
tangerine-wallet-browser-9be2248d7accfc51fd21f31b16d08c18a468c0f7.tar.zst
tangerine-wallet-browser-9be2248d7accfc51fd21f31b16d08c18a468c0f7.zip
Merge pull request #4625 from MetaMask/initial-trezor-support
Initial trezor support
Diffstat (limited to 'ui/app/components')
-rw-r--r--ui/app/components/account-menu/index.js69
-rw-r--r--ui/app/components/alert/index.js22
-rw-r--r--ui/app/components/modals/confirm-remove-account/confirm-remove-account.component.js93
-rw-r--r--ui/app/components/modals/confirm-remove-account/confirm-remove-account.container.js20
-rw-r--r--ui/app/components/modals/confirm-remove-account/index.js2
-rw-r--r--ui/app/components/modals/index.scss52
-rw-r--r--ui/app/components/modals/modal.js14
-rw-r--r--ui/app/components/pages/create-account/connect-hardware/account-list.js143
-rw-r--r--ui/app/components/pages/create-account/connect-hardware/connect-screen.js149
-rw-r--r--ui/app/components/pages/create-account/connect-hardware/index.js234
-rw-r--r--ui/app/components/pages/create-account/index.js25
-rw-r--r--ui/app/components/pages/create-account/new-account.js2
12 files changed, 819 insertions, 6 deletions
diff --git a/ui/app/components/account-menu/index.js b/ui/app/components/account-menu/index.js
index f34631ca8..9c063d31e 100644
--- a/ui/app/components/account-menu/index.js
+++ b/ui/app/components/account-menu/index.js
@@ -9,11 +9,17 @@ const actions = require('../../actions')
const { Menu, Item, Divider, CloseArea } = require('../dropdowns/components/menu')
const Identicon = require('../identicon')
const { formatBalance } = require('../../util')
+const { ENVIRONMENT_TYPE_POPUP } = require('../../../../app/scripts/lib/enums')
+const { getEnvironmentType } = require('../../../../app/scripts/lib/util')
+const Tooltip = require('../tooltip')
+
+
const {
SETTINGS_ROUTE,
INFO_ROUTE,
NEW_ACCOUNT_ROUTE,
IMPORT_ACCOUNT_ROUTE,
+ CONNECT_HARDWARE_ROUTE,
DEFAULT_ROUTE,
} = require('../../routes')
@@ -63,6 +69,9 @@ function mapDispatchToProps (dispatch) {
dispatch(actions.hideSidebar())
dispatch(actions.toggleAccountMenu())
},
+ showRemoveAccountConfirmationModal: (identity) => {
+ return dispatch(actions.showModal({ name: 'CONFIRM_REMOVE_ACCOUNT', identity }))
+ },
}
}
@@ -106,6 +115,18 @@ AccountMenu.prototype.render = function () {
icon: h('img.account-menu__item-icon', { src: 'images/import-account.svg' }),
text: this.context.t('importAccount'),
}),
+ h(Item, {
+ onClick: () => {
+ toggleAccountMenu()
+ if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_POPUP) {
+ global.platform.openExtensionInBrowser(CONNECT_HARDWARE_ROUTE)
+ } else {
+ history.push(CONNECT_HARDWARE_ROUTE)
+ }
+ },
+ icon: h('img.account-menu__item-icon', { src: 'images/connect-icon.svg' }),
+ text: this.context.t('connectHardwareWallet'),
+ }),
h(Divider),
h(Item, {
onClick: () => {
@@ -136,7 +157,8 @@ AccountMenu.prototype.renderAccounts = function () {
} = this.props
const accountOrder = keyrings.reduce((list, keyring) => list.concat(keyring.accounts), [])
- return accountOrder.map((address) => {
+ return accountOrder.filter(address => !!identities[address]).map((address) => {
+
const identity = identities[address]
const isSelected = identity.address === selectedAddress
@@ -170,16 +192,53 @@ AccountMenu.prototype.renderAccounts = function () {
h('div.account-menu__balance', formattedBalance),
]),
- this.indicateIfLoose(keyring),
+ this.renderKeyringType(keyring),
+ this.renderRemoveAccount(keyring, identity),
],
)
})
}
-AccountMenu.prototype.indicateIfLoose = function (keyring) {
+AccountMenu.prototype.renderRemoveAccount = function (keyring, identity) {
+ // Any account that's not from the HD wallet Keyring can be removed
+ const type = keyring.type
+ const isRemovable = type !== 'HD Key Tree'
+ if (isRemovable) {
+ return h(Tooltip, {
+ title: this.context.t('removeAccount'),
+ position: 'bottom',
+ }, [
+ h('a.remove-account-icon', {
+ onClick: (e) => this.removeAccount(e, identity),
+ }, ''),
+ ])
+ }
+ return null
+}
+
+AccountMenu.prototype.removeAccount = function (e, identity) {
+ e.preventDefault()
+ e.stopPropagation()
+ const { showRemoveAccountConfirmationModal } = this.props
+ showRemoveAccountConfirmationModal(identity)
+}
+
+AccountMenu.prototype.renderKeyringType = function (keyring) {
try { // Sometimes keyrings aren't loaded yet:
const type = keyring.type
- const isLoose = type !== 'HD Key Tree'
- return isLoose ? h('.keyring-label.allcaps', this.context.t('imported')) : null
+ let label
+ switch (type) {
+ case 'Trezor Hardware':
+ label = this.context.t('hardware')
+ break
+ case 'Simple Key Pair':
+ label = this.context.t('imported')
+ break
+ default:
+ label = ''
+ }
+
+ return label !== '' ? h('.keyring-label.allcaps', label) : null
+
} catch (e) { return }
}
diff --git a/ui/app/components/alert/index.js b/ui/app/components/alert/index.js
new file mode 100644
index 000000000..fc39d41e2
--- /dev/null
+++ b/ui/app/components/alert/index.js
@@ -0,0 +1,22 @@
+const { Component } = require('react')
+const PropTypes = require('prop-types')
+const h = require('react-hyperscript')
+
+class Alert extends Component {
+
+ render () {
+ const className = `.global-alert${this.props.visible ? '.visible' : '.hidden'}`
+ return (
+ h(`div${className}`, {},
+ h('a.msg', {}, this.props.msg)
+ )
+ )
+ }
+}
+
+Alert.propTypes = {
+ visible: PropTypes.bool.isRequired,
+ msg: PropTypes.string,
+}
+module.exports = Alert
+
diff --git a/ui/app/components/modals/confirm-remove-account/confirm-remove-account.component.js b/ui/app/components/modals/confirm-remove-account/confirm-remove-account.component.js
new file mode 100644
index 000000000..5a9f0f289
--- /dev/null
+++ b/ui/app/components/modals/confirm-remove-account/confirm-remove-account.component.js
@@ -0,0 +1,93 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import Button from '../../button'
+import { addressSummary } from '../../../util'
+import Identicon from '../../identicon'
+import genAccountLink from '../../../../lib/account-link'
+
+class ConfirmRemoveAccount extends Component {
+ static propTypes = {
+ hideModal: PropTypes.func.isRequired,
+ removeAccount: PropTypes.func.isRequired,
+ identity: PropTypes.object.isRequired,
+ network: PropTypes.string.isRequired,
+ }
+
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ handleRemove () {
+ this.props.removeAccount(this.props.identity.address)
+ .then(() => this.props.hideModal())
+ }
+
+ renderSelectedAccount () {
+ const { identity } = this.props
+ return (
+ <div className="modal-container__account">
+ <div className="modal-container__account__identicon">
+ <Identicon
+ address={identity.address}
+ diameter={32}
+ />
+ </div>
+ <div className="modal-container__account__name">
+ <span className="modal-container__account__label">Name</span>
+ <span className="account_value">{identity.name}</span>
+ </div>
+ <div className="modal-container__account__address">
+ <span className="modal-container__account__label">Public Address</span>
+ <span className="account_value">{ addressSummary(identity.address, 4, 4) }</span>
+ </div>
+ <div className="modal-container__account__link">
+ <a
+ className=""
+ href={genAccountLink(identity.address, this.props.network)}
+ target={'_blank'}
+ title={this.context.t('etherscanView')}
+ >
+ <img src="images/popout.svg" />
+ </a>
+ </div>
+ </div>
+ )
+ }
+
+ render () {
+ const { t } = this.context
+
+ return (
+ <div className="modal-container">
+ <div className="modal-container__content">
+ <div className="modal-container__title">
+ { `${t('removeAccount')}` }?
+ </div>
+ { this.renderSelectedAccount() }
+ <div className="modal-container__description">
+ { t('removeAccountDescription') }
+ <a className="modal-container__link" rel="noopener noreferrer" target="_blank" href="https://consensys.zendesk.com/hc/en-us/articles/360004180111-What-are-imported-accounts-New-UI-">{ t('learnMore') }</a>
+ </div>
+ </div>
+ <div className="modal-container__footer">
+ <Button
+ type="default"
+ className="modal-container__footer-button"
+ onClick={() => this.props.hideModal()}
+ >
+ { t('nevermind') }
+ </Button>
+ <Button
+ type="secondary"
+ className="modal-container__footer-button"
+ onClick={() => this.handleRemove()}
+ >
+ { t('remove') }
+ </Button>
+ </div>
+ </div>
+ )
+ }
+}
+
+export default ConfirmRemoveAccount
diff --git a/ui/app/components/modals/confirm-remove-account/confirm-remove-account.container.js b/ui/app/components/modals/confirm-remove-account/confirm-remove-account.container.js
new file mode 100644
index 000000000..4b194c995
--- /dev/null
+++ b/ui/app/components/modals/confirm-remove-account/confirm-remove-account.container.js
@@ -0,0 +1,20 @@
+import { connect } from 'react-redux'
+import ConfirmRemoveAccount from './confirm-remove-account.component'
+
+const { hideModal, removeAccount } = require('../../../actions')
+
+const mapStateToProps = state => {
+ return {
+ identity: state.appState.modal.modalState.props.identity,
+ network: state.metamask.network,
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ hideModal: () => dispatch(hideModal()),
+ removeAccount: (address) => dispatch(removeAccount(address)),
+ }
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(ConfirmRemoveAccount)
diff --git a/ui/app/components/modals/confirm-remove-account/index.js b/ui/app/components/modals/confirm-remove-account/index.js
new file mode 100644
index 000000000..9763fbe05
--- /dev/null
+++ b/ui/app/components/modals/confirm-remove-account/index.js
@@ -0,0 +1,2 @@
+import ConfirmRemoveAccount from './confirm-remove-account.container'
+module.exports = ConfirmRemoveAccount
diff --git a/ui/app/components/modals/index.scss b/ui/app/components/modals/index.scss
index 160911c10..e198cca44 100644
--- a/ui/app/components/modals/index.scss
+++ b/ui/app/components/modals/index.scss
@@ -20,6 +20,58 @@
font-size: .875rem;
}
+ &__account {
+ border: 1px solid #b7b7b7;
+ border-radius: 4px;
+ padding: 10px;
+ display: flex;
+ margin-top: 10px;
+ margin-bottom: 20px;
+ width: 100%;
+
+ &__identicon {
+ margin-right: 10px;
+ }
+
+ &__name,
+ &__address {
+ margin-right: 10px;
+ font-size: 14px;
+ }
+
+ &__name {
+ width: 100px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ &__label {
+ font-size: 11px;
+ display: block;
+ color: #9b9b9b;
+ }
+
+ &__link {
+ margin-top: 14px;
+
+ img {
+ width: 15px;
+ height: 15px;
+ }
+ }
+
+ @media screen and (max-width: 575px) {
+ &__name {
+ width: 90px;
+ }
+ }
+ }
+
+ &__link {
+ color: #2f9ae0;
+ }
+
&__content {
overflow-y: auto;
flex: 1;
diff --git a/ui/app/components/modals/modal.js b/ui/app/components/modals/modal.js
index 973438b6b..f59825ed1 100644
--- a/ui/app/components/modals/modal.js
+++ b/ui/app/components/modals/modal.js
@@ -20,6 +20,7 @@ const HideTokenConfirmationModal = require('./hide-token-confirmation-modal')
const CustomizeGasModal = require('../customize-gas-modal')
const NotifcationModal = require('./notification-modal')
const ConfirmResetAccount = require('./confirm-reset-account')
+const ConfirmRemoveAccount = require('./confirm-remove-account')
const TransactionConfirmed = require('./transaction-confirmed')
const WelcomeBeta = require('./welcome-beta')
const Notification = require('./notification')
@@ -243,6 +244,19 @@ const MODALS = {
},
},
+ CONFIRM_REMOVE_ACCOUNT: {
+ contents: h(ConfirmRemoveAccount),
+ mobileModalStyle: {
+ ...modalContainerMobileStyle,
+ },
+ laptopModalStyle: {
+ ...modalContainerLaptopStyle,
+ },
+ contentStyle: {
+ borderRadius: '8px',
+ },
+ },
+
NEW_ACCOUNT: {
contents: [
h(NewAccountModal, {}, []),
diff --git a/ui/app/components/pages/create-account/connect-hardware/account-list.js b/ui/app/components/pages/create-account/connect-hardware/account-list.js
new file mode 100644
index 000000000..c722d1f55
--- /dev/null
+++ b/ui/app/components/pages/create-account/connect-hardware/account-list.js
@@ -0,0 +1,143 @@
+const { Component } = require('react')
+const PropTypes = require('prop-types')
+const h = require('react-hyperscript')
+const genAccountLink = require('../../../../../lib/account-link.js')
+
+class AccountList extends Component {
+ constructor (props, context) {
+ super(props)
+ }
+
+ renderHeader () {
+ return (
+ h('div.hw-connect', [
+ h('h3.hw-connect__title', {}, this.context.t('selectAnAccount')),
+ h('p.hw-connect__msg', {}, this.context.t('selectAnAccountHelp')),
+ ])
+ )
+ }
+
+ renderAccounts () {
+ return h('div.hw-account-list', [
+ this.props.accounts.map((a, i) => {
+
+ return h('div.hw-account-list__item', { key: a.address }, [
+ h('div.hw-account-list__item__radio', [
+ h('input', {
+ type: 'radio',
+ name: 'selectedAccount',
+ id: `address-${i}`,
+ value: a.index,
+ onChange: (e) => this.props.onAccountChange(e.target.value),
+ checked: this.props.selectedAccount === a.index.toString(),
+ }),
+ h(
+ 'label.hw-account-list__item__label',
+ {
+ htmlFor: `address-${i}`,
+ },
+ [
+ h('span.hw-account-list__item__index', a.index + 1),
+ `${a.address.slice(0, 4)}...${a.address.slice(-4)}`,
+ h('span.hw-account-list__item__balance', `${a.balance}`),
+ ]),
+ ]),
+ h(
+ 'a.hw-account-list__item__link',
+ {
+ href: genAccountLink(a.address, this.props.network),
+ target: '_blank',
+ title: this.context.t('etherscanView'),
+ },
+ h('img', { src: 'images/popout.svg' })
+ ),
+ ])
+ }),
+ ])
+ }
+
+ renderPagination () {
+ return h('div.hw-list-pagination', [
+ h(
+ 'button.hw-list-pagination__button',
+ {
+ onClick: () => this.props.getPage(-1),
+ },
+ `< ${this.context.t('prev')}`
+ ),
+
+ h(
+ 'button.hw-list-pagination__button',
+ {
+ onClick: () => this.props.getPage(1),
+ },
+ `${this.context.t('next')} >`
+ ),
+ ])
+ }
+
+ renderButtons () {
+ const disabled = this.props.selectedAccount === null
+ const buttonProps = {}
+ if (disabled) {
+ buttonProps.disabled = true
+ }
+
+ return h('div.new-account-connect-form__buttons', {}, [
+ h(
+ 'button.btn-default.btn--large.new-account-connect-form__button',
+ {
+ onClick: this.props.onCancel.bind(this),
+ },
+ [this.context.t('cancel')]
+ ),
+
+ h(
+ `button.btn-primary.btn--large.new-account-connect-form__button.unlock ${disabled ? '.btn-primary--disabled' : ''}`,
+ {
+ onClick: this.props.onUnlockAccount.bind(this),
+ ...buttonProps,
+ },
+ [this.context.t('unlock')]
+ ),
+ ])
+ }
+
+ renderForgetDevice () {
+ return h('div.hw-forget-device-container', {}, [
+ h('a', {
+ onClick: this.props.onForgetDevice.bind(this),
+ }, this.context.t('forgetDevice')),
+ ])
+ }
+
+ render () {
+ return h('div.new-account-connect-form.account-list', {}, [
+ this.renderHeader(),
+ this.renderAccounts(),
+ this.renderPagination(),
+ this.renderButtons(),
+ this.renderForgetDevice(),
+ ])
+ }
+
+}
+
+
+AccountList.propTypes = {
+ accounts: PropTypes.array.isRequired,
+ onAccountChange: PropTypes.func.isRequired,
+ onForgetDevice: PropTypes.func.isRequired,
+ getPage: PropTypes.func.isRequired,
+ network: PropTypes.string,
+ selectedAccount: PropTypes.string,
+ history: PropTypes.object,
+ onUnlockAccount: PropTypes.func,
+ onCancel: PropTypes.func,
+}
+
+AccountList.contextTypes = {
+ t: PropTypes.func,
+}
+
+module.exports = AccountList
diff --git a/ui/app/components/pages/create-account/connect-hardware/connect-screen.js b/ui/app/components/pages/create-account/connect-hardware/connect-screen.js
new file mode 100644
index 000000000..cb2b86595
--- /dev/null
+++ b/ui/app/components/pages/create-account/connect-hardware/connect-screen.js
@@ -0,0 +1,149 @@
+const { Component } = require('react')
+const PropTypes = require('prop-types')
+const h = require('react-hyperscript')
+
+class ConnectScreen extends Component {
+ constructor (props, context) {
+ super(props)
+ }
+
+ renderUnsupportedBrowser () {
+ return (
+ h('div.new-account-connect-form.unsupported-browser', {}, [
+ h('div.hw-connect', [
+ h('h3.hw-connect__title', {}, this.context.t('browserNotSupported')),
+ h('p.hw-connect__msg', {}, this.context.t('chromeRequiredForTrezor')),
+ ]),
+ h(
+ 'button.btn-primary.btn--large',
+ {
+ onClick: () => global.platform.openWindow({
+ url: 'https://google.com/chrome',
+ }),
+ },
+ this.context.t('downloadGoogleChrome')
+ ),
+ ])
+ )
+ }
+
+ renderHeader () {
+ return (
+ h('div.hw-connect__header', {}, [
+ h('h3.hw-connect__header__title', {}, this.context.t(`hardwareSupport`)),
+ h('p.hw-connect__header__msg', {}, this.context.t(`hardwareSupportMsg`)),
+ ])
+ )
+ }
+
+ renderTrezorAffiliateLink () {
+ return h('div.hw-connect__get-trezor', {}, [
+ h('p.hw-connect__get-trezor__msg', {}, this.context.t(`dontHaveATrezorWallet`)),
+ h('a.hw-connect__get-trezor__link', {
+ href: 'https://shop.trezor.io/?a=metamask',
+ target: '_blank',
+ }, this.context.t('orderOneHere')),
+ ])
+ }
+
+ renderConnectToTrezorButton () {
+ return h(
+ 'button.btn-primary.btn--large',
+ { onClick: this.props.connectToTrezor.bind(this) },
+ this.props.btnText
+ )
+ }
+
+ scrollToTutorial = (e) => {
+ if (this.referenceNode) this.referenceNode.scrollIntoView({behavior: 'smooth'})
+ }
+
+ renderLearnMore () {
+ return (
+ h('p.hw-connect__learn-more', {
+ onClick: this.scrollToTutorial,
+ }, [
+ this.context.t('learnMore'),
+ h('img.hw-connect__learn-more__arrow', { src: 'images/caret-right.svg'}),
+ ])
+ )
+ }
+
+ renderTutorialSteps () {
+ const steps = [
+ {
+ asset: 'hardware-wallet-step-1',
+ dimensions: {width: '225px', height: '75px'},
+ },
+ {
+ asset: 'hardware-wallet-step-2',
+ dimensions: {width: '300px', height: '100px'},
+ },
+ {
+ asset: 'hardware-wallet-step-3',
+ dimensions: {width: '120px', height: '90px'},
+ },
+ ]
+
+ return h('.hw-tutorial', {
+ ref: node => { this.referenceNode = node },
+ },
+ steps.map((step, i) => (
+ h('div.hw-connect', {}, [
+ h('h3.hw-connect__title', {}, this.context.t(`step${i + 1}HardwareWallet`)),
+ h('p.hw-connect__msg', {}, this.context.t(`step${i + 1}HardwareWalletMsg`)),
+ h('img.hw-connect__step-asset', { src: `images/${step.asset}.svg`, ...step.dimensions }),
+ ])
+ ))
+ )
+ }
+
+ renderFooter () {
+ return (
+ h('div.hw-connect__footer', {}, [
+ h('h3.hw-connect__footer__title', {}, this.context.t(`readyToConnect`)),
+ this.renderConnectToTrezorButton(),
+ h('p.hw-connect__footer__msg', {}, [
+ this.context.t(`havingTroubleConnecting`),
+ h('a.hw-connect__footer__link', {
+ href: 'https://support.metamask.io/',
+ target: '_blank',
+ }, this.context.t('getHelp')),
+ ]),
+ ])
+ )
+ }
+
+ renderConnectScreen () {
+ return (
+ h('div.new-account-connect-form', {}, [
+ this.renderHeader(),
+ this.renderTrezorAffiliateLink(),
+ this.renderConnectToTrezorButton(),
+ this.renderLearnMore(),
+ this.renderTutorialSteps(),
+ this.renderFooter(),
+ ])
+ )
+ }
+
+ render () {
+ if (this.props.browserSupported) {
+ return this.renderConnectScreen()
+ }
+ return this.renderUnsupportedBrowser()
+ }
+}
+
+ConnectScreen.propTypes = {
+ connectToTrezor: PropTypes.func.isRequired,
+ btnText: PropTypes.string.isRequired,
+ browserSupported: PropTypes.bool.isRequired,
+}
+
+ConnectScreen.contextTypes = {
+ t: PropTypes.func,
+}
+
+module.exports = ConnectScreen
+
diff --git a/ui/app/components/pages/create-account/connect-hardware/index.js b/ui/app/components/pages/create-account/connect-hardware/index.js
new file mode 100644
index 000000000..cc3761c04
--- /dev/null
+++ b/ui/app/components/pages/create-account/connect-hardware/index.js
@@ -0,0 +1,234 @@
+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 ConnectScreen = require('./connect-screen')
+const AccountList = require('./account-list')
+const { DEFAULT_ROUTE } = require('../../../../routes')
+const { formatBalance } = require('../../../../util')
+
+class ConnectHardwareForm extends Component {
+ constructor (props, context) {
+ super(props)
+ this.state = {
+ error: null,
+ btnText: context.t('connectToTrezor'),
+ selectedAccount: null,
+ accounts: [],
+ browserSupported: true,
+ }
+ }
+
+ componentWillReceiveProps (nextProps) {
+ const { accounts } = nextProps
+ const newAccounts = this.state.accounts.map(a => {
+ const normalizedAddress = a.address.toLowerCase()
+ const balanceValue = accounts[normalizedAddress] && accounts[normalizedAddress].balance || null
+ a.balance = balanceValue ? formatBalance(balanceValue, 6) : '...'
+ return a
+ })
+ this.setState({accounts: newAccounts})
+ }
+
+
+ async componentDidMount () {
+ const unlocked = await this.props.checkHardwareStatus('trezor')
+ if (unlocked) {
+ this.getPage(0)
+ }
+ }
+
+ connectToTrezor = () => {
+ if (this.state.accounts.length) {
+ return null
+ }
+ this.setState({ btnText: this.context.t('connecting')})
+ this.getPage(0)
+ }
+
+ onAccountChange = (account) => {
+ this.setState({selectedAccount: account.toString(), error: null})
+ }
+
+ showTemporaryAlert () {
+ this.props.showAlert(this.context.t('hardwareWalletConnected'))
+ // Autohide the alert after 5 seconds
+ setTimeout(_ => {
+ this.props.hideAlert()
+ }, 5000)
+ }
+
+ getPage = (page) => {
+ this.props
+ .connectHardware('trezor', page)
+ .then(accounts => {
+ if (accounts.length) {
+
+ // If we just loaded the accounts for the first time
+ // show the global alert
+ if (this.state.accounts.length === 0) {
+ this.showTemporaryAlert()
+ }
+
+ const newState = {}
+ // Default to the first account
+ if (this.state.selectedAccount === null) {
+ accounts.forEach((a, i) => {
+ if (a.address.toLowerCase() === this.props.address) {
+ newState.selectedAccount = a.index.toString()
+ }
+ })
+ // If the page doesn't contain the selected account, let's deselect it
+ } else if (!accounts.filter(a => a.index.toString() === this.state.selectedAccount).length) {
+ newState.selectedAccount = null
+ }
+
+
+ // Map accounts with balances
+ newState.accounts = accounts.map(account => {
+ const normalizedAddress = account.address.toLowerCase()
+ const balanceValue = this.props.accounts[normalizedAddress] && this.props.accounts[normalizedAddress].balance || null
+ account.balance = balanceValue ? formatBalance(balanceValue, 6) : '...'
+ return account
+ })
+
+ this.setState(newState)
+ }
+ })
+ .catch(e => {
+ if (e === 'Window blocked') {
+ this.setState({ browserSupported: false })
+ }
+ this.setState({ btnText: this.context.t('connectToTrezor') })
+ })
+ }
+
+ onForgetDevice = () => {
+ this.props.forgetDevice('trezor')
+ .then(_ => {
+ this.setState({
+ error: null,
+ btnText: this.context.t('connectToTrezor'),
+ selectedAccount: null,
+ accounts: [],
+ })
+ }).catch(e => {
+ this.setState({ error: e.toString() })
+ })
+ }
+
+ onUnlockAccount = () => {
+
+ if (this.state.selectedAccount === null) {
+ this.setState({ error: this.context.t('accountSelectionRequired') })
+ }
+
+ this.props.unlockTrezorAccount(this.state.selectedAccount)
+ .then(_ => {
+ this.props.history.push(DEFAULT_ROUTE)
+ }).catch(e => {
+ this.setState({ error: e.toString() })
+ })
+ }
+
+ onCancel = () => {
+ this.props.history.push(DEFAULT_ROUTE)
+ }
+
+ renderError () {
+ return this.state.error
+ ? h('span.error', { style: { marginBottom: 40 } }, this.state.error)
+ : null
+ }
+
+ renderContent () {
+ if (!this.state.accounts.length) {
+ return h(ConnectScreen, {
+ connectToTrezor: this.connectToTrezor,
+ btnText: this.state.btnText,
+ browserSupported: this.state.browserSupported,
+ })
+ }
+
+ return h(AccountList, {
+ accounts: this.state.accounts,
+ selectedAccount: this.state.selectedAccount,
+ onAccountChange: this.onAccountChange,
+ network: this.props.network,
+ getPage: this.getPage,
+ history: this.props.history,
+ onUnlockAccount: this.onUnlockAccount,
+ onForgetDevice: this.onForgetDevice,
+ onCancel: this.onCancel,
+ })
+ }
+
+ render () {
+ return h('div', [
+ this.renderError(),
+ this.renderContent(),
+ ])
+ }
+}
+
+ConnectHardwareForm.propTypes = {
+ hideModal: PropTypes.func,
+ showImportPage: PropTypes.func,
+ showConnectPage: PropTypes.func,
+ connectHardware: PropTypes.func,
+ checkHardwareStatus: PropTypes.func,
+ forgetDevice: PropTypes.func,
+ showAlert: PropTypes.func,
+ hideAlert: PropTypes.func,
+ unlockTrezorAccount: PropTypes.func,
+ numberOfExistingAccounts: PropTypes.number,
+ history: PropTypes.object,
+ t: PropTypes.func,
+ network: PropTypes.string,
+ accounts: PropTypes.object,
+ address: PropTypes.string,
+}
+
+const mapStateToProps = state => {
+ const {
+ metamask: { network, selectedAddress, identities = {}, accounts = [] },
+ } = state
+ const numberOfExistingAccounts = Object.keys(identities).length
+
+ return {
+ network,
+ accounts,
+ address: selectedAddress,
+ numberOfExistingAccounts,
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ connectHardware: (deviceName, page) => {
+ return dispatch(actions.connectHardware(deviceName, page))
+ },
+ checkHardwareStatus: (deviceName) => {
+ return dispatch(actions.checkHardwareStatus(deviceName))
+ },
+ forgetDevice: (deviceName) => {
+ return dispatch(actions.forgetDevice(deviceName))
+ },
+ unlockTrezorAccount: index => {
+ return dispatch(actions.unlockTrezorAccount(index))
+ },
+ showImportPage: () => dispatch(actions.showImportPage()),
+ showConnectPage: () => dispatch(actions.showConnectPage()),
+ showAlert: (msg) => dispatch(actions.showAlert(msg)),
+ hideAlert: () => dispatch(actions.hideAlert()),
+ }
+}
+
+ConnectHardwareForm.contextTypes = {
+ t: PropTypes.func,
+}
+
+module.exports = connect(mapStateToProps, mapDispatchToProps)(
+ ConnectHardwareForm
+)
diff --git a/ui/app/components/pages/create-account/index.js b/ui/app/components/pages/create-account/index.js
index 5681e43a9..d3de1ea01 100644
--- a/ui/app/components/pages/create-account/index.js
+++ b/ui/app/components/pages/create-account/index.js
@@ -8,7 +8,12 @@ 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')
+const ConnectHardwareForm = require('./connect-hardware')
+const {
+ NEW_ACCOUNT_ROUTE,
+ IMPORT_ACCOUNT_ROUTE,
+ CONNECT_HARDWARE_ROUTE,
+} = require('../../../routes')
class CreateAccountPage extends Component {
renderTabs () {
@@ -36,6 +41,19 @@ class CreateAccountPage extends Component {
}, [
this.context.t('import'),
]),
+ h(
+ 'div.new-account__tabs__tab',
+ {
+ className: classnames('new-account__tabs__tab', {
+ 'new-account__tabs__selected': matchPath(location.pathname, {
+ path: CONNECT_HARDWARE_ROUTE,
+ exact: true,
+ }),
+ }),
+ onClick: () => history.push(CONNECT_HARDWARE_ROUTE),
+ },
+ this.context.t('connect')
+ ),
])
}
@@ -57,6 +75,11 @@ class CreateAccountPage extends Component {
path: IMPORT_ACCOUNT_ROUTE,
component: NewAccountImportForm,
}),
+ h(Route, {
+ exact: true,
+ path: CONNECT_HARDWARE_ROUTE,
+ component: ConnectHardwareForm,
+ }),
]),
]),
])
diff --git a/ui/app/components/pages/create-account/new-account.js b/ui/app/components/pages/create-account/new-account.js
index 9c94990e0..402b8f03b 100644
--- a/ui/app/components/pages/create-account/new-account.js
+++ b/ui/app/components/pages/create-account/new-account.js
@@ -62,6 +62,7 @@ class NewAccountCreateForm extends Component {
NewAccountCreateForm.propTypes = {
hideModal: PropTypes.func,
showImportPage: PropTypes.func,
+ showConnectPage: PropTypes.func,
createAccount: PropTypes.func,
numberOfExistingAccounts: PropTypes.number,
history: PropTypes.object,
@@ -92,6 +93,7 @@ const mapDispatchToProps = dispatch => {
})
},
showImportPage: () => dispatch(actions.showImportPage()),
+ showConnectPage: () => dispatch(actions.showConnectPage()),
}
}