import React, { useState, useContext, useEffect, useRef } from 'react'; import { useDispatch } from 'react-redux'; import PropTypes from 'prop-types'; import BigNumber from 'bignumber.js'; import { addHexPrefix } from 'ethereumjs-util'; import { I18nContext } from '../../../contexts/i18n'; import { AlignItems, Display, FlexDirection, TextVariant, Size, BackgroundColor, TextColor, BlockSize, } from '../../../helpers/constants/design-system'; import { Text, ButtonLink, Icon, IconName, IconSize, Label, TextField, HelpText, Box, } from '../../component-library'; import { setCustomTokenAmount } from '../../../ducks/app/app'; import { calcTokenAmount } from '../../../../shared/lib/transactions-controller-utils'; import { hexToDecimal } from '../../../../shared/modules/conversion.utils'; import { MAX_TOKEN_ALLOWANCE_AMOUNT, NUM_W_OPT_DECIMAL_COMMA_OR_DOT_REGEX, DECIMAL_REGEX, } from '../../../../shared/constants/tokens'; import { Numeric } from '../../../../shared/modules/Numeric'; import { estimateGas } from '../../../store/actions'; import { getCustomTxParamsData } from '../../../pages/confirm-approve/confirm-approve.util'; import { useGasFeeContext } from '../../../contexts/gasFee'; import ZENDESK_URLS from '../../../helpers/constants/zendesk-url'; import { CustomSpendingCapTooltip } from './custom-spending-cap-tooltip'; export default function CustomSpendingCap({ txParams, tokenName, currentTokenBalance, dappProposedValue, siteOrigin, passTheErrorText, decimals, setInputChangeInProgress, customSpendingCap, setCustomSpendingCap, }) { const t = useContext(I18nContext); const dispatch = useDispatch(); const { updateTransaction } = useGasFeeContext(); const inputRef = useRef(null); const [error, setError] = useState(''); const [showUseSiteSuggestionButton, setShowUseSiteSuggestionButton] = useState(customSpendingCap !== String(dappProposedValue) && true); const inputLogicEmptyStateText = t('inputLogicEmptyState'); const replaceCommaToDot = (inputValue) => { return inputValue.replace(/,/gu, '.'); }; const decConversionGreaterThan = (tokenValue, tokenBalance) => { return new Numeric(Number(replaceCommaToDot(tokenValue)), 10).greaterThan( Number(tokenBalance), 10, ); }; const getInputTextLogic = (inputNumber) => { if ( new Numeric(Number(replaceCommaToDot(inputNumber)), 10).lessThanOrEqualTo( new Numeric(Number(currentTokenBalance), 10), ) ) { return { className: 'custom-spending-cap__lowerValue', description: t('inputLogicEqualOrSmallerNumber', [ {replaceCommaToDot(inputNumber)} {tokenName} , ]), }; } else if (decConversionGreaterThan(inputNumber, currentTokenBalance)) { return { className: 'custom-spending-cap__higherValue', description: t('inputLogicHigherNumber'), }; } return { className: 'custom-spending-cap__emptyState', description: t('inputLogicEmptyState'), }; }; const [customSpendingCapText, setCustomSpendingCapText] = useState( getInputTextLogic(customSpendingCap).description, ); const handleChange = async (valueInput) => { if (!txParams) { return; } setInputChangeInProgress(true); let spendingCapError = ''; const inputTextLogic = getInputTextLogic(valueInput); const inputTextLogicDescription = inputTextLogic.description; const match = DECIMAL_REGEX.exec(replaceCommaToDot(valueInput)); if (match?.[1]?.length > decimals) { setInputChangeInProgress(false); return; } if (valueInput && !NUM_W_OPT_DECIMAL_COMMA_OR_DOT_REGEX.test(valueInput)) { spendingCapError = t('spendingCapError'); setCustomSpendingCapText(t('spendingCapErrorDescription', [siteOrigin])); setError(spendingCapError); } else { setCustomSpendingCapText(inputTextLogicDescription); setError(''); } const maxTokenAmount = calcTokenAmount( MAX_TOKEN_ALLOWANCE_AMOUNT, decimals, ); if (Number(valueInput.length) > 1 && Number(valueInput)) { const customSpendLimitNumber = new BigNumber(valueInput); if (customSpendLimitNumber.greaterThan(maxTokenAmount)) { spendingCapError = t('spendLimitTooLarge'); setError(spendingCapError); } } setCustomSpendingCap(String(valueInput)); dispatch(setCustomTokenAmount(String(valueInput))); if (String(valueInput) !== '') { try { const newData = getCustomTxParamsData(txParams.data, { customPermissionAmount: valueInput, decimals, }); const { from, to, value: txValue } = txParams; const estimatedGasLimit = await estimateGas({ from, to, value: txValue, data: newData, }); if (estimatedGasLimit) { await updateTransaction({ gasLimit: hexToDecimal(addHexPrefix(estimatedGasLimit)), }); } } catch (exp) { console.error('Error in trying to update gas limit', exp); } } setInputChangeInProgress(false); }; useEffect(() => { if (customSpendingCap === String(dappProposedValue)) { setShowUseSiteSuggestionButton(false); } else { setShowUseSiteSuggestionButton(true); } }, [customSpendingCap, dappProposedValue]); useEffect(() => { passTheErrorText(error); }, [error, passTheErrorText]); useEffect(() => { if (inputRef.current) { inputRef.current.focus({ preventScroll: true, }); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [inputRef.current]); const chooseTooltipContentText = decConversionGreaterThan( customSpendingCap, currentTokenBalance, ) ? t('warningTooltipText', [ {' '} {t('beCareful')} , ]) : t('inputLogicEmptyState'); return ( <>
{showUseSiteSuggestionButton && ( { e.preventDefault(); setShowUseSiteSuggestionButton(false); handleChange(dappProposedValue); }} > {t('useSiteSuggestion')} )}
handleChange(e.target.value)} placeholder={t('enterANumber')} value={customSpendingCap} endAccessory={ { e.preventDefault(); handleChange(currentTokenBalance); }} data-testid="custom-spending-cap-max-button" > {t('max')} } width={BlockSize.Full} /> {error || (replaceCommaToDot(customSpendingCap) ? customSpendingCapText : inputLogicEmptyStateText)} {t('learnMoreUpperCase')}
); } CustomSpendingCap.propTypes = { /** * Transaction params */ txParams: PropTypes.object.isRequired, /** * Displayed the token name currently tracked in description related to the input state */ tokenName: PropTypes.string, /** * The current token balance of the token */ currentTokenBalance: PropTypes.string, /** * The dapp suggested amount */ dappProposedValue: PropTypes.string, /** * The origin of the site generally the URL */ siteOrigin: PropTypes.string, /** * Parent component's callback function passed in order to get the error text */ passTheErrorText: PropTypes.func, /** * Number of decimals */ decimals: PropTypes.string, /** * Updating input state to changing */ setInputChangeInProgress: PropTypes.func.isRequired, /** * Custom token amount or The dapp suggested amount */ customSpendingCap: PropTypes.string, /** * State method to update the custom token value */ setCustomSpendingCap: PropTypes.func.isRequired, };