mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-11-26 12:29:06 +01:00
52c44924e3
* Replacing deprecated components for component library components * Update * Lint fix --------- Co-authored-by: georgewrmarshall <george.marshall@consensys.net>
359 lines
10 KiB
JavaScript
359 lines
10 KiB
JavaScript
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', [
|
|
<Text
|
|
key="custom-spending-cap"
|
|
variant={TextVariant.bodySmBold}
|
|
as="span"
|
|
>
|
|
{replaceCommaToDot(inputNumber)} {tokenName}
|
|
</Text>,
|
|
]),
|
|
};
|
|
} 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', [
|
|
<Text
|
|
key="tooltip-text"
|
|
variant={TextVariant.bodySmBold}
|
|
as="span"
|
|
color={TextColor.errorDefault}
|
|
>
|
|
<Icon name={IconName.Danger} size={IconSize.Inherit} />{' '}
|
|
{t('beCareful')}
|
|
</Text>,
|
|
])
|
|
: t('inputLogicEmptyState');
|
|
|
|
return (
|
|
<>
|
|
<Box
|
|
className="custom-spending-cap"
|
|
display={Display.Flex}
|
|
alignItems={AlignItems.flexStart}
|
|
flexDirection={FlexDirection.Column}
|
|
gap={2}
|
|
padding={4}
|
|
backgroundColor={BackgroundColor.backgroundAlternative}
|
|
borderRadius={Size.SM}
|
|
>
|
|
<Box
|
|
display={Display.Flex}
|
|
alignItems={AlignItems.center}
|
|
gap={1}
|
|
width={BlockSize.Full}
|
|
>
|
|
<div>
|
|
<Label
|
|
htmlFor="custom-spending-cap"
|
|
display={Display.Inline}
|
|
marginRight={1}
|
|
>
|
|
{t('customSpendingCap')}
|
|
</Label>
|
|
<CustomSpendingCapTooltip
|
|
tooltipContentText={
|
|
replaceCommaToDot(customSpendingCap)
|
|
? chooseTooltipContentText
|
|
: ''
|
|
}
|
|
tooltipIcon={
|
|
replaceCommaToDot(customSpendingCap)
|
|
? decConversionGreaterThan(
|
|
customSpendingCap,
|
|
currentTokenBalance,
|
|
)
|
|
: ''
|
|
}
|
|
/>
|
|
</div>
|
|
{showUseSiteSuggestionButton && (
|
|
<ButtonLink
|
|
marginLeft="auto"
|
|
size={Size.auto}
|
|
onClick={(e) => {
|
|
e.preventDefault();
|
|
setShowUseSiteSuggestionButton(false);
|
|
handleChange(dappProposedValue);
|
|
}}
|
|
>
|
|
{t('useSiteSuggestion')}
|
|
</ButtonLink>
|
|
)}
|
|
</Box>
|
|
<TextField
|
|
inputRef={inputRef}
|
|
inputProps={{
|
|
'data-testid': 'custom-spending-cap-input',
|
|
color: decConversionGreaterThan(
|
|
customSpendingCap,
|
|
currentTokenBalance,
|
|
)
|
|
? TextColor.errorDefault
|
|
: TextColor.textDefault,
|
|
paddingInlineEnd: 4,
|
|
}}
|
|
id="custom-spending-cap"
|
|
onChange={(e) => handleChange(e.target.value)}
|
|
placeholder={t('enterANumber')}
|
|
value={customSpendingCap}
|
|
endAccessory={
|
|
<ButtonLink
|
|
onClick={(e) => {
|
|
e.preventDefault();
|
|
handleChange(currentTokenBalance);
|
|
}}
|
|
data-testid="custom-spending-cap-max-button"
|
|
>
|
|
{t('max')}
|
|
</ButtonLink>
|
|
}
|
|
width={BlockSize.Full}
|
|
/>
|
|
<HelpText
|
|
color={error ? TextColor.errorDefault : TextColor.textDefault}
|
|
variant={TextVariant.bodySm}
|
|
>
|
|
{error ||
|
|
(replaceCommaToDot(customSpendingCap)
|
|
? customSpendingCapText
|
|
: inputLogicEmptyStateText)}
|
|
</HelpText>
|
|
<ButtonLink
|
|
size={Size.SM}
|
|
href={ZENDESK_URLS.TOKEN_ALLOWANCE_WITH_SPENDING_CAP}
|
|
externalLink
|
|
>
|
|
{t('learnMoreUpperCase')}
|
|
</ButtonLink>
|
|
</Box>
|
|
</>
|
|
);
|
|
}
|
|
|
|
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,
|
|
};
|