commit
e7c2710a55
@ -0,0 +1,6 @@ |
|||||||
|
#!/usr/bin/env bash |
||||||
|
|
||||||
|
echo "Downloading firefox..." |
||||||
|
wget https://ftp.mozilla.org/pub/firefox/releases/58.0/linux-x86_64/en-US/firefox-58.0.tar.bz2 \ |
||||||
|
&& tar xjf firefox-58.0.tar.bz2 |
||||||
|
echo "firefox download complete" |
@ -0,0 +1,8 @@ |
|||||||
|
#!/usr/bin/env bash |
||||||
|
|
||||||
|
echo "Installing firefox..." |
||||||
|
sudo rm -r /opt/firefox |
||||||
|
sudo mv firefox /opt/firefox58 |
||||||
|
sudo mv /usr/bin/firefox /usr/bin/firefox-old |
||||||
|
sudo ln -s /opt/firefox58/firefox /usr/bin/firefox |
||||||
|
echo "Firefox installed." |
@ -0,0 +1,24 @@ |
|||||||
|
const Config = require('./recipient-blacklist-config.json') |
||||||
|
|
||||||
|
/** @module*/ |
||||||
|
module.exports = { |
||||||
|
checkAccount, |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Checks if a specified account on a specified network is blacklisted. |
||||||
|
@param networkId {number} |
||||||
|
@param account {string} |
||||||
|
*/ |
||||||
|
function checkAccount (networkId, account) { |
||||||
|
|
||||||
|
const mainnetId = 1 |
||||||
|
if (networkId !== mainnetId) { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
const accountToCheck = account.toLowerCase() |
||||||
|
if (Config.blacklist.includes(accountToCheck)) { |
||||||
|
throw new Error('Recipient is a public account') |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,14 @@ |
|||||||
|
{ |
||||||
|
"blacklist": [ |
||||||
|
"0x627306090abab3a6e1400e9345bc60c78a8bef57", |
||||||
|
"0xf17f52151ebef6c7334fad080c5704d77216b732", |
||||||
|
"0xc5fdf4076b8f3a5357c5e395ab970b5b54098fef", |
||||||
|
"0x821aea9a577a9b44299b9c15c88cf3087f3b5544", |
||||||
|
"0x0d1d4e623d10f9fba5db95830f7d3839406c6af2", |
||||||
|
"0x2932b7a2355d6fecc4b5c0b6bd44cc31df247a2e", |
||||||
|
"0x2191ef87e392377ec08e7c08eb105ef5448eced5", |
||||||
|
"0x0f4f2ac550a1b4e2280d04c21cea7ebd822934b5", |
||||||
|
"0x6330a553fc93768f612722bb8c2ec78ac90b3bbc", |
||||||
|
"0x5aeda56215b167893e80b4fe645ba6d5bab767de" |
||||||
|
] |
||||||
|
} |
@ -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'} |
||||||
|
|
||||||
|
}) |
||||||
|
|
||||||
|
})` |
||||||
|
} |
@ -0,0 +1,77 @@ |
|||||||
|
const assert = require('assert') |
||||||
|
const recipientBlackListChecker = require('../../../../../app/scripts/controllers/transactions/lib/recipient-blacklist-checker') |
||||||
|
const { |
||||||
|
ROPSTEN_CODE, |
||||||
|
RINKEYBY_CODE, |
||||||
|
KOVAN_CODE, |
||||||
|
} = require('../../../../../app/scripts/controllers/network/enums') |
||||||
|
|
||||||
|
const KeyringController = require('eth-keyring-controller') |
||||||
|
|
||||||
|
describe('Recipient Blacklist Checker', function () { |
||||||
|
|
||||||
|
let publicAccounts |
||||||
|
|
||||||
|
before(async function () { |
||||||
|
const damnedMnemonic = 'candy maple cake sugar pudding cream honey rich smooth crumble sweet treat' |
||||||
|
const keyringController = new KeyringController({}) |
||||||
|
const Keyring = keyringController.getKeyringClassForType('HD Key Tree') |
||||||
|
const opts = { |
||||||
|
mnemonic: damnedMnemonic, |
||||||
|
numberOfAccounts: 10, |
||||||
|
} |
||||||
|
const keyring = new Keyring(opts) |
||||||
|
publicAccounts = await keyring.getAccounts() |
||||||
|
}) |
||||||
|
|
||||||
|
describe('#checkAccount', function () { |
||||||
|
it('does not fail on test networks', function () { |
||||||
|
let callCount = 0 |
||||||
|
const networks = [ROPSTEN_CODE, RINKEYBY_CODE, KOVAN_CODE] |
||||||
|
for (let networkId in networks) { |
||||||
|
publicAccounts.forEach((account) => { |
||||||
|
recipientBlackListChecker.checkAccount(networkId, account) |
||||||
|
callCount++ |
||||||
|
}) |
||||||
|
} |
||||||
|
assert.equal(callCount, 30) |
||||||
|
}) |
||||||
|
|
||||||
|
it('fails on mainnet', function () { |
||||||
|
const mainnetId = 1 |
||||||
|
let callCount = 0 |
||||||
|
publicAccounts.forEach((account) => { |
||||||
|
try { |
||||||
|
recipientBlackListChecker.checkAccount(mainnetId, account) |
||||||
|
assert.fail('function should have thrown an error') |
||||||
|
} catch (err) { |
||||||
|
assert.equal(err.message, 'Recipient is a public account') |
||||||
|
} |
||||||
|
callCount++ |
||||||
|
}) |
||||||
|
assert.equal(callCount, 10) |
||||||
|
}) |
||||||
|
|
||||||
|
it('fails for public account - uppercase', function () { |
||||||
|
const mainnetId = 1 |
||||||
|
const publicAccount = '0X0D1D4E623D10F9FBA5DB95830F7D3839406C6AF2' |
||||||
|
try { |
||||||
|
recipientBlackListChecker.checkAccount(mainnetId, publicAccount) |
||||||
|
assert.fail('function should have thrown an error') |
||||||
|
} catch (err) { |
||||||
|
assert.equal(err.message, 'Recipient is a public account') |
||||||
|
} |
||||||
|
})
|
||||||
|
|
||||||
|
it('fails for public account - lowercase', async function () { |
||||||
|
const mainnetId = 1 |
||||||
|
const publicAccount = '0x0d1d4e623d10f9fba5db95830f7d3839406c6af2' |
||||||
|
try { |
||||||
|
await recipientBlackListChecker.checkAccount(mainnetId, publicAccount) |
||||||
|
assert.fail('function should have thrown an error') |
||||||
|
} catch (err) { |
||||||
|
assert.equal(err.message, 'Recipient is a public account') |
||||||
|
} |
||||||
|
})
|
||||||
|
}) |
||||||
|
}) |
@ -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, |
|
||||||
}) |
|
||||||
} |
|
@ -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> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -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> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -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,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)), |
|
||||||
} |
|
||||||
} |
|
@ -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), |
||||||
|
} |
||||||
|
} |
@ -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', |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
}) |
||||||
|
|
||||||
|
}) |
@ -0,0 +1 @@ |
|||||||
|
export { default } from './send.container' |
@ -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, |
||||||
|
} |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue