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

decentraland / marketplace / 6982743061

24 Nov 2023 03:54PM UTC coverage: 43.329% (+2.5%) from 40.865%
6982743061

Pull #2042

github

juanmahidalgo
test: fix how the lib is mocked
Pull Request #2042: feat: use squid to calculate route between two chains and tokens

2621 of 7292 branches covered (0.0%)

Branch coverage included in aggregate %.

271 of 349 new or added lines in 18 files covered. (77.65%)

1 existing line in 1 file now uncovered.

4621 of 9422 relevant lines covered (49.04%)

24.0 hits per line

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

89.27
/webapp/src/components/Modals/BuyWithCryptoModal/BuyWithCryptoModal.tsx
1
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
2
import classNames from 'classnames'
3
import compact from 'lodash/compact'
4
import { ethers, BigNumber } from 'ethers'
5
import { ChainId, Contract, Network } from '@dcl/schemas'
6
import { getNetwork } from '@dcl/schemas/dist/dapps/chain-id'
7
import {
8
  ChainButton,
9
  withAuthorizedAction
10
} from 'decentraland-dapps/dist/containers'
11
import { Button, Icon, Loader, ModalNavigation, Popup } from 'decentraland-ui'
12
import { ContractName, getContract } from 'decentraland-transactions'
13
import Modal from 'decentraland-dapps/dist/containers/Modal'
14
import { t } from 'decentraland-dapps/dist/modules/translation/utils'
15
import { getNetworkProvider } from 'decentraland-dapps/dist/lib/eth'
16
import { AuthorizedAction } from 'decentraland-dapps/dist/containers/withAuthorizedAction/AuthorizationModal'
17
import { AuthorizationType } from 'decentraland-dapps/dist/modules/authorization/types'
18
import { getAnalytics } from 'decentraland-dapps/dist/modules/analytics/utils'
19
import { Contract as DCLContract } from '../../../modules/vendor/services'
20
import { isNFT, isWearableOrEmote } from '../../../modules/asset/utils'
21
import infoIcon from '../../../images/infoIcon.png'
22
import { getContractNames } from '../../../modules/vendor'
23
import * as events from '../../../utils/events'
24
import { Mana } from '../../Mana'
25
import { formatWeiMANA } from '../../../lib/mana'
26
import {
27
  CROSS_CHAIN_SUPPORTED_CHAINS,
28
  ChainData,
29
  RouteResponse,
30
  Token,
31
  CrossChainProvider
32
} from 'decentraland-transactions/dist/crossChain/types'
33
import { AxelarProvider } from 'decentraland-transactions/dist/crossChain/AxelarProvider'
34
import { AssetImage } from '../../AssetImage'
35
import { isPriceTooLow } from '../../BuyPage/utils'
36
import { CardPaymentsExplanation } from '../../BuyPage/CardPaymentsExplanation'
37
import { ManaToFiat } from '../../ManaToFiat'
38
import { getBuyItemStatus, getError } from '../../../modules/order/selectors'
39
import { getMintItemStatus } from '../../../modules/item/selectors'
40
import { NFT } from '../../../modules/nft/types'
41
import { config } from '../../../config'
42
import ChainAndTokenSelector from './ChainAndTokenSelector/ChainAndTokenSelector'
43
import { formatPrice, getMANAToken, getShouldUseMetaTx, isToken } from './utils'
44
import { Props } from './BuyWithCryptoModal.types'
45
import styles from './BuyWithCryptoModal.module.css'
46

47
export const CANCEL_DATA_TEST_ID = 'confirm-buy-with-crypto-modal-cancel'
1✔
48
export const BUY_NOW_BUTTON_TEST_ID = 'buy-now-button'
1✔
49
export const SWITCH_NETWORK_BUTTON_TEST_ID = 'switch-network'
1✔
50
export const GET_MANA_BUTTON_TEST_ID = 'get-mana-button'
1✔
51
export const BUY_WITH_CARD_TEST_ID = 'buy-with-card-button'
1✔
52
export const PAY_WITH_DATA_TEST_ID = 'pay-with-container'
1✔
53
export const CHAIN_SELECTOR_DATA_TEST_ID = 'chain-selector'
1✔
54
export const TOKEN_SELECTOR_DATA_TEST_ID = 'token-selector'
1✔
55
export const FREE_TX_CONVERED_TEST_ID = 'free-tx-label'
1✔
56
export const PRICE_TOO_LOW_TEST_ID = 'price-too-low-label'
1✔
57

58
const NATIVE_TOKEN = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE'
1✔
59
const ROUTE_FETCH_INTERVAL = 10000000 // 10 secs
1✔
60

61
export type ProviderChain = ChainData
62
export type ProviderToken = Token
63

64
const squidURL = config.get('SQUID_API_URL')
1✔
65

