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

decentraland / marketplace / 6970244998

23 Nov 2023 01:18PM UTC coverage: 43.312% (+2.4%) from 40.865%
6970244998

Pull #2042

github

juanmahidalgo
feat: add missing select token and chain events
Pull Request #2042: feat: use squid to calculate route between two chains and tokens

2626 of 7307 branches covered (0.0%)

Branch coverage included in aggregate %.

277 of 382 new or added lines in 20 files covered. (72.51%)

1 existing line in 1 file now uncovered.

4634 of 9455 relevant lines covered (49.01%)

23.67 hits per line

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

88.75
/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 '../../../lib/xchain'
33
import { AssetImage } from '../../AssetImage'
34
import { isPriceTooLow } from '../../BuyPage/utils'
35
import { CardPaymentsExplanation } from '../../BuyPage/CardPaymentsExplanation'
36
import { ManaToFiat } from '../../ManaToFiat'
37
import { getBuyItemStatus, getError } from '../../../modules/order/selectors'
38
import { getMintItemStatus } from '../../../modules/item/selectors'
39
import { NFT } from '../../../modules/nft/types'
40
import ChainAndTokenSelector from './ChainAndTokenSelector/ChainAndTokenSelector'
41
import { formatPrice, getMANAToken, getShouldUseMetaTx, isToken } from './utils'
42
import { Props } from './BuyWithCryptoModal.types'
43
import styles from './BuyWithCryptoModal.module.css'
44

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

56
const NATIVE_TOKEN = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE'
1✔
57
const ROUTE_FETCH_INTERVAL = 10000000 // 10 secs
1✔
58

59
export type ProviderChain = ChainData
60
export type ProviderToken = Token
61

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

83
  const analytics = getAnalytics()
292✔
84
  const destinyChainMANA = getContract(ContractName.MANAToken, asset.chainId)
292✔
85
    .address
86
  const abortControllerRef = useRef(new AbortController())
292✔
87

88
  // useStates
89
  const [providerChains, setProviderChains] = useState<ChainData[]>([])
292✔
90
  const [providerTokens, setProviderTokens] = useState<Token[]>([])
292✔
91
  const [selectedChain, setSelectedChain] = useState(asset.chainId)
292✔
92
  const [selectedToken, setSelectedToken] = useState<Token>()
292✔
93
  const [isFetchingBalance, setIsFetchingBalance] = useState(false)
292✔
94
  const [isFetchingRoute, setIsFetchingRoute] = useState(false)
292✔
95
  const [selectedTokenBalance, setSelectedTokenBalance] = useState<BigNumber>()
292✔
96
  const [route, setRoute] = useState<RouteResponse>()
292✔
97
  const [routeFailed, setRouteFailed] = useState(false)
292✔
98
  const [canBuyItem, setCanBuyItem] = useState<boolean | undefined>(undefined)
292✔
99
  const [fromAmount, setFromAmount] = useState<string | undefined>(undefined)
292✔
100
  const [showChainSelector, setShowChainSelector] = useState(false)
292✔
101
  const [showTokenSelector, setShowTokenSelector] = useState(false)
292✔
102

103
  useEffect(() => {
292✔
104
    crossChainProvider.init() // init the provider on the mount
29✔
105
  }, [])
106

107
  // useMemos
108

109
  // if the tx should be done through the provider
110
  const shouldUseCrossChainProvider = useMemo(
292✔
111
    () =>
112
      selectedToken &&
82✔
113
      !(
114
        (
115
          (selectedToken.symbol === 'MANA' &&
185✔
116
            getNetwork(selectedChain) === Network.MATIC &&
117
            asset.network === Network.MATIC) || // MANA selected and it's sending the tx from MATIC
118
          (selectedToken.symbol === 'MANA' &&
119
            getNetwork(selectedChain) === Network.ETHEREUM &&
120
            asset.network === Network.ETHEREUM)
121
        ) // MANA selected and it's connected to ETH and buying a L1 NFT
122
      ),
123
    [asset.network, selectedChain, selectedToken]
124
  )
