feat: improve logic around hidden section of mini-portfolio balances spam tokens (#6988)
This commit is contained in:
parent
51dc10b467
commit
485764fe38
@ -18,7 +18,9 @@ describe('Token explore filter', () => {
|
||||
searchFor('dao')
|
||||
|
||||
cy.get('@filteredTokens').then((filteredTokens) => {
|
||||
cy.get('[data-cy="token-name"]').should('deep.equal', filteredTokens)
|
||||
cy.get('[data-cy="token-name"]').then((tokens) => {
|
||||
cy.wrap(Array.from(tokens)).should('deep.equal', Array.from(filteredTokens))
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
File diff suppressed because one or more lines are too long
@ -3,7 +3,7 @@ import { TraceEvent } from 'analytics'
|
||||
import { useCachedPortfolioBalancesQuery } from 'components/AccountDrawer/PrefetchBalancesWrapper'
|
||||
import Row from 'components/Row'
|
||||
import { formatDelta } from 'components/Tokens/TokenDetails/PriceChart'
|
||||
import { PortfolioBalancesQuery } from 'graphql/data/__generated__/types-and-hooks'
|
||||
import { TokenBalance } from 'graphql/data/__generated__/types-and-hooks'
|
||||
import { getTokenDetailsURL, gqlToCurrency, logSentryErrorForUnsupportedChain } from 'graphql/data/util'
|
||||
import { useAtomValue } from 'jotai/utils'
|
||||
import { EmptyWalletModule } from 'nft/components/profile/view/EmptyWalletContent'
|
||||
@ -12,6 +12,7 @@ import { useNavigate } from 'react-router-dom'
|
||||
import styled from 'styled-components'
|
||||
import { EllipsisStyle, ThemedText } from 'theme'
|
||||
import { formatNumber, NumberType } from 'utils/formatNumbers'
|
||||
import { splitHiddenTokens } from 'utils/splitHiddenTokens'
|
||||
|
||||
import { useToggleAccountDrawer } from '../..'
|
||||
import { PortfolioArrow } from '../../AuthenticatedHeader'
|
||||
@ -20,12 +21,6 @@ import { ExpandoRow } from '../ExpandoRow'
|
||||
import { PortfolioLogo } from '../PortfolioLogo'
|
||||
import PortfolioRow, { PortfolioSkeleton, PortfolioTabWrapper } from '../PortfolioRow'
|
||||
|
||||
const HIDE_SMALL_USD_BALANCES_THRESHOLD = 1
|
||||
|
||||
function meetsThreshold(tokenBalance: TokenBalance, hideSmallBalances: boolean) {
|
||||
return !hideSmallBalances || (tokenBalance.denominatedValue?.value ?? 0) > HIDE_SMALL_USD_BALANCES_THRESHOLD
|
||||
}
|
||||
|
||||
export default function Tokens({ account }: { account: string }) {
|
||||
const toggleWalletDrawer = useToggleAccountDrawer()
|
||||
const hideSmallBalances = useAtomValue(hideSmallBalancesAtom)
|
||||
@ -33,27 +28,18 @@ export default function Tokens({ account }: { account: string }) {
|
||||
|
||||
const { data } = useCachedPortfolioBalancesQuery({ account })
|
||||
|
||||
const visibleTokens = useMemo(() => {
|
||||
return !hideSmallBalances
|
||||
? data?.portfolios?.[0].tokenBalances ?? []
|
||||
: data?.portfolios?.[0].tokenBalances?.filter((tokenBalance) =>
|
||||
meetsThreshold(tokenBalance, hideSmallBalances)
|
||||
) ?? []
|
||||
}, [data?.portfolios, hideSmallBalances])
|
||||
const tokenBalances = data?.portfolios?.[0].tokenBalances as TokenBalance[] | undefined
|
||||
|
||||
const hiddenTokens = useMemo(() => {
|
||||
return !hideSmallBalances
|
||||
? []
|
||||
: data?.portfolios?.[0].tokenBalances?.filter(
|
||||
(tokenBalance) => !meetsThreshold(tokenBalance, hideSmallBalances)
|
||||
) ?? []
|
||||
}, [data?.portfolios, hideSmallBalances])
|
||||
const { visibleTokens, hiddenTokens } = useMemo(
|
||||
() => splitHiddenTokens(tokenBalances ?? [], { hideSmallBalances }),
|
||||
[hideSmallBalances, tokenBalances]
|
||||
)
|
||||
|
||||
if (!data) {
|
||||
return <PortfolioSkeleton />
|
||||
}
|
||||
|
||||
if (data?.portfolios?.[0].tokenBalances?.length === 0) {
|
||||
if (tokenBalances?.length === 0) {
|
||||
// TODO: consider launching moonpay here instead of just closing the drawer
|
||||
return <EmptyWalletModule type="token" onNavigateClick={toggleWalletDrawer} />
|
||||
}
|
||||
@ -64,10 +50,7 @@ export default function Tokens({ account }: { account: string }) {
|
||||
<PortfolioTabWrapper>
|
||||
{visibleTokens.map(
|
||||
(tokenBalance) =>
|
||||
tokenBalance.token &&
|
||||
meetsThreshold(tokenBalance, hideSmallBalances) && (
|
||||
<TokenRow key={tokenBalance.id} {...tokenBalance} token={tokenBalance.token} />
|
||||
)
|
||||
tokenBalance.token && <TokenRow key={tokenBalance.id} {...tokenBalance} token={tokenBalance.token} />
|
||||
)}
|
||||
<ExpandoRow isExpanded={showHiddenTokens} toggle={toggleHiddenTokens} numItems={hiddenTokens.length}>
|
||||
{hiddenTokens.map(
|
||||
@ -86,10 +69,6 @@ const TokenNameText = styled(ThemedText.SubHeader)`
|
||||
${EllipsisStyle}
|
||||
`
|
||||
|
||||
type TokenBalance = NonNullable<
|
||||
NonNullable<NonNullable<PortfolioBalancesQuery['portfolios']>[number]>['tokenBalances']
|
||||
>[number]
|
||||
|
||||
type PortfolioToken = NonNullable<TokenBalance['token']>
|
||||
|
||||
function TokenRow({ token, quantity, denominatedValue, tokenProjectMarket }: TokenBalance & { token: PortfolioToken }) {
|
||||
|
@ -35,6 +35,7 @@ gql`
|
||||
tokenProject {
|
||||
id
|
||||
logoUrl
|
||||
isSpam
|
||||
}
|
||||
}
|
||||
token {
|
||||
|
117
src/utils/splitHiddenTokens.test.tsx
Normal file
117
src/utils/splitHiddenTokens.test.tsx
Normal file
@ -0,0 +1,117 @@
|
||||
import { Currency, TokenBalance } from 'graphql/data/__generated__/types-and-hooks'
|
||||
|
||||
import { splitHiddenTokens } from './splitHiddenTokens'
|
||||
|
||||
const tokens: TokenBalance[] = [
|
||||
// low balance
|
||||
{
|
||||
id: 'low-balance',
|
||||
ownerAddress: '',
|
||||
__typename: 'TokenBalance',
|
||||
denominatedValue: {
|
||||
id: '',
|
||||
value: 0.5,
|
||||
},
|
||||
tokenProjectMarket: {
|
||||
id: '',
|
||||
currency: Currency.Eth,
|
||||
tokenProject: {
|
||||
id: '',
|
||||
tokens: [],
|
||||
isSpam: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
// spam
|
||||
{
|
||||
id: 'spam',
|
||||
ownerAddress: '',
|
||||
__typename: 'TokenBalance',
|
||||
denominatedValue: {
|
||||
id: '',
|
||||
value: 100,
|
||||
},
|
||||
tokenProjectMarket: {
|
||||
id: '',
|
||||
currency: Currency.Eth,
|
||||
tokenProject: {
|
||||
id: '',
|
||||
tokens: [],
|
||||
isSpam: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
// valid
|
||||
{
|
||||
id: 'valid',
|
||||
ownerAddress: '',
|
||||
__typename: 'TokenBalance',
|
||||
denominatedValue: {
|
||||
id: '',
|
||||
value: 100,
|
||||
},
|
||||
tokenProjectMarket: {
|
||||
id: '',
|
||||
currency: Currency.Eth,
|
||||
tokenProject: {
|
||||
id: '',
|
||||
tokens: [],
|
||||
isSpam: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
// empty value
|
||||
{
|
||||
id: 'undefined-value',
|
||||
ownerAddress: '',
|
||||
__typename: 'TokenBalance',
|
||||
denominatedValue: {
|
||||
id: '',
|
||||
// @ts-ignore this is evidently possible but not represented in our types
|
||||
value: undefined,
|
||||
},
|
||||
tokenProjectMarket: {
|
||||
id: '',
|
||||
currency: Currency.Eth,
|
||||
tokenProject: {
|
||||
id: '',
|
||||
tokens: [],
|
||||
isSpam: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
describe('splitHiddenTokens', () => {
|
||||
it('splits spam tokens into hidden but keeps small balances if hideSmallBalances = false', () => {
|
||||
const { visibleTokens, hiddenTokens } = splitHiddenTokens(tokens, {
|
||||
hideSmallBalances: false,
|
||||
})
|
||||
|
||||
expect(hiddenTokens.length).toBe(1)
|
||||
expect(hiddenTokens[0].id).toBe('spam')
|
||||
|
||||
expect(visibleTokens.length).toBe(3)
|
||||
expect(visibleTokens[0].id).toBe('low-balance')
|
||||
expect(visibleTokens[1].id).toBe('valid')
|
||||
})
|
||||
|
||||
it('splits low balance into hidden by default', () => {
|
||||
const { visibleTokens, hiddenTokens } = splitHiddenTokens(tokens)
|
||||
|
||||
expect(hiddenTokens.length).toBe(2)
|
||||
expect(hiddenTokens[0].id).toBe('low-balance')
|
||||
expect(hiddenTokens[1].id).toBe('spam')
|
||||
|
||||
expect(visibleTokens.length).toBe(2)
|
||||
expect(visibleTokens[0].id).toBe('valid')
|
||||
})
|
||||
|
||||
it('splits undefined value tokens into visible', () => {
|
||||
const { visibleTokens } = splitHiddenTokens(tokens)
|
||||
|
||||
expect(visibleTokens.length).toBe(2)
|
||||
expect(visibleTokens[0].id).toBe('valid')
|
||||
expect(visibleTokens[1].id).toBe('undefined-value')
|
||||
})
|
||||
})
|
39
src/utils/splitHiddenTokens.tsx
Normal file
39
src/utils/splitHiddenTokens.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
import { TokenBalance } from 'graphql/data/__generated__/types-and-hooks'
|
||||
|
||||
const HIDE_SMALL_USD_BALANCES_THRESHOLD = 1
|
||||
|
||||
export function splitHiddenTokens(
|
||||
tokenBalances: TokenBalance[],
|
||||
options?: {
|
||||
hideSmallBalances?: boolean
|
||||
}
|
||||
) {
|
||||
const visibleTokens: TokenBalance[] = []
|
||||
const hiddenTokens: TokenBalance[] = []
|
||||
|
||||
for (const tokenBalance of tokenBalances) {
|
||||
const isValidValue =
|
||||
// if undefined we keep visible (see https://linear.app/uniswap/issue/WEB-1940/[mp]-update-how-we-handle-what-goes-in-hidden-token-section-of-mini)
|
||||
typeof tokenBalance.denominatedValue?.value === 'undefined' ||
|
||||
// if below $1
|
||||
options?.hideSmallBalances === false ||
|
||||
meetsThreshold(tokenBalance)
|
||||
|
||||
if (
|
||||
isValidValue &&
|
||||
// a spam token
|
||||
!tokenBalance.tokenProjectMarket?.tokenProject?.isSpam
|
||||
) {
|
||||
visibleTokens.push(tokenBalance)
|
||||
} else {
|
||||
hiddenTokens.push(tokenBalance)
|
||||
}
|
||||
}
|
||||
|
||||
return { visibleTokens, hiddenTokens }
|
||||
}
|
||||
|
||||
function meetsThreshold(tokenBalance: TokenBalance) {
|
||||
const value = tokenBalance.denominatedValue?.value ?? 0
|
||||
return value > HIDE_SMALL_USD_BALANCES_THRESHOLD
|
||||
}
|
Loading…
Reference in New Issue
Block a user