66
export const BuyWithCryptoModal = (props: Props) => {
1✔
67
  const {
68
    wallet,
69
    metadata: { asset, order },
70
    getContract: getContractProp,
71
    isLoading,
72
    isLoadingBuyCrossChain,
73
    isLoadingAuthorization,
74
    isSwitchingNetwork,
75
    isBuyWithCardPage,
76
    onAuthorizedAction,
77
    onSwitchNetwork,
78
    onBuyItem: onBuyItemProp,
79
    onBuyItemThroughProvider,
80
    onBuyItemWithCard,
81
    onExecuteOrder,
82
    onExecuteOrderWithCard,
83
    onGetMana,
84
    onClose
85
  } = props
321✔
86

87
  const analytics = getAnalytics()
321✔
88
  const destinyChainMANA = getContract(ContractName.MANAToken, asset.chainId)
321✔
89
    .address
90
  const abortControllerRef = useRef(new AbortController())
321✔
91

92
  // useStates
93
  const [providerChains, setProviderChains] = useState<ChainData[]>([])
321✔
94
  const [providerTokens, setProviderTokens] = useState<Token[]>([])
321✔
95
  const [selectedChain, setSelectedChain] = useState(asset.chainId)
321✔
96
  const [selectedToken, setSelectedToken] = useState<Token>()
321✔
97
  const [isFetchingBalance, setIsFetchingBalance] = useState(false)
321✔
98
  const [isFetchingRoute, setIsFetchingRoute] = useState(false)
321✔
99
  const [selectedTokenBalance, setSelectedTokenBalance] = useState<BigNumber>()
321✔
100
  const [route, setRoute] = useState<RouteResponse>()
321✔
101
  const [routeFailed, setRouteFailed] = useState(false)
321✔
102
  const [canBuyItem, setCanBuyItem] = useState<boolean | undefined>(undefined)
321✔
103
  const [fromAmount, setFromAmount] = useState<string | undefined>(undefined)
321✔
104
  const [showChainSelector, setShowChainSelector] = useState(false)
321✔
105
  const [showTokenSelector, setShowTokenSelector] = useState(false)
321✔
106
  const [crossChainProvider, setCrossChainProvider] = useState<
321✔
107
    CrossChainProvider
108
  >()
109

110
  useEffect(() => {
321✔
111
    const provider = new AxelarProvider(squidURL)
29✔
112
    provider.init() // init the provider on the mount
29✔
113
    setCrossChainProvider(provider)
29✔
114
  }, [])
115

116
  // useMemos
117

118
  // if the tx should be done through the provider
119
  const shouldUseCrossChainProvider = useMemo(
321✔
120
    () =>
121
      selectedToken &&
82✔
122
      !(
123
        (
124
          (selectedToken.symbol === 'MANA' &&
185✔
125
            getNetwork(selectedChain) === Network.MATIC &&
126
            asset.network === Network.MATIC) || // MANA selected and it's sending the tx from MATIC
127
          (selectedToken.symbol === 'MANA' &&
128
            getNetwork(selectedChain) === Network.ETHEREUM &&
129
            asset.network === Network.ETHEREUM)
130
        ) // MANA selected and it's connected to ETH and buying a L1 NFT
131
      ),
132
    [asset.network, selectedChain, selectedToken]
133
  )
134

135
  // Compute if the process should use a meta tx (connected in ETH and buying a L2 NFT)
136
  const useMetaTx = useMemo(() => {
321✔
137
    return (
82✔
138
      !!selectedToken &&
188✔
139
      !!wallet &&
140
      getShouldUseMetaTx(
141
        asset,
142
        selectedChain,
143
        selectedToken.address,
144
        destinyChainMANA,
145
        wallet.network
146
      )
147
    )
148
  }, [asset, destinyChainMANA, selectedChain, selectedToken, wallet])
149

150
  const selectedProviderChain = useMemo(() => {
321✔
151
    return providerChains.find(c => c.chainId === selectedChain.toString())
66✔
152
  }, [providerChains, selectedChain])
153

154
  // the price of the order or the item
155
  const price = useMemo(
321✔
156
    () => (order ? order.price : !isNFT(asset) ? asset.price : ''),
29!
157
    [asset, order]
158
  )
159

160
  // Compute if the price is too low for meta tx
161
  const hasLowPriceForMetaTx = useMemo(
321✔
162
    () => wallet?.chainId !== ChainId.MATIC_MAINNET && isPriceTooLow(price), // not connected to polygon AND has price < minimun for meta tx
29✔
163
    [price, wallet?.chainId]
164
  )
165

166
  // Compute the route fee cost
167
  const routeFeeCost = useMemo(() => {
321✔
168
    if (route) {
45✔
169
      const {
170
        route: {
171
          estimate: { gasCosts, feeCosts }
172
        }
173
      } = route
16✔
174
      const totalGasCost = gasCosts
16✔
175
        .map(c => BigNumber.from(c.amount))
16✔
176
        .reduce((a, b) => a.add(b), BigNumber.from(0))
16✔
177
      const totalFeeCost = feeCosts
16✔
NEW
178
        .map(c => BigNumber.from(c.amount))
×
NEW
179
        .reduce((a, b) => a.add(b), BigNumber.from(0))
×
180
      const token = gasCosts[0].token
16✔
181
      return {
16✔
182
        token,
183
        gasCostWei: totalGasCost,
184
        gasCost: parseFloat(
185
          ethers.utils.formatUnits(
186
            totalGasCost,
187
            route.route.estimate.gasCosts[0].token.decimals
188
          )
189
        ).toFixed(6),
190
        feeCost: parseFloat(
191
          ethers.utils.formatUnits(
192
            totalFeeCost,
193
            route.route.estimate.gasCosts[0].token.decimals
194
          )
195
        ).toFixed(6),
196
        feeCostWei: totalFeeCost,
197
        totalCost: parseFloat(
198
          ethers.utils.formatUnits(
199
            totalGasCost.add(totalFeeCost),
200
            token.decimals
201
          )
202
        ).toFixed(6)
203
      }
204
    }
205
  }, [route])
206

207
  const routeTotalUSDCost = useMemo(() => {
321✔
208
    if (
114✔
209
      route &&
178✔
210
      routeFeeCost &&
211
      routeFeeCost.token.usdPrice &&
212
      fromAmount &&
213
      selectedToken?.usdPrice
214
    ) {
215
      const { feeCost, gasCost } = routeFeeCost
16✔
216
      return (
16✔
217
        routeFeeCost.token.usdPrice * (Number(gasCost) + Number(feeCost)) +
218
        selectedToken.usdPrice * Number(fromAmount)
219
      )
220
    }
221
  }, [fromAmount, route, routeFeeCost, selectedToken])
222

223
  // useEffects
224

225
  // init lib if necessary and fetch chains & supported tokens
226
  useEffect(() => {
321✔
227
    ;(async () => {
58✔
228
      try {
58✔
229
        if (crossChainProvider) {
58✔
230
          if (!crossChainProvider.isLibInitialized()) {
29!
NEW
231
            await crossChainProvider.init()
×
232
          }
233
          const supportedTokens = crossChainProvider.getSupportedTokens()
29✔
234
          const supportedChains = crossChainProvider.getSupportedChains()
29✔
235
          // if for any reasons Polygon is not in the supported chains, add it manually to support our main flow (buying Polygon NFTs with Polygon MANA)
236
          if (
29!
237
            !supportedChains.find(c => +c.chainId === ChainId.MATIC_MAINNET)
58✔
238
          ) {
NEW
239
            supportedChains.push({
×
240
              chainId: ChainId.MATIC_MAINNET.toString(),
241
              networkName: 'Polygon',
242
              nativeCurrency: {
243
                name: 'MATIC',
244
                symbol: 'MATIC',
245
                decimals: 18,
246
                icon:
247
                  'https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/matic.svg'
248
              }
249
            } as ChainData)
250
          }
251
          setProviderChains(
29✔
252
            supportedChains.filter(c =>
253
              CROSS_CHAIN_SUPPORTED_CHAINS.includes(+c.chainId)
58✔
254
            )
255
          )
256
          setProviderTokens(
29✔
257
            supportedTokens.filter(t =>
258
              CROSS_CHAIN_SUPPORTED_CHAINS.includes(+t.chainId)
116✔
259
            )
260
          )
261
        }
262
      } catch (error) {
NEW
263
        console.log('error: ', error)
×
264
      }
265
    })()
266
  }, [crossChainProvider, wallet])
267

268
  // calculates Route for the selectedToken
269
  const calculateRoute = useCallback(async () => {
321✔
270
    const abortController = abortControllerRef.current
24✔
271
    const signal = abortController.signal
24✔
272

273
    const providerMANA = providerTokens.find(
24✔
274
      t =>
275
        t.address.toLocaleLowerCase() === destinyChainMANA.toLocaleLowerCase()
38✔
276
    )
277
    if (
24!
278
      !crossChainProvider ||
120✔
279
      !crossChainProvider.isLibInitialized() ||
280
      !wallet ||
281
      !selectedToken ||
282
      !providerMANA
283
    ) {
NEW
284
      return
×
285
    }
286
    try {
24✔
287
      setRoute(undefined)
24✔
288
      setIsFetchingRoute(true)
24✔
289
      setRouteFailed(false)
24✔
290
      let route: RouteResponse | undefined = undefined
24✔
291
      const fromAmountParams = {
24✔
292
        fromToken: selectedToken,
293
        toAmount: ethers.utils.formatEther(price),
294
        toToken: providerMANA
295
      }
296
      const fromAmount = Number(
24✔
297
        await crossChainProvider.getFromAmount(fromAmountParams)
298
      ).toFixed(6)
299
      setFromAmount(fromAmount)
24✔
300

301
      const fromAmountWei = ethers.utils
24✔
302
        .parseUnits(fromAmount.toString(), selectedToken.decimals)
303
        .toString()
304

305
      const baseRouteConfig = {
24✔
306
        fromAddress: wallet.address,
307
        fromAmount: fromAmountWei,
308
        fromChain: selectedChain,
309
        fromToken: selectedToken.address
310
      }
311

312
      if (order) {
24!
313
        // there's an order so it's buying an NFT
NEW
314
        route = await crossChainProvider.getBuyNFTRoute({
×
315
          ...baseRouteConfig,
316
          nft: {
317
            collectionAddress: order.contractAddress,
318
            tokenId: order.tokenId,
319
            price: order.price
320
          },
321
          toAmount: order.price,
322
          toChain: order.chainId
323
        })
324
      } else if (!isNFT(asset)) {
24!
325
        // buying an item
326
        route = await crossChainProvider.getMintNFTRoute({
24✔
327
          ...baseRouteConfig,
328
          item: {
329
            collectionAddress: asset.contractAddress,
330
            itemId: asset.itemId,
331
            price: asset.price
332
          },
333
          toAmount: asset.price,
334
          toChain: asset.chainId
335
        })
336
      }
337

338
      if (route && !signal.aborted) {
24✔
339
        setRoute(route)
16✔
340
      }
341
    } catch (error) {
NEW
342
      console.error('Error while getting Route: ', error)
×
NEW
343
      analytics.track(events.ERROR_GETTING_ROUTE, {
×
344
        error,
345
        selectedToken,
346
        selectedChain
347
      })
NEW
348
      setRouteFailed(true)
×
349
    } finally {
350
      setIsFetchingRoute(false)
24✔
351
    }
352
  }, [
353
    analytics,
354
    asset,
355
    crossChainProvider,
356
    destinyChainMANA,
357
    order,
358
    price,
359
    providerTokens,
360
    selectedChain,
361
    selectedToken,
362
    wallet
363
  ])
364

365
  // when providerTokens are loaded and there's no selected token or the token selected if from another network
366
  useEffect(() => {
321✔
367
    if (
140✔
368
      crossChainProvider?.isLibInitialized() &&
444✔
369
      ((!selectedToken && providerTokens.length) || // only run if not selectedToken, meaning the first render
370
        (selectedToken && selectedChain.toString() !== selectedToken.chainId)) // or if selectedToken is not from the selectedChain
371
    ) {
372
      const MANAToken = providerTokens.find(
29✔
373
        t => t.symbol === 'MANA' && selectedChain.toString() === t.chainId
45✔
374
      )
375
      setSelectedToken(MANAToken || getMANAToken(selectedChain)) // if it's not in the providerTokens, create the object manually with the right conectract address
29!
376
    }
377
  }, [
378
    calculateRoute,
379
    crossChainProvider,
380
    providerTokens,
381
    selectedChain,
382
    selectedToken
383
  ])
384

385
  // fetch selected token balance & price
386
  useEffect(() => {
321✔
387
    let cancel = false
111✔
388
    ;(async () => {
111✔
389
      try {
111✔
390
        setIsFetchingBalance(true)
111✔
391
        if (
111✔
392
          crossChainProvider &&
426✔
393
          crossChainProvider.isLibInitialized() &&
394
          selectedChain &&
395
          selectedToken &&
396
          selectedToken.symbol !== 'MANA' && // mana balance is already available in the wallet
397
          wallet
398
        ) {
399
          const networkProvider = await getNetworkProvider(selectedChain)
16✔
400
          const provider = new ethers.providers.Web3Provider(networkProvider)
16✔
401

402
          // if native token
403
          if (selectedToken.address === NATIVE_TOKEN) {
16!
NEW
404
            const balanceWei = await provider.getBalance(wallet.address)
×
NEW
405
            setSelectedTokenBalance(balanceWei)
×
406

NEW
407
            return
×
408
          }
409
          // else ERC20
410
          const tokenContract = new ethers.Contract(
16✔
411
            selectedToken.address,
412
            ['function balanceOf(address owner) view returns (uint256)'],
413
            provider
414
          )
415
          const balance: BigNumber = await tokenContract.balanceOf(
16✔
416
            wallet.address
417
          )
418

419
          if (!cancel) {
16!
420
            setSelectedTokenBalance(balance)
16✔
421
          }
422
        }
423
      } catch (error) {
NEW
424
        console.error('Error getting balance: ', error)
×
425
      } finally {
426
        if (!cancel) {
111!
427
          setIsFetchingBalance(false)
111✔
428
        }
429
      }
430
    })()
431
    return () => {
111✔
432
      cancel = true
111✔
433
    }
434
  }, [selectedToken, selectedChain, wallet, crossChainProvider])
435

436
  // computes if the user can buy the item with the selected token
437
  useEffect(() => {
321✔
438
    ;(async () => {
156✔
439
      if (
156✔
440
        selectedToken &&
326✔
441
        ((selectedToken.symbol === 'MANA' && !!wallet) ||
442
          (selectedToken.symbol !== 'MANA' && // MANA balance is calculated differently
443
            selectedTokenBalance))
444
      ) {
445
        let canBuy
446
        if (selectedToken.symbol === 'MANA' && wallet) {
53✔
447
          // wants to buy a L2 item with ETH MANA (through the provider)
448
          if (
37✔
449
            asset.network === Network.MATIC &&
59✔
450
            getNetwork(selectedChain) === Network.ETHEREUM
451
          ) {
452
            canBuy =
6✔
453
              wallet.networks[Network.ETHEREUM].mana >=
454
              +ethers.utils.formatEther(price)
455
          } else {
456
            canBuy =
31✔
457
              wallet.networks[asset.network].mana >=
458
              +ethers.utils.formatEther(price)
459
          }
460
        } else if (selectedTokenBalance) {
16!
461
          const balance = parseFloat(
16✔
462
            ethers.utils.formatUnits(
463
              selectedTokenBalance,
464
              selectedToken.decimals
465
            )
466
          )
467
          const destinyChainMANA = getContract(
16✔
468
            ContractName.MANAToken,
469
            asset.chainId
470
          ).address
471

472
          const providerMANA = providerTokens.find(
16✔
473
            t =>
474
              t.address.toLocaleLowerCase() ===
24✔
475
              destinyChainMANA.toLocaleLowerCase()
476
          )
477
          if (providerMANA && selectedToken && crossChainProvider) {
16!
478
            const fromAmountParams = {
16✔
479
              fromToken: selectedToken,
480
              toAmount: ethers.utils.formatEther(price),
481
              toToken: providerMANA
482
            }
483
            const from = await crossChainProvider.getFromAmount(
16✔
484
              fromAmountParams
485
            )
486
            const fromAmount = Number(from).toFixed(6)
16✔
487
            canBuy = balance > Number(fromAmount)
16✔
488
          }
489
        }
490
        setCanBuyItem(canBuy)
53✔
491
      }
492
    })()
493
  }, [
494
    asset,
495
    crossChainProvider,
496
    order,
497
    price,
498
    providerTokens,
499
    selectedChain,
500
    selectedToken,
501
    selectedTokenBalance,
502
    wallet
503
  ])
504

505
  // sets interval to refresh route within a certain amount of time
506
  useEffect(() => {
321✔
507
    let interval: NodeJS.Timeout | undefined = undefined
156✔
508
    if (route) {
156✔
509
      // setRouteSetInterval(
510
      interval = setInterval(() => {
16✔
NEW
511
        setIsFetchingRoute(true)
×
NEW
512
        calculateRoute()
×
513
      }, ROUTE_FETCH_INTERVAL)
514
      // )
515
    }
516
    return () => {
156✔
517
      if (interval) {
156✔
518
        clearInterval(interval)
16✔
519
      }
520
    }
521
  }, [calculateRoute, route])
522

523
  // when changing the selectedToken and it's not fetching route, trigger fetch route
524
  useEffect(() => {
321✔
525
    if (
204✔
526
      selectedToken &&
503✔
527
      !route &&
528
      !isFetchingRoute &&
529
      !useMetaTx &&
530
      !routeFailed
531
    ) {
532
      const isBuyingL1WithOtherTokenThanEthereumMANA =
533
        asset.chainId === ChainId.ETHEREUM_MAINNET &&
44✔
534
        selectedToken.chainId !== ChainId.ETHEREUM_MAINNET.toString() &&
535
        selectedToken.symbol !== 'MANA'
536

537
      const isPayingWithOtherTokenThanMANA = selectedToken.symbol !== 'MANA'
44✔
538
      const isPayingWithMANAButFromOtherChain =
539
        selectedToken.symbol === 'MANA' &&
44✔
540
        selectedToken.chainId !== asset.chainId.toString()
541

542
      if (
44✔
543
        isBuyingL1WithOtherTokenThanEthereumMANA ||
114✔
544
        isPayingWithOtherTokenThanMANA ||
545
        isPayingWithMANAButFromOtherChain
546
      ) {
547
        setIsFetchingRoute(true)
24✔
548
        calculateRoute()
24✔
549
      }
550
    }
551
  }, [
552
    route,
553
    useMetaTx,
554
    routeFailed,
555
    selectedToken,
556
    isFetchingRoute,
557
    selectedChain,
558
    asset.chainId,
559
    calculateRoute
560
  ])
561

562
  const onBuyWithCrypto = useCallback(async () => {
321✔
563
    if (route && crossChainProvider && crossChainProvider.isLibInitialized()) {
3!
564
      onBuyItemThroughProvider(route)
3✔
565
    }
566
  }, [crossChainProvider, onBuyItemThroughProvider, route])
567

568
  // useCallbacks
569

570
  const renderSwitchNetworkButton = useCallback(() => {
321✔
571
    return (
11✔
572
      <Button
573
        fluid
574
        inverted
575
        className={styles.switchNetworkButton}
576
        disabled={isSwitchingNetwork}
577
        data-testid={SWITCH_NETWORK_BUTTON_TEST_ID}
578
        onClick={() => onSwitchNetwork(selectedChain)}
5✔
579
      >
580
        {isSwitchingNetwork ? (
11!
581
          <>
582
            <Loader inline active size="tiny" />
583
            {t('buy_with_crypto_modal.confirm_switch_network')}
584
          </>
585
        ) : (
586
          t('buy_with_crypto_modal.switch_network', {
587
            chain: providerChains.find(
588
              c => c.chainId === selectedChain.toString()
16✔
589
            )?.networkName
590
          })
591
        )}
592
      </Button>
593
    )
594
  }, [isSwitchingNetwork, onSwitchNetwork, providerChains, selectedChain])
595

596
  const onBuyNFT = useCallback(() => {
321✔
597
    if (order) {
3!
598
      const contractNames = getContractNames()
3✔
599

600
      const mana = getContractProp({
3✔
601
        name: contractNames.MANA,
602
        network: asset.network
603
      }) as DCLContract
604

605
      const marketplace = getContractProp({
3✔
606
        address: order?.marketplaceAddress,
607
        network: asset.network
608
      }) as DCLContract
609

610
      onAuthorizedAction({
3✔
611
        targetContractName: ContractName.MANAToken,
612
        authorizationType: AuthorizationType.ALLOWANCE,
613
        authorizedAddress: order.marketplaceAddress,
614
        targetContract: mana as Contract,
615
        authorizedContractLabel: marketplace.label || marketplace.name,
6✔
616
        requiredAllowanceInWei: order.price,
617
        onAuthorized: alreadyAuthorized =>
618
          onExecuteOrder(order, asset as NFT, undefined, !alreadyAuthorized) // undefined as fingerprint
3✔
619
      })
620
    }
621
  }, [asset, order, getContractProp, onAuthorizedAction, onExecuteOrder])
622

623
  const onBuyItem = useCallback(() => {
321✔
624
    if (!isNFT(asset)) {
3!
625
      const contractNames = getContractNames()
3✔
626

627
      const mana = getContractProp({
3✔
628
        name: contractNames.MANA,
629
        network: asset.network
630
      }) as DCLContract
631

632
      const collectionStore = getContractProp({
3✔
633
        name: contractNames.COLLECTION_STORE,
634
        network: asset.network
635
      }) as DCLContract
636

637
      onAuthorizedAction({
3✔
638
        targetContractName: ContractName.MANAToken,
639
        authorizationType: AuthorizationType.ALLOWANCE,
640
        authorizedAddress: collectionStore.address,
641
        targetContract: mana as Contract,
642
        authorizedContractLabel: collectionStore.label || collectionStore.name,
6✔
643
        requiredAllowanceInWei: asset.price,
644
        onAuthorized: () => onBuyItemProp(asset)
3✔
645
      })
646
    }
647
  }, [asset, getContractProp, onAuthorizedAction, onBuyItemProp])
648

649
  const onBuyWithCard = useCallback(() => {
321✔
NEW
650
    analytics.track(events.CLICK_BUY_NFT_WITH_CARD)
×
NEW
651
    return !isNFT(asset)
×
652
      ? onBuyItemWithCard(asset)
653
      : !!order
×
654
      ? onExecuteOrderWithCard(asset)
655
      : () => {}
656
  }, [analytics, asset, onBuyItemWithCard, onExecuteOrderWithCard, order])
657

658
  const renderGetMANAButton = useCallback(() => {
321✔
659
    return (
55✔
660
      <>
661
        <Button
662
          fluid
663
          primary
664
          data-testid={GET_MANA_BUTTON_TEST_ID}
665
          loading={isFetchingBalance || isLoading}
110✔
666
          onClick={() => {
NEW
667
            onGetMana()
×
NEW
668
            onClose()
×
669
          }}
670
        >
671
          {t('buy_with_crypto_modal.get_mana')}
672
        </Button>
673
        <ChainButton
674
          inverted
675
          fluid
676
          chainId={asset.chainId}
677
          data-testid={BUY_WITH_CARD_TEST_ID}
678
          disabled={isLoading || isLoadingAuthorization}
110✔
679
          loading={isLoading || isLoadingAuthorization}
110✔
680
          onClick={onBuyWithCard}
681
        >
682
          <Icon name="credit card outline" />
683
          {t(`buy_with_crypto_modal.buy_with_card`)}
684
        </ChainButton>
685
      </>
686
    )
687
  }, [
688
    isFetchingBalance,
689
    isLoading,
690
    asset.chainId,
691
    isLoadingAuthorization,
692
    onBuyWithCard,
693
    onGetMana,
694
    onClose
695
  ])
696

697
  const renderBuyNowButton = useCallback(() => {
321✔
698
    let onClick =
699
      selectedToken?.symbol === 'MANA' && !route
11✔
700
        ? !!order
6✔
701
          ? onBuyNFT
702
          : onBuyItem
703
        : onBuyWithCrypto
704

705
    return (
11✔
706
      <>
707
        <Button
708
          fluid
709
          primary
710
          data-testid={BUY_NOW_BUTTON_TEST_ID}
711
          disabled={
712
            (selectedToken?.symbol !== 'MANA' && !route) ||
35✔
713
            isFetchingRoute ||
714
            isLoadingBuyCrossChain
715
          }
716
          loading={isFetchingBalance || isLoading}
22✔
717
          onClick={onClick}
718
        >
719
          <>
720
            {isLoadingBuyCrossChain || isFetchingRoute ? (
33✔
721
              <Loader inline active size="tiny" />
722
            ) : null}
723
            {!isFetchingRoute // if fetching route, just render the Loader
11✔
724
              ? isLoadingBuyCrossChain
9!
725
                ? t('buy_with_crypto_modal.confirm_transaction')
726
                : t('buy_with_crypto_modal.buy_now')
727
              : null}
728
          </>
729
        </Button>
730
      </>
731
    )
732
  }, [
733
    route,
734
    order,
735
    selectedToken,
736
    isFetchingRoute,
737
    isLoadingBuyCrossChain,
738
    isFetchingBalance,
739
    isLoading,
740
    onBuyNFT,
741
    onBuyItem,
742
    onBuyWithCrypto
743
  ])
744

745
  const renderMainActionButton = useCallback(() => {
321✔
746
    if (wallet && selectedToken && canBuyItem !== undefined) {
297✔
747
      if (canBuyItem) {
77✔
748
        // it's paying with MANA but connected on Ethereum
749
        if (
22✔
750
          selectedToken.symbol === 'MANA' &&
30✔
751
          wallet.network === Network.ETHEREUM
752
        ) {
753
          return asset.network === Network.ETHEREUM // if it's buying a L1 NFT, render buy now
5✔
754
            ? renderBuyNowButton()
755
            : isPriceTooLow(price) // if it's too low for a meta tx, render switch button
3✔
756
            ? renderSwitchNetworkButton()
757
            : renderBuyNowButton() // else, buy button
758
        }
759
        // for any other token, it needs to be connected on the selectedChain network
760
        return selectedChain === wallet.chainId
17✔
761
          ? renderBuyNowButton()
762
          : renderSwitchNetworkButton()
763
      } else {
764
        // can't buy Get Mana and Buy With Card buttons
765
        return renderGetMANAButton()
55✔
766
      }
767
    } else if (!route && routeFailed) {
220!
768
      // can't buy Get Mana and Buy With Card buttons
NEW
769
      return renderGetMANAButton()
×
770
    }
771
  }, [
772
    wallet,
773
    selectedToken,
774
    canBuyItem,
775
    route,
776
    asset,
777
    price,
778
    routeFailed,
779
    selectedChain,
780
    renderBuyNowButton,
781
    renderSwitchNetworkButton,
782
    renderGetMANAButton
783
  ])
784

785
  const renderTokenBalance = useCallback(() => {
321✔
786
    let balance
787
    if (selectedToken && selectedToken.symbol === 'MANA') {
210✔
788
      balance = wallet?.networks[getNetwork(selectedChain)]?.mana.toFixed(2)
74✔
789
    } else if (selectedToken && selectedTokenBalance) {
136✔
790
      balance = Number(
72✔
791
        ethers.utils.formatUnits(selectedTokenBalance, selectedToken.decimals)
792
      ).toFixed(4)
793
    }
794

795
    return !isFetchingBalance ? (
210✔
796
      <span className={styles.balance}>{balance}</span>
797
    ) : (
798
      <div className={styles.balanceSkeleton} />
799
    )
800
  }, [
801
    wallet,
802
    isFetchingBalance,
803
    selectedChain,
804
    selectedToken,
805
    selectedTokenBalance
806
  ])
807

808
  const onTokenOrChainSelection = useCallback(
321✔
809
    (selectedOption: Token | ChainData) => {
810
      setShowChainSelector(false)
24✔
811
      setShowTokenSelector(false)
24✔
812

813
      if (isToken(selectedOption)) {
24✔
814
        abortControllerRef.current.abort()
16✔
815

816
        const selectedToken = providerTokens.find(
16✔
817
          t =>
818
            t.address === selectedOption.address &&
60✔
819
            t.chainId === selectedChain.toString()
820
        ) as Token
821
        // reset all fields
822
        setSelectedToken(selectedToken)
16✔
823
        setFromAmount(undefined)
16✔
824
        setSelectedTokenBalance(undefined)
16✔
825
        setCanBuyItem(undefined)
16✔
826
        setRoute(undefined)
16✔
827
        setRouteFailed(false)
16✔
828
        abortControllerRef.current = new AbortController()
16✔
829
        analytics.track(events.CROSS_CHAIN_TOKEN_SELECTION, {
16✔
830
          selectedToken
831
        })
832
      } else {
833
        setSelectedChain(Number(selectedOption.chainId) as ChainId)
8✔
834
        const manaDestinyChain = providerTokens.find(
8✔
835
          t => t.symbol === 'MANA' && t.chainId === selectedOption.chainId
10✔
836
        )
837
        // set the selected token on the new chain selected to MANA or the first one found
838
        setSelectedToken(
8✔
839
          manaDestinyChain ||
8!
NEW
840
            providerTokens.find(t => t.chainId === selectedOption.chainId)
×
841
        )
842
        setRoute(undefined)
8✔
843
        setRouteFailed(false)
8✔
844

845
        analytics.track(events.CROSS_CHAIN_CHAIN_SELECTION, {
8✔
846
          selectedChain
847
        })
848
      }
849
    },
850
    [analytics, providerTokens, selectedChain]
851
  )
852

853
  const renderModalNavigation = useCallback(() => {
321✔
854
    if (showChainSelector || showTokenSelector) {
321✔
855
      return (
24✔
856
        <ModalNavigation
857
          title={t(
858
            `buy_with_crypto_modal.token_and_chain_selector.select_${
859
              showChainSelector ? 'chain' : 'token'
24✔
860
            }`
861
          )}
862
          onBack={() => {
NEW
863
            setShowChainSelector(false)
×
NEW
864
            setShowTokenSelector(false)
×
865
          }}
866
        />
867
      )
868
    }
869
    return (
297✔
870
      <ModalNavigation
871
        title={t('buy_with_crypto_modal.title', {
872
          name: asset.name,
NEW
873
          b: (children: React.ReactChildren) => <b>{children}</b>
×
874
        })}
875
        onClose={onClose}
876
      />
877
    )
878
  }, [asset.name, onClose, showChainSelector, showTokenSelector])
879

880
  const translationPageDescriptorId = compact([
321✔
881
    'mint',
882
    isWearableOrEmote(asset)
321!
883
      ? isBuyWithCardPage
321!
884
        ? 'with_card'
885
        : 'with_mana'
886
      : null,
887
    'page'
888
  ]).join('_')
889

890
  return (
321✔
891
    <Modal size="tiny" onClose={onClose} className={styles.buyWithCryptoModal}>
892
      {renderModalNavigation()}
893
      <Modal.Content>
894
        <>
895
          {showChainSelector || showTokenSelector ? (
955✔
896
            <div>
897
              {showChainSelector ? (
24✔
898
                <ChainAndTokenSelector
899
                  currentChain={selectedChain}
900
                  chains={providerChains}
901
                  onSelect={onTokenOrChainSelection}
902
                />
903
              ) : null}
904
              {showTokenSelector ? (
24✔
905
                <ChainAndTokenSelector
906
                  currentChain={selectedChain}
907
                  tokens={providerTokens}
908
                  onSelect={onTokenOrChainSelection}
909
                />
910
              ) : null}
911
            </div>
912
          ) : (
913
            <>
914
              <div className={styles.assetContainer}>
915
                <AssetImage asset={asset} isSmall />
916
                <span className={styles.assetName}>{asset.name}</span>
917
                <div className={styles.priceContainer}>
918
                  <Mana network={asset.network} inline withTooltip>
919
                    {formatWeiMANA(price)}
920
                  </Mana>
921
                  <span className={styles.priceInUSD}>
922
                    <ManaToFiat mana={price} digits={4} />
923
                  </span>
924
                </div>
925
              </div>
926

927
              {!providerTokens.length || !selectedToken ? (
833✔
928
                <Loader inline active className={styles.mainLoader} />
929
              ) : (
930
                <div
931
                  className={styles.payWithContainer}
932
                  data-testid={PAY_WITH_DATA_TEST_ID}
933
                >
934
                  <div className={styles.dropdownContainer}>
935
                    <div>
936
                      <span>{t('buy_with_crypto_modal.pay_with')}</span>
937
                      <div
938
                        className={styles.tokenAndChainSelector}
939
                        data-testid={CHAIN_SELECTOR_DATA_TEST_ID}
940
                        onClick={() => setShowChainSelector(true)}
8✔
941
                      >
942
                        <img
943
                          src={selectedProviderChain?.nativeCurrency.icon}
944
                          alt={selectedProviderChain?.nativeCurrency.name}
945
                        />
946
                        <span className={styles.tokenAndChainSelectorName}>
947
                          {' '}
948
                          {selectedProviderChain?.networkName}{' '}
949
                        </span>
950
                        <Icon name="chevron down" />
951
                      </div>
952
                    </div>
953
                    <div className={styles.tokenDropdownContainer}>
954
                      <div
955
                        className={classNames(
956
                          styles.tokenAndChainSelector,
957
                          styles.tokenDropdown
958
                        )}
959
                        data-testid={TOKEN_SELECTOR_DATA_TEST_ID}
960
                        onClick={() => setShowTokenSelector(true)}
16✔
961
                      >
962
                        <img
963
                          src={selectedToken.logoURI}
964
                          alt={selectedToken.name}
965
                        />
966
                        <span className={styles.tokenAndChainSelectorName}>
967
                          {selectedToken.symbol}{' '}
968
                        </span>
969
                        <div className={styles.balanceContainer}>
970
                          {t('buy_with_crypto_modal.balance')}:{' '}
971
                          {renderTokenBalance()}
972
                        </div>
973
                        <Icon name="chevron down" />
974
                      </div>
975
                    </div>
976
                  </div>
977
                  <div className={styles.costContainer}>
978
                    {!!selectedToken ? (
210!
979
                      <>
980
                        <div className={styles.itemCost}>
981
                          <>
982
                            <div className={styles.itemCostLabels}>
983
                              {t('buy_with_crypto_modal.item_cost')}
984
                            </div>
985
                            <div className={styles.fromAmountContainer}>
986
                              <div className={styles.fromAmountTokenContainer}>
987
                                <img
988
                                  src={selectedToken?.logoURI}
989
                                  alt={selectedToken?.name}
990
                                />
991
                                {selectedToken.symbol === 'MANA' ? (
210✔
992
                                  ethers.utils.formatEther(price)
993
                                ) : !!fromAmount ? (
136✔
994
                                  fromAmount
995
                                ) : (
996
                                  <span
997
                                    className={classNames(
998
                                      styles.skeleton,
999
                                      styles.estimatedFeeSkeleton
1000
                                    )}
1001
                                  />
1002
                                )}
1003
                              </div>
1004
                              {selectedToken.usdPrice ? (
210!
1005
                                fromAmount ||
518✔
1006
                                selectedToken.symbol === 'MANA' ? (
1007
                                  <span className={styles.fromAmountUSD}>
1008
                                    ≈{' '}
1009
                                    {!!route ? (
186✔
1010
                                      <>
1011
                                        $
1012
                                        {(
1013
                                          Number(fromAmount) *
1014
                                          selectedToken.usdPrice
1015
                                        ).toFixed(4)}
1016
                                      </>
1017
                                    ) : (
1018
                                      <ManaToFiat mana={price} digits={4} />
1019
                                    )}
1020
                                  </span>
1021
                                ) : null
1022
                              ) : null}
1023
                            </div>
1024
                          </>
1025
                        </div>
1026

1027
                        {shouldUseCrossChainProvider ? (
210✔
1028
                          <div className={styles.itemCost}>
1029
                            <div className={styles.feeCostContainer}>
1030
                              {t('buy_with_crypto_modal.fee_cost')}
1031
                              <Popup
1032
                                content={t(
1033
                                  'best_buying_option.minting.minting_popup'
1034
                                )}
1035
                                style={{ zIndex: 3001 }}
1036
                                position="top center"
1037
                                className={styles.infoIconPopUp}
1038
                                trigger={
1039
                                  <img
1040
                                    src={infoIcon}
1041
                                    alt="info"
1042
                                    className={styles.informationTooltip}
1043
                                  />
1044
                                }
1045
                                on="hover"
1046
                              />
1047
                            </div>
1048
                            <div className={styles.fromAmountContainer}>
1049
                              {!!route && routeFeeCost ? (
360✔
1050
                                <div
1051
                                  className={styles.fromAmountTokenContainer}
1052
                                >
1053
                                  <img
1054
                                    src={
1055
                                      route.route.estimate.gasCosts[0].token
1056
                                        .logoURI
1057
                                    }
1058
                                    alt={
1059
                                      route.route.estimate.gasCosts[0].token
1060
                                        .name
1061
                                    }
1062
                                  />
1063
                                  {routeFeeCost.totalCost}
1064
                                </div>
1065
                              ) : (
1066
                                <div
1067
                                  className={classNames(
1068
                                    styles.skeleton,
1069
                                    styles.estimatedFeeSkeleton
1070
                                  )}
1071
                                />
1072
                              )}
1073
                              {!!routeFeeCost && routeFeeCost.token.usdPrice ? (
360✔
1074
                                <span className={styles.fromAmountUSD}>
1075
                                  ≈ $
1076
                                  {(
1077
                                    (Number(routeFeeCost.feeCost) +
1078
                                      Number(routeFeeCost.gasCost)) *
1079
                                    routeFeeCost.token.usdPrice
1080
                                  ).toFixed(4)}
1081
                                </span>
1082
                              ) : null}
1083
                            </div>
1084
                          </div>
1085
                        ) : null}
1086
                      </>
1087
                    ) : null}
1088
                  </div>
1089
                </div>
1090
              )}
1091
              <div className={styles.totalContainer}>
1092
                <div>
1093
                  <span className={styles.total}>
1094
                    {t('buy_with_crypto_modal.total')}
1095
                  </span>
1096
                  {useMetaTx && !isPriceTooLow(price) ? (
612✔
1097
                    <span
1098
                      className={styles.feeCovered}
1099
                      data-testid={FREE_TX_CONVERED_TEST_ID}
1100
                    >
1101
                      {t('buy_with_crypto_modal.transaction_fee_covered', {
1102
                        covered: (
1103
                          <span className={styles.feeCoveredFree}>
1104
                            {t('buy_with_crypto_modal.covered_by_dao')}
1105
                          </span>
1106
                        )
1107
                      })}
1108
                    </span>
1109
                  ) : null}
1110
                </div>
1111
                <div className={styles.totalPrice}>
1112
                  <div>
1113
                    {!!selectedToken ? (
297✔
1114
                      shouldUseCrossChainProvider ? (
210✔
1115
                        !!route && routeFeeCost ? (
360✔
1116
                          <>
1117
                            <img
1118
                              src={selectedToken?.logoURI}
1119
                              alt={selectedToken?.name}
1120
                            />
1121
                            {routeFeeCost?.token.symbol !==
168!
1122
                              selectedToken.symbol && fromAmount ? (
1123
                              <>
1124
                                {formatPrice(fromAmount, selectedToken)}
1125
                                <span> + </span>
1126
                                <img
1127
                                  src={routeFeeCost.token.logoURI}
1128
                                  alt={routeFeeCost.token.name}
1129
                                />
1130
                                {formatPrice(
1131
                                  routeFeeCost.totalCost,
1132
                                  routeFeeCost.token
1133
                                )}
1134
                              </>
1135
                            ) : (
1136
                              <>
1137
                                {formatPrice(
1138
                                  Number(fromAmount) +
1139
                                    Number(routeFeeCost.totalCost),
1140
                                  selectedToken
1141
                                )}
1142
                              </>
1143
                            )}
1144
                          </>
1145
                        ) : isFetchingRoute ? (
96✔
1146
                          <span
1147
                            className={classNames(
1148
                              styles.skeleton,
1149
                              styles.estimatedFeeSkeleton
1150
                            )}
1151
                          />
1152
                        ) : null
1153
                      ) : (
1154
                        <>
1155
                          <img
1156
                            src={selectedToken?.logoURI}
1157
                            alt={selectedToken?.name}
1158
                          />
1159
                          {ethers.utils.formatEther(price)}
1160
                        </>
1161
                      )
1162
                    ) : null}
1163
                  </div>
1164
                  <div>
1165
                    <span className={styles.fromAmountUSD}>
1166
                      {shouldUseCrossChainProvider ? (
297✔
1167
                        <>
1168
                          {' '}
1169
                          {!!route
152✔
1170
                            ? `$${routeTotalUSDCost?.toFixed(6)}`
1171
                            : null}{' '}
1172
                        </>
1173
                      ) : (
1174
                        <ManaToFiat mana={price} digits={4} />
1175
                      )}
1176
                    </span>
1177
                  </div>
1178
                </div>
1179
              </div>
1180
              {selectedToken && shouldUseCrossChainProvider ? (
804✔
1181
                <div className={styles.durationAndExchangeContainer}>
1182
                  <div>
1183
                    <span>
1184
                      <Icon name="clock outline" />{' '}
1185
                      {t(
1186
                        'buy_with_crypto_modal.durations.transaction_duration'
1187
                      )}{' '}
1188
                    </span>
1189
                    {route ? (
152✔
1190
                      t(
1191
                        `buy_with_crypto_modal.durations.${
1192
                          route.route.estimate.estimatedRouteDuration === 0
56!
1193
                            ? 'fast'
1194
                            : route.route.estimate.estimatedRouteDuration === 20
×
1195
                            ? 'normal'
1196
                            : 'slow'
1197
                        }`
1198
                      )
1199
                    ) : (
1200
                      <span
1201
                        className={classNames(
1202
                          styles.skeleton,
1203
                          styles.fromAmountUSDSkeleton
1204
                        )}
1205
                      />
1206
                    )}
1207
                  </div>
1208
                  <div className={styles.exchangeContainer}>
1209
                    <div className={styles.exchangeContainerLabel}>
1210
                      <span className={styles.exchangeIcon} />
1211
                      <span> {t('buy_with_crypto_modal.exchange_rate')} </span>
1212
                    </div>
1213
                    {route && selectedToken ? (
360✔
1214
                      <>
1215
                        1 {selectedToken.symbol} ={' '}
1216
                        {route.route.estimate.exchangeRate?.slice(0, 7)} MANA
1217
                      </>
1218
                    ) : (
1219
                      <span
1220
                        className={classNames(
1221
                          styles.skeleton,
1222
                          styles.fromAmountUSDSkeleton
1223
                        )}
1224
                      />
1225
                    )}
1226
                  </div>
1227
                </div>
1228
              ) : null}
1229

1230
              {selectedToken &&
1,038✔
1231
              shouldUseCrossChainProvider &&
1232
              asset.network === Network.MATIC && // and it's buying a MATIC wearable
1233
              !isPriceTooLow(price) ? (
1234
                <span className={styles.rememberFreeTxs}>
1235
                  {t('buy_with_crypto_modal.remember_transaction_fee_covered', {
1236
                    free: (
1237
                      <span className={styles.feeCoveredFree}>
1238
                        {t('buy_with_crypto_modal.free')}
1239
                      </span>
1240
                    )
1241
                  })}
1242
                </span>
1243
              ) : null}
1244

1245
              {hasLowPriceForMetaTx && !isBuyWithCardPage && useMetaTx ? (
644✔
1246
                <span
1247
                  className={styles.warning}
1248
                  data-testid={PRICE_TOO_LOW_TEST_ID}
1249
                >
1250
                  {' '}
1251
                  {t('buy_with_crypto_modal.price_too_low', {
1252
                    learn_more: (
1253
                      <a
1254
                        href="https://docs.decentraland.org"
1255
                        target="_blank"
1256
                        rel="noreferrer"
1257
                      >
1258
                        {/* TODO: add this URL */}
1259
                        <u> {t('buy_with_crypto_modal.learn_more')} </u>
1260
                      </a>
1261
                    )
1262
                  })}
1263
                </span>
1264
              ) : null}
1265
              {canBuyItem === false && isWearableOrEmote(asset) ? (
649✔
1266
                <span className={styles.warning}>
1267
                  {t('buy_with_crypto_modal.insufficient_funds', {
1268
                    token: selectedToken?.symbol || 'MANA'
55!
1269
                  })}
1270
                </span>
1271
              ) : null}
1272
              {routeFailed && selectedToken ? (
594!
1273
                <span className={styles.warning}>
1274
                  {' '}
1275
                  {t('buy_with_crypto_modal.route_unavailable', {
1276
                    token: selectedToken.symbol
1277
                  })}
1278
                </span>
1279
              ) : null}
1280
            </>
1281
          )}
1282
        </>
1283
      </Modal.Content>
1284
      {showChainSelector || showTokenSelector ? null : (
955✔
1285
        <Modal.Actions>
1286
          <div
1287
            className={classNames(
1288
              styles.buttons,
1289
              isWearableOrEmote(asset) && 'with-mana'
594✔
1290
            )}
1291
          >
1292
            {renderMainActionButton()}
1293
          </div>
1294
          {isWearableOrEmote(asset) && isBuyWithCardPage ? (
891!
1295
            <CardPaymentsExplanation
1296
              translationPageDescriptorId={translationPageDescriptorId}
1297
            />
1298
          ) : null}
1299
        </Modal.Actions>
1300
      )}
1301
    </Modal>
1302
  )
1303
}
1304

1305
export const BuyNFTWithCryptoModal = React.memo(
1✔
1306
  withAuthorizedAction(
1307
    BuyWithCryptoModal,
1308
    AuthorizedAction.BUY,
1309
    {
1310
      action: 'buy_with_mana_page.authorization.action',
1311
      title_action: 'buy_with_mana_page.authorization.title_action'
1312
    },
1313
    getBuyItemStatus,
1314
    getError
1315
  )
1316
)
1317

1318
export const MintNFTWithCryptoModal = React.memo(
1✔
1319
  withAuthorizedAction(
1320
    BuyWithCryptoModal,
1321
    AuthorizedAction.MINT,
1322
    {
1323
      action: 'mint_with_mana_page.authorization.action',
1324
      title_action: 'mint_with_mana_page.authorization.title_action'
1325
    },
1326
    getMintItemStatus,
1327
    getError
1328
  )
1329
)
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