From f1fb9e10a06d1811d97f61b6369684979b7ecf70 Mon Sep 17 00:00:00 2001 From: Chi Kei Chan Date: Wed, 6 Sep 2017 03:17:49 -0700 Subject: [PATCH] Adding Token transaction detail screen --- ui/app/actions.js | 9 ++ ui/app/components/balance-component.js | 28 ++++- ui/app/components/token-balance.js | 104 ++++++++++++++++++ ui/app/components/token-cell.js | 25 ++++- ui/app/components/token-list.js | 10 +- ui/app/components/tx-view.js | 75 +++++++------ ui/app/components/wallet-view.js | 48 ++++---- ui/app/css/itcss/components/token-list.scss | 13 +++ .../css/itcss/components/wallet-balance.scss | 10 +- ui/app/reducers/metamask.js | 6 + ui/app/selectors.js | 9 ++ 11 files changed, 256 insertions(+), 81 deletions(-) create mode 100644 ui/app/components/token-balance.js diff --git a/ui/app/actions.js b/ui/app/actions.js index 312c1106d..fe6048aa2 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -83,6 +83,8 @@ var actions = { hideWarning: hideWarning, // accounts screen SET_SELECTED_ACCOUNT: 'SET_SELECTED_ACCOUNT', + SET_SELECTED_TOKEN: 'SET_SELECTED_TOKEN', + setSelectedToken, SHOW_ACCOUNT_DETAIL: 'SHOW_ACCOUNT_DETAIL', SHOW_ACCOUNTS_PAGE: 'SHOW_ACCOUNTS_PAGE', SHOW_CONF_TX_PAGE: 'SHOW_CONF_TX_PAGE', @@ -585,6 +587,13 @@ function setCurrentAccountTab (newTabName) { return callBackgroundThenUpdateNoSpinner(background.setCurrentAccountTab, newTabName) } +function setSelectedToken (tokenAddress) { + return { + type: actions.SET_SELECTED_TOKEN, + value: tokenAddress || null, + } +} + function showAccountDetail (address) { return (dispatch) => { dispatch(actions.showLoadingIndication()) diff --git a/ui/app/components/balance-component.js b/ui/app/components/balance-component.js index 48efc7b6a..6b997944f 100644 --- a/ui/app/components/balance-component.js +++ b/ui/app/components/balance-component.js @@ -2,13 +2,19 @@ const Component = require('react').Component const connect = require('react-redux').connect const h = require('react-hyperscript') const inherits = require('util').inherits +const TokenBalance = require('./token-balance') const { formatBalance, generateBalanceObject } = require('../util') module.exports = connect(mapStateToProps)(BalanceComponent) function mapStateToProps (state) { + const accounts = state.metamask.accounts + const selectedAddress = state.metamask.selectedAddress || Object.keys(accounts)[0] + const account = accounts[selectedAddress] + return { + account, conversionRate: state.metamask.conversionRate, currentCurrency: state.metamask.currentCurrency, } @@ -21,9 +27,8 @@ function BalanceComponent () { BalanceComponent.prototype.render = function () { const props = this.props - const { balanceValue } = props - const needsParse = 'needsParse' in props ? props.needsParse : true - const formattedBalance = balanceValue ? formatBalance(balanceValue, 6, needsParse) : '...' + // const { balanceValue } = props + const { token } = props return h('div.balance-container', {}, [ @@ -33,13 +38,24 @@ BalanceComponent.prototype.render = function () { style: {}, }), - this.renderBalance(formattedBalance), + token ? this.renderTokenBalance() : this.renderBalance(), ]) } -BalanceComponent.prototype.renderBalance = function (formattedBalance) { +BalanceComponent.prototype.renderTokenBalance = function () { + const { token } = this.props + + return h('div.flex-column.balance-display', [ + h('div.token-amount', [ h(TokenBalance, { token }) ]), + ]) +} + +BalanceComponent.prototype.renderBalance = function () { const props = this.props - const { shorten } = props + const { shorten, account } = props + const balanceValue = account && account.balance + const needsParse = 'needsParse' in props ? props.needsParse : true + const formattedBalance = balanceValue ? formatBalance(balanceValue, 6, needsParse) : '...' const showFiat = 'showFiat' in props ? props.showFiat : true if (formattedBalance === 'None' || formattedBalance === '...') { diff --git a/ui/app/components/token-balance.js b/ui/app/components/token-balance.js new file mode 100644 index 000000000..0757cc65c --- /dev/null +++ b/ui/app/components/token-balance.js @@ -0,0 +1,104 @@ +const Component = require('react').Component +const h = require('react-hyperscript') +const inherits = require('util').inherits +const TokenTracker = require('eth-token-tracker') +const connect = require('react-redux').connect +const selectors = require('../selectors') + +function mapStateToProps (state) { + return { + userAddress: selectors.getSelectedAddress(state), + } +} + +module.exports = connect(mapStateToProps)(TokenBalance) + + +inherits(TokenBalance, Component) +function TokenBalance () { + this.state = { + balance: '', + isLoading: true, + error: null, + } + Component.call(this) +} + +TokenBalance.prototype.render = function () { + const state = this.state + const { balance, isLoading } = state + + return isLoading + ? h('span', '') + : h('span', balance) +} + +TokenBalance.prototype.componentDidMount = function () { + this.createFreshTokenTracker() +} + +TokenBalance.prototype.createFreshTokenTracker = function () { + if (this.tracker) { + // Clean up old trackers when refreshing: + this.tracker.stop() + this.tracker.removeListener('update', this.balanceUpdater) + this.tracker.removeListener('error', this.showError) + } + + if (!global.ethereumProvider) return + const { userAddress, token } = this.props + + this.tracker = new TokenTracker({ + userAddress, + provider: global.ethereumProvider, + tokens: [token], + pollingInterval: 8000, + }) + + + // Set up listener instances for cleaning up + this.balanceUpdater = this.updateBalance.bind(this) + this.showError = error => { + this.setState({ error, isLoading: false }) + } + this.tracker.on('update', this.balanceUpdater) + this.tracker.on('error', this.showError) + + this.tracker.updateBalances() + .then(() => { + this.updateBalance(this.tracker.serialize()) + }) + .catch((reason) => { + log.error(`Problem updating balances`, reason) + this.setState({ isLoading: false }) + }) +} + +TokenBalance.prototype.componentDidUpdate = function (nextProps) { + const { + userAddress: oldAddress, + } = this.props + const { + userAddress: newAddress, + } = nextProps + + if (!oldAddress || !newAddress) return + if (oldAddress === newAddress) return + + this.setState({ isLoading: true }) + this.createFreshTokenTracker() +} + +TokenBalance.prototype.updateBalance = function (tokens = []) { + const [{ string }] = tokens + this.setState({ + balance: string, + isLoading: false, + }) +} + +TokenBalance.prototype.componentWillUnmount = function () { + if (!this.tracker) return + this.tracker.stop() +} + diff --git a/ui/app/components/token-cell.js b/ui/app/components/token-cell.js index a24e4e1ac..7fae67de6 100644 --- a/ui/app/components/token-cell.js +++ b/ui/app/components/token-cell.js @@ -1,10 +1,27 @@ const Component = require('react').Component const h = require('react-hyperscript') const inherits = require('util').inherits +const connect = require('react-redux').connect const Identicon = require('./identicon') const prefixForNetwork = require('../../lib/etherscan-prefix-for-network') +const selectors = require('../selectors') +const actions = require('../actions') -module.exports = TokenCell +function mapStateToProps (state) { + return { + network: state.metamask.network, + selectedTokenAddress: state.metamask.selectedTokenAddress, + userAddress: selectors.getSelectedAddress(state), + } +} + +function mapDispatchToProps (dispatch) { + return { + setSelectedToken: address => dispatch(actions.setSelectedToken(address)), + } +} + +module.exports = connect(mapStateToProps, mapDispatchToProps)(TokenCell) inherits(TokenCell, Component) function TokenCell () { @@ -18,13 +35,17 @@ TokenCell.prototype.render = function () { symbol, string, network, + setSelectedToken, + selectedTokenAddress, // userAddress, } = props return ( h('div.token-list-item', { - style: { cursor: network === '1' ? 'pointer' : 'default' }, + className: `token-list-item ${selectedTokenAddress ? 'token-list-item--active' : ''}`, + // style: { cursor: network === '1' ? 'pointer' : 'default' }, // onClick: this.view.bind(this, address, userAddress, network), + onClick: () => setSelectedToken(address), }, [ h(Identicon, { diff --git a/ui/app/components/token-list.js b/ui/app/components/token-list.js index fea87a733..2d1dd0ea7 100644 --- a/ui/app/components/token-list.js +++ b/ui/app/components/token-list.js @@ -8,7 +8,6 @@ const connect = require('react-redux').connect const selectors = require('../selectors') function mapStateToProps (state) { - return { network: state.metamask.network, tokens: state.metamask.tokens, @@ -42,7 +41,6 @@ function TokenList () { TokenList.prototype.render = function () { const state = this.state const { tokens, isLoading, error } = state - const { userAddress, network } = this.props if (isLoading) { return this.message('Loading Tokens...') @@ -53,13 +51,7 @@ TokenList.prototype.render = function () { return this.message('There was a problem loading your token balances.') } - const tokenViews = tokens.map((tokenData) => { - tokenData.network = network - tokenData.userAddress = userAddress - return h(TokenCell, tokenData) - }) - - return h('div', tokenViews) + return h('div', tokens.map((tokenData) => h(TokenCell, tokenData))) } TokenList.prototype.message = function (body) { diff --git a/ui/app/components/tx-view.js b/ui/app/components/tx-view.js index 9f75f7b31..d7e4a5b4b 100644 --- a/ui/app/components/tx-view.js +++ b/ui/app/components/tx-view.js @@ -4,10 +4,12 @@ const h = require('react-hyperscript') const ethUtil = require('ethereumjs-util') const inherits = require('util').inherits const actions = require('../actions') +const selectors = require('../selectors') const BalanceComponent = require('./balance-component') const TxList = require('./tx-list') const Identicon = require('./identicon') +const TokenBalance = require('./token-balance') module.exports = connect(mapStateToProps, mapDispatchToProps)(TxView) @@ -16,6 +18,7 @@ function mapStateToProps (state) { const identities = state.metamask.identities const accounts = state.metamask.accounts + const selectedTokenAddress = state.metamask.selectedTokenAddress const selectedAddress = state.metamask.selectedAddress || Object.keys(accounts)[0] const checksumAddress = selectedAddress && ethUtil.toChecksumAddress(selectedAddress) const identity = identities[selectedAddress] @@ -25,6 +28,8 @@ function mapStateToProps (state) { sidebarOpen, selectedAddress, checksumAddress, + selectedTokenAddress, + selectedToken: selectors.getSelectedToken(state), identity, account, } @@ -44,9 +49,41 @@ function TxView () { Component.call(this) } +TxView.prototype.renderHeroBalance = function () { + const {account, selectedToken, showModal, showSendPage } = this.props + + return h('div.hero-balance', {}, [ + + h(BalanceComponent, { + balanceValue: account && account.balance, + token: selectedToken, + }), + + h('div.flex-row.flex-center.hero-balance-buttons', {}, [ + h('button.btn-clear', { + style: { + textAlign: 'center', + }, + onClick: () => showModal({ + name: 'BUY', + }), + }, 'BUY'), + + h('button.btn-clear', { + style: { + textAlign: 'center', + marginLeft: '0.8em', + }, + onClick: showSendPage, + }, 'SEND'), + + ]), + ]) +} + TxView.prototype.render = function () { - const { selectedAddress, identity, account } = this.props + const { selectedAddress, identity } = this.props return h('div.tx-view.flex-column', { style: {}, @@ -87,41 +124,7 @@ TxView.prototype.render = function () { ]), - h('div.hero-balance', { - style: {}, - }, [ - - h(BalanceComponent, { - balanceValue: account && account.balance, - style: {}, - }), - - h('div.flex-row.flex-center.hero-balance-buttons', { - style: {}, - }, [ - h('button.btn-clear', { - style: { - textAlign: 'center', - }, - onClick: () => { - this.props.showModal({ - name: 'BUY', - }) - }, - }, 'BUY'), - - h('button.btn-clear', { - style: { - textAlign: 'center', - marginLeft: '0.8em', - }, - onClick: () => { - this.props.showSendPage() - }, - }, 'SEND'), - - ]), - ]), + this.renderHeroBalance(), h(TxList, {}), diff --git a/ui/app/components/wallet-view.js b/ui/app/components/wallet-view.js index 304b5daba..f9710ea4c 100644 --- a/ui/app/components/wallet-view.js +++ b/ui/app/components/wallet-view.js @@ -22,6 +22,7 @@ function mapStateToProps (state) { selectedAddress: selectors.getSelectedAddress(state), selectedIdentity: selectors.getSelectedIdentity(state), selectedAccount: selectors.getSelectedAccount(state), + selectedTokenAddress: state.metamask.selectedTokenAddress, } } @@ -29,6 +30,7 @@ function mapDispatchToProps (dispatch) { return { showSendPage: () => { dispatch(actions.showSendPage()) }, hideSidebar: () => { dispatch(actions.hideSidebar()) }, + unsetSelectedToken: () => dispatch(actions.setSelectedToken()), } } @@ -37,15 +39,26 @@ function WalletView () { Component.call(this) } -WalletView.prototype.renderTokenBalances = function () { - // const { tokens = [] } = this.props - // return tokens.map(({ address, decimals, symbol }) => ( - // h(BalanceComponent, { - // balanceValue: 0, - // style: {}, - // }) - // )) - return h(TokenList) +WalletView.prototype.renderWalletBalance = function () { + const { selectedTokenAddress, selectedAccount, unsetSelectedToken } = this.props + const selectedClass = selectedTokenAddress + ? '' + : 'wallet-balance-wrapper--active' + const className = `flex-column wallet-balance-wrapper ${selectedClass}` + + return h('div', { className }, [ + h('div.wallet-balance', + { + onClick: () => unsetSelectedToken(), + }, + [ + h(BalanceComponent, { + balanceValue: selectedAccount.balance, + style: {}, + }), + ] + ), + ]) } WalletView.prototype.render = function () { @@ -139,22 +152,9 @@ WalletView.prototype.render = function () { ]), ]), - // Wallet Balances - h('div.flex-column.wallet-balance-wrapper.wallet-balance-wrapper-active', {}, [ + this.renderWalletBalance(), - h('div.wallet-balance', {}, [ - - h(BalanceComponent, { - balanceValue: selectedAccount.balance, - style: {}, - }), - - ]), - - - ]), - - this.renderTokenBalances(), + h(TokenList), ]) } diff --git a/ui/app/css/itcss/components/token-list.scss b/ui/app/css/itcss/components/token-list.scss index dd1d533c7..2195dc1b8 100644 --- a/ui/app/css/itcss/components/token-list.scss +++ b/ui/app/css/itcss/components/token-list.scss @@ -1,9 +1,22 @@ +$wallet-balance-breakpoint: 890px; +$wallet-balance-breakpoint-range: "screen and (min-width: #{$break-large}) and (max-width: #{$wallet-balance-breakpoint})"; + .token-list-item { display: flex; flex-flow: row nowrap; align-items: center; padding: 20px 24px; cursor: pointer; + transition: linear 200ms; + background-color: rgba($wallet-balance-bg, 0); + + @media #{$wallet-balance-breakpoint-range} { + padding: 10% 4%; + } + + &--active { + background-color: rgba($wallet-balance-bg, 1); + } &__identicon { margin-right: 15px; diff --git a/ui/app/css/itcss/components/wallet-balance.scss b/ui/app/css/itcss/components/wallet-balance.scss index 113380769..cd44f89bb 100644 --- a/ui/app/css/itcss/components/wallet-balance.scss +++ b/ui/app/css/itcss/components/wallet-balance.scss @@ -4,6 +4,12 @@ $wallet-balance-breakpoint-range: "screen and (min-width: #{$break-large}) and ( .wallet-balance-wrapper { flex: 0 0 auto; + transition: linear 200ms; + background: rgba($wallet-balance-bg, 0); + + &--active { + background: rgba($wallet-balance-bg, 1); + } } .wallet-balance { @@ -62,7 +68,3 @@ $wallet-balance-breakpoint-range: "screen and (min-width: #{$break-large}) and ( border: 1px solid $alto; } } - -.wallet-balance-wrapper-active { - background: $wallet-balance-bg; -} diff --git a/ui/app/reducers/metamask.js b/ui/app/reducers/metamask.js index e0c416c2d..8d361195d 100644 --- a/ui/app/reducers/metamask.js +++ b/ui/app/reducers/metamask.js @@ -17,6 +17,7 @@ function reduceMetamask (state, action) { lastUnreadNotice: undefined, frequentRpcList: [], addressBook: [], + selectedTokenAddress: null, }, state.metamask) switch (action.type) { @@ -115,6 +116,11 @@ function reduceMetamask (state, action) { delete newState.seedWords return newState + case actions.SET_SELECTED_TOKEN: + return extend(metamaskState, { + selectedTokenAddress: action.value, + }) + case actions.SAVE_ACCOUNT_LABEL: const account = action.value.account const name = action.value.label diff --git a/ui/app/selectors.js b/ui/app/selectors.js index 4ff3e33f2..400f5cd45 100644 --- a/ui/app/selectors.js +++ b/ui/app/selectors.js @@ -4,6 +4,7 @@ const selectors = { getSelectedAddress, getSelectedIdentity, getSelectedAccount, + getSelectedToken, conversionRateSelector, transactionsSelector, } @@ -31,6 +32,14 @@ function getSelectedAccount (state) { return accounts[selectedAddress] } +function getSelectedToken (state) { + const tokens = state.metamask.tokens || [] + const selectedTokenAddress = state.metamask.selectedTokenAddress + const selectedToken = tokens.filter(({ address }) => address === selectedTokenAddress)[0] + + return selectedToken || null +} + function conversionRateSelector (state) { return state.metamask.conversionRate }