Merge branch 'develop' into double-fired-events-fix

This commit is contained in:
Victor Baranov 2018-11-21 15:07:28 +03:00 committed by GitHub
commit 8f0e5877b9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 1030 additions and 10 deletions

View File

@ -1 +1,21 @@
<svg height="22" viewBox="0 0 22 22" width="22" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><mask id="a" fill="#fff"><path d="m-.00035 0h11.00025v10.9997h-11.00025z" fill="none"/></mask><g fill="#4a4a4a" fill-rule="evenodd"><path d="m10.9229.6177c-.102-.244-.296-.439-.541-.541-.122-.05-.251-.077-.382-.077h-6c-.552 0-1 .448-1 1 0 .553.448 1 1 1h3.586l-7.293 7.293c-.391.391-.391 1.024 0 1.414.195.196.451.293.707.293s.512-.097.707-.293l7.293-7.293v3.586c0 .553.448 1 1 1s1-.447 1-1v-6c0-.13-.026-.259-.077-.382" mask="url(#a)" transform="translate(11)"/><path d="m19 10c-.552 0-1 .448-1 1v8c0 .551-.449 1-1 1h-14c-.551 0-1-.449-1-1v-14c0-.551.449-1 1-1h8c.552 0 1-.448 1-1s-.448-1-1-1h-8c-1.654 0-3 1.346-3 3v14c0 1.654 1.346 3 3 3h14c1.654 0 3-1.346 3-3v-8c0-.552-.448-1-1-1"/></g></svg>
<?xml version="1.0" encoding="UTF-8"?>
<svg width="22px" height="22px" viewBox="0 0 22 22" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 48.2 (47327) - http://www.bohemiancoding.com/sketch -->
<title>popout</title>
<desc>Created with Sketch.</desc>
<defs>
<polygon id="path-1" points="-0.00035 0 10.9999 0 10.9999 10.9997 -0.00035 10.9997"></polygon>
</defs>
<g id="MetaMascara-Mobile---structured-TOKEN" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" transform="translate(-327.000000, -96.000000)">
<g id="popout" transform="translate(327.000000, 96.000000)">
<g id="Group-3" transform="translate(11.000000, 0.000000)">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<g id="Clip-2"></g>
<path d="M10.9229,0.6177 C10.8209,0.3737 10.6269,0.1787 10.3819,0.0767 C10.2599,0.0267 10.1309,-0.0003 9.9999,-0.0003 L3.9999,-0.0003 C3.4479,-0.0003 2.9999,0.4477 2.9999,0.9997 C2.9999,1.5527 3.4479,1.9997 3.9999,1.9997 L7.5859,1.9997 L0.2929,9.2927 C-0.0981,9.6837 -0.0981,10.3167 0.2929,10.7067 C0.4879,10.9027 0.7439,10.9997 0.9999,10.9997 C1.2559,10.9997 1.5119,10.9027 1.7069,10.7067 L8.9999,3.4137 L8.9999,6.9997 C8.9999,7.5527 9.4479,7.9997 9.9999,7.9997 C10.5519,7.9997 10.9999,7.5527 10.9999,6.9997 L10.9999,0.9997 C10.9999,0.8697 10.9739,0.7407 10.9229,0.6177" id="Fill-1" fill="#4A4A4A" mask="url(#mask-2)"></path>
</g>
<path d="M19,10 C18.448,10 18,10.448 18,11 L18,19 C18,19.551 17.551,20 17,20 L3,20 C2.449,20 2,19.551 2,19 L2,5 C2,4.449 2.449,4 3,4 L11,4 C11.552,4 12,3.552 12,3 C12,2.448 11.552,2 11,2 L3,2 C1.346,2 0,3.346 0,5 L0,19 C0,20.654 1.346,22 3,22 L17,22 C18.654,22 20,20.654 20,19 L20,11 C20,10.448 19.552,10 19,10" id="Fill-4" fill="#4A4A4A"></path>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 823 B

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -352,7 +352,7 @@ class TransactionStateManager extends EventEmitter {
setTxStatusFailed (txId, err) {
const txMeta = this.getTx(txId)
txMeta.err = {
message: err.toString(),
message: (err ? (err.message || err.error || err) : '').toString(),
rpc: err.value,
stack: err.stack,
}

View File

@ -139,6 +139,19 @@ function removeListeners (listeners, emitter) {
})
}
/**
* Capitalizes first letter in the first word of the message
* @param {string} msg The input message
* returns {string} The message with capitalized first letter of the first word
**/
function capitalizeFirstLetter (msg) {
if (!msg) {
return ''
}
return msg.charAt(0).toUpperCase() + msg.slice(1)
}
module.exports = {
removeListeners,
applyListeners,
@ -149,4 +162,5 @@ module.exports = {
hexToBn,
bnToHex,
BnMultiplyByFraction,
capitalizeFirstLetter,
}

View File

@ -821,7 +821,11 @@ module.exports = class MetamaskController extends EventEmitter {
this.accountTracker.removeAccount([address])
// Remove account from the keyring
await this.keyringController.removeAccount(address)
try {
await this.keyringController.removeAccount(address)
} catch (e) {
log.error(e)
}
return address
}

View File

@ -1,5 +1,6 @@
const extension = require('extensionizer')
const explorerLinks = require('eth-net-props').explorerLinks
const { capitalizeFirstLetter } = require('../lib/util')
class ExtensionPlatform {
@ -94,7 +95,7 @@ class ExtensionPlatform {
const nonce = parseInt(txMeta.txParams.nonce, 16)
const title = 'Failed transaction'
const message = `Transaction ${nonce} failed! ${txMeta.err.message}`
const message = `Transaction ${nonce} failed! ${capitalizeFirstLetter(txMeta.err.message)}`
this._showNotification(title, message)
}

View File

@ -28,6 +28,7 @@ const ConfirmAddTokenScreen = require('./components/confirm-add-token')
const RemoveTokenScreen = require('./remove-token')
const AddSuggestedTokenScreen = require('./add-suggested-token')
const Import = require('./accounts/import')
import ConnectHardwareForm from './components/connect-hardware/index.js'
const InfoScreen = require('./info')
const AppBar = require('./components/app-bar')
const Loading = require('./components/loading')
@ -265,6 +266,10 @@ App.prototype.renderPrimary = function () {
log.debug('rendering import screen')
return h(Import, {key: 'import-menu'})
case 'hardware-wallets-menu':
log.debug('rendering hardware wallet menu screen')
return h(ConnectHardwareForm, {key: 'hardware-wallets-menu'})
case 'reveal-seed-conf':
log.debug('rendering reveal seed confirmation screen')
return h(RevealSeedConfirmation, {key: 'reveal-seed-conf'})

View File

@ -27,6 +27,9 @@ class AccountDropdowns extends Component {
return accountOrder.map((address, index) => {
const identity = identities[address]
if (!identity) {
return null
}
const isSelected = identity.address === selected
const simpleAddress = identity.address.substring(2).toLowerCase()
@ -172,6 +175,25 @@ class AccountDropdowns extends Component {
}, 'Import Account'),
]
),
h(
DropdownMenuItem,
{
style: {
padding: '8px 0px',
},
closeMenu: () => {},
onClick: () => actions.showConnectHWWalletPage(),
},
[
h('span', {
style: {
fontSize: '16px',
marginBottom: '5px',
color: '#60db97',
},
}, 'Connect hardware wallet'),
]
),
]
)
}
@ -322,6 +344,7 @@ const mapDispatchToProps = (dispatch) => {
showAccountDetail: (address) => dispatch(actions.showAccountDetail(address)),
addNewAccount: () => dispatch(actions.addNewAccount()),
showImportPage: () => dispatch(actions.showImportPage()),
showConnectHWWalletPage: () => dispatch(actions.showConnectHWWalletPage()),
showQrView: (selected, identity) => dispatch(actions.showQrView(selected, identity)),
showDeleteImportedAccount: (identity) => dispatch(actions.showDeleteImportedAccount(identity)),
},

