aboutsummaryrefslogtreecommitdiffstats
path: root/ui/app/components/pages/add-token.js
diff options
context:
space:
mode:
Diffstat (limited to 'ui/app/components/pages/add-token.js')
-rw-r--r--ui/app/components/pages/add-token.js431
1 files changed, 431 insertions, 0 deletions
diff --git a/ui/app/components/pages/add-token.js b/ui/app/components/pages/add-token.js
new file mode 100644
index 000000000..566e42450
--- /dev/null
+++ b/ui/app/components/pages/add-token.js
@@ -0,0 +1,431 @@
+const inherits = require('util').inherits
+const Component = require('react').Component
+const classnames = require('classnames')
+const h = require('react-hyperscript')
+const PropTypes = require('prop-types')
+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')
+const Identicon = require('../../components/identicon')
+const contractList = Object.entries(contractMap)
+ .map(([ _, tokenData]) => tokenData)
+ .filter(tokenData => Boolean(tokenData.erc20))
+const fuse = new Fuse(contractList, {
+ shouldSort: true,
+ threshold: 0.45,
+ location: 0,
+ distance: 100,
+ maxPatternLength: 32,
+ minMatchCharLength: 1,
+ 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 { DEFAULT_ROUTE } = require('../../routes')
+
+const emptyAddr = '0x0000000000000000000000000000000000000000'
+
+AddTokenScreen.contextTypes = {
+ t: PropTypes.func,
+}
+
+module.exports = connect(mapStateToProps, mapDispatchToProps)(AddTokenScreen)
+
+
+function mapStateToProps (state) {
+ const { identities, tokens } = state.metamask
+ return {
+ identities,
+ tokens,
+ }
+}
+
+function mapDispatchToProps (dispatch) {
+ return {
+ addTokens: tokens => dispatch(actions.addTokens(tokens)),
+ }
+}
+
+inherits(AddTokenScreen, Component)
+function AddTokenScreen () {
+ this.state = {
+ isShowingConfirmation: false,
+ isShowingInfoBox: true,
+ customAddress: '',
+ customSymbol: '',
+ customDecimals: '',
+ searchQuery: '',
+ selectedTokens: {},
+ errors: {},
+ autoFilled: false,
+ displayedTab: 'SEARCH',
+ }
+ this.tokenAddressDidChange = this.tokenAddressDidChange.bind(this)
+ this.tokenSymbolDidChange = this.tokenSymbolDidChange.bind(this)
+ this.tokenDecimalsDidChange = this.tokenDecimalsDidChange.bind(this)
+ this.onNext = this.onNext.bind(this)
+ Component.call(this)
+}
+
+AddTokenScreen.prototype.componentWillMount = function () {
+ this.tokenInfoGetter = tokenInfoGetter()
+}
+
+AddTokenScreen.prototype.toggleToken = function (address, token) {
+ const { selectedTokens = {}, errors } = this.state
+ const selectedTokensCopy = { ...selectedTokens }
+
+ if (address in selectedTokensCopy) {
+ delete selectedTokensCopy[address]
+ } else {
+ selectedTokensCopy[address] = token
+ }
+
+ this.setState({
+ selectedTokens: selectedTokensCopy,
+ 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.tokenSymbolDidChange = function (e) {
+ const customSymbol = e.target.value.trim()
+ this.setState({ customSymbol })
+}
+
+AddTokenScreen.prototype.tokenDecimalsDidChange = function (e) {
+ const customDecimals = e.target.value.trim()
+ this.setState({ customDecimals })
+}
+
+AddTokenScreen.prototype.checkExistingAddresses = function (address) {
+ if (!address) return false
+ 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)
+ const { customAddress, customSymbol, customDecimals, selectedTokens } = this.state
+ const standardAddress = ethUtil.addHexPrefix(customAddress).toLowerCase()
+
+ if (customAddress) {
+ const validAddress = ethUtil.isValidAddress(customAddress)
+ if (!validAddress) {
+ errors.customAddress = this.context.t('invalidAddress')
+ }
+
+ const validDecimals = customDecimals !== null
+ && customDecimals !== ''
+ && customDecimals >= 0
+ && customDecimals < 36
+ if (!validDecimals) {
+ errors.customDecimals = this.context.t('decimalsMustZerotoTen')
+ }
+
+ const symbolLen = customSymbol.trim().length
+ const validSymbol = symbolLen > 0 && symbolLen < 10
+ if (!validSymbol) {
+ errors.customSymbol = this.context.t('symbolBetweenZeroTen')
+ }
+
+ const ownAddress = identitiesList.includes(standardAddress)
+ if (ownAddress) {
+ errors.customAddress = this.context.t('personalAddressDetected')
+ }
+
+ const tokenAlreadyAdded = this.checkExistingAddresses(customAddress)
+ if (tokenAlreadyAdded) {
+ errors.customAddress = this.context.t('tokenAlreadyAdded')
+ }
+ } else if (
+ Object.entries(selectedTokens)
+ .reduce((isEmpty, [ symbol, isSelected ]) => (
+ isEmpty && !isSelected
+ ), true)
+ ) {
+ errors.tokenSelector = this.context.t('mustSelectOne')
+ }
+
+ return {
+ isValid: !Object.keys(errors).length,
+ errors,
+ }
+}
+
+AddTokenScreen.prototype.attemptToAutoFillTokenParams = async function (address) {
+ const { symbol, decimals } = await this.tokenInfoGetter(address)
+ if (symbol && decimals) {
+ this.setState({
+ customSymbol: symbol,
+ customDecimals: decimals.toString(),
+ autoFilled: true,
+ })
+ }
+}
+
+AddTokenScreen.prototype.renderCustomForm = function () {
+ const { autoFilled, customAddress, customSymbol, customDecimals, errors } = this.state
+
+ return (
+ h('div.add-token__add-custom-form', [
+ h('div', {
+ className: classnames('add-token__add-custom-field', {
+ 'add-token__add-custom-field--error': errors.customAddress,
+ }),
+ }, [
+ h('div.add-token__add-custom-label', this.context.t('tokenAddress')),
+ 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', {
+ className: classnames('add-token__add-custom-field', {
+ 'add-token__add-custom-field--error': errors.customSymbol,
+ }),
+ }, [
+ h('div.add-token__add-custom-label', this.context.t('tokenSymbol')),
+ h('input.add-token__add-custom-input', {
+ type: 'text',
+ onChange: this.tokenSymbolDidChange,
+ value: customSymbol,
+ disabled: autoFilled,
+ }),
+ h('div.add-token__add-custom-error-message', errors.customSymbol),
+ ]),
+ h('div', {
+ className: classnames('add-token__add-custom-field', {
+ 'add-token__add-custom-field--error': errors.customDecimals,
+ }),
+ }, [
+ h('div.add-token__add-custom-label', this.context.t('decimal')),
+ h('input.add-token__add-custom-input', {
+ type: 'number',
+ onChange: this.tokenDecimalsDidChange,
+ value: customDecimals,
+ disabled: autoFilled,
+ }),
+ h('div.add-token__add-custom-error-message', errors.customDecimals),
+ ]),
+ ])
+ )
+}
+
+AddTokenScreen.prototype.renderTokenList = function () {
+ const { searchQuery = '', selectedTokens } = this.state
+ const fuseSearchResult = fuse.search(searchQuery)
+ const addressSearchResult = contractList.filter(token => {
+ return token.address.toLowerCase() === searchQuery.toLowerCase()
+ })
+ const results = [...addressSearchResult, ...fuseSearchResult]
+
+ return h('div', [
+ results.length > 0 && h('div.add-token__token-icons-title', this.context.t('popularTokens')),
+ h('div.add-token__token-icons-container', 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--selected': selectedTokens[address],
+ 'add-token__token-wrapper--disabled': tokenAlreadyAdded,
+ }),
+ onClick: () => !tokenAlreadyAdded && this.toggleToken(address, results[i]),
+ }, [
+ h('div.add-token__token-icon', {
+ style: {
+ backgroundImage: logo && `url(images/contract/${logo})`,
+ },
+ }),
+ h('div.add-token__token-data', [
+ h('div.add-token__token-symbol', symbol),
+ h('div.add-token__token-name', name),
+ ]),
+ // tokenAlreadyAdded && (
+ // h('div.add-token__token-message', 'Already added')
+ // ),
+ ])
+ )
+ })),
+ ])
+}
+
+AddTokenScreen.prototype.renderConfirmation = function () {
+ const {
+ customAddress: address,
+ customSymbol: symbol,
+ customDecimals: decimals,
+ selectedTokens,
+ } = this.state
+
+ const { addTokens, history } = 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__content-container.add-token__confirmation-content', [
+ h('div.add-token__description.add-token__confirmation-description', this.context.t('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--lg.add-token__cancel-button', {
+ onClick: () => this.setState({ isShowingConfirmation: false }),
+ }, this.context.t('back')),
+ h('button.btn-primary--lg', {
+ onClick: () => addTokens(tokens).then(() => history.push(DEFAULT_ROUTE)),
+ }, this.context.t('addTokens')),
+ ]),
+ ])
+ )
+}
+
+AddTokenScreen.prototype.displayTab = function (selectedTab) {
+ this.setState({ displayedTab: selectedTab })
+}
+
+AddTokenScreen.prototype.renderTabs = function () {
+ const { isShowingInfoBox, displayedTab, errors } = this.state
+
+ return displayedTab === 'CUSTOM_TOKEN'
+ ? this.renderCustomForm()
+ : h('div', [
+ h('div.add-token__wrapper', [
+ h('div.add-token__content-container', [
+ isShowingInfoBox && h('div.add-token__info-box', [
+ h('div.add-token__info-box__close', {
+ onClick: () => this.setState({ isShowingInfoBox: false }),
+ }),
+ h('div.add-token__info-box__title', this.context.t('whatsThis')),
+ h('div.add-token__info-box__copy', this.context.t('keepTrackTokens')),
+ h('a.add-token__info-box__copy--blue', {
+ href: 'http://metamask.helpscoutdocs.com/article/16-managing-erc20-tokens',
+ target: '_blank',
+ }, this.context.t('learnMore')),
+ ]),
+ h('div.add-token__input-container', [
+ h('input.add-token__input', {
+ type: 'text',
+ placeholder: this.context.t('searchTokens'),
+ onChange: e => this.setState({ searchQuery: e.target.value }),
+ }),
+ h('div.add-token__search-input-error-message', errors.tokenSelector),
+ ]),
+ this.renderTokenList(),
+ ]),
+ ]),
+ ])
+}
+
+AddTokenScreen.prototype.render = function () {
+ const {
+ isShowingConfirmation,
+ displayedTab,
+ } = this.state
+ const { history } = this.props
+
+ return h('div.add-token', [
+ h('div.add-token__header', [
+ h('div.add-token__header__cancel', {
+ onClick: () => history.push(DEFAULT_ROUTE),
+ }, [
+ h('i.fa.fa-angle-left.fa-lg'),
+ h('span', this.context.t('cancel')),
+ ]),
+ h('div.add-token__header__title', this.context.t('addTokens')),
+ isShowingConfirmation && h('div.add-token__header__subtitle', this.context.t('likeToAddTokens')),
+ !isShowingConfirmation && h('div.add-token__header__tabs', [
+
+ h('div.add-token__header__tabs__tab', {
+ className: classnames('add-token__header__tabs__tab', {
+ 'add-token__header__tabs__selected': displayedTab === 'SEARCH',
+ 'add-token__header__tabs__unselected': displayedTab !== 'SEARCH',
+ }),
+ onClick: () => this.displayTab('SEARCH'),
+ }, this.context.t('search')),
+
+ h('div.add-token__header__tabs__tab', {
+ className: classnames('add-token__header__tabs__tab', {
+ 'add-token__header__tabs__selected': displayedTab === 'CUSTOM_TOKEN',
+ 'add-token__header__tabs__unselected': displayedTab !== 'CUSTOM_TOKEN',
+ }),
+ onClick: () => this.displayTab('CUSTOM_TOKEN'),
+ }, this.context.t('customToken')),
+
+ ]),
+ ]),
+
+ isShowingConfirmation
+ ? this.renderConfirmation()
+ : this.renderTabs(),
+
+ !isShowingConfirmation && h('div.add-token__buttons', [
+ h('button.btn-secondary--lg.add-token__cancel-button', {
+ onClick: () => history.push(DEFAULT_ROUTE),
+ }, this.context.t('cancel')),
+ h('button.btn-primary--lg.add-token__confirm-button', {
+ onClick: this.onNext,
+ }, this.context.t('next')),
+ ]),
+ ])
+}