diff options
Diffstat (limited to 'ui/app')
-rw-r--r-- | ui/app/actions.js | 39 | ||||
-rw-r--r-- | ui/app/add-token.js | 428 | ||||
-rw-r--r-- | ui/app/components/token-balance.js | 14 | ||||
-rw-r--r-- | ui/app/components/tx-list.js | 2 | ||||
-rw-r--r-- | ui/app/css/itcss/components/add-token.scss | 95 | ||||
-rw-r--r-- | ui/app/css/itcss/components/buttons.scss | 10 |
6 files changed, 333 insertions, 255 deletions
diff --git a/ui/app/actions.js b/ui/app/actions.js index 678c68a6a..1231fc296 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -147,6 +147,7 @@ var actions = { SHOW_ADD_TOKEN_PAGE: 'SHOW_ADD_TOKEN_PAGE', showAddTokenPage, addToken, + addTokens, setRpcTarget: setRpcTarget, setDefaultRpcTarget: setDefaultRpcTarget, setProviderType: setProviderType, @@ -700,18 +701,40 @@ function showAddTokenPage () { function addToken (address, symbol, decimals) { return (dispatch) => { dispatch(actions.showLoadingIndication()) - background.addToken(address, symbol, decimals, (err) => { - dispatch(actions.hideLoadingIndication()) - if (err) { - return dispatch(actions.displayWarning(err.message)) - } - setTimeout(() => { - dispatch(actions.goHome()) - }, 250) + return new Promise((resolve, reject) => { + background.addToken(address, symbol, decimals, (err) => { + dispatch(actions.hideLoadingIndication()) + if (err) { + dispatch(actions.displayWarning(err.message)) + reject(err) + } + resolve() + // setTimeout(() => { + // dispatch(actions.goHome()) + // }, 250) + }) }) } } +function addTokens (tokens) { + return dispatch => { + if (Array.isArray(tokens)) { + return Promise.all(tokens.map(({ address, symbol, decimals }) => ( + dispatch(addToken(address, symbol, decimals)) + ))) + } else { + return Promise.all( + Object + .entries(tokens) + .map(([_, { address, symbol, decimals }]) => ( + dispatch(addToken(address, symbol, decimals)) + )) + ) + } + } +} + function goBackToInitView () { return { type: actions.BACK_TO_INIT_MENU, 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(), - }) - } -} diff --git a/ui/app/components/token-balance.js b/ui/app/components/token-balance.js index 3a923eb9d..0342c1da9 100644 --- a/ui/app/components/token-balance.js +++ b/ui/app/components/token-balance.js @@ -17,7 +17,8 @@ module.exports = connect(mapStateToProps)(TokenBalance) inherits(TokenBalance, Component) function TokenBalance () { this.state = { - balance: '', + string: '', + symbol: '', isLoading: true, error: null, } @@ -26,11 +27,14 @@ function TokenBalance () { TokenBalance.prototype.render = function () { const state = this.state - const { balance, isLoading } = state + const { symbol, string, balanceOnly, isLoading } = state return isLoading ? h('span', '') - : h('span', balance) + : h('span.token-balance', [ + h('span.token-balance__amount', string), + !balanceOnly && h('span.token-balance__symbol', symbol), + ]) } TokenBalance.prototype.componentDidMount = function () { @@ -93,10 +97,10 @@ TokenBalance.prototype.componentDidUpdate = function (nextProps) { TokenBalance.prototype.updateBalance = function (tokens = []) { const [{ string, symbol }] = tokens - const { balanceOnly } = this.props this.setState({ - balance: balanceOnly ? string : `${string} ${symbol}`, + string, + symbol, isLoading: false, }) } diff --git a/ui/app/components/tx-list.js b/ui/app/components/tx-list.js index 7a147e942..f817d03a9 100644 --- a/ui/app/components/tx-list.js +++ b/ui/app/components/tx-list.js @@ -77,7 +77,7 @@ TxList.prototype.renderTransactionListItem = function (transaction, conversionRa const { showConfTxPage } = this.props const opts = { - key: transActionId, + key: transActionId || transactionHash, txParams: transaction.txParams, transactionStatus, transActionId, diff --git a/ui/app/css/itcss/components/add-token.scss b/ui/app/css/itcss/components/add-token.scss index ebfdf7b11..d5d1aab71 100644 --- a/ui/app/css/itcss/components/add-token.scss +++ b/ui/app/css/itcss/components/add-token.scss @@ -56,6 +56,10 @@ margin-top: 24px; } + &__confirmation-description { + margin: 12px 0; + } + &__content-container { width: 100%; border-bottom: 1px solid $gallery; @@ -65,6 +69,18 @@ padding: 11px 0; width: 263px; margin: 0 auto; + position: relative; + } + + &__search-input-error-message { + position: absolute; + bottom: -10px; + font-size: 12px; + width: 100%; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + color: $red; } &__input { @@ -89,9 +105,13 @@ font-size: 18px; line-height: 24px; text-align: center; - padding: 11px 0 19px; + padding: 12px 0; font-weight: 600; cursor: pointer; + + &:hover { + background-color: $gallery; + } } &__add-custom-form { @@ -103,6 +123,24 @@ &__add-custom-field { width: 290px; margin: 0 auto; + position: relative; + + &--error { + .add-token__add-custom-input { + border-color: $red; + } + } + } + + &__add-custom-error-message { + position: absolute; + bottom: -21px; + font-size: 12px; + width: 100%; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + color: $red; } &__add-custom-label { @@ -152,9 +190,12 @@ cursor: pointer; border: 2px solid transparent; - &:hover, + &:hover { + border: 2px solid rgba($malibu-blue, .5); + } + &--selected { - border: 2px solid $malibu-blue; + border: 2px solid $malibu-blue !important; } } @@ -181,4 +222,52 @@ margin-right: 12px; flex: 0 0 auto; } + + &__confirmation-token-list { + display: flex; + flex-flow: column nowrap; + + .token-balance { + display: flex; + flex-flow: row nowrap; + align-items: flex-start; + + &__amount { + color: $scorpion; + font-size: 43px; + font-weight: 300; + line-height: 43px; + margin-right: 8px; + } + + &__symbol { + color: $scorpion; + font-size: 16px; + line-height: 24px; + } + } + } + + &__confirmation-title { + padding: 30px 120px 12px; + } + + &__confirmation-content { + padding-bottom: 60px; + } + + &__confirmation-token-list-item { + display: flex; + flex-flow: row nowrap; + padding: 0 120px; + align-items: center; + } + + &__confirmation-token-list-item + &__confirmation-token-list-item { + margin-top: 30px; + } + + &__confirmation-token-icon { + margin-right: 18px; + } } diff --git a/ui/app/css/itcss/components/buttons.scss b/ui/app/css/itcss/components/buttons.scss index 0946cdbbb..2c5e6cf57 100644 --- a/ui/app/css/itcss/components/buttons.scss +++ b/ui/app/css/itcss/components/buttons.scss @@ -30,8 +30,9 @@ button.btn-clear { button[disabled], input[type="submit"][disabled] { cursor: not-allowed; - background: rgba(197, 197, 197, 1); - box-shadow: 0 3px 6px rgba(197, 197, 197, .36); + opacity: .5; + // background: rgba(197, 197, 197, 1); + // box-shadow: 0 3px 6px rgba(197, 197, 197, .36); } // button.spaced { @@ -90,6 +91,11 @@ button.btn-thin { font-size: 16px; line-height: 24px; padding: 16px 42px; + + &[disabled] { + background-color: $white !important; + opacity: .5; + } } .btn-tertiary { |