125

126
  // Compute if the process should use a meta tx (connected in ETH and buying a L2 NFT)
127
  const useMetaTx = useMemo(() => {
292✔
128
    return (
82✔
129
      !!selectedToken &&
188✔
130
      !!wallet &&
131
      getShouldUseMetaTx(
132
        asset,
133
        selectedChain,
134
        selectedToken.address,
135
        destinyChainMANA,
136
        wallet.network
137
      )
138
    )
139
  }, [asset, destinyChainMANA, selectedChain, selectedToken, wallet])
140

141
  const selectedProviderChain = useMemo(() => {
292✔
142
    return providerChains.find(c => c.chainId === selectedChain.toString())
66✔
143
  }, [providerChains, selectedChain])
144

145
  // the price of the order or the item
146
  const price = useMemo(
292✔
147
    () => (order ? order.price : !isNFT(asset) ? asset.price : ''),
29!
148
    [asset, order]
149
  )
150

151
  // Compute if the price is too low for meta tx
152
  const hasLowPriceForMetaTx = useMemo(
292✔
153
    () => wallet?.chainId !== ChainId.MATIC_MAINNET && isPriceTooLow(price), // not connected to polygon AND has price < minimun for meta tx
29✔
154
    [price, wallet?.chainId]
155
  )
156

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

198
  const routeTotalUSDCost = useMemo(() => {
292✔
199
    if (
114✔
200
      route &&
178✔
201
      routeFeeCost &&
202
      routeFeeCost.token.usdPrice &&
203
      fromAmount &&
204
      selectedToken?.usdPrice
205
    ) {
206
      const { feeCost, gasCost } = routeFeeCost
16✔
207
      return (
16✔
208
        routeFeeCost.token.usdPrice * (Number(gasCost) + Number(feeCost)) +
209
        selectedToken.usdPrice * Number(fromAmount)
210
      )
211
    }
212
  }, [fromAmount, route, routeFeeCost, selectedToken])
213

214
  // useEffects
215

216
  // init lib if necessary and fetch chains & supported tokens
217
  useEffect(() => {
292✔
218
    ;(async () => {
29✔
219
      try {
29✔
220
        if (crossChainProvider) {
29!
221
          if (!crossChainProvider.isLibInitialized()) {
29!
NEW
222
            await crossChainProvider.init()
×
223
          }
224
          const supportedTokens = crossChainProvider.getSupportedTokens()
29✔
225
          const supportedChains = crossChainProvider.getSupportedChains()
29✔
226
          // 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)
227
          if (
29!
228
            !supportedChains.find(c => +c.chainId === ChainId.MATIC_MAINNET)
58✔
229
          ) {
NEW
230
            supportedChains.push({
×
231
              chainId: ChainId.MATIC_MAINNET.toString(),
232
              networkName: 'Polygon',
233
              nativeCurrency: {
234
                name: 'MATIC',
235
                symbol: 'MATIC',
236
                decimals: 18,
237
                icon:
238
                  'https://raw.githubusercontent.com/0xsquid/assets/main/images/tokens/matic.svg'
239
              }
240
            } as ChainData)
241
          }
242
          setProviderChains(
29✔
243
            supportedChains.filter(c =>
244
              CROSS_CHAIN_SUPPORTED_CHAINS.includes(+c.chainId)
58✔
245
            )
246
          )
247
          setProviderTokens(
29✔
248
            supportedTokens.filter(t =>
249
              CROSS_CHAIN_SUPPORTED_CHAINS.includes(+t.chainId)
116✔
250
            )
251
          )
252
        }
253
      } catch (error) {
NEW
254
        console.log('error: ', error)
×
255
      }
256
    })()
257
  }, [wallet])
258

259
  // calculates Route for the selectedToken
