oceanprotocol/market

View on GitHub
src/components/Asset/AssetActions/Download/index.tsx

Summary

Maintainability
A
2 hrs
Test Coverage
F
0%
import React, { ReactElement, useEffect, useState } from 'react'
import FileIcon from '@shared/FileIcon'
import Price from '@shared/Price'
import { useAsset } from '@context/Asset'
import ButtonBuy from '../ButtonBuy'
import { secondsToString } from '@utils/ddo'
import styles from './index.module.css'
import {
  AssetPrice,
  FileInfo,
  LoggerInstance,
  UserCustomParameters,
  ZERO_ADDRESS
} from '@oceanprotocol/lib'
import { order } from '@utils/order'
import { downloadFile } from '@utils/provider'
import { getOrderFeedback } from '@utils/feedback'
import {
  getAvailablePrice,
  getOrderPriceAndFees
} from '@utils/accessDetailsAndPricing'
import { toast } from 'react-toastify'
import { useIsMounted } from '@hooks/useIsMounted'
import { useMarketMetadata } from '@context/MarketMetadata'
import Alert from '@shared/atoms/Alert'
import Loader from '@shared/atoms/Loader'
import { useAccount, useSigner } from 'wagmi'
import useNetworkMetadata from '@hooks/useNetworkMetadata'
import ConsumerParameters, {
  parseConsumerParameterValues
} from '../ConsumerParameters'
import { Form, Formik, useFormikContext } from 'formik'
import { getDownloadValidationSchema } from './_validation'
import { getDefaultValues } from '../ConsumerParameters/FormConsumerParameters'

