diff options
Diffstat (limited to 'ui/app')
-rw-r--r-- | ui/app/account-detail.js | 10 | ||||
-rw-r--r-- | ui/app/accounts/import/index.js | 9 | ||||
-rw-r--r-- | ui/app/actions.js | 27 | ||||
-rw-r--r-- | ui/app/add-token.js | 219 | ||||
-rw-r--r-- | ui/app/app.js | 54 | ||||
-rw-r--r-- | ui/app/components/ens-input.js | 2 | ||||
-rw-r--r-- | ui/app/components/network.js | 23 | ||||
-rw-r--r-- | ui/app/components/pending-tx.js | 2 | ||||
-rw-r--r-- | ui/app/components/token-cell.js | 42 | ||||
-rw-r--r-- | ui/app/components/token-list.js | 109 | ||||
-rw-r--r-- | ui/app/config.js | 28 | ||||
-rw-r--r-- | ui/app/keychains/hd/create-vault-complete.js | 2 | ||||
-rw-r--r-- | ui/app/reducers/app.js | 10 | ||||
-rw-r--r-- | ui/app/send.js | 4 |
14 files changed, 469 insertions, 72 deletions
diff --git a/ui/app/account-detail.js b/ui/app/account-detail.js index 836032b3c..bed05a7fb 100644 --- a/ui/app/account-detail.js +++ b/ui/app/account-detail.js @@ -35,6 +35,7 @@ function mapStateToProps (state) { conversionRate: state.metamask.conversionRate, currentCurrency: state.metamask.currentCurrency, currentAccountTab: state.metamask.currentAccountTab, + tokens: state.metamask.tokens, } } @@ -273,11 +274,16 @@ AccountDetailScreen.prototype.tabSections = function () { AccountDetailScreen.prototype.tabSwitchView = function () { const props = this.props const { address, network } = props - const { currentAccountTab } = this.props + const { currentAccountTab, tokens } = this.props switch (currentAccountTab) { case 'tokens': - return h(TokenList, { userAddress: address, network }) + return h(TokenList, { + userAddress: address, + network, + tokens, + addToken: () => this.props.dispatch(actions.showAddTokenPage()), + }) default: return this.transactionList() } diff --git a/ui/app/accounts/import/index.js b/ui/app/accounts/import/index.js index a0f0f9bdb..97b387229 100644 --- a/ui/app/accounts/import/index.js +++ b/ui/app/accounts/import/index.js @@ -2,6 +2,7 @@ const inherits = require('util').inherits const Component = require('react').Component const h = require('react-hyperscript') const connect = require('react-redux').connect +const actions = require('../../actions') import Select from 'react-select' // Subviews @@ -37,6 +38,14 @@ AccountImportSubview.prototype.render = function () { style: { }, }, [ + h('.section-title.flex-row.flex-center', [ + h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { + onClick: (event) => { + props.dispatch(actions.goHome()) + }, + }), + h('h2.page-subtitle', 'Import Accounts'), + ]), h('div', { style: { padding: '10px', diff --git a/ui/app/actions.js b/ui/app/actions.js index b6b5d6eb1..d99291e46 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -121,7 +121,10 @@ var actions = { SET_PROVIDER_TYPE: 'SET_PROVIDER_TYPE', USE_ETHERSCAN_PROVIDER: 'USE_ETHERSCAN_PROVIDER', useEtherscanProvider: useEtherscanProvider, - showConfigPage: showConfigPage, + showConfigPage, + SHOW_ADD_TOKEN_PAGE: 'SHOW_ADD_TOKEN_PAGE', + showAddTokenPage, + addToken, setRpcTarget: setRpcTarget, setDefaultRpcTarget: setDefaultRpcTarget, setProviderType: setProviderType, @@ -627,6 +630,28 @@ function showConfigPage (transitionForward = true) { } } +function showAddTokenPage (transitionForward = true) { + return { + type: actions.SHOW_ADD_TOKEN_PAGE, + value: transitionForward, + } +} + +function addToken (address, symbol, decimals) { + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + background.addToken(address, symbol, decimals, (err) => { + dispatch(actions.hideLoadingIndication()) + if (err) { + return dispatch(actions.displayWarning(err.message)) + } + setTimeout(() => { + dispatch(actions.goHome()) + }, 250) + }) + } +} + function goBackToInitView () { return { type: actions.BACK_TO_INIT_MENU, diff --git a/ui/app/add-token.js b/ui/app/add-token.js new file mode 100644 index 000000000..15ef7a852 --- /dev/null +++ b/ui/app/add-token.js @@ -0,0 +1,219 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const connect = require('react-redux').connect +const actions = require('./actions') + +const ethUtil = require('ethereumjs-util') +const abi = require('human-standard-token-abi') +const Eth = require('ethjs-query') +const EthContract = require('ethjs-contract') + +const emptyAddr = '0x0000000000000000000000000000000000000000' + +module.exports = connect(mapStateToProps)(AddTokenScreen) + +function mapStateToProps (state) { + return { + } +} + +inherits(AddTokenScreen, Component) +function AddTokenScreen () { + this.state = { + warning: null, + address: null, + symbol: 'TOKEN', + decimals: 18, + } + Component.call(this) +} + +AddTokenScreen.prototype.render = function () { + const state = this.state + const props = this.props + const { warning, symbol, decimals } = state + + return ( + h('.flex-column.flex-grow', [ + + // subtitle and nav + h('.section-title.flex-row.flex-center', [ + h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', { + onClick: (event) => { + props.dispatch(actions.goHome()) + }, + }), + h('h2.page-subtitle', 'Add 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('span', { + style: { fontWeight: 'bold', paddingRight: '10px'}, + }, 'Token Address'), + ]), + + h('section.flex-row.flex-center', [ + h('input#token-address', { + name: 'address', + placeholder: 'Token Address', + onChange: this.tokenAddressDidChange.bind(this), + style: { + width: 'inherit', + flex: '1 0 auto', + height: '30px', + margin: '8px', + }, + }), + ]), + + h('div', [ + h('span', { + style: { fontWeight: 'bold', paddingRight: '10px'}, + }, 'Token Symbol'), + ]), + + h('div', { style: {display: 'flex'} }, [ + h('input#token_symbol', { + placeholder: `Like "ETH"`, + value: symbol, + style: { + width: 'inherit', + flex: '1 0 auto', + height: '30px', + margin: '8px', + }, + onChange: (event) => { + var element = event.target + var symbol = element.value + this.setState({ symbol }) + }, + }), + ]), + + h('div', [ + h('span', { + style: { fontWeight: 'bold', paddingRight: '10px'}, + }, 'Decimals of Precision'), + ]), + + h('div', { style: {display: 'flex'} }, [ + h('input#token_decimals', { + value: decimals, + type: 'number', + min: 0, + max: 36, + style: { + width: 'inherit', + flex: '1 0 auto', + height: '30px', + margin: '8px', + }, + onChange: (event) => { + var element = event.target + var decimals = element.value.trim() + this.setState({ decimals }) + }, + }), + ]), + + h('button', { + style: { + alignSelf: 'center', + }, + onClick: (event) => { + const valid = this.validateInputs() + if (!valid) return + + const { address, symbol, decimals } = this.state + this.props.dispatch(actions.addToken(address.trim(), symbol.trim(), decimals)) + }, + }, 'Add'), + ]), + ]), + ]) + ) +} + +AddTokenScreen.prototype.componentWillMount = function () { + if (typeof global.ethereumProvider === 'undefined') return + + this.eth = new Eth(global.ethereumProvider) + this.contract = new EthContract(this.eth) + this.TokenContract = this.contract(abi) +} + +AddTokenScreen.prototype.tokenAddressDidChange = function (event) { + const el = event.target + const address = el.value.trim() + if (ethUtil.isValidAddress(address) && address !== emptyAddr) { + this.setState({ address }) + this.attemptToAutoFillTokenParams(address) + } +} + +AddTokenScreen.prototype.validateInputs = function () { + let msg = '' + const state = this.state + const { address, symbol, decimals } = state + + 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 isValid = validAddress && validDecimals + + if (!isValid) { + this.setState({ + warning: msg, + }) + } else { + this.setState({ warning: null }) + } + + return isValid +} + +AddTokenScreen.prototype.attemptToAutoFillTokenParams = async function (address) { + const contract = this.TokenContract.at(address) + + const results = await Promise.all([ + contract.symbol(), + contract.decimals(), + ]) + + const [ symbol, decimals ] = results + if (symbol && decimals) { + console.log('SETTING SYMBOL AND DECIMALS', { symbol, decimals }) + this.setState({ symbol: symbol[0], decimals: decimals[0].toString() }) + } +} + diff --git a/ui/app/app.js b/ui/app/app.js index d444a8349..1a63002e1 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -19,6 +19,7 @@ const NoticeScreen = require('./components/notice') const generateLostAccountsNotice = require('../lib/lost-accounts-notice') // other views const ConfigScreen = require('./config') +const AddTokenScreen = require('./add-token') const Import = require('./accounts/import') const InfoScreen = require('./info') const Loading = require('./components/loading') @@ -65,9 +66,9 @@ function mapStateToProps (state) { App.prototype.render = function () { var props = this.props const { isLoading, loadingMessage, transForward, network } = props - const isLoadingNetwork = network === 'loading' + const isLoadingNetwork = network === 'loading' && props.currentView.name !== 'config' const loadMessage = loadingMessage || isLoadingNetwork ? - 'Searching for Network' : null + `Connecting to ${this.getNetworkName()}` : null log.debug('Main ui render function') @@ -135,7 +136,7 @@ App.prototype.renderAppBar = function () { }, }, [ - h('div', { + h('div.left-menu-section', { style: { display: 'flex', flexDirection: 'row', @@ -150,21 +151,15 @@ App.prototype.renderAppBar = function () { src: '/images/icon-128.png', }), - h('#network-spacer.flex-center', { - style: { - marginRight: '-72px', + h(NetworkIndicator, { + network: this.props.network, + provider: this.props.provider, + onClick: (event) => { + event.preventDefault() + event.stopPropagation() + this.setState({ isNetworkMenuOpen: !isNetworkMenuOpen }) }, - }, [ - h(NetworkIndicator, { - network: this.props.network, - provider: this.props.provider, - onClick: (event) => { - event.preventDefault() - event.stopPropagation() - this.setState({ isNetworkMenuOpen: !isNetworkMenuOpen }) - }, - }), - ]), + }), ]), // metamask name @@ -458,6 +453,10 @@ App.prototype.renderPrimary = function () { log.debug('rendering confirm tx screen') return h(ConfirmTxScreen, {key: 'confirm-tx'}) + case 'add-token': + log.debug('rendering add-token screen from unlock screen.') + return h(AddTokenScreen, {key: 'add-token'}) + case 'config': log.debug('rendering config screen') return h(ConfigScreen, {key: 'config'}) @@ -550,6 +549,27 @@ App.prototype.renderCustomOption = function (provider) { } } +App.prototype.getNetworkName = function () { + const { provider } = this.props + const providerName = provider.type + + let name + + if (providerName === 'mainnet') { + name = 'Main Ethereum Network' + } else if (providerName === 'ropsten') { + name = 'Ropsten Test Network' + } else if (providerName === 'kovan') { + name = 'Kovan Test Network' + } else if (providerName === 'rinkeby') { + name = 'Rinkeby Test Network' + } else { + name = 'Unknown Private Network' + } + + return name +} + App.prototype.renderCommonRpc = function (rpcList, provider) { const { rpcTarget } = provider const props = this.props diff --git a/ui/app/components/ens-input.js b/ui/app/components/ens-input.js index 16c50db84..3a33ebf74 100644 --- a/ui/app/components/ens-input.js +++ b/ui/app/components/ens-input.js @@ -41,7 +41,6 @@ EnsInput.prototype.render = function () { this.checkName() }, }) - return h('div', { style: { width: '100%' }, }, [ @@ -55,6 +54,7 @@ EnsInput.prototype.render = function () { return h('option', { value: identity.address, label: identity.name, + key: identity.address, }) }), // Corresponds to previously sent-to addresses. diff --git a/ui/app/components/network.js b/ui/app/components/network.js index 31a8fc17c..d5d3e18cd 100644 --- a/ui/app/components/network.js +++ b/ui/app/components/network.js @@ -22,15 +22,24 @@ Network.prototype.render = function () { let iconName, hoverText if (networkNumber === 'loading') { - return h('img.network-indicator', { - title: 'Attempting to connect to blockchain.', - onClick: (event) => this.props.onClick(event), + return h('span', { style: { - width: '27px', - marginRight: '-27px', + display: 'flex', + alignItems: 'center', + flexDirection: 'row', }, - src: 'images/loading.svg', - }) + onClick: (event) => this.props.onClick(event), + }, [ + h('img', { + title: 'Attempting to connect to blockchain.', + style: { + width: '27px', + }, + src: 'images/loading.svg', + }), + h('i.fa.fa-sort-desc'), + ]) + } else if (providerName === 'mainnet') { hoverText = 'Main Ethereum Network' iconName = 'ethereum-network' diff --git a/ui/app/components/pending-tx.js b/ui/app/components/pending-tx.js index f33a5d948..d7d602f31 100644 --- a/ui/app/components/pending-tx.js +++ b/ui/app/components/pending-tx.js @@ -315,7 +315,7 @@ PendingTx.prototype.render = function () { // Accept Button h('input.confirm.btn-green', { type: 'submit', - value: 'ACCEPT', + value: 'SUBMIT', style: { marginLeft: '10px' }, disabled: insufficientBalance || !this.state.valid || !isValidAddress || this.state.submitting, }), diff --git a/ui/app/components/token-cell.js b/ui/app/components/token-cell.js index d3a895d36..19d7139bb 100644 --- a/ui/app/components/token-cell.js +++ b/ui/app/components/token-cell.js @@ -2,6 +2,7 @@ const Component = require('react').Component const h = require('react-hyperscript') const inherits = require('util').inherits const Identicon = require('./identicon') +const prefixForNetwork = require('../../lib/etherscan-prefix-for-network') module.exports = TokenCell @@ -17,12 +18,7 @@ TokenCell.prototype.render = function () { return ( h('li.token-cell', { style: { cursor: network === '1' ? 'pointer' : 'default' }, - onClick: (event) => { - const url = urlFor(address, userAddress, network) - if (url) { - navigateTo(url) - } - }, + onClick: this.view.bind(this, address, userAddress, network), }, [ h(Identicon, { @@ -32,15 +28,45 @@ TokenCell.prototype.render = function () { }), h('h3', `${string || 0} ${symbol}`), + + h('span', { style: { flex: '1 0 auto' } }), + + /* + h('button', { + onClick: this.send.bind(this, address), + }, 'SEND'), + */ + ]) ) } +TokenCell.prototype.send = function (address, event) { + event.preventDefault() + event.stopPropagation() + const url = tokenFactoryFor(address) + if (url) { + navigateTo(url) + } +} + +TokenCell.prototype.view = function (address, userAddress, network, event) { + const url = etherscanLinkFor(address, userAddress, network) + if (url) { + navigateTo(url) + } +} + function navigateTo (url) { global.platform.openWindow({ url }) } -function urlFor (tokenAddress, address, network) { - return `https://etherscan.io/token/${tokenAddress}?a=${address}` +function etherscanLinkFor (tokenAddress, address, network) { + const prefix = prefixForNetwork(network) + return `https://${prefix}etherscan.io/token/${tokenAddress}?a=${address}` +} + +function tokenFactoryFor (tokenAddress) { + return `https://tokenfactory.surge.sh/#/token/${tokenAddress}` } diff --git a/ui/app/components/token-list.js b/ui/app/components/token-list.js index 633d3ccfe..20cfa897e 100644 --- a/ui/app/components/token-list.js +++ b/ui/app/components/token-list.js @@ -3,14 +3,15 @@ const h = require('react-hyperscript') const inherits = require('util').inherits const TokenTracker = require('eth-token-tracker') const TokenCell = require('./token-cell.js') -const contracts = require('eth-contract-metadata') +const normalizeAddress = require('eth-sig-util').normalize -const tokens = [] +const defaultTokens = [] +const contracts = require('eth-contract-metadata') for (const address in contracts) { const contract = contracts[address] if (contract.erc20) { contract.address = address - tokens.push(contract) + defaultTokens.push(contract) } } @@ -18,15 +19,18 @@ module.exports = TokenList inherits(TokenList, Component) function TokenList () { - this.state = { tokens, isLoading: true, network: null } + this.state = { + tokens: [], + isLoading: true, + network: null, + } Component.call(this) } TokenList.prototype.render = function () { const state = this.state const { tokens, isLoading, error } = state - - const { userAddress } = this.props + const { userAddress, network } = this.props if (isLoading) { return this.message('Loading') @@ -37,40 +41,65 @@ TokenList.prototype.render = function () { return this.message('There was a problem loading your token balances.') } - const network = this.props.network - const tokenViews = tokens.map((tokenData) => { tokenData.network = network tokenData.userAddress = userAddress return h(TokenCell, tokenData) }) - return ( + return h('div', [ h('ol', { style: { - height: '302px', + height: '260px', overflowY: 'auto', + display: 'flex', + flexDirection: 'column', }, - }, [h('style', ` - - li.token-cell { - display: flex; - flex-direction: row; - align-items: center; - padding: 10px; - } - - li.token-cell > h3 { - margin-left: 12px; - } - - li.token-cell:hover { - background: white; - cursor: pointer; - } + }, [ + h('style', ` + + li.token-cell { + display: flex; + flex-direction: row; + align-items: center; + padding: 10px; + } + + li.token-cell > h3 { + margin-left: 12px; + } + + li.token-cell:hover { + background: white; + cursor: pointer; + } + + `), + ...tokenViews, + tokenViews.length ? null : this.message('No Tokens Found.'), + ]), + this.addTokenButtonElement(), + ]) +} - `)].concat(tokenViews.length ? tokenViews : this.message('No Tokens Found.'))) - ) +TokenList.prototype.addTokenButtonElement = function () { + return h('div', [ + h('div.footer.hover-white.pointer', { + key: 'reveal-account-bar', + onClick: () => { + this.props.addToken() + }, + style: { + display: 'flex', + height: '40px', + padding: '10px', + justifyContent: 'center', + alignItems: 'center', + }, + }, [ + h('i.fa.fa-plus.fa-lg'), + ]), + ]) } TokenList.prototype.message = function (body) { @@ -80,6 +109,7 @@ TokenList.prototype.message = function (body) { height: '250px', alignItems: 'center', justifyContent: 'center', + padding: '30px', }, }, body) } @@ -101,7 +131,7 @@ TokenList.prototype.createFreshTokenTracker = function () { this.tracker = new TokenTracker({ userAddress, provider: global.ethereumProvider, - tokens: tokens, + tokens: uniqueMergeTokens(defaultTokens, this.props.tokens), pollingInterval: 8000, }) @@ -135,8 +165,10 @@ TokenList.prototype.componentWillUpdate = function (nextProps) { } } -TokenList.prototype.updateBalances = function (tokenData) { - const heldTokens = tokenData.filter(token => token.balance !== '0' && token.string !== '0.000') +TokenList.prototype.updateBalances = function (tokens) { + const heldTokens = tokens.filter(token => { + return token.balance !== '0' && token.string !== '0.000' + }) this.setState({ tokens: heldTokens, isLoading: false }) } @@ -145,3 +177,16 @@ TokenList.prototype.componentWillUnmount = function () { this.tracker.stop() } +function uniqueMergeTokens (tokensA, tokensB) { + const uniqueAddresses = [] + const result = [] + tokensA.concat(tokensB).forEach((token) => { + const normal = normalizeAddress(token.address) + if (!uniqueAddresses.includes(normal)) { + uniqueAddresses.push(normal) + result.push(token) + } + }) + return result +} + diff --git a/ui/app/config.js b/ui/app/config.js index d7be26757..62785c49b 100644 --- a/ui/app/config.js +++ b/ui/app/config.js @@ -5,6 +5,7 @@ const connect = require('react-redux').connect const actions = require('./actions') const currencies = require('./conversion.json').rows const validUrl = require('valid-url') +const copyToClipboard = require('copy-to-clipboard') module.exports = connect(mapStateToProps)(ConfigScreen) @@ -85,8 +86,35 @@ ConfigScreen.prototype.render = function () { }, }, 'Save'), ]), + h('hr.horizontal-line'), + currentConversionInformation(metamaskState, state), + + h('hr.horizontal-line'), + + h('div', { + style: { + marginTop: '20px', + }, + }, [ + h('p', { + style: { + fontFamily: 'Montserrat Light', + fontSize: '13px', + }, + }, `State logs contain your public account addresses and sent transactions.`), + h('br'), + h('button', { + style: { + alignSelf: 'center', + }, + onClick (event) { + copyToClipboard(window.logState()) + }, + }, 'Copy State Logs'), + ]), + h('hr.horizontal-line'), h('div', { diff --git a/ui/app/keychains/hd/create-vault-complete.js b/ui/app/keychains/hd/create-vault-complete.js index 9741155f7..a318a9b50 100644 --- a/ui/app/keychains/hd/create-vault-complete.js +++ b/ui/app/keychains/hd/create-vault-complete.js @@ -20,7 +20,7 @@ function mapStateToProps (state) { CreateVaultCompleteScreen.prototype.render = function () { var state = this.props - var seed = state.seed || state.cachedSeed + var seed = state.seed || state.cachedSeed || '' return ( diff --git a/ui/app/reducers/app.js b/ui/app/reducers/app.js index deacad0a7..2fcc9bfe0 100644 --- a/ui/app/reducers/app.js +++ b/ui/app/reducers/app.js @@ -103,7 +103,17 @@ function reduceApp (state, action) { transForward: action.value, }) + case actions.SHOW_ADD_TOKEN_PAGE: + return extend(appState, { + currentView: { + name: 'add-token', + context: appState.currentView.context, + }, + transForward: action.value, + }) + case actions.SHOW_IMPORT_PAGE: + return extend(appState, { currentView: { name: 'import-menu', diff --git a/ui/app/send.js b/ui/app/send.js index fd6994145..a21a219eb 100644 --- a/ui/app/send.js +++ b/ui/app/send.js @@ -189,7 +189,7 @@ SendTransactionScreen.prototype.render = function () { style: { textTransform: 'uppercase', }, - }, 'Send'), + }, 'Next'), ]), @@ -244,7 +244,7 @@ SendTransactionScreen.prototype.recipientDidChange = function (recipient, nickna SendTransactionScreen.prototype.onSubmit = function () { const state = this.state || {} - const recipient = state.recipient || document.querySelector('input[name="address"]').value + const recipient = state.recipient || document.querySelector('input[name="address"]').value.replace(/^[.\s]+|[.\s]+$/g, '') const nickname = state.nickname || ' ' const input = document.querySelector('input[name="amount"]').value const value = util.normalizeEthStringToWei(input) |