diff options
Diffstat (limited to 'ui/app/add-token.js')
-rw-r--r-- | ui/app/add-token.js | 428 |
1 files changed, 192 insertions, 236 deletions
diff --git a/ui/app/add-token.js b/ui/app/add-token.js index 622cf2bc2..f723ff07c 100644 --- a/ui/app/add-token.js +++ b/ui/app/add-token.js @@ -5,6 +5,8 @@ const h = require('react-hyperscript') const connect = require('react-redux').connect const Fuse = require('fuse.js') const contractMap = require('eth-contract-metadata') +const TokenBalance = require('./components/token-balance') +const Identicon = require('./components/identicon') const contractList = Object.entries(contractMap).map(([ _, tokenData]) => tokenData) const fuse = new Fuse(contractList, { shouldSort: true, @@ -16,9 +18,6 @@ const fuse = new Fuse(contractList, { keys: ['address', 'name', 'symbol'], }) const actions = require('./actions') -// const Tooltip = require('./components/tooltip.js') - - const ethUtil = require('ethereumjs-util') const abi = require('human-standard-token-abi') const Eth = require('ethjs-query') @@ -37,86 +36,193 @@ function mapStateToProps (state) { function mapDispatchToProps (dispatch) { return { goHome: () => dispatch(actions.goHome()), + addTokens: tokens => dispatch(actions.addTokens(tokens)), } } inherits(AddTokenScreen, Component) function AddTokenScreen () { this.state = { - // warning: null, - // address: null, - // symbol: 'TOKEN', - // decimals: 18, + isShowingConfirmation: false, customAddress: '', customSymbol: '', customDecimals: 0, searchQuery: '', isCollapsed: true, - selectedToken: {}, + selectedTokens: {}, + errors: {}, } this.tokenAddressDidChange = this.tokenAddressDidChange.bind(this) + this.onNext = this.onNext.bind(this) Component.call(this) } -AddTokenScreen.prototype.toggleToken = function (symbol) { - const { selectedToken } = this.state - const { [symbol]: isSelected } = selectedToken +AddTokenScreen.prototype.componentWillMount = function () { + if (typeof global.ethereumProvider === 'undefined') return + + this.eth = new Eth(global.ethereumProvider) + this.contract = new EthContract(this.eth) + this.TokenContract = this.contract(abi) +} + +AddTokenScreen.prototype.toggleToken = function (address, token) { + const { selectedTokens, errors } = this.state + const { [address]: selectedToken } = selectedTokens this.setState({ - selectedToken: { - ...selectedToken, - [symbol]: !isSelected, + selectedTokens: { + ...selectedTokens, + [address]: selectedToken ? null : token, + }, + errors: { + ...errors, + tokenSelector: null, }, }) } +AddTokenScreen.prototype.onNext = function () { + const { isValid, errors } = this.validate() + + return !isValid + ? this.setState({ errors }) + : this.setState({ isShowingConfirmation: true }) +} + +AddTokenScreen.prototype.tokenAddressDidChange = function (e) { + const customAddress = e.target.value.trim() + this.setState({ customAddress }) + if (ethUtil.isValidAddress(customAddress) && customAddress !== emptyAddr) { + this.attemptToAutoFillTokenParams(customAddress) + } else { + this.setState({ + customSymbol: '', + customDecimals: 0, + }) + } +} + +AddTokenScreen.prototype.validate = function () { + const errors = {} + const identitiesList = Object.keys(this.props.identities) + const { customAddress, customSymbol, customDecimals, selectedTokens } = this.state + const standardAddress = ethUtil.addHexPrefix(customAddress).toLowerCase() + + if (customAddress) { + const validAddress = ethUtil.isValidAddress(customAddress) + if (!validAddress) { + errors.customAddress = 'Address is invalid. ' + } + + const validDecimals = customDecimals >= 0 && customDecimals < 36 + if (!validDecimals) { + errors.customDecimals = 'Decimals must be at least 0, and not over 36.' + } + + const symbolLen = customSymbol.trim().length + const validSymbol = symbolLen > 0 && symbolLen < 10 + if (!validSymbol) { + errors.customSymbol = 'Symbol must be between 0 and 10 characters.' + } + + const ownAddress = identitiesList.includes(standardAddress) + if (ownAddress) { + errors.customAddress = 'Personal address detected. Input the token contract address.' + } + } else if ( + Object.entries(selectedTokens) + .reduce((isEmpty, [ symbol, isSelected ]) => ( + isEmpty && !isSelected + ), true) + ) { + errors.tokenSelector = 'Must select at least 1 token.' + } + + return { + isValid: !Object.keys(errors).length, + errors, + } +} + +AddTokenScreen.prototype.attemptToAutoFillTokenParams = async function (address) { + const contract = this.TokenContract.at(address) + + const results = await Promise.all([ + contract.symbol(), + contract.decimals(), + ]) + + const [ symbol, decimals ] = results + if (symbol && decimals) { + this.setState({ + customSymbol: symbol[0], + customDecimals: decimals[0].toString(), + }) + } +} + AddTokenScreen.prototype.renderCustomForm = function () { - const { customAddress, customSymbol, customDecimals } = this.state + const { customAddress, customSymbol, customDecimals, errors } = this.state return !this.state.isCollapsed && ( h('div.add-token__add-custom-form', [ - h('div.add-token__add-custom-field', [ + h('div', { + className: classnames('add-token__add-custom-field', { + 'add-token__add-custom-field--error': errors.customAddress, + }), + }, [ h('div.add-token__add-custom-label', 'Token Address'), h('input.add-token__add-custom-input', { type: 'text', onChange: this.tokenAddressDidChange, value: customAddress, }), + h('div.add-token__add-custom-error-message', errors.customAddress), ]), - h('div.add-token__add-custom-field', [ + h('div', { + className: classnames('add-token__add-custom-field', { + 'add-token__add-custom-field--error': errors.customSymbol, + }), + }, [ h('div.add-token__add-custom-label', 'Token Symbol'), h('input.add-token__add-custom-input', { type: 'text', value: customSymbol, disabled: true, }), + h('div.add-token__add-custom-error-message', errors.customSymbol), ]), - h('div.add-token__add-custom-field', [ + h('div', { + className: classnames('add-token__add-custom-field', { + 'add-token__add-custom-field--error': errors.customDecimals, + }), + }, [ h('div.add-token__add-custom-label', 'Decimals of Precision'), h('input.add-token__add-custom-input', { type: 'number', value: customDecimals, disabled: true, }), + h('div.add-token__add-custom-error-message', errors.customDecimals), ]), ]) ) } AddTokenScreen.prototype.renderTokenList = function () { - const { searchQuery = '', selectedToken } = this.state + const { searchQuery = '', selectedTokens } = this.state const results = searchQuery ? fuse.search(searchQuery) || [] : contractList return Array(6).fill(undefined) .map((_, i) => { - const { logo, symbol, name } = results[i] || {} + const { logo, symbol, name, address } = results[i] || {} return Boolean(logo || symbol || name) && ( h('div.add-token__token-wrapper', { className: classnames('add-token__token-wrapper', { - 'add-token__token-wrapper--selected': selectedToken[symbol], + 'add-token__token-wrapper--selected': selectedTokens[address], }), - onClick: () => this.toggleToken(symbol), + onClick: () => this.toggleToken(address, results[i]), }, [ h('div.add-token__token-icon', { style: { @@ -132,11 +238,69 @@ AddTokenScreen.prototype.renderTokenList = function () { }) } +AddTokenScreen.prototype.renderConfirmation = function () { + const { + customAddress: address, + customSymbol: symbol, + customDecimals: decimals, + selectedTokens, + } = this.state + + const { addTokens, goHome } = this.props + + const customToken = { + address, + symbol, + decimals, + } + + const tokens = address && symbol && decimals + ? { ...selectedTokens, [address]: customToken } + : selectedTokens + + return ( + h('div.add-token', [ + h('div.add-token__wrapper', [ + h('div.add-token__title-container.add-token__confirmation-title', [ + h('div.add-token__title', 'Add Token'), + h('div.add-token__description', 'Would you like to add these tokens?'), + ]), + h('div.add-token__content-container.add-token__confirmation-content', [ + h('div.add-token__description.add-token__confirmation-description', 'Your balances'), + h('div.add-token__confirmation-token-list', + Object.entries(tokens) + .map(([ address, token ]) => ( + h('span.add-token__confirmation-token-list-item', [ + h(Identicon, { + className: 'add-token__confirmation-token-icon', + diameter: 75, + address, + }), + h(TokenBalance, { token }), + ]) + )) + ), + ]), + ]), + h('div.add-token__buttons', [ + h('button.btn-secondary', { + onClick: () => addTokens(tokens).then(goHome), + }, 'Add Tokens'), + h('button.btn-tertiary', { + onClick: () => this.setState({ isShowingConfirmation: false }), + }, 'Back'), + ]), + ]) + ) +} + AddTokenScreen.prototype.render = function () { - const { isCollapsed } = this.state + const { isCollapsed, errors, isShowingConfirmation } = this.state const { goHome } = this.props - return ( + return isShowingConfirmation + ? this.renderConfirmation() + : ( h('div.add-token', [ h('div.add-token__wrapper', [ h('div.add-token__title-container', [ @@ -151,6 +315,7 @@ AddTokenScreen.prototype.render = function () { placeholder: 'Search', onChange: e => this.setState({ searchQuery: e.target.value }), }), + h('div.add-token__search-input-error-message', errors.tokenSelector), ]), h( 'div.add-token__token-icons-container', @@ -165,7 +330,9 @@ AddTokenScreen.prototype.render = function () { ]), ]), h('div.add-token__buttons', [ - h('button.btn-secondary', 'Next'), + h('button.btn-secondary', { + onClick: this.onNext, + }, 'Next'), h('button.btn-tertiary', { onClick: goHome, }, 'Cancel'), @@ -173,214 +340,3 @@ AddTokenScreen.prototype.render = function () { ]) ) } - -// AddTokenScreen.prototype.render = function () { -// const state = this.state -// const props = this.props -// const { warning, symbol, decimals } = state - -// return ( -// h('.flex-column.flex-grow', [ - -// // subtitle and nav -// h('.section-title.flex-row.flex-center', [ -// h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { -// onClick: (event) => { -// props.dispatch(actions.goHome()) -// }, -// }), -// h('h2.page-subtitle', 'Add Token'), -// ]), - -// h('.error', { -// style: { -// display: warning ? 'block' : 'none', -// padding: '0 20px', -// textAlign: 'center', -// }, -// }, warning), - -// // conf view -// h('.flex-column.flex-justify-center.flex-grow.select-none', [ -// h('.flex-space-around', { -// style: { -// padding: '20px', -// }, -// }, [ - -// h('div', [ -// h(Tooltip, { -// position: 'top', -// title: 'The contract of the actual token contract. Click for more info.', -// }, [ -// h('a', { -// style: { fontWeight: 'bold', paddingRight: '10px'}, -// href: 'https://consensyssupport.happyfox.com/staff/kb/article/24-what-is-a-token-contract-address', -// target: '_blank', -// }, [ -// h('span', 'Token Contract Address '), -// h('i.fa.fa-question-circle'), -// ]), -// ]), -// ]), - -// h('section.flex-row.flex-center', [ -// h('input#token-address', { -// name: 'address', -// placeholder: 'Token Contract Address', -// onChange: this.tokenAddressDidChange.bind(this), -// style: { -// width: 'inherit', -// flex: '1 0 auto', -// height: '30px', -// margin: '8px', -// }, -// }), -// ]), - -// h('div', [ -// h('span', { -// style: { fontWeight: 'bold', paddingRight: '10px'}, -// }, 'Token Symbol'), -// ]), - -// h('div', { style: {display: 'flex'} }, [ -// h('input#token_symbol', { -// placeholder: `Like "ETH"`, -// value: symbol, -// style: { -// width: 'inherit', -// flex: '1 0 auto', -// height: '30px', -// margin: '8px', -// }, -// onChange: (event) => { -// var element = event.target -// var symbol = element.value -// this.setState({ symbol }) -// }, -// }), -// ]), - -// h('div', [ -// h('span', { -// style: { fontWeight: 'bold', paddingRight: '10px'}, -// }, 'Decimals of Precision'), -// ]), - -// h('div', { style: {display: 'flex'} }, [ -// h('input#token_decimals', { -// value: decimals, -// type: 'number', -// min: 0, -// max: 36, -// style: { -// width: 'inherit', -// flex: '1 0 auto', -// height: '30px', -// margin: '8px', -// }, -// onChange: (event) => { -// var element = event.target -// var decimals = element.value.trim() -// this.setState({ decimals }) -// }, -// }), -// ]), - -// h('button', { -// style: { -// alignSelf: 'center', -// }, -// onClick: (event) => { -// const valid = this.validateInputs() -// if (!valid) return - -// const { address, symbol, decimals } = this.state -// this.props.dispatch(actions.addToken(address.trim(), symbol.trim(), decimals)) -// }, -// }, 'Add'), -// ]), -// ]), -// ]) -// ) -// } - -AddTokenScreen.prototype.componentWillMount = function () { - if (typeof global.ethereumProvider === 'undefined') return - - this.eth = new Eth(global.ethereumProvider) - this.contract = new EthContract(this.eth) - this.TokenContract = this.contract(abi) -} - -AddTokenScreen.prototype.tokenAddressDidChange = function (e) { - const customAddress = e.target.value.trim() - this.setState({ customAddress }) - if (ethUtil.isValidAddress(customAddress) && customAddress !== emptyAddr) { - this.attemptToAutoFillTokenParams(customAddress) - } else { - this.setState({ - customSymbol: '', - customDecimals: 0, - }) - } -} - -AddTokenScreen.prototype.validateInputs = function () { - let msg = '' - const state = this.state - const identitiesList = Object.keys(this.props.identities) - const { address, symbol, decimals } = state - const standardAddress = ethUtil.addHexPrefix(address).toLowerCase() - - const validAddress = ethUtil.isValidAddress(address) - if (!validAddress) { - msg += 'Address is invalid. ' - } - - const validDecimals = decimals >= 0 && decimals < 36 - if (!validDecimals) { - msg += 'Decimals must be at least 0, and not over 36. ' - } - - const symbolLen = symbol.trim().length - const validSymbol = symbolLen > 0 && symbolLen < 10 - if (!validSymbol) { - msg += 'Symbol must be between 0 and 10 characters.' - } - - const ownAddress = identitiesList.includes(standardAddress) - if (ownAddress) { - msg = 'Personal address detected. Input the token contract address.' - } - - const isValid = validAddress && validDecimals && !ownAddress - - if (!isValid) { - this.setState({ - warning: msg, - }) - } else { - this.setState({ warning: null }) - } - - return isValid -} - -AddTokenScreen.prototype.attemptToAutoFillTokenParams = async function (address) { - const contract = this.TokenContract.at(address) - - const results = await Promise.all([ - contract.symbol(), - contract.decimals(), - ]) - - const [ symbol, decimals ] = results - if (symbol && decimals) { - this.setState({ - customSymbol: symbol[0], - customDecimals: decimals[0].toString(), - }) - } -} |