Merge branch 'master' into i3076-UseStorageLocalInstead

This commit is contained in:
Dan Finlay 2018-03-07 13:39:44 -08:00
commit d0ba2d2d94
29 changed files with 1594 additions and 184 deletions

View File

@ -3,9 +3,15 @@
## Current Master
- Fix bug that could cause MetaMask to lose all of its local data.
## 4.2.0 Tue Mar 06 2018
- Replace "Loose" wording to "Imported".
- Replace "Unlock" wording with "Log In".
- Add Imported Account disclaimer.
- Allow adding custom tokens to classic ui when balance is 0
- Allow editing of symbol and decimal info when adding custom token in new-ui
- NewUI shapeshift form can select all coins (not just BTC)
- Add most of Microsoft Edge support.
## 4.1.3 2018-2-28

View File

@ -1,7 +1,7 @@
{
"name": "MetaMask",
"short_name": "Metamask",
"version": "4.1.3",
"version": "4.2.0",
"manifest_version": 2,
"author": "https://metamask.io",
"description": "Ethereum Browser Extension",
@ -60,7 +60,7 @@
"clipboardWrite",
"http://localhost:8545/",
"https://*.infura.io/"
],
],
"web_accessible_resources": [
"scripts/inpage.js"
],
@ -69,4 +69,4 @@
"https://metamask.io/*"
]
}
}
}

View File

