Merge pull request #4606 from MetaMask/WatchTokenFeature
Add metamask_watchAssetfeature/default_network_editable
commit
4560df6e73
@ -0,0 +1,202 @@ |
|||||||
|
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') |
||||||
|
const Copyable = require('./components/copyable') |
||||||
|
const addressSummary = require('./util').addressSummary |
||||||
|
|
||||||
|
|
||||||
|
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('div', { |
||||||
|
style: { display: 'flex' }, |
||||||
|
}, [ |
||||||
|
h(Copyable, { |
||||||
|
value: ethUtil.toChecksumAddress(address), |
||||||
|
}, [ |
||||||
|
h('span#token-address', { |
||||||
|
style: { |
||||||
|
width: 'inherit', |
||||||
|
flex: '1 0 auto', |
||||||
|
height: '30px', |
||||||
|
margin: '8px', |
||||||
|
display: 'flex', |
||||||
|
}, |
||||||
|
}, addressSummary(address, 24, 4, false)), |
||||||
|
]), |
||||||
|
]), |
||||||
|
|
||||||
|
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 |
||||||
|
} |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,126 @@ |
|||||||
|
import React, { Component } from 'react' |
||||||
|
import PropTypes from 'prop-types' |
||||||
|
import { DEFAULT_ROUTE } from '../../../routes' |
||||||
|
import Button from '../../button' |
||||||
|
import Identicon from '../../../components/identicon' |
||||||
|
import TokenBalance from '../../token-balance' |
||||||
|
|
||||||
|
export default class ConfirmAddSuggestedToken extends Component { |
||||||
|
static contextTypes = { |
||||||
|
t: PropTypes.func, |
||||||
|
} |
||||||
|
|
||||||
|
static propTypes = { |
||||||
|
history: PropTypes.object, |
||||||
|
clearPendingTokens: PropTypes.func, |
||||||
|
addToken: PropTypes.func, |
||||||
|
pendingTokens: PropTypes.object, |
||||||
|
removeSuggestedTokens: PropTypes.func, |
||||||
|
} |
||||||
|
|
||||||
|
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 { addToken, pendingTokens, removeSuggestedTokens, history } = this.props |
||||||
|
const pendingTokenKey = Object.keys(pendingTokens)[0] |
||||||
|
const pendingToken = pendingTokens[pendingTokenKey] |
||||||
|
|
||||||
|
return ( |
||||||
|
<div className="page-container"> |
||||||
|
<div className="page-container__header"> |
||||||
|
<div className="page-container__title"> |
||||||
|
{ this.context.t('addSuggestedTokens') } |
||||||
|
</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, image } = 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} |
||||||
|
image={image} |
||||||
|
/> |
||||||
|
<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="default" |
||||||
|
large |
||||||
|
className="page-container__footer-button" |
||||||
|
onClick={() => { |
||||||
|
removeSuggestedTokens() |
||||||
|
.then(() => { |
||||||
|
history.push(DEFAULT_ROUTE) |
||||||
|
}) |
||||||
|
}} |
||||||
|
> |
||||||
|
{ this.context.t('cancel') } |
||||||
|
</Button> |
||||||
|
<Button |
||||||
|
type="primary" |
||||||
|
large |
||||||
|
className="page-container__footer-button" |
||||||
|
onClick={() => { |
||||||
|
addToken(pendingToken) |
||||||
|
.then(() => { |
||||||
|
removeSuggestedTokens() |
||||||
|
.then(() => { |
||||||
|
history.push(DEFAULT_ROUTE) |
||||||
|
}) |
||||||
|
}) |
||||||
|
}} |
||||||
|
> |
||||||
|
{ this.context.t('addToken') } |
||||||
|
</Button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,29 @@ |
|||||||
|
import { connect } from 'react-redux' |
||||||
|
import { compose } from 'recompose' |
||||||
|
import ConfirmAddSuggestedToken from './confirm-add-suggested-token.component' |
||||||
|
import { withRouter } from 'react-router-dom' |
||||||
|
|
||||||
|
const extend = require('xtend') |
||||||
|
|
||||||
|
const { addToken, removeSuggestedTokens } = require('../../../actions') |
||||||
|
|
||||||
|
const mapStateToProps = ({ metamask }) => { |
||||||
|
const { pendingTokens, suggestedTokens } = metamask |
||||||
|
const params = extend(pendingTokens, suggestedTokens) |
||||||
|
|
||||||
|
return { |
||||||
|
pendingTokens: params, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const mapDispatchToProps = dispatch => { |
||||||
|
return { |
||||||
|
addToken: ({address, symbol, decimals, image}) => dispatch(addToken(address, symbol, decimals, image)), |
||||||
|
removeSuggestedTokens: () => dispatch(removeSuggestedTokens()), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export default compose( |
||||||
|
withRouter, |
||||||
|
connect(mapStateToProps, mapDispatchToProps) |
||||||
|
)(ConfirmAddSuggestedToken) |
@ -0,0 +1,2 @@ |
|||||||
|
import ConfirmAddSuggestedToken from './confirm-add-suggested-token.container' |
||||||
|
module.exports = ConfirmAddSuggestedToken |
Loading…
Reference in new issue