mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-11-22 18:00:18 +01:00
Merge pull request #4090 from MetaMask/i3725-refactor-send-component-
I3725 Refactor Send Component
This commit is contained in:
commit
988283778a
@ -142,6 +142,7 @@
|
||||
"operator-linebreak": [1, "after", { "overrides": { "?": "ignore", ":": "ignore" } }],
|
||||
"padded-blocks": "off",
|
||||
"quotes": [2, "single", {"avoidEscape": true, "allowTemplateLiterals": true}],
|
||||
"react/no-deprecated": 0,
|
||||
"semi": [2, "never"],
|
||||
"semi-spacing": [2, { "before": false, "after": true }],
|
||||
"space-before-blocks": [1, "always"],
|
||||
|
@ -253,6 +253,9 @@
|
||||
"editAccountName": {
|
||||
"message": "Edit Account Name"
|
||||
},
|
||||
"editingTransaction": {
|
||||
"message": "Make changes to your transaction"
|
||||
},
|
||||
"emailUs": {
|
||||
"message": "Email us!"
|
||||
},
|
||||
@ -771,6 +774,10 @@
|
||||
"onlySendToEtherAddress": {
|
||||
"message": "Only send ETH to an Ethereum address."
|
||||
},
|
||||
"onlySendTokensToAccountAddress": {
|
||||
"message": "Only send $1 to an Ethereum account address.",
|
||||
"description": "displays token symbol"
|
||||
},
|
||||
"searchTokens": {
|
||||
"message": "Search Tokens"
|
||||
},
|
||||
|
@ -394,6 +394,8 @@ module.exports = class MetamaskController extends EventEmitter {
|
||||
updateAndApproveTransaction: nodeify(txController.updateAndApproveTransaction, txController),
|
||||
retryTransaction: nodeify(this.retryTransaction, this),
|
||||
getFilteredTxList: nodeify(txController.getFilteredTxList, txController),
|
||||
isNonceTaken: nodeify(txController.isNonceTaken, txController),
|
||||
estimateGas: nodeify(this.estimateGas, this),
|
||||
|
||||
// messageManager
|
||||
signMessage: nodeify(this.signMessage, this),
|
||||
@ -958,6 +960,18 @@ module.exports = class MetamaskController extends EventEmitter {
|
||||
return state
|
||||
}
|
||||
|
||||
estimateGas (estimateGasParams) {
|
||||
return new Promise((resolve, reject) => {
|
||||
return this.txController.txGasUtil.query.estimateGas(estimateGasParams, (err, res) => {
|
||||
if (err) {
|
||||
return reject(err)
|
||||
}
|
||||
|
||||
return resolve(res)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// PASSWORD MANAGEMENT
|
||||
//=============================================================================
|
||||
|
@ -151,5 +151,10 @@
|
||||
"scrollToBottom": false,
|
||||
"forgottenPassword": null
|
||||
},
|
||||
"identities": {}
|
||||
"identities": {},
|
||||
"send": {
|
||||
"fromDropdownOpen": false,
|
||||
"toDropdownOpen": false,
|
||||
"errors": {}
|
||||
}
|
||||
}
|
||||
|
@ -151,5 +151,10 @@
|
||||
"scrollToBottom": false,
|
||||
"forgottenPassword": null
|
||||
},
|
||||
"identities": {}
|
||||
"identities": {},
|
||||
"send": {
|
||||
"fromDropdownOpen": false,
|
||||
"toDropdownOpen": false,
|
||||
"errors": {}
|
||||
}
|
||||
}
|
||||
|
@ -130,5 +130,10 @@
|
||||
"scrollToBottom": false,
|
||||
"forgottenPassword": null
|
||||
},
|
||||
"identities": {}
|
||||
"identities": {},
|
||||
"send": {
|
||||
"fromDropdownOpen": false,
|
||||
"toDropdownOpen": false,
|
||||
"errors": {}
|
||||
}
|
||||
}
|
||||
|
@ -124,5 +124,10 @@
|
||||
"scrollToBottom": false,
|
||||
"forgottenPassword": null
|
||||
},
|
||||
"identities": {}
|
||||
"identities": {},
|
||||
"send": {
|
||||
"fromDropdownOpen": false,
|
||||
"toDropdownOpen": false,
|
||||
"errors": {}
|
||||
}
|
||||
}
|
||||
|
234
gentests.js
Normal file
234
gentests.js
Normal file
@ -0,0 +1,234 @@
|
||||
const fs = require('fs')
|
||||
const async = require('async')
|
||||
const path = require('path')
|
||||
const promisify = require('pify')
|
||||
|
||||
// start(/\.selectors.js/, generateSelectorTest).catch(console.error)
|
||||
// start(/\.utils.js/, generateUtilTest).catch(console.error)
|
||||
startContainer(/\.container.js/, generateContainerTest).catch(console.error)
|
||||
|
||||
async function getAllFileNames (dirName) {
|
||||
const rootPath = path.join(__dirname, dirName)
|
||||
const allNames = (await promisify(fs.readdir)(dirName))
|
||||
const fileNames = allNames.filter(name => name.match(/^.+\./))
|
||||
const dirNames = allNames.filter(name => name.match(/^[^.]+$/))
|
||||
|
||||
const fullPathDirNames = dirNames.map(d => `${dirName}/${d}`)
|
||||
const subNameArrays = await promisify(async.map)(fullPathDirNames, getAllFileNames)
|
||||
let subNames = []
|
||||
subNameArrays.forEach(subNameArray => subNames = [...subNames, ...subNameArray])
|
||||
|
||||
return [
|
||||
...fileNames.map(name => dirName + '/' + name),
|
||||
...subNames,
|
||||
]
|
||||
}
|
||||
|
||||
async function start (fileRegEx, testGenerator) {
|
||||
const fileNames = await getAllFileNames('./ui/app')
|
||||
const sFiles = fileNames.filter(name => name.match(fileRegEx))
|
||||
|
||||
let sFileMethodNames
|
||||
let testFilePath
|
||||
async.each(sFiles, async (sFile, cb) => {
|
||||
let [, sRootPath, sPath] = sFile.match(/^(.+\/)([^/]+)$/)
|
||||
sFileMethodNames = Object.keys(require(__dirname + '/' + sFile))
|
||||
|
||||
testFilePath = sPath.replace('.', '-').replace('.', '.test.')
|
||||
|
||||
await promisify(fs.writeFile)(
|
||||
`${__dirname}/${sRootPath}tests/${testFilePath}`,
|
||||
testGenerator(sPath, sFileMethodNames),
|
||||
'utf8'
|
||||
)
|
||||
}, (err) => {
|
||||
console.log(err)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
async function startContainer (fileRegEx, testGenerator) {
|
||||
const fileNames = await getAllFileNames('./ui/app')
|
||||
const sFiles = fileNames.filter(name => name.match(fileRegEx))
|
||||
|
||||
let sFileMethodNames
|
||||
async.each(sFiles, async (sFile, cb) => {
|
||||
console.log(`sFile`, sFile);
|
||||
let [, sRootPath, sPath] = sFile.match(/^(.+\/)([^/]+)$/)
|
||||
|
||||
let testFilePath = sPath.replace('.', '-').replace('.', '.test.')
|
||||
|
||||
await promisify(fs.readFile)(
|
||||
__dirname + '/' + sFile,
|
||||
'utf8',
|
||||
async (err, result) => {
|
||||
console.log(`result`, result.length);
|
||||
const returnObjectStrings = result
|
||||
.match(/return\s(\{[\s\S]+?})\n}/g)
|
||||
.map(str => {
|
||||
return str
|
||||
.slice(0, str.length - 1)
|
||||
.slice(7)
|
||||
.replace(/\n/g, '')
|
||||
.replace(/\s\s+/g, ' ')
|
||||
|
||||
})
|
||||
const mapStateToPropsAssertionObject = returnObjectStrings[0]
|
||||
.replace(/\w+:\s\w+\([\w,\s]+\),/g, str => {
|
||||
const strKey = str.match(/^\w+/)[0]
|
||||
return strKey + ': \'mock' + str.match(/^\w+/)[0].replace(/^./, c => c.toUpperCase()) + ':mockState\',\n'
|
||||
})
|
||||
.replace(/{\s\w.+/, firstLinePair => `{\n ${firstLinePair.slice(2)}`)
|
||||
.replace(/\w+:.+,/g, s => ` ${s}`)
|
||||
.replace(/}/g, s => ` ${s}`)
|
||||
let mapDispatchToPropsMethodNames
|
||||
if (returnObjectStrings[1]) {
|
||||
mapDispatchToPropsMethodNames = returnObjectStrings[1].match(/\s\w+:\s/g).map(str => str.match(/\w+/)[0])
|
||||
}
|
||||
const proxyquireObject = ('{\n ' + result
|
||||
.match(/import\s{[\s\S]+?}\sfrom\s.+/g)
|
||||
.map(s => s.replace(/\n/g, ''))
|
||||
.map((s, i) => {
|
||||
const proxyKeys = s.match(/{.+}/)[0].match(/\w+/g)
|
||||
return '\'' + s.match(/'(.+)'/)[1] + '\': { ' + (proxyKeys.length > 1
|
||||
? '\n ' + proxyKeys.join(': () => {},\n ') + ': () => {},\n '
|
||||
: proxyKeys[0] + ': () => {},') + ' }'
|
||||
})
|
||||
.join(',\n ') + '\n}')
|
||||
.replace('{ connect: () => {}, },', `{
|
||||
connect: (ms, md) => {
|
||||
mapStateToProps = ms
|
||||
mapDispatchToProps = md
|
||||
return () => ({})
|
||||
},
|
||||
},`)
|
||||
// console.log(`proxyquireObject`, proxyquireObject);
|
||||
// console.log(`mapStateToPropsAssertionObject`, mapStateToPropsAssertionObject);
|
||||
// console.log(`mapDispatchToPropsMethodNames`, mapDispatchToPropsMethodNames);
|
||||
|
||||
const containerTest = generateContainerTest(sPath, {
|
||||
mapStateToPropsAssertionObject,
|
||||
mapDispatchToPropsMethodNames,
|
||||
proxyquireObject,
|
||||
})
|
||||
// console.log(`containerTest`, `${__dirname}/${sRootPath}tests/${testFilePath}`, containerTest);
|
||||
console.log('----')
|
||||
console.log(`sRootPath`, sRootPath);
|
||||
console.log(`testFilePath`, testFilePath);
|
||||
await promisify(fs.writeFile)(
|
||||
`${__dirname}/${sRootPath}tests/${testFilePath}`,
|
||||
containerTest,
|
||||
'utf8'
|
||||
)
|
||||
}
|
||||
)
|
||||
}, (err) => {
|
||||
console.log('123', err)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
function generateMethodList (methodArray) {
|
||||
return methodArray.map(n => ' ' + n).join(',\n') + ','
|
||||
}
|
||||
|
||||
function generateMethodDescribeBlock (methodName, index) {
|
||||
const describeBlock =
|
||||
`${index ? ' ' : ''}describe('${methodName}()', () => {
|
||||
it('should', () => {
|
||||
const state = {}
|
||||
|
||||
assert.equal(${methodName}(state), )
|
||||
})
|
||||
})`
|
||||
return describeBlock
|
||||
}
|
||||
|
||||
function generateDispatchMethodDescribeBlock (methodName, index) {
|
||||
const describeBlock =
|
||||
`${index ? ' ' : ''}describe('${methodName}()', () => {
|
||||
it('should dispatch an action', () => {
|
||||
mapDispatchToPropsObject.${methodName}()
|
||||
assert(dispatchSpy.calledOnce)
|
||||
})
|
||||
})`
|
||||
return describeBlock
|
||||
}
|
||||
|
||||
function generateMethodDescribeBlocks (methodArray) {
|
||||
return methodArray
|
||||
.map((methodName, index) => generateMethodDescribeBlock(methodName, index))
|
||||
.join('\n\n')
|
||||
}
|
||||
|
||||
function generateDispatchMethodDescribeBlocks (methodArray) {
|
||||
return methodArray
|
||||
.map((methodName, index) => generateDispatchMethodDescribeBlock(methodName, index))
|
||||
.join('\n\n')
|
||||
}
|
||||
|
||||
function generateSelectorTest (name, methodArray) {
|
||||
return `import assert from 'assert'
|
||||
import {
|
||||
${generateMethodList(methodArray)}
|
||||
} from '../${name}'
|
||||
|
||||
describe('${name.match(/^[^.]+/)} selectors', () => {
|
||||
|
||||
${generateMethodDescribeBlocks(methodArray)}
|
||||
|
||||
})`
|
||||
}
|
||||
|
||||
function generateUtilTest (name, methodArray) {
|
||||
return `import assert from 'assert'
|
||||
import {
|
||||
${generateMethodList(methodArray)}
|
||||
} from '../${name}'
|
||||
|
||||
describe('${name.match(/^[^.]+/)} utils', () => {
|
||||
|
||||
${generateMethodDescribeBlocks(methodArray)}
|
||||
|
||||
})`
|
||||
}
|
||||
|
||||
function generateContainerTest (sPath, {
|
||||
mapStateToPropsAssertionObject,
|
||||
mapDispatchToPropsMethodNames,
|
||||
proxyquireObject,
|
||||
}) {
|
||||
return `import assert from 'assert'
|
||||
import proxyquire from 'proxyquire'
|
||||
import sinon from 'sinon'
|
||||
|
||||
let mapStateToProps
|
||||
let mapDispatchToProps
|
||||
|
||||
proxyquire('../${sPath}', ${proxyquireObject})
|
||||
|
||||
describe('${sPath.match(/^[^.]+/)} container', () => {
|
||||
|
||||
describe('mapStateToProps()', () => {
|
||||
|
||||
it('should map the correct properties to props', () => {
|
||||
assert.deepEqual(mapStateToProps('mockState'), ${mapStateToPropsAssertionObject})
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
describe('mapDispatchToProps()', () => {
|
||||
let dispatchSpy
|
||||
let mapDispatchToPropsObject
|
||||
|
||||
beforeEach(() => {
|
||||
dispatchSpy = sinon.spy()
|
||||
mapDispatchToPropsObject = mapDispatchToProps(dispatchSpy)
|
||||
})
|
||||
|
||||
${mapDispatchToPropsMethodNames ? generateDispatchMethodDescribeBlocks(mapDispatchToPropsMethodNames) : 'delete'}
|
||||
|
||||
})
|
||||
|
||||
})`
|
||||
}
|
50
package-lock.json
generated
50
package-lock.json
generated
@ -9597,6 +9597,16 @@
|
||||
"integrity": "sha512-ZH7loueKBoDb7yG9esn1U+fgq7BzlzW6NRi5/rMdxIZ05dj7GFD/Xc5rq2CDt5Yq86CyfSYVyx4242QQNZbx1g==",
|
||||
"dev": true
|
||||
},
|
||||
"fill-keys": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/fill-keys/-/fill-keys-1.0.2.tgz",
|
||||
"integrity": "sha1-mo+jb06K1jTjv2tPPIiCVRRS6yA=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"is-object": "1.0.1",
|
||||
"merge-descriptors": "1.0.1"
|
||||
}
|
||||
},
|
||||
"fill-range": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.3.tgz",
|
||||
@ -18712,6 +18722,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"module-not-found-error": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/module-not-found-error/-/module-not-found-error-1.0.1.tgz",
|
||||
"integrity": "sha1-z4tP9PKWQGdNbN0CsOO8UjwrvcA=",
|
||||
"dev": true
|
||||
},
|
||||
"moment": {
|
||||
"version": "2.22.1",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.22.1.tgz",
|
||||
@ -24501,6 +24517,28 @@
|
||||
"integrity": "sha1-M8UDmPcOp+uW0h97gXYwpVeRx+4=",
|
||||
"dev": true
|
||||
},
|
||||
"proxyquire": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/proxyquire/-/proxyquire-2.0.1.tgz",
|
||||
"integrity": "sha512-fQr3VQrbdzHrdaDn3XuisVoJlJNDJizHAvUXw9IuXRR8BpV2x0N7LsCxrpJkeKfPbNjiNU/V5vc008cI0TmzzQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"fill-keys": "1.0.2",
|
||||
"module-not-found-error": "1.0.1",
|
||||
"resolve": "1.5.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"resolve": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.5.0.tgz",
|
||||
"integrity": "sha512-hgoSGrc3pjzAPHNBg+KnFcK2HwlHTs/YrAGUr6qgTVUZmXv1UEXXl0bZNBKMA9fud6lRYFdPGz0xXxycPzmmiw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"path-parse": "1.0.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"prr": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
|
||||
@ -26474,6 +26512,18 @@
|
||||
"object-assign": "4.1.1"
|
||||
}
|
||||
},
|
||||
"react": {
|
||||
"version": "15.6.2",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-15.6.2.tgz",
|
||||
"integrity": "sha1-26BDSrQ5z+gvEI8PURZjkIF5qnI=",
|
||||
"requires": {
|
||||
"create-react-class": "15.6.2",
|
||||
"fbjs": "0.8.16",
|
||||
"loose-envify": "1.3.1",
|
||||
"object-assign": "4.1.1",
|
||||
"prop-types": "15.6.1"
|
||||
}
|
||||
},
|
||||
"react-hyperscript": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/react-hyperscript/-/react-hyperscript-2.4.2.tgz",
|
||||
|
@ -9,7 +9,7 @@
|
||||
"dist": "gulp dist",
|
||||
"doc": "jsdoc -c development/tools/.jsdoc.json",
|
||||
"test": "npm run test:unit && npm run test:integration && npm run lint",
|
||||
"test:unit": "cross-env METAMASK_ENV=test mocha --exit --require test/setup.js --recursive \"test/unit/**/*.js\" && dot-only-hunter",
|
||||
"test:unit": "cross-env METAMASK_ENV=test mocha --exit --require test/setup.js --recursive \"test/unit/**/*.js\" \"ui/app/**/*.test.js\" && dot-only-hunter",
|
||||
"test:single": "cross-env METAMASK_ENV=test mocha --require test/helper.js",
|
||||
"test:integration": "npm run test:integration:build && npm run test:flat && npm run test:mascara",
|
||||
"test:integration:build": "gulp build:scss",
|
||||
@ -276,6 +276,7 @@
|
||||
"path": "^0.12.7",
|
||||
"png-file-stream": "^1.0.0",
|
||||
"prompt": "^1.0.0",
|
||||
"proxyquire": "2.0.1",
|
||||
"qs": "^6.2.0",
|
||||
"qunitjs": "^2.4.1",
|
||||
"radgrad-jsdoc-template": "^1.1.3",
|
||||
|
@ -285,7 +285,7 @@ describe('Using MetaMask with an existing account', function () {
|
||||
await driver.switchTo().window(faucet)
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const send1eth = await findElement(driver, By.xpath(`//button[contains(text(), '10 ether')]`))
|
||||
const send1eth = await findElement(driver, By.xpath(`//button[contains(text(), '10 ether')]`), 14000)
|
||||
await send1eth.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
@ -293,7 +293,7 @@ describe('Using MetaMask with an existing account', function () {
|
||||
await loadExtension(driver, extensionId)
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`))
|
||||
const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`), 14000)
|
||||
await confirmButton.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
|
@ -367,7 +367,7 @@ describe('MetaMask', function () {
|
||||
await driver.switchTo().window(faucet)
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const send1eth = await findElement(driver, By.xpath(`//button[contains(text(), '10 ether')]`))
|
||||
const send1eth = await findElement(driver, By.xpath(`//button[contains(text(), '10 ether')]`), 14000)
|
||||
await send1eth.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
@ -375,7 +375,7 @@ describe('MetaMask', function () {
|
||||
await loadExtension(driver, extensionId)
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`))
|
||||
const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`), 14000)
|
||||
await confirmButton.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
|
@ -101,7 +101,7 @@ async function runSendFlowTest(assert, done) {
|
||||
const sendAmountField = await queryAsync($, '.send-v2__form-row:eq(2)')
|
||||
sendAmountField.find('.currency-display')[0].click()
|
||||
|
||||
const sendAmountFieldInput = await findAsync(sendAmountField, 'input:text')
|
||||
const sendAmountFieldInput = await findAsync(sendAmountField, '.currency-display__input')
|
||||
sendAmountFieldInput.val('5.1')
|
||||
reactTriggerChange(sendAmountField.find('input')[0])
|
||||
|
||||
@ -117,7 +117,7 @@ async function runSendFlowTest(assert, done) {
|
||||
const sendGasField = await queryAsync($, '.send-v2__gas-fee-display')
|
||||
assert.equal(
|
||||
sendGasField.find('.currency-display__input-wrapper > input').val(),
|
||||
'0.000198',
|
||||
'0.000198264',
|
||||
'send gas field should show estimated gas total'
|
||||
)
|
||||
assert.equal(
|
||||
@ -127,7 +127,7 @@ async function runSendFlowTest(assert, done) {
|
||||
)
|
||||
|
||||
await customizeGas(assert, 0, 21000, '0', '$0.00 USD')
|
||||
await customizeGas(assert, 500, 60000, '0.003', '$3.60 USD')
|
||||
await customizeGas(assert, 500, 60000, '0.03', '$36.03 USD')
|
||||
|
||||
const sendButton = await queryAsync($, 'button.btn-primary.btn--large.page-container__footer-button')
|
||||
assert.equal(sendButton[0].textContent, 'Next', 'next button rendered')
|
||||
@ -165,7 +165,7 @@ async function runSendFlowTest(assert, done) {
|
||||
const sendAmountFieldInEdit = await queryAsync($, '.send-v2__form-row:eq(2)')
|
||||
sendAmountFieldInEdit.find('.currency-display')[0].click()
|
||||
|
||||
const sendAmountFieldInputInEdit = sendAmountFieldInEdit.find('input:text')
|
||||
const sendAmountFieldInputInEdit = sendAmountFieldInEdit.find('.currency-display__input')
|
||||
sendAmountFieldInputInEdit.val('1.0')
|
||||
reactTriggerChange(sendAmountFieldInputInEdit[0])
|
||||
|
||||
|
@ -2,6 +2,12 @@ const abi = require('human-standard-token-abi')
|
||||
const pify = require('pify')
|
||||
const getBuyEthUrl = require('../../app/scripts/lib/buy-eth-url')
|
||||
const { getTokenAddressFromTokenObject } = require('./util')
|
||||
const {
|
||||
calcGasTotal,
|
||||
calcTokenBalance,
|
||||
estimateGas,
|
||||
estimateGasPriceFromRecentBlocks,
|
||||
} = require('./components/send_/send.utils')
|
||||
const ethUtil = require('ethereumjs-util')
|
||||
const { fetchLocale } = require('../i18n-helper')
|
||||
const log = require('loglevel')
|
||||
@ -155,8 +161,6 @@ var actions = {
|
||||
updateTransactionParams,
|
||||
UPDATE_TRANSACTION_PARAMS: 'UPDATE_TRANSACTION_PARAMS',
|
||||
// send screen
|
||||
estimateGas,
|
||||
getGasPrice,
|
||||
UPDATE_GAS_LIMIT: 'UPDATE_GAS_LIMIT',
|
||||
UPDATE_GAS_PRICE: 'UPDATE_GAS_PRICE',
|
||||
UPDATE_GAS_TOTAL: 'UPDATE_GAS_TOTAL',
|
||||
@ -169,17 +173,21 @@ var actions = {
|
||||
UPDATE_MAX_MODE: 'UPDATE_MAX_MODE',
|
||||
UPDATE_SEND: 'UPDATE_SEND',
|
||||
CLEAR_SEND: 'CLEAR_SEND',
|
||||
updateGasLimit,
|
||||
updateGasPrice,
|
||||
updateGasTotal,
|
||||
OPEN_FROM_DROPDOWN: 'OPEN_FROM_DROPDOWN',
|
||||
CLOSE_FROM_DROPDOWN: 'CLOSE_FROM_DROPDOWN',
|
||||
setGasLimit,
|
||||
setGasPrice,
|
||||
updateGasData,
|
||||
setGasTotal,
|
||||
setSendTokenBalance,
|
||||
updateSendTokenBalance,
|
||||
updateSendFrom,
|
||||
updateSendTo,
|
||||
updateSendAmount,
|
||||
updateSendMemo,
|
||||
updateSendErrors,
|
||||
setMaxModeTo,
|
||||
updateSend,
|
||||
updateSendErrors,
|
||||
clearSend,
|
||||
setSelectedAddress,
|
||||
// app messages
|
||||
@ -703,60 +711,96 @@ function signTx (txData) {
|
||||
}
|
||||
}
|
||||
|
||||
function estimateGas (params = {}) {
|
||||
return (dispatch) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
global.ethQuery.estimateGas(params, (err, data) => {
|
||||
if (err) {
|
||||
dispatch(actions.displayWarning(err.message))
|
||||
return reject(err)
|
||||
}
|
||||
dispatch(actions.hideWarning())
|
||||
dispatch(actions.updateGasLimit(data))
|
||||
return resolve(data)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function updateGasLimit (gasLimit) {
|
||||
function setGasLimit (gasLimit) {
|
||||
return {
|
||||
type: actions.UPDATE_GAS_LIMIT,
|
||||
value: gasLimit,
|
||||
}
|
||||
}
|
||||
|
||||
function getGasPrice () {
|
||||
return (dispatch) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
global.ethQuery.gasPrice((err, data) => {
|
||||
if (err) {
|
||||
dispatch(actions.displayWarning(err.message))
|
||||
return reject(err)
|
||||
}
|
||||
dispatch(actions.hideWarning())
|
||||
dispatch(actions.updateGasPrice(data))
|
||||
return resolve(data)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function updateGasPrice (gasPrice) {
|
||||
function setGasPrice (gasPrice) {
|
||||
return {
|
||||
type: actions.UPDATE_GAS_PRICE,
|
||||
value: gasPrice,
|
||||
}
|
||||
}
|
||||
|
||||
function updateGasTotal (gasTotal) {
|
||||
function setGasTotal (gasTotal) {
|
||||
return {
|
||||
type: actions.UPDATE_GAS_TOTAL,
|
||||
value: gasTotal,
|
||||
}
|
||||
}
|
||||
|
||||
function updateSendTokenBalance (tokenBalance) {
|
||||
function updateGasData ({
|
||||
blockGasLimit,
|
||||
recentBlocks,
|
||||
selectedAddress,
|
||||
selectedToken,
|
||||
to,
|
||||
value,
|
||||
}) {
|
||||
const estimatedGasPrice = estimateGasPriceFromRecentBlocks(recentBlocks)
|
||||
return (dispatch) => {
|
||||
return Promise.all([
|
||||
Promise.resolve(estimatedGasPrice),
|
||||
estimateGas({
|
||||
estimateGasMethod: background.estimateGas,
|
||||
blockGasLimit,
|
||||
selectedAddress,
|
||||
selectedToken,
|
||||
to,
|
||||
value,
|
||||
gasPrice: estimatedGasPrice,
|
||||
}),
|
||||
])
|
||||
.then(([gasPrice, gas]) => {
|
||||
dispatch(actions.setGasPrice(gasPrice))
|
||||
dispatch(actions.setGasLimit(gas))
|
||||
return calcGasTotal(gas, gasPrice)
|
||||
})
|
||||
.then((gasEstimate) => {
|
||||
dispatch(actions.setGasTotal(gasEstimate))
|
||||
dispatch(updateSendErrors({ gasLoadingError: null }))
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err)
|
||||
dispatch(updateSendErrors({ gasLoadingError: 'gasLoadingError' }))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function updateSendTokenBalance ({
|
||||
selectedToken,
|
||||
tokenContract,
|
||||
address,
|
||||
}) {
|
||||
return (dispatch) => {
|
||||
const tokenBalancePromise = tokenContract
|
||||
? tokenContract.balanceOf(address)
|
||||
: Promise.resolve()
|
||||
return tokenBalancePromise
|
||||
.then(usersToken => {
|
||||
if (usersToken) {
|
||||
const newTokenBalance = calcTokenBalance({ selectedToken, usersToken })
|
||||
dispatch(setSendTokenBalance(newTokenBalance.toString(10)))
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err)
|
||||
updateSendErrors({ tokenBalance: 'tokenBalanceError' })
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function updateSendErrors (errorObject) {
|
||||
return {
|
||||
type: actions.UPDATE_SEND_ERRORS,
|
||||
value: errorObject,
|
||||
}
|
||||
}
|
||||
|
||||
function setSendTokenBalance (tokenBalance) {
|
||||
return {
|
||||
type: actions.UPDATE_SEND_TOKEN_BALANCE,
|
||||
value: tokenBalance,
|
||||
@ -791,13 +835,6 @@ function updateSendMemo (memo) {
|
||||
}
|
||||
}
|
||||
|
||||
function updateSendErrors (error) {
|
||||
return {
|
||||
type: actions.UPDATE_SEND_ERRORS,
|
||||
value: error,
|
||||
}
|
||||
}
|
||||
|
||||
function setMaxModeTo (bool) {
|
||||
return {
|
||||
type: actions.UPDATE_MAX_MODE,
|
||||
|
@ -11,7 +11,7 @@ const log = require('loglevel')
|
||||
// init
|
||||
const InitializeScreen = require('../../mascara/src/app/first-time').default
|
||||
// accounts
|
||||
const SendTransactionScreen2 = require('./components/send/send-v2-container')
|
||||
const SendTransactionScreen = require('./components/send_/send.container')
|
||||
const ConfirmTxScreen = require('./conf-tx')
|
||||
|
||||
// slideout menu
|
||||
@ -77,7 +77,7 @@ class App extends Component {
|
||||
h(Authenticated, { path: SETTINGS_ROUTE, component: Settings }),
|
||||
h(Authenticated, { path: NOTICE_ROUTE, exact, component: NoticeScreen }),
|
||||
h(Authenticated, { path: `${CONFIRM_TRANSACTION_ROUTE}/:id?`, component: ConfirmTxScreen }),
|
||||
h(Authenticated, { path: SEND_ROUTE, exact, component: SendTransactionScreen2 }),
|
||||
h(Authenticated, { path: SEND_ROUTE, exact, component: SendTransactionScreen }),
|
||||
h(Authenticated, { path: ADD_TOKEN_ROUTE, exact, component: AddTokenPage }),
|
||||
h(Authenticated, { path: CONFIRM_ADD_TOKEN_ROUTE, exact, component: ConfirmAddTokenPage }),
|
||||
h(Authenticated, { path: NEW_ACCOUNT_ROUTE, component: CreateAccountPage }),
|
||||
|
@ -1,113 +0,0 @@
|
||||
const Component = require('react').Component
|
||||
const h = require('react-hyperscript')
|
||||
const inherits = require('util').inherits
|
||||
|
||||
module.exports = CurrencyInput
|
||||
|
||||
inherits(CurrencyInput, Component)
|
||||
function CurrencyInput (props) {
|
||||
Component.call(this)
|
||||
|
||||
const sanitizedValue = sanitizeValue(props.value)
|
||||
|
||||
this.state = {
|
||||
value: sanitizedValue,
|
||||
emptyState: false,
|
||||
focused: false,
|
||||
}
|
||||
}
|
||||
|
||||
function removeNonDigits (str) {
|
||||
return str.match(/\d|$/g).join('')
|
||||
}
|
||||
|
||||
// Removes characters that are not digits, then removes leading zeros
|
||||
function sanitizeInteger (val) {
|
||||
return String(parseInt(removeNonDigits(val) || '0', 10))
|
||||
}
|
||||
|
||||
function sanitizeDecimal (val) {
|
||||
return removeNonDigits(val)
|
||||
}
|
||||
|
||||
// Take a single string param and returns a non-negative integer or float as a string.
|
||||
// Breaks the input into three parts: the integer, the decimal point, and the decimal/fractional part.
|
||||
// Removes leading zeros from the integer, and non-digits from the integer and decimal
|
||||
// The integer is returned as '0' in cases where it would be empty. A decimal point is
|
||||
// included in the returned string if one is included in the param
|
||||
// Examples:
|
||||
// sanitizeValue('0') -> '0'
|
||||
// sanitizeValue('a') -> '0'
|
||||
// sanitizeValue('010.') -> '10.'
|
||||
// sanitizeValue('0.005') -> '0.005'
|
||||
// sanitizeValue('22.200') -> '22.200'
|
||||
// sanitizeValue('.200') -> '0.200'
|
||||
// sanitizeValue('a.b.1.c,89.123') -> '0.189123'
|
||||
function sanitizeValue (value) {
|
||||
let [ , integer, point, decimal] = (/([^.]*)([.]?)([^.]*)/).exec(value)
|
||||
|
||||
integer = sanitizeInteger(integer) || '0'
|
||||
decimal = sanitizeDecimal(decimal)
|
||||
|
||||
return `${integer}${point}${decimal}`
|
||||
}
|
||||
|
||||
CurrencyInput.prototype.handleChange = function (newValue) {
|
||||
const { onInputChange } = this.props
|
||||
const { value } = this.state
|
||||
|
||||
let parsedValue = newValue
|
||||
const newValueLastIndex = newValue.length - 1
|
||||
|
||||
if (value === '0' && newValue[newValueLastIndex] === '0') {
|
||||
parsedValue = parsedValue.slice(0, newValueLastIndex)
|
||||
}
|
||||
const sanitizedValue = sanitizeValue(parsedValue)
|
||||
this.setState({
|
||||
value: sanitizedValue,
|
||||
emptyState: newValue === '' && sanitizedValue === '0',
|
||||
})
|
||||
onInputChange(sanitizedValue)
|
||||
}
|
||||
|
||||
// If state.value === props.value plus a decimal point, or at least one
|
||||
// zero or a decimal point and at least one zero, then this returns state.value
|
||||
// after it is sanitized with getValueParts
|
||||
CurrencyInput.prototype.getValueToRender = function () {
|
||||
const { value } = this.props
|
||||
const { value: stateValue } = this.state
|
||||
|
||||
const trailingStateString = (new RegExp(`^${value}(.+)`)).exec(stateValue)
|
||||
const trailingDecimalAndZeroes = trailingStateString && (/^[.0]0*/).test(trailingStateString[1])
|
||||
|
||||
return sanitizeValue(trailingDecimalAndZeroes
|
||||
? stateValue
|
||||
: value)
|
||||
}
|
||||
|
||||
CurrencyInput.prototype.render = function () {
|
||||
const {
|
||||
className,
|
||||
placeholder,
|
||||
readOnly,
|
||||
inputRef,
|
||||
type,
|
||||
} = this.props
|
||||
const { emptyState, focused } = this.state
|
||||
|
||||
const inputSizeMultiplier = readOnly ? 1 : 1.2
|
||||
|
||||
const valueToRender = this.getValueToRender()
|
||||
return h('input', {
|
||||
className,
|
||||
type,
|
||||
value: emptyState ? '' : valueToRender,
|
||||
placeholder: focused ? '' : placeholder,
|
||||
size: valueToRender.length * inputSizeMultiplier,
|
||||
readOnly,
|
||||
onFocus: () => this.setState({ focused: true, emptyState: valueToRender === '0' }),
|
||||
onBlur: () => this.setState({ focused: false, emptyState: false }),
|
||||
onChange: e => this.handleChange(e.target.value),
|
||||
ref: inputRef,
|
||||
})
|
||||
}
|
@ -8,15 +8,19 @@ const GasModalCard = require('./gas-modal-card')
|
||||
|
||||
const ethUtil = require('ethereumjs-util')
|
||||
|
||||
import {
|
||||
updateSendErrors,
|
||||
} from '../../ducks/send.duck'
|
||||
|
||||
const {
|
||||
MIN_GAS_PRICE_DEC,
|
||||
MIN_GAS_LIMIT_DEC,
|
||||
MIN_GAS_PRICE_GWEI,
|
||||
} = require('../send/send-constants')
|
||||
} = require('../send_/send.constants')
|
||||
|
||||
const {
|
||||
isBalanceSufficient,
|
||||
} = require('../send/send-utils')
|
||||
} = require('../send_/send.utils')
|
||||
|
||||
const {
|
||||
conversionUtil,
|
||||
@ -61,11 +65,11 @@ function mapStateToProps (state) {
|
||||
function mapDispatchToProps (dispatch) {
|
||||
return {
|
||||
hideModal: () => dispatch(actions.hideModal()),
|
||||
updateGasPrice: newGasPrice => dispatch(actions.updateGasPrice(newGasPrice)),
|
||||
updateGasLimit: newGasLimit => dispatch(actions.updateGasLimit(newGasLimit)),
|
||||
updateGasTotal: newGasTotal => dispatch(actions.updateGasTotal(newGasTotal)),
|
||||
setGasPrice: newGasPrice => dispatch(actions.setGasPrice(newGasPrice)),
|
||||
setGasLimit: newGasLimit => dispatch(actions.setGasLimit(newGasLimit)),
|
||||
setGasTotal: newGasTotal => dispatch(actions.setGasTotal(newGasTotal)),
|
||||
updateSendAmount: newAmount => dispatch(actions.updateSendAmount(newAmount)),
|
||||
updateSendErrors: error => dispatch(actions.updateSendErrors(error)),
|
||||
updateSendErrors: error => dispatch(updateSendErrors(error)),
|
||||
}
|
||||
}
|
||||
|
||||
@ -105,10 +109,10 @@ module.exports = connect(mapStateToProps, mapDispatchToProps)(CustomizeGasModal)
|
||||
|
||||
CustomizeGasModal.prototype.save = function (gasPrice, gasLimit, gasTotal) {
|
||||
const {
|
||||
updateGasPrice,
|
||||
updateGasLimit,
|
||||
setGasPrice,
|
||||
setGasLimit,
|
||||
hideModal,
|
||||
updateGasTotal,
|
||||
setGasTotal,
|
||||
maxModeOn,
|
||||
selectedToken,
|
||||
balance,
|
||||
@ -125,9 +129,9 @@ CustomizeGasModal.prototype.save = function (gasPrice, gasLimit, gasTotal) {
|
||||
updateSendAmount(maxAmount)
|
||||
}
|
||||
|
||||
updateGasPrice(ethUtil.addHexPrefix(gasPrice))
|
||||
updateGasLimit(ethUtil.addHexPrefix(gasLimit))
|
||||
updateGasTotal(ethUtil.addHexPrefix(gasTotal))
|
||||
setGasPrice(ethUtil.addHexPrefix(gasPrice))
|
||||
setGasLimit(ethUtil.addHexPrefix(gasLimit))
|
||||
setGasTotal(ethUtil.addHexPrefix(gasTotal))
|
||||
updateSendErrors({ insufficientFunds: false })
|
||||
hideModal()
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
const Component = require('react').Component
|
||||
const h = require('react-hyperscript')
|
||||
const inherits = require('util').inherits
|
||||
const AccountListItem = require('../send/account-list-item')
|
||||
const AccountListItem = require('../send_/account-list-item/account-list-item.component').default
|
||||
|
||||
module.exports = AccountDropdownMini
|
||||
|
||||
|
@ -1,7 +1,6 @@
|
||||
const Component = require('react').Component
|
||||
const h = require('react-hyperscript')
|
||||
const inherits = require('util').inherits
|
||||
const CurrencyInput = require('./currency-input')
|
||||
const {
|
||||
addCurrencies,
|
||||
conversionGTE,
|
||||
@ -51,14 +50,15 @@ InputNumber.prototype.render = function () {
|
||||
const { unitLabel, step = 1, placeholder, value = 0 } = this.props
|
||||
|
||||
return h('div.customize-gas-input-wrapper', {}, [
|
||||
h(CurrencyInput, {
|
||||
h('input', {
|
||||
className: 'customize-gas-input',
|
||||
value,
|
||||
placeholder,
|
||||
type: 'number',
|
||||
onInputChange: newValue => {
|
||||
this.setValue(newValue)
|
||||
onChange: e => {
|
||||
this.setValue(e.target.value)
|
||||
},
|
||||
min: 0,
|
||||
}),
|
||||
h('span.gas-tooltip-input-detail', {}, [unitLabel]),
|
||||
h('div.gas-tooltip-input-arrows', {}, [
|
||||
|
1
ui/app/components/page-container/index.js
Normal file
1
ui/app/components/page-container/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './page-container.component'
|
@ -0,0 +1,18 @@
|
||||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
export default class PageContainerContent extends Component {
|
||||
|
||||
static propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
render () {
|
||||
return (
|
||||
<div className="page-container__content">
|
||||
{this.props.children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1 @@
|
||||
export { default } from './page-container-footer.component'
|
@ -0,0 +1,54 @@
|
||||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import Button from '../../button'
|
||||
|
||||
export default class PageContainerFooter extends Component {
|
||||
|
||||
static propTypes = {
|
||||
onCancel: PropTypes.func,
|
||||
cancelText: PropTypes.string,
|
||||
onSubmit: PropTypes.func,
|
||||
submitText: PropTypes.string,
|
||||
disabled: PropTypes.bool,
|
||||
}
|
||||
|
||||
static contextTypes = {
|
||||
t: PropTypes.func,
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
onCancel,
|
||||
cancelText,
|
||||
onSubmit,
|
||||
submitText,
|
||||
disabled,
|
||||
} = this.props
|
||||
|
||||
return (
|
||||
<div className="page-container__footer">
|
||||
|
||||
<Button
|
||||
type="default"
|
||||
large={true}
|
||||
className="page-container__footer-button"
|
||||
onClick={() => onCancel()}
|
||||
>
|
||||
{ cancelText || this.context.t('cancel') }
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="primary"
|
||||
large={true}
|
||||
className="page-container__footer-button"
|
||||
disabled={disabled}
|
||||
onClick={e => onSubmit(e)}
|
||||
>
|
||||
{ submitText || this.context.t('next') }
|
||||
</Button>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
export default class PageContainerHeader extends Component {
|
||||
|
||||
static propTypes = {
|
||||
title: PropTypes.string,
|
||||
subtitle: PropTypes.string,
|
||||
onClose: PropTypes.func,
|
||||
};
|
||||
|
||||
render () {
|
||||
const { title, subtitle, onClose } = this.props
|
||||
|
||||
return (
|
||||
<div className="page-container__header">
|
||||
|
||||
<div className="page-container__title">
|
||||
{title}
|
||||
</div>
|
||||
|
||||
<div className="page-container__subtitle">
|
||||
{subtitle}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="page-container__header-close"
|
||||
onClick={() => onClose()}
|
||||
/>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1 @@
|
||||
export { default } from './page-container-header.component'
|
@ -0,0 +1,57 @@
|
||||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
export default class PageContainerHeader extends Component {
|
||||
|
||||
static propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
subtitle: PropTypes.string,
|
||||
onClose: PropTypes.func,
|
||||
showBackButton: PropTypes.bool,
|
||||
onBackButtonClick: PropTypes.func,
|
||||
backButtonStyles: PropTypes.object,
|
||||
backButtonString: PropTypes.string,
|
||||
};
|
||||
|
||||
renderHeaderRow () {
|
||||
const { showBackButton, onBackButtonClick, backButtonStyles, backButtonString } = this.props
|
||||
|
||||
return showBackButton && (
|
||||
<div className="page-container__header-row">
|
||||
<span
|
||||
className="page-container__back-button"
|
||||
onClick={onBackButtonClick}
|
||||
style={backButtonStyles}
|
||||
>
|
||||
{ backButtonString || 'Back' }
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
render () {
|
||||
const { title, subtitle, onClose } = this.props
|
||||
|
||||
return (
|
||||
<div className="page-container__header">
|
||||
|
||||
{ this.renderHeaderRow() }
|
||||
|
||||
<div className="page-container__title">
|
||||
{title}
|
||||
</div>
|
||||
|
||||
<div className="page-container__subtitle">
|
||||
{subtitle}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="page-container__header-close"
|
||||
onClick={() => onClose()}
|
||||
/>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
72
ui/app/components/page-container/page-container.component.js
Normal file
72
ui/app/components/page-container/page-container.component.js
Normal file
@ -0,0 +1,72 @@
|
||||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import PageContainerHeader from './page-container-header'
|
||||
import PageContainerFooter from './page-container-footer'
|
||||
|
||||
export default class PageContainer extends Component {
|
||||
|
||||
static propTypes = {
|
||||
// PageContainerHeader props
|
||||
title: PropTypes.string.isRequired,
|
||||
subtitle: PropTypes.string,
|
||||
onClose: PropTypes.func,
|
||||
showBackButton: PropTypes.bool,
|
||||
onBackButtonClick: PropTypes.func,
|
||||
backButtonStyles: PropTypes.object,
|
||||
backButtonString: PropTypes.string,
|
||||
// Content props
|
||||
ContentComponent: PropTypes.func,
|
||||
contentComponentProps: PropTypes.object,
|
||||
// PageContainerFooter props
|
||||
onCancel: PropTypes.func,
|
||||
cancelText: PropTypes.string,
|
||||
onSubmit: PropTypes.func,
|
||||
submitText: PropTypes.string,
|
||||
disabled: PropTypes.bool,
|
||||
};
|
||||
|
||||
render () {
|
||||
const {
|
||||
title,
|
||||
subtitle,
|
||||
onClose,
|
||||
showBackButton,
|
||||
onBackButtonClick,
|
||||
backButtonStyles,
|
||||
backButtonString,
|
||||
ContentComponent,
|
||||
contentComponentProps,
|
||||
onCancel,
|
||||
cancelText,
|
||||
onSubmit,
|
||||
submitText,
|
||||
disabled,
|
||||
} = this.props
|
||||
|
||||
return (
|
||||
<div className="page-container">
|
||||
<PageContainerHeader
|
||||
title={title}
|
||||
subtitle={subtitle}
|
||||
onClose={onClose}
|
||||
showBackButton={showBackButton}
|
||||
onBackButtonClick={onBackButtonClick}
|
||||
backButtonStyles={backButtonStyles}
|
||||
backButtonString={backButtonString}
|
||||
/>
|
||||
<div className="page-container__content">
|
||||
<ContentComponent { ...contentComponentProps } />
|
||||
</div>
|
||||
<PageContainerFooter
|
||||
onCancel={onCancel}
|
||||
cancelText={cancelText}
|
||||
onSubmit={onSubmit}
|
||||
submitText={submitText}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
@ -11,7 +11,7 @@ const { conversionUtil } = require('../../conversion-util')
|
||||
const SenderToRecipient = require('../sender-to-recipient')
|
||||
const NetworkDisplay = require('../network-display')
|
||||
|
||||
const { MIN_GAS_PRICE_HEX } = require('../send/send-constants')
|
||||
const { MIN_GAS_PRICE_HEX } = require('../send_/send.constants')
|
||||
|
||||
class ConfirmDeployContract extends Component {
|
||||
constructor (props) {
|
||||
|
@ -17,22 +17,26 @@ const {
|
||||
multiplyCurrencies,
|
||||
} = require('../../conversion-util')
|
||||
const {
|
||||
getGasTotal,
|
||||
calcGasTotal,
|
||||
isBalanceSufficient,
|
||||
} = require('../send/send-utils')
|
||||
} = require('../send_/send.utils')
|
||||
const GasFeeDisplay = require('../send/gas-fee-display-v2')
|
||||
const SenderToRecipient = require('../sender-to-recipient')
|
||||
const NetworkDisplay = require('../network-display')
|
||||
const currencyFormatter = require('currency-formatter')
|
||||
const currencies = require('currency-formatter/currencies')
|
||||
|
||||
const { MIN_GAS_PRICE_HEX } = require('../send/send-constants')
|
||||
const { MIN_GAS_PRICE_HEX } = require('../send_/send.constants')
|
||||
const { SEND_ROUTE, DEFAULT_ROUTE } = require('../../routes')
|
||||
const {
|
||||
ENVIRONMENT_TYPE_POPUP,
|
||||
ENVIRONMENT_TYPE_NOTIFICATION,
|
||||
} = require('../../../../app/scripts/lib/enums')
|
||||
|
||||
import {
|
||||
updateSendErrors,
|
||||
} from '../../ducks/send.duck'
|
||||
|
||||
ConfirmSendEther.contextTypes = {
|
||||
t: PropTypes.func,
|
||||
}
|
||||
@ -109,7 +113,7 @@ function mapDispatchToProps (dispatch) {
|
||||
}))
|
||||
dispatch(actions.showModal({ name: 'CUSTOMIZE_GAS' }))
|
||||
},
|
||||
updateSendErrors: error => dispatch(actions.updateSendErrors(error)),
|
||||
updateSendErrors: error => dispatch(updateSendErrors(error)),
|
||||
}
|
||||
}
|
||||
|
||||
@ -145,7 +149,7 @@ ConfirmSendEther.prototype.updateComponentSendErrors = function (prevProps) {
|
||||
if (shouldUpdateBalanceSendErrors) {
|
||||
const balanceIsSufficient = this.isBalanceSufficient(txMeta)
|
||||
updateSendErrors({
|
||||
insufficientFunds: balanceIsSufficient ? false : this.context.t('insufficientFunds'),
|
||||
insufficientFunds: balanceIsSufficient ? false : 'insufficientFunds',
|
||||
})
|
||||
}
|
||||
|
||||
@ -153,7 +157,7 @@ ConfirmSendEther.prototype.updateComponentSendErrors = function (prevProps) {
|
||||
|
||||
if (shouldUpdateSimulationSendError) {
|
||||
updateSendErrors({
|
||||
simulationFails: !txMeta.simulationFails ? false : this.context.t('transactionError'),
|
||||
simulationFails: !txMeta.simulationFails ? false : 'transactionError',
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -585,9 +589,9 @@ ConfirmSendEther.prototype.onSubmit = function (event) {
|
||||
if (valid && this.verifyGasParams() && balanceIsSufficient) {
|
||||
this.props.sendTransaction(txMeta, event)
|
||||
} else if (!balanceIsSufficient) {
|
||||
updateSendErrors({ insufficientFunds: this.context.t('insufficientFunds') })
|
||||
updateSendErrors({ insufficientFunds: 'insufficientFunds' })
|
||||
} else {
|
||||
updateSendErrors({ invalidGasParams: this.context.t('invalidGasParams') })
|
||||
updateSendErrors({ invalidGasParams: 'invalidGasParams' })
|
||||
this.setState({ submitting: false })
|
||||
}
|
||||
}
|
||||
@ -612,7 +616,7 @@ ConfirmSendEther.prototype.isBalanceSufficient = function (txMeta) {
|
||||
value: amount,
|
||||
},
|
||||
} = txMeta
|
||||
const gasTotal = getGasTotal(gas, gasPrice)
|
||||
const gasTotal = calcGasTotal(gas, gasPrice)
|
||||
|
||||
return isBalanceSufficient({
|
||||
amount,
|
||||
|
@ -21,9 +21,9 @@ const {
|
||||
addCurrencies,
|
||||
} = require('../../conversion-util')
|
||||
const {
|
||||
getGasTotal,
|
||||
calcGasTotal,
|
||||
isBalanceSufficient,
|
||||
} = require('../send/send-utils')
|
||||
} = require('../send_/send.utils')
|
||||
const {
|
||||
calcTokenAmount,
|
||||
} = require('../../token-util')
|
||||
@ -31,7 +31,7 @@ const classnames = require('classnames')
|
||||
const currencyFormatter = require('currency-formatter')
|
||||
const currencies = require('currency-formatter/currencies')
|
||||
|
||||
const { MIN_GAS_PRICE_HEX } = require('../send/send-constants')
|
||||
const { MIN_GAS_PRICE_HEX } = require('../send_/send.constants')
|
||||
|
||||
const {
|
||||
getTokenExchangeRate,
|
||||
@ -40,6 +40,10 @@ const {
|
||||
} = require('../../selectors')
|
||||
const { SEND_ROUTE, DEFAULT_ROUTE } = require('../../routes')
|
||||
|
||||
import {
|
||||
updateSendErrors,
|
||||
} from '../../ducks/send.duck'
|
||||
|
||||
const {
|
||||
ENVIRONMENT_TYPE_POPUP,
|
||||
ENVIRONMENT_TYPE_NOTIFICATION,
|
||||
@ -109,7 +113,7 @@ function mapDispatchToProps (dispatch, ownProps) {
|
||||
to,
|
||||
amount: tokenAmountInHex,
|
||||
errors: { to: null, amount: null },
|
||||
editingTransactionId: id,
|
||||
editingTransactionId: id && id.toString(),
|
||||
token: ownProps.token,
|
||||
}))
|
||||
dispatch(actions.showSendTokenPage())
|
||||
@ -147,7 +151,7 @@ function mapDispatchToProps (dispatch, ownProps) {
|
||||
}))
|
||||
dispatch(actions.showModal({ name: 'CUSTOMIZE_GAS' }))
|
||||
},
|
||||
updateSendErrors: error => dispatch(actions.updateSendErrors(error)),
|
||||
updateSendErrors: error => dispatch(updateSendErrors(error)),
|
||||
}
|
||||
}
|
||||
|
||||
@ -589,9 +593,9 @@ ConfirmSendToken.prototype.onSubmit = function (event) {
|
||||
if (valid && this.verifyGasParams() && balanceIsSufficient) {
|
||||
this.props.sendTransaction(txMeta, event)
|
||||
} else if (!balanceIsSufficient) {
|
||||
updateSendErrors({ insufficientFunds: this.context.t('insufficientFunds') })
|
||||
updateSendErrors({ insufficientFunds: 'insufficientFunds' })
|
||||
} else {
|
||||
updateSendErrors({ invalidGasParams: this.context.t('invalidGasParams') })
|
||||
updateSendErrors({ invalidGasParams: 'invalidGasParams' })
|
||||
this.setState({ submitting: false })
|
||||
}
|
||||
}
|
||||
@ -607,7 +611,7 @@ ConfirmSendToken.prototype.isBalanceSufficient = function (txMeta) {
|
||||
gasPrice,
|
||||
},
|
||||
} = txMeta
|
||||
const gasTotal = getGasTotal(gas, gasPrice)
|
||||
const gasTotal = calcGasTotal(gas, gasPrice)
|
||||
|
||||
return isBalanceSufficient({
|
||||
amount: '0',
|
||||
|
@ -1,74 +0,0 @@
|
||||
const Component = require('react').Component
|
||||
const h = require('react-hyperscript')
|
||||
const inherits = require('util').inherits
|
||||
const connect = require('react-redux').connect
|
||||
const { checksumAddress } = require('../../util')
|
||||
const Identicon = require('../identicon')
|
||||
const CurrencyDisplay = require('./currency-display')
|
||||
const { conversionRateSelector, getCurrentCurrency } = require('../../selectors')
|
||||
|
||||
inherits(AccountListItem, Component)
|
||||
function AccountListItem () {
|
||||
Component.call(this)
|
||||
}
|
||||
|
||||
function mapStateToProps (state) {
|
||||
return {
|
||||
conversionRate: conversionRateSelector(state),
|
||||
currentCurrency: getCurrentCurrency(state),
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = connect(mapStateToProps)(AccountListItem)
|
||||
|
||||
AccountListItem.prototype.render = function () {
|
||||
const {
|
||||
className,
|
||||
account,
|
||||
handleClick,
|
||||
icon = null,
|
||||
conversionRate,
|
||||
currentCurrency,
|
||||
displayBalance = true,
|
||||
displayAddress = false,
|
||||
} = this.props
|
||||
|
||||
const { name, address, balance } = account || {}
|
||||
|
||||
return h('div.account-list-item', {
|
||||
className,
|
||||
onClick: () => handleClick({ name, address, balance }),
|
||||
}, [
|
||||
|
||||
h('div.account-list-item__top-row', {}, [
|
||||
|
||||
h(
|
||||
Identicon,
|
||||
{
|
||||
address,
|
||||
diameter: 18,
|
||||
className: 'account-list-item__identicon',
|
||||
},
|
||||
),
|
||||
|
||||
h('div.account-list-item__account-name', {}, name || address),
|
||||
|
||||
icon && h('div.account-list-item__icon', [icon]),
|
||||
|
||||
]),
|
||||
|
||||
displayAddress && name && h('div.account-list-item__account-address', checksumAddress(address)),
|
||||
|
||||
displayBalance && h(CurrencyDisplay, {
|
||||
primaryCurrency: 'ETH',
|
||||
convertedCurrency: currentCurrency,
|
||||
value: balance,
|
||||
conversionRate,
|
||||
readOnly: true,
|
||||
className: 'account-list-item__account-balances',
|
||||
primaryBalanceClassName: 'account-list-item__account-primary-balance',
|
||||
convertedBalanceClassName: 'account-list-item__account-secondary-balance',
|
||||
}, name),
|
||||
|
||||
])
|
||||
}
|
@ -1,10 +1,10 @@
|
||||
const Component = require('react').Component
|
||||
const h = require('react-hyperscript')
|
||||
const inherits = require('util').inherits
|
||||
const CurrencyInput = require('../currency-input')
|
||||
const { conversionUtil, multiplyCurrencies } = require('../../conversion-util')
|
||||
const currencyFormatter = require('currency-formatter')
|
||||
const currencies = require('currency-formatter/currencies')
|
||||
const ethUtil = require('ethereumjs-util')
|
||||
|
||||
module.exports = CurrencyDisplay
|
||||
|
||||
@ -21,36 +21,51 @@ function toHexWei (value) {
|
||||
})
|
||||
}
|
||||
|
||||
CurrencyDisplay.prototype.componentWillMount = function () {
|
||||
this.setState({
|
||||
valueToRender: this.getValueToRender(this.props),
|
||||
})
|
||||
}
|
||||
|
||||
CurrencyDisplay.prototype.componentWillReceiveProps = function (nextProps) {
|
||||
const currentValueToRender = this.getValueToRender(this.props)
|
||||
const newValueToRender = this.getValueToRender(nextProps)
|
||||
if (currentValueToRender !== newValueToRender) {
|
||||
this.setState({
|
||||
valueToRender: newValueToRender,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
CurrencyDisplay.prototype.getAmount = function (value) {
|
||||
const { selectedToken } = this.props
|
||||
const { decimals } = selectedToken || {}
|
||||
const multiplier = Math.pow(10, Number(decimals || 0))
|
||||
|
||||
const sendAmount = multiplyCurrencies(value, multiplier, {toNumericBase: 'hex'})
|
||||
const sendAmount = multiplyCurrencies(value || '0', multiplier, {toNumericBase: 'hex'})
|
||||
|
||||
return selectedToken
|
||||
? sendAmount
|
||||
: toHexWei(value)
|
||||
}
|
||||
|
||||
CurrencyDisplay.prototype.getValueToRender = function () {
|
||||
const { selectedToken, conversionRate, value } = this.props
|
||||
|
||||
CurrencyDisplay.prototype.getValueToRender = function ({ selectedToken, conversionRate, value, readOnly }) {
|
||||
if (value === '0x0') return readOnly ? '0' : ''
|
||||
const { decimals, symbol } = selectedToken || {}
|
||||
const multiplier = Math.pow(10, Number(decimals || 0))
|
||||
|
||||
return selectedToken
|
||||
? conversionUtil(value, {
|
||||
? conversionUtil(ethUtil.addHexPrefix(value), {
|
||||
fromNumericBase: 'hex',
|
||||
toCurrency: symbol,
|
||||
conversionRate: multiplier,
|
||||
invertConversionRate: true,
|
||||
})
|
||||
: conversionUtil(value, {
|
||||
: conversionUtil(ethUtil.addHexPrefix(value), {
|
||||
fromNumericBase: 'hex',
|
||||
toNumericBase: 'dec',
|
||||
fromDenomination: 'WEI',
|
||||
numberOfDecimals: 6,
|
||||
numberOfDecimals: 9,
|
||||
conversionRate,
|
||||
})
|
||||
}
|
||||
@ -76,6 +91,18 @@ CurrencyDisplay.prototype.getConvertedValueToRender = function (nonFormattedValu
|
||||
: convertedValue
|
||||
}
|
||||
|
||||
CurrencyDisplay.prototype.handleChange = function (newVal) {
|
||||
this.setState({ valueToRender: newVal })
|
||||
this.props.onChange(this.getAmount(newVal))
|
||||
}
|
||||
|
||||
CurrencyDisplay.prototype.getInputWidth = function (valueToRender, readOnly) {
|
||||
const valueString = String(valueToRender)
|
||||
const valueLength = valueString.length || 1
|
||||
const decimalPointDeficit = valueString.match(/\./) ? -0.5 : 0
|
||||
return (valueLength + decimalPointDeficit + 0.75) + 'ch'
|
||||
}
|
||||
|
||||
CurrencyDisplay.prototype.render = function () {
|
||||
const {
|
||||
className = 'currency-display',
|
||||
@ -85,10 +112,10 @@ CurrencyDisplay.prototype.render = function () {
|
||||
convertedCurrency,
|
||||
readOnly = false,
|
||||
inError = false,
|
||||
handleChange,
|
||||
onBlur,
|
||||
} = this.props
|
||||
const { valueToRender } = this.state
|
||||
|
||||
const valueToRender = this.getValueToRender()
|
||||
const convertedValueToRender = this.getConvertedValueToRender(valueToRender)
|
||||
|
||||
return h('div', {
|
||||
@ -96,24 +123,30 @@ CurrencyDisplay.prototype.render = function () {
|
||||
style: {
|
||||
borderColor: inError ? 'red' : null,
|
||||
},
|
||||
onClick: () => this.currencyInput && this.currencyInput.focus(),
|
||||
onClick: () => {
|
||||
this.currencyInput && this.currencyInput.focus()
|
||||
},
|
||||
}, [
|
||||
|
||||
h('div.currency-display__primary-row', [
|
||||
|
||||
h('div.currency-display__input-wrapper', [
|
||||
|
||||
h(readOnly ? 'input' : CurrencyInput, {
|
||||
h('input', {
|
||||
className: primaryBalanceClassName,
|
||||
value: `${valueToRender}`,
|
||||
placeholder: '0',
|
||||
type: 'number',
|
||||
readOnly,
|
||||
...(!readOnly ? {
|
||||
onInputChange: newValue => {
|
||||
handleChange(this.getAmount(newValue))
|
||||
},
|
||||
inputRef: input => { this.currencyInput = input },
|
||||
onChange: e => this.handleChange(e.target.value),
|
||||
onBlur: () => onBlur(this.getAmount(valueToRender)),
|
||||
} : {}),
|
||||
ref: input => { this.currencyInput = input },
|
||||
style: {
|
||||
width: this.getInputWidth(valueToRender, readOnly),
|
||||
},
|
||||
min: 0,
|
||||
}),
|
||||
|
||||
h('span.currency-display__currency-symbol', primaryCurrency),
|
||||
|
@ -1,72 +0,0 @@
|
||||
const Component = require('react').Component
|
||||
const h = require('react-hyperscript')
|
||||
const inherits = require('util').inherits
|
||||
const AccountListItem = require('./account-list-item')
|
||||
|
||||
module.exports = FromDropdown
|
||||
|
||||
inherits(FromDropdown, Component)
|
||||
function FromDropdown () {
|
||||
Component.call(this)
|
||||
}
|
||||
|
||||
FromDropdown.prototype.getListItemIcon = function (currentAccount, selectedAccount) {
|
||||
const listItemIcon = h(`i.fa.fa-check.fa-lg`, { style: { color: '#02c9b1' } })
|
||||
|
||||
return currentAccount.address === selectedAccount.address
|
||||
? listItemIcon
|
||||
: null
|
||||
}
|
||||
|
||||
FromDropdown.prototype.renderDropdown = function () {
|
||||
const {
|
||||
accounts,
|
||||
selectedAccount,
|
||||
closeDropdown,
|
||||
onSelect,
|
||||
} = this.props
|
||||
|
||||
return h('div', {}, [
|
||||
|
||||
h('div.send-v2__from-dropdown__close-area', {
|
||||
onClick: closeDropdown,
|
||||
}),
|
||||
|
||||
h('div.send-v2__from-dropdown__list', {}, [
|
||||
|
||||
...accounts.map(account => h(AccountListItem, {
|
||||
className: 'account-list-item__dropdown',
|
||||
account,
|
||||
handleClick: () => {
|
||||
onSelect(account)
|
||||
closeDropdown()
|
||||
},
|
||||
icon: this.getListItemIcon(account, selectedAccount),
|
||||
})),
|
||||
|
||||
]),
|
||||
|
||||
])
|
||||
}
|
||||
|
||||
FromDropdown.prototype.render = function () {
|
||||
const {
|
||||
selectedAccount,
|
||||
openDropdown,
|
||||
dropdownOpen,
|
||||
} = this.props
|
||||
|
||||
return h('div.send-v2__from-dropdown', {}, [
|
||||
|
||||
h(AccountListItem, {
|
||||
account: selectedAccount,
|
||||
handleClick: openDropdown,
|
||||
icon: h(`i.fa.fa-caret-down.fa-lg`, { style: { color: '#dedede' } }),
|
||||
}),
|
||||
|
||||
dropdownOpen && this.renderDropdown(),
|
||||
|
||||
])
|
||||
|
||||
}
|
||||
|
@ -1,106 +0,0 @@
|
||||
const Component = require('react').Component
|
||||
const PropTypes = require('prop-types')
|
||||
const h = require('react-hyperscript')
|
||||
const inherits = require('util').inherits
|
||||
const InputNumber = require('../input-number.js')
|
||||
const connect = require('react-redux').connect
|
||||
|
||||
GasTooltip.contextTypes = {
|
||||
t: PropTypes.func,
|
||||
}
|
||||
|
||||
module.exports = connect()(GasTooltip)
|
||||
|
||||
|
||||
inherits(GasTooltip, Component)
|
||||
function GasTooltip () {
|
||||
Component.call(this)
|
||||
this.state = {
|
||||
gasLimit: 0,
|
||||
gasPrice: 0,
|
||||
}
|
||||
|
||||
this.updateGasPrice = this.updateGasPrice.bind(this)
|
||||
this.updateGasLimit = this.updateGasLimit.bind(this)
|
||||
this.onClose = this.onClose.bind(this)
|
||||
}
|
||||
|
||||
GasTooltip.prototype.componentWillMount = function () {
|
||||
const { gasPrice = 0, gasLimit = 0} = this.props
|
||||
|
||||
this.setState({
|
||||
gasPrice: parseInt(gasPrice, 16) / 1000000000,
|
||||
gasLimit: parseInt(gasLimit, 16),
|
||||
})
|
||||
}
|
||||
|
||||
GasTooltip.prototype.updateGasPrice = function (newPrice) {
|
||||
const { onFeeChange } = this.props
|
||||
const { gasLimit } = this.state
|
||||
|
||||
this.setState({ gasPrice: newPrice })
|
||||
onFeeChange({
|
||||
gasLimit: gasLimit.toString(16),
|
||||
gasPrice: (newPrice * 1000000000).toString(16),
|
||||
})
|
||||
}
|
||||
|
||||
GasTooltip.prototype.updateGasLimit = function (newLimit) {
|
||||
const { onFeeChange } = this.props
|
||||
const { gasPrice } = this.state
|
||||
|
||||
this.setState({ gasLimit: newLimit })
|
||||
onFeeChange({
|
||||
gasLimit: newLimit.toString(16),
|
||||
gasPrice: (gasPrice * 1000000000).toString(16),
|
||||
})
|
||||
}
|
||||
|
||||
GasTooltip.prototype.onClose = function (e) {
|
||||
e.stopPropagation()
|
||||
this.props.onClose()
|
||||
}
|
||||
|
||||
GasTooltip.prototype.render = function () {
|
||||
const { gasPrice, gasLimit } = this.state
|
||||
|
||||
return h('div.gas-tooltip', {}, [
|
||||
h('div.gas-tooltip-close-area', {
|
||||
onClick: this.onClose,
|
||||
}),
|
||||
h('div.customize-gas-tooltip-container', {}, [
|
||||
h('div.customize-gas-tooltip', {}, [
|
||||
h('div.gas-tooltip-header.gas-tooltip-label', {}, ['Customize Gas']),
|
||||
h('div.gas-tooltip-input-label', {}, [
|
||||
h('span.gas-tooltip-label', {}, ['Gas Price']),
|
||||
h('i.fa.fa-info-circle'),
|
||||
]),
|
||||
h(InputNumber, {
|
||||
unitLabel: 'GWEI',
|
||||
step: 1,
|
||||
min: 0,
|
||||
placeholder: '0',
|
||||
value: gasPrice,
|
||||
onChange: (newPrice) => this.updateGasPrice(newPrice),
|
||||
}),
|
||||
h('div.gas-tooltip-input-label', {
|
||||
style: {
|
||||
'marginTop': '81px',
|
||||
},
|
||||
}, [
|
||||
h('span.gas-tooltip-label', {}, [this.context.t('gasLimit')]),
|
||||
h('i.fa.fa-info-circle'),
|
||||
]),
|
||||
h(InputNumber, {
|
||||
unitLabel: 'UNITS',
|
||||
step: 1,
|
||||
min: 0,
|
||||
placeholder: '0',
|
||||
value: gasLimit,
|
||||
onChange: (newLimit) => this.updateGasLimit(newLimit),
|
||||
}),
|
||||
]),
|
||||
h('div.gas-tooltip-arrow', {}),
|
||||
]),
|
||||
])
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
// const Component = require('react').Component
|
||||
// const h = require('react-hyperscript')
|
||||
// const inherits = require('util').inherits
|
||||
// const Identicon = require('../identicon')
|
||||
|
||||
// module.exports = MemoTextArea
|
||||
|
||||
// inherits(MemoTextArea, Component)
|
||||
// function MemoTextArea () {
|
||||
// Component.call(this)
|
||||
// }
|
||||
|
||||
// MemoTextArea.prototype.render = function () {
|
||||
// const { memo, identities, onChange } = this.props
|
||||
|
||||
// return h('div.send-v2__memo-text-area', [
|
||||
|
||||
// h('textarea.send-v2__memo-text-area__input', {
|
||||
// placeholder: 'Optional',
|
||||
// value: memo,
|
||||
// onChange,
|
||||
// // onBlur: () => {
|
||||
// // this.setErrorsFor('memo')
|
||||
// // },
|
||||
// onFocus: event => {
|
||||
// // this.clearErrorsFor('memo')
|
||||
// },
|
||||
// }),
|
||||
|
||||
// ])
|
||||
|
||||
// }
|
||||
|
@ -1,78 +0,0 @@
|
||||
const {
|
||||
addCurrencies,
|
||||
conversionUtil,
|
||||
conversionGTE,
|
||||
multiplyCurrencies,
|
||||
} = require('../../conversion-util')
|
||||
const {
|
||||
calcTokenAmount,
|
||||
} = require('../../token-util')
|
||||
|
||||
function isBalanceSufficient ({
|
||||
amount = '0x0',
|
||||
gasTotal = '0x0',
|
||||
balance,
|
||||
primaryCurrency,
|
||||
amountConversionRate,
|
||||
conversionRate,
|
||||
}) {
|
||||
const totalAmount = addCurrencies(amount, gasTotal, {
|
||||
aBase: 16,
|
||||
bBase: 16,
|
||||
toNumericBase: 'hex',
|
||||
})
|
||||
|
||||
const balanceIsSufficient = conversionGTE(
|
||||
{
|
||||
value: balance,
|
||||
fromNumericBase: 'hex',
|
||||
fromCurrency: primaryCurrency,
|
||||
conversionRate,
|
||||
},
|
||||
{
|
||||
value: totalAmount,
|
||||
fromNumericBase: 'hex',
|
||||
conversionRate: amountConversionRate || conversionRate,
|
||||
fromCurrency: primaryCurrency,
|
||||
},
|
||||
)
|
||||
|
||||
return balanceIsSufficient
|
||||
}
|
||||
|
||||
function isTokenBalanceSufficient ({
|
||||
amount = '0x0',
|
||||
tokenBalance,
|
||||
decimals,
|
||||
}) {
|
||||
const amountInDec = conversionUtil(amount, {
|
||||
fromNumericBase: 'hex',
|
||||
})
|
||||
|
||||
const tokenBalanceIsSufficient = conversionGTE(
|
||||
{
|
||||
value: tokenBalance,
|
||||
fromNumericBase: 'dec',
|
||||
},
|
||||
{
|
||||
value: calcTokenAmount(amountInDec, decimals),
|
||||
fromNumericBase: 'dec',
|
||||
},
|
||||
)
|
||||
|
||||
return tokenBalanceIsSufficient
|
||||
}
|
||||
|
||||
function getGasTotal (gasLimit, gasPrice) {
|
||||
return multiplyCurrencies(gasLimit, gasPrice, {
|
||||
toNumericBase: 'hex',
|
||||
multiplicandBase: 16,
|
||||
multiplierBase: 16,
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getGasTotal,
|
||||
isBalanceSufficient,
|
||||
isTokenBalanceSufficient,
|
||||
}
|
@ -1,89 +0,0 @@
|
||||
const connect = require('react-redux').connect
|
||||
const actions = require('../../actions')
|
||||
const abi = require('ethereumjs-abi')
|
||||
const SendEther = require('../../send-v2')
|
||||
const { withRouter } = require('react-router-dom')
|
||||
const { compose } = require('recompose')
|
||||
|
||||
const {
|
||||
accountsWithSendEtherInfoSelector,
|
||||
getCurrentAccountWithSendEtherInfo,
|
||||
conversionRateSelector,
|
||||
getSelectedToken,
|
||||
getSelectedAddress,
|
||||
getAddressBook,
|
||||
getSendFrom,
|
||||
getCurrentCurrency,
|
||||
getSelectedTokenToFiatRate,
|
||||
getSelectedTokenContract,
|
||||
} = require('../../selectors')
|
||||
|
||||
module.exports = compose(
|
||||
withRouter,
|
||||
connect(mapStateToProps, mapDispatchToProps)
|
||||
)(SendEther)
|
||||
|
||||
function mapStateToProps (state) {
|
||||
const fromAccounts = accountsWithSendEtherInfoSelector(state)
|
||||
const selectedAddress = getSelectedAddress(state)
|
||||
const selectedToken = getSelectedToken(state)
|
||||
const conversionRate = conversionRateSelector(state)
|
||||
|
||||
let data
|
||||
let primaryCurrency
|
||||
let tokenToFiatRate
|
||||
if (selectedToken) {
|
||||
data = Array.prototype.map.call(
|
||||
abi.rawEncode(['address', 'uint256'], [selectedAddress, '0x0']),
|
||||
x => ('00' + x.toString(16)).slice(-2)
|
||||
).join('')
|
||||
|
||||
primaryCurrency = selectedToken.symbol
|
||||
|
||||
tokenToFiatRate = getSelectedTokenToFiatRate(state)
|
||||
}
|
||||
|
||||
return {
|
||||
...state.metamask.send,
|
||||
from: getSendFrom(state) || getCurrentAccountWithSendEtherInfo(state),
|
||||
fromAccounts,
|
||||
toAccounts: [...fromAccounts, ...getAddressBook(state)],
|
||||
conversionRate,
|
||||
selectedToken,
|
||||
primaryCurrency,
|
||||
convertedCurrency: getCurrentCurrency(state),
|
||||
data,
|
||||
selectedAddress,
|
||||
amountConversionRate: selectedToken ? tokenToFiatRate : conversionRate,
|
||||
tokenContract: getSelectedTokenContract(state),
|
||||
unapprovedTxs: state.metamask.unapprovedTxs,
|
||||
network: state.metamask.network,
|
||||
}
|
||||
}
|
||||
|
||||
function mapDispatchToProps (dispatch) {
|
||||
return {
|
||||
showCustomizeGasModal: () => dispatch(actions.showModal({ name: 'CUSTOMIZE_GAS' })),
|
||||
estimateGas: params => dispatch(actions.estimateGas(params)),
|
||||
getGasPrice: () => dispatch(actions.getGasPrice()),
|
||||
signTokenTx: (tokenAddress, toAddress, amount, txData) => (
|
||||
dispatch(actions.signTokenTx(tokenAddress, toAddress, amount, txData))
|
||||
),
|
||||
signTx: txParams => dispatch(actions.signTx(txParams)),
|
||||
updateAndApproveTx: txParams => dispatch(actions.updateAndApproveTx(txParams)),
|
||||
updateTx: txData => dispatch(actions.updateTransaction(txData)),
|
||||
setSelectedAddress: address => dispatch(actions.setSelectedAddress(address)),
|
||||
addToAddressBook: (address, nickname) => dispatch(actions.addToAddressBook(address, nickname)),
|
||||
updateGasTotal: newTotal => dispatch(actions.updateGasTotal(newTotal)),
|
||||
updateGasPrice: newGasPrice => dispatch(actions.updateGasPrice(newGasPrice)),
|
||||
updateGasLimit: newGasLimit => dispatch(actions.updateGasLimit(newGasLimit)),
|
||||
updateSendTokenBalance: tokenBalance => dispatch(actions.updateSendTokenBalance(tokenBalance)),
|
||||
updateSendFrom: newFrom => dispatch(actions.updateSendFrom(newFrom)),
|
||||
updateSendTo: (newTo, nickname) => dispatch(actions.updateSendTo(newTo, nickname)),
|
||||
updateSendAmount: newAmount => dispatch(actions.updateSendAmount(newAmount)),
|
||||
updateSendMemo: newMemo => dispatch(actions.updateSendMemo(newMemo)),
|
||||
updateSendErrors: newError => dispatch(actions.updateSendErrors(newError)),
|
||||
clearSend: () => dispatch(actions.clearSend()),
|
||||
setMaxModeTo: bool => dispatch(actions.setMaxModeTo(bool)),
|
||||
}
|
||||
}
|
@ -2,7 +2,7 @@ const Component = require('react').Component
|
||||
const PropTypes = require('prop-types')
|
||||
const h = require('react-hyperscript')
|
||||
const inherits = require('util').inherits
|
||||
const AccountListItem = require('./account-list-item')
|
||||
const AccountListItem = require('../send_/account-list-item/account-list-item.component').default
|
||||
const connect = require('react-redux').connect
|
||||
|
||||
ToAutoComplete.contextTypes = {
|
||||
|
0
ui/app/components/send_/README.md
Normal file
0
ui/app/components/send_/README.md
Normal file
@ -0,0 +1,74 @@
|
||||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { checksumAddress } from '../../../util'
|
||||
import Identicon from '../../identicon'
|
||||
import CurrencyDisplay from '../../send/currency-display'
|
||||
|
||||
export default class AccountListItem extends Component {
|
||||
|
||||
static propTypes = {
|
||||
account: PropTypes.object,
|
||||
className: PropTypes.string,
|
||||
conversionRate: PropTypes.number,
|
||||
currentCurrency: PropTypes.string,
|
||||
displayAddress: PropTypes.bool,
|
||||
displayBalance: PropTypes.bool,
|
||||
handleClick: PropTypes.func,
|
||||
icon: PropTypes.node,
|
||||
};
|
||||
|
||||
render () {
|
||||
const {
|
||||
account,
|
||||
className,
|
||||
conversionRate,
|
||||
currentCurrency,
|
||||
displayAddress = false,
|
||||
displayBalance = true,
|
||||
handleClick,
|
||||
icon = null,
|
||||
} = this.props
|
||||
|
||||
const { name, address, balance } = account || {}
|
||||
|
||||
return (<div
|
||||
className={`account-list-item ${className}`}
|
||||
onClick={() => handleClick({ name, address, balance })}
|
||||
>
|
||||
|
||||
<div className="account-list-item__top-row">
|
||||
<Identicon
|
||||
address={address}
|
||||
className="account-list-item__identicon"
|
||||
diameter={18}
|
||||
/>
|
||||
|
||||
<div className="account-list-item__account-name">{ name || address }</div>
|
||||
|
||||
{icon && <div className="account-list-item__icon">{ icon }</div>}
|
||||
|
||||
</div>
|
||||
|
||||
{displayAddress && name && <div className="account-list-item__account-address">
|
||||
{ checksumAddress(address) }
|
||||
</div>}
|
||||
|
||||
{displayBalance && <CurrencyDisplay
|
||||
className="account-list-item__account-balances"
|
||||
conversionRate={conversionRate}
|
||||
convertedBalanceClassName="account-list-item__account-secondary-balance"
|
||||
convertedCurrency={currentCurrency}
|
||||
primaryBalanceClassName="account-list-item__account-primary-balance"
|
||||
primaryCurrency="ETH"
|
||||
readOnly={true}
|
||||
value={balance}
|
||||
/>}
|
||||
|
||||
</div>)
|
||||
}
|
||||
}
|
||||
|
||||
AccountListItem.contextTypes = {
|
||||
t: PropTypes.func,
|
||||
}
|
||||
|
@ -0,0 +1,15 @@
|
||||
import { connect } from 'react-redux'
|
||||
import {
|
||||
getConversionRate,
|
||||
getConvertedCurrency,
|
||||
} from '../send.selectors.js'
|
||||
import AccountListItem from './account-list-item.component'
|
||||
|
||||
export default connect(mapStateToProps)(AccountListItem)
|
||||
|
||||
function mapStateToProps (state) {
|
||||
return {
|
||||
conversionRate: getConversionRate(state),
|
||||
currentCurrency: getConvertedCurrency(state),
|
||||
}
|
||||
}
|
1
ui/app/components/send_/account-list-item/index.js
Normal file
1
ui/app/components/send_/account-list-item/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './account-list-item.container'
|
@ -0,0 +1,138 @@
|
||||
import React from 'react'
|
||||
import assert from 'assert'
|
||||
import { shallow } from 'enzyme'
|
||||
import sinon from 'sinon'
|
||||
import proxyquire from 'proxyquire'
|
||||
import Identicon from '../../../identicon'
|
||||
import CurrencyDisplay from '../../../send/currency-display'
|
||||
|
||||
const utilsMethodStubs = {
|
||||
checksumAddress: sinon.stub().returns('mockCheckSumAddress'),
|
||||
}
|
||||
|
||||
const AccountListItem = proxyquire('../account-list-item.component.js', {
|
||||
'../../../util': utilsMethodStubs,
|
||||
}).default
|
||||
|
||||
|
||||
const propsMethodSpies = {
|
||||
handleClick: sinon.spy(),
|
||||
}
|
||||
|
||||
describe('AccountListItem Component', function () {
|
||||
let wrapper
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = shallow(<AccountListItem
|
||||
account={ { address: 'mockAddress', name: 'mockName', balance: 'mockBalance' } }
|
||||
className={'mockClassName'}
|
||||
conversionRate={4}
|
||||
currentCurrency={'mockCurrentyCurrency'}
|
||||
displayAddress={false}
|
||||
displayBalance={false}
|
||||
handleClick={propsMethodSpies.handleClick}
|
||||
icon={<i className="mockIcon" />}
|
||||
/>, { context: { t: str => str + '_t' } })
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
propsMethodSpies.handleClick.resetHistory()
|
||||
})
|
||||
|
||||
describe('render', () => {
|
||||
it('should render a div with the passed className', () => {
|
||||
assert.equal(wrapper.find('.mockClassName').length, 1)
|
||||
assert(wrapper.find('.mockClassName').is('div'))
|
||||
assert(wrapper.find('.mockClassName').hasClass('account-list-item'))
|
||||
})
|
||||
|
||||
it('should call handleClick with the expected props when the root div is clicked', () => {
|
||||
const { onClick } = wrapper.find('.mockClassName').props()
|
||||
assert.equal(propsMethodSpies.handleClick.callCount, 0)
|
||||
onClick()
|
||||
assert.equal(propsMethodSpies.handleClick.callCount, 1)
|
||||
assert.deepEqual(
|
||||
propsMethodSpies.handleClick.getCall(0).args,
|
||||
[{ address: 'mockAddress', name: 'mockName', balance: 'mockBalance' }]
|
||||
)
|
||||
})
|
||||
|
||||
it('should have a top row div', () => {
|
||||
assert.equal(wrapper.find('.mockClassName > .account-list-item__top-row').length, 1)
|
||||
assert(wrapper.find('.mockClassName > .account-list-item__top-row').is('div'))
|
||||
})
|
||||
|
||||
it('should have an identicon, name and icon in the top row', () => {
|
||||
const topRow = wrapper.find('.mockClassName > .account-list-item__top-row')
|
||||
assert.equal(topRow.find(Identicon).length, 1)
|
||||
assert.equal(topRow.find('.account-list-item__account-name').length, 1)
|
||||
assert.equal(topRow.find('.account-list-item__icon').length, 1)
|
||||
})
|
||||
|
||||
it('should show the account name if it exists', () => {
|
||||
const topRow = wrapper.find('.mockClassName > .account-list-item__top-row')
|
||||
assert.equal(topRow.find('.account-list-item__account-name').text(), 'mockName')
|
||||
})
|
||||
|
||||
it('should show the account address if there is no name', () => {
|
||||
wrapper.setProps({ account: { address: 'addressButNoName' } })
|
||||
const topRow = wrapper.find('.mockClassName > .account-list-item__top-row')
|
||||
assert.equal(topRow.find('.account-list-item__account-name').text(), 'addressButNoName')
|
||||
})
|
||||
|
||||
it('should render the passed icon', () => {
|
||||
const topRow = wrapper.find('.mockClassName > .account-list-item__top-row')
|
||||
assert(topRow.find('.account-list-item__icon').childAt(0).is('i'))
|
||||
assert(topRow.find('.account-list-item__icon').childAt(0).hasClass('mockIcon'))
|
||||
})
|
||||
|
||||
it('should not render an icon if none is passed', () => {
|
||||
wrapper.setProps({ icon: null })
|
||||
const topRow = wrapper.find('.mockClassName > .account-list-item__top-row')
|
||||
assert.equal(topRow.find('.account-list-item__icon').length, 0)
|
||||
})
|
||||
|
||||
it('should render the account address as a checksumAddress if displayAddress is true and name is provided', () => {
|
||||
wrapper.setProps({ displayAddress: true })
|
||||
assert.equal(wrapper.find('.account-list-item__account-address').length, 1)
|
||||
assert.equal(wrapper.find('.account-list-item__account-address').text(), 'mockCheckSumAddress')
|
||||
assert.deepEqual(
|
||||
utilsMethodStubs.checksumAddress.getCall(0).args,
|
||||
['mockAddress']
|
||||
)
|
||||
})
|
||||
|
||||
it('should not render the account address as a checksumAddress if displayAddress is false', () => {
|
||||
wrapper.setProps({ displayAddress: false })
|
||||
assert.equal(wrapper.find('.account-list-item__account-address').length, 0)
|
||||
})
|
||||
|
||||
it('should not render the account address as a checksumAddress if name is not provided', () => {
|
||||
wrapper.setProps({ account: { address: 'someAddressButNoName' } })
|
||||
assert.equal(wrapper.find('.account-list-item__account-address').length, 0)
|
||||
})
|
||||
|
||||
it('should render a CurrencyDisplay with the correct props if displayBalance is true', () => {
|
||||
wrapper.setProps({ displayBalance: true })
|
||||
assert.equal(wrapper.find(CurrencyDisplay).length, 1)
|
||||
assert.deepEqual(
|
||||
wrapper.find(CurrencyDisplay).props(),
|
||||
{
|
||||
className: 'account-list-item__account-balances',
|
||||
conversionRate: 4,
|
||||
convertedBalanceClassName: 'account-list-item__account-secondary-balance',
|
||||
convertedCurrency: 'mockCurrentyCurrency',
|
||||
primaryBalanceClassName: 'account-list-item__account-primary-balance',
|
||||
primaryCurrency: 'ETH',
|
||||
readOnly: true,
|
||||
value: 'mockBalance',
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('should not render a CurrencyDisplay if displayBalance is false', () => {
|
||||
wrapper.setProps({ displayBalance: false })
|
||||
assert.equal(wrapper.find(CurrencyDisplay).length, 0)
|
||||
})
|
||||
})
|
||||
})
|
@ -0,0 +1,32 @@
|
||||
import assert from 'assert'
|
||||
import proxyquire from 'proxyquire'
|
||||
|
||||
let mapStateToProps
|
||||
|
||||
proxyquire('../account-list-item.container.js', {
|
||||
'react-redux': {
|
||||
connect: (ms, md) => {
|
||||
mapStateToProps = ms
|
||||
return () => ({})
|
||||
},
|
||||
},
|
||||
'../send.selectors.js': {
|
||||
getConversionRate: (s) => `mockConversionRate:${s}`,
|
||||
getConvertedCurrency: (s) => `mockCurrentCurrency:${s}`,
|
||||
},
|
||||
})
|
||||
|
||||
describe('account-list-item container', () => {
|
||||
|
||||
describe('mapStateToProps()', () => {
|
||||
|
||||
it('should map the correct properties to props', () => {
|
||||
assert.deepEqual(mapStateToProps('mockState'), {
|
||||
conversionRate: 'mockConversionRate:mockState',
|
||||
currentCurrency: 'mockCurrentCurrency:mockState',
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
})
|
1
ui/app/components/send_/index.js
Normal file
1
ui/app/components/send_/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './send.container'
|
1
ui/app/components/send_/send-content/index.js
Normal file
1
ui/app/components/send_/send-content/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './send-content.component'
|
@ -0,0 +1,54 @@
|
||||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
export default class AmountMaxButton extends Component {
|
||||
|
||||
static propTypes = {
|
||||
balance: PropTypes.string,
|
||||
gasTotal: PropTypes.string,
|
||||
maxModeOn: PropTypes.bool,
|
||||
selectedToken: PropTypes.object,
|
||||
setAmountToMax: PropTypes.func,
|
||||
setMaxModeTo: PropTypes.func,
|
||||
tokenBalance: PropTypes.string,
|
||||
};
|
||||
|
||||
setMaxAmount () {
|
||||
const {
|
||||
balance,
|
||||
gasTotal,
|
||||
selectedToken,
|
||||
setAmountToMax,
|
||||
tokenBalance,
|
||||
} = this.props
|
||||
|
||||
setAmountToMax({
|
||||
balance,
|
||||
gasTotal,
|
||||
selectedToken,
|
||||
tokenBalance,
|
||||
})
|
||||
}
|
||||
|
||||
render () {
|
||||
const { setMaxModeTo, maxModeOn } = this.props
|
||||
|
||||
return (
|
||||
<div
|
||||
className="send-v2__amount-max"
|
||||
onClick={(event) => {
|
||||
event.preventDefault()
|
||||
setMaxModeTo(true)
|
||||
this.setMaxAmount()
|
||||
}}
|
||||
>
|
||||
{!maxModeOn ? this.context.t('max') : ''}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
AmountMaxButton.contextTypes = {
|
||||
t: PropTypes.func,
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
import { connect } from 'react-redux'
|
||||
import {
|
||||
getGasTotal,
|
||||
getSelectedToken,
|
||||
getSendFromBalance,
|
||||
getTokenBalance,
|
||||
} from '../../../send.selectors.js'
|
||||
import { getMaxModeOn } from './amount-max-button.selectors.js'
|
||||
import { calcMaxAmount } from './amount-max-button.utils.js'
|
||||
import {
|
||||
updateSendAmount,
|
||||
setMaxModeTo,
|
||||
} from '../../../../../actions'
|
||||
import AmountMaxButton from './amount-max-button.component'
|
||||
import {
|
||||
updateSendErrors,
|
||||
} from '../../../../../ducks/send.duck'
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(AmountMaxButton)
|
||||
|
||||
function mapStateToProps (state) {
|
||||
|
||||
return {
|
||||
balance: getSendFromBalance(state),
|
||||
gasTotal: getGasTotal(state),
|
||||
maxModeOn: getMaxModeOn(state),
|
||||
selectedToken: getSelectedToken(state),
|
||||
tokenBalance: getTokenBalance(state),
|
||||
}
|
||||
}
|
||||
|
||||
function mapDispatchToProps (dispatch) {
|
||||
return {
|
||||
setAmountToMax: maxAmountDataObject => {
|
||||
dispatch(updateSendErrors({ amount: null }))
|
||||
dispatch(updateSendAmount(calcMaxAmount(maxAmountDataObject)))
|
||||
},
|
||||
setMaxModeTo: bool => dispatch(setMaxModeTo(bool)),
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
const selectors = {
|
||||
getMaxModeOn,
|
||||
}
|
||||
|
||||
module.exports = selectors
|
||||
|
||||
function getMaxModeOn (state) {
|
||||
return state.metamask.send.maxModeOn
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
const {
|
||||
multiplyCurrencies,
|
||||
subtractCurrencies,
|
||||
} = require('../../../../../conversion-util')
|
||||
const ethUtil = require('ethereumjs-util')
|
||||
|
||||
function calcMaxAmount ({ balance, gasTotal, selectedToken, tokenBalance }) {
|
||||
const { decimals } = selectedToken || {}
|
||||
const multiplier = Math.pow(10, Number(decimals || 0))
|
||||
|
||||
return selectedToken
|
||||
? multiplyCurrencies(tokenBalance, multiplier, {toNumericBase: 'hex'})
|
||||
: subtractCurrencies(
|
||||
ethUtil.addHexPrefix(balance),
|
||||
ethUtil.addHexPrefix(gasTotal),
|
||||
{ toNumericBase: 'hex' }
|
||||
)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
calcMaxAmount,
|
||||
}
|
@ -0,0 +1 @@
|
||||
export { default } from './amount-max-button.container'
|
@ -0,0 +1,90 @@
|
||||
import React from 'react'
|
||||
import assert from 'assert'
|
||||
import { shallow } from 'enzyme'
|
||||
import sinon from 'sinon'
|
||||
import AmountMaxButton from '../amount-max-button.component.js'
|
||||
|
||||
const propsMethodSpies = {
|
||||
setAmountToMax: sinon.spy(),
|
||||
setMaxModeTo: sinon.spy(),
|
||||
}
|
||||
|
||||
const MOCK_EVENT = { preventDefault: () => {} }
|
||||
|
||||
sinon.spy(AmountMaxButton.prototype, 'setMaxAmount')
|
||||
|
||||
describe('AmountMaxButton Component', function () {
|
||||
let wrapper
|
||||
let instance
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = shallow(<AmountMaxButton
|
||||
balance={'mockBalance'}
|
||||
gasTotal={'mockGasTotal'}
|
||||
maxModeOn={false}
|
||||
selectedToken={ { address: 'mockTokenAddress' } }
|
||||
setAmountToMax={propsMethodSpies.setAmountToMax}
|
||||
setMaxModeTo={propsMethodSpies.setMaxModeTo}
|
||||
tokenBalance={'mockTokenBalance'}
|
||||
/>, { context: { t: str => str + '_t' } })
|
||||
instance = wrapper.instance()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
propsMethodSpies.setAmountToMax.resetHistory()
|
||||
propsMethodSpies.setMaxModeTo.resetHistory()
|
||||
AmountMaxButton.prototype.setMaxAmount.resetHistory()
|
||||
})
|
||||
|
||||
describe('setMaxAmount', () => {
|
||||
|
||||
it('should call setAmountToMax with the correct params', () => {
|
||||
assert.equal(propsMethodSpies.setAmountToMax.callCount, 0)
|
||||
instance.setMaxAmount()
|
||||
assert.equal(propsMethodSpies.setAmountToMax.callCount, 1)
|
||||
assert.deepEqual(
|
||||
propsMethodSpies.setAmountToMax.getCall(0).args,
|
||||
[{
|
||||
balance: 'mockBalance',
|
||||
gasTotal: 'mockGasTotal',
|
||||
selectedToken: { address: 'mockTokenAddress' },
|
||||
tokenBalance: 'mockTokenBalance',
|
||||
}]
|
||||
)
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
describe('render', () => {
|
||||
it('should render a div with a send-v2__amount-max class', () => {
|
||||
assert.equal(wrapper.find('.send-v2__amount-max').length, 1)
|
||||
assert(wrapper.find('.send-v2__amount-max').is('div'))
|
||||
})
|
||||
|
||||
it('should call setMaxModeTo and setMaxAmount when the send-v2__amount-max div is clicked', () => {
|
||||
const {
|
||||
onClick,
|
||||
} = wrapper.find('.send-v2__amount-max').props()
|
||||
|
||||
assert.equal(AmountMaxButton.prototype.setMaxAmount.callCount, 0)
|
||||
assert.equal(propsMethodSpies.setMaxModeTo.callCount, 0)
|
||||
onClick(MOCK_EVENT)
|
||||
assert.equal(AmountMaxButton.prototype.setMaxAmount.callCount, 1)
|
||||
assert.equal(propsMethodSpies.setMaxModeTo.callCount, 1)
|
||||
assert.deepEqual(
|
||||
propsMethodSpies.setMaxModeTo.getCall(0).args,
|
||||
[true]
|
||||
)
|
||||
})
|
||||
|
||||
it('should not render text when maxModeOn is true', () => {
|
||||
wrapper.setProps({ maxModeOn: true })
|
||||
assert.equal(wrapper.find('.send-v2__amount-max').text(), '')
|
||||
})
|
||||
|
||||
it('should render the expected text when maxModeOn is false', () => {
|
||||
wrapper.setProps({ maxModeOn: false })
|
||||
assert.equal(wrapper.find('.send-v2__amount-max').text(), 'max_t')
|
||||
})
|
||||
})
|
||||
})
|
@ -0,0 +1,91 @@
|
||||
import assert from 'assert'
|
||||
import proxyquire from 'proxyquire'
|
||||
import sinon from 'sinon'
|
||||
|
||||
let mapStateToProps
|
||||
let mapDispatchToProps
|
||||
|
||||
const actionSpies = {
|
||||
setMaxModeTo: sinon.spy(),
|
||||
updateSendAmount: sinon.spy(),
|
||||
}
|
||||
const duckActionSpies = {
|
||||
updateSendErrors: sinon.spy(),
|
||||
}
|
||||
|
||||
proxyquire('../amount-max-button.container.js', {
|
||||
'react-redux': {
|
||||
connect: (ms, md) => {
|
||||
mapStateToProps = ms
|
||||
mapDispatchToProps = md
|
||||
return () => ({})
|
||||
},
|
||||
},
|
||||
'../../../send.selectors.js': {
|
||||
getGasTotal: (s) => `mockGasTotal:${s}`,
|
||||
getSelectedToken: (s) => `mockSelectedToken:${s}`,
|
||||
getSendFromBalance: (s) => `mockBalance:${s}`,
|
||||
getTokenBalance: (s) => `mockTokenBalance:${s}`,
|
||||
},
|
||||
'./amount-max-button.selectors.js': { getMaxModeOn: (s) => `mockMaxModeOn:${s}` },
|
||||
'./amount-max-button.utils.js': { calcMaxAmount: (mockObj) => mockObj.val + 1 },
|
||||
'../../../../../actions': actionSpies,
|
||||
'../../../../../ducks/send.duck': duckActionSpies,
|
||||
})
|
||||
|
||||
describe('amount-max-button container', () => {
|
||||
|
||||
describe('mapStateToProps()', () => {
|
||||
|
||||
it('should map the correct properties to props', () => {
|
||||
assert.deepEqual(mapStateToProps('mockState'), {
|
||||
balance: 'mockBalance:mockState',
|
||||
gasTotal: 'mockGasTotal:mockState',
|
||||
maxModeOn: 'mockMaxModeOn:mockState',
|
||||
selectedToken: 'mockSelectedToken:mockState',
|
||||
tokenBalance: 'mockTokenBalance:mockState',
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
describe('mapDispatchToProps()', () => {
|
||||
let dispatchSpy
|
||||
let mapDispatchToPropsObject
|
||||
|
||||
beforeEach(() => {
|
||||
dispatchSpy = sinon.spy()
|
||||
mapDispatchToPropsObject = mapDispatchToProps(dispatchSpy)
|
||||
})
|
||||
|
||||
describe('setAmountToMax()', () => {
|
||||
it('should dispatch an action', () => {
|
||||
mapDispatchToPropsObject.setAmountToMax({ val: 11, foo: 'bar' })
|
||||
assert(dispatchSpy.calledTwice)
|
||||
assert(duckActionSpies.updateSendErrors.calledOnce)
|
||||
assert.deepEqual(
|
||||
duckActionSpies.updateSendErrors.getCall(0).args[0],
|
||||
{ amount: null }
|
||||
)
|
||||
assert(actionSpies.updateSendAmount.calledOnce)
|
||||
assert.equal(
|
||||
actionSpies.updateSendAmount.getCall(0).args[0],
|
||||
12
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('setMaxModeTo()', () => {
|
||||
it('should dispatch an action', () => {
|
||||
mapDispatchToPropsObject.setMaxModeTo('mockVal')
|
||||
assert(dispatchSpy.calledOnce)
|
||||
assert.equal(
|
||||
actionSpies.setMaxModeTo.getCall(0).args[0],
|
||||
'mockVal'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
})
|
@ -0,0 +1,22 @@
|
||||
import assert from 'assert'
|
||||
import {
|
||||
getMaxModeOn,
|
||||
} from '../amount-max-button.selectors.js'
|
||||
|
||||
describe('amount-max-button selectors', () => {
|
||||
|
||||
describe('getMaxModeOn()', () => {
|
||||
it('should', () => {
|
||||
const state = {
|
||||
metamask: {
|
||||
send: {
|
||||
maxModeOn: null,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
assert.equal(getMaxModeOn(state), null)
|
||||
})
|
||||
})
|
||||
|
||||
})
|
@ -0,0 +1,27 @@
|
||||
import assert from 'assert'
|
||||
import {
|
||||
calcMaxAmount,
|
||||
} from '../amount-max-button.utils.js'
|
||||
|
||||
describe('amount-max-button utils', () => {
|
||||
|
||||
describe('calcMaxAmount()', () => {
|
||||
it('should calculate the correct amount when no selectedToken defined', () => {
|
||||
assert.deepEqual(calcMaxAmount({
|
||||
balance: 'ffffff',
|
||||
gasTotal: 'ff',
|
||||
selectedToken: false,
|
||||
}), 'ffff00')
|
||||
})
|
||||
|
||||
it('should calculate the correct amount when a selectedToken is defined', () => {
|
||||
assert.deepEqual(calcMaxAmount({
|
||||
selectedToken: {
|
||||
decimals: 10,
|
||||
},
|
||||
tokenBalance: 100,
|
||||
}), 'e8d4a51000')
|
||||
})
|
||||
})
|
||||
|
||||
})
|
@ -0,0 +1 @@
|
||||
export { default } from './send-amount-row.container'
|
@ -0,0 +1,96 @@
|
||||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import SendRowWrapper from '../send-row-wrapper/'
|
||||
import AmountMaxButton from './amount-max-button/'
|
||||
import CurrencyDisplay from '../../../send/currency-display'
|
||||
|
||||
export default class SendAmountRow extends Component {
|
||||
|
||||
static propTypes = {
|
||||
amount: PropTypes.string,
|
||||
amountConversionRate: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.number,
|
||||
]),
|
||||
balance: PropTypes.string,
|
||||
conversionRate: PropTypes.number,
|
||||
convertedCurrency: PropTypes.string,
|
||||
gasTotal: PropTypes.string,
|
||||
inError: PropTypes.bool,
|
||||
primaryCurrency: PropTypes.string,
|
||||
selectedToken: PropTypes.object,
|
||||
setMaxModeTo: PropTypes.func,
|
||||
tokenBalance: PropTypes.string,
|
||||
updateSendAmount: PropTypes.func,
|
||||
updateSendAmountError: PropTypes.func,
|
||||
}
|
||||
|
||||
validateAmount (amount) {
|
||||
const {
|
||||
amountConversionRate,
|
||||
balance,
|
||||
conversionRate,
|
||||
gasTotal,
|
||||
primaryCurrency,
|
||||
selectedToken,
|
||||
tokenBalance,
|
||||
updateSendAmountError,
|
||||
} = this.props
|
||||
|
||||
updateSendAmountError({
|
||||
amount,
|
||||
amountConversionRate,
|
||||
balance,
|
||||
conversionRate,
|
||||
gasTotal,
|
||||
primaryCurrency,
|
||||
selectedToken,
|
||||
tokenBalance,
|
||||
})
|
||||
}
|
||||
|
||||
updateAmount (amount) {
|
||||
const { updateSendAmount, setMaxModeTo } = this.props
|
||||
|
||||
setMaxModeTo(false)
|
||||
updateSendAmount(amount)
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
amount,
|
||||
amountConversionRate,
|
||||
convertedCurrency,
|
||||
gasTotal,
|
||||
inError,
|
||||
primaryCurrency,
|
||||
selectedToken,
|
||||
} = this.props
|
||||
|
||||
return (
|
||||
<SendRowWrapper
|
||||
label={`${this.context.t('amount')}:`}
|
||||
showError={inError}
|
||||
errorType={'amount'}
|
||||
>
|
||||
{!inError && gasTotal && <AmountMaxButton />}
|
||||
<CurrencyDisplay
|
||||
conversionRate={amountConversionRate}
|
||||
convertedCurrency={convertedCurrency}
|
||||
onBlur={newAmount => this.updateAmount(newAmount)}
|
||||
onChange={newAmount => this.validateAmount(newAmount)}
|
||||
inError={inError}
|
||||
primaryCurrency={primaryCurrency || 'ETH'}
|
||||
selectedToken={selectedToken}
|
||||
value={amount || '0x0'}
|
||||
/>
|
||||
</SendRowWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
SendAmountRow.contextTypes = {
|
||||
t: PropTypes.func,
|
||||
}
|
||||
|
@ -0,0 +1,51 @@
|
||||
import { connect } from 'react-redux'
|
||||
import {
|
||||
getAmountConversionRate,
|
||||
getConversionRate,
|
||||
getConvertedCurrency,
|
||||
getGasTotal,
|
||||
getPrimaryCurrency,
|
||||
getSelectedToken,
|
||||
getSendAmount,
|
||||
getSendFromBalance,
|
||||
getTokenBalance,
|
||||
} from '../../send.selectors'
|
||||
import {
|
||||
sendAmountIsInError,
|
||||
} from './send-amount-row.selectors'
|
||||
import { getAmountErrorObject } from '../../send.utils'
|
||||
import {
|
||||
setMaxModeTo,
|
||||
updateSendAmount,
|
||||
} from '../../../../actions'
|
||||
import {
|
||||
updateSendErrors,
|
||||
} from '../../../../ducks/send.duck'
|
||||
import SendAmountRow from './send-amount-row.component'
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(SendAmountRow)
|
||||
|
||||
function mapStateToProps (state) {
|
||||
return {
|
||||
amount: getSendAmount(state),
|
||||
amountConversionRate: getAmountConversionRate(state),
|
||||
balance: getSendFromBalance(state),
|
||||
conversionRate: getConversionRate(state),
|
||||
convertedCurrency: getConvertedCurrency(state),
|
||||
gasTotal: getGasTotal(state),
|
||||
inError: sendAmountIsInError(state),
|
||||
primaryCurrency: getPrimaryCurrency(state),
|
||||
selectedToken: getSelectedToken(state),
|
||||
tokenBalance: getTokenBalance(state),
|
||||
}
|
||||
}
|
||||
|
||||
function mapDispatchToProps (dispatch) {
|
||||
return {
|
||||
setMaxModeTo: bool => dispatch(setMaxModeTo(bool)),
|
||||
updateSendAmount: newAmount => dispatch(updateSendAmount(newAmount)),
|
||||
updateSendAmountError: (amountDataObject) => {
|
||||
dispatch(updateSendErrors(getAmountErrorObject(amountDataObject)))
|
||||
},
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
const selectors = {
|
||||
sendAmountIsInError,
|
||||
}
|
||||
|
||||
module.exports = selectors
|
||||
|
||||
function sendAmountIsInError (state) {
|
||||
return Boolean(state.send.errors.amount)
|
||||
}
|
@ -0,0 +1,164 @@
|
||||
import React from 'react'
|
||||
import assert from 'assert'
|
||||
import { shallow } from 'enzyme'
|
||||
import sinon from 'sinon'
|
||||
import SendAmountRow from '../send-amount-row.component.js'
|
||||
|
||||
import SendRowWrapper from '../../send-row-wrapper/send-row-wrapper.component'
|
||||
import AmountMaxButton from '../amount-max-button/amount-max-button.container'
|
||||
import CurrencyDisplay from '../../../../send/currency-display'
|
||||
|
||||
const propsMethodSpies = {
|
||||
setMaxModeTo: sinon.spy(),
|
||||
updateSendAmount: sinon.spy(),
|
||||
updateSendAmountError: sinon.spy(),
|
||||
}
|
||||
|
||||
sinon.spy(SendAmountRow.prototype, 'updateAmount')
|
||||
sinon.spy(SendAmountRow.prototype, 'validateAmount')
|
||||
|
||||
describe('SendAmountRow Component', function () {
|
||||
let wrapper
|
||||
let instance
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = shallow(<SendAmountRow
|
||||
amount={'mockAmount'}
|
||||
amountConversionRate={'mockAmountConversionRate'}
|
||||
balance={'mockBalance'}
|
||||
conversionRate={7}
|
||||
convertedCurrency={'mockConvertedCurrency'}
|
||||
gasTotal={'mockGasTotal'}
|
||||
inError={false}
|
||||
primaryCurrency={'mockPrimaryCurrency'}
|
||||
selectedToken={ { address: 'mockTokenAddress' } }
|
||||
setMaxModeTo={propsMethodSpies.setMaxModeTo}
|
||||
tokenBalance={'mockTokenBalance'}
|
||||
updateSendAmount={propsMethodSpies.updateSendAmount}
|
||||
updateSendAmountError={propsMethodSpies.updateSendAmountError}
|
||||
/>, { context: { t: str => str + '_t' } })
|
||||
instance = wrapper.instance()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
propsMethodSpies.setMaxModeTo.resetHistory()
|
||||
propsMethodSpies.updateSendAmount.resetHistory()
|
||||
propsMethodSpies.updateSendAmountError.resetHistory()
|
||||
SendAmountRow.prototype.validateAmount.resetHistory()
|
||||
SendAmountRow.prototype.updateAmount.resetHistory()
|
||||
})
|
||||
|
||||
describe('validateAmount', () => {
|
||||
|
||||
it('should call updateSendAmountError with the correct params', () => {
|
||||
assert.equal(propsMethodSpies.updateSendAmountError.callCount, 0)
|
||||
instance.validateAmount('someAmount')
|
||||
assert.equal(propsMethodSpies.updateSendAmountError.callCount, 1)
|
||||
assert.deepEqual(
|
||||
propsMethodSpies.updateSendAmountError.getCall(0).args,
|
||||
[{
|
||||
amount: 'someAmount',
|
||||
amountConversionRate: 'mockAmountConversionRate',
|
||||
balance: 'mockBalance',
|
||||
conversionRate: 7,
|
||||
gasTotal: 'mockGasTotal',
|
||||
primaryCurrency: 'mockPrimaryCurrency',
|
||||
selectedToken: { address: 'mockTokenAddress' },
|
||||
tokenBalance: 'mockTokenBalance',
|
||||
}]
|
||||
)
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
describe('updateAmount', () => {
|
||||
|
||||
it('should call setMaxModeTo', () => {
|
||||
assert.equal(propsMethodSpies.setMaxModeTo.callCount, 0)
|
||||
instance.updateAmount('someAmount')
|
||||
assert.equal(propsMethodSpies.setMaxModeTo.callCount, 1)
|
||||
assert.deepEqual(
|
||||
propsMethodSpies.setMaxModeTo.getCall(0).args,
|
||||
[false]
|
||||
)
|
||||
})
|
||||
|
||||
it('should call updateSendAmount', () => {
|
||||
assert.equal(propsMethodSpies.updateSendAmount.callCount, 0)
|
||||
instance.updateAmount('someAmount')
|
||||
assert.equal(propsMethodSpies.updateSendAmount.callCount, 1)
|
||||
assert.deepEqual(
|
||||
propsMethodSpies.updateSendAmount.getCall(0).args,
|
||||
['someAmount']
|
||||
)
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
describe('render', () => {
|
||||
it('should render a SendRowWrapper component', () => {
|
||||
assert.equal(wrapper.find(SendRowWrapper).length, 1)
|
||||
})
|
||||
|
||||
it('should pass the correct props to SendRowWrapper', () => {
|
||||
const {
|
||||
errorType,
|
||||
label,
|
||||
showError,
|
||||
} = wrapper.find(SendRowWrapper).props()
|
||||
|
||||
assert.equal(errorType, 'amount')
|
||||
|
||||
assert.equal(label, 'amount_t:')
|
||||
|
||||
assert.equal(showError, false)
|
||||
})
|
||||
|
||||
it('should render an AmountMaxButton as the first child of the SendRowWrapper', () => {
|
||||
assert(wrapper.find(SendRowWrapper).childAt(0).is(AmountMaxButton))
|
||||
})
|
||||
|
||||
it('should render a CurrencyDisplay as the second child of the SendRowWrapper', () => {
|
||||
assert(wrapper.find(SendRowWrapper).childAt(1).is(CurrencyDisplay))
|
||||
})
|
||||
|
||||
it('should render the CurrencyDisplay with the correct props', () => {
|
||||
const {
|
||||
conversionRate,
|
||||
convertedCurrency,
|
||||
onBlur,
|
||||
onChange,
|
||||
inError,
|
||||
primaryCurrency,
|
||||
selectedToken,
|
||||
value,
|
||||
} = wrapper.find(SendRowWrapper).childAt(1).props()
|
||||
assert.equal(conversionRate, 'mockAmountConversionRate')
|
||||
assert.equal(convertedCurrency, 'mockConvertedCurrency')
|
||||
assert.equal(inError, false)
|
||||
assert.equal(primaryCurrency, 'mockPrimaryCurrency')
|
||||
assert.deepEqual(selectedToken, { address: 'mockTokenAddress' })
|
||||
assert.equal(value, 'mockAmount')
|
||||
assert.equal(SendAmountRow.prototype.updateAmount.callCount, 0)
|
||||
onBlur('mockNewAmount')
|
||||
assert.equal(SendAmountRow.prototype.updateAmount.callCount, 1)
|
||||
assert.deepEqual(
|
||||
SendAmountRow.prototype.updateAmount.getCall(0).args,
|
||||
['mockNewAmount']
|
||||
)
|
||||
assert.equal(SendAmountRow.prototype.validateAmount.callCount, 0)
|
||||
onChange('mockNewAmount')
|
||||
assert.equal(SendAmountRow.prototype.validateAmount.callCount, 1)
|
||||
assert.deepEqual(
|
||||
SendAmountRow.prototype.validateAmount.getCall(0).args,
|
||||
['mockNewAmount']
|
||||
)
|
||||
})
|
||||
|
||||
it('should pass the default primaryCurrency to the CurrencyDisplay if primaryCurrency is falsy', () => {
|
||||
wrapper.setProps({ primaryCurrency: null })
|
||||
const { primaryCurrency } = wrapper.find(SendRowWrapper).childAt(1).props()
|
||||
assert.equal(primaryCurrency, 'ETH')
|
||||
})
|
||||
})
|
||||
})
|
@ -0,0 +1,109 @@
|
||||
import assert from 'assert'
|
||||
import proxyquire from 'proxyquire'
|
||||
import sinon from 'sinon'
|
||||
|
||||
let mapStateToProps
|
||||
let mapDispatchToProps
|
||||
|
||||
const actionSpies = {
|
||||
setMaxModeTo: sinon.spy(),
|
||||
updateSendAmount: sinon.spy(),
|
||||
}
|
||||
const duckActionSpies = {
|
||||
updateSendErrors: sinon.spy(),
|
||||
}
|
||||
|
||||
proxyquire('../send-amount-row.container.js', {
|
||||
'react-redux': {
|
||||
connect: (ms, md) => {
|
||||
mapStateToProps = ms
|
||||
mapDispatchToProps = md
|
||||
return () => ({})
|
||||
},
|
||||
},
|
||||
'../../send.selectors': {
|
||||
getAmountConversionRate: (s) => `mockAmountConversionRate:${s}`,
|
||||
getConversionRate: (s) => `mockConversionRate:${s}`,
|
||||
getConvertedCurrency: (s) => `mockConvertedCurrency:${s}`,
|
||||
getGasTotal: (s) => `mockGasTotal:${s}`,
|
||||
getPrimaryCurrency: (s) => `mockPrimaryCurrency:${s}`,
|
||||
getSelectedToken: (s) => `mockSelectedToken:${s}`,
|
||||
getSendAmount: (s) => `mockAmount:${s}`,
|
||||
getSendFromBalance: (s) => `mockBalance:${s}`,
|
||||
getTokenBalance: (s) => `mockTokenBalance:${s}`,
|
||||
},
|
||||
'./send-amount-row.selectors': { sendAmountIsInError: (s) => `mockInError:${s}` },
|
||||
'../../send.utils': { getAmountErrorObject: (mockDataObject) => ({ ...mockDataObject, mockChange: true }) },
|
||||
'../../../../actions': actionSpies,
|
||||
'../../../../ducks/send.duck': duckActionSpies,
|
||||
})
|
||||
|
||||
describe('send-amount-row container', () => {
|
||||
|
||||
describe('mapStateToProps()', () => {
|
||||
|
||||
it('should map the correct properties to props', () => {
|
||||
assert.deepEqual(mapStateToProps('mockState'), {
|
||||
amount: 'mockAmount:mockState',
|
||||
amountConversionRate: 'mockAmountConversionRate:mockState',
|
||||
balance: 'mockBalance:mockState',
|
||||
conversionRate: 'mockConversionRate:mockState',
|
||||
convertedCurrency: 'mockConvertedCurrency:mockState',
|
||||
gasTotal: 'mockGasTotal:mockState',
|
||||
inError: 'mockInError:mockState',
|
||||
primaryCurrency: 'mockPrimaryCurrency:mockState',
|
||||
selectedToken: 'mockSelectedToken:mockState',
|
||||
tokenBalance: 'mockTokenBalance:mockState',
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
describe('mapDispatchToProps()', () => {
|
||||
let dispatchSpy
|
||||
let mapDispatchToPropsObject
|
||||
|
||||
beforeEach(() => {
|
||||
dispatchSpy = sinon.spy()
|
||||
mapDispatchToPropsObject = mapDispatchToProps(dispatchSpy)
|
||||
})
|
||||
|
||||
describe('setMaxModeTo()', () => {
|
||||
it('should dispatch an action', () => {
|
||||
mapDispatchToPropsObject.setMaxModeTo('mockBool')
|
||||
assert(dispatchSpy.calledOnce)
|
||||
assert(actionSpies.setMaxModeTo.calledOnce)
|
||||
assert.equal(
|
||||
actionSpies.setMaxModeTo.getCall(0).args[0],
|
||||
'mockBool'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('updateSendAmount()', () => {
|
||||
it('should dispatch an action', () => {
|
||||
mapDispatchToPropsObject.updateSendAmount('mockAmount')
|
||||
assert(dispatchSpy.calledOnce)
|
||||
assert(actionSpies.updateSendAmount.calledOnce)
|
||||
assert.equal(
|
||||
actionSpies.updateSendAmount.getCall(0).args[0],
|
||||
'mockAmount'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('updateSendAmountError()', () => {
|
||||
it('should dispatch an action', () => {
|
||||
mapDispatchToPropsObject.updateSendAmountError({ some: 'data' })
|
||||
assert(dispatchSpy.calledOnce)
|
||||
assert(duckActionSpies.updateSendErrors.calledOnce)
|
||||
assert.deepEqual(
|
||||
duckActionSpies.updateSendErrors.getCall(0).args[0],
|
||||
{ some: 'data', mockChange: true }
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
})
|
@ -0,0 +1,34 @@
|
||||
import assert from 'assert'
|
||||
import {
|
||||
sendAmountIsInError,
|
||||
} from '../send-amount-row.selectors.js'
|
||||
|
||||
describe('send-amount-row selectors', () => {
|
||||
|
||||
describe('sendAmountIsInError()', () => {
|
||||
it('should return true if send.errors.amount is truthy', () => {
|
||||
const state = {
|
||||
send: {
|
||||
errors: {
|
||||
amount: 'abc',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
assert.equal(sendAmountIsInError(state), true)
|
||||
})
|
||||
|
||||
it('should return false if send.errors.amount is falsy', () => {
|
||||
const state = {
|
||||
send: {
|
||||
errors: {
|
||||
amount: null,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
assert.equal(sendAmountIsInError(state), false)
|
||||
})
|
||||
})
|
||||
|
||||
})
|
@ -0,0 +1,28 @@
|
||||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import PageContainerContent from '../../page-container/page-container-content.component'
|
||||
import SendAmountRow from './send-amount-row/'
|
||||
import SendFromRow from './send-from-row/'
|
||||
import SendGasRow from './send-gas-row/'
|
||||
import SendToRow from './send-to-row/'
|
||||
|
||||
export default class SendContent extends Component {
|
||||
|
||||
static propTypes = {
|
||||
updateGas: PropTypes.func,
|
||||
};
|
||||
|
||||
render () {
|
||||
return (
|
||||
<PageContainerContent>
|
||||
<div className="send-v2__form">
|
||||
<SendFromRow />
|
||||
<SendToRow updateGas={(updateData) => this.props.updateGas(updateData)} />
|
||||
<SendAmountRow />
|
||||
<SendGasRow />
|
||||
</div>
|
||||
</PageContainerContent>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1 @@
|
||||
export { default } from './send-dropdown-list.component'
|
@ -0,0 +1,52 @@
|
||||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import AccountListItem from '../../account-list-item/'
|
||||
|
||||
export default class SendDropdownList extends Component {
|
||||
|
||||
static propTypes = {
|
||||
accounts: PropTypes.array,
|
||||
closeDropdown: PropTypes.func,
|
||||
onSelect: PropTypes.func,
|
||||
activeAddress: PropTypes.string,
|
||||
};
|
||||
|
||||
getListItemIcon (accountAddress, activeAddress) {
|
||||
return accountAddress === activeAddress
|
||||
? <i className={`fa fa-check fa-lg`} style={ { color: '#02c9b1' } }/>
|
||||
: null
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
accounts,
|
||||
closeDropdown,
|
||||
onSelect,
|
||||
activeAddress,
|
||||
} = this.props
|
||||
|
||||
return (<div>
|
||||
<div
|
||||
className="send-v2__from-dropdown__close-area"
|
||||
onClick={() => closeDropdown()}
|
||||
/>
|
||||
<div className="send-v2__from-dropdown__list">
|
||||
{accounts.map((account, index) => <AccountListItem
|
||||
account={account}
|
||||
className="account-list-item__dropdown"
|
||||
handleClick={() => {
|
||||
onSelect(account)
|
||||
closeDropdown()
|
||||
}}
|
||||
icon={this.getListItemIcon(account.address, activeAddress)}
|
||||
key={`send-dropdown-account-#${index}`}
|
||||
/>)}
|
||||
</div>
|
||||
</div>)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
SendDropdownList.contextTypes = {
|
||||
t: PropTypes.func,
|
||||
}
|
@ -0,0 +1,105 @@
|
||||
import React from 'react'
|
||||
import assert from 'assert'
|
||||
import { shallow } from 'enzyme'
|
||||
import sinon from 'sinon'
|
||||
import SendDropdownList from '../send-dropdown-list.component.js'
|
||||
|
||||
import AccountListItem from '../../../account-list-item/account-list-item.container'
|
||||
|
||||
const propsMethodSpies = {
|
||||
closeDropdown: sinon.spy(),
|
||||
onSelect: sinon.spy(),
|
||||
}
|
||||
|
||||
sinon.spy(SendDropdownList.prototype, 'getListItemIcon')
|
||||
|
||||
describe('SendDropdownList Component', function () {
|
||||
let wrapper
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = shallow(<SendDropdownList
|
||||
accounts={[
|
||||
{ address: 'mockAccount0' },
|
||||
{ address: 'mockAccount1' },
|
||||
{ address: 'mockAccount2' },
|
||||
]}
|
||||
closeDropdown={propsMethodSpies.closeDropdown}
|
||||
onSelect={propsMethodSpies.onSelect}
|
||||
activeAddress={'mockAddress2'}
|
||||
/>, { context: { t: str => str + '_t' } })
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
propsMethodSpies.closeDropdown.resetHistory()
|
||||
propsMethodSpies.onSelect.resetHistory()
|
||||
SendDropdownList.prototype.getListItemIcon.resetHistory()
|
||||
})
|
||||
|
||||
describe('getListItemIcon', () => {
|
||||
it('should return check icon if the passed addresses are the same', () => {
|
||||
assert.deepEqual(
|
||||
wrapper.instance().getListItemIcon('mockAccount0', 'mockAccount0'),
|
||||
<i className={`fa fa-check fa-lg`} style={ { color: '#02c9b1' } }/>
|
||||
)
|
||||
})
|
||||
|
||||
it('should return null if the passed addresses are different', () => {
|
||||
assert.equal(
|
||||
wrapper.instance().getListItemIcon('mockAccount0', 'mockAccount1'),
|
||||
null
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('render', () => {
|
||||
it('should render a single div with two children', () => {
|
||||
assert(wrapper.is('div'))
|
||||
assert.equal(wrapper.children().length, 2)
|
||||
})
|
||||
|
||||
it('should render the children with the correct classes', () => {
|
||||
assert(wrapper.childAt(0).hasClass('send-v2__from-dropdown__close-area'))
|
||||
assert(wrapper.childAt(1).hasClass('send-v2__from-dropdown__list'))
|
||||
})
|
||||
|
||||
it('should call closeDropdown onClick of the send-v2__from-dropdown__close-area', () => {
|
||||
assert.equal(propsMethodSpies.closeDropdown.callCount, 0)
|
||||
wrapper.childAt(0).props().onClick()
|
||||
assert.equal(propsMethodSpies.closeDropdown.callCount, 1)
|
||||
})
|
||||
|
||||
it('should render an AccountListItem for each item in accounts', () => {
|
||||
assert.equal(wrapper.childAt(1).children().length, 3)
|
||||
assert(wrapper.childAt(1).children().every(AccountListItem))
|
||||
})
|
||||
|
||||
it('should pass the correct props to the AccountListItem', () => {
|
||||
wrapper.childAt(1).children().forEach((accountListItem, index) => {
|
||||
const {
|
||||
account,
|
||||
className,
|
||||
handleClick,
|
||||
} = accountListItem.props()
|
||||
assert.deepEqual(account, { address: 'mockAccount' + index })
|
||||
assert.equal(className, 'account-list-item__dropdown')
|
||||
assert.equal(propsMethodSpies.onSelect.callCount, 0)
|
||||
handleClick()
|
||||
assert.equal(propsMethodSpies.onSelect.callCount, 1)
|
||||
assert.deepEqual(propsMethodSpies.onSelect.getCall(0).args[0], { address: 'mockAccount' + index })
|
||||
propsMethodSpies.onSelect.resetHistory()
|
||||
propsMethodSpies.closeDropdown.resetHistory()
|
||||
assert.equal(propsMethodSpies.closeDropdown.callCount, 0)
|
||||
handleClick()
|
||||
assert.equal(propsMethodSpies.closeDropdown.callCount, 1)
|
||||
propsMethodSpies.onSelect.resetHistory()
|
||||
propsMethodSpies.closeDropdown.resetHistory()
|
||||
})
|
||||
})
|
||||
|
||||
it('should call this.getListItemIcon for each AccountListItem', () => {
|
||||
assert.equal(SendDropdownList.prototype.getListItemIcon.callCount, 3)
|
||||
const getListItemIconCalls = SendDropdownList.prototype.getListItemIcon.getCalls()
|
||||
assert(getListItemIconCalls.every(({ args }, index) => args[0] === 'mockAccount' + index))
|
||||
})
|
||||
})
|
||||
})
|
@ -0,0 +1,46 @@
|
||||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import AccountListItem from '../../../account-list-item/'
|
||||
import SendDropdownList from '../../send-dropdown-list/'
|
||||
|
||||
export default class FromDropdown extends Component {
|
||||
|
||||
static propTypes = {
|
||||
accounts: PropTypes.array,
|
||||
closeDropdown: PropTypes.func,
|
||||
dropdownOpen: PropTypes.bool,
|
||||
onSelect: PropTypes.func,
|
||||
openDropdown: PropTypes.func,
|
||||
selectedAccount: PropTypes.object,
|
||||
};
|
||||
|
||||
render () {
|
||||
const {
|
||||
accounts,
|
||||
closeDropdown,
|
||||
dropdownOpen,
|
||||
openDropdown,
|
||||
selectedAccount,
|
||||
onSelect,
|
||||
} = this.props
|
||||
|
||||
return <div className="send-v2__from-dropdown">
|
||||
<AccountListItem
|
||||
account={selectedAccount}
|
||||
handleClick={openDropdown}
|
||||
icon={<i className={`fa fa-caret-down fa-lg`} style={ { color: '#dedede' } }/>}
|
||||
/>
|
||||
{dropdownOpen && <SendDropdownList
|
||||
accounts={accounts}
|
||||
closeDropdown={closeDropdown}
|
||||
onSelect={onSelect}
|
||||
activeAddress={selectedAccount.address}
|
||||
/>}
|
||||
</div>
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
FromDropdown.contextTypes = {
|
||||
t: PropTypes.func,
|
||||
}
|
@ -0,0 +1 @@
|
||||
export { default } from './from-dropdown.component'
|
@ -0,0 +1,88 @@
|
||||
import React from 'react'
|
||||
import assert from 'assert'
|
||||
import { shallow } from 'enzyme'
|
||||
import sinon from 'sinon'
|
||||
import FromDropdown from '../from-dropdown.component.js'
|
||||
|
||||
import AccountListItem from '../../../../account-list-item/account-list-item.container'
|
||||
import SendDropdownList from '../../../send-dropdown-list/send-dropdown-list.component'
|
||||
|
||||
const propsMethodSpies = {
|
||||
closeDropdown: sinon.spy(),
|
||||
openDropdown: sinon.spy(),
|
||||
onSelect: sinon.spy(),
|
||||
}
|
||||
|
||||
describe('FromDropdown Component', function () {
|
||||
let wrapper
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = shallow(<FromDropdown
|
||||
accounts={['mockAccount']}
|
||||
closeDropdown={propsMethodSpies.closeDropdown}
|
||||
dropdownOpen={false}
|
||||
onSelect={propsMethodSpies.onSelect}
|
||||
openDropdown={propsMethodSpies.openDropdown}
|
||||
selectedAccount={ { address: 'mockAddress' } }
|
||||
/>, { context: { t: str => str + '_t' } })
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
propsMethodSpies.closeDropdown.resetHistory()
|
||||
propsMethodSpies.openDropdown.resetHistory()
|
||||
propsMethodSpies.onSelect.resetHistory()
|
||||
})
|
||||
|
||||
describe('render', () => {
|
||||
it('should render a div with a .send-v2__from-dropdown class', () => {
|
||||
assert.equal(wrapper.find('.send-v2__from-dropdown').length, 1)
|
||||
})
|
||||
|
||||
it('should render an AccountListItem as the first child of the .send-v2__from-dropdown div', () => {
|
||||
assert(wrapper.find('.send-v2__from-dropdown').childAt(0).is(AccountListItem))
|
||||
})
|
||||
|
||||
it('should pass the correct props to AccountListItem', () => {
|
||||
const {
|
||||
account,
|
||||
handleClick,
|
||||
icon,
|
||||
} = wrapper.find('.send-v2__from-dropdown').childAt(0).props()
|
||||
assert.deepEqual(account, { address: 'mockAddress' })
|
||||
assert.deepEqual(
|
||||
icon,
|
||||
<i className={`fa fa-caret-down fa-lg`} style={ { color: '#dedede' } }/>
|
||||
)
|
||||
assert.equal(propsMethodSpies.openDropdown.callCount, 0)
|
||||
handleClick()
|
||||
assert.equal(propsMethodSpies.openDropdown.callCount, 1)
|
||||
})
|
||||
|
||||
it('should not render a SendDropdownList when dropdownOpen is false', () => {
|
||||
assert.equal(wrapper.find(SendDropdownList).length, 0)
|
||||
})
|
||||
|
||||
it('should render a SendDropdownList when dropdownOpen is true', () => {
|
||||
wrapper.setProps({ dropdownOpen: true })
|
||||
assert(wrapper.find(SendDropdownList).length, 1)
|
||||
})
|
||||
|
||||
it('should pass the correct props to the SendDropdownList]', () => {
|
||||
wrapper.setProps({ dropdownOpen: true })
|
||||
const {
|
||||
accounts,
|
||||
closeDropdown,
|
||||
onSelect,
|
||||
activeAddress,
|
||||
} = wrapper.find(SendDropdownList).props()
|
||||
assert.deepEqual(accounts, ['mockAccount'])
|
||||
assert.equal(activeAddress, 'mockAddress')
|
||||
assert.equal(propsMethodSpies.closeDropdown.callCount, 0)
|
||||
closeDropdown()
|
||||
assert.equal(propsMethodSpies.closeDropdown.callCount, 1)
|
||||
assert.equal(propsMethodSpies.onSelect.callCount, 0)
|
||||
onSelect()
|
||||
assert.equal(propsMethodSpies.onSelect.callCount, 1)
|
||||
})
|
||||
})
|
||||
})
|
@ -0,0 +1 @@
|
||||
export { default } from './send-from-row.container'
|
@ -0,0 +1,63 @@
|
||||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import SendRowWrapper from '../send-row-wrapper/'
|
||||
import FromDropdown from './from-dropdown/'
|
||||
|
||||
export default class SendFromRow extends Component {
|
||||
|
||||
static propTypes = {
|
||||
closeFromDropdown: PropTypes.func,
|
||||
conversionRate: PropTypes.number,
|
||||
from: PropTypes.object,
|
||||
fromAccounts: PropTypes.array,
|
||||
fromDropdownOpen: PropTypes.bool,
|
||||
openFromDropdown: PropTypes.func,
|
||||
tokenContract: PropTypes.object,
|
||||
updateSendFrom: PropTypes.func,
|
||||
setSendTokenBalance: PropTypes.func,
|
||||
};
|
||||
|
||||
async handleFromChange (newFrom) {
|
||||
const {
|
||||
updateSendFrom,
|
||||
tokenContract,
|
||||
setSendTokenBalance,
|
||||
} = this.props
|
||||
|
||||
if (tokenContract) {
|
||||
const usersToken = await tokenContract.balanceOf(newFrom.address)
|
||||
setSendTokenBalance(usersToken)
|
||||
}
|
||||
updateSendFrom(newFrom)
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
closeFromDropdown,
|
||||
conversionRate,
|
||||
from,
|
||||
fromAccounts,
|
||||
fromDropdownOpen,
|
||||
openFromDropdown,
|
||||
} = this.props
|
||||
|
||||
return (
|
||||
<SendRowWrapper label={`${this.context.t('from')}:`}>
|
||||
<FromDropdown
|
||||
accounts={fromAccounts}
|
||||
closeDropdown={() => closeFromDropdown()}
|
||||
conversionRate={conversionRate}
|
||||
dropdownOpen={fromDropdownOpen}
|
||||
onSelect={newFrom => this.handleFromChange(newFrom)}
|
||||
openDropdown={() => openFromDropdown()}
|
||||
selectedAccount={from}
|
||||
/>
|
||||
</SendRowWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
SendFromRow.contextTypes = {
|
||||
t: PropTypes.func,
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
import { connect } from 'react-redux'
|
||||
import {
|
||||
accountsWithSendEtherInfoSelector,
|
||||
getConversionRate,
|
||||
getSelectedTokenContract,
|
||||
getSendFromObject,
|
||||
} from '../../send.selectors.js'
|
||||
import {
|
||||
getFromDropdownOpen,
|
||||
} from './send-from-row.selectors.js'
|
||||
import { calcTokenBalance } from '../../send.utils.js'
|
||||
import {
|
||||
updateSendFrom,
|
||||
setSendTokenBalance,
|
||||
} from '../../../../actions'
|
||||
import {
|
||||
closeFromDropdown,
|
||||
openFromDropdown,
|
||||
} from '../../../../ducks/send.duck'
|
||||
import SendFromRow from './send-from-row.component'
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(SendFromRow)
|
||||
|
||||
function mapStateToProps (state) {
|
||||
return {
|
||||
conversionRate: getConversionRate(state),
|
||||
from: getSendFromObject(state),
|
||||
fromAccounts: accountsWithSendEtherInfoSelector(state),
|
||||
fromDropdownOpen: getFromDropdownOpen(state),
|
||||
tokenContract: getSelectedTokenContract(state),
|
||||
}
|
||||
}
|
||||
|
||||
function mapDispatchToProps (dispatch) {
|
||||
return {
|
||||
closeFromDropdown: () => dispatch(closeFromDropdown()),
|
||||
openFromDropdown: () => dispatch(openFromDropdown()),
|
||||
updateSendFrom: newFrom => dispatch(updateSendFrom(newFrom)),
|
||||
setSendTokenBalance: (usersToken, selectedToken) => {
|
||||
if (!usersToken) return
|
||||
|
||||
const tokenBalance = calcTokenBalance({ usersToken, selectedToken })
|
||||
dispatch(setSendTokenBalance(tokenBalance))
|
||||
},
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
const selectors = {
|
||||
getFromDropdownOpen,
|
||||
}
|
||||
|
||||
module.exports = selectors
|
||||
|
||||
function getFromDropdownOpen (state) {
|
||||
return state.send.fromDropdownOpen
|
||||
}
|
@ -0,0 +1,121 @@
|
||||
import React from 'react'
|
||||
import assert from 'assert'
|
||||
import { shallow } from 'enzyme'
|
||||
import sinon from 'sinon'
|
||||
import SendFromRow from '../send-from-row.component.js'
|
||||
|
||||
import SendRowWrapper from '../../send-row-wrapper/send-row-wrapper.component'
|
||||
import FromDropdown from '../from-dropdown/from-dropdown.component'
|
||||
|
||||
const propsMethodSpies = {
|
||||
closeFromDropdown: sinon.spy(),
|
||||
openFromDropdown: sinon.spy(),
|
||||
updateSendFrom: sinon.spy(),
|
||||
setSendTokenBalance: sinon.spy(),
|
||||
}
|
||||
|
||||
sinon.spy(SendFromRow.prototype, 'handleFromChange')
|
||||
|
||||
describe('SendFromRow Component', function () {
|
||||
let wrapper
|
||||
let instance
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = shallow(<SendFromRow
|
||||
closeFromDropdown={propsMethodSpies.closeFromDropdown}
|
||||
conversionRate={15}
|
||||
from={ { address: 'mockAddress' } }
|
||||
fromAccounts={['mockAccount']}
|
||||
fromDropdownOpen={false}
|
||||
openFromDropdown={propsMethodSpies.openFromDropdown}
|
||||
setSendTokenBalance={propsMethodSpies.setSendTokenBalance}
|
||||
tokenContract={null}
|
||||
updateSendFrom={propsMethodSpies.updateSendFrom}
|
||||
/>, { context: { t: str => str + '_t' } })
|
||||
instance = wrapper.instance()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
propsMethodSpies.closeFromDropdown.resetHistory()
|
||||
propsMethodSpies.openFromDropdown.resetHistory()
|
||||
propsMethodSpies.updateSendFrom.resetHistory()
|
||||
propsMethodSpies.setSendTokenBalance.resetHistory()
|
||||
SendFromRow.prototype.handleFromChange.resetHistory()
|
||||
})
|
||||
|
||||
describe('handleFromChange', () => {
|
||||
|
||||
it('should call updateSendFrom', () => {
|
||||
assert.equal(propsMethodSpies.updateSendFrom.callCount, 0)
|
||||
instance.handleFromChange('mockFrom')
|
||||
assert.equal(propsMethodSpies.updateSendFrom.callCount, 1)
|
||||
assert.deepEqual(
|
||||
propsMethodSpies.updateSendFrom.getCall(0).args,
|
||||
['mockFrom']
|
||||
)
|
||||
})
|
||||
|
||||
it('should call tokenContract.balanceOf and setSendTokenBalance if tokenContract is defined', async () => {
|
||||
wrapper.setProps({
|
||||
tokenContract: {
|
||||
balanceOf: () => new Promise((resolve) => resolve('mockUsersToken')),
|
||||
},
|
||||
})
|
||||
assert.equal(propsMethodSpies.setSendTokenBalance.callCount, 0)
|
||||
await instance.handleFromChange('mockFrom')
|
||||
assert.equal(propsMethodSpies.setSendTokenBalance.callCount, 1)
|
||||
assert.deepEqual(
|
||||
propsMethodSpies.setSendTokenBalance.getCall(0).args,
|
||||
['mockUsersToken']
|
||||
)
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
describe('render', () => {
|
||||
it('should render a SendRowWrapper component', () => {
|
||||
assert.equal(wrapper.find(SendRowWrapper).length, 1)
|
||||
})
|
||||
|
||||
it('should pass the correct props to SendRowWrapper', () => {
|
||||
const {
|
||||
label,
|
||||
} = wrapper.find(SendRowWrapper).props()
|
||||
|
||||
assert.equal(label, 'from_t:')
|
||||
})
|
||||
|
||||
it('should render an FromDropdown as a child of the SendRowWrapper', () => {
|
||||
assert(wrapper.find(SendRowWrapper).childAt(0).is(FromDropdown))
|
||||
})
|
||||
|
||||
it('should render the FromDropdown with the correct props', () => {
|
||||
const {
|
||||
accounts,
|
||||
closeDropdown,
|
||||
conversionRate,
|
||||
dropdownOpen,
|
||||
onSelect,
|
||||
openDropdown,
|
||||
selectedAccount,
|
||||
} = wrapper.find(SendRowWrapper).childAt(0).props()
|
||||
assert.deepEqual(accounts, ['mockAccount'])
|
||||
assert.equal(dropdownOpen, false)
|
||||
assert.equal(conversionRate, 15)
|
||||
assert.deepEqual(selectedAccount, { address: 'mockAddress' })
|
||||
assert.equal(propsMethodSpies.closeFromDropdown.callCount, 0)
|
||||
closeDropdown()
|
||||
assert.equal(propsMethodSpies.closeFromDropdown.callCount, 1)
|
||||
assert.equal(propsMethodSpies.openFromDropdown.callCount, 0)
|
||||
openDropdown()
|
||||
assert.equal(propsMethodSpies.openFromDropdown.callCount, 1)
|
||||
assert.equal(SendFromRow.prototype.handleFromChange.callCount, 0)
|
||||
onSelect('mockNewFrom')
|
||||
assert.equal(SendFromRow.prototype.handleFromChange.callCount, 1)
|
||||
assert.deepEqual(
|
||||
SendFromRow.prototype.handleFromChange.getCall(0).args,
|
||||
['mockNewFrom']
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
@ -0,0 +1,110 @@
|
||||
import assert from 'assert'
|
||||
import proxyquire from 'proxyquire'
|
||||
import sinon from 'sinon'
|
||||
|
||||
let mapStateToProps
|
||||
let mapDispatchToProps
|
||||
|
||||
const actionSpies = {
|
||||
updateSendFrom: sinon.spy(),
|
||||
setSendTokenBalance: sinon.spy(),
|
||||
}
|
||||
const duckActionSpies = {
|
||||
closeFromDropdown: sinon.spy(),
|
||||
openFromDropdown: sinon.spy(),
|
||||
}
|
||||
|
||||
proxyquire('../send-from-row.container.js', {
|
||||
'react-redux': {
|
||||
connect: (ms, md) => {
|
||||
mapStateToProps = ms
|
||||
mapDispatchToProps = md
|
||||
return () => ({})
|
||||
},
|
||||
},
|
||||
'../../send.selectors.js': {
|
||||
accountsWithSendEtherInfoSelector: (s) => `mockFromAccounts:${s}`,
|
||||
getConversionRate: (s) => `mockConversionRate:${s}`,
|
||||
getSelectedTokenContract: (s) => `mockTokenContract:${s}`,
|
||||
getSendFromObject: (s) => `mockFrom:${s}`,
|
||||
},
|
||||
'./send-from-row.selectors.js': { getFromDropdownOpen: (s) => `mockFromDropdownOpen:${s}` },
|
||||
'../../send.utils.js': { calcTokenBalance: ({ usersToken, selectedToken }) => usersToken + selectedToken },
|
||||
'../../../../actions': actionSpies,
|
||||
'../../../../ducks/send.duck': duckActionSpies,
|
||||
})
|
||||
|
||||
describe('send-from-row container', () => {
|
||||
|
||||
describe('mapStateToProps()', () => {
|
||||
|
||||
it('should map the correct properties to props', () => {
|
||||
assert.deepEqual(mapStateToProps('mockState'), {
|
||||
conversionRate: 'mockConversionRate:mockState',
|
||||
from: 'mockFrom:mockState',
|
||||
fromAccounts: 'mockFromAccounts:mockState',
|
||||
fromDropdownOpen: 'mockFromDropdownOpen:mockState',
|
||||
tokenContract: 'mockTokenContract:mockState',
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
describe('mapDispatchToProps()', () => {
|
||||
let dispatchSpy
|
||||
let mapDispatchToPropsObject
|
||||
|
||||
beforeEach(() => {
|
||||
dispatchSpy = sinon.spy()
|
||||
mapDispatchToPropsObject = mapDispatchToProps(dispatchSpy)
|
||||
})
|
||||
|
||||
describe('closeFromDropdown()', () => {
|
||||
it('should dispatch a closeFromDropdown action', () => {
|
||||
mapDispatchToPropsObject.closeFromDropdown()
|
||||
assert(dispatchSpy.calledOnce)
|
||||
assert(duckActionSpies.closeFromDropdown.calledOnce)
|
||||
assert.equal(
|
||||
duckActionSpies.closeFromDropdown.getCall(0).args[0],
|
||||
undefined
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('openFromDropdown()', () => {
|
||||
it('should dispatch a openFromDropdown action', () => {
|
||||
mapDispatchToPropsObject.openFromDropdown()
|
||||
assert(dispatchSpy.calledOnce)
|
||||
assert(duckActionSpies.openFromDropdown.calledOnce)
|
||||
assert.equal(
|
||||
duckActionSpies.openFromDropdown.getCall(0).args[0],
|
||||
undefined
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('updateSendFrom()', () => {
|
||||
it('should dispatch an updateSendFrom action', () => {
|
||||
mapDispatchToPropsObject.updateSendFrom('mockFrom')
|
||||
assert(dispatchSpy.calledOnce)
|
||||
assert.equal(
|
||||
actionSpies.updateSendFrom.getCall(0).args[0],
|
||||
'mockFrom'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('setSendTokenBalance()', () => {
|
||||
it('should dispatch an setSendTokenBalance action', () => {
|
||||
mapDispatchToPropsObject.setSendTokenBalance('mockUsersToken', 'mockSelectedToken')
|
||||
assert(dispatchSpy.calledOnce)
|
||||
assert.equal(
|
||||
actionSpies.setSendTokenBalance.getCall(0).args[0],
|
||||
'mockUsersTokenmockSelectedToken'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
})
|
@ -0,0 +1,20 @@
|
||||
import assert from 'assert'
|
||||
import {
|
||||
getFromDropdownOpen,
|
||||
} from '../send-from-row.selectors.js'
|
||||
|
||||
describe('send-from-row selectors', () => {
|
||||
|
||||
describe('getFromDropdownOpen()', () => {
|
||||
it('should get send.fromDropdownOpen', () => {
|
||||
const state = {
|
||||
send: {
|
||||
fromDropdownOpen: null,
|
||||
},
|
||||
}
|
||||
|
||||
assert.equal(getFromDropdownOpen(state), null)
|
||||
})
|
||||
})
|
||||
|
||||
})
|
@ -0,0 +1 @@
|
||||
export { default } from './send-gas-row.container'
|
@ -0,0 +1,42 @@
|
||||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import SendRowWrapper from '../send-row-wrapper/'
|
||||
import GasFeeDisplay from '../../../send/gas-fee-display-v2'
|
||||
|
||||
export default class SendGasRow extends Component {
|
||||
|
||||
static propTypes = {
|
||||
conversionRate: PropTypes.number,
|
||||
convertedCurrency: PropTypes.string,
|
||||
gasLoadingError: PropTypes.bool,
|
||||
gasTotal: PropTypes.string,
|
||||
showCustomizeGasModal: PropTypes.func,
|
||||
};
|
||||
|
||||
render () {
|
||||
const {
|
||||
conversionRate,
|
||||
convertedCurrency,
|
||||
gasLoadingError,
|
||||
gasTotal,
|
||||
showCustomizeGasModal,
|
||||
} = this.props
|
||||
|
||||
return (
|
||||
<SendRowWrapper label={`${this.context.t('gasFee')}:`}>
|
||||
<GasFeeDisplay
|
||||
conversionRate={conversionRate}
|
||||
convertedCurrency={convertedCurrency}
|
||||
gasLoadingError={gasLoadingError}
|
||||
gasTotal={gasTotal}
|
||||
onClick={() => showCustomizeGasModal()}
|
||||
/>
|
||||
</SendRowWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
SendGasRow.contextTypes = {
|
||||
t: PropTypes.func,
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
import { connect } from 'react-redux'
|
||||
import {
|
||||
getConversionRate,
|
||||
getConvertedCurrency,
|
||||
getGasTotal,
|
||||
} from '../../send.selectors.js'
|
||||
import { sendGasIsInError } from './send-gas-row.selectors.js'
|
||||
import { showModal } from '../../../../actions'
|
||||
import SendGasRow from './send-gas-row.component'
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(SendGasRow)
|
||||
|
||||
function mapStateToProps (state) {
|
||||
return {
|
||||
conversionRate: getConversionRate(state),
|
||||
convertedCurrency: getConvertedCurrency(state),
|
||||
gasTotal: getGasTotal(state),
|
||||
gasLoadingError: sendGasIsInError(state),
|
||||
}
|
||||
}
|
||||
|
||||
function mapDispatchToProps (dispatch) {
|
||||
return {
|
||||
showCustomizeGasModal: () => dispatch(showModal({ name: 'CUSTOMIZE_GAS' })),
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
const selectors = {
|
||||
sendGasIsInError,
|
||||
}
|
||||
|
||||
module.exports = selectors
|
||||
|
||||
function sendGasIsInError (state) {
|
||||
return state.send.errors.gasLoading
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
import React from 'react'
|
||||
import assert from 'assert'
|
||||
import { shallow } from 'enzyme'
|
||||
import sinon from 'sinon'
|
||||
import SendGasRow from '../send-gas-row.component.js'
|
||||
|
||||
import SendRowWrapper from '../../send-row-wrapper/send-row-wrapper.component'
|
||||
import GasFeeDisplay from '../../../../send/gas-fee-display-v2'
|
||||
|
||||
const propsMethodSpies = {
|
||||
showCustomizeGasModal: sinon.spy(),
|
||||
}
|
||||
|
||||
describe('SendGasRow Component', function () {
|
||||
let wrapper
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = shallow(<SendGasRow
|
||||
conversionRate={20}
|
||||
convertedCurrency={'mockConvertedCurrency'}
|
||||
gasLoadingError={false}
|
||||
gasTotal={'mockGasTotal'}
|
||||
showCustomizeGasModal={propsMethodSpies.showCustomizeGasModal}
|
||||
/>, { context: { t: str => str + '_t' } })
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
propsMethodSpies.showCustomizeGasModal.resetHistory()
|
||||
})
|
||||
|
||||
describe('render', () => {
|
||||
it('should render a SendRowWrapper component', () => {
|
||||
assert.equal(wrapper.find(SendRowWrapper).length, 1)
|
||||
})
|
||||
|
||||
it('should pass the correct props to SendRowWrapper', () => {
|
||||
const {
|
||||
label,
|
||||
} = wrapper.find(SendRowWrapper).props()
|
||||
|
||||
assert.equal(label, 'gasFee_t:')
|
||||
})
|
||||
|
||||
it('should render a GasFeeDisplay as a child of the SendRowWrapper', () => {
|
||||
assert(wrapper.find(SendRowWrapper).childAt(0).is(GasFeeDisplay))
|
||||
})
|
||||
|
||||
it('should render the GasFeeDisplay with the correct props', () => {
|
||||
const {
|
||||
conversionRate,
|
||||
convertedCurrency,
|
||||
gasLoadingError,
|
||||
gasTotal,
|
||||
onClick,
|
||||
} = wrapper.find(SendRowWrapper).childAt(0).props()
|
||||
assert.equal(conversionRate, 20)
|
||||
assert.equal(convertedCurrency, 'mockConvertedCurrency')
|
||||
assert.equal(gasLoadingError, false)
|
||||
assert.equal(gasTotal, 'mockGasTotal')
|
||||
assert.equal(propsMethodSpies.showCustomizeGasModal.callCount, 0)
|
||||
onClick()
|
||||
assert.equal(propsMethodSpies.showCustomizeGasModal.callCount, 1)
|
||||
})
|
||||
})
|
||||
})
|
@ -0,0 +1,66 @@
|
||||
import assert from 'assert'
|
||||
import proxyquire from 'proxyquire'
|
||||
import sinon from 'sinon'
|
||||
|
||||
let mapStateToProps
|
||||
let mapDispatchToProps
|
||||
|
||||
const actionSpies = {
|
||||
showModal: sinon.spy(),
|
||||
}
|
||||
|
||||
proxyquire('../send-gas-row.container.js', {
|
||||
'react-redux': {
|
||||
connect: (ms, md) => {
|
||||
mapStateToProps = ms
|
||||
mapDispatchToProps = md
|
||||
return () => ({})
|
||||
},
|
||||
},
|
||||
'../../send.selectors.js': {
|
||||
getConversionRate: (s) => `mockConversionRate:${s}`,
|
||||
getConvertedCurrency: (s) => `mockConvertedCurrency:${s}`,
|
||||
getGasTotal: (s) => `mockGasTotal:${s}`,
|
||||
},
|
||||
'./send-gas-row.selectors.js': { sendGasIsInError: (s) => `mockGasLoadingError:${s}` },
|
||||
'../../../../actions': actionSpies,
|
||||
})
|
||||
|
||||
describe('send-gas-row container', () => {
|
||||
|
||||
describe('mapStateToProps()', () => {
|
||||
|
||||
it('should map the correct properties to props', () => {
|
||||
assert.deepEqual(mapStateToProps('mockState'), {
|
||||
conversionRate: 'mockConversionRate:mockState',
|
||||
convertedCurrency: 'mockConvertedCurrency:mockState',
|
||||
gasTotal: 'mockGasTotal:mockState',
|
||||
gasLoadingError: 'mockGasLoadingError:mockState',
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
describe('mapDispatchToProps()', () => {
|
||||
let dispatchSpy
|
||||
let mapDispatchToPropsObject
|
||||
|
||||
beforeEach(() => {
|
||||
dispatchSpy = sinon.spy()
|
||||
mapDispatchToPropsObject = mapDispatchToProps(dispatchSpy)
|
||||
})
|
||||
|
||||
describe('showCustomizeGasModal()', () => {
|
||||
it('should dispatch an action', () => {
|
||||
mapDispatchToPropsObject.showCustomizeGasModal()
|
||||
assert(dispatchSpy.calledOnce)
|
||||
assert.deepEqual(
|
||||
actionSpies.showModal.getCall(0).args[0],
|
||||
{ name: 'CUSTOMIZE_GAS' }
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
})
|
@ -0,0 +1,22 @@
|
||||
import assert from 'assert'
|
||||
import {
|
||||
sendGasIsInError,
|
||||
} from '../send-gas-row.selectors.js'
|
||||
|
||||
describe('send-gas-row selectors', () => {
|
||||
|
||||
describe('sendGasIsInError()', () => {
|
||||
it('should return send.errors.gasLoading', () => {
|
||||
const state = {
|
||||
send: {
|
||||
errors: {
|
||||
gasLoading: 'abc',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
assert.equal(sendGasIsInError(state), 'abc')
|
||||
})
|
||||
})
|
||||
|
||||
})
|
@ -0,0 +1 @@
|
||||
export { default } from './send-row-wrapper.component'
|
@ -0,0 +1 @@
|
||||
export { default } from './send-row-error-message.container'
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user