From 3ed81847d1b6f00e208dbcb973cafcc633c268ad Mon Sep 17 00:00:00 2001 From: sdtsui Date: Wed, 2 Aug 2017 23:54:21 -0700 Subject: Isolate routing logic for isUnlocked, remove stray logs --- ui/app/add-token.js | 1 - 1 file changed, 1 deletion(-) (limited to 'ui/app/add-token.js') diff --git a/ui/app/add-token.js b/ui/app/add-token.js index 15ef7a852..5c6dea4a0 100644 --- a/ui/app/add-token.js +++ b/ui/app/add-token.js @@ -212,7 +212,6 @@ AddTokenScreen.prototype.attemptToAutoFillTokenParams = async function (address) const [ symbol, decimals ] = results if (symbol && decimals) { - console.log('SETTING SYMBOL AND DECIMALS', { symbol, decimals }) this.setState({ symbol: symbol[0], decimals: decimals[0].toString() }) } } -- cgit From 0204aa2001af25da01ba61aed32f36eac47079a1 Mon Sep 17 00:00:00 2001 From: Chi Kei Chan Date: Tue, 19 Sep 2017 21:18:36 -0700 Subject: Add Add Token UI; Add Fuzzy search for tokens --- ui/app/add-token.js | 348 +++++++++++++++++++++++++++++++++------------------- 1 file changed, 223 insertions(+), 125 deletions(-) (limited to 'ui/app/add-token.js') diff --git a/ui/app/add-token.js b/ui/app/add-token.js index 4374ee586..dbba8e4f1 100644 --- a/ui/app/add-token.js +++ b/ui/app/add-token.js @@ -2,8 +2,20 @@ const inherits = require('util').inherits const Component = require('react').Component const h = require('react-hyperscript') const connect = require('react-redux').connect -const actions = require('./actions') -const Tooltip = require('./components/tooltip.js') +const Fuse = require('fuse.js') +const contractMap = require('eth-contract-metadata') +const contractList = Object.entries(contractMap).map(([ _, tokenData]) => tokenData) +const fuse = new Fuse(contractList, { + shouldSort: true, + threshold: 0.3, + location: 0, + distance: 100, + maxPatternLength: 32, + minMatchCharLength: 1, + keys: ['address', 'name', 'symbol'], +}) +// const actions = require('./actions') +// const Tooltip = require('./components/tooltip.js') const ethUtil = require('ethereumjs-util') @@ -24,146 +36,232 @@ function mapStateToProps (state) { inherits(AddTokenScreen, Component) function AddTokenScreen () { this.state = { - warning: null, - address: null, - symbol: 'TOKEN', - decimals: 18, + // warning: null, + // address: null, + // symbol: 'TOKEN', + // decimals: 18, + searchQuery: '', + isCollapsed: true, } Component.call(this) } -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'), +AddTokenScreen.prototype.renderCustomForm = function () { + return !this.state.isCollapsed && ( + h('div.add-token__add-custom-form', [ + h('div.add-token__add-custom-field', [ + h('div.add-token__add-custom-label', 'Token Address'), + h('input.add-token__add-custom-input', { type: 'text' }), ]), + h('div.add-token__add-custom-field', [ + h('div.add-token__add-custom-label', 'Token Symbol'), + h('input.add-token__add-custom-input', { type: 'text', disabled: true }), + ]), + h('div.add-token__add-custom-field', [ + h('div.add-token__add-custom-label', 'Decimals of Precision'), + h('input.add-token__add-custom-input', { type: 'text', disabled: true }), + ]), + ]) + ) +} - 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 }) - }, - }), +AddTokenScreen.prototype.renderTokenList = function () { + const { searchQuery = '' } = this.state + const results = searchQuery + ? fuse.search(searchQuery) || [] + : contractList + + return Array(6).fill(undefined) + .map((_, i) => { + const { logo, symbol, name } = results[i] || {} + console.log({ i, logo, symbol, name }) + return Boolean(logo || symbol || name) && ( + h('div.add-token__token-wrapper', [ + h('div.add-token__token-icon', { + style: { + backgroundImage: `url(images/contract/${logo})`, + }, + }), + h('div.add-token__token-data', [ + h('div.add-token__token-symbol', symbol), + h('div.add-token__token-name', name), ]), + ]) + ) + }) +} - h('div', [ - h('span', { - style: { fontWeight: 'bold', paddingRight: '10px'}, - }, 'Decimals of Precision'), - ]), +AddTokenScreen.prototype.render = function () { + const { isCollapsed } = this.state - 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 }) - }, + return ( + h('div.add-token', [ + h('div.add-token__wrapper', [ + h('div.add-token__title-container', [ + h('div.add-token__title', 'Add Token'), + h('div.add-token__description', 'Keep track of the tokens you’ve bought with your MetaMask account. If you bought tokens using a different account, those tokens will not appear here.'), + h('div.add-token__description', 'Search for tokens or select from our list of popular tokens.'), + ]), + h('div.add-token__content-container', [ + h('div.add-token__input-container', [ + h('input.add-token__input', { + type: 'text', + placeholder: 'Search', + onChange: e => this.setState({ searchQuery: e.target.value }), }), ]), - - 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'), + h( + 'div.add-token__token-icons-container', + this.renderTokenList(), + ), + ]), + h('div.add-token__footers', [ + h('div.add-token__add-custom', { + onClick: () => this.setState({ isCollapsed: !isCollapsed }), + }, 'Add custom token'), + this.renderCustomForm(), ]), ]), + h('div.add-token__buttons', [ + h('button.btn-secondary', 'Next'), + h('button.btn-tertiary', 'Cancel'), + ]), ]) ) } +// 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 -- cgit From 04da22db0863a9a361a0f414d9cc37bf3bb3a392 Mon Sep 17 00:00:00 2001 From: Chi Kei Chan Date: Wed, 20 Sep 2017 22:57:36 -0700 Subject: Add Token UI - hover/select state; fetch token address --- ui/app/add-token.js | 85 ++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 68 insertions(+), 17 deletions(-) (limited to 'ui/app/add-token.js') diff --git a/ui/app/add-token.js b/ui/app/add-token.js index dbba8e4f1..622cf2bc2 100644 --- a/ui/app/add-token.js +++ b/ui/app/add-token.js @@ -1,5 +1,6 @@ const inherits = require('util').inherits const Component = require('react').Component +const classnames = require('classnames') const h = require('react-hyperscript') const connect = require('react-redux').connect const Fuse = require('fuse.js') @@ -7,14 +8,14 @@ const contractMap = require('eth-contract-metadata') const contractList = Object.entries(contractMap).map(([ _, tokenData]) => tokenData) const fuse = new Fuse(contractList, { shouldSort: true, - threshold: 0.3, + threshold: 0.45, location: 0, distance: 100, maxPatternLength: 32, minMatchCharLength: 1, keys: ['address', 'name', 'symbol'], }) -// const actions = require('./actions') +const actions = require('./actions') // const Tooltip = require('./components/tooltip.js') @@ -25,7 +26,7 @@ const EthContract = require('ethjs-contract') const emptyAddr = '0x0000000000000000000000000000000000000000' -module.exports = connect(mapStateToProps)(AddTokenScreen) +module.exports = connect(mapStateToProps, mapDispatchToProps)(AddTokenScreen) function mapStateToProps (state) { return { @@ -33,6 +34,12 @@ function mapStateToProps (state) { } } +function mapDispatchToProps (dispatch) { + return { + goHome: () => dispatch(actions.goHome()), + } +} + inherits(AddTokenScreen, Component) function AddTokenScreen () { this.state = { @@ -40,33 +47,63 @@ function AddTokenScreen () { // address: null, // symbol: 'TOKEN', // decimals: 18, + customAddress: '', + customSymbol: '', + customDecimals: 0, searchQuery: '', isCollapsed: true, + selectedToken: {}, } + this.tokenAddressDidChange = this.tokenAddressDidChange.bind(this) Component.call(this) } +AddTokenScreen.prototype.toggleToken = function (symbol) { + const { selectedToken } = this.state + const { [symbol]: isSelected } = selectedToken + this.setState({ + selectedToken: { + ...selectedToken, + [symbol]: !isSelected, + }, + }) +} + AddTokenScreen.prototype.renderCustomForm = function () { + const { customAddress, customSymbol, customDecimals } = this.state + return !this.state.isCollapsed && ( h('div.add-token__add-custom-form', [ h('div.add-token__add-custom-field', [ h('div.add-token__add-custom-label', 'Token Address'), - h('input.add-token__add-custom-input', { type: 'text' }), + h('input.add-token__add-custom-input', { + type: 'text', + onChange: this.tokenAddressDidChange, + value: customAddress, + }), ]), h('div.add-token__add-custom-field', [ h('div.add-token__add-custom-label', 'Token Symbol'), - h('input.add-token__add-custom-input', { type: 'text', disabled: true }), + h('input.add-token__add-custom-input', { + type: 'text', + value: customSymbol, + disabled: true, + }), ]), h('div.add-token__add-custom-field', [ h('div.add-token__add-custom-label', 'Decimals of Precision'), - h('input.add-token__add-custom-input', { type: 'text', disabled: true }), + h('input.add-token__add-custom-input', { + type: 'number', + value: customDecimals, + disabled: true, + }), ]), ]) ) } AddTokenScreen.prototype.renderTokenList = function () { - const { searchQuery = '' } = this.state + const { searchQuery = '', selectedToken } = this.state const results = searchQuery ? fuse.search(searchQuery) || [] : contractList @@ -74,9 +111,13 @@ AddTokenScreen.prototype.renderTokenList = function () { return Array(6).fill(undefined) .map((_, i) => { const { logo, symbol, name } = results[i] || {} - console.log({ i, logo, symbol, name }) return Boolean(logo || symbol || name) && ( - h('div.add-token__token-wrapper', [ + h('div.add-token__token-wrapper', { + className: classnames('add-token__token-wrapper', { + 'add-token__token-wrapper--selected': selectedToken[symbol], + }), + onClick: () => this.toggleToken(symbol), + }, [ h('div.add-token__token-icon', { style: { backgroundImage: `url(images/contract/${logo})`, @@ -93,6 +134,7 @@ AddTokenScreen.prototype.renderTokenList = function () { AddTokenScreen.prototype.render = function () { const { isCollapsed } = this.state + const { goHome } = this.props return ( h('div.add-token', [ @@ -124,7 +166,9 @@ AddTokenScreen.prototype.render = function () { ]), h('div.add-token__buttons', [ h('button.btn-secondary', 'Next'), - h('button.btn-tertiary', 'Cancel'), + h('button.btn-tertiary', { + onClick: goHome, + }, 'Cancel'), ]), ]) ) @@ -270,12 +314,16 @@ AddTokenScreen.prototype.componentWillMount = function () { this.TokenContract = this.contract(abi) } -AddTokenScreen.prototype.tokenAddressDidChange = function (event) { - const el = event.target - const address = el.value.trim() - if (ethUtil.isValidAddress(address) && address !== emptyAddr) { - this.setState({ address }) - this.attemptToAutoFillTokenParams(address) +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, + }) } } @@ -330,6 +378,9 @@ AddTokenScreen.prototype.attemptToAutoFillTokenParams = async function (address) const [ symbol, decimals ] = results if (symbol && decimals) { - this.setState({ symbol: symbol[0], decimals: decimals[0].toString() }) + this.setState({ + customSymbol: symbol[0], + customDecimals: decimals[0].toString(), + }) } } -- cgit From 3ec2f534632426876c28b22c58cbbf14b4904d97 Mon Sep 17 00:00:00 2001 From: Chi Kei Chan Date: Thu, 21 Sep 2017 18:44:52 -0700 Subject: Integrate Add Token --- ui/app/add-token.js | 428 +++++++++++++++++++++++----------------------------- 1 file changed, 192 insertions(+), 236 deletions(-) (limited to 'ui/app/add-token.js') 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(), - }) - } -} -- cgit From a59972dcabc56c3d92f09ba1b88a2ded70ce8c34 Mon Sep 17 00:00:00 2001 From: Alexander Tseung Date: Fri, 13 Oct 2017 17:14:48 -0400 Subject: Prevent adding already added tokens (#2362) --- ui/app/add-token.js | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) (limited to 'ui/app/add-token.js') diff --git a/ui/app/add-token.js b/ui/app/add-token.js index f723ff07c..90edc8de1 100644 --- a/ui/app/add-token.js +++ b/ui/app/add-token.js @@ -22,14 +22,17 @@ const ethUtil = require('ethereumjs-util') const abi = require('human-standard-token-abi') const Eth = require('ethjs-query') const EthContract = require('ethjs-contract') +const R = require('ramda') const emptyAddr = '0x0000000000000000000000000000000000000000' module.exports = connect(mapStateToProps, mapDispatchToProps)(AddTokenScreen) function mapStateToProps (state) { + const { identities, tokens } = state.metamask return { - identities: state.metamask.identities, + identities, + tokens, } } @@ -101,6 +104,15 @@ AddTokenScreen.prototype.tokenAddressDidChange = function (e) { } } +AddTokenScreen.prototype.checkExistingAddresses = function (address) { + const tokensList = this.props.tokens + const matchesAddress = existingToken => { + return existingToken.address.toLowerCase() === address.toLowerCase() + } + + return R.any(matchesAddress)(tokensList) +} + AddTokenScreen.prototype.validate = function () { const errors = {} const identitiesList = Object.keys(this.props.identities) @@ -128,6 +140,11 @@ AddTokenScreen.prototype.validate = function () { if (ownAddress) { errors.customAddress = 'Personal address detected. Input the token contract address.' } + + const tokenAlreadyAdded = this.checkExistingAddresses(customAddress) + if (tokenAlreadyAdded) { + errors.customAddress = 'Token has already been added.' + } } else if ( Object.entries(selectedTokens) .reduce((isEmpty, [ symbol, isSelected ]) => ( @@ -217,12 +234,14 @@ AddTokenScreen.prototype.renderTokenList = function () { return Array(6).fill(undefined) .map((_, i) => { const { logo, symbol, name, address } = results[i] || {} + const tokenAlreadyAdded = this.checkExistingAddresses(address) return Boolean(logo || symbol || name) && ( h('div.add-token__token-wrapper', { - className: classnames('add-token__token-wrapper', { + className: classnames({ 'add-token__token-wrapper--selected': selectedTokens[address], + 'add-token__token-wrapper--disabled': tokenAlreadyAdded, }), - onClick: () => this.toggleToken(address, results[i]), + onClick: () => !tokenAlreadyAdded && this.toggleToken(address, results[i]), }, [ h('div.add-token__token-icon', { style: { @@ -233,6 +252,9 @@ AddTokenScreen.prototype.renderTokenList = function () { h('div.add-token__token-symbol', symbol), h('div.add-token__token-name', name), ]), + tokenAlreadyAdded && ( + h('div.add-token__token-message', 'Already added') + ), ]) ) }) -- cgit From 89573533b8da75138bddffbadbaeff6f9eb68001 Mon Sep 17 00:00:00 2001 From: Dan Date: Fri, 20 Oct 2017 11:48:01 -0230 Subject: Fixes add token search. --- ui/app/add-token.js | 1 + 1 file changed, 1 insertion(+) (limited to 'ui/app/add-token.js') diff --git a/ui/app/add-token.js b/ui/app/add-token.js index 90edc8de1..3ef9b6814 100644 --- a/ui/app/add-token.js +++ b/ui/app/add-token.js @@ -105,6 +105,7 @@ AddTokenScreen.prototype.tokenAddressDidChange = function (e) { } AddTokenScreen.prototype.checkExistingAddresses = function (address) { + if (!address) return false const tokensList = this.props.tokens const matchesAddress = existingToken => { return existingToken.address.toLowerCase() === address.toLowerCase() -- cgit From b25c73a866140e362ea27d455101d07ccf1a56e6 Mon Sep 17 00:00:00 2001 From: Dan Date: Fri, 20 Oct 2017 12:01:34 -0230 Subject: Only show erc20 tokens in add token search. --- ui/app/add-token.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'ui/app/add-token.js') diff --git a/ui/app/add-token.js b/ui/app/add-token.js index 3ef9b6814..e313babf3 100644 --- a/ui/app/add-token.js +++ b/ui/app/add-token.js @@ -7,7 +7,9 @@ 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 contractList = Object.entries(contractMap) + .map(([ _, tokenData]) => tokenData) + .filter(tokenData => Boolean(tokenData.erc20)) const fuse = new Fuse(contractList, { shouldSort: true, threshold: 0.45, -- cgit From ba9cefb24e61e570407154c37b6cc30b22829667 Mon Sep 17 00:00:00 2001 From: Chi Kei Chan Date: Tue, 24 Oct 2017 00:38:39 -0700 Subject: Add caret to Add Token toggle --- ui/app/add-token.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'ui/app/add-token.js') diff --git a/ui/app/add-token.js b/ui/app/add-token.js index e313babf3..148a8c622 100644 --- a/ui/app/add-token.js +++ b/ui/app/add-token.js @@ -350,7 +350,10 @@ AddTokenScreen.prototype.render = function () { h('div.add-token__footers', [ h('div.add-token__add-custom', { onClick: () => this.setState({ isCollapsed: !isCollapsed }), - }, 'Add custom token'), + }, [ + 'Add custom token', + h(`i.fa.fa-angle-${isCollapsed ? 'down' : 'up'}`), + ]), this.renderCustomForm(), ]), ]), -- cgit From b2e440e4ffba6db29cf8a928a41534c1f204d485 Mon Sep 17 00:00:00 2001 From: Chi Kei Chan Date: Wed, 25 Oct 2017 18:47:28 -0700 Subject: Add Token styling fix --- ui/app/add-token.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'ui/app/add-token.js') diff --git a/ui/app/add-token.js b/ui/app/add-token.js index 148a8c622..518701a1d 100644 --- a/ui/app/add-token.js +++ b/ui/app/add-token.js @@ -255,9 +255,9 @@ AddTokenScreen.prototype.renderTokenList = function () { h('div.add-token__token-symbol', symbol), h('div.add-token__token-name', name), ]), - tokenAlreadyAdded && ( - h('div.add-token__token-message', 'Already added') - ), + // tokenAlreadyAdded && ( + // h('div.add-token__token-message', 'Already added') + // ), ]) ) }) -- cgit From c8c918d44e26e9541beead982ef0ed79a56d6e6f Mon Sep 17 00:00:00 2001 From: Dan Date: Fri, 27 Oct 2017 15:09:40 -0230 Subject: Add utility for getting token data; get token data in tx-list even if token has been removed. --- ui/app/add-token.js | 23 +++++------------------ 1 file changed, 5 insertions(+), 18 deletions(-) (limited to 'ui/app/add-token.js') diff --git a/ui/app/add-token.js b/ui/app/add-token.js index 518701a1d..10aaae103 100644 --- a/ui/app/add-token.js +++ b/ui/app/add-token.js @@ -21,9 +21,7 @@ const fuse = new Fuse(contractList, { }) const actions = require('./actions') const ethUtil = require('ethereumjs-util') -const abi = require('human-standard-token-abi') -const Eth = require('ethjs-query') -const EthContract = require('ethjs-contract') +const { tokenInfoGetter } = require('./token-util') const R = require('ramda') const emptyAddr = '0x0000000000000000000000000000000000000000' @@ -63,11 +61,7 @@ function AddTokenScreen () { } 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) + this.tokenInfoGetter = tokenInfoGetter() } AddTokenScreen.prototype.toggleToken = function (address, token) { @@ -164,18 +158,11 @@ AddTokenScreen.prototype.validate = function () { } 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 + const { symbol, decimals } = await this.tokenInfoGetter(address) if (symbol && decimals) { this.setState({ - customSymbol: symbol[0], - customDecimals: decimals[0].toString(), + customSymbol: symbol, + customDecimals: decimals.toString(), }) } } -- cgit From dce6dcf437ecb7769d45ee38f1d2227a74f09ca6 Mon Sep 17 00:00:00 2001 From: Alexander Tseung Date: Fri, 12 Jan 2018 16:16:28 -0800 Subject: Fix Add Token button order, styling --- ui/app/add-token.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'ui/app/add-token.js') diff --git a/ui/app/add-token.js b/ui/app/add-token.js index 10aaae103..9f1357591 100644 --- a/ui/app/add-token.js +++ b/ui/app/add-token.js @@ -345,12 +345,12 @@ AddTokenScreen.prototype.render = function () { ]), ]), h('div.add-token__buttons', [ - h('button.btn-secondary', { - onClick: this.onNext, - }, 'Next'), - h('button.btn-tertiary', { + h('button.btn-cancel.add-token__button', { onClick: goHome, }, 'Cancel'), + h('button.btn-clear.add-token__button', { + onClick: this.onNext, + }, 'Next'), ]), ]) ) -- cgit From b42baacdf4683be560c5632752347eadf77f1d0f Mon Sep 17 00:00:00 2001 From: Alexander Tseung Date: Fri, 12 Jan 2018 16:41:29 -0800 Subject: Fix confirm Add Token button order --- ui/app/add-token.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'ui/app/add-token.js') diff --git a/ui/app/add-token.js b/ui/app/add-token.js index 9f1357591..bb34f5d00 100644 --- a/ui/app/add-token.js +++ b/ui/app/add-token.js @@ -295,12 +295,12 @@ AddTokenScreen.prototype.renderConfirmation = function () { ]), ]), h('div.add-token__buttons', [ - h('button.btn-secondary', { - onClick: () => addTokens(tokens).then(goHome), - }, 'Add Tokens'), - h('button.btn-tertiary', { + h('button.btn-cancel.add-token__button', { onClick: () => this.setState({ isShowingConfirmation: false }), }, 'Back'), + h('button.btn-clear.add-token__button', { + onClick: () => addTokens(tokens).then(goHome), + }, 'Add Tokens'), ]), ]) ) -- cgit From fdaf6eacb2dc6cbd6941dd7debdd5cd89fca4672 Mon Sep 17 00:00:00 2001 From: Dan Date: Wed, 17 Jan 2018 17:29:25 -0330 Subject: Show now tokens by default and improve search on add token screen. --- ui/app/add-token.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) (limited to 'ui/app/add-token.js') diff --git a/ui/app/add-token.js b/ui/app/add-token.js index bb34f5d00..e3fe93362 100644 --- a/ui/app/add-token.js +++ b/ui/app/add-token.js @@ -3,6 +3,7 @@ const Component = require('react').Component const classnames = require('classnames') const h = require('react-hyperscript') const connect = require('react-redux').connect +const R = require('ramda'); const Fuse = require('fuse.js') const contractMap = require('eth-contract-metadata') const TokenBalance = require('./components/token-balance') @@ -17,12 +18,14 @@ const fuse = new Fuse(contractList, { distance: 100, maxPatternLength: 32, minMatchCharLength: 1, - keys: ['address', 'name', 'symbol'], + keys: [ + { name: 'name', weight: 0.5 }, + { name: 'symbol', weight: 0.5 }, + ], }) const actions = require('./actions') const ethUtil = require('ethereumjs-util') const { tokenInfoGetter } = require('./token-util') -const R = require('ramda') const emptyAddr = '0x0000000000000000000000000000000000000000' @@ -217,9 +220,11 @@ AddTokenScreen.prototype.renderCustomForm = function () { AddTokenScreen.prototype.renderTokenList = function () { const { searchQuery = '', selectedTokens } = this.state - const results = searchQuery - ? fuse.search(searchQuery) || [] - : contractList + const fuseSearchResult = fuse.search(searchQuery) + const addressSearchResult = contractList.filter(token => { + return token.address.toLowerCase() === searchQuery.toLowerCase() + }) + const results = [...addressSearchResult, ...fuseSearchResult] return Array(6).fill(undefined) .map((_, i) => { -- cgit From 853e7f84204226b9cc35fe8bca5cb6ebe7ff03b0 Mon Sep 17 00:00:00 2001 From: Alexander Tseung Date: Wed, 17 Jan 2018 18:50:18 -0800 Subject: Fix lint errors --- ui/app/add-token.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'ui/app/add-token.js') diff --git a/ui/app/add-token.js b/ui/app/add-token.js index e3fe93362..3a806d34b 100644 --- a/ui/app/add-token.js +++ b/ui/app/add-token.js @@ -3,7 +3,7 @@ const Component = require('react').Component const classnames = require('classnames') const h = require('react-hyperscript') const connect = require('react-redux').connect -const R = require('ramda'); +const R = require('ramda') const Fuse = require('fuse.js') const contractMap = require('eth-contract-metadata') const TokenBalance = require('./components/token-balance') -- cgit