Merge pull request #4308 from MetaMask/i4232-addtoken
Update designs for Add Token screenfeature/default_network_editable
commit
b5bbfd3264
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 5.5 KiB |
@ -1,2 +1,2 @@ |
|||||||
const Button = require('./button.component') |
import Button from './button.component' |
||||||
module.exports = Button |
module.exports = Button |
||||||
|
@ -0,0 +1,5 @@ |
|||||||
|
@import './export-text-container/index'; |
||||||
|
|
||||||
|
@import './info-box/index'; |
||||||
|
|
||||||
|
@import './pages/index'; |
@ -0,0 +1,2 @@ |
|||||||
|
import InfoBox from './info-box.component' |
||||||
|
module.exports = InfoBox |
@ -0,0 +1,24 @@ |
|||||||
|
.info-box { |
||||||
|
border-radius: 4px; |
||||||
|
background-color: $alabaster; |
||||||
|
position: relative; |
||||||
|
padding: 16px; |
||||||
|
display: flex; |
||||||
|
flex-flow: column; |
||||||
|
color: $mid-gray; |
||||||
|
|
||||||
|
&__close::after { |
||||||
|
content: '\00D7'; |
||||||
|
font-size: 29px; |
||||||
|
font-weight: 200; |
||||||
|
color: $dusty-gray; |
||||||
|
position: absolute; |
||||||
|
right: 12px; |
||||||
|
top: 0; |
||||||
|
cursor: pointer; |
||||||
|
} |
||||||
|
|
||||||
|
&__description { |
||||||
|
font-size: .75rem; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,49 @@ |
|||||||
|
import React, { Component } from 'react' |
||||||
|
import PropTypes from 'prop-types' |
||||||
|
|
||||||
|
export default class InfoBox extends Component { |
||||||
|
static contextTypes = { |
||||||
|
t: PropTypes.func, |
||||||
|
} |
||||||
|
|
||||||
|
static propTypes = { |
||||||
|
onClose: PropTypes.func, |
||||||
|
title: PropTypes.string, |
||||||
|
description: PropTypes.string, |
||||||
|
} |
||||||
|
|
||||||
|
constructor (props) { |
||||||
|
super(props) |
||||||
|
|
||||||
|
this.state = { |
||||||
|
isShowing: true, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
handleClose () { |
||||||
|
const { onClose } = this.props |
||||||
|
|
||||||
|
if (onClose) { |
||||||
|
onClose() |
||||||
|
} else { |
||||||
|
this.setState({ isShowing: false }) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
render () { |
||||||
|
const { title, description } = this.props |
||||||
|
|
||||||
|
return !this.state.isShowing |
||||||
|
? null |
||||||
|
: ( |
||||||
|
<div className="info-box"> |
||||||
|
<div |
||||||
|
className="info-box__close" |
||||||
|
onClick={() => this.handleClose()} |
||||||
|
/> |
||||||
|
<div className="info-box__title">{ title }</div> |
||||||
|
<div className="info-box__description">{ description }</div> |
||||||
|
</div> |
||||||
|
) |
||||||
|
} |
||||||
|
} |
@ -1,431 +0,0 @@ |
|||||||
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, |
|
||||||
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')), |
|
||||||
]), |
|
||||||
]) |
|
||||||
} |
|
@ -0,0 +1,351 @@ |
|||||||
|
import React, { Component } from 'react' |
||||||
|
import classnames from 'classnames' |
||||||
|
import PropTypes from 'prop-types' |
||||||
|
import ethUtil from 'ethereumjs-util' |
||||||
|
import { checkExistingAddresses } from './util' |
||||||
|
import { tokenInfoGetter } from '../../../token-util' |
||||||
|
import { DEFAULT_ROUTE, CONFIRM_ADD_TOKEN_ROUTE } from '../../../routes' |
||||||
|
import Button from '../../button' |
||||||
|
import TextField from '../../text-field' |
||||||
|
import TokenList from './token-list' |
||||||
|
import TokenSearch from './token-search' |
||||||
|
|
||||||
|
const emptyAddr = '0x0000000000000000000000000000000000000000' |
||||||
|
const SEARCH_TAB = 'SEARCH' |
||||||
|
const CUSTOM_TOKEN_TAB = 'CUSTOM_TOKEN' |
||||||
|
|
||||||
|
class AddToken extends Component { |
||||||
|
static contextTypes = { |
||||||
|
t: PropTypes.func, |
||||||
|
} |
||||||
|
|
||||||
|
static propTypes = { |
||||||
|
history: PropTypes.object, |
||||||
|
setPendingTokens: PropTypes.func, |
||||||
|
pendingTokens: PropTypes.object, |
||||||
|
clearPendingTokens: PropTypes.func, |
||||||
|
tokens: PropTypes.array, |
||||||
|
identities: PropTypes.object, |
||||||
|
} |
||||||
|
|
||||||
|
constructor (props) { |
||||||
|
super(props) |
||||||
|
|
||||||
|
this.state = { |
||||||
|
customAddress: '', |
||||||
|
customSymbol: '', |
||||||
|
customDecimals: 0, |
||||||
|
searchResults: [], |
||||||
|
selectedTokens: {}, |
||||||
|
tokenSelectorError: null, |
||||||
|
customAddressError: null, |
||||||
|
customSymbolError: null, |
||||||
|
customDecimalsError: null, |
||||||
|
autoFilled: false, |
||||||
|
displayedTab: SEARCH_TAB, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
componentDidMount () { |
||||||
|
this.tokenInfoGetter = tokenInfoGetter() |
||||||
|
const { pendingTokens = {} } = this.props |
||||||
|
const pendingTokenKeys = Object.keys(pendingTokens) |
||||||
|
|
||||||
|
if (pendingTokenKeys.length > 0) { |
||||||
|
let selectedTokens = {} |
||||||
|
let customToken = {} |
||||||
|
|
||||||
|
pendingTokenKeys.forEach(tokenAddress => { |
||||||
|
const token = pendingTokens[tokenAddress] |
||||||
|
const { isCustom } = token |
||||||
|
|
||||||
|
if (isCustom) { |
||||||
|
customToken = { ...token } |
||||||
|
} else { |
||||||
|
selectedTokens = { ...selectedTokens, [tokenAddress]: { ...token } } |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
const { |
||||||
|
address: customAddress = '', |
||||||
|
symbol: customSymbol = '', |
||||||
|
decimals: customDecimals = 0, |
||||||
|
} = customToken |
||||||
|
|
||||||
|
const displayedTab = Object.keys(selectedTokens).length > 0 ? SEARCH_TAB : CUSTOM_TOKEN_TAB |
||||||
|
this.setState({ selectedTokens, customAddress, customSymbol, customDecimals, displayedTab }) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
handleToggleToken (token) { |
||||||
|
const { address } = token |
||||||
|
const { selectedTokens = {} } = this.state |
||||||
|
const selectedTokensCopy = { ...selectedTokens } |
||||||
|
|
||||||
|
if (address in selectedTokensCopy) { |
||||||
|
delete selectedTokensCopy[address] |
||||||
|
} else { |
||||||
|
selectedTokensCopy[address] = token |
||||||
|
} |
||||||
|
|
||||||
|
this.setState({ |
||||||
|
selectedTokens: selectedTokensCopy, |
||||||
|
tokenSelectorError: null, |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
hasError () { |
||||||
|
const { |
||||||
|
tokenSelectorError, |
||||||
|
customAddressError, |
||||||
|
customSymbolError, |
||||||
|
customDecimalsError, |
||||||
|
} = this.state |
||||||
|
|
||||||
|
return tokenSelectorError || customAddressError || customSymbolError || customDecimalsError |
||||||
|
} |
||||||
|
|
||||||
|
hasSelected () { |
||||||
|
const { customAddress = '', selectedTokens = {} } = this.state |
||||||
|
return customAddress || Object.keys(selectedTokens).length > 0 |
||||||
|
} |
||||||
|
|
||||||
|
handleNext () { |
||||||
|
if (this.hasError()) { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
if (!this.hasSelected()) { |
||||||
|
this.setState({ tokenSelectorError: this.context.t('mustSelectOne') }) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
const { setPendingTokens, history } = this.props |
||||||
|
const { |
||||||
|
customAddress: address, |
||||||
|
customSymbol: symbol, |
||||||
|
customDecimals: decimals, |
||||||
|
selectedTokens, |
||||||
|
} = this.state |
||||||
|
|
||||||
|
const customToken = { |
||||||
|
address, |
||||||
|
symbol, |
||||||
|
decimals, |
||||||
|
} |
||||||
|
|
||||||
|
setPendingTokens({ customToken, selectedTokens }) |
||||||
|
history.push(CONFIRM_ADD_TOKEN_ROUTE) |
||||||
|
} |
||||||
|
|
||||||
|
async attemptToAutoFillTokenParams (address) { |
||||||
|
const { symbol = '', decimals = 0 } = await this.tokenInfoGetter(address) |
||||||
|
|
||||||
|
const autoFilled = Boolean(symbol && decimals) |
||||||
|
this.setState({ autoFilled }) |
||||||
|
this.handleCustomSymbolChange(symbol || '') |
||||||
|
this.handleCustomDecimalsChange(decimals) |
||||||
|
} |
||||||
|
|
||||||
|
handleCustomAddressChange (value) { |
||||||
|
const customAddress = value.trim() |
||||||
|
this.setState({ |
||||||
|
customAddress, |
||||||
|
customAddressError: null, |
||||||
|
tokenSelectorError: null, |
||||||
|
autoFilled: false, |
||||||
|
}) |
||||||
|
|
||||||
|
const isValidAddress = ethUtil.isValidAddress(customAddress) |
||||||
|
const standardAddress = ethUtil.addHexPrefix(customAddress).toLowerCase() |
||||||
|
|
||||||
|
switch (true) { |
||||||
|
case !isValidAddress: |
||||||
|
this.setState({ |
||||||
|
customAddressError: this.context.t('invalidAddress'), |
||||||
|
customSymbol: '', |
||||||
|
customDecimals: 0, |
||||||
|
customSymbolError: null, |
||||||
|
customDecimalsError: null, |
||||||
|
}) |
||||||
|
|
||||||
|
break |
||||||
|
case Boolean(this.props.identities[standardAddress]): |
||||||
|
this.setState({ |
||||||
|
customAddressError: this.context.t('personalAddressDetected'), |
||||||
|
}) |
||||||
|
|
||||||
|
break |
||||||
|
case checkExistingAddresses(customAddress, this.props.tokens): |
||||||
|
this.setState({ |
||||||
|
customAddressError: this.context.t('tokenAlreadyAdded'), |
||||||
|
}) |
||||||
|
|
||||||
|
break |
||||||
|
default: |
||||||
|
if (customAddress !== emptyAddr) { |
||||||
|
this.attemptToAutoFillTokenParams(customAddress) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
handleCustomSymbolChange (value) { |
||||||
|
const customSymbol = value.trim() |
||||||
|
const symbolLength = customSymbol.length |
||||||
|
let customSymbolError = null |
||||||
|
|
||||||
|
if (symbolLength <= 0 || symbolLength >= 10) { |
||||||
|
customSymbolError = this.context.t('symbolBetweenZeroTen') |
||||||
|
} |
||||||
|
|
||||||
|
this.setState({ customSymbol, customSymbolError }) |
||||||
|
} |
||||||
|
|
||||||
|
handleCustomDecimalsChange (value) { |
||||||
|
const customDecimals = value.trim() |
||||||
|
const validDecimals = customDecimals !== null && |
||||||
|
customDecimals !== '' && |
||||||
|
customDecimals >= 0 && |
||||||
|
customDecimals < 36 |
||||||
|
let customDecimalsError = null |
||||||
|
|
||||||
|
if (!validDecimals) { |
||||||
|
customDecimalsError = this.context.t('decimalsMustZerotoTen') |
||||||
|
} |
||||||
|
|
||||||
|
this.setState({ customDecimals, customDecimalsError }) |
||||||
|
} |
||||||
|
|
||||||
|
renderCustomTokenForm () { |
||||||
|
const { |
||||||
|
customAddress, |
||||||
|
customSymbol, |
||||||
|
customDecimals, |
||||||
|
customAddressError, |
||||||
|
customSymbolError, |
||||||
|
customDecimalsError, |
||||||
|
autoFilled, |
||||||
|
} = this.state |
||||||
|
|
||||||
|
return ( |
||||||
|
<div className="add-token__custom-token-form"> |
||||||
|
<TextField |
||||||
|
id="custom-address" |
||||||
|
label="Token Address" |
||||||
|
type="text" |
||||||
|
value={customAddress} |
||||||
|
onChange={e => this.handleCustomAddressChange(e.target.value)} |
||||||
|
error={customAddressError} |
||||||
|
fullWidth |
||||||
|
margin="normal" |
||||||
|
/> |
||||||
|
<TextField |
||||||
|
id="custom-symbol" |
||||||
|
label="Token Symbol" |
||||||
|
type="text" |
||||||
|
value={customSymbol} |
||||||
|
onChange={e => this.handleCustomSymbolChange(e.target.value)} |
||||||
|
error={customSymbolError} |
||||||
|
fullWidth |
||||||
|
margin="normal" |
||||||
|
disabled={autoFilled} |
||||||
|
/> |
||||||
|
<TextField |
||||||
|
id="custom-decimals" |
||||||
|
label="Decimals of Precision" |
||||||
|
type="number" |
||||||
|
value={customDecimals} |
||||||
|
onChange={e => this.handleCustomDecimalsChange(e.target.value)} |
||||||
|
error={customDecimalsError} |
||||||
|
fullWidth |
||||||
|
margin="normal" |
||||||
|
disabled={autoFilled} |
||||||
|
/> |
||||||
|
</div> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
renderSearchToken () { |
||||||
|
const { tokenSelectorError, selectedTokens, searchResults } = this.state |
||||||
|
|
||||||
|
return ( |
||||||
|
<div className="add-token__search-token"> |
||||||
|
<TokenSearch |
||||||
|
onSearch={({ results = [] }) => this.setState({ searchResults: results })} |
||||||
|
error={tokenSelectorError} |
||||||
|
/> |
||||||
|
<div className="add-token__token-list"> |
||||||
|
<TokenList |
||||||
|
results={searchResults} |
||||||
|
selectedTokens={selectedTokens} |
||||||
|
onToggleToken={token => this.handleToggleToken(token)} |
||||||
|
/> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
render () { |
||||||
|
const { displayedTab } = this.state |
||||||
|
const { history, clearPendingTokens } = this.props |
||||||
|
|
||||||
|
return ( |
||||||
|
<div className="page-container"> |
||||||
|
<div className="page-container__header page-container__header--no-padding-bottom"> |
||||||
|
<div className="page-container__title"> |
||||||
|
{ this.context.t('addTokens') } |
||||||
|
</div> |
||||||
|
<div className="page-container__tabs"> |
||||||
|
<div |
||||||
|
className={classnames('page-container__tab', { |
||||||
|
'page-container__tab--selected': displayedTab === SEARCH_TAB, |
||||||
|
})} |
||||||
|
onClick={() => this.setState({ displayedTab: SEARCH_TAB })} |
||||||
|
> |
||||||
|
{ this.context.t('search') } |
||||||
|
</div> |
||||||
|
<div |
||||||
|
className={classnames('page-container__tab', { |
||||||
|
'page-container__tab--selected': displayedTab === CUSTOM_TOKEN_TAB, |
||||||
|
})} |
||||||
|
onClick={() => this.setState({ displayedTab: CUSTOM_TOKEN_TAB })} |
||||||
|
> |
||||||
|
{ this.context.t('customToken') } |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div className="page-container__content"> |
||||||
|
{ |
||||||
|
displayedTab === CUSTOM_TOKEN_TAB |
||||||
|
? this.renderCustomTokenForm() |
||||||
|
: this.renderSearchToken() |
||||||
|
} |
||||||
|
</div> |
||||||
|
<div className="page-container__footer"> |
||||||
|
<Button |
||||||
|
type="secondary" |
||||||
|
large |
||||||
|
className="page-container__footer-button" |
||||||
|
onClick={() => { |
||||||
|
clearPendingTokens() |
||||||
|
history.push(DEFAULT_ROUTE) |
||||||
|
}} |
||||||
|
> |
||||||
|
{ this.context.t('cancel') } |
||||||
|
</Button> |
||||||
|
<Button |
||||||
|
type="primary" |
||||||
|
large |
||||||
|
className="page-container__footer-button" |
||||||
|
onClick={() => this.handleNext()} |
||||||
|
disabled={this.hasError() || !this.hasSelected()} |
||||||
|
> |
||||||
|
{ this.context.t('next') } |
||||||
|
</Button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export default AddToken |
@ -0,0 +1,22 @@ |
|||||||
|
import { connect } from 'react-redux' |
||||||
|
import AddToken from './add-token.component' |
||||||
|
|
||||||
|
const { setPendingTokens, clearPendingTokens } = require('../../../actions') |
||||||
|
|
||||||
|
const mapStateToProps = ({ metamask }) => { |
||||||
|
const { identities, tokens, pendingTokens } = metamask |
||||||
|
return { |
||||||
|
identities, |
||||||
|
tokens, |
||||||
|
pendingTokens, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const mapDispatchToProps = dispatch => { |
||||||
|
return { |
||||||
|
setPendingTokens: tokens => dispatch(setPendingTokens(tokens)), |
||||||
|
clearPendingTokens: () => dispatch(clearPendingTokens()), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(AddToken) |
@ -0,0 +1,2 @@ |
|||||||
|
import AddToken from './add-token.container' |
||||||
|
module.exports = AddToken |
@ -0,0 +1,25 @@ |
|||||||
|
@import './token-list/index'; |
||||||
|
|
||||||
|
.add-token { |
||||||
|
&__custom-token-form { |
||||||
|
padding: 8px 16px 16px; |
||||||
|
|
||||||
|
input[type="number"]::-webkit-inner-spin-button { |
||||||
|
-webkit-appearance: none; |
||||||
|
display: none; |
||||||
|
} |
||||||
|
|
||||||
|
input[type="number"]:hover::-webkit-inner-spin-button { |
||||||
|
-webkit-appearance: none; |
||||||
|
display: none; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
&__search-token { |
||||||
|
padding: 16px; |
||||||
|
} |
||||||
|
|
||||||
|
&__token-list { |
||||||
|
margin-top: 16px; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,2 @@ |
|||||||
|
import TokenList from './token-list.container' |
||||||
|
module.exports = TokenList |
@ -0,0 +1,65 @@ |
|||||||
|
@import './token-list-placeholder/index'; |
||||||
|
|
||||||
|
.token-list { |
||||||
|
&__title { |
||||||
|
font-size: .75rem; |
||||||
|
} |
||||||
|
|
||||||
|
&__tokens-container { |
||||||
|
display: flex; |
||||||
|
flex-direction: column; |
||||||
|
} |
||||||
|
|
||||||
|
&__token { |
||||||
|
transition: 200ms ease-in-out; |
||||||
|
display: flex; |
||||||
|
flex-flow: row nowrap; |
||||||
|
align-items: center; |
||||||
|
padding: 8px; |
||||||
|
margin-top: 8px; |
||||||
|
box-sizing: border-box; |
||||||
|
border-radius: 10px; |
||||||
|
cursor: pointer; |
||||||
|
border: 2px solid transparent; |
||||||
|
position: relative; |
||||||
|
|
||||||
|
&:hover { |
||||||
|
border: 2px solid rgba($malibu-blue, .5); |
||||||
|
} |
||||||
|
|
||||||
|
&--selected { |
||||||
|
border: 2px solid $malibu-blue !important; |
||||||
|
} |
||||||
|
|
||||||
|
&--disabled { |
||||||
|
opacity: .4; |
||||||
|
pointer-events: none; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
&__token-icon { |
||||||
|
width: 48px; |
||||||
|
height: 48px; |
||||||
|
background-repeat: no-repeat; |
||||||
|
background-size: contain; |
||||||
|
background-position: center; |
||||||
|
border-radius: 50%; |
||||||
|
background-color: $white; |
||||||
|
box-shadow: 0 2px 4px 0 rgba($black, .24); |
||||||
|
margin-right: 12px; |
||||||
|
flex: 0 0 auto; |
||||||
|
} |
||||||
|
|
||||||
|
&__token-data { |
||||||
|
display: flex; |
||||||
|
flex-direction: row; |
||||||
|
align-items: center; |
||||||
|
min-width: 0; |
||||||
|
} |
||||||
|
|
||||||
|
&__token-name { |
||||||
|
overflow: hidden; |
||||||
|
text-overflow: ellipsis; |
||||||
|
white-space: nowrap; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,2 @@ |
|||||||
|
import TokenListPlaceholder from './token-list-placeholder.component' |
||||||
|
module.exports = TokenListPlaceholder |
@ -0,0 +1,19 @@ |
|||||||
|
.token-list-placeholder { |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
padding-top: 36px; |
||||||
|
flex-direction: column; |
||||||
|
line-height: 22px; |
||||||
|
opacity: .5; |
||||||
|
|
||||||
|
&__text { |
||||||
|
color: $silver-chalice; |
||||||
|
width: 50%; |
||||||
|
text-align: center; |
||||||
|
margin-top: 8px; |
||||||
|
} |
||||||
|
|
||||||
|
&__link { |
||||||
|
color: $curious-blue; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,27 @@ |
|||||||
|
import React, { Component } from 'react' |
||||||
|
import PropTypes from 'prop-types' |
||||||
|
|
||||||
|
export default class TokenListPlaceholder extends Component { |
||||||
|
static contextTypes = { |
||||||
|
t: PropTypes.func, |
||||||
|
} |
||||||
|
|
||||||
|
render () { |
||||||
|
return ( |
||||||
|
<div className="token-list-placeholder"> |
||||||
|
<img src="images/tokensearch.svg" /> |
||||||
|
<div className="token-list-placeholder__text"> |
||||||
|
{ this.context.t('addAcquiredTokens') } |
||||||
|
</div> |
||||||
|
<a |
||||||
|
className="token-list-placeholder__link" |
||||||
|
href="http://metamask.helpscoutdocs.com/article/16-managing-erc20-tokens" |
||||||
|
target="_blank" |
||||||
|
rel="noopener noreferrer" |
||||||
|
> |
||||||
|
{ this.context.t('learnMore') } |
||||||
|
</a> |
||||||
|
</div> |
||||||
|
) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,60 @@ |
|||||||
|
import React, { Component } from 'react' |
||||||
|
import PropTypes from 'prop-types' |
||||||
|
import classnames from 'classnames' |
||||||
|
import { checkExistingAddresses } from '../util' |
||||||
|
import TokenListPlaceholder from './token-list-placeholder' |
||||||
|
|
||||||
|
export default class InfoBox extends Component { |
||||||
|
static contextTypes = { |
||||||
|
t: PropTypes.func, |
||||||
|
} |
||||||
|
|
||||||
|
static propTypes = { |
||||||
|
tokens: PropTypes.array, |
||||||
|
results: PropTypes.array, |
||||||
|
selectedTokens: PropTypes.object, |
||||||
|
onToggleToken: PropTypes.func, |
||||||
|
} |
||||||
|
|
||||||
|
render () { |
||||||
|
const { results = [], selectedTokens = {}, onToggleToken, tokens = [] } = this.props |
||||||
|
|
||||||
|
return results.length === 0 |
||||||
|
? <TokenListPlaceholder /> |
||||||
|
: ( |
||||||
|
<div className="token-list"> |
||||||
|
<div className="token-list__title"> |
||||||
|
{ this.context.t('searchResults') } |
||||||
|
</div> |
||||||
|
<div className="token-list__tokens-container"> |
||||||
|
{ |
||||||
|
Array(6).fill(undefined) |
||||||
|
.map((_, i) => { |
||||||
|
const { logo, symbol, name, address } = results[i] || {} |
||||||
|
const tokenAlreadyAdded = checkExistingAddresses(address, tokens) |
||||||
|
|
||||||
|
return Boolean(logo || symbol || name) && ( |
||||||
|
<div |
||||||
|
className={classnames('token-list__token', { |
||||||
|
'token-list__token--selected': selectedTokens[address], |
||||||
|
'token-list__token--disabled': tokenAlreadyAdded, |
||||||
|
})} |
||||||
|
onClick={() => !tokenAlreadyAdded && onToggleToken(results[i])} |
||||||
|
key={i} |
||||||
|
> |
||||||
|
<div |
||||||
|
className="token-list__token-icon" |
||||||
|
style={{ backgroundImage: logo && `url(images/contract/${logo})` }}> |
||||||
|
</div> |
||||||
|
<div className="token-list__token-data"> |
||||||
|
<span className="token-list__token-name">{ `${name} (${symbol})` }</span> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
) |
||||||
|
}) |
||||||
|
} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,11 @@ |
|||||||
|
import { connect } from 'react-redux' |
||||||
|
import TokenList from './token-list.component' |
||||||
|
|
||||||
|
const mapStateToProps = ({ metamask }) => { |
||||||
|
const { tokens } = metamask |
||||||
|
return { |
||||||
|
tokens, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export default connect(mapStateToProps)(TokenList) |
@ -0,0 +1,2 @@ |
|||||||
|
import TokenSearch from './token-search.component' |
||||||
|
module.exports = TokenSearch |
@ -0,0 +1,85 @@ |
|||||||
|
import React, { Component } from 'react' |
||||||
|
import PropTypes from 'prop-types' |
||||||
|
import contractMap from 'eth-contract-metadata' |
||||||
|
import Fuse from 'fuse.js' |
||||||
|
import InputAdornment from '@material-ui/core/InputAdornment' |
||||||
|
import TextField from '../../../text-field' |
||||||
|
|
||||||
|
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 }, |
||||||
|
], |
||||||
|
}) |
||||||
|
|
||||||
|
export default class TokenSearch extends Component { |
||||||
|
static contextTypes = { |
||||||
|
t: PropTypes.func, |
||||||
|
} |
||||||
|
|
||||||
|
static defaultProps = { |
||||||
|
error: null, |
||||||
|
} |
||||||
|
|
||||||
|
static propTypes = { |
||||||
|
onSearch: PropTypes.func, |
||||||
|
error: PropTypes.string, |
||||||
|
} |
||||||
|
|
||||||
|
constructor (props) { |
||||||
|
super(props) |
||||||
|
|
||||||
|
this.state = { |
||||||
|
searchQuery: '', |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
handleSearch (searchQuery) { |
||||||
|
this.setState({ searchQuery }) |
||||||
|
const fuseSearchResult = fuse.search(searchQuery) |
||||||
|
const addressSearchResult = contractList.filter(token => { |
||||||
|
return token.address.toLowerCase() === searchQuery.toLowerCase() |
||||||
|
}) |
||||||
|
const results = [...addressSearchResult, ...fuseSearchResult] |
||||||
|
this.props.onSearch({ searchQuery, results }) |
||||||
|
} |
||||||
|
|
||||||
|
renderAdornment () { |
||||||
|
return ( |
||||||
|
<InputAdornment |
||||||
|
position="start" |
||||||
|
style={{ marginRight: '12px' }} |
||||||
|
> |
||||||
|
<img src="images/search.svg" /> |
||||||
|
</InputAdornment> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
render () { |
||||||
|
const { error } = this.props |
||||||
|
const { searchQuery } = this.state |
||||||
|
|
||||||
|
return ( |
||||||
|
<TextField |
||||||
|
id="search-tokens" |
||||||
|
placeholder={this.context.t('searchTokens')} |
||||||
|
type="text" |
||||||
|
value={searchQuery} |
||||||
|
onChange={e => this.handleSearch(e.target.value)} |
||||||
|
error={error} |
||||||
|
fullWidth |
||||||
|
startAdornment={this.renderAdornment()} |
||||||
|
/> |
||||||
|
) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,13 @@ |
|||||||
|
import R from 'ramda' |
||||||
|
|
||||||
|
export function checkExistingAddresses (address, tokenList = []) { |
||||||
|
if (!address) { |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
const matchesAddress = existingToken => { |
||||||
|
return existingToken.address.toLowerCase() === address.toLowerCase() |
||||||
|
} |
||||||
|
|
||||||
|
return R.any(matchesAddress)(tokenList) |
||||||
|
} |
@ -0,0 +1,115 @@ |
|||||||
|
import React, { Component } from 'react' |
||||||
|
import PropTypes from 'prop-types' |
||||||
|
import { DEFAULT_ROUTE, ADD_TOKEN_ROUTE } from '../../../routes' |
||||||
|
import Button from '../../button' |
||||||
|
import Identicon from '../../../components/identicon' |
||||||
|
import TokenBalance from './token-balance' |
||||||
|
|
||||||
|
export default class ConfirmAddToken extends Component { |
||||||
|
static contextTypes = { |
||||||
|
t: PropTypes.func, |
||||||
|
} |
||||||
|
|
||||||
|
static propTypes = { |
||||||
|
history: PropTypes.object, |
||||||
|
clearPendingTokens: PropTypes.func, |
||||||
|
addTokens: PropTypes.func, |
||||||
|
pendingTokens: PropTypes.object, |
||||||
|
} |
||||||
|
|
||||||
|
componentDidMount () { |
||||||
|
const { pendingTokens = {}, history } = this.props |
||||||
|
|
||||||
|
if (Object.keys(pendingTokens).length === 0) { |
||||||
|
history.push(DEFAULT_ROUTE) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
getTokenName (name, symbol) { |
||||||
|
return typeof name === 'undefined' |
||||||
|
? symbol |
||||||
|
: `${name} (${symbol})` |
||||||
|
} |
||||||
|
|
||||||
|
render () { |
||||||
|
const { history, addTokens, clearPendingTokens, pendingTokens } = this.props |
||||||
|
|
||||||
|
return ( |
||||||
|
<div className="page-container"> |
||||||
|
<div className="page-container__header"> |
||||||
|
<div className="page-container__title"> |
||||||
|
{ this.context.t('addTokens') } |
||||||
|
</div> |
||||||
|
<div className="page-container__subtitle"> |
||||||
|
{ this.context.t('likeToAddTokens') } |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div className="page-container__content"> |
||||||
|
<div className="confirm-add-token"> |
||||||
|
<div className="confirm-add-token__header"> |
||||||
|
<div className="confirm-add-token__token"> |
||||||
|
{ this.context.t('token') } |
||||||
|
</div> |
||||||
|
<div className="confirm-add-token__balance"> |
||||||
|
{ this.context.t('balance') } |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div className="confirm-add-token__token-list"> |
||||||
|
{ |
||||||
|
Object.entries(pendingTokens) |
||||||
|
.map(([ address, token ]) => { |
||||||
|
const { name, symbol } = token |
||||||
|
|
||||||
|
return ( |
||||||
|
<div |
||||||
|
className="confirm-add-token__token-list-item" |
||||||
|
key={address} |
||||||
|
> |
||||||
|
<div className="confirm-add-token__token confirm-add-token__data"> |
||||||
|
<Identicon |
||||||
|
className="confirm-add-token__token-icon" |
||||||
|
diameter={48} |
||||||
|
address={address} |
||||||
|
/> |
||||||
|
<div className="confirm-add-token__name"> |
||||||
|
{ this.getTokenName(name, symbol) } |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div className="confirm-add-token__balance"> |
||||||
|
<TokenBalance token={token} /> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
) |
||||||
|
}) |
||||||
|
} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div className="page-container__footer"> |
||||||
|
<Button |
||||||
|
type="secondary" |
||||||
|
large |
||||||
|
className="page-container__footer-button" |
||||||
|
onClick={() => history.push(ADD_TOKEN_ROUTE)} |
||||||
|
> |
||||||
|
{ this.context.t('back') } |
||||||
|
</Button> |
||||||
|
<Button |
||||||
|
type="primary" |
||||||
|
large |
||||||
|
className="page-container__footer-button" |
||||||
|
onClick={() => { |
||||||
|
addTokens(pendingTokens) |
||||||
|
.then(() => { |
||||||
|
clearPendingTokens() |
||||||
|
history.push(DEFAULT_ROUTE) |
||||||
|
}) |
||||||
|
}} |
||||||
|
> |
||||||
|
{ this.context.t('addTokens') } |
||||||
|
</Button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,20 @@ |
|||||||
|
import { connect } from 'react-redux' |
||||||
|
import ConfirmAddToken from './confirm-add-token.component' |
||||||
|
|
||||||
|
const { addTokens, clearPendingTokens } = require('../../../actions') |
||||||
|
|
||||||
|
const mapStateToProps = ({ metamask }) => { |
||||||
|
const { pendingTokens } = metamask |
||||||
|
return { |
||||||
|
pendingTokens, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const mapDispatchToProps = dispatch => { |
||||||
|
return { |
||||||
|
addTokens: tokens => dispatch(addTokens(tokens)), |
||||||
|
clearPendingTokens: () => dispatch(clearPendingTokens()), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(ConfirmAddToken) |
@ -0,0 +1,2 @@ |
|||||||
|
import ConfirmAddToken from './confirm-add-token.container' |
||||||
|
module.exports = ConfirmAddToken |
@ -0,0 +1,69 @@ |
|||||||
|
.confirm-add-token { |
||||||
|
padding: 16px; |
||||||
|
|
||||||
|
&__header { |
||||||
|
font-size: .75rem; |
||||||
|
display: flex; |
||||||
|
} |
||||||
|
|
||||||
|
&__token { |
||||||
|
flex: 1; |
||||||
|
min-width: 0; |
||||||
|
} |
||||||
|
|
||||||
|
&__balance { |
||||||
|
flex: 0 0 30%; |
||||||
|
min-width: 0; |
||||||
|
} |
||||||
|
|
||||||
|
&__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; |
||||||
|
line-height: 43px; |
||||||
|
margin-right: 8px; |
||||||
|
} |
||||||
|
|
||||||
|
&__symbol { |
||||||
|
color: $scorpion; |
||||||
|
font-size: 16px; |
||||||
|
font-weight: 400; |
||||||
|
line-height: 24px; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
&__token-list-item { |
||||||
|
display: flex; |
||||||
|
flex-flow: row nowrap; |
||||||
|
align-items: center; |
||||||
|
margin-top: 8px; |
||||||
|
box-sizing: border-box; |
||||||
|
} |
||||||
|
|
||||||
|
&__data { |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
padding: 8px; |
||||||
|
} |
||||||
|
|
||||||
|
&__name { |
||||||
|
min-width: 0; |
||||||
|
white-space: nowrap; |
||||||
|
overflow: hidden; |
||||||
|
text-overflow: ellipsis; |
||||||
|
} |
||||||
|
|
||||||
|
&__token-icon { |
||||||
|
margin-right: 12px; |
||||||
|
flex: 0 0 auto; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,2 @@ |
|||||||
|
import TokenBalance from './token-balance.container' |
||||||
|
module.exports = TokenBalance |
@ -0,0 +1,16 @@ |
|||||||
|
import React, { Component } from 'react' |
||||||
|
import PropTypes from 'prop-types' |
||||||
|
|
||||||
|
export default class TokenBalance extends Component { |
||||||
|
static propTypes = { |
||||||
|
string: PropTypes.string, |
||||||
|
symbol: PropTypes.string, |
||||||
|
error: PropTypes.string, |
||||||
|
} |
||||||
|
|
||||||
|
render () { |
||||||
|
return ( |
||||||
|
<div className="hide-text-overflow">{ this.props.string }</div> |
||||||
|
) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,16 @@ |
|||||||
|
import { connect } from 'react-redux' |
||||||
|
import { compose } from 'recompose' |
||||||
|
import withTokenTracker from '../../../../helpers/with-token-tracker' |
||||||
|
import TokenBalance from './token-balance.component' |
||||||
|
import selectors from '../../../../selectors' |
||||||
|
|
||||||
|
const mapStateToProps = state => { |
||||||
|
return { |
||||||
|
userAddress: selectors.getSelectedAddress(state), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export default compose( |
||||||
|
connect(mapStateToProps), |
||||||
|
withTokenTracker |
||||||
|
)(TokenBalance) |
@ -0,0 +1,5 @@ |
|||||||
|
@import './unlock-page/index'; |
||||||
|
|
||||||
|
@import './add-token/index'; |
||||||
|
|
||||||
|
@import './confirm-add-token/index'; |
@ -1,59 +1,102 @@ |
|||||||
import React from 'react' |
import React, { Component } from 'react' |
||||||
import PropTypes from 'prop-types' |
import PropTypes from 'prop-types' |
||||||
import { withStyles } from 'material-ui/styles' |
import { withStyles } from '@material-ui/core/styles' |
||||||
import { default as MaterialTextField } from 'material-ui/TextField' |
import { default as MaterialTextField } from '@material-ui/core/TextField' |
||||||
|
|
||||||
const styles = { |
const styles = { |
||||||
cssLabel: { |
materialLabel: { |
||||||
'&$cssFocused': { |
'&$materialFocused': { |
||||||
color: '#aeaeae', |
color: '#aeaeae', |
||||||
}, |
}, |
||||||
'&$cssError': { |
'&$materialError': { |
||||||
color: '#aeaeae', |
color: '#aeaeae', |
||||||
}, |
}, |
||||||
fontWeight: '400', |
fontWeight: '400', |
||||||
color: '#aeaeae', |
color: '#aeaeae', |
||||||
}, |
}, |
||||||
cssFocused: {}, |
materialFocused: {}, |
||||||
cssUnderline: { |
materialUnderline: { |
||||||
'&:after': { |
'&:after': { |
||||||
backgroundColor: '#f7861c', |
borderBottom: '2px solid #f7861c', |
||||||
}, |
}, |
||||||
}, |
}, |
||||||
cssError: {}, |
materialError: {}, |
||||||
|
// Non-material styles
|
||||||
|
formLabel: { |
||||||
|
'&$formLabelFocused': { |
||||||
|
color: '#5b5b5b', |
||||||
|
}, |
||||||
|
'&$materialError': { |
||||||
|
color: '#5b5b5b', |
||||||
|
}, |
||||||
|
}, |
||||||
|
formLabelFocused: {}, |
||||||
|
inputFocused: {}, |
||||||
|
inputRoot: { |
||||||
|
'label + &': { |
||||||
|
marginTop: '8px', |
||||||
|
}, |
||||||
|
border: '1px solid #d2d8dd', |
||||||
|
height: '48px', |
||||||
|
borderRadius: '4px', |
||||||
|
padding: '0 16px', |
||||||
|
display: 'flex', |
||||||
|
alignItems: 'center', |
||||||
|
'&$inputFocused': { |
||||||
|
border: '1px solid #2f9ae0', |
||||||
|
}, |
||||||
|
}, |
||||||
|
inputLabel: { |
||||||
|
fontSize: '.75rem', |
||||||
|
transform: 'none', |
||||||
|
transition: 'none', |
||||||
|
position: 'initial', |
||||||
|
color: '#5b5b5b', |
||||||
|
}, |
||||||
|
} |
||||||
|
|
||||||
|
class TextField extends Component { |
||||||
|
static defaultProps = { |
||||||
|
error: null, |
||||||
} |
} |
||||||
|
|
||||||
const TextField = props => { |
static propTypes = { |
||||||
const { error, classes, ...textFieldProps } = props |
error: PropTypes.string, |
||||||
|
classes: PropTypes.object, |
||||||
|
material: PropTypes.bool, |
||||||
|
startAdornment: PropTypes.element, |
||||||
|
} |
||||||
|
|
||||||
|
render () { |
||||||
|
const { error, classes, material, startAdornment, ...textFieldProps } = this.props |
||||||
|
|
||||||
return ( |
return ( |
||||||
<MaterialTextField |
<MaterialTextField |
||||||
error={Boolean(error)} |
error={Boolean(error)} |
||||||
helperText={error} |
helperText={error} |
||||||
InputLabelProps={{ |
InputLabelProps={{ |
||||||
|
shrink: material ? undefined : true, |
||||||
|
className: material ? '' : classes.inputLabel, |
||||||
FormLabelClasses: { |
FormLabelClasses: { |
||||||
root: classes.cssLabel, |
root: material ? classes.materialLabel : classes.formLabel, |
||||||
focused: classes.cssFocused, |
focused: material ? classes.materialFocused : classes.formLabelFocused, |
||||||
error: classes.cssError, |
error: classes.materialError, |
||||||
}, |
}, |
||||||
}} |
}} |
||||||
InputProps={{ |
InputProps={{ |
||||||
|
startAdornment: startAdornment || undefined, |
||||||
|
disableUnderline: !material, |
||||||
classes: { |
classes: { |
||||||
underline: classes.cssUnderline, |
root: material ? '' : classes.inputRoot, |
||||||
|
input: material ? '' : classes.input, |
||||||
|
underline: material ? classes.materialUnderline : '', |
||||||
|
focused: material ? '' : classes.inputFocused, |
||||||
}, |
}, |
||||||
}} |
}} |
||||||
{...textFieldProps} |
{...textFieldProps} |
||||||
/> |
/> |
||||||
) |
) |
||||||
} |
} |
||||||
|
|
||||||
TextField.defaultProps = { |
|
||||||
error: null, |
|
||||||
} |
|
||||||
|
|
||||||
TextField.propTypes = { |
|
||||||
error: PropTypes.string, |
|
||||||
classes: PropTypes.object, |
|
||||||
} |
} |
||||||
|
|
||||||
export default withStyles(styles)(TextField) |
export default withStyles(styles)(TextField) |
||||||
|
@ -1,461 +0,0 @@ |
|||||||
.add-token { |
|
||||||
width: 498px; |
|
||||||
max-height: 805px; |
|
||||||
display: flex; |
|
||||||
flex-flow: column nowrap; |
|
||||||
position: relative; |
|
||||||
z-index: 12; |
|
||||||
font-family: 'Roboto'; |
|
||||||
background: white; |
|
||||||
border-radius: 8px; |
|
||||||
box-shadow: 0 0 7px 0 rgba(0, 0, 0, 0.08); |
|
||||||
|
|
||||||
&__wrapper { |
|
||||||
background-color: $white; |
|
||||||
display: flex; |
|
||||||
flex-flow: column nowrap; |
|
||||||
align-items: center; |
|
||||||
flex: 0 0 auto; |
|
||||||
} |
|
||||||
|
|
||||||
&__header { |
|
||||||
display: flex; |
|
||||||
flex-flow: column nowrap; |
|
||||||
padding: 20px 20px 0px; |
|
||||||
border-bottom: 1px solid $geyser; |
|
||||||
flex: 0 0 auto; |
|
||||||
|
|
||||||
&__cancel { |
|
||||||
color: $dodger-blue; |
|
||||||
display: flex; |
|
||||||
align-items: center; |
|
||||||
|
|
||||||
span { |
|
||||||
font-family: Roboto; |
|
||||||
font-size: 16px; |
|
||||||
font-weight: 400; |
|
||||||
line-height: 21px; |
|
||||||
margin-left: 8px; |
|
||||||
cursor:pointer; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
&__title { |
|
||||||
color: $tundora; |
|
||||||
font-size: 32px; |
|
||||||
font-weight: 500; |
|
||||||
margin-top: 4px; |
|
||||||
} |
|
||||||
|
|
||||||
&__subtitle { |
|
||||||
font-weight: 400; |
|
||||||
margin-top: 15px; |
|
||||||
margin-bottom: 21px; |
|
||||||
} |
|
||||||
|
|
||||||
&__tabs { |
|
||||||
display: flex; |
|
||||||
|
|
||||||
&__tab { |
|
||||||
height: 54px; |
|
||||||
padding: 15px 10px; |
|
||||||
color: $dusty-gray; |
|
||||||
font-family: Roboto; |
|
||||||
font-size: 18px; |
|
||||||
font-weight: 400; |
|
||||||
line-height: 24px; |
|
||||||
text-align: center; |
|
||||||
} |
|
||||||
|
|
||||||
&__tab:first-of-type { |
|
||||||
margin-right: 20px; |
|
||||||
} |
|
||||||
|
|
||||||
&__unselected:hover { |
|
||||||
color: $black; |
|
||||||
border-bottom: none; |
|
||||||
cursor: pointer; |
|
||||||
} |
|
||||||
|
|
||||||
&__selected { |
|
||||||
color: $curious-blue; |
|
||||||
border-bottom: 3px solid $curious-blue; |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
&__info-box { |
|
||||||
height: 96px; |
|
||||||
margin: 20px 20px 0px; |
|
||||||
border-radius: 4px; |
|
||||||
background-color: $alabaster; |
|
||||||
position: relative; |
|
||||||
padding-left: 18px; |
|
||||||
display: flex; |
|
||||||
flex-flow: column; |
|
||||||
|
|
||||||
&__close::after { |
|
||||||
content: '\00D7'; |
|
||||||
font-size: 29px; |
|
||||||
font-weight: 200; |
|
||||||
color: $dusty-gray; |
|
||||||
position: absolute; |
|
||||||
right: 17px; |
|
||||||
cursor: pointer; |
|
||||||
} |
|
||||||
|
|
||||||
&__title { |
|
||||||
color: $mid-gray; |
|
||||||
font-family: Roboto; |
|
||||||
font-size: 14px; |
|
||||||
font-weight: 400; |
|
||||||
margin-top: 15px; |
|
||||||
margin-bottom: 9px; |
|
||||||
} |
|
||||||
|
|
||||||
&__copy, |
|
||||||
&__copy--blue { |
|
||||||
color: $mid-gray; |
|
||||||
font-family: Roboto; |
|
||||||
font-size: 12px; |
|
||||||
font-weight: 400; |
|
||||||
line-height: 18px; |
|
||||||
} |
|
||||||
|
|
||||||
&__copy--blue { |
|
||||||
color: $curious-blue; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
&__description { |
|
||||||
text-align: center; |
|
||||||
} |
|
||||||
|
|
||||||
&__description + &__description { |
|
||||||
margin-top: 24px; |
|
||||||
} |
|
||||||
|
|
||||||
&__confirmation-description { |
|
||||||
font-weight: 400; |
|
||||||
margin: 20px 0 40px 0; |
|
||||||
} |
|
||||||
|
|
||||||
&__content-container { |
|
||||||
width: 100%; |
|
||||||
} |
|
||||||
|
|
||||||
&__input-container { |
|
||||||
display: flex; |
|
||||||
position: relative; |
|
||||||
} |
|
||||||
|
|
||||||
&__search-input-error-message { |
|
||||||
position: absolute; |
|
||||||
bottom: -10px; |
|
||||||
left: 22px; |
|
||||||
font-size: 12px; |
|
||||||
width: 100%; |
|
||||||
text-overflow: ellipsis; |
|
||||||
overflow: hidden; |
|
||||||
white-space: nowrap; |
|
||||||
color: $red; |
|
||||||
} |
|
||||||
|
|
||||||
&__input, |
|
||||||
&__add-custom-input { |
|
||||||
height: 54px; |
|
||||||
padding: 0px 20px; |
|
||||||
border: 1px solid $geyser; |
|
||||||
border-radius: 4px; |
|
||||||
margin: 22px 24px; |
|
||||||
position: relative; |
|
||||||
flex: 1 0 auto; |
|
||||||
color: $scorpion; |
|
||||||
font-family: Roboto; |
|
||||||
font-size: 16px; |
|
||||||
|
|
||||||
&::placeholder { |
|
||||||
color: $scorpion; |
|
||||||
font-family: Roboto; |
|
||||||
font-size: 16px; |
|
||||||
line-height: 21px; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
&__footers { |
|
||||||
width: 100%; |
|
||||||
} |
|
||||||
|
|
||||||
&__add-custom { |
|
||||||
color: $scorpion; |
|
||||||
font-size: 18px; |
|
||||||
line-height: 24px; |
|
||||||
text-align: center; |
|
||||||
padding: 12px 0; |
|
||||||
font-weight: 600; |
|
||||||
cursor: pointer; |
|
||||||
position: relative; |
|
||||||
|
|
||||||
&:hover { |
|
||||||
background-color: rgba(0, 0, 0, .05); |
|
||||||
} |
|
||||||
|
|
||||||
&:active { |
|
||||||
background-color: rgba(0, 0, 0, .1); |
|
||||||
} |
|
||||||
|
|
||||||
.fa { |
|
||||||
position: absolute; |
|
||||||
right: 24px; |
|
||||||
font-size: 24px; |
|
||||||
line-height: 24px; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
&__add-custom-form { |
|
||||||
display: flex; |
|
||||||
flex-flow: column nowrap; |
|
||||||
margin: 40px 0 30px; |
|
||||||
} |
|
||||||
|
|
||||||
&__add-custom-field { |
|
||||||
position: relative; |
|
||||||
display: flex; |
|
||||||
flex-flow: column; |
|
||||||
flex: 1 0 auto; |
|
||||||
|
|
||||||
&--error { |
|
||||||
.add-token__add-custom-input { |
|
||||||
border-color: $red; |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
&__add-custom-error-message { |
|
||||||
position: absolute; |
|
||||||
bottom: 1px; |
|
||||||
left: 22px; |
|
||||||
font-size: 12px; |
|
||||||
width: 100%; |
|
||||||
text-overflow: ellipsis; |
|
||||||
overflow: hidden; |
|
||||||
white-space: nowrap; |
|
||||||
color: $red; |
|
||||||
} |
|
||||||
|
|
||||||
&__add-custom-label { |
|
||||||
font-size: 16px; |
|
||||||
font-weight: 400; |
|
||||||
line-height: 21px; |
|
||||||
margin-left: 22px; |
|
||||||
color: $scorpion; |
|
||||||
} |
|
||||||
|
|
||||||
&__add-custom-input { |
|
||||||
margin-top: 6px; |
|
||||||
font-size: 16px; |
|
||||||
|
|
||||||
&::placeholder { |
|
||||||
color: $silver; |
|
||||||
font-size: 16px; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
&__add-custom-field + &__add-custom-field { |
|
||||||
margin-top: 6px; |
|
||||||
} |
|
||||||
|
|
||||||
&__buttons { |
|
||||||
display: flex; |
|
||||||
flex-flow: row nowrap; |
|
||||||
flex: 0 0 auto; |
|
||||||
align-items: center; |
|
||||||
justify-content: center; |
|
||||||
padding-bottom: 30px; |
|
||||||
padding-top: 20px; |
|
||||||
} |
|
||||||
|
|
||||||
&__confirm-button, |
|
||||||
&__cancel-button { |
|
||||||
margin: 0 12px; |
|
||||||
padding: 10px 13px; |
|
||||||
height: 54px; |
|
||||||
width: 133px; |
|
||||||
margin-right: 1.2rem; |
|
||||||
} |
|
||||||
|
|
||||||
&__token-icons-title { |
|
||||||
color: #5B5D67; |
|
||||||
font-family: Roboto; |
|
||||||
font-size: 18px; |
|
||||||
font-weight: 400; |
|
||||||
line-height: 24px; |
|
||||||
margin-left: 24px; |
|
||||||
margin-top: 8px; |
|
||||||
margin-bottom: 20px; |
|
||||||
} |
|
||||||
|
|
||||||
&__token-icons-container { |
|
||||||
display: flex; |
|
||||||
flex-flow: row wrap; |
|
||||||
} |
|
||||||
|
|
||||||
&__token-wrapper { |
|
||||||
transition: 200ms ease-in-out; |
|
||||||
display: flex; |
|
||||||
flex-flow: row nowrap; |
|
||||||
flex: 0 0 42.5%; |
|
||||||
align-items: center; |
|
||||||
padding: 12px; |
|
||||||
margin: 0% 2.5% 1.5%; |
|
||||||
box-sizing: border-box; |
|
||||||
border-radius: 10px; |
|
||||||
cursor: pointer; |
|
||||||
border: 2px solid transparent; |
|
||||||
position: relative; |
|
||||||
|
|
||||||
&:hover { |
|
||||||
border: 2px solid rgba($malibu-blue, .5); |
|
||||||
} |
|
||||||
|
|
||||||
&--selected { |
|
||||||
border: 2px solid $malibu-blue !important; |
|
||||||
} |
|
||||||
|
|
||||||
&--disabled { |
|
||||||
opacity: .4; |
|
||||||
pointer-events: none; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
&__token-data { |
|
||||||
align-self: flex-start; |
|
||||||
} |
|
||||||
|
|
||||||
&__token-name { |
|
||||||
font-weight: 400; |
|
||||||
font-size: 14px; |
|
||||||
line-height: 19px; |
|
||||||
} |
|
||||||
|
|
||||||
&__token-symbol { |
|
||||||
font-size: 22px; |
|
||||||
line-height: 29px; |
|
||||||
font-weight: 600; |
|
||||||
} |
|
||||||
|
|
||||||
&__token-icon { |
|
||||||
width: 60px; |
|
||||||
height: 60px; |
|
||||||
background-repeat: no-repeat; |
|
||||||
background-size: contain; |
|
||||||
background-position: center; |
|
||||||
border-radius: 50%; |
|
||||||
background-color: $white; |
|
||||||
box-shadow: 0 2px 4px 0 rgba($black, .24); |
|
||||||
margin-right: 12px; |
|
||||||
flex: 0 0 auto; |
|
||||||
} |
|
||||||
|
|
||||||
&__token-message { |
|
||||||
position: absolute; |
|
||||||
color: $caribbean-green; |
|
||||||
font-size: 11px; |
|
||||||
bottom: 0; |
|
||||||
left: 85px; |
|
||||||
} |
|
||||||
|
|
||||||
&__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; |
|
||||||
line-height: 43px; |
|
||||||
margin-right: 8px; |
|
||||||
} |
|
||||||
|
|
||||||
&__symbol { |
|
||||||
color: $scorpion; |
|
||||||
font-size: 16px; |
|
||||||
font-weight: 400; |
|
||||||
line-height: 24px; |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
&__confirmation-title { |
|
||||||
padding: 30px 120px 12px; |
|
||||||
|
|
||||||
@media screen and (max-width: $break-small) { |
|
||||||
padding: 20px 0; |
|
||||||
width: 100%; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
&__confirmation-content { |
|
||||||
padding-bottom: 60px; |
|
||||||
} |
|
||||||
|
|
||||||
&__confirmation-token-list-item { |
|
||||||
display: flex; |
|
||||||
flex-flow: row nowrap; |
|
||||||
margin: 0 auto; |
|
||||||
align-items: center; |
|
||||||
} |
|
||||||
|
|
||||||
&__confirmation-token-list-item + &__confirmation-token-list-item { |
|
||||||
margin-top: 30px; |
|
||||||
} |
|
||||||
|
|
||||||
&__confirmation-token-icon { |
|
||||||
margin-right: 18px; |
|
||||||
} |
|
||||||
|
|
||||||
@media screen and (max-width: $break-small) { |
|
||||||
top: 0; |
|
||||||
width: 100%; |
|
||||||
overflow: hidden; |
|
||||||
flex: 1 0 auto; |
|
||||||
|
|
||||||
&__wrapper { |
|
||||||
box-shadow: none !important; |
|
||||||
flex: 1 1 auto; |
|
||||||
width: 100%; |
|
||||||
overflow-y: scroll; |
|
||||||
height: 400px; |
|
||||||
} |
|
||||||
|
|
||||||
&__footers { |
|
||||||
border-bottom: 1px solid $gallery; |
|
||||||
} |
|
||||||
|
|
||||||
&__token-icon { |
|
||||||
width: 50px; |
|
||||||
height: 50px; |
|
||||||
} |
|
||||||
|
|
||||||
&__token-symbol { |
|
||||||
font-size: 18px; |
|
||||||
line-height: 24px; |
|
||||||
} |
|
||||||
|
|
||||||
&__token-name { |
|
||||||
font-size: 12px; |
|
||||||
line-height: 16px; |
|
||||||
} |
|
||||||
|
|
||||||
&__buttons { |
|
||||||
padding: 1rem; |
|
||||||
margin: 0; |
|
||||||
border-top: 1px solid $gallery; |
|
||||||
width: 100%; |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
@ -0,0 +1,105 @@ |
|||||||
|
import React, { Component } from 'react' |
||||||
|
import PropTypes from 'prop-types' |
||||||
|
import TokenTracker from 'eth-token-tracker' |
||||||
|
|
||||||
|
const withTokenTracker = WrappedComponent => { |
||||||
|
return class TokenTrackerWrappedComponent extends Component { |
||||||
|
static propTypes = { |
||||||
|
userAddress: PropTypes.string.isRequired, |
||||||
|
token: PropTypes.object.isRequired, |
||||||
|
} |
||||||
|
|
||||||
|
constructor (props) { |
||||||
|
super(props) |
||||||
|
|
||||||
|
this.state = { |
||||||
|
string: '', |
||||||
|
symbol: '', |
||||||
|
error: null, |
||||||
|
} |
||||||
|
|
||||||
|
this.tracker = null |
||||||
|
this.updateBalance = this.updateBalance.bind(this) |
||||||
|
this.setError = this.setError.bind(this) |
||||||
|
} |
||||||
|
|
||||||
|
componentDidMount () { |
||||||
|
this.createFreshTokenTracker() |
||||||
|
} |
||||||
|
|
||||||
|
componentDidUpdate (prevProps) { |
||||||
|
const { userAddress: newAddress, token: { address: newTokenAddress } } = this.props |
||||||
|
const { userAddress: oldAddress, token: { address: oldTokenAddress } } = prevProps |
||||||
|
|
||||||
|
if ((oldAddress === newAddress) && (oldTokenAddress === newTokenAddress)) { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
if ((!oldAddress || !newAddress) && (!oldTokenAddress || !newTokenAddress)) { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
this.createFreshTokenTracker() |
||||||
|
} |
||||||
|
|
||||||
|
componentWillUnmount () { |
||||||
|
this.removeListeners() |
||||||
|
} |
||||||
|
|
||||||
|
createFreshTokenTracker () { |
||||||
|
this.removeListeners() |
||||||
|
|
||||||
|
if (!global.ethereumProvider) { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
const { userAddress, token } = this.props |
||||||
|
|
||||||
|
this.tracker = new TokenTracker({ |
||||||
|
userAddress, |
||||||
|
provider: global.ethereumProvider, |
||||||
|
tokens: [token], |
||||||
|
pollingInterval: 8000, |
||||||
|
}) |
||||||
|
|
||||||
|
this.tracker.on('update', this.updateBalance) |
||||||
|
this.tracker.on('error', this.setError) |
||||||
|
|
||||||
|
this.tracker.updateBalances() |
||||||
|
.then(() => this.updateBalance(this.tracker.serialize())) |
||||||
|
.catch(error => this.setState({ error: error.message })) |
||||||
|
} |
||||||
|
|
||||||
|
setError (error) { |
||||||
|
this.setState({ error }) |
||||||
|
} |
||||||
|
|
||||||
|
updateBalance (tokens = []) { |
||||||
|
const [{ string, symbol }] = tokens |
||||||
|
this.setState({ string, symbol, error: null }) |
||||||
|
} |
||||||
|
|
||||||
|
removeListeners () { |
||||||
|
if (this.tracker) { |
||||||
|
this.tracker.stop() |
||||||
|
this.tracker.removeListener('update', this.updateBalance) |
||||||
|
this.tracker.removeListener('error', this.setError) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
render () { |
||||||
|
const { string, symbol, error } = this.state |
||||||
|
|
||||||
|
return ( |
||||||
|
<WrappedComponent |
||||||
|
{ ...this.props } |
||||||
|
string={string} |
||||||
|
symbol={symbol} |
||||||
|
error={error} |
||||||
|
/> |
||||||
|
) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
module.exports = withTokenTracker |
Loading…
Reference in new issue