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

decentraland / marketplace / 8194016372

07 Mar 2024 07:54PM UTC coverage: 66.152% (-0.09%) from 66.242%
8194016372

Pull #2167

github

LautaroPetaccio
fix: Add toasts
Pull Request #2167: feat: Use new cross chain transaction payload action

2507 of 4904 branches covered (51.12%)

Branch coverage included in aggregate %.

8 of 13 new or added lines in 6 files covered. (61.54%)

6 existing lines in 5 files now uncovered.

7601 of 10376 relevant lines covered (73.26%)

70.55 hits per line

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

82.14
/webapp/src/components/Modals/BuyWithCryptoModal/BuyWithCryptoModal.tsx
1
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
1✔
2
import { useHistory, useLocation } from 'react-router-dom'
1✔
3
import classNames from 'classnames'
1✔
4
import compact from 'lodash/compact'
1✔
5
import { ethers } from 'ethers'
1✔
6
import { ChainId, Network } from '@dcl/schemas'
1✔
7
import { getNetwork } from '@dcl/schemas/dist/dapps/chain-id'
1✔
8
import { Button, Icon, Loader, ModalNavigation } from 'decentraland-ui'
1✔
9
import { ContractName, getContract } from 'decentraland-transactions'
1✔
10
import Modal from 'decentraland-dapps/dist/containers/Modal'
1✔
11
import { t } from 'decentraland-dapps/dist/modules/translation/utils'
1✔
12
import { getNetworkProvider } from 'decentraland-dapps/dist/lib/eth'
1✔
13
import { getAnalytics } from 'decentraland-dapps/dist/modules/analytics/utils'
1✔
14
import { isWearableOrEmote } from '../../../modules/asset/utils'
1✔
15
import * as events from '../../../utils/events'
1✔
16
import { Mana } from '../../Mana'
1✔
17
import { formatWeiMANA } from '../../../lib/mana'
1✔
18
import type { ChainData, Token, CrossChainProvider } from 'decentraland-transactions/crossChain'
19
import { AssetImage } from '../../AssetImage'
1✔
20
import { isPriceTooLow } from '../../BuyPage/utils'
1✔
21
import { CardPaymentsExplanation } from '../../BuyPage/CardPaymentsExplanation'
1✔
22
import { ManaToFiat } from '../../ManaToFiat'
1✔
23
import { config } from '../../../config'
1✔
24
import ChainAndTokenSelector from './ChainAndTokenSelector/ChainAndTokenSelector'
1✔
25
import {
26
  getDefaultChains,
27
  getMANAToken,
28
  getShouldUseMetaTx,
29
  isToken
30
} from './utils'
1✔
31
import { Props } from './BuyWithCryptoModal.types'
32
import styles from './BuyWithCryptoModal.module.css'
1✔
33
import { useShouldUseCrossChainProvider, useTokenBalance } from './hooks'
1✔
34
import PaymentSelector from './PaymentSelector'
1✔
35
import PurchaseTotal from './PurchaseTotal'
1✔
36

37
export const CANCEL_DATA_TEST_ID = 'confirm-buy-with-crypto-modal-cancel'
1✔
38
export const BUY_NOW_BUTTON_TEST_ID = 'buy-now-button'
6✔
39
export const SWITCH_NETWORK_BUTTON_TEST_ID = 'switch-network'
7✔
40
export const GET_MANA_BUTTON_TEST_ID = 'get-mana-button'
13✔
41
export const BUY_WITH_CARD_TEST_ID = 'buy-with-card-button'
13✔
42
export const PRICE_TOO_LOW_TEST_ID = 'price-too-low-label'
1✔
43

44
export type ProviderChain = ChainData
45
export type ProviderToken = Token
46

47
const squidURL = config.get('SQUID_API_URL')
1✔
48