View File

@ -0,0 +1,191 @@
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'
import { capitalizeFirstLetter } from '../../../../app/scripts/lib/util'
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">Select HD Path</h3>
<p className="hw-connect__msg">{`If you don't see your existing Ledger accounts below, try switching paths to "Legacy (MEW / MyCrypto)"`}</p>
<div className="hw-connect__hdPath">
<Select
className="hw-connect__hdPath__select"
name="hd-path-select"
clearable={false}
value={selectedPath}
options={options}
onChange={(opt) => {
onPathChange(opt.value)
}}
/>
</div>
</div>
)
}
renderHeader = () => {
const { device } = this.props
return (
<div className="hw-connect">
<h3 className="hw-connect">
<h3 className="hw-connect__unlock-title">{`Unlock ${capitalizeFirstLetter(device)}`}</h3>
{device.toLowerCase() === 'ledger' ? this.renderHdPathSelector() : null}
<p className="hw-connect__msg">Select the account to view in Nifty Wallet</p>
</h3>
</div>
)
}
renderAccounts = () => {
const rows = []
this.props.accounts.forEach((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}`}>
{`${a.address.slice(0, 4)}...${a.address.slice(-4)}`}
<span
className="hw-account-list__item__balance"
onClick={(event) => {
event.preventDefault()
global.platform.openWindow({
url: ethNetProps.explorerLinks.getExplorerAccountLinkFor(a.address, this.props.network),
})
}}
>{`${a.balance}`}</span>
</label>
</div>
</div>
)
})
return (
<div className="hw-account-list">{rows}</div>
)
}
renderPagination = () => {
return (
<div className="hw-list-pagination">
<button
className="hw-list-pagination__button"
onClick={this.goToNextPage}
>{`Next >`}</button>
<button
className="hw-list-pagination__button"
onClick={this.goToPreviousPage}
>{`< Prev`}</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 btn-violet"
onClick={this.props.onCancel.bind(this)}
>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)}
>Unlock</Button>
</div>
)
}
renderForgetDevice = () => {
return (
<div className="hw-forget-device-container">
<a onClick={this.props.onForgetDevice.bind(this, this.props.device)}>Forget this device</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,
}
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 Nifty Wallet 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 Nifty Wallet`}</p>
</div>
)
}
getAffiliateLinks () {
const links = {
trezor: `<a class='hw-connect__get-hw__link' href='https://shop.trezor.io/?a=niftywallet' target='_blank'>Trezor</a>`,
ledger: `<a class='hw-connect__get-hw__link' href='https://www.ledger.com/products/ledger-nano-s' 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,285 @@
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 { formatBalance } from '../../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, network } = 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, 4, undefined, network) : '...'
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, 4, undefined, this.props.network) : '...'
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.message || e.toString()) })
}
})
}
onForgetDevice = (device) => {
this.props.forgetDevice(device)
.then(_ => {
this.setState({
error: null,
selectedAccount: null,
accounts: [],
unlocked: false,
})
}).catch(e => {
this.setState({ error: (e.message || 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.goHome()
}).catch(e => {
this.setState({ error: (e.message || e.toString()) })
})
}
onCancel = () => {
this.props.goHome()
}
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}
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 = {
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,
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)

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

@ -0,0 +1,310 @@
.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: Nunito Regular;
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: Nunito Regular;
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: rgba(103, 41, 168, 0.1);
}
.hw-account-list__item:nth-of-type(odd) {
background: rgba(103, 41, 168, 0.2);
}
.hw-account-list__item:hover {
background-color: rgba(103, 41, 168, 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;
font-family: 'Nunito Bold';
cursor: pointer;
}
.hw-account-list__item__link {
display: flex;
margin-top: 13px;
}
.hw-account-list__item__link img {
width: 15px;
height: 15px;
}
.hw-list-pagination {
margin-top: 10px;
}
.hw-list-pagination__button {
height: 19px;
float: right;
color: #6729a8;
font-size: 14px;
line-height: 19px;
border: none;
min-width: 46px;
padding: 0px;
font-family: Nunito Regular;
background: transparent;
}
.hw-forget-device-container {
display: flex;
flex-flow: column;
align-items: center;
padding: 22px;
}
.hw-forget-device-container a {
color: #60db97;
font-size: 14px;
cursor: pointer;
}
.new-account-connect-form {
flex-flow: column;
align-items: center;
}
.new-account-connect-form.unsupported-browser {
height: 210px;
}
.new-account-connect-form.account-list {
height: auto;
padding-left: 20px;
padding-right: 20px;
}
.new-account-connect-form__buttons {
margin-top: 39px;
display: flex;
width: 100%;
justify-content: space-between;
}
.new-account-connect-form__button {
width: 150px;
min-width: initial;
}
.new-account-connect-form .btn-primary {
background-color: #259DE5;
color: #FFFFFF;
border: none;
width: 100%;
min-height: 54px;
font-weight: 300;
font-size: 14px;
margin-bottom: 20px;
}
.new-account-connect-form__button.unlock {
width: 50%;
}
.new-account-connect-form__button.btn-primary--disabled {
cursor: not-allowed;
opacity: .5;
}

View File

@ -40,7 +40,7 @@ RevealSeedConfirmation.prototype.render = function () {
h('.page-subtitle', 'Reveal Seed Words'),
]),
h('.div', {
h('div', {
style: {
display: 'flex',
flexDirection: 'column',

View File

@ -8,6 +8,7 @@ var cssFiles = {
'reset.css': fs.readFileSync(path.join(__dirname, '/app/css/reset.css'), 'utf8'),
'lib.css': fs.readFileSync(path.join(__dirname, '/app/css/lib.css'), 'utf8'),
'search-token.css': fs.readFileSync(path.join(__dirname, '/app/css/search-token.css'), 'utf8'),
'hw.css': fs.readFileSync(path.join(__dirname, '/app/css/hw.css'), 'utf8'),
'confirm-add-token.css': fs.readFileSync(path.join(__dirname, '/app/css/confirm-add-token.css'), 'utf8'),
'page-container.css': fs.readFileSync(path.join(__dirname, '/app/css/page-container.css'), 'utf8'),
'index.css': fs.readFileSync(path.join(__dirname, '/app/css/index.css'), 'utf8'),

View File

@ -113,8 +113,8 @@
"eth-block-tracker": "^4.0.3",
"eth-contract-metadata": "github:MetaMask/eth-contract-metadata#master",
"eth-ens-namehash": "^2.0.8",
"eth-hd-keyring": "^2.0.0",
"eth-json-rpc-filters": "github:poanetwork/eth-json-rpc-filters#3.0.2",
"eth-json-rpc-filters": "^3.0.1",
"eth-json-rpc-infura": "^3.0.0",
"eth-keychain-controller": "^5.0.0",
"eth-ledger-bridge-keyring": "^0.1.0",

View File

@ -1,5 +1,5 @@
const assert = require('assert')
const { sufficientBalance } = require('../../../app/scripts/lib/util')
const { sufficientBalance, capitalizeFirstLetter } = require('../../../app/scripts/lib/util')
describe('SufficientBalance', function () {
@ -39,3 +39,14 @@ describe('SufficientBalance', function () {
assert.ok(!result, 'insufficient balance found.')
})
})
describe('capitalizeFirstLetter', () => {
it('returns correct output with capitalized first letter of the first word', () => {
assert.equal('T', capitalizeFirstLetter('t'))
assert.equal('Test', capitalizeFirstLetter('test'))
assert.equal('Test with multiple words', capitalizeFirstLetter('test with multiple words'))
assert.equal('Test with multiple words', capitalizeFirstLetter('Test with multiple words'))
assert.equal('', capitalizeFirstLetter(''))
assert.equal('', capitalizeFirstLetter())
})
})

View File

@ -71,6 +71,7 @@ var actions = {
SHOW_NEW_VAULT_SEED: 'SHOW_NEW_VAULT_SEED',
SHOW_INFO_PAGE: 'SHOW_INFO_PAGE',
SHOW_IMPORT_PAGE: 'SHOW_IMPORT_PAGE',
SHOW_HARDWARE_WALLET_PAGE: 'SHOW_HARDWARE_WALLET_PAGE',
SHOW_NEW_ACCOUNT_PAGE: 'SHOW_NEW_ACCOUNT_PAGE',
SET_NEW_ACCOUNT_FORM: 'SET_NEW_ACCOUNT_FORM',
unlockMetamask: unlockMetamask,
@ -80,6 +81,7 @@ var actions = {
showRestoreVault: showRestoreVault,
showInitializeMenu: showInitializeMenu,
showImportPage,
showConnectHWWalletPage: showConnectHWWalletPage,
showNewAccountPage,
setNewAccountForm,
createNewVaultAndKeychain: createNewVaultAndKeychain,
@ -1114,8 +1116,9 @@ function sendTx (txData) {
log.debug(`actions calling background.approveTransaction`)
background.approveTransaction(txData.id, (err) => {
if (err) {
err = err.message || err.error || err
dispatch(actions.txError(err))
return log.error(err.message)
return log.error(err)
}
dispatch(actions.completedTx(txData.id))
@ -1149,6 +1152,7 @@ function updateTransaction (txData) {
background.updateTransaction(txData, (err) => {
dispatch(actions.updateTransactionParams(txData.id, txData.txParams))
if (err) {
err = err.message || err.error || err
dispatch(actions.txError(err))
dispatch(actions.goHome())
log.error(err.message)
@ -1180,9 +1184,10 @@ function updateAndApproveTx (txData) {
dispatch(actions.clearSend())
if (err) {
err = err.message || err.error || err
dispatch(actions.txError(err))
dispatch(actions.goHome())
log.error(err.message)
log.error(err)
reject(err)
}
@ -1228,7 +1233,7 @@ function updateTransactionParams (id, txParams) {
function txError (err) {
return {
type: actions.TRANSACTION_ERROR,
message: err.message,
message: (err.message || err.error || err),
}
}
@ -1451,6 +1456,12 @@ function showImportPage () {
}
}
function showConnectHWWalletPage () {
return {
type: actions.SHOW_HARDWARE_WALLET_PAGE,
}
}
function showNewAccountPage (formToSelect) {
return {
type: actions.SHOW_NEW_ACCOUNT_PAGE,

View File

@ -258,6 +258,15 @@ function reduceApp (state, action) {
warning: null,
})
case actions.SHOW_HARDWARE_WALLET_PAGE:
return extend(appState, {
currentView: {
name: 'hardware-wallets-menu',
},
transForward: true,
warning: null,
})
case actions.SHOW_NEW_ACCOUNT_PAGE:
return extend(appState, {
currentView: {