260
  const calculateRoute = useCallback(async () => {
292✔
261
    const abortController = abortControllerRef.current
24✔
262
    const signal = abortController.signal
24✔
263

264
    const providerMANA = providerTokens.find(
24✔
265
      t =>
266
        t.address.toLocaleLowerCase() === destinyChainMANA.toLocaleLowerCase()
38✔
267
    )
268
    if (
24!
269
      !crossChainProvider ||
120✔
270
      !crossChainProvider.isLibInitialized() ||
271
      !wallet ||
272
      !selectedToken ||
273
      !providerMANA
274
    ) {
NEW
275
      return
×
276
    }
277
    try {
24✔
278
      setRoute(undefined)
24✔
279
      setIsFetchingRoute(true)
24✔
280
      setRouteFailed(false)
24✔
281
      let route: RouteResponse | undefined = undefined
24✔
282
      const fromAmountParams = {
24✔
283
        fromToken: selectedToken,
284
        toAmount: ethers.utils.formatEther(
285
          order
24!
286
            ? order.price
287
            : !isNFT(asset) && +asset.price > 0
72!
288
            ? asset.price
289
            : 1 //TODO: review this
290
        ),
291
        toToken: providerMANA
292
      }
293
      const fromAmount = Number(
24✔
294
        await crossChainProvider.getFromAmount(fromAmountParams)
295
      ).toFixed(6)
296
      setFromAmount(fromAmount)
24✔
297

298
      const fromAmountWei = ethers.utils
24✔
299
        .parseUnits(fromAmount.toString(), selectedToken.decimals)
300
        .toString()
301

302
      const baseRouteConfig = {
24✔
303
        fromAddress: wallet.address,
304
        fromAmount: fromAmountWei,
305
        fromChain: selectedChain,
306
        fromToken: selectedToken.address
307
      }
308

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

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

360
  // when providerTokens are loaded and there's no selected token or the token selected if from another network
361
  useEffect(() => {
292✔
362
    if (
111✔
363
      crossChainProvider.initialized &&
415✔
364
      ((!selectedToken && providerTokens.length) || // only run if not selectedToken, meaning the first render
365
        (selectedToken && selectedChain.toString() !== selectedToken.chainId)) // or if selectedToken is not from the selectedChain
366
    ) {
367
      const MANAToken = providerTokens.find(
29✔
368
        t => t.symbol === 'MANA' && selectedChain.toString() === t.chainId
45✔
369
      )
370
      setSelectedToken(MANAToken || getMANAToken(selectedChain)) // if it's not in the providerTokens, create the object manually with the right conectract address
29!
371
    }
372
  }, [calculateRoute, providerTokens, selectedChain, selectedToken])
373

374
  // fetch selected token balance & price
375
  useEffect(() => {
292✔
376
    let cancel = false
82✔
377
    ;(async () => {
82✔
378
      try {
82✔
379
        setIsFetchingBalance(true)
82✔
380
        if (
82✔
381
          crossChainProvider &&
397✔
382
          crossChainProvider.isLibInitialized() &&
383
          selectedChain &&
384
          selectedToken &&
385
          selectedToken.symbol !== 'MANA' && // mana balance is already available in the wallet
386
          wallet
387
        ) {
388
          const networkProvider = await getNetworkProvider(selectedChain)
16✔
389
          const provider = new ethers.providers.Web3Provider(networkProvider)
16✔
390

391
          // if native token
392
          if (selectedToken.address === NATIVE_TOKEN) {
16!
NEW
393
            const balanceWei = await provider.getBalance(wallet.address)
×
NEW
394
            setSelectedTokenBalance(balanceWei)
×
395

NEW
396
            return
×
397
          }
398
          // else ERC20
399
          const tokenContract = new ethers.Contract(
16✔
400
            selectedToken.address,
401
            ['function balanceOf(address owner) view returns (uint256)'],
402
            provider
403
          )
404
          const balance: BigNumber = await tokenContract.balanceOf(
16✔
405
            wallet.address
406
          )
407

408
          if (!cancel) {
16!
409
            setSelectedTokenBalance(balance)
16✔
410
          }
411
        }
412
      } catch (error) {
NEW
413
        console.error('Error getting balance: ', error)
×
414
      } finally {
415
        if (!cancel) {
82!
416
          setIsFetchingBalance(false)
82✔
417
        }
418
      }
419
    })()
420
    return () => {
82✔
421
      cancel = true
82✔
422
    }
423
  }, [selectedToken, selectedChain, wallet])
424

425
  // computes if the user can buy the item with the selected token
426
  useEffect(() => {
292✔
427
    ;(async () => {
127✔
428
      if (
127✔
429
        selectedToken &&
297✔
430
        ((selectedToken.symbol === 'MANA' && !!wallet) ||
431
          (selectedToken.symbol !== 'MANA' && // MANA balance is calculated differently
432
            selectedTokenBalance))
433
      ) {
434
        let canBuy
435
        if (selectedToken.symbol === 'MANA' && wallet) {
53✔
436
          // wants to buy a L2 item with ETH MANA (through the provider)
437
          if (
37✔
438
            asset.network === Network.MATIC &&
59✔
439
            getNetwork(selectedChain) === Network.ETHEREUM
440
          ) {
441
            canBuy =
6✔
442
              wallet.networks[Network.ETHEREUM].mana >=
443
              +ethers.utils.formatEther(price)
444
          } else {
445
            canBuy =
31✔
446
              wallet.networks[asset.network].mana >=
447
              +ethers.utils.formatEther(price)
448
          }
449
        } else if (selectedTokenBalance) {
16!
450
          const balance = parseFloat(
16✔
451
            ethers.utils.formatUnits(
452
              selectedTokenBalance,
453
              selectedToken.decimals
454
            )
455
          )
456
          const destinyChainMANA = getContract(
16✔
457
            ContractName.MANAToken,
458
            asset.chainId
459
          ).address
460

461
          const providerMANA = providerTokens.find(
16✔
462
            t =>
463
              t.address.toLocaleLowerCase() ===
24✔
464
              destinyChainMANA.toLocaleLowerCase()
465
          )
466
          if (providerMANA && selectedToken) {
16!
467
            const fromAmountParams = {
16✔
468
              fromToken: selectedToken,
469
              toAmount: ethers.utils.formatEther(price),
470
              toToken: providerMANA
471
            }
472
            const from = await crossChainProvider.getFromAmount(
16✔
473
              fromAmountParams
474
            )
475
            const fromAmount = Number(from).toFixed(6)
16✔
476
            canBuy = balance > Number(fromAmount)
16✔
477
          }
478
        }
479
        setCanBuyItem(canBuy)
53✔
480
        // setCanBuyItem(balance > Number(fromAmount))
481
      }
482
    })()
483
  }, [
484
    asset,
485
    order,
486
    price,
487
    providerTokens,
488
    selectedChain,
489
    selectedToken,
490
    selectedTokenBalance,
491
    wallet
492
  ])
493

494
  // sets interval to refresh route within a certain amount of time
495
  useEffect(() => {
292✔
496
    let interval: NodeJS.Timeout | undefined = undefined
127✔
497
    if (route) {
127✔
498
      // setRouteSetInterval(
499
      interval = setInterval(() => {
16✔
NEW
500
        setIsFetchingRoute(true)
×
NEW
501
        calculateRoute()
×
502
      }, ROUTE_FETCH_INTERVAL)
503
      // )
504
    }
505
    return () => {
127✔
506
      if (interval) {
127✔
507
        clearInterval(interval)
16✔
508
      }
509
    }
510
  }, [calculateRoute, route])
511

512
  // when changing the selectedToken and it's not fetching route, trigger fetch route
513
  useEffect(() => {
292✔
514
    if (
175✔
515
      selectedToken &&
474✔
516
      !route &&
517
      !isFetchingRoute &&
518
      !useMetaTx &&
519
      !routeFailed
520
    ) {
521
      const isBuyingL1WithOtherTokenThanEthereumMANA =
522
        asset.chainId === ChainId.ETHEREUM_MAINNET &&
44✔
523
        selectedToken.chainId !== ChainId.ETHEREUM_MAINNET.toString() &&
524
        selectedToken.symbol !== 'MANA'
525

526
      const isPayingWithOtherTokenThanMANA = selectedToken.symbol !== 'MANA'
44✔
527
      const isPayingWithMANAButFromOtherChain =
528
        selectedToken.symbol === 'MANA' &&
44✔
529
        selectedToken.chainId !== asset.chainId.toString()
530

531
      if (
44✔
532
        isBuyingL1WithOtherTokenThanEthereumMANA ||
114✔
533
        isPayingWithOtherTokenThanMANA ||
534
        isPayingWithMANAButFromOtherChain
535
      ) {
536
        setIsFetchingRoute(true)
24✔
537
        calculateRoute()
24✔
538
      }
539
    }
540
  }, [
541
    route,
542
    useMetaTx,
543
    routeFailed,
544
    selectedToken,
545
    isFetchingRoute,
546
    selectedChain,
547
    asset.chainId,
548
    calculateRoute
549
  ])
550

551
  const onBuyWithCrypto = useCallback(async () => {
292✔
552
    if (route && crossChainProvider && crossChainProvider.isLibInitialized()) {
3!
553
      onBuyItemThroughProvider(route)
3✔
554
    }
555
  }, [onBuyItemThroughProvider, route])
556

557
  // useCallbacks
558

559
  const renderSwitchNetworkButton = useCallback(() => {
292✔
560
    return (
11✔
561
      <Button
562
        fluid
563
        inverted
564
        className={styles.switchNetworkButton}
565
        disabled={isSwitchingNetwork}
566
        data-testid={SWITCH_NETWORK_BUTTON_TEST_ID}
567
        onClick={() => onSwitchNetwork(selectedChain)}
5✔
568
      >
569
        {isSwitchingNetwork ? (
11!
570
          <>
571
            <Loader inline active size="tiny" />
572
            {t('buy_with_crypto_modal.confirm_switch_network')}
573
          </>
574
        ) : (
575
          t('buy_with_crypto_modal.switch_network', {
576
            chain: providerChains.find(
577
              c => c.chainId === selectedChain.toString()
16✔
578
            )?.networkName
579
          })
580
        )}
581
      </Button>
582
    )
583
  }, [isSwitchingNetwork, onSwitchNetwork, providerChains, selectedChain])
584

585
  const onBuyNFT = useCallback(() => {
292✔
586
    if (order) {
3!
587
      const contractNames = getContractNames()
3✔
588

589
      const mana = getContractProp({
3✔
590
        name: contractNames.MANA,
591
        network: asset.network
592
      }) as DCLContract
593

594
      const marketplace = getContractProp({
3✔
595
        address: order?.marketplaceAddress,
596
        network: asset.network
597
      }) as DCLContract
598

599
      onAuthorizedAction({
3✔
600
        targetContractName: ContractName.MANAToken,
601
        authorizationType: AuthorizationType.ALLOWANCE,
602
        authorizedAddress: order.marketplaceAddress,
603
        targetContract: mana as Contract,
604
        authorizedContractLabel: marketplace.label || marketplace.name,
6✔
605
        requiredAllowanceInWei: order.price,
606
        onAuthorized: alreadyAuthorized =>
607
          onExecuteOrder(order, asset as NFT, undefined, !alreadyAuthorized) // undefined as fingerprint
3✔
608
      })
609
    }
610
  }, [asset, order, getContractProp, onAuthorizedAction, onExecuteOrder])
611

612
  const onBuyItem = useCallback(() => {
292✔
613
    if (!isNFT(asset)) {
3!
614
      const contractNames = getContractNames()
3✔
615

616
      const mana = getContractProp({
3✔
617
        name: contractNames.MANA,
618
        network: asset.network
619
      }) as DCLContract
620

621
      const collectionStore = getContractProp({
3✔
622
        name: contractNames.COLLECTION_STORE,
623
        network: asset.network
624
      }) as DCLContract
625

626
      onAuthorizedAction({
3✔
627
        targetContractName: ContractName.MANAToken,
628
        authorizationType: AuthorizationType.ALLOWANCE,
629
        authorizedAddress: collectionStore.address,
630
        targetContract: mana as Contract,
631
        authorizedContractLabel: collectionStore.label || collectionStore.name,
6✔
632
        requiredAllowanceInWei: asset.price,
633
        onAuthorized: () => onBuyItemProp(asset)
3✔
634
      })
635
    }
636
  }, [asset, getContractProp, onAuthorizedAction, onBuyItemProp])
637

638
  const onBuyWithCard = useCallback(() => {
292✔
NEW
639
    analytics.track(events.CLICK_BUY_NFT_WITH_CARD)
×
NEW
640
    return !isNFT(asset)
×
641
      ? onBuyItemWithCard(asset)
642
      : !!order
×
643
      ? onExecuteOrderWithCard(asset)
644
      : () => {}
645
  }, [analytics, asset, onBuyItemWithCard, onExecuteOrderWithCard, order])
646

647
  const renderGetMANAButton = useCallback(() => {
292✔
648
    return (
55✔
649
      <>
650
        <Button
651
          fluid
652
          primary
653
          data-testid={GET_MANA_BUTTON_TEST_ID}
654
          loading={isFetchingBalance || isLoading}
110✔
655
          onClick={() => {
NEW
656
            onGetMana()
×
NEW
657
            onClose()
×
658
          }}
659
        >
660
          {t('buy_with_crypto_modal.get_mana')}
661
        </Button>
662
        <ChainButton
663
          inverted
664
          fluid
665
          chainId={asset.chainId}
666
          data-testid={BUY_WITH_CARD_TEST_ID}
667
          disabled={isLoading || isLoadingAuthorization}
110✔
668
          loading={isLoading || isLoadingAuthorization}
110✔
669
          onClick={onBuyWithCard}
670
        >
671
          <Icon name="credit card outline" />
672
          {t(`buy_with_crypto_modal.buy_with_card`)}
673
        </ChainButton>
674
      </>
675
    )
676
  }, [
677
    isFetchingBalance,
678
    isLoading,
679
    asset.chainId,
680
    isLoadingAuthorization,
681
    onBuyWithCard,
682
    onGetMana,
683
    onClose
684
  ])
685

686
  const renderBuyNowButton = useCallback(() => {
292✔
687
    let onClick =
688
      selectedToken?.symbol === 'MANA' && !route
11✔
689
        ? !!order
6✔
690
          ? onBuyNFT
691
          : onBuyItem
692
        : onBuyWithCrypto
693

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

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

774
  const renderTokenBalance = useCallback(() => {
292✔
775
    let balance
776
    if (selectedToken && selectedToken.symbol === 'MANA') {
210✔
777
      balance = wallet?.networks[getNetwork(selectedChain)]?.mana.toFixed(2)
74✔
778
    } else if (selectedToken && selectedTokenBalance) {
136✔
779
      balance = Number(
72✔
780
        ethers.utils.formatUnits(selectedTokenBalance, selectedToken.decimals)
781
      ).toFixed(4)
782
    }
783

784
    return !isFetchingBalance ? (
210✔
785
      <span className={styles.balance}>{balance}</span>
786
    ) : (
787
      <div className={styles.balanceSkeleton} />
788
    )
789
  }, [
790
    wallet,
791
    isFetchingBalance,
792
    selectedChain,
793
    selectedToken,
794
    selectedTokenBalance
795
  ])
796

797
  const onTokenOrChainSelection = useCallback(
292✔
798
    (selectedOption: Token | ChainData) => {
799
      setShowChainSelector(false)
24✔
800
      setShowTokenSelector(false)
24✔
801

802
      if (isToken(selectedOption)) {
24✔
803
        abortControllerRef.current.abort()
16✔
804

805
        const selectedToken = providerTokens.find(
16✔
806
          t =>
807
            t.address === selectedOption.address &&
60✔
808
            t.chainId === selectedChain.toString()
809
        ) as Token
810
        // reset all fields
811
        setSelectedToken(selectedToken)
16✔
812
        setFromAmount(undefined)
16✔
813
        setSelectedTokenBalance(undefined)
16✔
814
        setCanBuyItem(undefined)
16✔
815
        setRoute(undefined)
16✔
816
        setRouteFailed(false)
16✔
817
        abortControllerRef.current = new AbortController()
16✔
818
        analytics.track(events.CROSS_CHAIN_TOKEN_SELECTION, {
16✔
819
          selectedToken
820
        })
821
      } else {
822
        setSelectedChain(Number(selectedOption.chainId) as ChainId)
8✔
823
        const manaDestinyChain = providerTokens.find(
8✔
824
          t => t.symbol === 'MANA' && t.chainId === selectedOption.chainId
10✔
825
        )
826
        // set the selected token on the new chain selected to MANA or the first one found
827
        setSelectedToken(
8✔
828
          manaDestinyChain ||
8!
NEW
829
            providerTokens.find(t => t.chainId === selectedOption.chainId)
×
830
        )
831
        setRoute(undefined)
8✔
832
        setRouteFailed(false)
8✔
833

834
        analytics.track(events.CROSS_CHAIN_CHAIN_SELECTION, {
8✔
835
          selectedChain
836
        })
837
      }
838
    },
839
    [analytics, providerTokens, selectedChain]
840
  )
841

842
  const renderModalNavigation = useCallback(() => {
292✔
843
    if (showChainSelector || showTokenSelector) {
292✔
844
      return (
24✔
845
        <ModalNavigation
846
          title={t(
847
            `buy_with_crypto_modal.token_and_chain_selector.select_${
848
              showChainSelector ? 'chain' : 'token'
24✔
849
            }`
850
          )}
851
          onBack={() => {
NEW
852
            setShowChainSelector(false)
×
NEW
853
            setShowTokenSelector(false)
×
854
          }}
855
        />
856
      )
857
    }
858
    return (
268✔
859
      <ModalNavigation
860
        title={t('buy_with_crypto_modal.title', {
861
          name: asset.name,
NEW
862
          b: (children: React.ReactChildren) => <b>{children}</b>
×
863
        })}
864
        onClose={onClose}
865
      />
866
    )
867
  }, [asset.name, onClose, showChainSelector, showTokenSelector])
868

869
  const translationPageDescriptorId = compact([
292✔
870
    'mint',
871
    isWearableOrEmote(asset)
292!
872
      ? isBuyWithCardPage
292!
873
        ? 'with_card'
874
        : 'with_mana'
875
      : null,
876
    'page'
877
  ]).join('_')
878

879
  return (
292✔
880
    <Modal size="tiny" onClose={onClose} className={styles.buyWithCryptoModal}>
881
      {renderModalNavigation()}
882
      <Modal.Content>
883
        <>
884
          {showChainSelector || showTokenSelector ? (
868✔
885
            <div>
886
              {showChainSelector ? (
24✔
887
                <ChainAndTokenSelector
888
                  currentChain={selectedChain}
889
                  chains={providerChains}
890
                  onSelect={onTokenOrChainSelection}
891
                />
892
              ) : null}
893
              {showTokenSelector ? (
24✔
894
                <ChainAndTokenSelector
895
                  currentChain={selectedChain}
896
                  tokens={providerTokens}
897
                  onSelect={onTokenOrChainSelection}
898
                />
899
              ) : null}
900
            </div>
901
          ) : (
902
            <>
903
              <div className={styles.assetContainer}>
904
                <AssetImage asset={asset} isSmall />
905
                <span className={styles.assetName}>{asset.name}</span>
906
                <div className={styles.priceContainer}>
907
                  <Mana network={asset.network} inline withTooltip>
908
                    {formatWeiMANA(price)}
909
                  </Mana>
910
                  <span className={styles.priceInUSD}>
911
                    <ManaToFiat mana={price} digits={4} />
912
                  </span>
913
                </div>
914
              </div>
915

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

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

1219
              {selectedToken &&
980✔
1220
              shouldUseCrossChainProvider &&
1221
              asset.network === Network.MATIC && // and it's buying a MATIC wearable
1222
              !isPriceTooLow(price) ? (
1223
                <span className={styles.rememberFreeTxs}>
1224
                  {t('buy_with_crypto_modal.remember_transaction_fee_covered', {
1225
                    free: (
1226
                      <span className={styles.feeCoveredFree}>
1227
                        {t('buy_with_crypto_modal.free')}
1228
                      </span>
1229
                    )
1230
                  })}
1231
                </span>
1232
              ) : null}
1233

1234
              {hasLowPriceForMetaTx && !isBuyWithCardPage && useMetaTx ? (
576✔
1235
                <span
1236
                  className={styles.warning}
1237
                  data-testid={PRICE_TOO_LOW_TEST_ID}
1238
                >
1239
                  {' '}
1240
                  {t('buy_with_crypto_modal.price_too_low', {
1241
                    learn_more: (
1242
                      <a
1243
                        href="https://docs.decentraland.org"
1244
                        target="_blank"
1245
                        rel="noreferrer"
1246
                      >
1247
                        {/* TODO: add this URL */}
1248
                        <u> {t('buy_with_crypto_modal.learn_more')} </u>
1249
                      </a>
1250
                    )
1251
                  })}
1252
                </span>
1253
              ) : null}
1254
              {canBuyItem === false && isWearableOrEmote(asset) ? (
591✔
1255
                <span className={styles.warning}>
1256
                  {t('buy_with_crypto_modal.insufficient_funds', {
1257
                    token: selectedToken?.symbol || 'MANA'
55!
1258
                  })}
1259
                </span>
1260
              ) : null}
1261
              {routeFailed && selectedToken ? (
536!
1262
                <span className={styles.warning}>
1263
                  {' '}
1264
                  {t('buy_with_crypto_modal.route_unavailable', {
1265
                    token: selectedToken.symbol
1266
                  })}
1267
                </span>
1268
              ) : null}
1269
            </>
1270
          )}
1271
        </>
1272
      </Modal.Content>
1273
      {showChainSelector || showTokenSelector ? null : (
868✔
1274
        <Modal.Actions>
1275
          <div
1276
            className={classNames(
1277
              styles.buttons,
1278
              isWearableOrEmote(asset) && 'with-mana'
536✔
1279
            )}
1280
          >
1281
            {renderMainActionButton()}
1282
          </div>
1283
          {isWearableOrEmote(asset) && isBuyWithCardPage ? (
804!
1284
            <CardPaymentsExplanation
1285
              translationPageDescriptorId={translationPageDescriptorId}
1286
            />
1287
          ) : null}
1288
        </Modal.Actions>
1289
      )}
1290
    </Modal>
1291
  )
1292
}
1293

1294
export const BuyNFTWithCryptoModal = React.memo(
1✔
1295
  withAuthorizedAction(
1296
    BuyWithCryptoModal,
1297
    AuthorizedAction.BUY,
1298
    {
1299
      action: 'buy_with_mana_page.authorization.action',
1300
      title_action: 'buy_with_mana_page.authorization.title_action'
1301
    },
1302
    getBuyItemStatus,
1303
    getError
1304
  )
1305
)
1306

1307
export const MintNFTWithCryptoModal = React.memo(
1✔
1308
  withAuthorizedAction(
1309
    BuyWithCryptoModal,
1310
    AuthorizedAction.MINT,
1311
    {
1312
      action: 'mint_with_mana_page.authorization.action',
1313
      title_action: 'mint_with_mana_page.authorization.title_action'
1314
    },
1315
    getMintItemStatus,
1316
    getError
1317
  )
1318
)
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