• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

decentraland / marketplace / 14311899718

07 Apr 2025 02:29PM UTC coverage: 66.286% (-0.08%) from 66.366%
14311899718

push

github

web-flow
fix: Retrieve user balance in the BuyWithCryptoModal (#2393)

* fix: Retrieve user balance

* fix: Make span simpler

* fix: Tests

2719 of 5387 branches covered (50.47%)

Branch coverage included in aggregate %.

24 of 35 new or added lines in 3 files covered. (68.57%)

1 existing line in 1 file now uncovered.

8087 of 10915 relevant lines covered (74.09%)

79.27 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

57.35
/webapp/src/components/Modals/BuyWithCryptoModal/ChainAndTokenSelector/ChainAndTokenSelector.tsx
1
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
1✔
2
import { InView } from 'react-intersection-observer'
1✔
3
import { BigNumber, ethers } from 'ethers'
1✔
4
import { ChainId } from '@dcl/schemas'
5
import { getNetwork } from '@dcl/schemas/dist/dapps/chain-id'
1✔
6
import { t } from 'decentraland-dapps/dist/modules/translation/utils'
1✔
7
import { Wallet } from 'decentraland-dapps/dist/modules/wallet/types'
8
import type { ChainData, Token } from 'decentraland-transactions/crossChain'
9
import { Close, Icon, Loader } from 'decentraland-ui'
1✔
10
import { getTokenBalance } from '../utils'
1✔
11
import styles from './ChainAndTokenSelector.module.css'
1✔
12

13
export const CHAIN_AND_TOKEN_SELECTOR_DATA_TEST_ID = 'chain-and-token-selector'
1✔
14

15
type Props = {
16
  wallet: Wallet
17
  currentChain: ChainId
18
  chains?: ChainData[]
19
  tokens?: Token[]
20
  onSelect: (chain: ChainData | Token) => void
21
}
22

23
const ChainAndTokenSelector = (props: Props) => {
1✔
24
  const mounted = useRef(false)
25✔
25
  const [search, setSearch] = useState('')
24✔
26
  const { currentChain, chains, tokens, onSelect, wallet } = props
24✔
27
  const title = useMemo(
24✔
28
    () =>
29
      t(`buy_with_crypto_modal.token_and_chain_selector.available_${chains ? 'chains' : 'tokens'}`, {
24✔
30
        chain: (
31
          <>
32
            {' '}
33
            <b> {getNetwork(currentChain)}</b>
34
          </>
35
        )
36
      }),
37
    [chains, currentChain]
38
  )
39

40
  const filteredChains = useMemo(() => {
24✔
41
    return chains?.filter(chain => chain.networkName.toLowerCase().includes(search.toLowerCase()))
24✔
42
  }, [chains, search])
43

44
  const [balances, setBalances] = useState<Record<string, BigNumber>>({})
24✔
45
  const [fetchingBalances, setFetchingBalances] = useState<Record<string, boolean>>({})
24✔
46

47
  useEffect(() => {
24✔
48
    mounted.current = true
24✔
49
    return () => {
24✔
50
      mounted.current = false
24✔
51
    }
52
  }, [])
53

54
  const fetchBalance = useCallback(
24✔
55
    async (token: Token) => {
NEW
56
      if (balances[token.symbol] !== undefined || fetchingBalances[token.symbol]) {
×
NEW
57
        return
×
58
      }
NEW
59
      setFetchingBalances(prev => ({ ...prev, [token.symbol]: true }))
×
60

UNCOV
61
      try {
×
NEW
62
        const balance = await getTokenBalance(token, currentChain, wallet.address)
×
NEW
63
        if (mounted.current) {
×
NEW
64
          setBalances(prev => ({ ...prev, [token.symbol]: balance }))
×
65
        }
66
      } catch (error) {
NEW
67
        if (mounted.current) {
×
NEW
68
          setBalances(prev => ({ ...prev, [token.symbol]: BigNumber.from(0) }))
×
69
        }
70
      } finally {
NEW
71
        if (mounted.current) {
×
NEW
72
          setFetchingBalances(prev => ({ ...prev, [token.symbol]: false }))
×
73
        }
74
      }
75
    },
76
    [balances, setBalances, currentChain, wallet.address]
77
  )
78

79
  const filteredTokens = useMemo(() => {
24✔
80
    const filtered = tokens?.filter(
24✔
81
      token => token.symbol.toLowerCase().includes(search.toLowerCase()) && token.chainId === currentChain.toString()
64✔
82
    )
83
    return filtered
24✔
84
  }, [tokens, search, currentChain, balances])
85

86
  return (
87
    <div className={styles.chainAndTokenSelector} data-testid={CHAIN_AND_TOKEN_SELECTOR_DATA_TEST_ID}>
88
      <div className={styles.searchContainer}>
89
        <Icon name="search" className={styles.searchIcon} />
90
        <input className={styles.searchInput} value={search} onChange={e => setSearch(e.target.value)} placeholder={t('global.search')} />
×
91
        {search ? <Close onClick={() => setSearch('')} /> : null}
×
92
      </div>
93
      <span className={styles.title}>{title}</span>
94
      <div className={styles.listContainer}>
95
        {filteredChains?.map(chain => (
96
          <div key={chain.chainId} className={styles.rowItem} onClick={() => onSelect(chain)}>
16✔
97
            <img src={chain.nativeCurrency.icon} alt={chain.networkName} />
98
            <span>{chain.networkName}</span>
99
          </div>
100
        ))}
101
        {filteredTokens?.map(token => {
102
          return (
NEW
103
            <InView onChange={inView => inView && fetchBalance(token)}>
×
104
              <div key={`${token.symbol}-${token.address}`} className={styles.rowItem} onClick={() => onSelect(token)}>
16✔
105
                <div className={styles.tokenDataContainer}>
106
                  <img src={token.logoURI} alt={token.symbol} />
107
                  <div className={styles.tokenNameAndSymbolContainer}>
108
                    <span>{token.symbol}</span>
109
                    <span className={styles.tokenName}>{token.name}</span>
110
                  </div>
111
                </div>
112
                <span className={styles.balance}>
113
                  {fetchingBalances[token.symbol] !== false ? (
32!
114
                    <Loader active inline size="small" />
115
                  ) : balances[token.symbol] !== undefined ? (
×
116
                    <>
117
                      {Number(ethers.utils.formatUnits(balances[token.symbol], token.decimals)).toFixed(5)}
118
                      <span>&#8202;</span>
119
                    </>
120
                  ) : (
121
                    0
122
                  )}
123
                </span>
124
              </div>
125
            </InView>
126
          )
127
        })}
128
      </div>
129

130
      {!!search && !filteredChains?.length && !filteredTokens?.length ? (
48!
131
        <span className={styles.noResults}>
132
          {t('buy_with_crypto_modal.token_and_chain_selector.no_matches', {
133
            search
134
          })}
135
        </span>
136
      ) : null}
137
    </div>
138
  )
139
}
140

141
export default ChainAndTokenSelector
24✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc