diff --git a/old-ui/app/account-detail.js b/old-ui/app/account-detail.js index 808619e46..d240fc38e 100644 --- a/old-ui/app/account-detail.js +++ b/old-ui/app/account-detail.js @@ -50,6 +50,10 @@ AccountDetailScreen.prototype.render = function () { var account = props.accounts[selected] const { network, conversionRate, currentCurrency } = props + if (Object.keys(props.suggestedTokens).length > 0) { + this.props.dispatch(actions.showAddSuggestedTokenPage()) + } + return ( h('.account-detail-section.full-flex-height', [ diff --git a/old-ui/app/add-suggested-token.js b/old-ui/app/add-suggested-token.js index e69de29bb..1a0fc0461 100644 --- a/old-ui/app/add-suggested-token.js +++ b/old-ui/app/add-suggested-token.js @@ -0,0 +1,193 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const connect = require('react-redux').connect +const actions = require('../../ui/app/actions') +const Tooltip = require('./components/tooltip.js') +const ethUtil = require('ethereumjs-util') + +module.exports = connect(mapStateToProps)(AddSuggestedTokenScreen) + +function mapStateToProps (state) { + return { + identities: state.metamask.identities, + suggestedTokens: state.metamask.suggestedTokens, + } +} + +inherits(AddSuggestedTokenScreen, Component) +function AddSuggestedTokenScreen () { + this.state = { + warning: null, + } + Component.call(this) +} + +AddSuggestedTokenScreen.prototype.render = function () { + const state = this.state + const props = this.props + const { warning } = state + const key = Object.keys(props.suggestedTokens)[0] + const { address, symbol, decimals } = props.suggestedTokens[key] + + return ( + h('.flex-column.flex-grow', [ + + // subtitle and nav + h('.section-title.flex-row.flex-center', [ + h('h2.page-subtitle', 'Add Suggested Token'), + ]), + + h('.error', { + style: { + display: warning ? 'block' : 'none', + padding: '0 20px', + textAlign: 'center', + }, + }, warning), + + // conf view + h('.flex-column.flex-justify-center.flex-grow.select-none', [ + h('.flex-space-around', { + style: { + padding: '20px', + }, + }, [ + + h('div', [ + h(Tooltip, { + position: 'top', + title: 'The contract of the actual token contract. Click for more info.', + }, [ + h('a', { + style: { fontWeight: 'bold', paddingRight: '10px'}, + href: 'https://support.metamask.io/kb/article/24-what-is-a-token-contract-address', + target: '_blank', + }, [ + h('span', 'Token Contract Address '), + h('i.fa.fa-question-circle'), + ]), + ]), + ]), + + h('section.flex-row.flex-center', [ + h('p#token-address', { + name: 'address', + style: { + width: 'inherit', + flex: '1 0 auto', + height: '30px', + margin: '8px', + }, + }, address), + ]), + + h('div', [ + h('span', { + style: { fontWeight: 'bold', paddingRight: '10px'}, + }, 'Token Symbol'), + ]), + + h('div', { style: {display: 'flex'} }, [ + h('p#token_symbol', { + style: { + width: 'inherit', + flex: '1 0 auto', + height: '30px', + margin: '8px', + }, + }, symbol), + ]), + + h('div', [ + h('span', { + style: { fontWeight: 'bold', paddingRight: '10px'}, + }, 'Decimals of Precision'), + ]), + + h('div', { style: {display: 'flex'} }, [ + h('p#token_decimals', { + type: 'number', + style: { + width: 'inherit', + flex: '1 0 auto', + height: '30px', + margin: '8px', + }, + }, decimals), + ]), + + h('button', { + style: { + alignSelf: 'center', + margin: '8px', + }, + onClick: (event) => { + this.props.dispatch(actions.removeSuggestedTokens()) + }, + }, 'Cancel'), + + h('button', { + style: { + alignSelf: 'center', + margin: '8px', + }, + onClick: (event) => { + const valid = this.validateInputs({ address, symbol, decimals }) + if (!valid) return + + this.props.dispatch(actions.addToken(address.trim(), symbol.trim(), decimals)) + .then(() => { + this.props.dispatch(actions.removeSuggestedTokens()) + }) + }, + }, 'Add'), + ]), + ]), + ]) + ) +} + +AddSuggestedTokenScreen.prototype.componentWillMount = function () { + if (typeof global.ethereumProvider === 'undefined') return +} + +AddSuggestedTokenScreen.prototype.validateInputs = function (opts) { + let msg = '' + const identitiesList = Object.keys(this.props.identities) + const { address, symbol, decimals } = opts + const standardAddress = ethUtil.addHexPrefix(address).toLowerCase() + + const validAddress = ethUtil.isValidAddress(address) + if (!validAddress) { + msg += 'Address is invalid.' + } + + const validDecimals = decimals >= 0 && decimals < 36 + if (!validDecimals) { + msg += 'Decimals must be at least 0, and not over 36. ' + } + + const symbolLen = symbol.trim().length + const validSymbol = symbolLen > 0 && symbolLen < 10 + if (!validSymbol) { + msg += 'Symbol must be between 0 and 10 characters.' + } + + const ownAddress = identitiesList.includes(standardAddress) + if (ownAddress) { + msg = 'Personal address detected. Input the token contract address.' + } + + const isValid = validAddress && validDecimals && !ownAddress + + if (!isValid) { + this.setState({ + warning: msg, + }) + } else { + this.setState({ warning: null }) + } + + return isValid +} diff --git a/old-ui/app/app.js b/old-ui/app/app.js index 0637e3b5b..fb15d414d 100644 --- a/old-ui/app/app.js +++ b/old-ui/app/app.js @@ -22,6 +22,7 @@ const generateLostAccountsNotice = require('../lib/lost-accounts-notice') // other views const ConfigScreen = require('./config') const AddTokenScreen = require('./add-token') +const AddSuggestedTokenScreen = require('./add-suggested-token') const Import = require('./accounts/import') const InfoScreen = require('./info') const Loading = require('./components/loading') @@ -77,6 +78,7 @@ function mapStateToProps (state) { lostAccounts: state.metamask.lostAccounts, frequentRpcList: state.metamask.frequentRpcList || [], featureFlags, + suggestedTokens: state.metamask.suggestedTokens, // state needed to get account dropdown temporarily rendering from app bar identities, @@ -554,6 +556,10 @@ App.prototype.renderPrimary = function () { log.debug('rendering add-token screen from unlock screen.') return h(AddTokenScreen, {key: 'add-token'}) + case 'add-suggested-token': + log.debug('rendering add-token screen from unlock screen.') + return h(AddSuggestedTokenScreen, {key: 'add-suggested-token'}) + case 'config': log.debug('rendering config screen') return h(ConfigScreen, {key: 'config'}) diff --git a/ui/app/actions.js b/ui/app/actions.js index 7b414a038..04ad344cf 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -221,7 +221,9 @@ var actions = { SET_PROVIDER_TYPE: 'SET_PROVIDER_TYPE', showConfigPage, SHOW_ADD_TOKEN_PAGE: 'SHOW_ADD_TOKEN_PAGE', + SHOW_ADD_SUGGESTED_TOKEN_PAGE: 'SHOW_ADD_SUGGESTED_TOKEN_PAGE', showAddTokenPage, + showAddSuggestedTokenPage, addToken, addTokens, removeToken, @@ -1559,6 +1561,13 @@ function showAddTokenPage (transitionForward = true) { } } +function showAddSuggestedTokenPage (transitionForward = true) { + return { + type: actions.SHOW_ADD_SUGGESTED_TOKEN_PAGE, + value: transitionForward, + } +} + function addToken (address, symbol, decimals) { return (dispatch) => { dispatch(actions.showLoadingIndication()) diff --git a/ui/app/reducers/app.js b/ui/app/reducers/app.js index 50d8bcba7..f76b73132 100644 --- a/ui/app/reducers/app.js +++ b/ui/app/reducers/app.js @@ -196,6 +196,15 @@ function reduceApp (state, action) { transForward: action.value, }) + case actions.SHOW_ADD_SUGGESTED_TOKEN_PAGE: + return extend(appState, { + currentView: { + name: 'add-suggested-token', + context: appState.currentView.context, + }, + transForward: action.value, + }) + case actions.SHOW_IMPORT_PAGE: return extend(appState, { currentView: {