import React, { useState, useContext, useEffect, useRef } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import PropTypes from 'prop-types'; import classnames from 'classnames'; import BigNumber from 'bignumber.js'; import { addHexPrefix } from 'ethereumjs-util'; import { I18nContext } from '../../../contexts/i18n'; import Box from '../../ui/box'; import FormField from '../../ui/form-field'; import { Text, ButtonLink, Icon, IconName } from '../../component-library'; import { AlignItems, DISPLAY, FLEX_DIRECTION, TextAlign, TextVariant, JustifyContent, Size, BLOCK_SIZES, BackgroundColor, TextColor, } from '../../../helpers/constants/design-system'; import { getCustomTokenAmount } from '../../../selectors'; 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 { CustomSpendingCapTooltip } from './custom-spending-cap-tooltip'; export default function CustomSpendingCap({ txParams, tokenName, currentTokenBalance, dappProposedValue, siteOrigin, passTheErrorText, decimals, setInputChangeInProgress, }) { const t = useContext(I18nContext); const dispatch = useDispatch(); const { updateTransaction } = useGasFeeContext(); const inputRef = useRef(null); const value = useSelector(getCustomTokenAmount); const [error, setError] = useState(''); const [showUseDefaultButton, setShowUseDefaultButton] = useState( value !== 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(value).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); } } dispatch(setCustomTokenAmount(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 (value !== String(dappProposedValue)) { setShowUseDefaultButton(true); } }, [value, 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( value, currentTokenBalance, ) ? t('warningTooltipText', [ {t('beCareful')} , ]) : t('inputLogicEmptyState'); return ( <> ); } 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, };