49
export const BuyWithCryptoModal = (props: Props) => {
26✔
50
  const {
51
    price,
52
    wallet,
53
    metadata: { asset },
54
    isBuyingAsset,
55
    isLoadingAuthorization,
56
    isSwitchingNetwork,
57
    isBuyWithCardPage,
58
    onSwitchNetwork,
59
    onGetGasCost,
60
    onGetCrossChainRoute,
61
    onBuyNatively,
62
    onBuyWithCard,
63
    onBuyCrossChain,
64
    onGetMana,
65
    onClose,
66
    onGoBack
67
  } = props
202✔
68

69
  const crossChainSupportedChains = useRef<ChainId[]>([])
202✔
70
  const analytics = getAnalytics()
202✔
71
  const manaAddressOnAssetChain = getContract(
202✔
72
    ContractName.MANAToken,
73
    asset.chainId
74
  ).address
75
  const abortControllerRef = useRef(new AbortController())
202✔
76

77
  // useStates
78
  const [providerChains, setProviderChains] = useState<ChainData[]>(
202✔
79
    getDefaultChains()
80
  )
81
  const [providerTokens, setProviderTokens] = useState<Token[]>([])
202✔
82
  const [selectedChain, setSelectedChain] = useState(asset.chainId)
202✔
83
  const [selectedToken, setSelectedToken] = useState<Token>(
202✔
84
    getMANAToken(asset.chainId)
85
  )
86
  const [canBuyAsset, setCanBuyAsset] = useState<boolean | undefined>()
202✔
87
  const [insufficientToken, setInsufficientToken] = useState<
202✔
88
    Token | undefined
89
  >()
90
  const [showChainSelector, setShowChainSelector] = useState(false)
202✔
91
  const [showTokenSelector, setShowTokenSelector] = useState(false)
202✔
92
  const [crossChainProvider, setCrossChainProvider] = useState<
202✔
93
    CrossChainProvider
94
  >()
95
  const manaTokenOnSelectedChain: Token | undefined = useMemo(() => {
202✔
96
    return providerTokens.find(
60✔
97
      t => t.symbol === 'MANA' && t.chainId === selectedChain.toString()
50✔
98
    )
99
  }, [providerTokens, selectedChain])
100
  const manaTokenOnAssetChain: Token | undefined = useMemo(() => {
202✔
101
    return providerTokens.find(
52✔
102
      t =>
103
        t.address.toLocaleLowerCase() ===
40✔
104
        manaAddressOnAssetChain.toLocaleLowerCase()
105
    )
106
  }, [providerTokens, manaAddressOnAssetChain])
107

108
  const selectedProviderChain = useMemo(() => {
202✔
109
    return providerChains.find(
60✔
110
      c => c.chainId.toString() === selectedChain.toString()
90✔
111
    )
112
  }, [providerChains, selectedChain])
113

114
  const chainNativeToken = useMemo(() => {
202✔
115
    return providerTokens.find(
60✔
116
      t =>
117
        +t.chainId === selectedChain &&
136✔
118
        t.symbol === selectedProviderChain?.nativeCurrency.symbol
119
    )
120
  }, [selectedChain, selectedProviderChain, providerTokens])
121

122
  const { gasCost, isFetchingGasCost } = onGetGasCost(
202✔
123
    selectedToken,
124
    chainNativeToken,
125
    wallet
126
  )
127

128
  const {
129
    route,
130
    fromAmount,
131
    routeFeeCost,
132
    routeTotalUSDCost,
133
    isFetchingRoute,
134
    routeFailed
135
  } = onGetCrossChainRoute(
202✔
136
    selectedToken,
137
    selectedChain,
138
    providerTokens,
139
    crossChainProvider,
140
    wallet
141
  )
142

143
  useEffect(() => {
202✔
144
    const initializeCrossChainProvider = async () => {
26✔
145
      const { AxelarProvider, CROSS_CHAIN_SUPPORTED_CHAINS } = await import('decentraland-transactions/crossChain')
26✔
146
      const provider = new AxelarProvider(squidURL)
26✔
147
      crossChainSupportedChains.current = CROSS_CHAIN_SUPPORTED_CHAINS
26✔
148
      await provider.init() // init the provider on the mount
149
      setCrossChainProvider(provider)
26✔
150
    }
151

152
    initializeCrossChainProvider()
26✔
153
  }, [])
154

155
  const {
156
    isFetchingBalance,
157
    tokenBalance: selectedTokenBalance
158
  } = useTokenBalance(selectedToken, selectedChain, wallet?.address)
202✔
159

160
  // if the tx should be done through the provider
161
  const shouldUseCrossChainProvider = useShouldUseCrossChainProvider(
202✔
162
    selectedToken,
163
    asset.network
164
  )
165

166
  // Compute if the process should use a meta tx (connected in ETH and buying a L2 NFT)
167
  const useMetaTx = useMemo(() => {
202✔
168
    return (
50✔
169
      !!selectedToken &&
150✔
170
      !!wallet &&
171
      getShouldUseMetaTx(
172
        asset.chainId,
173
        selectedChain,
174
        selectedToken.address,
175
        manaAddressOnAssetChain,
176
        wallet.network
177
      )
178
    )
179
  }, [asset, manaAddressOnAssetChain, selectedChain, selectedToken, wallet])
180

181
  // Compute if the price is too low for meta tx
182
  const hasLowPriceForMetaTx = useMemo(
202✔
183
    () => wallet?.chainId !== ChainId.MATIC_MAINNET && isPriceTooLow(price), // not connected to polygon AND has price < minimum for meta tx
26✔
184
    [price, wallet?.chainId]
185
  )
186

187
  // init lib if necessary and fetch chains & supported tokens
188
  useEffect(() => {
202✔
189
    ;(async () => {
52✔
190
      try {
52✔
191
        if (crossChainProvider) {
52✔
192
          if (!crossChainProvider.isLibInitialized()) {
26!
193
            await crossChainProvider.init()
×
194
          }
195
          const defaultChains = getDefaultChains()
26✔
196
          const supportedTokens = crossChainProvider.getSupportedTokens()
26✔
197
          const supportedChains = [
26✔
198
            ...defaultChains,
199
            ...crossChainProvider
200
              .getSupportedChains()
201
              .filter(c => defaultChains.every(dc => dc.chainId !== c.chainId))
1,092✔
202
          ] // keep the defaults since we support MANA on them natively
203
          setProviderChains(
26✔
204
            supportedChains.filter(
205
              c =>
206
              crossChainSupportedChains.current.includes(+c.chainId) &&
234✔
207
              defaultChains.find(t => t.chainId === c.chainId)
78✔
208
            )
209
          )
210
          setProviderTokens(
26✔
211
            supportedTokens.filter(t =>
212
              crossChainSupportedChains.current.includes(+t.chainId)
104✔
213
            )
214
          )
215
        }
216
      } catch (error) {
217
        console.log('error: ', error)
×
218
      }
219
    })()
220
  }, [crossChainProvider, wallet])
221

222
  // when providerTokens are loaded and there's no selected token or the token selected if from another network
223
  useEffect(() => {
202✔
224
    if (
102!
225
      crossChainProvider?.isLibInitialized() &&
330!
226
      ((!selectedToken && providerTokens.length) || // only run if not selectedToken, meaning the first render
227
        (selectedToken && selectedChain.toString() !== selectedToken.chainId)) // or if selectedToken is not from the selectedChain
228
    ) {
229
      try {
×
230
        setSelectedToken(
231
          manaTokenOnSelectedChain || getMANAToken(selectedChain)
×
232
        ) // if it's not in the providerTokens, create the object manually with the right conectract address
233
      } catch (error) {
234
        const selectedChainTokens = providerTokens.filter(
×
235
          t => t.chainId === selectedChain.toString()
×
236
        )
237
        setSelectedToken(selectedChainTokens[0])
×
238
      }
239
    }
240
  }, [
241
    crossChainProvider,
242
    providerTokens.length,
243
    manaTokenOnSelectedChain,
244
    selectedChain,
245
    selectedToken
246
  ])
247

248
  // computes if the user can buy the item with the selected token
249
  useEffect(() => {
202✔
250
    ;(async () => {
102✔
251
      if (
102✔
252
        selectedToken &&
322✔
253
        ((selectedToken.symbol === 'MANA' && !!wallet) ||
254
          (selectedToken.symbol !== 'MANA' && // MANA balance is calculated differently
255
            selectedTokenBalance))
256
      ) {
257
        let canBuy
258
        if (selectedToken.symbol === 'MANA' && wallet) {
102✔
259
          // wants to buy a L2 item with ETH MANA (through the provider)
260
          if (
86✔
261
            asset.network === Network.MATIC &&
134✔
262
            getNetwork(selectedChain) === Network.ETHEREUM
263
          ) {
264
            canBuy =
6✔
265
              wallet.networks[Network.ETHEREUM].mana >=
266
              +ethers.utils.formatEther(price)
267
          } else {
268
            canBuy =
80✔
269
              wallet.networks[asset.network].mana >=
270
              +ethers.utils.formatEther(price)
271
          }
272
        } else if (selectedTokenBalance && routeFeeCost) {
16✔
273
          const balance = parseFloat(
16✔
274
            ethers.utils.formatUnits(
275
              selectedTokenBalance,
276
              selectedToken.decimals
277
            )
278
          )
279

280
          if (
16✔
281
            manaTokenOnAssetChain &&
64✔
282
            selectedToken &&
283
            crossChainProvider &&
284
            wallet
285
          ) {
286
            // fee is paid with same token selected
287
            if (selectedToken.symbol === routeFeeCost.token.symbol) {
16!
288
              canBuy =
16✔
289
                balance > Number(fromAmount) + Number(routeFeeCost.totalCost)
290
              if (!canBuy) {
16✔
291
                setInsufficientToken(selectedToken)
8✔
292
              }
293
            } else {
294
              const networkProvider = await getNetworkProvider(
×
295
                Number(routeFeeCost.token.chainId)
296
              )
297
              const provider = new ethers.providers.Web3Provider(
×
298
                networkProvider
299
              )
300
              const balanceNativeTokenWei = await provider.getBalance(
×
301
                wallet.address
302
              )
303
              const canPayForGas = balanceNativeTokenWei.gte(
×
304
                ethers.utils.parseEther(routeFeeCost.totalCost)
305
              )
306
              canBuy = canPayForGas && balance > Number(fromAmount)
×
307
              if (!canBuy) {
×
308
                setInsufficientToken(
×
309
                  !canPayForGas ? routeFeeCost.token : selectedToken
×
310
                )
311
              }
312
            }
313
          }
314
        }
315
        setCanBuyAsset(canBuy)
102✔
316
      }
317
    })()
318
  }, [
319
    asset,
320
    crossChainProvider,
321
    fromAmount,
322
    price,
323
    providerTokens,
324
    routeFeeCost,
325
    selectedChain,
326
    selectedToken,
327
    selectedTokenBalance,
328
    wallet
329
  ])
330

331
  const handleCrossChainBuy = useCallback(async () => {
202✔
332
    if (route && crossChainProvider && crossChainProvider.isLibInitialized()) {
3✔
333
      onBuyCrossChain(route)
3✔
334
    }
335
  }, [crossChainProvider, onBuyCrossChain, route])
336

337
  const renderSwitchNetworkButton = useCallback(() => {
202✔
338
    return (
339
      <Button
340
        fluid
341
        inverted
342
        className={styles.switchNetworkButton}
343
        disabled={isSwitchingNetwork}
344
        data-testid={SWITCH_NETWORK_BUTTON_TEST_ID}
345
        onClick={() => onSwitchNetwork(selectedChain)}
5✔
346
      >
347
        {isSwitchingNetwork ? (
13!
348
          <>
349
            <Loader inline active size="tiny" />
350
            {t('buy_with_crypto_modal.confirm_switch_network')}
351
          </>
352
        ) : (
353
          t('buy_with_crypto_modal.switch_network', {
354
            chain: selectedProviderChain?.networkName
355
          })
356
        )}
357
      </Button>
358
    )
359
  }, [
360
    isSwitchingNetwork,
361
    onSwitchNetwork,
362
    selectedProviderChain,
363
    selectedChain
364
  ])
365

366
  const handleBuyWithCard = useCallback(() => {
202✔
367
    if (onBuyWithCard) {
×
368
      onBuyWithCard()
×
369
    }
370
  }, [onBuyWithCard])
371

372
  const renderGetMANAButton = useCallback(() => {
202✔
373
    return (
374
      <>
375
        <Button
376
          fluid
377
          primary
378
          data-testid={GET_MANA_BUTTON_TEST_ID}
379
          loading={isFetchingBalance || isBuyingAsset}
216✔
380
          onClick={() => {
381
            // onGetMana()
NEW
382
            console.log("Getting MANA")
×
NEW
383
            handleCrossChainBuy()
×
UNCOV
384
            onClose()
×
385
          }}
386
        >
387
          {t('buy_with_crypto_modal.get_mana')}
388
        </Button>
389
        {onBuyWithCard && (
108✔
390
          <Button
391
            inverted
392
            fluid
393
            data-testid={BUY_WITH_CARD_TEST_ID}
394
            disabled={isBuyingAsset}
395
            loading={isBuyingAsset}
396
            onClick={handleBuyWithCard}
397
          >
398
            <Icon name="credit card outline" />
399
            {t(`buy_with_crypto_modal.buy_with_card`)}
400
          </Button>
401
        )}
402
      </>
403
    )
404
  }, [
405
    isFetchingBalance,
406
    isBuyingAsset,
407
    asset.chainId,
408
    isLoadingAuthorization,
409
    onBuyWithCard,
410
    handleCrossChainBuy,
411
    handleBuyWithCard,
412
    onGetMana,
413
    onClose
414
  ])
415

416
  const renderBuyNowButton = useCallback(() => {
202✔
417
    // if L1 asset and paying with ETH MANA
418
    // or if L2 asset and paying with MATIC MANA => native buy
419
    const onClick = shouldUseCrossChainProvider
15✔
420
      ? handleCrossChainBuy
421
      : onBuyNatively
422

423
    return (
424
      <>
425
        <Button
426
          fluid
427
          primary
428
          data-testid={BUY_NOW_BUTTON_TEST_ID}
429
          disabled={
430
            (selectedToken?.symbol !== 'MANA' && !route) ||
63✔
431
            isFetchingRoute ||
432
            isBuyingAsset ||
433
            isLoadingAuthorization
434
          }
435
          loading={isFetchingBalance || isLoadingAuthorization}
30✔
436
          onClick={onClick}
437
        >
438
          <>
439
            {isBuyingAsset || isFetchingRoute ? (
45!
440
              <Loader inline active size="tiny" />
441
            ) : null}
442
            {!isFetchingRoute // if fetching route, just render the Loader
15!
443
              ? isBuyingAsset
15!
444
                ? t('buy_with_crypto_modal.confirm_transaction')
445
                : t('buy_with_crypto_modal.buy_now')
446
              : null}
447
          </>
448
        </Button>
449
      </>
450
    )
451
  }, [
452
    route,
453
    selectedToken,
454
    isFetchingRoute,
455
    isBuyingAsset,
456
    isLoadingAuthorization,
457
    isFetchingBalance,
458
    onBuyNatively,
459
    handleCrossChainBuy,
460
    shouldUseCrossChainProvider
461
  ])
462

463
  const renderMainActionButton = useCallback(() => {
202✔
464
    // has a selected token and canBuyAsset was computed
465
    if (wallet && selectedToken && canBuyAsset !== undefined) {
178✔
466
      // if can't buy Get Mana and Buy With Card buttons
467
      if (!canBuyAsset) {
136✔
468
        return renderGetMANAButton()
108✔
469
      }
470

471
      // for any token other than MANA, it user needs to be connected on the origin chain
472
      if (selectedToken.symbol !== 'MANA') {
28✔
473
        return selectedChain === wallet.chainId
8✔
474
          ? renderBuyNowButton()
475
          : renderSwitchNetworkButton()
476
      }
477

478
      // for L1 NFTs
479
      if (asset.network === Network.ETHEREUM) {
20✔
480
        // if tries to buy with ETH MANA and connected to other network, should switch to ETH network to pay directly
481
        return selectedToken.symbol === 'MANA' &&
8✔
482
          wallet.network !== Network.ETHEREUM &&
483
          getNetwork(selectedChain) === Network.ETHEREUM
484
          ? renderSwitchNetworkButton()
485
          : renderBuyNowButton()
486
      }
487

488
      // for L2 NFTs paying with MANA
489

490
      // And connected to MATIC, should render the buy now button otherwise check if a meta tx is available
491
      if (getNetwork(selectedChain) === Network.MATIC) {
12✔
492
        return wallet.network === Network.MATIC
12✔
493
          ? renderBuyNowButton()
494
          : isPriceTooLow(price)
8✔
495
          ? renderSwitchNetworkButton() // switch to MATIC to pay for the gas
496
          : renderBuyNowButton()
497
      }
498

499
      // can buy it with MANA from other chain through the provider
500
      return renderBuyNowButton()
×
501
    } else if (!route && routeFailed) {
42!
502
      // can't buy Get Mana and Buy With Card buttons
503
      return renderGetMANAButton()
×
504
    }
505
  }, [
506
    wallet,
507
    selectedToken,
508
    canBuyAsset,
509
    route,
510
    asset,
511
    price,
512
    routeFailed,
513
    selectedChain,
514
    renderBuyNowButton,
515
    renderSwitchNetworkButton,
516
    renderGetMANAButton
517
  ])
518

519
  const onTokenOrChainSelection = useCallback(
202✔
520
    (selectedOption: Token | ChainData) => {
521
      setShowChainSelector(false)
24✔
522
      setShowTokenSelector(false)
24✔
523

524
      if (isToken(selectedOption)) {
24✔
525
        abortControllerRef.current.abort()
16✔
526

527
        const selectedToken = providerTokens.find(
16✔
528
          t =>
529
            t.address === selectedOption.address &&
60✔
530
            t.chainId === selectedChain.toString()
531
        ) as Token
532
        // reset all fields
533
        setSelectedToken(selectedToken)
16✔
534
        setCanBuyAsset(undefined)
16✔
535
        abortControllerRef.current = new AbortController()
16✔
536
        analytics.track(events.CROSS_CHAIN_TOKEN_SELECTION, {
16✔
537
          selectedToken
538
        })
539
      } else {
540
        setSelectedChain(Number(selectedOption.chainId) as ChainId)
8✔
541
        const manaDestinyChain = providerTokens.find(
8✔
542
          t => t.symbol === 'MANA' && t.chainId === selectedOption.chainId
10✔
543
        )
544
        // set the selected token on the new chain selected to MANA or the first one found
545
        const selectedToken = providerTokens.find(
8✔
546
          t => t.chainId === selectedOption.chainId
10✔
547
        )
548
        const token = manaDestinyChain || selectedToken
8!
549
        if (token) {
8✔
550
          setSelectedToken(token)
8✔
551
        }
552

553
        analytics.track(events.CROSS_CHAIN_CHAIN_SELECTION, {
8✔
554
          selectedChain: selectedOption.chainId
555
        })
556
      }
557
    },
558
    [analytics, providerTokens, selectedChain]
559
  )
560

561
  const renderModalNavigation = useCallback(() => {
202✔
562
    if (showChainSelector || showTokenSelector) {
202✔
563
      return (
564
        <ModalNavigation
565
          title={t(
566
            `buy_with_crypto_modal.token_and_chain_selector.select_${
567
              showChainSelector ? 'chain' : 'token'
24✔
568
            }`
569
          )}
570
          onBack={() => {
571
            setShowChainSelector(false)
×
572
            setShowTokenSelector(false)
×
573
          }}
574
        />
575
      )
576
    }
577
    return (
578
      <ModalNavigation
579
        title={t('buy_with_crypto_modal.title', {
580
          name: asset.name,
581
          b: (children: React.ReactChildren) => <b>{children}</b>
582
        })}
583
        onBack={!isBuyingAsset ? onGoBack : undefined}
178!
584
        onClose={!isBuyingAsset ? onClose : undefined}
178!
585
      />
586
    )
587
  }, [asset.name, onClose, showChainSelector, showTokenSelector, isBuyingAsset])
588

589
  const translationPageDescriptorId = compact([
202✔
590
    'mint',
591
    isWearableOrEmote(asset)
202!
592
      ? isBuyWithCardPage
202!
593
        ? 'with_card'
594
        : 'with_mana'
595
      : null,
596
    'page'
597
  ]).join('_')
598

599
  const location = useLocation()
202✔
600
  const history = useHistory()
202✔
601

602
  const handleOnClose = useCallback(() => {
202✔
603
    const search = new URLSearchParams(location.search)
×
604
    const hasModalQueryParam = search.get('buyWithCrypto')
×
605
    if (hasModalQueryParam) {
×
606
      search.delete('buyWithCrypto')
×
607
      history.replace({
×
608
        search: search.toString()
609
      })
610
    }
611
    onClose()
×
612
  }, [history, location.search, onClose])
613

614
  const handleShowChainSelector = useCallback(() => {
202✔
615
    setShowChainSelector(true)
8✔
616
  }, [])
617

618
  const handleShowTokenSelector = useCallback(() => {
202✔
619
    setShowTokenSelector(true)
16✔
620
  }, [])
621

622
  const assetName = useMemo(() => {
202✔
623
    return asset.data.ens ? (
26!
624
      <>
625
        <strong>{asset.name}</strong>.dcl.eth
626
      </>
627
    ) : (
628
      asset.name
629
    )
630
  }, [asset])
631

632
  const assetDescription = useMemo(() => {
202✔
633
    if (asset.data.ens) {
26!
634
      return t('buy_with_crypto_modal.asset_description.ens')
×
635
    } else if (asset.data.emote) {
26!
636
      return t('buy_with_crypto_modal.asset_description.emotes')
×
637
    } else if (asset.data.wearable) {
26!
638
      return t('buy_with_crypto_modal.asset_description.wearables')
26✔
639
    } else if (asset.data.estate || asset.data.parcel) {
×
640
      return t('buy_with_crypto_modal.asset_description.land')
×
641
    } else {
642
      return t('buy_with_crypto_modal.asset_description.other')
×
643
    }
644
  }, [asset])
645

646
  return (
647
    <Modal
648
      size="tiny"
649
      onClose={handleOnClose}
650
      className={styles.buyWithCryptoModal}
651
    >
652
      {renderModalNavigation()}
653
      <Modal.Content>
654
        <>
655
          {showChainSelector || showTokenSelector ? (
396✔
656
            <div>
24✔
657
              {showChainSelector && wallet ? (
56✔
658
                <ChainAndTokenSelector
659
                  wallet={wallet}
660
                  currentChain={selectedChain}
661
                  chains={providerChains}
662
                  onSelect={onTokenOrChainSelection}
663
                />
664
              ) : null}
665
              {showTokenSelector && wallet ? (
64✔
666
                <ChainAndTokenSelector
667
                  wallet={wallet}
668
                  currentChain={selectedChain}
669
                  tokens={providerTokens}
670
                  onSelect={onTokenOrChainSelection}
671
                />
672
              ) : null}
673
            </div>
674
          ) : (
675
            <>
676
              <div className={styles.assetContainer}>
677
                <AssetImage asset={asset} isSmall />
678
                <div className={styles.assetDetails}>
679
                  <span className={styles.assetName}>{assetName}</span>
680
                  <span className={styles.assetDescription}>
681
                    {assetDescription}
682
                  </span>
683
                </div>
684
                <div className={styles.priceContainer}>
685
                  <Mana network={asset.network} inline withTooltip>
686
                    {formatWeiMANA(price)}
687
                  </Mana>
688
                  <span className={styles.priceInUSD}>
689
                    <ManaToFiat mana={price} digits={4} />
690
                  </span>
691
                </div>
692
              </div>
693

694
              <PaymentSelector
695
                price={price}
696
                wallet={wallet}
697
                isBuyingAsset={isBuyingAsset}
698
                providerTokens={providerTokens}
699
                selectedToken={selectedToken}
700
                selectedChain={selectedChain}
701
                shouldUseCrossChainProvider={shouldUseCrossChainProvider}
702
                gasCost={gasCost}
703
                isFetchingGasCost={isFetchingGasCost}
704
                isFetchingBalance={isFetchingBalance}
705
                selectedProviderChain={selectedProviderChain}
706
                selectedTokenBalance={selectedTokenBalance}
707
                onShowChainSelector={handleShowChainSelector}
708
                onShowTokenSelector={handleShowTokenSelector}
709
                amountInSelectedToken={fromAmount}
710
                route={route}
711
                routeFeeCost={routeFeeCost}
712
              />
713

714
              <PurchaseTotal
715
                selectedToken={selectedToken}
716
                price={price}
717
                useMetaTx={useMetaTx}
718
                shouldUseCrossChainProvider={shouldUseCrossChainProvider}
719
                route={route}
720
                routeFeeCost={routeFeeCost}
721
                fromAmount={fromAmount}
722
                isLoading={
723
                  isFetchingRoute ||
582✔
724
                  isFetchingGasCost ||
725
                  (shouldUseCrossChainProvider && !route)
726
                }
727
                gasCost={gasCost}
728
                manaTokenOnSelectedChain={manaTokenOnAssetChain}
729
                routeTotalUSDCost={routeTotalUSDCost}
730
              />
731

732
              {selectedToken && shouldUseCrossChainProvider ? (
534✔
733
                <div className={styles.durationAndExchangeContainer}>
734
                  <div>
735
                    <span>
736
                      <Icon name="clock outline" />{' '}
737
                      {t(
738
                        'buy_with_crypto_modal.durations.transaction_duration'
739
                      )}{' '}
740
                    </span>
741
                    {route ? (
742
                      t(
48✔
743
                        `buy_with_crypto_modal.durations.${
744
                          route.route.estimate.estimatedRouteDuration === 0
48!
745
                            ? 'fast'
746
                            : route.route.estimate.estimatedRouteDuration === 20
×
747
                            ? 'normal'
748
                            : 'slow'
749
                        }`
750
                      )
751
                    ) : (
752
                      <span
753
                        className={classNames(
754
                          styles.skeleton,
755
                          styles.fromAmountUSDSkeleton
756
                        )}
757
                      />
758
                    )}
759
                  </div>
760
                  <div className={styles.exchangeContainer}>
761
                    <div className={styles.exchangeContainerLabel}>
762
                      <span className={styles.exchangeIcon} />
763
                      <span> {t('buy_with_crypto_modal.exchange_rate')} </span>
764
                    </div>
765
                    {route && selectedToken ? (
96✔
766
                      <>
48✔
767
                        1 {selectedToken.symbol} ={' '}
768
                        {route.route.estimate.exchangeRate?.slice(0, 7)} MANA
769
                      </>
770
                    ) : (
771
                      <span
772
                        className={classNames(
773
                          styles.skeleton,
774
                          styles.fromAmountUSDSkeleton
775
                        )}
776
                      />
777
                    )}
778
                  </div>
779
                </div>
780
              ) : null}
781

782
              {selectedToken &&
610✔
783
              shouldUseCrossChainProvider &&
784
              asset.network === Network.MATIC && // and it's buying a MATIC asset
785
              !isPriceTooLow(price) ? (
786
                <span className={styles.rememberFreeTxs}>
787
                  {t('buy_with_crypto_modal.remember_transaction_fee_covered', {
788
                    covered: (
789
                      <span className={styles.feeCoveredFree}>
790
                        {t('buy_with_crypto_modal.covered_for_you_by_dao')}
791
                      </span>
792
                    )
793
                  })}
794
                </span>
795
              ) : null}
796

797
              {hasLowPriceForMetaTx && !isBuyWithCardPage && useMetaTx ? (
386✔
798
                <span
799
                  className={styles.warning}
800
                  data-testid={PRICE_TOO_LOW_TEST_ID}
801
                >
802
                  {' '}
803
                  {t('buy_with_crypto_modal.price_too_low', {
804
                    learn_more: (
805
                      <a
806
                        href="https://docs.decentraland.org/player/blockchain-integration/transactions-in-polygon/#why-do-i-have-to-cover-the-tra[…]ems-that-cost-less-than-1-mana"
807
                        target="_blank"
808
                        rel="noreferrer"
809
                      >
810
                        {/* TODO: add this URL */}
811
                        <u> {t('buy_with_crypto_modal.learn_more')} </u>
812
                      </a>
813
                    )
814
                  })}
815
                </span>
816
              ) : null}
817
              {canBuyAsset === false &&
572✔
818
              !isFetchingBalance &&
819
              !isFetchingRoute ? (
820
                <span className={styles.warning}>
821
                  {t('buy_with_crypto_modal.insufficient_funds', {
822
                    token: insufficientToken?.symbol || 'MANA'
208✔
823
                  })}
824
                </span>
825
              ) : null}
826
              {routeFailed && selectedToken ? (
356!
827
                <span className={styles.warning}>
828
                  {' '}
829
                  {t('buy_with_crypto_modal.route_unavailable', {
830
                    token: selectedToken.symbol
831
                  })}
832
                </span>
833
              ) : null}
834
            </>
835
          )}
836
        </>
837
      </Modal.Content>
838
      {showChainSelector || showTokenSelector ? null : (
420✔
839
        <Modal.Actions>
840
          <div
841
            className={classNames(
842
              styles.buttons,
843
              isWearableOrEmote(asset) && 'with-mana'
356✔
844
            )}
845
          >
846
            {renderMainActionButton()}
847
          </div>
848
          {isWearableOrEmote(asset) && isBuyWithCardPage ? (
534!
849
            <CardPaymentsExplanation
850
              translationPageDescriptorId={translationPageDescriptorId}
851
            />
852
          ) : null}
853
        </Modal.Actions>
854
      )}
855
    </Modal>
856
  )
857
}
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

© 2025 Coveralls, Inc