From 8c4d58aa4508e3d54c3f69847347e78d09c63b97 Mon Sep 17 00:00:00 2001 From: Bruno Date: Sun, 10 Jun 2018 03:52:32 -0400 Subject: initial trezor support --- ui/app/actions.js | 42 ++++ ui/app/components/account-menu/index.js | 9 + .../pages/create-account/connect-hardware.js | 234 +++++++++++++++++++++ ui/app/components/pages/create-account/index.js | 25 ++- .../components/pages/create-account/new-account.js | 2 + ui/app/css/itcss/components/new-account.scss | 105 ++++++++- ui/app/routes.js | 2 + 7 files changed, 413 insertions(+), 6 deletions(-) create mode 100644 ui/app/components/pages/create-account/connect-hardware.js (limited to 'ui') diff --git a/ui/app/actions.js b/ui/app/actions.js index 1edf692b6..758696203 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -79,6 +79,8 @@ var actions = { addNewKeyring, importNewAccount, addNewAccount, + connectHardware, + unlockTrezorAccount, NEW_ACCOUNT_SCREEN: 'NEW_ACCOUNT_SCREEN', navigateToNewAccountScreen, resetAccount, @@ -596,6 +598,46 @@ function addNewAccount () { } } +function connectHardware (deviceName, page) { + log.debug(`background.connectHardware`, deviceName, page) + return (dispatch, getState) => { + dispatch(actions.showLoadingIndication()) + return new Promise((resolve, reject) => { + background.connectHardware(deviceName, page, (err, accounts) => { + if (err) { + log.error(err) + dispatch(actions.displayWarning(err.message)) + return reject(err) + } + + dispatch(actions.hideLoadingIndication()) + + forceUpdateMetamaskState(dispatch) + return resolve(accounts) + }) + }) + } +} + +function unlockTrezorAccount (index) { + log.debug(`background.unlockTrezorAccount`, index) + return (dispatch, getState) => { + dispatch(actions.showLoadingIndication()) + return new Promise((resolve, reject) => { + background.unlockTrezorAccount(index, (err, accounts) => { + if (err) { + log.error(err) + dispatch(actions.displayWarning(err.message)) + return reject(err) + } + + dispatch(actions.hideLoadingIndication()) + return resolve() + }) + }) + } +} + function showInfoPage () { return { type: actions.SHOW_INFO_PAGE, diff --git a/ui/app/components/account-menu/index.js b/ui/app/components/account-menu/index.js index f34631ca8..629669dac 100644 --- a/ui/app/components/account-menu/index.js +++ b/ui/app/components/account-menu/index.js @@ -14,6 +14,7 @@ const { INFO_ROUTE, NEW_ACCOUNT_ROUTE, IMPORT_ACCOUNT_ROUTE, + CONNECT_HARDWARE_ROUTE, DEFAULT_ROUTE, } = require('../../routes') @@ -106,6 +107,14 @@ 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() + history.push(CONNECT_HARDWARE_ROUTE) + }, + icon: h('img.account-menu__item-icon', { src: 'images/connect-icon.svg' }), + text: this.context.t('connectHardware'), + }), h(Divider), h(Item, { onClick: () => { diff --git a/ui/app/components/pages/create-account/connect-hardware.js b/ui/app/components/pages/create-account/connect-hardware.js new file mode 100644 index 000000000..37b7414b3 --- /dev/null +++ b/ui/app/components/pages/create-account/connect-hardware.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 genAccountLink = require('../../../../lib/account-link.js') +const log = require('loglevel') +const { DEFAULT_ROUTE } = require('../../../routes') + +class ConnectHardwareForm extends Component { + constructor (props, context) { + super(props) + this.state = { + error: null, + response: null, + btnText: 'Connect to Trezor', // Test + selectedAccount: '', + accounts: [], + } + } + + connectToTrezor () { + if (this.state.accounts.length) { + return null + } + this.setState({ btnText: 'Connecting...' }) + this.getPage() + } + + getPage (page = 1) { + this.props + .connectHardware('trezor', page) + .then(accounts => { + if (accounts.length) { + this.setState({ accounts: accounts }) + } + }) + .catch(e => { + this.setState({ btnText: 'Connect to Trezor' }) + }) + } + + unlockAccount () { + if (this.state.selectedAccount === '') { + return Promise.reject({ error: 'You need to select an account!' }) + } + log.debug('should unlock account ', this.state.selectedAccount) + return this.props.unlockTrezorAccount(this.state.selectedAccount) + } + + handleRadioChange = e => { + log.debug('Selected account with index ', e.target.value) + + this.setState({ + selectedAccount: e.target.value, + error: null, + }) + } + + renderAccounts () { + if (!this.state.accounts.length) { + return null + } + log.debug('ACCOUNTS : ', this.state.accounts) + log.debug('SELECTED?', this.state.selectedAccount) + + return h('div.hw-account-list', [ + h('div.hw-account-list__title_wrapper', [ + h('div.hw-account-list__title', {}, ['Select an Address']), + h('div.hw-account-list__device', {}, ['Trezor - ETH']), + ]), + this.state.accounts.map((a, i) => { + return h('div.hw-account-list__item', { key: a.address }, [ + h('span.hw-account-list__item__index', a.index + 1), + h('div.hw-account-list__item__radio', [ + h('input', { + type: 'radio', + name: 'selectedAccount', + id: `address-${i}`, + value: a.index, + onChange: this.handleRadioChange, + }), + h( + 'label.hw-account-list__item__label', + { + htmlFor: `address-${i}`, + }, + `${a.address.slice(0, 4)}...${a.address.slice(-4)}` + ), + ]), + h('span.hw-account-list__item__balance', `${a.balance} ETH`), + 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 () { + if (!this.state.accounts.length) { + return null + } + return h('div.hw-list-pagination', [ + h( + 'button.btn-primary.hw-list-pagination__button', + { + onClick: () => this.getPage(-1), + }, + '< Prev' + ), + + h( + 'button.btn-primary.hw-list-pagination__button', + { + onClick: () => this.getPage(), + }, + 'Next >' + ), + ]) + } + + renderButtons () { + if (!this.state.accounts.length) { + return null + } + const { history } = this.props + + return h('div.new-account-create-form__buttons', {}, [ + h( + 'button.btn-default.btn--large.new-account-create-form__button', + { + onClick: () => history.push(DEFAULT_ROUTE), + }, + [this.context.t('cancel')] + ), + + h( + 'button.btn-primary.btn--large.new-account-create-form__button', + { + onClick: () => { + this.unlockAccount(this.state.selectedAccount) + .then(() => history.push(DEFAULT_ROUTE)) + .catch(e => { + this.setState({ error: e.error }) + }) + }, + }, + [this.context.t('unlock')] + ), + ]) + } + + renderError () { + return this.state.error + ? h('span.error', { style: { marginBottom: 40 } }, this.state.error) + : null + } + + renderConnectButton () { + return !this.state.accounts.length + ? h( + 'button.btn-primary.btn--large', + { onClick: () => this.connectToTrezor(), style: { margin: 12 } }, + this.state.btnText + ) + : null + } + + render () { + return h('div.new-account-create-form', [ + this.renderError(), + this.renderConnectButton(), + this.renderAccounts(), + this.renderPagination(), + this.renderButtons(), + ]) + } +} + +ConnectHardwareForm.propTypes = { + hideModal: PropTypes.func, + showImportPage: PropTypes.func, + showConnectPage: PropTypes.func, + connectHardware: PropTypes.func, + unlockTrezorAccount: PropTypes.func, + numberOfExistingAccounts: PropTypes.number, + history: PropTypes.object, + t: PropTypes.func, + network: PropTypes.string, +} + +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()), + connectHardware: (deviceName, page) => { + return dispatch(actions.connectHardware(deviceName, page)) + }, + unlockTrezorAccount: index => { + return dispatch(actions.unlockTrezorAccount(index)) + }, + showImportPage: () => dispatch(actions.showImportPage()), + showConnectPage: () => dispatch(actions.showConnectPage()), + } +} + +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 6e3b93742..69a4db80a 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()), } } diff --git a/ui/app/css/itcss/components/new-account.scss b/ui/app/css/itcss/components/new-account.scss index 293579058..2ecc157f1 100644 --- a/ui/app/css/itcss/components/new-account.scss +++ b/ui/app/css/itcss/components/new-account.scss @@ -28,7 +28,6 @@ &__tab { height: 54px; - width: 75px; padding: 15px 10px; color: $dusty-gray; font-family: Roboto; @@ -38,10 +37,6 @@ cursor: pointer; } - &__tab:first-of-type { - margin-right: 20px; - } - &__tab:hover { color: $black; border-bottom: none; @@ -158,6 +153,106 @@ } } +.hw-account-list { + display: flex; + flex: 1; + flex-flow: column; + width: 100%; + + &__title_wrapper { + display: flex; + flex-direction: row; + flex: 1; + } + + &__title { + margin-bottom: 23px; + align-self: flex-start; + color: $scorpion; + font-family: Roboto; + font-size: 16px; + line-height: 21px; + font-weight: bold; + display: flex; + flex: 1; + } + + &__device { + margin-bottom: 23px; + align-self: flex-end; + color: $scorpion; + font-family: Roboto; + font-size: 16px; + line-height: 21px; + font-weight: normal; + display: flex; + } + + &__item { + font-size: 15px; + flex-direction: row; + display: flex; + padding: 10px; + } + + &__item:nth-of-type(even) { + background-color: #fbfbfb; + } + + &__item:nth-of-type(odd) { + background: rgba(0, 0, 0, 0.03); + } + + &__item:hover { + background-color: rgba(0, 0, 0, 0.06); + } + + &__item__index { + display: flex; + margin-right: 20px; + } + + &__item__radio { + display: flex; + flex: 1; + } + + &__item__label { + margin-left: 10px; + display: flex; + flex: 1; + } + + &__item__balance { + display: flex; + flex: 1; + justify-content: center; + } + + &__item__link { + display: flex; + } + + &__item__link img { + width: 15px; + height: 15px; + } +} + +.hw-list-pagination { + display: flex; + align-self: flex-end; + margin-top: 10px; + + &__button { + height: 25px; + flex: initial; + min-width: 90px; + font-size: 12px; + } +} + + .new-account-create-form { display: flex; flex-flow: column; diff --git a/ui/app/routes.js b/ui/app/routes.js index 0ff3f644d..04f71165f 100644 --- a/ui/app/routes.js +++ b/ui/app/routes.js @@ -9,6 +9,7 @@ const ADD_TOKEN_ROUTE = '/add-token' const CONFIRM_ADD_TOKEN_ROUTE = '/confirm-add-token' const NEW_ACCOUNT_ROUTE = '/new-account' const IMPORT_ACCOUNT_ROUTE = '/new-account/import' +const CONNECT_HARDWARE_ROUTE = '/new-account/connect' const SEND_ROUTE = '/send' const CONFIRM_TRANSACTION_ROUTE = '/confirm-transaction' const SIGNATURE_REQUEST_ROUTE = '/confirm-transaction/signature-request' @@ -35,6 +36,7 @@ module.exports = { CONFIRM_ADD_TOKEN_ROUTE, NEW_ACCOUNT_ROUTE, IMPORT_ACCOUNT_ROUTE, + CONNECT_HARDWARE_ROUTE, SEND_ROUTE, CONFIRM_TRANSACTION_ROUTE, NOTICE_ROUTE, -- cgit v1.2.3