@ -3,7 +3,7 @@ const ObservableStore = require('obs-store')
const ethUtil = require('ethereumjs-util')
const Transaction = require('ethereumjs-tx')
const EthQuery = require('ethjs-query')
const TransactionStateManger = require('../lib/tx-state-manager')
const TransactionStateManager = require('../lib/tx-state-manager')
const TxGasUtil = require('../lib/tx-gas-utils')
const PendingTransactionTracker = require('../lib/pending-tx-tracker')
const createId = require('../lib/random-id')
@ -38,7 +38,7 @@ module.exports = class TransactionController extends EventEmitter {
this.query = new EthQuery(this.provider)
this.txGasUtil = new TxGasUtil(this.provider)
this.txStateManager = new TransactionStateManger({
this.txStateManager = new TransactionStateManager({
initState: opts.initState,
txHistoryLimit: opts.txHistoryLimit,
getNetwork: this.getNetwork.bind(this),

View File

@ -4,7 +4,7 @@ const ObservableStore = require('obs-store')
const ethUtil = require('ethereumjs-util')
const txStateHistoryHelper = require('./tx-state-history-helper')
module.exports = class TransactionStateManger extends EventEmitter {
module.exports = class TransactionStateManager extends EventEmitter {
constructor ({ initState, txHistoryLimit, getNetwork }) {
super()

View File

@ -7,12 +7,13 @@ const changelogPath = path.join(__dirname, '..', 'CHANGELOG.md')
const manifestPath = path.join(__dirname, '..', 'app', 'manifest.json')
const manifest = require('../app/manifest.json')
const versionBump = require('./version-bump')
const bumpType = normalizeType(process.argv[2])
start().catch(console.error)
readFile(changelogPath)
.then(async (changeBuffer) => {
async function start() {
const changeBuffer = await readFile(changelogPath)
const changelog = changeBuffer.toString()
const newData = await versionBump(bumpType, changelog, manifest)
@ -22,10 +23,8 @@ readFile(changelogPath)
await writeFile(changelogPath, newData.changelog)
await writeFile(manifestPath, manifestString)
return newData.version
})
.then((version) => console.log(`Bumped ${bumpType} to version ${version}`))
.catch(console.error)
console.log(`Bumped ${bumpType} to version ${newData.version}`)
}
function normalizeType (userInput) {

View File

@ -34,10 +34,7 @@ AccountImportSubview.prototype.render = function () {
const { type } = state
return (
h('div', {
style: {
},
}, [
h('div', [
h('.section-title.flex-row.flex-center', [
h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', {
onClick: (event) => {
@ -46,6 +43,27 @@ AccountImportSubview.prototype.render = function () {
}),
h('h2.page-subtitle', 'Import Accounts'),
]),
h('.error', {
style: {
display: 'inline-block',
alignItems: 'center',
padding: '5px 15px 0px 15px',
},
}, [
h('span', 'Imported accounts will not be associated with your originally created MetaMask account seedphrase. Learn more about imported accounts '),
h('span', {
style: {
color: 'rgba(247, 134, 28, 1)',
cursor: 'pointer',
textDecoration: 'underline',
},
onClick: () => {
global.platform.openWindow({
url: 'https://metamask.helpscoutdocs.com/article/17-what-are-loose-accounts',
})
},
}, 'here.'),
]),
h('div', {
style: {
padding: '10px',

View File

@ -79,7 +79,7 @@ class AccountDropdowns extends Component {
try { // Sometimes keyrings aren't loaded yet:
const type = keyring.type
const isLoose = type !== 'HD Key Tree'
return isLoose ? h('.keyring-label', 'LOOSE') : null
return isLoose ? h('.keyring-label', 'IMPORTED') : null
} catch (e) { return }
}

View File

@ -217,7 +217,7 @@ hr.horizontal-line {
background: rgba(255,0,0,0.8);
color: white;
bottom: 0px;
left: -8px;
left: -18px;
border-radius: 10px;
height: 20px;
min-width: 20px;

View File

@ -69,7 +69,7 @@ UnlockScreen.prototype.render = function () {
style: {
margin: 10,
},
}, 'Unlock'),
}, 'Log In'),
]),
h('.flex-row.flex-center.flex-grow', [

View File

@ -13,7 +13,7 @@
"dist": "npm run dist:clear && npm install && gulp dist",
"dist:clear": "rm -rf node_modules/eth-contract-metadata && rm -rf node_modules/eth-phishing-detect",
"test": "npm run lint && npm run test:coverage && npm run test:integration",
"test:unit": "METAMASK_ENV=test mocha --exit --compilers js:babel-core/register --require test/helper.js --recursive \"test/unit/**/*.js\"",
"test:unit": "METAMASK_ENV=test mocha --exit --require babel-core/register --require test/helper.js --recursive \"test/unit/**/*.js\"",
"test:single": "METAMASK_ENV=test mocha --require test/helper.js",
"test:integration": "gulp build:scss && npm run test:flat && npm run test:mascara",
"test:coverage": "nyc npm run test:unit && npm run test:coveralls-upload",
@ -223,7 +223,7 @@
"jsdom": "^11.1.0",
"jsdom-global": "^3.0.2",
"jshint-stylish": "~2.2.1",
"karma": "^1.7.1",
"karma": "^2.0.0",
"karma-chrome-launcher": "^2.2.0",
"karma-cli": "^1.0.1",
"karma-firefox-launcher": "^1.0.1",

1374
test/stub/blacklist.json Normal file

File diff suppressed because it is too large Load Diff

View File

@ -38,4 +38,4 @@ describe('blacklist controller', function () {
assert.equal(result, false)
})
})
})
})

View File

@ -1,11 +1,11 @@
const assert = require('assert')
const MessageManger = require('../../app/scripts/lib/message-manager')
const MessageManager = require('../../app/scripts/lib/message-manager')
describe('Message Manager', function () {
let messageManager
beforeEach(function () {
messageManager = new MessageManger()
messageManager = new MessageManager()
})
describe('#getMsgList', function () {

View File

@ -1,129 +1,103 @@
const assert = require('assert')
const sinon = require('sinon')
const clone = require('clone')
const nock = require('nock')
const MetaMaskController = require('../../app/scripts/metamask-controller')
const blacklistJSON = require('../stub/blacklist')
const firstTimeState = require('../../app/scripts/first-time-state')
const BN = require('ethereumjs-util').BN
const GWEI_BN = new BN('1000000000')
describe('MetaMaskController', function () {
const noop = () => {}
const metamaskController = new MetaMaskController({
showUnconfirmedMessage: noop,
unlockAccountMessage: noop,
showUnapprovedTx: noop,
platform: {},
encryptor: {
encrypt: function(password, object) {
this.object = object
return Promise.resolve()
},
decrypt: function () {
return Promise.resolve(this.object)
}
},
// initial state
initState: clone(firstTimeState),
})
let metamaskController
const sandbox = sinon.sandbox.create()
const noop = () => { }
beforeEach(function () {
// sinon allows stubbing methods that are easily verified
this.sinon = sinon.sandbox.create()
nock('https://api.infura.io')
.persist()
.get('/v2/blacklist')
.reply(200, blacklistJSON)
nock('https://api.infura.io')
.persist()
.get(/.*/)
.reply(200)
metamaskController = new MetaMaskController({
showUnapprovedTx: noop,
encryptor: {
encrypt: function (password, object) {
this.object = object
return Promise.resolve()
},
decrypt: function () {
return Promise.resolve(this.object)
},
},
initState: clone(firstTimeState),
})
sandbox.spy(metamaskController.keyringController, 'createNewVaultAndKeychain')
sandbox.spy(metamaskController.keyringController, 'createNewVaultAndRestore')
})
afterEach(function () {
// sinon requires cleanup otherwise it will overwrite context
this.sinon.restore()
nock.cleanAll()
sandbox.restore()
})
describe('Metamask Controller', function () {
assert(metamaskController)
beforeEach(function () {
sinon.spy(metamaskController.keyringController, 'createNewVaultAndKeychain')
sinon.spy(metamaskController.keyringController, 'createNewVaultAndRestore')
})
afterEach(function () {
metamaskController.keyringController.createNewVaultAndKeychain.restore()
metamaskController.keyringController.createNewVaultAndRestore.restore()
})
describe('#getGasPrice', function () {
it('gives the 50th percentile lowest accepted gas price from recentBlocksController', async function () {
const realRecentBlocksController = metamaskController.recentBlocksController
metamaskController.recentBlocksController = {
store: {
getState: () => {
return {
recentBlocks: [
{ gasPrices: [ '0x3b9aca00', '0x174876e800'] },
{ gasPrices: [ '0x3b9aca00', '0x174876e800'] },
{ gasPrices: [ '0x174876e800', '0x174876e800' ]},
{ gasPrices: [ '0x174876e800', '0x174876e800' ]},
]
}
describe('#getGasPrice', function () {
it('gives the 50th percentile lowest accepted gas price from recentBlocksController', async function () {
const realRecentBlocksController = metamaskController.recentBlocksController
metamaskController.recentBlocksController = {
store: {
getState: () => {
return {
recentBlocks: [
{ gasPrices: [ '0x3b9aca00', '0x174876e800'] },
{ gasPrices: [ '0x3b9aca00', '0x174876e800'] },
{ gasPrices: [ '0x174876e800', '0x174876e800' ]},
{ gasPrices: [ '0x174876e800', '0x174876e800' ]},
],
}
}
}
},
},
}
const gasPrice = metamaskController.getGasPrice()
assert.equal(gasPrice, '0x3b9aca00', 'accurately estimates 50th percentile accepted gas price')
metamaskController.recentBlocksController = realRecentBlocksController
})
it('gives the 1 gwei price if no blocks have been seen.', async function () {
const realRecentBlocksController = metamaskController.recentBlocksController
metamaskController.recentBlocksController = {
store: {
getState: () => {
return {
recentBlocks: []
}
}
}
}
const gasPrice = metamaskController.getGasPrice()
assert.equal(gasPrice, '0x' + GWEI_BN.toString(16), 'defaults to 1 gwei')
metamaskController.recentBlocksController = realRecentBlocksController
})
const gasPrice = metamaskController.getGasPrice()
assert.equal(gasPrice, '0x3b9aca00', 'accurately estimates 50th percentile accepted gas price')
metamaskController.recentBlocksController = realRecentBlocksController
})
})
describe('#createNewVaultAndKeychain', function () {
it('can only create new vault on keyringController once', async function () {
const selectStub = sinon.stub(metamaskController, 'selectFirstIdentity')
describe('#createNewVaultAndKeychain', function () {
it('can only create new vault on keyringController once', async function () {
const selectStub = sandbox.stub(metamaskController, 'selectFirstIdentity')
const password = 'a-fake-password'
const password = 'a-fake-password'
await metamaskController.createNewVaultAndKeychain(password)
await metamaskController.createNewVaultAndKeychain(password)
const first = await metamaskController.createNewVaultAndKeychain(password)
const second = await metamaskController.createNewVaultAndKeychain(password)
assert(metamaskController.keyringController.createNewVaultAndKeychain.calledOnce)
assert(metamaskController.keyringController.createNewVaultAndKeychain.calledOnce)
selectStub.reset()
})
selectStub.reset()
})
})
describe('#createNewVaultAndRestore', function () {
it('should be able to call newVaultAndRestore despite a mistake.', async function () {
// const selectStub = sinon.stub(metamaskController, 'selectFirstIdentity')
describe('#createNewVaultAndRestore', function () {
it('should be able to call newVaultAndRestore despite a mistake.', async function () {
const password = 'what-what-what'
const wrongSeed = 'debris dizzy just program just float decrease vacant alarm reduce speak stadiu'
const rightSeed = 'debris dizzy just program just float decrease vacant alarm reduce speak stadium'
const first = await metamaskController.createNewVaultAndRestore(password, wrongSeed)
.catch((e) => {
return
})
const second = await metamaskController.createNewVaultAndRestore(password, rightSeed)
const password = 'what-what-what'
const wrongSeed = 'debris dizzy just program just float decrease vacant alarm reduce speak stadiu'
const rightSeed = 'debris dizzy just program just float decrease vacant alarm reduce speak stadium'
await metamaskController.createNewVaultAndRestore(password, wrongSeed)
.catch((e) => {
return
})
await metamaskController.createNewVaultAndRestore(password, rightSeed)
assert(metamaskController.keyringController.createNewVaultAndRestore.calledTwice)
})
assert(metamaskController.keyringController.createNewVaultAndRestore.calledTwice)
})
})
})

View File

@ -1,25 +1,38 @@
const assert = require('assert')
const nock = require('nock')
const NetworkController = require('../../app/scripts/controllers/network')
const { createTestProviderTools } = require('../stub/provider')
const providerResultStub = {}
const provider = createTestProviderTools({ scaffold: providerResultStub }).provider
describe('# Network Controller', function () {
let networkController
const noop = () => {}
const networkControllerProviderInit = {
getAccounts: () => {},
getAccounts: noop,
}
beforeEach(function () {
nock('https://api.infura.io')
.get('/*/')
.reply(200)
nock('https://rinkeby.infura.io')
.post('/metamask')
.reply(200)
networkController = new NetworkController({
provider: {
type: 'rinkeby',
},
provider,
})
networkController.initializeProvider(networkControllerProviderInit, dummyProviderConstructor)
networkController.initializeProvider(networkControllerProviderInit, provider)
})
describe('network', function () {
describe('#provider', function () {
it('provider should be updatable without reassignment', function () {
networkController.initializeProvider(networkControllerProviderInit, dummyProviderConstructor)
networkController.initializeProvider(networkControllerProviderInit, provider)
const proxy = networkController._proxy
proxy.setTarget({ test: true, on: () => {} })
assert.ok(proxy.test)
@ -64,21 +77,4 @@ describe('# Network Controller', function () {
})
})
})
})
function dummyProviderConstructor() {
return {
// provider
sendAsync: noop,
// block tracker
_blockTracker: {},
start: noop,
stop: noop,
on: noop,
addListener: noop,
once: noop,
removeAllListeners: noop,
}
}
function noop() {}
})

View File

@ -5,7 +5,7 @@ const TxStateManager = require('../../app/scripts/lib/tx-state-manager')
const txStateHistoryHelper = require('../../app/scripts/lib/tx-state-history-helper')
const noop = () => true
describe('TransactionStateManger', function () {
describe('TransactionStateManager', function () {
let txStateManager
const currentNetworkId = 42
const otherNetworkId = 2
@ -281,4 +281,4 @@ describe('TransactionStateManger', function () {
})
})
})
})

View File

@ -35,6 +35,21 @@ AccountImportSubview.prototype.render = function () {
return (
h('div.new-account-import-form', [
h('.new-account-import-disclaimer', [
h('span', 'Imported accounts will not be associated with your originally created MetaMask account seedphrase. Learn more about imported accounts '),
h('span', {
style: {
cursor: 'pointer',
textDecoration: 'underline',
},
onClick: () => {
global.platform.openWindow({
url: 'https://metamask.helpscoutdocs.com/article/17-what-are-loose-accounts',
})
},
}, 'here'),
]),
h('div.new-account-import-form__select-section', [
h('div.new-account-import-form__select-label', 'Select Type'),

View File

@ -1498,6 +1498,7 @@ function pairUpdate (coin) {
dispatch(actions.hideWarning())
shapeShiftRequest('marketinfo', {pair: `${coin.toLowerCase()}_eth`}, (mktResponse) => {
dispatch(actions.hideSubLoadingIndication())
if (mktResponse.error) return dispatch(actions.displayWarning(mktResponse.error))
dispatch({
type: actions.PAIR_UPDATE,
value: {

View File

@ -134,22 +134,6 @@ class AccountDropdowns extends Component {
]),
]),
// =======
// },
// ),
// this.indicateIfLoose(keyring),
// h('span', {
// style: {
// marginLeft: '20px',
// fontSize: '24px',
// maxWidth: '145px',
// whiteSpace: 'nowrap',
// overflow: 'hidden',
// textOverflow: 'ellipsis',
// },
// }, identity.name || ''),
// h('span', { style: { marginLeft: '20px', fontSize: '24px' } }, isSelected ? h('.check', '✓') : null),
// >>>>>>> master:ui/app/components/account-dropdowns.js
]
)
})
@ -159,7 +143,7 @@ class AccountDropdowns extends Component {
try { // Sometimes keyrings aren't loaded yet:
const type = keyring.type
const isLoose = type !== 'HD Key Tree'
return isLoose ? h('.keyring-label', 'LOOSE') : null
return isLoose ? h('.keyring-label', 'IMPORTED') : null
} catch (e) { return }
}

View File

@ -114,7 +114,7 @@ NetworkDropdown.prototype.render = function () {
[
providerType === 'mainnet' ? h('i.fa.fa-check') : h('.network-check__transparent', '✓'),
h(NetworkDropdownIcon, {
backgroundColor: '#038789', // $blue-lagoon
backgroundColor: '#29B6AF', // $java
isSelected: providerType === 'mainnet',
}),
h('span.network-name-item', {
@ -136,7 +136,7 @@ NetworkDropdown.prototype.render = function () {
[
providerType === 'ropsten' ? h('i.fa.fa-check') : h('.network-check__transparent', '✓'),
h(NetworkDropdownIcon, {
backgroundColor: '#e91550', // $crimson
backgroundColor: '#ff4a8d', // $wild-strawberry
isSelected: providerType === 'ropsten',
}),
h('span.network-name-item', {
@ -158,7 +158,7 @@ NetworkDropdown.prototype.render = function () {
[
providerType === 'kovan' ? h('i.fa.fa-check') : h('.network-check__transparent', '✓'),
h(NetworkDropdownIcon, {
backgroundColor: '#690496', // $purple
backgroundColor: '#7057ff', // $cornflower-blue
isSelected: providerType === 'kovan',
}),
h('span.network-name-item', {
@ -180,7 +180,7 @@ NetworkDropdown.prototype.render = function () {
[
providerType === 'rinkeby' ? h('i.fa.fa-check') : h('.network-check__transparent', '✓'),
h(NetworkDropdownIcon, {
backgroundColor: '#ebb33f', // $tulip-tree
backgroundColor: '#f6c343', // $saffron
isSelected: providerType === 'rinkeby',
}),
h('span.network-name-item', {

View File

@ -33,6 +33,9 @@ function mapDispatchToProps (dispatch) {
hideModal: () => {
dispatch(actions.hideModal())
},
hideWarning: () => {
dispatch(actions.hideWarning())
},
showAccountDetailModal: () => {
dispatch(actions.showModal({ name: 'ACCOUNT_DETAILS' }))
},
@ -119,6 +122,7 @@ DepositEtherModal.prototype.render = function () {
h('div.deposit-ether-modal__header__close', {
onClick: () => {
this.setState({ buyingWithShapeshift: false })
this.props.hideWarning()
this.props.hideModal()
},
}),
@ -179,6 +183,7 @@ DepositEtherModal.prototype.render = function () {
}
DepositEtherModal.prototype.goToAccountDetailsModal = function () {
this.props.hideWarning()
this.props.hideModal()
this.props.showAccountDetailModal()
}

View File

@ -79,6 +79,7 @@ const MODALS = {
contents: [
h(DepositEtherModal, {}, []),
],
onHide: (props) => props.hideWarning(),
mobileModalStyle: {
width: '100%',
height: '100%',
@ -286,6 +287,10 @@ function mapDispatchToProps (dispatch) {
hideModal: () => {
dispatch(actions.hideModal())
},
hideWarning: () => {
dispatch(actions.hideWarning())
},
}
}
@ -308,7 +313,12 @@ Modal.prototype.render = function () {
{
className: 'modal',
keyboard: false,
onHide: () => { this.onHide() },
onHide: () => {
if (modal.onHide) {
modal.onHide(this.props)
}
this.onHide()
},
ref: (ref) => {
this.modalRef = ref
},

View File

@ -14,11 +14,13 @@ function mapStateToProps (state) {
tokenExchangeRates,
selectedAddress,
} = state.metamask
const { warning } = state.appState
return {
coinOptions,
tokenExchangeRates,
selectedAddress,
warning,
}
}
@ -163,7 +165,7 @@ ShapeshiftForm.prototype.renderQrCode = function () {
ShapeshiftForm.prototype.render = function () {
const { coinOptions, btnClass } = this.props
const { coinOptions, btnClass, warning } = this.props
const { depositCoin, errorMessage, showQrCode, depositAddress } = this.state
const coinPair = `${depositCoin}_eth`
const { tokenExchangeRates } = this.props
@ -206,7 +208,9 @@ ShapeshiftForm.prototype.render = function () {
]),
h('div', {
warning && h('div.shapeshift-form__address-input-label', warning),
!warning && h('div', {
className: classnames('shapeshift-form__address-input-wrapper', {
'shapeshift-form__address-input-wrapper--error': errorMessage,
}),
@ -227,7 +231,7 @@ ShapeshiftForm.prototype.render = function () {
h('divshapeshift-form__address-input-error-message', [errorMessage]),
]),
this.renderMarketInfo(),
!warning && this.renderMarketInfo(),
]),

View File

@ -66,8 +66,9 @@
.keyring-label {
margin-top: 5px;
background-color: $black;
color: $dusty-gray;
background-color: $dusty-gray;
color: $black;
font-weight: normal;
}
}

View File

@ -787,6 +787,10 @@
width: auto;
flex: 1;
}
@media screen and (max-width: 575px) {
width: auto;
}
}
}

View File

@ -54,6 +54,16 @@
}
.new-account-import-disclaimer {
width: 120%;
background-color: #F4F9FC;
display: inline-block;
align-items: center;
padding: 20px 30px 20px;
font-size: 12px;
line-height: 1.5;
}
.new-account-import-form {
display: flex;
flex-flow: column;

View File

@ -46,6 +46,10 @@ $manatee: #93949d;
$spindle: #c7ddec;
$mid-gray: #5b5d67;
$cape-cod: #38393a;
$java: #29b6af;
$wild-strawberry: #ff4a8d;
$cornflower-blue: #7057ff;
$saffron: #f6c343;
/*
Z-Indicies

View File

@ -361,8 +361,9 @@ SendTransactionScreen.prototype.validateAmount = function (value) {
})
}
const verifyTokenBalance = selectedToken && tokenBalance !== null
let sufficientTokens
if (selectedToken) {
if (verifyTokenBalance) {
sufficientTokens = isTokenBalanceSufficient({
tokenBalance,
amount,
@ -377,7 +378,7 @@ SendTransactionScreen.prototype.validateAmount = function (value) {
if (conversionRate && !sufficientBalance) {
amountError = 'Insufficient funds.'
} else if (selectedToken && !sufficientTokens) {
} else if (verifyTokenBalance && !sufficientTokens) {
amountError = 'Insufficient tokens.'
} else if (amountLessThanZero) {
amountError = 'Can not send negative amounts of ETH.'
@ -396,14 +397,15 @@ SendTransactionScreen.prototype.renderAmountRow = function () {
amount,
setMaxModeTo,
maxModeOn,
gasTotal,
} = this.props
return h('div.send-v2__form-row', [
h('div.send-v2__form-label', [
h('div.send-v2__form-label', [
'Amount:',
this.renderErrorMessage('amount'),
!errors.amount && h('div.send-v2__amount-max', {
!errors.amount && gasTotal && h('div.send-v2__amount-max', {
onClick: (event) => {
event.preventDefault()
setMaxModeTo(true)
@ -491,9 +493,12 @@ SendTransactionScreen.prototype.renderFooter = function () {
goHome,
clearSend,
gasTotal,
tokenBalance,
selectedToken,
errors: { amount: amountError, to: toError },
} = this.props
const missingTokenBalance = selectedToken && !tokenBalance
const noErrors = !amountError && toError === null
return h('div.page-container__footer', [
@ -504,7 +509,7 @@ SendTransactionScreen.prototype.renderFooter = function () {
},
}, 'Cancel'),
h('button.btn-clear.page-container__footer-button', {
disabled: !noErrors || !gasTotal,
disabled: !noErrors || !gasTotal || missingTokenBalance,
onClick: event => this.onSubmit(event),
}, 'Next'),
])

View File

@ -71,7 +71,7 @@ UnlockScreen.prototype.render = function () {
style: {
margin: 10,
},
}, 'Unlock'),
}, 'Log In'),
]),
h('.flex-row.flex-center.flex-grow', [
@ -104,7 +104,7 @@ UnlockScreen.prototype.render = function () {
},
}, 'Use classic interface'),
]),
])
)
}