mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-23 09:52:26 +01:00
refactor token list (#8726)
Co-authored-by: Mark Stacey <markjstacey@gmail.com>
This commit is contained in:
parent
b96eb55c76
commit
ab06595a5d
@ -1 +1 @@
|
||||
export { default } from './token-list.container'
|
||||
export { default } from './token-list'
|
||||
|
@ -1,148 +0,0 @@
|
||||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import TokenTracker from '@metamask/eth-token-tracker'
|
||||
import { isEqual } from 'lodash'
|
||||
import contracts from 'eth-contract-metadata'
|
||||
|
||||
import { I18nContext } from '../../../contexts/i18n'
|
||||
import TokenCell from '../token-cell'
|
||||
|
||||
const defaultTokens = []
|
||||
for (const address in contracts) {
|
||||
const contract = contracts[address]
|
||||
if (contract.erc20) {
|
||||
contract.address = address
|
||||
defaultTokens.push(contract)
|
||||
}
|
||||
}
|
||||
|
||||
class TokenList extends Component {
|
||||
static contextType = I18nContext
|
||||
|
||||
static propTypes = {
|
||||
assetImages: PropTypes.object.isRequired,
|
||||
network: PropTypes.string.isRequired,
|
||||
onTokenClick: PropTypes.func.isRequired,
|
||||
tokens: PropTypes.array.isRequired,
|
||||
userAddress: PropTypes.string.isRequired,
|
||||
}
|
||||
|
||||
constructor () {
|
||||
super()
|
||||
|
||||
this.state = {
|
||||
error: null,
|
||||
tokensLoading: false,
|
||||
tokensWithBalances: [],
|
||||
}
|
||||
}
|
||||
|
||||
constructTokenTracker () {
|
||||
const { network, tokens, userAddress } = this.props
|
||||
if (!tokens || !tokens.length) {
|
||||
this.setState({
|
||||
tokensLoading: false,
|
||||
tokensWithBalances: [],
|
||||
})
|
||||
return
|
||||
}
|
||||
this.setState({ tokensLoading: true })
|
||||
|
||||
if (!userAddress || network === 'loading' || !global.ethereumProvider) {
|
||||
return
|
||||
}
|
||||
|
||||
const updateBalances = (tokensWithBalances) => {
|
||||
this.setState({
|
||||
error: null,
|
||||
tokensLoading: false,
|
||||
tokensWithBalances,
|
||||
})
|
||||
}
|
||||
const showError = (error) => {
|
||||
this.setState({
|
||||
error,
|
||||
tokensLoading: false,
|
||||
})
|
||||
}
|
||||
|
||||
this.tokenTracker = new TokenTracker({
|
||||
userAddress,
|
||||
provider: global.ethereumProvider,
|
||||
tokens: tokens,
|
||||
pollingInterval: 8000,
|
||||
})
|
||||
|
||||
this.tokenTracker.on('update', updateBalances)
|
||||
this.tokenTracker.on('error', showError)
|
||||
this.tokenTracker.updateBalances()
|
||||
}
|
||||
|
||||
stopTokenTracker () {
|
||||
if (this.tokenTracker) {
|
||||
this.tokenTracker.stop()
|
||||
this.tokenTracker.removeAllListeners('update')
|
||||
this.tokenTracker.removeAllListeners('error')
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.constructTokenTracker()
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
const { network, tokens, userAddress } = this.props
|
||||
if (
|
||||
isEqual(tokens, prevProps.tokens) &&
|
||||
userAddress === prevProps.userAddress &&
|
||||
network === prevProps.network
|
||||
) {
|
||||
return
|
||||
}
|
||||
this.stopTokenTracker()
|
||||
this.constructTokenTracker()
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
this.stopTokenTracker()
|
||||
}
|
||||
|
||||
render () {
|
||||
const t = this.context
|
||||
const { error, tokensLoading, tokensWithBalances } = this.state
|
||||
const { assetImages, network, onTokenClick } = this.props
|
||||
if (network === 'loading' || tokensLoading) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
height: '250px',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: '30px',
|
||||
}}
|
||||
>
|
||||
{t('loadingTokens')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{tokensWithBalances.map((tokenData, index) => {
|
||||
tokenData.image = assetImages[tokenData.address]
|
||||
return (
|
||||
<TokenCell
|
||||
key={index}
|
||||
{...tokenData}
|
||||
outdatedBalance={Boolean(error)}
|
||||
onClick={onTokenClick}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default TokenList
|
@ -1,21 +0,0 @@
|
||||
import { connect } from 'react-redux'
|
||||
import PropTypes from 'prop-types'
|
||||
import { getSelectedAddress } from '../../../selectors'
|
||||
import TokenList from './token-list.component'
|
||||
|
||||
function mapStateToProps (state) {
|
||||
return {
|
||||
network: state.metamask.network,
|
||||
tokens: state.metamask.tokens,
|
||||
userAddress: getSelectedAddress(state),
|
||||
assetImages: state.metamask.assetImages,
|
||||
}
|
||||
}
|
||||
|
||||
const TokenListContainer = connect(mapStateToProps)(TokenList)
|
||||
|
||||
TokenListContainer.propTypes = {
|
||||
onTokenClick: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
export default TokenListContainer
|
66
ui/app/components/app/token-list/token-list.js
Normal file
66
ui/app/components/app/token-list/token-list.js
Normal file
@ -0,0 +1,66 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import contracts from 'eth-contract-metadata'
|
||||
import { isEqual } from 'lodash'
|
||||
|
||||
import TokenCell from '../token-cell'
|
||||
import { useI18nContext } from '../../../hooks/useI18nContext'
|
||||
import { useTokenTracker } from '../../../hooks/useTokenTracker'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { getAssetImages } from '../../../selectors'
|
||||
import { getTokens } from '../../../ducks/metamask/metamask'
|
||||
|
||||
const defaultTokens = []
|
||||
for (const address in contracts) {
|
||||
const contract = contracts[address]
|
||||
if (contract.erc20) {
|
||||
contract.address = address
|
||||
defaultTokens.push(contract)
|
||||
}
|
||||
}
|
||||
|
||||
export default function TokenList ({ onTokenClick }) {
|
||||
const t = useI18nContext()
|
||||
const assetImages = useSelector(getAssetImages)
|
||||
// use `isEqual` comparison function because the token array is serialized
|
||||
// from the background so it has a new reference with each background update,
|
||||
// even if the tokens haven't changed
|
||||
const tokens = useSelector(getTokens, isEqual)
|
||||
const { loading, error, tokensWithBalances } = useTokenTracker(tokens)
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
height: '250px',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: '30px',
|
||||
}}
|
||||
>
|
||||
{t('loadingTokens')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{tokensWithBalances.map((tokenData, index) => {
|
||||
tokenData.image = assetImages[tokenData.address]
|
||||
return (
|
||||
<TokenCell
|
||||
key={index}
|
||||
{...tokenData}
|
||||
outdatedBalance={Boolean(error)}
|
||||
onClick={onTokenClick}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
TokenList.propTypes = {
|
||||
onTokenClick: PropTypes.func.isRequired,
|
||||
}
|
88
ui/app/hooks/useTokenTracker.js
Normal file
88
ui/app/hooks/useTokenTracker.js
Normal file
@ -0,0 +1,88 @@
|
||||
import { useState, useEffect, useRef, useCallback } from 'react'
|
||||
import TokenTracker from '@metamask/eth-token-tracker'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { getCurrentNetwork, getSelectedAddress } from '../selectors'
|
||||
|
||||
|
||||
export function useTokenTracker (tokens) {
|
||||
const network = useSelector(getCurrentNetwork)
|
||||
const userAddress = useSelector(getSelectedAddress)
|
||||
|
||||
const [loading, setLoading] = useState(() => tokens?.length >= 0)
|
||||
const [tokensWithBalances, setTokensWithBalances] = useState([])
|
||||
const [error, setError] = useState(null)
|
||||
const tokenTracker = useRef(null)
|
||||
|
||||
const updateBalances = useCallback((tokensWithBalances) => {
|
||||
setTokensWithBalances(tokensWithBalances)
|
||||
setLoading(false)
|
||||
setError(null)
|
||||
}, [])
|
||||
|
||||
const showError = useCallback((error) => {
|
||||
setError(error)
|
||||
setLoading(false)
|
||||
}, [])
|
||||
|
||||
const teardownTracker = useCallback(() => {
|
||||
if (tokenTracker.current) {
|
||||
tokenTracker.current.stop()
|
||||
tokenTracker.current.removeAllListeners('update')
|
||||
tokenTracker.current.removeAllListeners('error')
|
||||
tokenTracker.current = null
|
||||
}
|
||||
}, [])
|
||||
|
||||
const buildTracker = useCallback((address, tokenList) => {
|
||||
// clear out previous tracker, if it exists.
|
||||
teardownTracker()
|
||||
tokenTracker.current = new TokenTracker({
|
||||
userAddress: address,
|
||||
provider: global.ethereumProvider,
|
||||
tokens: tokenList,
|
||||
pollingInterval: 8000,
|
||||
})
|
||||
|
||||
tokenTracker.current.on('update', updateBalances)
|
||||
tokenTracker.current.on('error', showError)
|
||||
tokenTracker.current.updateBalances()
|
||||
}, [updateBalances, showError, teardownTracker])
|
||||
|
||||
// Effect to remove the tracker when the component is removed from DOM
|
||||
// Do not overload this effect with additional dependencies. teardownTracker
|
||||
// is the only dependency here, which itself has no dependencies and will
|
||||
// never update. The lack of dependencies that change is what confirms
|
||||
// that this effect only runs on mount/unmount
|
||||
useEffect(() => {
|
||||
return teardownTracker
|
||||
}, [teardownTracker])
|
||||
|
||||
|
||||
// Effect to set loading state and initialize tracker when values change
|
||||
useEffect(() => {
|
||||
// This effect will only run initially and when:
|
||||
// 1. network is updated,
|
||||
// 2. userAddress is changed,
|
||||
// 3. token list is updated and not equal to previous list
|
||||
// in any of these scenarios, we should indicate to the user that their token
|
||||
// values are in the process of updating by setting loading state.
|
||||
setLoading(true)
|
||||
|
||||
if (!userAddress || network === 'loading' || !global.ethereumProvider) {
|
||||
// If we do not have enough information to build a TokenTracker, we exit early
|
||||
// When the values above change, the effect will be restarted. We also teardown
|
||||
// tracker because inevitably this effect will run again momentarily.
|
||||
teardownTracker()
|
||||
return
|
||||
}
|
||||
|
||||
if (tokens.length === 0) {
|
||||
// sets loading state to false and token list to empty
|
||||
updateBalances([])
|
||||
}
|
||||
|
||||
buildTracker(userAddress, tokens)
|
||||
}, [userAddress, network, tokens, updateBalances, buildTracker])
|
||||
|
||||
return { loading, tokensWithBalances, error }
|
||||
}
|
Loading…
Reference in New Issue
Block a user