From c6d30a01bc8beb56e435f36483d3195bdde74373 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Thu, 1 Nov 2018 17:45:10 +0300 Subject: [PATCH] hw accounts list page --- .../connect-hardware/account-list.js | 200 ++++++++++++ .../connect-hardware/connect-screen.js | 135 ++++++++ .../app/components/connect-hardware/index.js | 290 ++++++++++++++++++ old-ui/app/css/hw.css | 262 ++++++++++++++++ 4 files changed, 887 insertions(+) create mode 100644 old-ui/app/components/connect-hardware/account-list.js create mode 100644 old-ui/app/components/connect-hardware/connect-screen.js create mode 100644 old-ui/app/components/connect-hardware/index.js create mode 100644 old-ui/app/css/hw.css diff --git a/old-ui/app/components/connect-hardware/account-list.js b/old-ui/app/components/connect-hardware/account-list.js new file mode 100644 index 000000000..c70809442 --- /dev/null +++ b/old-ui/app/components/connect-hardware/account-list.js @@ -0,0 +1,200 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import ethNetProps from 'eth-net-props' +import { default as Select } from 'react-select' +import Button from '../../../../ui/app/components/button' + +class AccountList extends Component { + constructor (props, context) { + super(props) + } + + getHdPaths () { + return [ + { + label: `Ledger Live`, + value: `m/44'/60'/0'/0/0`, + }, + { + label: `Legacy (MEW / MyCrypto)`, + value: `m/44'/60'/0'`, + }, + ] + } + + goToNextPage = () => { + // If we have < 5 accounts, it's restricted by BIP-44 + if (this.props.accounts.length === 5) { + this.props.getPage(this.props.device, 1, this.props.selectedPath) + } else { + this.props.onAccountRestriction() + } + } + + goToPreviousPage = () => { + this.props.getPage(this.props.device, -1, this.props.selectedPath) + } + + renderHdPathSelector () { + const { onPathChange, selectedPath } = this.props + + const options = this.getHdPaths() + return ( +
+

this.context.t('selectHdPath')

+

this.context.t('selectPathHelp')

+
+ this.props.onAccountChange(e.target.value)} + checked={this.props.selectedAccount === a.index.toString()} + /> + +
+ + +
+ ) + }) + + return ( +
{rows}
+ ) + } + + renderPagination () { + return ( +
+ + +
+ ) + } + + renderButtons () { + const disabled = this.props.selectedAccount === null + const buttonProps = {} + if (disabled) { + buttonProps.disabled = true + } + + return ( +
+ + +
+ ) + } + + renderForgetDevice () { + return ( +
+ {this.context.t('forgetDevice')} +
+ ) + } + + render () { + return ( +
+ {this.renderHeader()} + {this.renderAccounts()} + {this.renderPagination()} + {this.renderButtons()} + {this.renderForgetDevice()} +
+ ) + } + +} + + +AccountList.propTypes = { + onPathChange: PropTypes.func.isRequired, + selectedPath: PropTypes.string.isRequired, + device: PropTypes.string.isRequired, + accounts: PropTypes.array.isRequired, + onAccountChange: PropTypes.func.isRequired, + onForgetDevice: PropTypes.func.isRequired, + getPage: PropTypes.func.isRequired, + network: PropTypes.string, + selectedAccount: PropTypes.string, + history: PropTypes.object, + onUnlockAccount: PropTypes.func, + onCancel: PropTypes.func, + onAccountRestriction: PropTypes.func, +} + +AccountList.contextTypes = { + t: PropTypes.func, +} + +module.exports = AccountList diff --git a/old-ui/app/components/connect-hardware/connect-screen.js b/old-ui/app/components/connect-hardware/connect-screen.js new file mode 100644 index 000000000..b92c50136 --- /dev/null +++ b/old-ui/app/components/connect-hardware/connect-screen.js @@ -0,0 +1,135 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import Button from '../../../../ui/app/components/button' + +class ConnectScreen extends Component { + constructor (props, context) { + super(props) + this.state = { + selectedDevice: null, + } + } + + connect = () => { + if (this.state.selectedDevice) { + this.props.connectToHardwareWallet(this.state.selectedDevice) + } + return null + } + + renderConnectToTrezorButton () { + return ( + + ) + } + + renderConnectToLedgerButton () { + return ( + + ) + } + + renderButtons () { + return ( +
+
+ {this.renderConnectToLedgerButton()} + {this.renderConnectToTrezorButton()} +
+ +
+ ) + } + + renderUnsupportedBrowser () { + return ( +
+
+

Your Browser is not supported...

+

You need to use MetaMask on Google Chrome in order to connect to your Hardware Wallet.

+
+ +
+ ) + } + + renderHeader () { + return ( +
+

{`Select a hardware wallet you'd like to use with MetaMask`}

+
+ ) + } + + getAffiliateLinks () { + const links = { + trezor: `Trezor`, + ledger: `Ledger`, + } + + const text = 'Order a Trezor or Ledger and keep your funds in cold storage' + const response = text.replace('Trezor', links.trezor).replace('Ledger', links.ledger) + + return ( +
+ ) + } + + renderTrezorAffiliateLink () { + return ( +
+

Don’t have a hardware wallet?

+ {this.getAffiliateLinks()} +
+ ) + } + + + scrollToTutorial = (e) => { + if (this.referenceNode) this.referenceNode.scrollIntoView({behavior: 'smooth'}) + } + + renderConnectScreen () { + return ( +
+ {this.renderHeader()} + {this.renderButtons()} + {this.renderTrezorAffiliateLink()} +
+ ) + } + + render () { + if (this.props.browserSupported) { + return this.renderConnectScreen() + } + return this.renderUnsupportedBrowser() + } +} + +ConnectScreen.propTypes = { + connectToHardwareWallet: PropTypes.func.isRequired, + browserSupported: PropTypes.bool.isRequired, +} + +module.exports = ConnectScreen + diff --git a/old-ui/app/components/connect-hardware/index.js b/old-ui/app/components/connect-hardware/index.js new file mode 100644 index 000000000..58f7a06cc --- /dev/null +++ b/old-ui/app/components/connect-hardware/index.js @@ -0,0 +1,290 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import { connect } from 'react-redux' +import actions from '../../../../ui/app/actions' +import ConnectScreen from './connect-screen' +import AccountList from './account-list' +import { DEFAULT_ROUTE } from '../../../../ui/app/routes' +import { formatBalance } from '../../../../ui/app/util' +import { getPlatform } from '../../../../app/scripts/lib/util' +import { PLATFORM_FIREFOX } from '../../../../app/scripts/lib/enums' + +class ConnectHardwareForm extends Component { + constructor (props, context) { + super(props) + this.state = { + error: null, + selectedAccount: null, + accounts: [], + browserSupported: true, + unlocked: false, + device: null, + } + } + + componentWillReceiveProps (nextProps) { + const { accounts } = nextProps + const newAccounts = this.state.accounts.map(a => { + const normalizedAddress = a.address.toLowerCase() + const balanceValue = accounts[normalizedAddress] && accounts[normalizedAddress].balance || null + a.balance = balanceValue ? formatBalance(balanceValue, 6) : '...' + return a + }) + this.setState({accounts: newAccounts}) + } + + + componentDidMount () { + this.checkIfUnlocked() + } + + async checkIfUnlocked () { + ['trezor', 'ledger'].forEach(async device => { + const unlocked = await this.props.checkHardwareStatus(device, this.props.defaultHdPaths[device]) + if (unlocked) { + this.setState({unlocked: true}) + this.getPage(device, 0, this.props.defaultHdPaths[device]) + } + }) + } + + connectToHardwareWallet = (device) => { + // None of the hardware wallets are supported + // At least for now + if (getPlatform() === PLATFORM_FIREFOX) { + this.setState({ browserSupported: false, error: null}) + return null + } + + if (this.state.accounts.length) { + return null + } + + // Default values + this.getPage(device, 0, this.props.defaultHdPaths[device]) + } + + onPathChange = (path) => { + this.props.setHardwareWalletDefaultHdPath({device: this.state.device, path}) + this.getPage(this.state.device, 0, path) + } + + onAccountChange = (account) => { + this.setState({selectedAccount: account.toString(), error: null}) + } + + onAccountRestriction = () => { + this.setState({error: 'You need to make use your last account before you can add a new one.' }) + } + + showTemporaryAlert () { + this.props.showAlert('Hardware wallet connected') + // Autohide the alert after 5 seconds + setTimeout(_ => { + this.props.hideAlert() + }, 5000) + } + + getPage = (device, page, hdPath) => { + this.props + .connectHardware(device, page, hdPath) + .then(accounts => { + if (accounts.length) { + + // If we just loaded the accounts for the first time + // (device previously locked) show the global alert + if (this.state.accounts.length === 0 && !this.state.unlocked) { + this.showTemporaryAlert() + } + + const newState = { unlocked: true, device, error: null } + // Default to the first account + if (this.state.selectedAccount === null) { + accounts.forEach((a, i) => { + if (a.address.toLowerCase() === this.props.address) { + newState.selectedAccount = a.index.toString() + } + }) + // If the page doesn't contain the selected account, let's deselect it + } else if (!accounts.filter(a => a.index.toString() === this.state.selectedAccount).length) { + newState.selectedAccount = null + } + + + // Map accounts with balances + newState.accounts = accounts.map(account => { + const normalizedAddress = account.address.toLowerCase() + const balanceValue = this.props.accounts[normalizedAddress] && this.props.accounts[normalizedAddress].balance || null + account.balance = balanceValue ? formatBalance(balanceValue, 6) : '...' + return account + }) + + this.setState(newState) + } + }) + .catch(e => { + if (e === 'Window blocked') { + this.setState({ browserSupported: false, error: null}) + } else if (e !== 'Window closed') { + this.setState({ error: e.toString() }) + } + }) + } + + onForgetDevice = (device) => { + this.props.forgetDevice(device) + .then(_ => { + this.setState({ + error: null, + selectedAccount: null, + accounts: [], + unlocked: false, + }) + }).catch(e => { + this.setState({ error: e.toString() }) + }) + } + + onUnlockAccount = (device) => { + + if (this.state.selectedAccount === null) { + this.setState({ error: 'You need to select an account!' }) + } + + this.props.unlockHardwareWalletAccount(this.state.selectedAccount, device) + .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 + ? {this.state.error} + : null + } + + renderContent () { + if (!this.state.accounts.length) { + return ( + + ) + } + + return ( + + ) + } + + render () { + return ( +
+
+ this.props.goHome() } + style={{ + position: 'absolute', + left: '30px', + }}/> +

Connect to hardware wallet

+
+
+
+ {this.renderError()} + {this.renderContent()} +
+
+
+ ) + } +} + +ConnectHardwareForm.propTypes = { + hideModal: PropTypes.func, + showImportPage: PropTypes.func, + showConnectPage: PropTypes.func, + connectHardware: PropTypes.func, + checkHardwareStatus: PropTypes.func, + forgetDevice: PropTypes.func, + showAlert: PropTypes.func, + hideAlert: PropTypes.func, + unlockHardwareWalletAccount: PropTypes.func, + setHardwareWalletDefaultHdPath: PropTypes.func, + goHome: PropTypes.func, + numberOfExistingAccounts: PropTypes.number, + history: PropTypes.object, + t: PropTypes.func, + network: PropTypes.string, + accounts: PropTypes.object, + address: PropTypes.string, + defaultHdPaths: PropTypes.object, +} + +const mapStateToProps = state => { + const { + metamask: { network, selectedAddress, identities = {}, accounts = [] }, + } = state + const numberOfExistingAccounts = Object.keys(identities).length + const { + appState: { defaultHdPaths }, + } = state + + return { + network, + accounts, + address: selectedAddress, + numberOfExistingAccounts, + defaultHdPaths, + } +} + +const mapDispatchToProps = dispatch => { + return { + goHome: () => { + dispatch(actions.goHome()) + }, + setHardwareWalletDefaultHdPath: ({device, path}) => { + return dispatch(actions.setHardwareWalletDefaultHdPath({device, path})) + }, + connectHardware: (deviceName, page, hdPath) => { + return dispatch(actions.connectHardware(deviceName, page, hdPath)) + }, + checkHardwareStatus: (deviceName, hdPath) => { + return dispatch(actions.checkHardwareStatus(deviceName, hdPath)) + }, + forgetDevice: (deviceName) => { + return dispatch(actions.forgetDevice(deviceName)) + }, + unlockHardwareWalletAccount: (index, deviceName, hdPath) => { + return dispatch(actions.unlockHardwareWalletAccount(index, deviceName, hdPath)) + }, + showImportPage: () => dispatch(actions.showImportPage()), + showConnectPage: () => dispatch(actions.showConnectPage()), + showAlert: (msg) => dispatch(actions.showAlert(msg)), + hideAlert: () => dispatch(actions.hideAlert()), + } +} + +module.exports = connect(mapStateToProps, mapDispatchToProps)(ConnectHardwareForm) diff --git a/old-ui/app/css/hw.css b/old-ui/app/css/hw.css new file mode 100644 index 000000000..49f935edc --- /dev/null +++ b/old-ui/app/css/hw.css @@ -0,0 +1,262 @@ +.hw-tutorial { + width: 375px; + border-top: 1px solid #D2D8DD; + border-bottom: 1px solid #D2D8DD; + overflow: visible; + display: block; + padding: 15px 30px; +} + +.hw-connect { + width: 100%; +} +.hw-connect__header__title { + margin-top: 5px; + margin-bottom: 15px; + font-size: 22px; +} +.hw-connect__header__msg { + font-size: 14px; + color: #9b9b9b; + margin-top: 10px; + margin-bottom: 20px; +} +.hw-connect__btn-wrapper { + flex: 1; + flex-direction: row; + display: flex; +} +.hw-connect__connect-btn { + color: #fff; + border: none; + width: 100%; + min-height: 54px; + font-weight: 300; + font-size: 14px; + margin-bottom: 20px; + margin-top: 20px; + border-radius: 5px; + display: flex; + flex: 1; + justify-content: center; + text-transform: uppercase; +} +.hw-connect__connect-btn.disabled { + cursor: not-allowed; + opacity: .5; +} +.hw-connect__btn { + background: #fbfbfb; + border: 1px solid #e5e5e5; + height: 100px; + width: 150px; + flex: 1; + display: flex; + align-items: center; + justify-content: center; + border-radius: 5px; +} +.hw-connect__btn__img { + width: 95px; +} +.hw-connect__btn.selected { + border: 2px solid #60DB97; + width: 149px; +} +.hw-connect__btn:first-child { + margin-right: 15px; +} +.hw-connect__btn:last-child { + +} +.hw-connect__hdPath { + display: flex; + flex-direction: row; + margin-top: 15px; + margin-bottom: 30px; + font-size: 14px; +} +.hw-connect__hdPath__title { + display: flex; + margin-top: 10px; + margin-right: 15px; +} +.hw-connect__hdPath__select { + display: flex; + flex: 1; +} +.hw-connect__learn-more { + margin-top: 15px; + font-size: 14px; + color: #5B5D67; + line-height: 19px; + text-align: center; + cursor: pointer; +} +.hw-connect__learn-more__arrow { + -webkit-transform: rotate(90deg); + transform: rotate(90deg); + display: block; + text-align: center; + height: 30px; + margin: 0px auto 10px; +} +.hw-connect__title { + padding-top: 10px; + font-weight: 400; + font-size: 18px; +} +.hw-connect__unlock-title { + padding-top: 10px; + font-weight: 400; + font-size: 22px; + margin-bottom: 15px; +} +.hw-connect__msg { + font-size: 14px; + color: #9b9b9b; + margin-top: 10px; + margin-bottom: 15px; +} +.hw-connect__link { + color: #2f9ae0; +} +.hw-connect__footer__title { + padding-top: 15px; + padding-bottom: 12px; + font-weight: 400; + font-size: 18px; + text-align: center; +} +.hw-connect__footer__msg { + font-size: 14px; + color: #9b9b9b; + margin-top: 12px; + margin-bottom: 27px; + width: 100%; + display: block; + margin-left: 20px; +} +.hw-connect__footer__link { + color: #2f9ae0; + margin-left: 5px; +} +.hw-connect__get-hw { + width: 100%; + padding-bottom: 10px; + padding-top: 10px; +} +.hw-connect__get-hw__msg { + font-size: 14px; + color: #9b9b9b; +} +.hw-connect__get-hw__link { + font-size: 14px; + text-align: center; + color: #60DB97; + cursor: pointer; +} +.hw-connect__step-asset { + margin: 0px auto 20px; + display: flex; +} + +.hw-account-list { +display: flex; +flex: 1; +flex-flow: column; +width: 100%; +} +.hw-account-list__title_wrapper { + display: flex; + flex-direction: row; + flex: 1; +} +.hw-account-list__title { + margin-bottom: 23px; + align-self: flex-start; + color: #5d5d5d; + font-family: Roboto; + font-size: 16px; + line-height: 21px; + font-weight: bold; + display: flex; + flex: 1; +} +.hw-account-list__device { + margin-bottom: 23px; + align-self: flex-end; + color: #5d5d5d; + font-family: Roboto; + font-size: 16px; + line-height: 21px; + font-weight: normal; + display: flex; +} +.hw-account-list__item { + font-size: 15px; + flex-direction: row; + display: flex; + padding-left: 10px; + padding-right: 10px; +} +.hw-account-list__item:nth-of-type(even) { + background-color: #fbfbfb; +} +.hw-account-list__item:nth-of-type(odd) { + background: rgba(0, 0, 0, 0.03); +} +.hw-account-list__item:hover { + background-color: rgba(0, 0, 0, 0.06); +} +.hw-account-list__item__index { + display: flex; + width: 24px; +} +.hw-account-list__item__radio { + display: flex; + flex: 1; +} +.hw-account-list__item__radio input { + padding: 10px; + margin-top: 13px; +} +.hw-account-list__item__label { + display: flex; + flex: 1; + padding-left: 10px; + padding-top: 10px; + padding-bottom: 10px; +} +.hw-account-list__item__balance { + display: flex; + flex: 1; + justify-content: center; +} +.hw-account-list__item__link { + display: flex; + margin-top: 13px; +} +.hw-account-list__item__link img { + width: 15px; + height: 15px; +} +.hw-list-pagination { +display: flex; +align-self: flex-end; +margin-top: 10px; +} +.hw-list-pagination__button { + height: 19px; + display: flex; + color: #33a4e7; + font-size: 14px; + line-height: 19px; + border: none; + min-width: 46px; + margin-right: 0px; + margin-left: 16px; + padding: 0px; + text-transform: uppercase; + font-family: Roboto; +} \ No newline at end of file