diff options
Diffstat (limited to 'ui')
-rw-r--r-- | ui/app/actions.js | 107 | ||||
-rw-r--r-- | ui/app/components/account-menu/index.js | 69 | ||||
-rw-r--r-- | ui/app/components/modals/confirm-remove-account/confirm-remove-account.component.js | 93 | ||||
-rw-r--r-- | ui/app/components/modals/confirm-remove-account/confirm-remove-account.container.js | 20 | ||||
-rw-r--r-- | ui/app/components/modals/confirm-remove-account/index.js | 2 | ||||
-rw-r--r-- | ui/app/components/modals/index.scss | 52 | ||||
-rw-r--r-- | ui/app/components/modals/modal.js | 14 | ||||
-rw-r--r-- | ui/app/components/pages/create-account/connect-hardware/account-list.js | 143 | ||||
-rw-r--r-- | ui/app/components/pages/create-account/connect-hardware/connect-screen.js | 78 | ||||
-rw-r--r-- | ui/app/components/pages/create-account/connect-hardware/index.js | 215 | ||||
-rw-r--r-- | ui/app/components/pages/create-account/index.js | 25 | ||||
-rw-r--r-- | ui/app/components/pages/create-account/new-account.js | 2 | ||||
-rw-r--r-- | ui/app/css/itcss/components/account-menu.scss | 18 | ||||
-rw-r--r-- | ui/app/css/itcss/components/new-account.scss | 200 | ||||
-rw-r--r-- | ui/app/routes.js | 2 |
15 files changed, 1029 insertions, 11 deletions
diff --git a/ui/app/actions.js b/ui/app/actions.js index 0aeb802f8..3da56ebc2 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -78,9 +78,14 @@ var actions = { addNewKeyring, importNewAccount, addNewAccount, + connectHardware, + checkHardwareStatus, + forgetDevice, + unlockTrezorAccount, NEW_ACCOUNT_SCREEN: 'NEW_ACCOUNT_SCREEN', navigateToNewAccountScreen, resetAccount, + removeAccount, showNewVaultSeed: showNewVaultSeed, showInfoPage: showInfoPage, CLOSE_WELCOME_SCREEN: 'CLOSE_WELCOME_SCREEN', @@ -533,6 +538,26 @@ function resetAccount () { } } +function removeAccount (address) { + return dispatch => { + dispatch(actions.showLoadingIndication()) + + return new Promise((resolve, reject) => { + background.removeAccount(address, (err, account) => { + dispatch(actions.hideLoadingIndication()) + if (err) { + dispatch(actions.displayWarning(err.message)) + return reject(err) + } + + log.info('Account removed: ' + account) + dispatch(actions.showAccountsPage()) + resolve() + }) + }) + } +} + function addNewKeyring (type, opts) { return (dispatch) => { dispatch(actions.showLoadingIndication()) @@ -599,6 +624,88 @@ function addNewAccount () { } } +function checkHardwareStatus (deviceName) { + log.debug(`background.checkHardwareStatus`, deviceName) + return (dispatch, getState) => { + dispatch(actions.showLoadingIndication()) + return new Promise((resolve, reject) => { + background.checkHardwareStatus(deviceName, (err, unlocked) => { + if (err) { + log.error(err) + dispatch(actions.displayWarning(err.message)) + return reject(err) + } + + dispatch(actions.hideLoadingIndication()) + + forceUpdateMetamaskState(dispatch) + return resolve(unlocked) + }) + }) + } +} + +function forgetDevice (deviceName) { + log.debug(`background.forgetDevice`, deviceName) + return (dispatch, getState) => { + dispatch(actions.showLoadingIndication()) + return new Promise((resolve, reject) => { + background.forgetDevice(deviceName, (err, response) => { + if (err) { + log.error(err) + dispatch(actions.displayWarning(err.message)) + return reject(err) + } + + dispatch(actions.hideLoadingIndication()) + + forceUpdateMetamaskState(dispatch) + return resolve() + }) + }) + } +} + +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..fc48b60f3 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('connectHardware'), + }), 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/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..06102e16f --- /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.btn-primary.hw-list-pagination__button', + { + onClick: () => this.props.getPage(-1), + }, + `< ${this.context.t('prev')}` + ), + + h( + 'button.btn-primary.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 ${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', {}, [ + 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..7fb36b511 --- /dev/null +++ b/ui/app/components/pages/create-account/connect-hardware/connect-screen.js @@ -0,0 +1,78 @@ +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', {}, [ + 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') + ), + ]) + ) + } + + renderConnectScreen () { + return ( + h('div.new-account-connect-form', {}, [ + h('div.hw-connect', [ + h('h3.hw-connect__title', {}, this.context.t('trezorHardwareWallet')), + h('p.hw-connect__msg', {}, this.context.t('connectToTrezorHelp')), + h('p.hw-connect__msg', {}, [ + this.context.t('connectToTrezorTrouble'), + h('a.hw-connect__link', { + href: 'https://support.metamask.io/', + target: '_blank', + }, ` ${this.context.t('learnMore')}`), + ]), + ]), + h( + 'button.btn-primary.btn--large', + { onClick: this.props.connectToTrezor.bind(this) }, + this.props.btnText + ), + h('div.hw-connect__get-trezor', {}, [ + h('a', { + href: 'https://shop.trezor.io/?a=metamask', + target: '_blank', + }, this.context.t('getYourTrezor')), + ]), + ]) + ) + } + + 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..da42ddead --- /dev/null +++ b/ui/app/components/pages/create-account/connect-hardware/index.js @@ -0,0 +1,215 @@ +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}) + } + + getPage = (page) => { + this.props + .connectHardware('trezor', page) + .then(accounts => { + if (accounts.length) { + 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, + 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()), + } +} + +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()), } } diff --git a/ui/app/css/itcss/components/account-menu.scss b/ui/app/css/itcss/components/account-menu.scss index 96fba890c..b14753e23 100644 --- a/ui/app/css/itcss/components/account-menu.scss +++ b/ui/app/css/itcss/components/account-menu.scss @@ -72,6 +72,7 @@ background-color: $dusty-gray; color: $black; font-weight: normal; + letter-spacing: .5px; } } @@ -84,6 +85,23 @@ @media screen and (max-width: 575px) { padding: 12px 14px; } + + .remove-account-icon { + width: 15px; + margin-left: 10px; + height: 15px; + } + + &:hover { + .remove-account-icon::after { + content: '\00D7'; + font-size: 25px; + color: $white; + cursor: pointer; + position: absolute; + margin-top: -5px; + } + } } &__account-info { diff --git a/ui/app/css/itcss/components/new-account.scss b/ui/app/css/itcss/components/new-account.scss index 293579058..0eacf4615 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,201 @@ } } +.hw-connect { + &__title { + padding-top: 10px; + font-weight: 500; + font-size: 18px; + } + + &__msg { + font-size: 14px; + color: #9b9b9b; + margin-top: 10px; + margin-bottom: 20px; + } + + &__link { + color: #2f9ae0; + } + + &__get-trezor { + display: flex; + flex-flow: column; + align-items: center; + padding: 30px 30px 0; + width: 305px; + + a { + font-size: 14px; + text-align: center; + color: #2f9ae0; + cursor: pointer; + } + } +} + +.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; + width: 24px; + } + + &__item__radio { + display: flex; + flex: 1; + + input { + margin-right: 9px; + } + } + + &__item__label { + margin-left: 10px; + display: flex; + flex: 1; + } + + &__item__balance { + display: flex; + flex: 1; + justify-content: center; + } + + &__item__link { + display: flex; + margin-top: 3px; + } + + &__item__link img { + width: 15px; + height: 15px; + } +} + +.hw-list-pagination { + display: flex; + align-self: flex-end; + margin-top: 10px; + + &__button { + height: 19px; + display: flex; + width: 47px; + color: #33a4e7; + font-size: 14px; + line-height: 19px; + border: none; + min-width: 46px; + margin-right: 0px; + margin-left: 16px; + padding: 0px; + } +} + +.new-account-connect-form { + display: flex; + flex-flow: column; + align-items: center; + padding: 15px 30px 0; + + &__buttons { + margin-top: 39px; + display: flex; + width: 100%; + justify-content: space-between; + } + + &__button { + width: 150px; + min-width: initial; + } + + &__button.btn-primary { + background-color: #259DE5; + } + + &__button.btn-primary { + background-color: #259DE5; + color: #FFFFFF; + } + + &__button.btn-primary--disabled { + cursor: not-allowed; + opacity: .5; + } + + + + +} + +.hw-forget-device-container { + display: flex; + flex-flow: column; + align-items: center; + padding: 30px 30px 0; + + a { + color: #2f9ae0; + font-size: 14px; + cursor: pointer; + } +} + .new-account-create-form { display: flex; flex-flow: column; diff --git a/ui/app/routes.js b/ui/app/routes.js index 7ac606b1a..ec0cc9027 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 NOTICE_ROUTE = '/notice' const WELCOME_ROUTE = '/welcome' @@ -41,6 +42,7 @@ module.exports = { CONFIRM_ADD_TOKEN_ROUTE, NEW_ACCOUNT_ROUTE, IMPORT_ACCOUNT_ROUTE, + CONNECT_HARDWARE_ROUTE, SEND_ROUTE, NOTICE_ROUTE, WELCOME_ROUTE, |