Implement asset page (#8696)
A new page has been created for viewing assets. This replaces the old `selectedToken` state, which previously would augment the home page to show token-specific information. The new asset page shows the standard token overview as seen previously on the home page, plus a history filtered to show just transactions relevant to that token. The actions that were available in the old token list menu have been moved to a "Token Options" menu that mirrors the "Account Options" menu. The `selectedTokenAddress` state has been removed, as it is no longer being used for anything. `getMetaMetricState` has been renamed to `getBackgroundMetaMetricState` because its sole purpose is extracting data from the background state to send metrics from the background. It's not really a selector, but it was convenient for it to use the same selectors the UI uses to extract background data, so I left it there for now. A new Redux store has been added to track state related to browser history. The most recent "overview" page (i.e. the home page or the asset page) is currently being tracked, so that actions taken from the asset page can return the user back to the asset page when the action has finished.feature/default_network_editable
parent
ec2e5c848b
commit
df85ab6e10
@ -1,67 +0,0 @@ |
||||
import PropTypes from 'prop-types' |
||||
import React, { Component } from 'react' |
||||
import { connect } from 'react-redux' |
||||
import * as actions from '../../../store/actions' |
||||
import { createAccountLink as genAccountLink } from '@metamask/etherscan-link' |
||||
import { Menu, Item, CloseArea } from './components/menu' |
||||
|
||||
class TokenMenuDropdown extends Component { |
||||
static contextTypes = { |
||||
t: PropTypes.func, |
||||
} |
||||
|
||||
static propTypes = { |
||||
onClose: PropTypes.func.isRequired, |
||||
showHideTokenConfirmationModal: PropTypes.func.isRequired, |
||||
token: PropTypes.object.isRequired, |
||||
network: PropTypes.string.isRequired, |
||||
} |
||||
|
||||
onClose = (e) => { |
||||
e.stopPropagation() |
||||
this.props.onClose() |
||||
} |
||||
|
||||
render () { |
||||
const { showHideTokenConfirmationModal } = this.props |
||||
|
||||
return ( |
||||
<Menu className="token-menu-dropdown" isShowing> |
||||
<CloseArea onClick={this.onClose} /> |
||||
<Item |
||||
onClick={(e) => { |
||||
e.stopPropagation() |
||||
showHideTokenConfirmationModal(this.props.token) |
||||
this.props.onClose() |
||||
}} |
||||
text={this.context.t('hideToken')} |
||||
/> |
||||
<Item |
||||
onClick={(e) => { |
||||
e.stopPropagation() |
||||
const url = genAccountLink(this.props.token.address, this.props.network) |
||||
global.platform.openTab({ url }) |
||||
this.props.onClose() |
||||
}} |
||||
text={this.context.t('viewOnEtherscan')} |
||||
/> |
||||
</Menu> |
||||
) |
||||
} |
||||
} |
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(TokenMenuDropdown) |
||||
|
||||
function mapStateToProps (state) { |
||||
return { |
||||
network: state.metamask.network, |
||||
} |
||||
} |
||||
|
||||
function mapDispatchToProps (dispatch) { |
||||
return { |
||||
showHideTokenConfirmationModal: (token) => { |
||||
dispatch(actions.showModal({ name: 'HIDE_TOKEN_CONFIRMATION', token })) |
||||
}, |
||||
} |
||||
} |
@ -0,0 +1,38 @@ |
||||
import { createSlice } from '@reduxjs/toolkit' |
||||
|
||||
import { ASSET_ROUTE, DEFAULT_ROUTE } from '../../helpers/constants/routes' |
||||
|
||||
// Constants
|
||||
|
||||
const initialState = { |
||||
mostRecentOverviewPage: DEFAULT_ROUTE, |
||||
} |
||||
|
||||
const name = 'history' |
||||
|
||||
// Slice (reducer plus auto-generated actions and action creators)
|
||||
|
||||
const slice = createSlice({ |
||||
name, |
||||
initialState, |
||||
reducers: { |
||||
pageChanged: (state, action) => { |
||||
const path = action.payload |
||||
if (path === DEFAULT_ROUTE || path.startsWith(ASSET_ROUTE)) { |
||||
state.mostRecentOverviewPage = path |
||||
} |
||||
}, |
||||
}, |
||||
}) |
||||
|
||||
const { actions, reducer } = slice |
||||
|
||||
export default reducer |
||||
|
||||
// Selectors
|
||||
|
||||
export const getMostRecentOverviewPage = (state) => state[name].mostRecentOverviewPage |
||||
|
||||
// Actions / action-creators
|
||||
|
||||
export const { pageChanged } = actions |
@ -0,0 +1,65 @@ |
||||
import React from 'react' |
||||
import { useDispatch, useSelector } from 'react-redux' |
||||
import { Redirect, useHistory, useParams } from 'react-router-dom' |
||||
import { createAccountLink } from '@metamask/etherscan-link' |
||||
|
||||
import TransactionList from '../../components/app/transaction-list' |
||||
import { EthOverview, TokenOverview } from '../../components/app/wallet-overview' |
||||
import { getCurrentNetworkId, getSelectedIdentity } from '../../selectors/selectors' |
||||
import { getTokens } from '../../ducks/metamask/metamask' |
||||
import { DEFAULT_ROUTE } from '../../helpers/constants/routes' |
||||
import { showModal } from '../../store/actions' |
||||
|
||||
import AssetNavigation from './components/asset-navigation' |
||||
import TokenOptions from './components/token-options' |
||||
|
||||
const Asset = () => { |
||||
const dispatch = useDispatch() |
||||
const network = useSelector(getCurrentNetworkId) |
||||
const selectedAccountName = useSelector((state) => getSelectedIdentity(state).name) |
||||
const nativeCurrency = useSelector((state) => state.metamask.nativeCurrency) |
||||
const tokens = useSelector(getTokens) |
||||
const history = useHistory() |
||||
const { asset } = useParams() |
||||
|
||||
const token = tokens.find((token) => token.address === asset) |
||||
|
||||
let assetName |
||||
let optionsButton |
||||
|
||||
if (token) { |
||||
assetName = token.symbol |
||||
optionsButton = ( |
||||
<TokenOptions |
||||
onRemove={() => dispatch(showModal({ name: 'HIDE_TOKEN_CONFIRMATION', token }))} |
||||
onViewEtherscan={() => { |
||||
const url = createAccountLink(token.address, network) |
||||
global.platform.openTab({ url }) |
||||
}} |
||||
tokenSymbol={token.symbol} |
||||
/> |
||||
) |
||||
} else if (asset === nativeCurrency) { |
||||
assetName = nativeCurrency |
||||
} else { |
||||
return <Redirect to={{ pathname: DEFAULT_ROUTE }} /> |
||||
} |
||||
|
||||
const overview = token |
||||
? <TokenOverview className="asset__overview" token={token} /> |
||||
: <EthOverview className="asset__overview" /> |
||||
return ( |
||||
<div className="main-container asset__container"> |
||||
<AssetNavigation |
||||
accountName={selectedAccountName} |
||||
assetName={assetName} |
||||
onBack={() => history.push(DEFAULT_ROUTE)} |
||||
optionsButton={optionsButton} |
||||
/> |
||||
{ overview } |
||||
<TransactionList tokenAddress={token?.address} /> |
||||
</div> |
||||
) |
||||
} |
||||
|
||||
export default Asset |
@ -0,0 +1,45 @@ |
||||
.asset { |
||||
&__container { |
||||
background-color: white; |
||||
} |
||||
|
||||
&__overview { |
||||
box-shadow: 0px 3px 4px rgba(135, 134, 134, 0.16); |
||||
} |
||||
} |
||||
|
||||
.asset-navigation { |
||||
display: flex; |
||||
justify-content: space-between; |
||||
align-items: center; |
||||
padding: 16px; |
||||
height: 54px; |
||||
} |
||||
|
||||
.asset-breadcrumb { |
||||
font-size: 14px; |
||||
color: $Black-100; |
||||
|
||||
&__chevron { |
||||
padding: 0 10px 0 2px; |
||||
font-size: 16px; |
||||
background-color: inherit; |
||||
} |
||||
|
||||
&__asset { |
||||
font-weight: bold; |
||||
} |
||||
} |
||||
|
||||
.token-options { |
||||
&__button { |
||||
font-size: 20px; |
||||
color: $Black-100; |
||||
background-color: inherit; |
||||
padding: 2px 8px; |
||||
} |
||||
|
||||
&__icon { |
||||
font-size: 16px; |
||||
} |
||||
} |
@ -0,0 +1,25 @@ |
||||
import React from 'react' |
||||
import PropTypes from 'prop-types' |
||||
|
||||
const AssetBreadcrumb = ({ accountName, assetName, onBack }) => { |
||||
return ( |
||||
<div className="asset-breadcrumb"> |
||||
<button className="fas fa-chevron-left asset-breadcrumb__chevron" data-testid="asset__back" onClick={onBack} /> |
||||
<span> |
||||
{accountName} |
||||
</span> |
||||
/ |
||||
<span className="asset-breadcrumb__asset"> |
||||
{ assetName } |
||||
</span> |
||||
</div> |
||||
) |
||||
} |
||||
|
||||
AssetBreadcrumb.propTypes = { |
||||
accountName: PropTypes.string.isRequired, |
||||
assetName: PropTypes.string.isRequired, |
||||
onBack: PropTypes.func.isRequired, |
||||
} |
||||
|
||||
export default AssetBreadcrumb |
@ -0,0 +1,26 @@ |
||||
import React from 'react' |
||||
import PropTypes from 'prop-types' |
||||
|
||||
import AssetBreadcrumb from './asset-breadcrumb' |
||||
|
||||
const AssetNavigation = ({ accountName, assetName, onBack, optionsButton }) => { |
||||
return ( |
||||
<div className="asset-navigation"> |
||||
<AssetBreadcrumb accountName={accountName} assetName={assetName} onBack={onBack} /> |
||||
{ optionsButton } |
||||
</div> |
||||
) |
||||
} |
||||
|
||||
AssetNavigation.propTypes = { |
||||
accountName: PropTypes.string.isRequired, |
||||
assetName: PropTypes.string.isRequired, |
||||
onBack: PropTypes.func.isRequired, |
||||
optionsButton: PropTypes.element, |
||||
} |
||||
|
||||
AssetNavigation.defaultProps = { |
||||
optionsButton: undefined, |
||||
} |
||||
|
||||
export default AssetNavigation |
@ -0,0 +1,59 @@ |
||||
import React, { useContext, useState } from 'react' |
||||
import PropTypes from 'prop-types' |
||||
|
||||
import { I18nContext } from '../../../contexts/i18n' |
||||
import { Menu, MenuItem } from '../../../components/ui/menu' |
||||
|
||||
const TokenOptions = ({ onRemove, onViewEtherscan, tokenSymbol }) => { |
||||
const t = useContext(I18nContext) |
||||
const [tokenOptionsButtonElement, setTokenOptionsButtonElement] = useState(null) |
||||
const [tokenOptionsOpen, setTokenOptionsOpen] = useState(false) |
||||
|
||||
return ( |
||||
<> |
||||
<button |
||||
className="fas fa-ellipsis-v token-options__button" |
||||
data-testid="token-options__button" |
||||
onClick={() => setTokenOptionsOpen(true)} |
||||
ref={setTokenOptionsButtonElement} |
||||
title={t('tokenOptions')} |
||||
/> |
||||
{ |
||||
tokenOptionsOpen |
||||
? ( |
||||
<Menu anchorElement={tokenOptionsButtonElement} onHide={() => setTokenOptionsOpen(false)} > |
||||
<MenuItem |
||||
iconClassName="fas fa-external-link-alt token-options__icon" |
||||
data-testid="token-options__etherscan" |
||||
onClick={() => { |
||||
setTokenOptionsOpen(false) |
||||
onViewEtherscan() |
||||
}} |
||||
> |
||||
{ t('viewOnEtherscan') } |
||||
</MenuItem> |
||||
<MenuItem |
||||
iconClassName="fas fa-trash-alt token-options__icon" |
||||
data-testid="token-options__hide" |
||||
onClick={() => { |
||||
setTokenOptionsOpen(false) |
||||
onRemove() |
||||
}} |
||||
> |
||||
{ t('hideTokenSymbol', [tokenSymbol]) } |
||||
</MenuItem> |
||||
</Menu> |
||||
) |
||||
: null |
||||
} |
||||
</> |
||||
) |
||||
} |
||||
|
||||
TokenOptions.propTypes = { |
||||
onRemove: PropTypes.func.isRequired, |
||||
onViewEtherscan: PropTypes.func.isRequired, |
||||
tokenSymbol: PropTypes.string.isRequired, |
||||
} |
||||
|
||||
export default TokenOptions |
@ -0,0 +1 @@ |
||||
export { default } from './asset' |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue