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