synapsecns/sanguine

View on GitHub
packages/synapse-interface/components/StateManagedSwap/SwapInputContainer.tsx

Summary

Maintainability
C
1 day
Test Coverage
import React, { useEffect, useState, useRef, useCallback, useMemo } from 'react'
import { useDispatch } from 'react-redux'
import { useAccount } from 'wagmi'
import { debounce } from 'lodash'
import { useTranslations } from 'next-intl'

import { TokenSelector } from '@/components/ui/TokenSelector'
import { formatBigIntToString, stringToBigInt } from '@/utils/bigint/format'
import { cleanNumberInput } from '@/utils/cleanNumberInput'
import {
  ConnectToNetworkButton,
  ConnectWalletButton,
  ConnectedIndicator,
} from '@/components/ConnectionIndicators'
import { ChainSelector } from '@/components/ui/ChainSelector'
import { usePortfolioState } from '@/slices/portfolio/hooks'
import {
  initialState,
  setSwapChainId,
  setSwapFromToken,
  updateSwapFromValue,
} from '@/slices/swap/reducer'
import { useSwapState } from '@/slices/swap/hooks'
import { BridgeSectionContainer } from '@/components/ui/BridgeSectionContainer'
import { BridgeAmountContainer } from '@/components/ui/BridgeAmountContainer'
import { CHAINS_BY_ID } from '@/constants/chains'
import { useSwapChainListArray } from '@/components/StateManagedSwap//hooks/useSwapChainListArray'
import { useSwapFromTokenListArray } from '@/components/StateManagedSwap/hooks/useSwapFromTokenListOverlay'
import { AmountInput } from '@/components/ui/AmountInput'
import { joinClassNames } from '@/utils/joinClassNames'
import { MaxButton } from '../StateManagedBridge/MaxButton'
import { trimTrailingZeroesAfterDecimal } from '@/utils/trimTrailingZeroesAfterDecimal'
import { formatAmount } from '@/utils/formatAmount'
import { getParsedBalance } from '@/utils/getParsedBalance'
import { useWalletState } from '@/slices/wallet/hooks'

interface InputContainerProps {
  setIsTyping: React.Dispatch<React.SetStateAction<boolean>>
}

export const SwapInputContainer: React.FC<InputContainerProps> = ({
  setIsTyping,
}) => {
  const inputRef = useRef<HTMLInputElement>(null)
  const { swapChainId, swapFromToken, swapToToken, swapFromValue } =
    useSwapState()
  const [showValue, setShowValue] = useState('')

  const [hasMounted, setHasMounted] = useState(false)

  const { balances } = usePortfolioState()

  const { isWalletPending } = useWalletState()

  useEffect(() => {
    setHasMounted(true)
  }, [])

  const { isConnected } = useAccount()
  const { chain } = useAccount()

  const dispatch = useDispatch()

  const tokenData = balances[swapChainId]?.find(
    (token) => token.tokenAddress === swapFromToken?.addresses[swapChainId]
  )

  const balance = tokenData?.balance
  const decimals = tokenData?.token?.decimals[swapChainId]
  const parsedBalance = getParsedBalance(balance, decimals)
  const formattedBalance = formatAmount(parsedBalance)

  const isInputMax = parsedBalance === swapFromValue

  const t = useTranslations('Swap')

  useEffect(() => {
    if (
      swapFromToken &&
      swapFromToken.decimals[swapChainId] &&
      stringToBigInt(swapFromValue, swapFromToken.decimals[swapChainId]) !== 0n
    ) {
      setShowValue(swapFromValue)
    }

    if (swapFromValue === initialState.swapFromValue) {
      setShowValue(initialState.swapFromValue)
    }
  }, [swapFromValue, swapChainId, swapFromToken])

  const debouncedUpdateSwapFromValue = useMemo(
    () =>
      debounce((value: string) => dispatch(updateSwapFromValue(value)), 400),
    [dispatch]
  )

  useEffect(() => {
    return () => {
      debouncedUpdateSwapFromValue.cancel()
    }
  }, [debouncedUpdateSwapFromValue])

  const handleFromValueChange = (
    event: React.ChangeEvent<HTMLInputElement>
  ) => {
    const swapFromValueString: string = cleanNumberInput(event.target.value)
    try {
      setShowValue(swapFromValueString)
      debouncedUpdateSwapFromValue(swapFromValueString)
    } catch (error) {
      console.error('Invalid value for conversion to BigInteger')
      const inputValue = event.target.value
      const regex = /^[0-9]*[.,]?[0-9]*$/

      if (regex.test(inputValue) || inputValue === '') {
        debouncedUpdateSwapFromValue(inputValue)
        setShowValue(inputValue)
      }
    }
  }

  const onMaxBalance = useCallback(() => {
    dispatch(
      updateSwapFromValue(
        trimTrailingZeroesAfterDecimal(
          formatBigIntToString(balance, swapFromToken.decimals[swapChainId])
        )
      )
    )
  }, [balance, swapChainId, swapFromToken])

  const connectedStatus = useMemo(() => {
    if (hasMounted && isConnected) {
      if (swapChainId === chain.id) {
        return <ConnectedIndicator />
      } else if (swapChainId !== chain.id) {
        return <ConnectToNetworkButton chainId={swapChainId} />
      }
    } else if (hasMounted && !isConnected) {
      return <ConnectWalletButton />
    }
  }, [chain, swapChainId, isConnected, hasMounted])

  const labelClassNames = {
    space: 'block',
    textColor: 'text-xxs md:text-xs',
    cursor: 'cursor-default',
  }

  return (
    <BridgeSectionContainer>
      <div className="flex items-center justify-between">
        <SwapChainSelector />
        {connectedStatus}
      </div>
      <BridgeAmountContainer>
        <SwapFromTokenSelector />
        <div className="flex flex-col">
          <AmountInput
            setIsTyping={setIsTyping}
            inputRef={inputRef}
            showValue={showValue}
            handleFromValueChange={handleFromValueChange}
            disabled={isWalletPending}
          />
          <div className="flex">
            {hasMounted && isConnected && (
              <label
                htmlFor="inputRow"
                className={joinClassNames(labelClassNames)}
              >
                <span className="text-zinc-500 dark:text-zinc-400">
                  {t('available')}:{' '}
                </span>
                {formattedBalance ?? '0.0'}
              </label>
            )}
            <MaxButton
              onClick={onMaxBalance}
              isHidden={!isConnected || !balance || isInputMax}
            />
          </div>
        </div>
      </BridgeAmountContainer>
    </BridgeSectionContainer>
  )
}

const SwapChainSelector = () => {
  const { swapChainId } = useSwapState()
  const { isWalletPending } = useWalletState()

  const t = useTranslations('Bridge')

  return (
    <ChainSelector
      dataTestId="swap-origin-chain"
      isOrigin={true}
      selectedItem={CHAINS_BY_ID[swapChainId]}
      label={t('From')}
      itemListFunction={useSwapChainListArray}
      setFunction={setSwapChainId}
      action="Swap"
      disabled={isWalletPending}
    />
  )
}

const SwapFromTokenSelector = () => {
  const { swapFromToken } = useSwapState()
  const { isWalletPending } = useWalletState()

  const t = useTranslations('Bridge')

  return (
    <TokenSelector
      dataTestId="swap-origin-token"
      selectedItem={swapFromToken}
      isOrigin={true}
      placeholder={t('In')}
      itemListFunction={useSwapFromTokenListArray}
      setFunction={setSwapFromToken}
      action="Swap"
      disabled={isWalletPending}
    />
  )
}