export default function Download({
  asset,
  file,
  isBalanceSufficient,
  dtBalance,
  fileIsLoading,
  consumableFeedback
}: {
  asset: AssetExtended
  file: FileInfo
  isBalanceSufficient: boolean
  dtBalance: string
  fileIsLoading?: boolean
  consumableFeedback?: string
}): ReactElement {
  const { address: accountId, isConnected } = useAccount()
  const { data: signer } = useSigner()
  const { isSupportedOceanNetwork } = useNetworkMetadata()
  const { getOpcFeeForToken } = useMarketMetadata()
  const { isInPurgatory, isAssetNetwork } = useAsset()
  const isMounted = useIsMounted()

  const [isDisabled, setIsDisabled] = useState(true)
  const [hasDatatoken, setHasDatatoken] = useState(false)
  const [statusText, setStatusText] = useState('')
  const [isLoading, setIsLoading] = useState(false)
  const [isPriceLoading, setIsPriceLoading] = useState(false)
  const [isOwned, setIsOwned] = useState(false)
  const [validOrderTx, setValidOrderTx] = useState('')
  const [isOrderDisabled, setIsOrderDisabled] = useState(false)
  const [orderPriceAndFees, setOrderPriceAndFees] =
    useState<OrderPriceAndFees>()
  const [retry, setRetry] = useState<boolean>(false)

  const price: AssetPrice = getAvailablePrice(asset)
  const isUnsupportedPricing =
    !asset?.accessDetails ||
    !asset.services.length ||
    asset?.accessDetails?.type === 'NOT_SUPPORTED' ||
    (asset?.accessDetails?.type === 'fixed' &&
      !asset?.accessDetails?.baseToken?.symbol)

  useEffect(() => {
    Number(asset?.nft.state) === 4 && setIsOrderDisabled(true)
  }, [asset?.nft.state])

  useEffect(() => {
    if (isUnsupportedPricing) return

    setIsOwned(asset?.accessDetails?.isOwned || false)
    setValidOrderTx(asset?.accessDetails?.validOrderTx || '')

    // get full price and fees
    async function init() {
      if (
        asset.accessDetails.addressOrId === ZERO_ADDRESS ||
        asset.accessDetails.type === 'free'
      )
        return

      try {
        !orderPriceAndFees && setIsPriceLoading(true)

        const _orderPriceAndFees = await getOrderPriceAndFees(
          asset,
          ZERO_ADDRESS
        )
        setOrderPriceAndFees(_orderPriceAndFees)
        !orderPriceAndFees && setIsPriceLoading(false)
      } catch (error) {
        LoggerInstance.error('getOrderPriceAndFees', error)
        setIsPriceLoading(false)
      }
    }

    init()

    /**
     * we listen to the assets' changes to get the most updated price
     * based on the asset and the poolData's information.
     * Not adding isLoading and getOpcFeeForToken because we set these here. It is a compromise
     */
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [asset, getOpcFeeForToken, isUnsupportedPricing])

  useEffect(() => {
    setHasDatatoken(Number(dtBalance) >= 1)
  }, [dtBalance])

  useEffect(() => {
    if (
      (asset?.accessDetails?.type === 'fixed' && !orderPriceAndFees) ||
      !isMounted ||
      !accountId ||
      isUnsupportedPricing
    )
      return

    /**
     * disabled in these cases:
     * - if the asset is not purchasable
     * - if the user is on the wrong network
     * - if user balance is not sufficient
     * - if user has no datatokens
     */
    const isDisabled =
      !asset?.accessDetails.isPurchasable ||
      !isAssetNetwork ||
      ((!isBalanceSufficient || !isAssetNetwork) && !isOwned && !hasDatatoken)

    setIsDisabled(isDisabled)
  }, [
    isMounted,
    asset?.accessDetails,
    isBalanceSufficient,
    isAssetNetwork,
    hasDatatoken,
    accountId,
    isOwned,
    isUnsupportedPricing,
    orderPriceAndFees
  ])

  async function handleOrderOrDownload(dataParams?: UserCustomParameters) {
    setIsLoading(true)
    setRetry(false)
    try {
      if (isOwned) {
        setStatusText(
          getOrderFeedback(
            asset.accessDetails.baseToken?.symbol,
            asset.accessDetails.datatoken?.symbol
          )[3]
        )

        await downloadFile(signer, asset, accountId, validOrderTx, dataParams)
      } else {
        setStatusText(
          getOrderFeedback(
            asset.accessDetails.baseToken?.symbol,
            asset.accessDetails.datatoken?.symbol
          )[asset.accessDetails.type === 'fixed' ? 2 : 1]
        )
        const orderTx = await order(
          signer,
          asset,
          orderPriceAndFees,
          accountId,
          hasDatatoken
        )
        const tx = await orderTx.wait()
        if (!tx) {
          throw new Error()
        }
        setIsOwned(true)
        setValidOrderTx(tx.transactionHash)
      }
    } catch (error) {
      LoggerInstance.error(error)
      setRetry(true)
      const message = isOwned
        ? 'Failed to download file!'
        : 'An error occurred, please retry. Check console for more information.'
      toast.error(message)
    }
    setIsLoading(false)
  }

  const PurchaseButton = ({ isValid }: { isValid?: boolean }) => (
    <ButtonBuy
      action="download"
      disabled={isDisabled || !isValid}
      hasPreviousOrder={isOwned}
      hasDatatoken={hasDatatoken}
      btSymbol={asset?.accessDetails?.baseToken?.symbol}
      dtSymbol={asset?.datatokens[0]?.symbol}
      dtBalance={dtBalance}
      type="submit"
      assetTimeout={secondsToString(asset?.services?.[0]?.timeout)}
      assetType={asset?.metadata?.type}
      stepText={statusText}
      isLoading={isLoading}
      priceType={asset.accessDetails?.type}
      isConsumable={asset.accessDetails?.isPurchasable}
      isBalanceSufficient={isBalanceSufficient}
      consumableFeedback={consumableFeedback}
      retry={retry}
      isSupportedOceanNetwork={isSupportedOceanNetwork}
      isAccountConnected={isConnected}
    />
  )

  const AssetAction = ({ asset }: { asset: AssetExtended }) => {
    const { isValid } = useFormikContext()

    return (
      <div>
        {isOrderDisabled ? (
          <Alert
            className={styles.fieldWarning}
            state="info"
            text={`The publisher temporarily disabled ordering for this asset`}
          />
        ) : (
          <>
            {isUnsupportedPricing ? (
              <Alert
                className={styles.fieldWarning}
                state="info"
                text={`No pricing schema available for this asset.`}
              />
            ) : (
              <>
                {asset && <ConsumerParameters asset={asset} />}
                {!isInPurgatory && (
                  <div className={styles.buttonBuy}>
                    <PurchaseButton isValid={isValid} />
                  </div>
                )}
              </>
            )}
          </>
        )}
      </div>
    )
  }

  return (
    <Formik
      initialValues={{
        dataServiceParams: getDefaultValues(
          asset?.services[0].consumerParameters
        )
      }}
      validationSchema={getDownloadValidationSchema(
        asset?.services[0].consumerParameters
      )}
      onSubmit={async (values) => {
        const dataServiceParams = parseConsumerParameterValues(
          values?.dataServiceParams,
          asset.services[0].consumerParameters
        )

        await handleOrderOrDownload(dataServiceParams)
      }}
    >
      <Form>
        <aside className={styles.consume}>
          <div className={styles.info}>
            <div className={styles.filewrapper}>
              <FileIcon file={file} isLoading={fileIsLoading} small />
            </div>
            {isPriceLoading ? (
              <Loader message="Calculating full price (including fees)" />
            ) : (
              <Price
                className={styles.price}
                price={price}
                orderPriceAndFees={orderPriceAndFees}
                conversion
                size="large"
              />
            )}
          </div>
          <AssetAction asset={asset} />
        </aside>
      </Form>
    </Formik>
  )
}