aboutsummaryrefslogtreecommitdiffstats
path: root/ui
diff options
context:
space:
mode:
authorkumavis <kumavis@users.noreply.github.com>2018-05-23 03:53:59 +0800
committerGitHub <noreply@github.com>2018-05-23 03:53:59 +0800
commitb5bbfd32648a7deb0fd8b25c12825697e522adf6 (patch)
treedab36d56c4eaf230512b7430ddb3baf75356eab0 /ui
parenta8e0d38cfbb1c01705b56288511c2a8ce392e25d (diff)
parent8245bf010e22dda3e00c044429e3f5b880691fcb (diff)
downloadtangerine-wallet-browser-b5bbfd32648a7deb0fd8b25c12825697e522adf6.tar
tangerine-wallet-browser-b5bbfd32648a7deb0fd8b25c12825697e522adf6.tar.gz
tangerine-wallet-browser-b5bbfd32648a7deb0fd8b25c12825697e522adf6.tar.bz2
tangerine-wallet-browser-b5bbfd32648a7deb0fd8b25c12825697e522adf6.tar.lz
tangerine-wallet-browser-b5bbfd32648a7deb0fd8b25c12825697e522adf6.tar.xz
tangerine-wallet-browser-b5bbfd32648a7deb0fd8b25c12825697e522adf6.tar.zst
tangerine-wallet-browser-b5bbfd32648a7deb0fd8b25c12825697e522adf6.zip
Merge pull request #4308 from MetaMask/i4232-addtoken
Update designs for Add Token screen
Diffstat (limited to 'ui')
-rw-r--r--ui/app/actions.js23
-rw-r--r--ui/app/app.js3
-rw-r--r--ui/app/components/button/button.component.js19
-rw-r--r--ui/app/components/button/index.js2
-rw-r--r--ui/app/components/export-text-container/index.scss (renamed from ui/app/components/export-text-container/export-text-container.scss)0
-rw-r--r--ui/app/components/index.scss5
-rw-r--r--ui/app/components/info-box/index.js2
-rw-r--r--ui/app/components/info-box/index.scss24
-rw-r--r--ui/app/components/info-box/info-box.component.js49
-rw-r--r--ui/app/components/pages/add-token.js431
-rw-r--r--ui/app/components/pages/add-token/add-token.component.js351
-rw-r--r--ui/app/components/pages/add-token/add-token.container.js22
-rw-r--r--ui/app/components/pages/add-token/index.js2
-rw-r--r--ui/app/components/pages/add-token/index.scss25
-rw-r--r--ui/app/components/pages/add-token/token-list/index.js2
-rw-r--r--ui/app/components/pages/add-token/token-list/index.scss65
-rw-r--r--ui/app/components/pages/add-token/token-list/token-list-placeholder/index.js2
-rw-r--r--ui/app/components/pages/add-token/token-list/token-list-placeholder/index.scss19
-rw-r--r--ui/app/components/pages/add-token/token-list/token-list-placeholder/token-list-placeholder.component.js27
-rw-r--r--ui/app/components/pages/add-token/token-list/token-list.component.js60
-rw-r--r--ui/app/components/pages/add-token/token-list/token-list.container.js11
-rw-r--r--ui/app/components/pages/add-token/token-search/index.js2
-rw-r--r--ui/app/components/pages/add-token/token-search/token-search.component.js85
-rw-r--r--ui/app/components/pages/add-token/util.js13
-rw-r--r--ui/app/components/pages/confirm-add-token/confirm-add-token.component.js115
-rw-r--r--ui/app/components/pages/confirm-add-token/confirm-add-token.container.js20
-rw-r--r--ui/app/components/pages/confirm-add-token/index.js2
-rw-r--r--ui/app/components/pages/confirm-add-token/index.scss69
-rw-r--r--ui/app/components/pages/confirm-add-token/token-balance/index.js2
-rw-r--r--ui/app/components/pages/confirm-add-token/token-balance/token-balance.component.js16
-rw-r--r--ui/app/components/pages/confirm-add-token/token-balance/token-balance.container.js16
-rw-r--r--ui/app/components/pages/index.scss5
-rw-r--r--ui/app/components/pages/unlock-page/index.scss (renamed from ui/app/components/pages/unlock-page/unlock-page.scss)0
-rw-r--r--ui/app/components/pages/unlock-page/unlock-page.component.js3
-rw-r--r--ui/app/components/signature-request.js2
-rw-r--r--ui/app/components/text-field/text-field.component.js119
-rw-r--r--ui/app/css/itcss/components/add-token.scss461
-rw-r--r--ui/app/css/itcss/components/buttons.scss5
-rw-r--r--ui/app/css/itcss/components/index.scss4
-rw-r--r--ui/app/css/itcss/components/newui-sections.scss2
-rw-r--r--ui/app/css/itcss/components/pages/index.scss2
-rw-r--r--ui/app/css/itcss/generic/index.scss45
-rw-r--r--ui/app/helpers/with-token-tracker.js105
-rw-r--r--ui/app/reducers/metamask.js12
-rw-r--r--ui/app/routes.js2
45 files changed, 1280 insertions, 971 deletions
diff --git a/ui/app/actions.js b/ui/app/actions.js
index 57bffb046..894e31fde 100644
--- a/ui/app/actions.js
+++ b/ui/app/actions.js
@@ -275,6 +275,10 @@ var actions = {
UPDATE_NETWORK_ENDPOINT_TYPE: 'UPDATE_NETWORK_ENDPOINT_TYPE',
retryTransaction,
+ SET_PENDING_TOKENS: 'SET_PENDING_TOKENS',
+ CLEAR_PENDING_TOKENS: 'CLEAR_PENDING_TOKENS',
+ setPendingTokens,
+ clearPendingTokens,
}
module.exports = actions
@@ -1929,3 +1933,22 @@ function updateNetworkEndpointType (networkEndpointType) {
value: networkEndpointType,
}
}
+
+function setPendingTokens (pendingTokens) {
+ const { customToken = {}, selectedTokens = {} } = pendingTokens
+ const { address, symbol, decimals } = customToken
+ const tokens = address && symbol && decimals
+ ? { ...selectedTokens, [address]: { ...customToken, isCustom: true } }
+ : selectedTokens
+
+ return {
+ type: actions.SET_PENDING_TOKENS,
+ payload: tokens,
+ }
+}
+
+function clearPendingTokens () {
+ return {
+ type: actions.CLEAR_PENDING_TOKENS,
+ }
+}
diff --git a/ui/app/app.js b/ui/app/app.js
index f840cc34e..3d2961340 100644
--- a/ui/app/app.js
+++ b/ui/app/app.js
@@ -26,6 +26,7 @@ const UnlockPage = require('./components/pages/unlock-page')
const RestoreVaultPage = require('./components/pages/keychains/restore-vault')
const RevealSeedConfirmation = require('./components/pages/keychains/reveal-seed')
const AddTokenPage = require('./components/pages/add-token')
+const ConfirmAddTokenPage = require('./components/pages/confirm-add-token')
const CreateAccountPage = require('./components/pages/create-account')
const NoticeScreen = require('./components/pages/notice')
@@ -47,6 +48,7 @@ const {
REVEAL_SEED_ROUTE,
RESTORE_VAULT_ROUTE,
ADD_TOKEN_ROUTE,
+ CONFIRM_ADD_TOKEN_ROUTE,
NEW_ACCOUNT_ROUTE,
SEND_ROUTE,
CONFIRM_TRANSACTION_ROUTE,
@@ -77,6 +79,7 @@ class App extends Component {
h(Authenticated, { path: CONFIRM_TRANSACTION_ROUTE, component: ConfirmTxScreen }),
h(Authenticated, { path: SEND_ROUTE, exact, component: SendTransactionScreen2 }),
h(Authenticated, { path: ADD_TOKEN_ROUTE, exact, component: AddTokenPage }),
+ h(Authenticated, { path: CONFIRM_ADD_TOKEN_ROUTE, exact, component: ConfirmAddTokenPage }),
h(Authenticated, { path: NEW_ACCOUNT_ROUTE, component: CreateAccountPage }),
h(Authenticated, { path: DEFAULT_ROUTE, exact, component: Home }),
])
diff --git a/ui/app/components/button/button.component.js b/ui/app/components/button/button.component.js
index 7769e4875..fe3bf363c 100644
--- a/ui/app/components/button/button.component.js
+++ b/ui/app/components/button/button.component.js
@@ -1,7 +1,6 @@
-const { Component } = require('react')
-const h = require('react-hyperscript')
-const PropTypes = require('prop-types')
-const classnames = require('classnames')
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import classnames from 'classnames'
const SECONDARY = 'secondary'
const CLASSNAME_PRIMARY = 'btn-primary'
@@ -24,10 +23,12 @@ class Button extends Component {
const { type, large, className, ...buttonProps } = this.props
return (
- h('button', {
- className: classnames(getClassName(type, large), className),
- ...buttonProps,
- }, this.props.children)
+ <button
+ className={classnames(getClassName(type, large), className)}
+ { ...buttonProps }
+ >
+ { this.props.children }
+ </button>
)
}
}
@@ -39,5 +40,5 @@ Button.propTypes = {
children: PropTypes.string,
}
-module.exports = Button
+export default Button
diff --git a/ui/app/components/button/index.js b/ui/app/components/button/index.js
index 3dc7d1eea..33ae95ae2 100644
--- a/ui/app/components/button/index.js
+++ b/ui/app/components/button/index.js
@@ -1,2 +1,2 @@
-const Button = require('./button.component')
+import Button from './button.component'
module.exports = Button
diff --git a/ui/app/components/export-text-container/export-text-container.scss b/ui/app/components/export-text-container/index.scss
index 975d62f70..975d62f70 100644
--- a/ui/app/components/export-text-container/export-text-container.scss
+++ b/ui/app/components/export-text-container/index.scss
diff --git a/ui/app/components/index.scss b/ui/app/components/index.scss
new file mode 100644
index 000000000..f3fe823f8
--- /dev/null
+++ b/ui/app/components/index.scss
@@ -0,0 +1,5 @@
+@import './export-text-container/index';
+
+@import './info-box/index';
+
+@import './pages/index';
diff --git a/ui/app/components/info-box/index.js b/ui/app/components/info-box/index.js
new file mode 100644
index 000000000..6110422ed
--- /dev/null
+++ b/ui/app/components/info-box/index.js
@@ -0,0 +1,2 @@
+import InfoBox from './info-box.component'
+module.exports = InfoBox
diff --git a/ui/app/components/info-box/index.scss b/ui/app/components/info-box/index.scss
new file mode 100644
index 000000000..8b5626d79
--- /dev/null
+++ b/ui/app/components/info-box/index.scss
@@ -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;
+ }
+}
diff --git a/ui/app/components/info-box/info-box.component.js b/ui/app/components/info-box/info-box.component.js
new file mode 100644
index 000000000..8688b8e8f
--- /dev/null
+++ b/ui/app/components/info-box/info-box.component.js
@@ -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>
+ )
+ }
+}
diff --git a/ui/app/components/pages/add-token.js b/ui/app/components/pages/add-token.js
deleted file mode 100644
index 8d52571d0..000000000
--- a/ui/app/components/pages/add-token.js
+++ /dev/null
@@ -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')),
- ]),
- ])
-}
diff --git a/ui/app/components/pages/add-token/add-token.component.js b/ui/app/components/pages/add-token/add-token.component.js
new file mode 100644
index 000000000..0677b4317
--- /dev/null
+++ b/ui/app/components/pages/add-token/add-token.component.js
@@ -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
diff --git a/ui/app/components/pages/add-token/add-token.container.js b/ui/app/components/pages/add-token/add-token.container.js
new file mode 100644
index 000000000..87671b156
--- /dev/null
+++ b/ui/app/components/pages/add-token/add-token.container.js
@@ -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)
diff --git a/ui/app/components/pages/add-token/index.js b/ui/app/components/pages/add-token/index.js
new file mode 100644
index 000000000..3666cae82
--- /dev/null
+++ b/ui/app/components/pages/add-token/index.js
@@ -0,0 +1,2 @@
+import AddToken from './add-token.container'
+module.exports = AddToken
diff --git a/ui/app/components/pages/add-token/index.scss b/ui/app/components/pages/add-token/index.scss
new file mode 100644
index 000000000..39e86b97b
--- /dev/null
+++ b/ui/app/components/pages/add-token/index.scss
@@ -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;
+ }
+}
diff --git a/ui/app/components/pages/add-token/token-list/index.js b/ui/app/components/pages/add-token/token-list/index.js
new file mode 100644
index 000000000..21dd5ac72
--- /dev/null
+++ b/ui/app/components/pages/add-token/token-list/index.js
@@ -0,0 +1,2 @@
+import TokenList from './token-list.container'
+module.exports = TokenList
diff --git a/ui/app/components/pages/add-token/token-list/index.scss b/ui/app/components/pages/add-token/token-list/index.scss
new file mode 100644
index 000000000..e32739d59
--- /dev/null
+++ b/ui/app/components/pages/add-token/token-list/index.scss
@@ -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;
+ }
+}
diff --git a/ui/app/components/pages/add-token/token-list/token-list-placeholder/index.js b/ui/app/components/pages/add-token/token-list/token-list-placeholder/index.js
new file mode 100644
index 000000000..b82f45e93
--- /dev/null
+++ b/ui/app/components/pages/add-token/token-list/token-list-placeholder/index.js
@@ -0,0 +1,2 @@
+import TokenListPlaceholder from './token-list-placeholder.component'
+module.exports = TokenListPlaceholder
diff --git a/ui/app/components/pages/add-token/token-list/token-list-placeholder/index.scss b/ui/app/components/pages/add-token/token-list/token-list-placeholder/index.scss
new file mode 100644
index 000000000..9d0f4be32
--- /dev/null
+++ b/ui/app/components/pages/add-token/token-list/token-list-placeholder/index.scss
@@ -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;
+ }
+}
diff --git a/ui/app/components/pages/add-token/token-list/token-list-placeholder/token-list-placeholder.component.js b/ui/app/components/pages/add-token/token-list/token-list-placeholder/token-list-placeholder.component.js
new file mode 100644
index 000000000..abd599b26
--- /dev/null
+++ b/ui/app/components/pages/add-token/token-list/token-list-placeholder/token-list-placeholder.component.js
@@ -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>
+ )
+ }
+}
diff --git a/ui/app/components/pages/add-token/token-list/token-list.component.js b/ui/app/components/pages/add-token/token-list/token-list.component.js
new file mode 100644
index 000000000..724a68d6e
--- /dev/null
+++ b/ui/app/components/pages/add-token/token-list/token-list.component.js
@@ -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>
+ )
+ }
+}
diff --git a/ui/app/components/pages/add-token/token-list/token-list.container.js b/ui/app/components/pages/add-token/token-list/token-list.container.js
new file mode 100644
index 000000000..cd7b07a37
--- /dev/null
+++ b/ui/app/components/pages/add-token/token-list/token-list.container.js
@@ -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)
diff --git a/ui/app/components/pages/add-token/token-search/index.js b/ui/app/components/pages/add-token/token-search/index.js
new file mode 100644
index 000000000..acaa6b084
--- /dev/null
+++ b/ui/app/components/pages/add-token/token-search/index.js
@@ -0,0 +1,2 @@
+import TokenSearch from './token-search.component'
+module.exports = TokenSearch
diff --git a/ui/app/components/pages/add-token/token-search/token-search.component.js b/ui/app/components/pages/add-token/token-search/token-search.component.js
new file mode 100644
index 000000000..036b2db1e
--- /dev/null
+++ b/ui/app/components/pages/add-token/token-search/token-search.component.js
@@ -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()}
+ />
+ )
+ }
+}
diff --git a/ui/app/components/pages/add-token/util.js b/ui/app/components/pages/add-token/util.js
new file mode 100644
index 000000000..579c56cc0
--- /dev/null
+++ b/ui/app/components/pages/add-token/util.js
@@ -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)
+}
diff --git a/ui/app/components/pages/confirm-add-token/confirm-add-token.component.js b/ui/app/components/pages/confirm-add-token/confirm-add-token.component.js
new file mode 100644
index 000000000..9db9efc37
--- /dev/null
+++ b/ui/app/components/pages/confirm-add-token/confirm-add-token.component.js
@@ -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>
+ )
+ }
+}
diff --git a/ui/app/components/pages/confirm-add-token/confirm-add-token.container.js b/ui/app/components/pages/confirm-add-token/confirm-add-token.container.js
new file mode 100644
index 000000000..0190024d9
--- /dev/null
+++ b/ui/app/components/pages/confirm-add-token/confirm-add-token.container.js
@@ -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)
diff --git a/ui/app/components/pages/confirm-add-token/index.js b/ui/app/components/pages/confirm-add-token/index.js
new file mode 100644
index 000000000..b7decabec
--- /dev/null
+++ b/ui/app/components/pages/confirm-add-token/index.js
@@ -0,0 +1,2 @@
+import ConfirmAddToken from './confirm-add-token.container'
+module.exports = ConfirmAddToken
diff --git a/ui/app/components/pages/confirm-add-token/index.scss b/ui/app/components/pages/confirm-add-token/index.scss
new file mode 100644
index 000000000..66146cf78
--- /dev/null
+++ b/ui/app/components/pages/confirm-add-token/index.scss
@@ -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;
+ }
+}
diff --git a/ui/app/components/pages/confirm-add-token/token-balance/index.js b/ui/app/components/pages/confirm-add-token/token-balance/index.js
new file mode 100644
index 000000000..6fb5c8223
--- /dev/null
+++ b/ui/app/components/pages/confirm-add-token/token-balance/index.js
@@ -0,0 +1,2 @@
+import TokenBalance from './token-balance.container'
+module.exports = TokenBalance
diff --git a/ui/app/components/pages/confirm-add-token/token-balance/token-balance.component.js b/ui/app/components/pages/confirm-add-token/token-balance/token-balance.component.js
new file mode 100644
index 000000000..976788d4c
--- /dev/null
+++ b/ui/app/components/pages/confirm-add-token/token-balance/token-balance.component.js
@@ -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>
+ )
+ }
+}
diff --git a/ui/app/components/pages/confirm-add-token/token-balance/token-balance.container.js b/ui/app/components/pages/confirm-add-token/token-balance/token-balance.container.js
new file mode 100644
index 000000000..bc1289ce1
--- /dev/null
+++ b/ui/app/components/pages/confirm-add-token/token-balance/token-balance.container.js
@@ -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)
diff --git a/ui/app/components/pages/index.scss b/ui/app/components/pages/index.scss
new file mode 100644
index 000000000..b15c59863
--- /dev/null
+++ b/ui/app/components/pages/index.scss
@@ -0,0 +1,5 @@
+@import './unlock-page/index';
+
+@import './add-token/index';
+
+@import './confirm-add-token/index';
diff --git a/ui/app/components/pages/unlock-page/unlock-page.scss b/ui/app/components/pages/unlock-page/index.scss
index 3d44bd037..3d44bd037 100644
--- a/ui/app/components/pages/unlock-page/unlock-page.scss
+++ b/ui/app/components/pages/unlock-page/index.scss
diff --git a/ui/app/components/pages/unlock-page/unlock-page.component.js b/ui/app/components/pages/unlock-page/unlock-page.component.js
index 0976d9506..a2f009d8f 100644
--- a/ui/app/components/pages/unlock-page/unlock-page.component.js
+++ b/ui/app/components/pages/unlock-page/unlock-page.component.js
@@ -1,6 +1,6 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
-import Button from 'material-ui/Button'
+import Button from '@material-ui/core/Button'
import TextField from '../../text-field'
const { ENVIRONMENT_TYPE_POPUP } = require('../../../../../app/scripts/lib/enums')
@@ -129,6 +129,7 @@ class UnlockPage extends Component {
error={error}
autoFocus
autoComplete="current-password"
+ material
fullWidth
/>
</form>
diff --git a/ui/app/components/signature-request.js b/ui/app/components/signature-request.js
index b958a2d2d..474fcf439 100644
--- a/ui/app/components/signature-request.js
+++ b/ui/app/components/signature-request.js
@@ -115,7 +115,7 @@ SignatureRequest.prototype.renderBalance = function () {
return h('div.request-signature__balance', [
- h('div.request-signature__balance-text', [this.context.t('balance')]),
+ h('div.request-signature__balance-text', `${this.context.t('balance')}:`),
h('div.request-signature__balance-value', `${balanceInEther} ETH`),
diff --git a/ui/app/components/text-field/text-field.component.js b/ui/app/components/text-field/text-field.component.js
index 6fd3b82b4..b695a449a 100644
--- a/ui/app/components/text-field/text-field.component.js
+++ b/ui/app/components/text-field/text-field.component.js
@@ -1,59 +1,102 @@
-import React from 'react'
+import React, { Component } from 'react'
import PropTypes from 'prop-types'
-import { withStyles } from 'material-ui/styles'
-import { default as MaterialTextField } from 'material-ui/TextField'
+import { withStyles } from '@material-ui/core/styles'
+import { default as MaterialTextField } from '@material-ui/core/TextField'
const styles = {
- cssLabel: {
- '&$cssFocused': {
+ materialLabel: {
+ '&$materialFocused': {
color: '#aeaeae',
},
- '&$cssError': {
+ '&$materialError': {
color: '#aeaeae',
},
fontWeight: '400',
color: '#aeaeae',
},
- cssFocused: {},
- cssUnderline: {
+ materialFocused: {},
+ materialUnderline: {
'&: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',
+ },
}
-const TextField = props => {
- const { error, classes, ...textFieldProps } = props
+class TextField extends Component {
+ static defaultProps = {
+ error: null,
+ }
- return (
- <MaterialTextField
- error={Boolean(error)}
- helperText={error}
- InputLabelProps={{
- FormLabelClasses: {
- root: classes.cssLabel,
- focused: classes.cssFocused,
- error: classes.cssError,
- },
- }}
- InputProps={{
- classes: {
- underline: classes.cssUnderline,
- },
- }}
- {...textFieldProps}
- />
- )
-}
+ static propTypes = {
+ error: PropTypes.string,
+ classes: PropTypes.object,
+ material: PropTypes.bool,
+ startAdornment: PropTypes.element,
+ }
-TextField.defaultProps = {
- error: null,
-}
+ render () {
+ const { error, classes, material, startAdornment, ...textFieldProps } = this.props
-TextField.propTypes = {
- error: PropTypes.string,
- classes: PropTypes.object,
+ return (
+ <MaterialTextField
+ error={Boolean(error)}
+ helperText={error}
+ InputLabelProps={{
+ shrink: material ? undefined : true,
+ className: material ? '' : classes.inputLabel,
+ FormLabelClasses: {
+ root: material ? classes.materialLabel : classes.formLabel,
+ focused: material ? classes.materialFocused : classes.formLabelFocused,
+ error: classes.materialError,
+ },
+ }}
+ InputProps={{
+ startAdornment: startAdornment || undefined,
+ disableUnderline: !material,
+ classes: {
+ root: material ? '' : classes.inputRoot,
+ input: material ? '' : classes.input,
+ underline: material ? classes.materialUnderline : '',
+ focused: material ? '' : classes.inputFocused,
+ },
+ }}
+ {...textFieldProps}
+ />
+ )
+ }
}
export default withStyles(styles)(TextField)
diff --git a/ui/app/css/itcss/components/add-token.scss b/ui/app/css/itcss/components/add-token.scss
deleted file mode 100644
index a3ea0d85b..000000000
--- a/ui/app/css/itcss/components/add-token.scss
+++ /dev/null
@@ -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%;
- }
- }
-}
diff --git a/ui/app/css/itcss/components/buttons.scss b/ui/app/css/itcss/components/buttons.scss
index 86daf60d8..4cbed6093 100644
--- a/ui/app/css/itcss/components/buttons.scss
+++ b/ui/app/css/itcss/components/buttons.scss
@@ -15,8 +15,9 @@
font-size: 14px;
font-weight: 500;
transition: border-color .3s ease;
- padding: 0 20px;
+ padding: 0 16px;
min-width: 140px;
+ width: 100%;
text-transform: uppercase;
outline: none;
}
@@ -110,6 +111,7 @@
font-size: .85rem;
font-weight: 400;
transition: border-color .3s ease;
+ width: 100%;
&:hover {
border-color: $scorpion;
@@ -126,6 +128,7 @@
font-size: .85rem;
font-weight: 400;
transition: border-color .3s ease;
+ width: 100%;
}
// No longer used in flat design, remove when modal buttons done
diff --git a/ui/app/css/itcss/components/index.scss b/ui/app/css/itcss/components/index.scss
index 1c544e162..1d87b8004 100644
--- a/ui/app/css/itcss/components/index.scss
+++ b/ui/app/css/itcss/components/index.scss
@@ -30,8 +30,6 @@
@import './token-list.scss';
-@import './add-token.scss';
-
@import './currency-display.scss';
@import './account-menu.scss';
@@ -62,4 +60,4 @@
@import './sender-to-recipient.scss';
-@import '../../../components/export-text-container/export-text-container.scss';
+@import '../../../components/index';
diff --git a/ui/app/css/itcss/components/newui-sections.scss b/ui/app/css/itcss/components/newui-sections.scss
index 2903e07b4..bbe0ee661 100644
--- a/ui/app/css/itcss/components/newui-sections.scss
+++ b/ui/app/css/itcss/components/newui-sections.scss
@@ -144,8 +144,8 @@ $wallet-view-bg: $alabaster;
flex: 0 0 auto;
margin: 36px auto;
background: none;
- padding: .7rem 2rem;
transition: border-color .3s ease;
+ width: 150px;
&:hover {
border-color: $curious-blue;
diff --git a/ui/app/css/itcss/components/pages/index.scss b/ui/app/css/itcss/components/pages/index.scss
index 195185fff..709f8baf6 100644
--- a/ui/app/css/itcss/components/pages/index.scss
+++ b/ui/app/css/itcss/components/pages/index.scss
@@ -1,3 +1 @@
@import './reveal-seed.scss';
-
-@import '../../../../components/pages/unlock-page/unlock-page.scss';
diff --git a/ui/app/css/itcss/generic/index.scss b/ui/app/css/itcss/generic/index.scss
index 9b2982096..f667eeda8 100644
--- a/ui/app/css/itcss/generic/index.scss
+++ b/ui/app/css/itcss/generic/index.scss
@@ -74,28 +74,32 @@ input.large-input {
}
.page-container {
- width: 400px;
+ width: 408px;
background-color: $white;
box-shadow: 0 0 7px 0 rgba(0, 0, 0, .08);
z-index: 25;
display: flex;
flex-flow: column;
- border-radius: 7px;
+ border-radius: 8px;
&__header {
display: flex;
flex-flow: column;
border-bottom: 1px solid $geyser;
- padding: 20px;
+ padding: 16px;
flex: 0 0 auto;
position: relative;
+
+ &--no-padding-bottom {
+ padding-bottom: 0;
+ }
}
&__header-close {
color: $tundora;
position: absolute;
- top: 20px;
- right: 20px;
+ top: 16px;
+ right: 16px;
cursor: pointer;
overflow: hidden;
@@ -117,7 +121,7 @@ input.large-input {
flex-flow: row;
justify-content: center;
border-top: 1px solid $geyser;
- padding: 1.6rem;
+ padding: 16px;
flex: 0 0 auto;
.btn-clear,
@@ -128,11 +132,10 @@ input.large-input {
}
&__footer-button {
- width: 165px;
height: 55px;
font-size: 1rem;
text-transform: uppercase;
- margin-right: 1.2rem;
+ margin-right: 16px;
border-radius: 2px;
&:last-of-type {
@@ -162,25 +165,20 @@ input.large-input {
}
&__tabs {
- padding: 0 1.3rem;
display: flex;
+ margin-top: 16px;
}
&__tab {
min-width: 5rem;
- padding: .2rem .8rem .9rem;
+ padding: 8px;
color: $dusty-gray;
font-family: Roboto;
- font-size: 1.1rem;
- line-height: initial;
+ font-size: 1rem;
text-align: center;
cursor: pointer;
border-bottom: none;
- margin-right: 1rem;
-
- &:hover {
- color: $black;
- }
+ margin-right: 16px;
&:last-of-type {
margin-right: 0;
@@ -189,10 +187,6 @@ input.large-input {
&--selected {
color: $curious-blue;
border-bottom: 3px solid $curious-blue;
-
- &:hover {
- color: $curious-blue;
- }
}
}
@@ -260,7 +254,8 @@ input.large-input {
@media screen and (min-width: 576px) {
.page-container {
- height: 600px;
+ max-height: 82vh;
+ min-height: 570px;
flex: 0 0 auto;
}
}
@@ -303,3 +298,9 @@ input.form-control {
border: 1px solid $monzo;
}
}
+
+.hide-text-overflow {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
diff --git a/ui/app/helpers/with-token-tracker.js b/ui/app/helpers/with-token-tracker.js
new file mode 100644
index 000000000..e24517c18
--- /dev/null
+++ b/ui/app/helpers/with-token-tracker.js
@@ -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
diff --git a/ui/app/reducers/metamask.js b/ui/app/reducers/metamask.js
index 9afaf6a50..732fa6dea 100644
--- a/ui/app/reducers/metamask.js
+++ b/ui/app/reducers/metamask.js
@@ -28,6 +28,7 @@ function reduceMetamask (state, action) {
contractExchangeRates: {},
tokenExchangeRates: {},
tokens: [],
+ pendingTokens: {},
send: {
gasLimit: null,
gasPrice: null,
@@ -356,6 +357,17 @@ function reduceMetamask (state, action) {
currentLocale: action.value,
})
+ case actions.SET_PENDING_TOKENS:
+ return extend(metamaskState, {
+ pendingTokens: { ...action.payload },
+ })
+
+ case actions.CLEAR_PENDING_TOKENS: {
+ return extend(metamaskState, {
+ pendingTokens: {},
+ })
+ }
+
default:
return metamaskState
diff --git a/ui/app/routes.js b/ui/app/routes.js
index 4b3f8f4d8..0ff3f644d 100644
--- a/ui/app/routes.js
+++ b/ui/app/routes.js
@@ -6,6 +6,7 @@ const REVEAL_SEED_ROUTE = '/seed'
const CONFIRM_SEED_ROUTE = '/confirm-seed'
const RESTORE_VAULT_ROUTE = '/restore-vault'
const ADD_TOKEN_ROUTE = '/add-token'
+const CONFIRM_ADD_TOKEN_ROUTE = '/confirm-add-token'
const NEW_ACCOUNT_ROUTE = '/new-account'
const IMPORT_ACCOUNT_ROUTE = '/new-account/import'
const SEND_ROUTE = '/send'
@@ -31,6 +32,7 @@ module.exports = {
CONFIRM_SEED_ROUTE,
RESTORE_VAULT_ROUTE,
ADD_TOKEN_ROUTE,
+ CONFIRM_ADD_TOKEN_ROUTE,
NEW_ACCOUNT_ROUTE,
IMPORT_ACCOUNT_ROUTE,
SEND_ROUTE,