hw accounts list page

This commit is contained in:
Victor Baranov 2018-11-01 17:45:10 +03:00
parent db9701d45c
commit c6d30a01bc
4 changed files with 887 additions and 0 deletions

View File

@ -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 (
<div>
<h3 className='hw-connect__hdPath__title'>this.context.t('selectHdPath')</h3>
<p className='hw-connect__msg'>this.context.t('selectPathHelp')</p>
<div className='hw-connect__hdPath'>
<Select
className='hw-connect__hdPath__select'
name='hd-path-select'
clearable={false}
value={selectedPath}
options
onChange={(opt) => {
onPathChange(opt.value)
}}
/>
</div>
</div>
)
}
capitalizeDevice (device) {
return device.slice(0, 1).toUpperCase() + device.slice(1)
}
renderHeader () {
const { device } = this.props
return (
<div className='hw-connect'>
<h3 className='hw-connect'>
<h3 className='hw-connect__unlock-title'>`${this.context.t('unlock')} ${this.capitalizeDevice(device)}`</h3>
{device.toLowerCase() === 'ledger' ? this.renderHdPathSelector() : null}
<h3 className='hw-connect__hdPath__title'>{this.context.t('selectAnAccount')}</h3>
<p className='hw-connect__msg'>{this.context.t('selectAnAccountHelp')}</p>
</h3>
</div>
)
}
renderAccounts () {
const rows = []
this.props.accounts.map((a, i) => {
rows.push(
<div className='hw-account-list__item' key={a.address}>
<div className='hw-account-list__item__radio'>
<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()}
/>
<label className='hw-account-list__item__label' htmlFor={`address-${i}`}>
<span className='hw-account-list__item__index'>{a.index + 1}</span>
{`${a.address.slice(0, 4)}...${a.address.slice(-4)}`}
<span className='hw-account-list__item__balance'>{`${a.balance}`}</span>
</label>
</div>
<a
className='hw-account-list__item__link'
href={ethNetProps.explorerLinks.getExplorerAccountLinkFor(a.address, this.props.network)}
target='_blank'
title={this.context.t('etherscanView')}
/>
<img src='images/popout.svg' />
</div>
)
})
return (
<div className='hw-account-list'>{rows}</div>
)
}
renderPagination () {
return (
<div className='hw-list-pagination'>
<button
className='hw-list-pagination__button'
onClick={this.goToPreviousPage}
>{`< ${this.context.t('prev')}`}</button>
<button
className='hw-list-pagination__button'
onClick={this.goToNextPage}
>{`${this.context.t('next')} >`}</button>
</div>
)
}
renderButtons () {
const disabled = this.props.selectedAccount === null
const buttonProps = {}
if (disabled) {
buttonProps.disabled = true
}
return (
<div className='new-account-connect-form__buttons'>
<Button
type='default'
large={true}
className='new-account-connect-form__button'
onClick={this.props.onCancel.bind(this)}
>{this.context.t('cancel')}</Button>
<Button
type='primary'
large={true}
className='new-account-connect-form__button unlock'
disabled={disabled}
onClick={this.props.onUnlockAccount.bind(this, this.props.device)}
>{this.context.t('unlock')}</Button>
</div>
)
}
renderForgetDevice () {
return (
<div className='hw-forget-device-container'>
<a onClick={this.props.onForgetDevice.bind(this, this.props.device)}>{this.context.t('forgetDevice')}</a>
</div>
)
}
render () {
return (
<div className='new-account-connect-form.account-list'>
{this.renderHeader()}
{this.renderAccounts()}
{this.renderPagination()}
{this.renderButtons()}
{this.renderForgetDevice()}
</div>
)
}
}
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

View File

