aboutsummaryrefslogtreecommitdiffstats
path: root/ui/app/add-token.js
diff options
context:
space:
mode:
Diffstat (limited to 'ui/app/add-token.js')
-rw-r--r--ui/app/add-token.js428
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(),
- })
- }
-}