import React, { useState, useContext, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useHistory } from 'react-router-dom';
import PropTypes from 'prop-types';
import BigNumber from 'bignumber.js';
import Box from '../../components/ui/box/box';
import NetworkAccountBalanceHeader from '../../components/app/network-account-balance-header/network-account-balance-header';
import UrlIcon from '../../components/ui/url-icon/url-icon';
import {
AlignItems,
BorderStyle,
Color,
DISPLAY,
FLEX_DIRECTION,
FontWeight,
JustifyContent,
TextAlign,
TextColor,
TextVariant,
} from '../../helpers/constants/design-system';
import { I18nContext } from '../../contexts/i18n';
import ContractTokenValues from '../../components/ui/contract-token-values/contract-token-values';
import Button from '../../components/ui/button';
import ReviewSpendingCap from '../../components/ui/review-spending-cap/review-spending-cap';
import { PageContainerFooter } from '../../components/ui/page-container';
import ContractDetailsModal from '../../components/app/modals/contract-details-modal/contract-details-modal';
import {
getNetworkIdentifier,
transactionFeeSelector,
getKnownMethodData,
getRpcPrefsForCurrentProvider,
getUnapprovedTxCount,
getUnapprovedTransactions,
getUseCurrencyRateCheck,
getTargetAccountWithSendEtherInfo,
getCustomNonceValue,
getNextSuggestedNonce,
} from '../../selectors';
import { NETWORK_TO_NAME_MAP } from '../../../shared/constants/network';
import {
cancelTx,
cancelTxs,
showModal,
updateAndApproveTx,
getNextNonce,
updateCustomNonce,
} from '../../store/actions';
import { clearConfirmTransaction } from '../../ducks/confirm-transaction/confirm-transaction.duck';
import { getMostRecentOverviewPage } from '../../ducks/history/history';
import ApproveContentCard from '../../components/app/approve-content-card/approve-content-card';
import CustomSpendingCap from '../../components/app/custom-spending-cap/custom-spending-cap';
import Dialog from '../../components/ui/dialog';
import { useGasFeeContext } from '../../contexts/gasFee';
import { getCustomTxParamsData } from '../confirm-approve/confirm-approve.util';
import { setCustomTokenAmount } from '../../ducks/app/app';
import { valuesFor } from '../../helpers/utils/util';
import { calcTokenAmount } from '../../../shared/lib/transactions-controller-utils';
import {
MAX_TOKEN_ALLOWANCE_AMOUNT,
NUM_W_OPT_DECIMAL_COMMA_OR_DOT_REGEX,
} from '../../../shared/constants/tokens';
import { isSuspiciousResponse } from '../../../shared/modules/security-provider.utils';
import { ConfirmPageContainerNavigation } from '../../components/app/confirm-page-container';
import { useSimulationFailureWarning } from '../../hooks/useSimulationFailureWarning';
import SimulationErrorMessage from '../../components/ui/simulation-error-message';
import LedgerInstructionField from '../../components/app/ledger-instruction-field/ledger-instruction-field';
import SecurityProviderBannerMessage from '../../components/app/security-provider-banner-message/security-provider-banner-message';
import { Icon, IconName, Text } from '../../components/component-library';
import { ConfirmPageContainerWarning } from '../../components/app/confirm-page-container/confirm-page-container-content';
import CustomNonce from '../../components/app/custom-nonce';
const ALLOWED_HOSTS = ['portfolio.metamask.io'];
export default function TokenAllowance({
origin,
siteImage,
showCustomizeGasModal,
useNonceField,
currentCurrency,
nativeCurrency,
ethTransactionTotal,
fiatTransactionTotal,
hexTransactionTotal,
hexMinimumTransactionFee,
txData,
isMultiLayerFeeNetwork,
supportsEIP1559,
userAddress,
tokenAddress,
data,
isSetApproveForAll,
isApprovalOrRejection,
decimals,
dappProposedTokenAmount,
currentTokenBalance,
toAddress,
tokenSymbol,
fromAddressIsLedger,
warning,
}) {
const t = useContext(I18nContext);
const dispatch = useDispatch();
const history = useHistory();
const mostRecentOverviewPage = useSelector(getMostRecentOverviewPage);
const { hostname } = new URL(origin);
const thisOriginIsAllowedToSkipFirstPage = ALLOWED_HOSTS.includes(hostname);
const [customSpendingCap, setCustomSpendingCap] = useState(
dappProposedTokenAmount,
);
const [showContractDetails, setShowContractDetails] = useState(false);
const [inputChangeInProgress, setInputChangeInProgress] = useState(false);
const [showFullTxDetails, setShowFullTxDetails] = useState(false);
const [isFirstPage, setIsFirstPage] = useState(
dappProposedTokenAmount !== '0' && !thisOriginIsAllowedToSkipFirstPage,
);
const [errorText, setErrorText] = useState('');
const [userAcknowledgedGasMissing, setUserAcknowledgedGasMissing] =
useState(false);
const renderSimulationFailureWarning = useSimulationFailureWarning(
userAcknowledgedGasMissing,
);
const fromAccount = useSelector((state) =>
getTargetAccountWithSendEtherInfo(state, userAddress),
);
const networkIdentifier = useSelector(getNetworkIdentifier);
const rpcPrefs = useSelector(getRpcPrefsForCurrentProvider);
const unapprovedTxCount = useSelector(getUnapprovedTxCount);
const unapprovedTxs = useSelector(getUnapprovedTransactions);
const useCurrencyRateCheck = useSelector(getUseCurrencyRateCheck);
const nextNonce = useSelector(getNextSuggestedNonce);
const customNonceValue = useSelector(getCustomNonceValue);
const replaceCommaToDot = (inputValue) => {
return inputValue.replace(/,/gu, '.');
};
let customPermissionAmount = NUM_W_OPT_DECIMAL_COMMA_OR_DOT_REGEX.test(
customSpendingCap,
)
? replaceCommaToDot(customSpendingCap).toString()
: '0';
const maxTokenAmount = calcTokenAmount(MAX_TOKEN_ALLOWANCE_AMOUNT, decimals);
if (customSpendingCap.length > 1 && Number(customSpendingCap)) {
const customSpendLimitNumber = new BigNumber(customSpendingCap);
if (customSpendLimitNumber.greaterThan(maxTokenAmount)) {
customPermissionAmount = 0;
}
}
const customTxParamsData = customPermissionAmount
? getCustomTxParamsData(data, {
customPermissionAmount,
decimals,
})
: null;
let fullTxData = { ...txData };
if (customTxParamsData) {
fullTxData = {
...fullTxData,
txParams: {
...fullTxData.txParams,
data: customTxParamsData,
},
};
}
const fee = useSelector((state) => transactionFeeSelector(state, fullTxData));
const methodData = useSelector((state) => getKnownMethodData(state, data));
const { balanceError } = useGasFeeContext();
const disableNextButton =
isFirstPage && (customSpendingCap === '' || errorText !== '');
const disableApproveButton = !isFirstPage && balanceError;
const networkName =
NETWORK_TO_NAME_MAP[fullTxData.chainId] || networkIdentifier;
const customNonceMerge = (transactionData) =>
customNonceValue
? {
...transactionData,
customNonceValue,
}
: transactionData;
const handleReject = () => {
dispatch(updateCustomNonce(''));
dispatch(setCustomTokenAmount(''));
dispatch(cancelTx(fullTxData)).then(() => {
dispatch(clearConfirmTransaction());
history.push(mostRecentOverviewPage);
});
};
const handleApprove = () => {
const { name } = methodData;
if (fee.gasEstimationObject.baseFeePerGas) {
fullTxData.estimatedBaseFee = fee.gasEstimationObject.baseFeePerGas;
}
if (name) {
fullTxData.contractMethodName = name;
}
if (dappProposedTokenAmount) {
fullTxData.dappProposedTokenAmount = dappProposedTokenAmount;
fullTxData.originalApprovalAmount = dappProposedTokenAmount;
}
if (customSpendingCap) {
fullTxData.customTokenAmount = customSpendingCap;
fullTxData.finalApprovalAmount = customSpendingCap;
} else if (dappProposedTokenAmount !== undefined) {
fullTxData.finalApprovalAmount = dappProposedTokenAmount;
}
if (currentTokenBalance) {
fullTxData.currentTokenBalance = currentTokenBalance;
}
dispatch(updateCustomNonce(''));
dispatch(updateAndApproveTx(customNonceMerge(fullTxData))).then(() => {
dispatch(clearConfirmTransaction());
history.push(mostRecentOverviewPage);
});
};
const handleNextClick = () => {
setShowFullTxDetails(false);
setIsFirstPage(false);
};
const handleBackClick = () => {
setShowFullTxDetails(false);
setIsFirstPage(true);
};
const handleCancelAll = () => {
dispatch(
showModal({
name: 'REJECT_TRANSACTIONS',
unapprovedTxCount,
onSubmit: async () => {
await dispatch(cancelTxs(valuesFor(unapprovedTxs)));
dispatch(clearConfirmTransaction());
history.push(mostRecentOverviewPage);
},
}),
);
};
const handleNextNonce = () => {
dispatch(getNextNonce());
};
useEffect(() => {
handleNextNonce();
}, [dispatch]);
const handleUpdateCustomNonce = (value) => {
dispatch(updateCustomNonce(value));
};
const handleCustomizeNonceModal = (
/* eslint-disable no-shadow */
useNonceField,
nextNonce,
customNonceValue,
updateCustomNonce,
getNextNonce,
/* eslint-disable no-shadow */
) => {
dispatch(
showModal({
name: 'CUSTOMIZE_NONCE',
useNonceField,
nextNonce,
customNonceValue,
updateCustomNonce,
getNextNonce,
}),
);
};
const isEmpty = customSpendingCap === '';
const renderContractTokenValues = (
);
return (
{isSuspiciousResponse(txData?.securityProviderResponse) && (
)}
{!isFirstPage && (
)}
{isFirstPage ? 1 : 2} {t('ofTextNofM')} 2
{warning && (
)}
{origin}
{isFirstPage ? (
t('spendingCapRequest', [renderContractTokenValues])
) : (
{customSpendingCap === '0' || isEmpty
? t('revokeSpendingCap', [renderContractTokenValues])
: t('spendingCapRequest', [renderContractTokenValues])}
)}
{isFirstPage ? (
setErrorText(value)}
decimals={decimals}
setInputChangeInProgress={setInputChangeInProgress}
customSpendingCap={customSpendingCap}
setCustomSpendingCap={setCustomSpendingCap}
/>
) : (
handleBackClick()}
/>
)}
{!isFirstPage && balanceError && (
)}
{!isFirstPage && (
{renderSimulationFailureWarning && (
setUserAcknowledgedGasMissing(true)
}
/>
)}
}
title={t('transactionFee')}
showEdit
showAdvanceGasFeeOptions
onEditClick={showCustomizeGasModal}
renderTransactionDetailsContent
noBorder={useNonceField || !showFullTxDetails}
supportsEIP1559={supportsEIP1559}
isMultiLayerFeeNetwork={isMultiLayerFeeNetwork}
ethTransactionTotal={ethTransactionTotal}
nativeCurrency={nativeCurrency}
fullTxData={fullTxData}
userAcknowledgedGasMissing={userAcknowledgedGasMissing}
renderSimulationFailureWarning={renderSimulationFailureWarning}
hexTransactionTotal={hexTransactionTotal}
hexMinimumTransactionFee={hexMinimumTransactionFee}
fiatTransactionTotal={fiatTransactionTotal}
currentCurrency={currentCurrency}
useCurrencyRateCheck={useCurrencyRateCheck}
/>
)}
{useNonceField && (
handleCustomizeNonceModal(
useNonceField,
nextNonce,
customNonceValue,
handleUpdateCustomNonce,
handleNextNonce,
)
}
/>
)}
{showFullTxDetails ? (
}
title={t('data')}
renderDataContent
noBorder
supportsEIP1559={supportsEIP1559}
isSetApproveForAll={isSetApproveForAll}
fullTxData={fullTxData}
userAcknowledgedGasMissing={userAcknowledgedGasMissing}
renderSimulationFailureWarning={renderSimulationFailureWarning}
isApprovalOrRejection={isApprovalOrRejection}
data={customTxParamsData || data}
useCurrencyRateCheck={useCurrencyRateCheck}
hexMinimumTransactionFee={hexMinimumTransactionFee}
/>
) : null}
{!isFirstPage && fromAddressIsLedger && (
)}
handleReject()}
onSubmit={() => (isFirstPage ? handleNextClick() : handleApprove())}
disabled={
inputChangeInProgress || disableNextButton || disableApproveButton
}
>
{unapprovedTxCount > 1 && (
)}
{showContractDetails && (
setShowContractDetails(false)}
tokenAddress={tokenAddress}
toAddress={toAddress}
chainId={fullTxData.chainId}
rpcPrefs={rpcPrefs}
/>
)}
);
}
TokenAllowance.propTypes = {
/**
* Dapp URL
*/
origin: PropTypes.string,
/**
* Dapp image
*/
siteImage: PropTypes.string,
/**
* Function that is supposed to open the customized gas modal
*/
showCustomizeGasModal: PropTypes.func,
/**
* Whether nonce field should be used or not
*/
useNonceField: PropTypes.bool,
/**
* Current fiat currency (e.g. USD)
*/
currentCurrency: PropTypes.string,
/**
* Current native currency (e.g. RopstenETH)
*/
nativeCurrency: PropTypes.string,
/**
* Total sum of the transaction in native currency
*/
ethTransactionTotal: PropTypes.string,
/**
* Total sum of the transaction in fiat currency
*/
fiatTransactionTotal: PropTypes.string,
/**
* Total sum of the transaction converted to hex value
*/
hexTransactionTotal: PropTypes.string,
/**
* Minimum transaction fee converted to hex value
*/
hexMinimumTransactionFee: PropTypes.string,
/**
* Current transaction
*/
txData: PropTypes.object,
/**
* Is multi-layer fee network or not
*/
isMultiLayerFeeNetwork: PropTypes.bool,
/**
* Is the enhanced gas fee enabled or not
*/
supportsEIP1559: PropTypes.bool,
/**
* User's address
*/
userAddress: PropTypes.string,
/**
* Address of the token that is waiting to be allowed
*/
tokenAddress: PropTypes.string,
/**
* Current transaction data
*/
data: PropTypes.string,
/**
* Is set approve for all or not
*/
isSetApproveForAll: PropTypes.bool,
/**
* Whether a current set approval for all transaction will approve or revoke access
*/
isApprovalOrRejection: PropTypes.bool,
/**
* Number of decimals
*/
decimals: PropTypes.string,
/**
* Token amount proposed by the Dapp
*/
dappProposedTokenAmount: PropTypes.string,
/**
* Token balance of the current account
*/
currentTokenBalance: PropTypes.string,
/**
* Contract address requesting spending cap
*/
toAddress: PropTypes.string,
/**
* Symbol of the token that is waiting to be allowed
*/
tokenSymbol: PropTypes.string,
/**
* Whether the address sending the transaction is a ledger address
*/
fromAddressIsLedger: PropTypes.bool,
/**
* Customize nonce warning message
*/
warning: PropTypes.string,
};