@ -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 (
<button
className={`hw-connect__btn${this.state.selectedDevice === 'trezor' ? ' selected' : ''}`}
onClick={_ => this.setState({selectedDevice: 'trezor'})}
>
<img className="hw-connect__btn__img" src="images/trezor-logo.svg"/>
</button>
)
}
renderConnectToLedgerButton () {
return (
<button
className={`hw-connect__btn${this.state.selectedDevice === 'ledger' ? ' selected' : ''}`}
onClick={_ => this.setState({selectedDevice: 'ledger'})}
>
<img className="hw-connect__btn__img" src="images/ledger-logo.svg"/>
</button>
)
}
renderButtons () {
return (
<div>
<div className="hw-connect__btn-wrapper">
{this.renderConnectToLedgerButton()}
{this.renderConnectToTrezorButton()}
</div>
<button
className={`hw-connect__connect-btn${!this.state.selectedDevice ? ' disabled' : ''}`}
onClick={this.connect}
>Connect</button>
</div>
)
}
renderUnsupportedBrowser () {
return (
<div className="new-account-connect-form unsupported-browser">
<div className="hw-connect">
<h3 className="hw-connect__title">Your Browser is not supported...</h3>
<p className="hw-connect__msg">You need to use MetaMask on Google Chrome in order to connect to your Hardware Wallet.</p>
</div>
<Button
type="primary"
large={true}
onClick={() => global.platform.openWindow({
url: 'https://google.com/chrome',
})}
>Download Google Chrome</Button>
</div>
)
}
renderHeader () {
return (
<div className="hw-connect__header">
<p className="hw-connect__header__msg">{`Select a hardware wallet you'd like to use with MetaMask`}</p>
</div>
)
}
getAffiliateLinks () {
const links = {
trezor: `<a class='hw-connect__get-hw__link' href='https://shop.trezor.io/?a=metamask' target='_blank'>Trezor</a>`,
ledger: `<a class='hw-connect__get-hw__link' href='https://www.ledger.com/products/ledger-nano-s?r=17c4991a03fa&tracker=MY_TRACKER' target='_blank'>Ledger</a>`,
}
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 (
<div className="hw-connect__get-hw__msg" dangerouslySetInnerHTML={{ __html: response }} />
)
}
renderTrezorAffiliateLink () {
return (
<div className="hw-connect__get-hw">
<p className="hw-connect__get-hw__msg">Dont have a hardware wallet?</p>
{this.getAffiliateLinks()}
</div>
)
}
scrollToTutorial = (e) => {
if (this.referenceNode) this.referenceNode.scrollIntoView({behavior: 'smooth'})
}
renderConnectScreen () {
return (
<div className="new-account-connect-form">
{this.renderHeader()}
{this.renderButtons()}
{this.renderTrezorAffiliateLink()}
</div>
)
}
render () {
if (this.props.browserSupported) {
return this.renderConnectScreen()
}
return this.renderUnsupportedBrowser()
}
}
ConnectScreen.propTypes = {
connectToHardwareWallet: PropTypes.func.isRequired,
browserSupported: PropTypes.bool.isRequired,
}
module.exports = ConnectScreen

View File

@ -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
? <span className="error" style={{ display: 'block', textAlign: 'center' }}>{this.state.error}</span>
: null
}
renderContent () {
if (!this.state.accounts.length) {
return (
<ConnectScreen
connectToHardwareWallet={this.connectToHardwareWallet}
browserSupported={this.state.browserSupported}
/>
)
}
return (
<AccountList
onPathChange={this.onPathChange}
selectedPath={this.props.defaultHdPaths[this.state.device]}
device={this.state.device}
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}
onAccountRestriction={this.onAccountRestriction}
/>
)
}
render () {
return (
<div style={{width: '100%'}}>
<div className="section-title flex-row flex-center">
<i className="fa fa-arrow-left fa-lg cursor-pointer"
onClick={() => this.props.goHome() }
style={{
position: 'absolute',
left: '30px',
}}/>
<h2>Connect to hardware wallet</h2>
</div>
<div style={{overflowY: 'auto', height: '482px'}}>
<div style={{padding: '0 30px'}}>
{this.renderError()}
{this.renderContent()}
</div>
</div>
</div>
)
}
}
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)

262
old-ui/app/css/hw.css Normal file
View File

@ -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;
}