1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-23 09:52:26 +01:00

Display a warning and gas fee component for token allowance and NFT flow when transaction is expected to fail (#17437)

Co-authored-by: Pedro Figueiredo <pedro.figueiredo@consensys.net>
Co-authored-by: Dan J Miller <danjm.com@gmail.com>
Co-authored-by: Brad Decker <bhdecker84@gmail.com>
This commit is contained in:
Vladimir Saric 2023-02-02 00:14:09 +01:00 committed by PeterYinusa
parent 4b18d48366
commit 222a7dd881
12 changed files with 390 additions and 170 deletions

View File

@ -43,6 +43,8 @@ export default function ApproveContentCard({
isSetApproveForAll, isSetApproveForAll,
isApprovalOrRejection, isApprovalOrRejection,
data, data,
userAcknowledgedGasMissing,
renderSimulationFailureWarning,
}) { }) {
const t = useContext(I18nContext); const t = useContext(I18nContext);
@ -91,9 +93,14 @@ export default function ApproveContentCard({
</Button> </Button>
</Box> </Box>
)} )}
{showEdit && showAdvanceGasFeeOptions && supportsEIP1559 && ( {showEdit &&
<EditGasFeeButton /> showAdvanceGasFeeOptions &&
)} supportsEIP1559 &&
!renderSimulationFailureWarning && (
<EditGasFeeButton
userAcknowledgedGasMissing={userAcknowledgedGasMissing}
/>
)}
</Box> </Box>
)} )}
<Box <Box
@ -102,8 +109,12 @@ export default function ApproveContentCard({
className="approve-content-card-container__card-content" className="approve-content-card-container__card-content"
> >
{renderTransactionDetailsContent && {renderTransactionDetailsContent &&
(!isMultiLayerFeeNetwork && supportsEIP1559 ? ( (!isMultiLayerFeeNetwork &&
<GasDetailsItem /> supportsEIP1559 &&
!renderSimulationFailureWarning ? (
<GasDetailsItem
userAcknowledgedGasMissing={userAcknowledgedGasMissing}
/>
) : ( ) : (
<Box <Box
display={DISPLAY.FLEX} display={DISPLAY.FLEX}
@ -301,4 +312,12 @@ ApproveContentCard.propTypes = {
* Current transaction data * Current transaction data
*/ */
data: PropTypes.string, data: PropTypes.string,
/**
* User acknowledge gas is missing or not
*/
userAcknowledgedGasMissing: PropTypes.bool,
/**
* Render simulation failure warning
*/
renderSimulationFailureWarning: PropTypes.bool,
}; };

View File

@ -7,6 +7,7 @@ import { submittedPendingTransactionsSelector } from '../../../selectors/transac
import { useGasFeeContext } from '../../../contexts/gasFee'; import { useGasFeeContext } from '../../../contexts/gasFee';
import { useI18nContext } from '../../../hooks/useI18nContext'; import { useI18nContext } from '../../../hooks/useI18nContext';
import ActionableMessage from '../../ui/actionable-message/actionable-message'; import ActionableMessage from '../../ui/actionable-message/actionable-message';
import SimulationErrorMessage from '../../ui/simulation-error-message';
import Typography from '../../ui/typography'; import Typography from '../../ui/typography';
import { TYPOGRAPHY } from '../../../helpers/constants/design-system'; import { TYPOGRAPHY } from '../../../helpers/constants/design-system';
import ZENDESK_URLS from '../../../helpers/constants/zendesk-url'; import ZENDESK_URLS from '../../../helpers/constants/zendesk-url';
@ -23,19 +24,9 @@ const TransactionAlerts = ({
return ( return (
<div className="transaction-alerts"> <div className="transaction-alerts">
{supportsEIP1559 && hasSimulationError && ( {supportsEIP1559 && hasSimulationError && (
<ActionableMessage <SimulationErrorMessage
message={t('simulationErrorMessageV2')} userAcknowledgedGasMissing={userAcknowledgedGasMissing}
useIcon setUserAcknowledgedGasMissing={setUserAcknowledgedGasMissing}
iconFillColor="var(--color-error-default)"
type="danger"
primaryActionV2={
userAcknowledgedGasMissing === true
? undefined
: {
label: t('proceedWithTransaction'),
onClick: setUserAcknowledgedGasMissing,
}
}
/> />
)} )}
{supportsEIP1559 && pendingTransactions?.length > 0 && ( {supportsEIP1559 && pendingTransactions?.length > 0 && (

View File

@ -0,0 +1 @@
export { default } from './simulation-error-message';

View File

@ -0,0 +1,33 @@
import React, { useContext } from 'react';
import PropTypes from 'prop-types';
import ActionableMessage from '../actionable-message';
import { I18nContext } from '../../../../.storybook/i18n';
export default function SimulationErrorMessage({
userAcknowledgedGasMissing = false,
setUserAcknowledgedGasMissing,
}) {
const t = useContext(I18nContext);
return (
<ActionableMessage
message={t('simulationErrorMessageV2')}
useIcon
iconFillColor="var(--color-error-default)"
type="danger"
primaryActionV2={
userAcknowledgedGasMissing === true
? undefined
: {
label: t('proceedWithTransaction'),
onClick: setUserAcknowledgedGasMissing,
}
}
/>
);
}
SimulationErrorMessage.propTypes = {
userAcknowledgedGasMissing: PropTypes.bool,
setUserAcknowledgedGasMissing: PropTypes.func,
};

View File

@ -0,0 +1,65 @@
import React from 'react';
import configureMockStore from 'redux-mock-store';
import { fireEvent } from '@testing-library/react';
import { renderWithProvider } from '../../../../test/lib/render-helpers';
import SimulationErrorMessage from './simulation-error-message';
describe('Simulation Error Message', () => {
const store = configureMockStore()({});
let props = {};
beforeEach(() => {
props = {
userAcknowledgedGasMissing: false,
setUserAcknowledgedGasMissing: jest.fn(),
};
});
it('should render SimulationErrorMessage component with I want to procced anyway link', () => {
const { queryByText } = renderWithProvider(
<SimulationErrorMessage {...props} />,
store,
);
expect(
queryByText(
'We were not able to estimate gas. There might be an error in the contract and this transaction may fail.',
),
).toBeInTheDocument();
expect(queryByText('I want to proceed anyway')).toBeInTheDocument();
});
it('should render SimulationErrorMessage component without I want to procced anyway link', () => {
props.userAcknowledgedGasMissing = true;
const { queryByText } = renderWithProvider(
<SimulationErrorMessage {...props} />,
store,
);
expect(
queryByText(
'We were not able to estimate gas. There might be an error in the contract and this transaction may fail.',
),
).toBeInTheDocument();
expect(queryByText('I want to proceed anyway')).not.toBeInTheDocument();
});
it('should render SimulationErrorMessage component with I want to proceed anyway and fire that event', () => {
props.userAcknowledgedGasMissing = false;
const { queryByText, getByText } = renderWithProvider(
<SimulationErrorMessage {...props} />,
store,
);
expect(
queryByText(
'We were not able to estimate gas. There might be an error in the contract and this transaction may fail.',
),
).toBeInTheDocument();
expect(queryByText('I want to proceed anyway')).toBeInTheDocument();
const proceedAnywayLink = getByText('I want to proceed anyway');
fireEvent.click(proceedAnywayLink);
expect(props.setUserAcknowledgedGasMissing).toHaveBeenCalledTimes(1);
});
});

View File

@ -0,0 +1,18 @@
import { useSelector } from 'react-redux';
import { txDataSelector } from '../selectors';
/**
* Returns the simulation failure warning if a simulaiton error
* is present and user didn't acknowledge gas missing
*
* @param {boolean} userAcknowledgedGasMissing - Whether the user acknowledge gas missing
* @returns {boolean} simulation failure warning
*/
export function useSimulationFailureWarning(userAcknowledgedGasMissing) {
const txData = useSelector(txDataSelector) || {};
const hasSimulationError = Boolean(txData.simulationFails);
const renderSimulationFailureWarning =
hasSimulationError && !userAcknowledgedGasMissing;
return renderSimulationFailureWarning;
}

View File

@ -2,16 +2,15 @@ import React, { Component } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classnames from 'classnames'; import classnames from 'classnames';
import copyToClipboard from 'copy-to-clipboard'; import copyToClipboard from 'copy-to-clipboard';
import { getTokenTrackerLink, getAccountLink } from '@metamask/etherscan-link'; import { getTokenTrackerLink } from '@metamask/etherscan-link';
import UrlIcon from '../../../components/ui/url-icon'; import UrlIcon from '../../../components/ui/url-icon';
import { addressSummary } from '../../../helpers/utils/util'; import { addressSummary } from '../../../helpers/utils/util';
import { formatCurrency } from '../../../helpers/utils/confirm-tx.util'; import { formatCurrency } from '../../../helpers/utils/confirm-tx.util';
import { ellipsify } from '../../send/send.utils';
import Typography from '../../../components/ui/typography'; import Typography from '../../../components/ui/typography';
import Box from '../../../components/ui/box'; import Box from '../../../components/ui/box';
import Button from '../../../components/ui/button'; import Button from '../../../components/ui/button';
import SimulationErrorMessage from '../../../components/ui/simulation-error-message';
import EditGasFeeButton from '../../../components/app/edit-gas-fee-button'; import EditGasFeeButton from '../../../components/app/edit-gas-fee-button';
import Identicon from '../../../components/ui/identicon';
import MultiLayerFeeMessage from '../../../components/app/multilayer-fee-message'; import MultiLayerFeeMessage from '../../../components/app/multilayer-fee-message';
import CopyIcon from '../../../components/ui/icon/copy-icon.component'; import CopyIcon from '../../../components/ui/icon/copy-icon.component';
import { import {
@ -74,12 +73,15 @@ export default class ConfirmApproveContent extends Component {
isSetApproveForAll: PropTypes.bool, isSetApproveForAll: PropTypes.bool,
isApprovalOrRejection: PropTypes.bool, isApprovalOrRejection: PropTypes.bool,
userAddress: PropTypes.string, userAddress: PropTypes.string,
userAcknowledgedGasMissing: PropTypes.bool,
setUserAcknowledgedGasMissing: PropTypes.func,
renderSimulationFailureWarning: PropTypes.bool,
}; };
state = { state = {
showFullTxDetails: false, showFullTxDetails: false,
copied: false, copied: false,
setshowContractDetails: false, setShowContractDetails: false,
}; };
renderApproveContentCard({ renderApproveContentCard({
@ -93,7 +95,11 @@ export default class ConfirmApproveContent extends Component {
footer, footer,
noBorder, noBorder,
}) { }) {
const { supportsEIP1559 } = this.props; const {
supportsEIP1559,
renderSimulationFailureWarning,
userAcknowledgedGasMissing,
} = this.props;
const { t } = this.context; const { t } = this.context;
return ( return (
<div <div
@ -125,9 +131,14 @@ export default class ConfirmApproveContent extends Component {
</Button> </Button>
</Box> </Box>
)} )}
{showEdit && showAdvanceGasFeeOptions && supportsEIP1559 && ( {showEdit &&
<EditGasFeeButton /> showAdvanceGasFeeOptions &&
)} supportsEIP1559 &&
!renderSimulationFailureWarning && (
<EditGasFeeButton
userAcknowledgedGasMissing={userAcknowledgedGasMissing}
/>
)}
</div> </div>
)} )}
<div className="confirm-approve-content__card-content">{content}</div> <div className="confirm-approve-content__card-content">{content}</div>
@ -148,9 +159,19 @@ export default class ConfirmApproveContent extends Component {
txData, txData,
isMultiLayerFeeNetwork, isMultiLayerFeeNetwork,
supportsEIP1559, supportsEIP1559,
userAcknowledgedGasMissing,
renderSimulationFailureWarning,
} = this.props; } = this.props;
if (!isMultiLayerFeeNetwork && supportsEIP1559) { if (
return <GasDetailsItem />; !isMultiLayerFeeNetwork &&
supportsEIP1559 &&
!renderSimulationFailureWarning
) {
return (
<GasDetailsItem
userAcknowledgedGasMissing={userAcknowledgedGasMissing}
/>
);
} }
return ( return (
<div className="confirm-approve-content__transaction-details-content"> <div className="confirm-approve-content__transaction-details-content">
@ -604,16 +625,10 @@ export default class ConfirmApproveContent extends Component {
render() { render() {
const { t } = this.context; const { t } = this.context;
const { const {
decimals,
siteImage, siteImage,
tokenAmount,
customTokenAmount,
origin, origin,
tokenSymbol, tokenSymbol,
showCustomizeGasModal, showCustomizeGasModal,
showEditApprovalPermissionModal,
setCustomAmount,
tokenBalance,
useNonceField, useNonceField,
warning, warning,
txData, txData,
@ -621,15 +636,15 @@ export default class ConfirmApproveContent extends Component {
toAddress, toAddress,
chainId, chainId,
rpcPrefs, rpcPrefs,
isContract,
assetStandard, assetStandard,
userAddress,
tokenId, tokenId,
tokenAddress, tokenAddress,
assetName, assetName,
isSetApproveForAll, userAcknowledgedGasMissing,
setUserAcknowledgedGasMissing,
renderSimulationFailureWarning,
} = this.props; } = this.props;
const { showFullTxDetails, setshowContractDetails } = this.state; const { showFullTxDetails, setShowContractDetails } = this.state;
return ( return (
<div <div
@ -672,125 +687,44 @@ export default class ConfirmApproveContent extends Component {
<div className="confirm-approve-content__description"> <div className="confirm-approve-content__description">
{this.renderDescription()} {this.renderDescription()}
</div> </div>
{assetStandard === TokenStandard.ERC20 || <Box marginBottom={4} marginTop={2}>
(tokenSymbol && !tokenId && !isSetApproveForAll) ? ( <Button
<Box className="confirm-approve-content__address-display-content"> type="link"
<Box display={DISPLAY.FLEX}> className="confirm-approve-content__verify-contract-details"
<Identicon onClick={() => this.setState({ setShowContractDetails: true })}
className="confirm-approve-content__address-identicon" >
diameter={20} {t('verifyContractDetails')}
address={toAddress} </Button>
/> {setShowContractDetails && (
<Typography <ContractDetailsModal
variant={TYPOGRAPHY.H6} onClose={() => this.setState({ setShowContractDetails: false })}
fontWeight={FONT_WEIGHT.NORMAL} tokenName={tokenSymbol}
color={COLORS.TEXT_ALTERNATIVE} tokenAddress={tokenAddress}
boxProps={{ marginBottom: 0 }} toAddress={toAddress}
> chainId={chainId}
{ellipsify(toAddress)} rpcPrefs={rpcPrefs}
</Typography> tokenId={tokenId}
<Button assetName={assetName}
type="link" assetStandard={assetStandard}
className="confirm-approve-content__copy-address" />
onClick={() => { )}
this.setState({ copied: true }); </Box>
this.copyTimeout = setTimeout(
() => this.setState({ copied: false }),
SECOND * 3,
);
copyToClipboard(toAddress);
}}
title={
this.state.copied
? t('copiedExclamation')
: t('copyToClipboard')
}
>
<CopyIcon size={9} color="var(--color-icon-default)" />
</Button>
<Button
type="link"
className="confirm-approve-content__etherscan-link"
onClick={() => {
const blockExplorerTokenLink = isContract
? getTokenTrackerLink(
toAddress,
chainId,
null,
userAddress,
{
blockExplorerUrl: rpcPrefs?.blockExplorerUrl ?? null,
},
)
: getAccountLink(
toAddress,
chainId,
{
blockExplorerUrl: rpcPrefs?.blockExplorerUrl ?? null,
},
null,
);
global.platform.openTab({
url: blockExplorerTokenLink,
});
}}
target="_blank"
rel="noopener noreferrer"
title={t('etherscanView')}
>
<i
className="fa fa-share-square fa-sm"
style={{ color: 'var(--color-icon-default)', fontSize: 11 }}
title={t('etherscanView')}
/>
</Button>
</Box>
</Box>
) : (
<Box marginBottom={4} marginTop={2}>
<Button
type="link"
className="confirm-approve-content__verify-contract-details"
onClick={() => this.setState({ setshowContractDetails: true })}
>
{t('verifyContractDetails')}
</Button>
{setshowContractDetails && (
<ContractDetailsModal
onClose={() => this.setState({ setshowContractDetails: false })}
tokenName={tokenSymbol}
tokenAddress={tokenAddress}
toAddress={toAddress}
chainId={chainId}
rpcPrefs={rpcPrefs}
tokenId={tokenId}
assetName={assetName}
assetStandard={assetStandard}
/>
)}
</Box>
)}
{assetStandard === TokenStandard.ERC20 ? (
<div className="confirm-approve-content__edit-submission-button-container">
<div
className="confirm-approve-content__medium-link-text cursor-pointer"
onClick={() =>
showEditApprovalPermissionModal({
customTokenAmount,
decimals,
origin,
setCustomAmount,
tokenAmount,
tokenSymbol,
tokenBalance,
})
}
>
{t('editPermission')}
</div>
</div>
) : null}
<div className="confirm-approve-content__card-wrapper"> <div className="confirm-approve-content__card-wrapper">
{renderSimulationFailureWarning && (
<Box
paddingTop={0}
paddingRight={6}
paddingBottom={4}
paddingLeft={6}
>
<SimulationErrorMessage
userAcknowledgedGasMissing={userAcknowledgedGasMissing}
setUserAcknowledgedGasMissing={() =>
setUserAcknowledgedGasMissing(true)
}
/>
</Box>
)}
{this.renderApproveContentCard({ {this.renderApproveContentCard({
symbol: <i className="fa fa-tag" />, symbol: <i className="fa fa-tag" />,
title: t('transactionFee'), title: t('transactionFee'),

View File

@ -33,6 +33,10 @@ const props = {
useNonceField: true, useNonceField: true,
nextNonce: 1, nextNonce: 1,
customNonceValue: '2', customNonceValue: '2',
txData: { simulationFails: null },
userAcknowledgedGasMissing: false,
setUserAcknowledgedGasMissing: jest.fn(),
renderSimulationFailureWarning: false,
showCustomizeNonceModal: jest.fn(), showCustomizeNonceModal: jest.fn(),
chainId: '1337', chainId: '1337',
rpcPrefs: {}, rpcPrefs: {},
@ -54,7 +58,13 @@ describe('ConfirmApproveContent Component', () => {
'By granting permission, you are allowing the following contract to access your funds', 'By granting permission, you are allowing the following contract to access your funds',
), ),
).toBeInTheDocument(); ).toBeInTheDocument();
expect(queryByText('0x9bc5...fef4')).toBeInTheDocument(); expect(queryByText('Verify contract details')).toBeInTheDocument();
expect(
queryByText(
'We were not able to estimate gas. There might be an error in the contract and this transaction may fail.',
),
).not.toBeInTheDocument();
expect(queryByText('I want to proceed anyway')).not.toBeInTheDocument();
expect(queryByText('View full transaction details')).toBeInTheDocument(); expect(queryByText('View full transaction details')).toBeInTheDocument();
expect(queryByText('Edit permission')).toBeInTheDocument(); expect(queryByText('Edit permission')).toBeInTheDocument();
@ -88,4 +98,124 @@ describe('ConfirmApproveContent Component', () => {
expect(getByText('Granted to:')).toBeInTheDocument(); expect(getByText('Granted to:')).toBeInTheDocument();
expect(getByText('0x9bc5...fef4')).toBeInTheDocument(); expect(getByText('0x9bc5...fef4')).toBeInTheDocument();
}); });
it('should render Confirm approve page correctly and simulation error message without I want to procced anyway link', () => {
props.userAcknowledgedGasMissing = true;
props.renderSimulationFailureWarning = true;
const { queryByText, getByText, getAllByText, getByTestId } =
renderComponent(props);
expect(
queryByText('https://metamask.github.io/test-dapp/'),
).toBeInTheDocument();
expect(getByTestId('confirm-approve-title').textContent).toStrictEqual(
' Allow access to and transfer of your TestDappCollectibles (#1)? ',
);
expect(
queryByText(
'This allows a third party to access and transfer the following NFTs without further notice until you revoke its access.',
),
).toBeInTheDocument();
expect(queryByText('Verify contract details')).toBeInTheDocument();
expect(
queryByText(
'We were not able to estimate gas. There might be an error in the contract and this transaction may fail.',
),
).toBeInTheDocument();
expect(queryByText('I want to proceed anyway')).not.toBeInTheDocument();
expect(queryByText('View full transaction details')).toBeInTheDocument();
const editButtons = getAllByText('Edit');
expect(queryByText('Transaction fee')).toBeInTheDocument();
expect(
queryByText('A fee is associated with this request.'),
).toBeInTheDocument();
expect(queryByText(`${props.ethTransactionTotal} ETH`)).toBeInTheDocument();
fireEvent.click(editButtons[0]);
expect(props.showCustomizeGasModal).toHaveBeenCalledTimes(2);
expect(queryByText('Nonce')).toBeInTheDocument();
expect(queryByText('2')).toBeInTheDocument();
fireEvent.click(editButtons[1]);
expect(props.showCustomizeNonceModal).toHaveBeenCalledTimes(2);
const showViewTxDetails = getByText('View full transaction details');
expect(queryByText('Permission request')).not.toBeInTheDocument();
expect(queryByText('Approved asset:')).not.toBeInTheDocument();
expect(queryByText('Granted to:')).not.toBeInTheDocument();
expect(queryByText('Data')).not.toBeInTheDocument();
fireEvent.click(showViewTxDetails);
expect(getByText('Hide full transaction details')).toBeInTheDocument();
expect(getByText('Permission request')).toBeInTheDocument();
expect(getByText('Approved asset:')).toBeInTheDocument();
expect(getByText('Granted to:')).toBeInTheDocument();
expect(getByText('Contract (0x9bc5baF8...fEF4)')).toBeInTheDocument();
expect(getByText('Data')).toBeInTheDocument();
expect(getByText('Function: Approve')).toBeInTheDocument();
expect(
getByText(
'0x095ea7b30000000000000000000000009bc5baf874d2da8d216ae9f137804184ee5afef40000000000000000000000000000000000000000000000000000000000011170',
),
).toBeInTheDocument();
});
it('should render Confirm approve page correctly and simulation error message with I want to procced anyway link', () => {
props.userAcknowledgedGasMissing = false;
props.renderSimulationFailureWarning = true;
const { queryByText, getByText, getAllByText, getByTestId } =
renderComponent(props);
expect(
queryByText('https://metamask.github.io/test-dapp/'),
).toBeInTheDocument();
expect(getByTestId('confirm-approve-title').textContent).toStrictEqual(
' Allow access to and transfer of your TestDappCollectibles (#1)? ',
);
expect(
queryByText(
'This allows a third party to access and transfer the following NFTs without further notice until you revoke its access.',
),
).toBeInTheDocument();
expect(queryByText('Verify contract details')).toBeInTheDocument();
expect(
queryByText(
'We were not able to estimate gas. There might be an error in the contract and this transaction may fail.',
),
).toBeInTheDocument();
expect(queryByText('I want to proceed anyway')).toBeInTheDocument();
expect(queryByText('View full transaction details')).toBeInTheDocument();
const editButtons = getAllByText('Edit');
expect(queryByText('Transaction fee')).toBeInTheDocument();
expect(
queryByText('A fee is associated with this request.'),
).toBeInTheDocument();
expect(queryByText(`${props.ethTransactionTotal} ETH`)).toBeInTheDocument();
fireEvent.click(editButtons[0]);
expect(props.showCustomizeGasModal).toHaveBeenCalledTimes(3);
expect(queryByText('Nonce')).toBeInTheDocument();
expect(queryByText('2')).toBeInTheDocument();
fireEvent.click(editButtons[1]);
expect(props.showCustomizeNonceModal).toHaveBeenCalledTimes(3);
const showViewTxDetails = getByText('View full transaction details');
expect(queryByText('Permission request')).not.toBeInTheDocument();
expect(queryByText('Approved asset:')).not.toBeInTheDocument();
expect(queryByText('Granted to:')).not.toBeInTheDocument();
expect(queryByText('Data')).not.toBeInTheDocument();
fireEvent.click(showViewTxDetails);
expect(getByText('Hide full transaction details')).toBeInTheDocument();
expect(getByText('Permission request')).toBeInTheDocument();
expect(getByText('Approved asset:')).toBeInTheDocument();
expect(getByText('Granted to:')).toBeInTheDocument();
expect(getByText('Contract (0x9bc5baF8...fEF4)')).toBeInTheDocument();
expect(getByText('Data')).toBeInTheDocument();
expect(getByText('Function: Approve')).toBeInTheDocument();
expect(
getByText(
'0x095ea7b30000000000000000000000009bc5baf874d2da8d216ae9f137804184ee5afef40000000000000000000000000000000000000000000000000000000000011170',
),
).toBeInTheDocument();
});
}); });

View File

@ -129,6 +129,7 @@ export default {
useNonceField: true, useNonceField: true,
nextNonce: 1, nextNonce: 1,
customNonceValue: '2', customNonceValue: '2',
txData: { simulationFails: null },
chainId: '1337', chainId: '1337',
rpcPrefs: {}, rpcPrefs: {},
isContract: true, isContract: true,

View File

@ -30,6 +30,7 @@ import {
getIsImprovedTokenAllowanceEnabled, getIsImprovedTokenAllowanceEnabled,
} from '../../selectors'; } from '../../selectors';
import { useApproveTransaction } from '../../hooks/useApproveTransaction'; import { useApproveTransaction } from '../../hooks/useApproveTransaction';
import { useSimulationFailureWarning } from '../../hooks/useSimulationFailureWarning';
import AdvancedGasFeePopover from '../../components/app/advanced-gas-fee-popover'; import AdvancedGasFeePopover from '../../components/app/advanced-gas-fee-popover';
import EditGasFeePopover from '../../components/app/edit-gas-fee-popover'; import EditGasFeePopover from '../../components/app/edit-gas-fee-popover';
import EditGasPopover from '../../components/app/edit-gas-popover/edit-gas-popover.component'; import EditGasPopover from '../../components/app/edit-gas-popover/edit-gas-popover.component';
@ -84,6 +85,8 @@ export default function ConfirmApprove({
const [customPermissionAmount, setCustomPermissionAmount] = useState(''); const [customPermissionAmount, setCustomPermissionAmount] = useState('');
const [submitWarning, setSubmitWarning] = useState(''); const [submitWarning, setSubmitWarning] = useState('');
const [isContract, setIsContract] = useState(false); const [isContract, setIsContract] = useState(false);
const [userAcknowledgedGasMissing, setUserAcknowledgedGasMissing] =
useState(false);
const supportsEIP1559 = networkAndAccountSupports1559; const supportsEIP1559 = networkAndAccountSupports1559;
@ -97,6 +100,9 @@ export default function ConfirmApprove({
showCustomizeGasPopover, showCustomizeGasPopover,
closeCustomizeGasPopover, closeCustomizeGasPopover,
} = useApproveTransaction(); } = useApproveTransaction();
const renderSimulationFailureWarning = useSimulationFailureWarning(
userAcknowledgedGasMissing,
);
useEffect(() => { useEffect(() => {
if (customPermissionAmount && previousTokenAmount.current !== tokenAmount) { if (customPermissionAmount && previousTokenAmount.current !== tokenAmount) {
@ -284,6 +290,9 @@ export default function ConfirmApprove({
useNonceField={useNonceField} useNonceField={useNonceField}
nextNonce={nextNonce} nextNonce={nextNonce}
customNonceValue={customNonceValue} customNonceValue={customNonceValue}
userAcknowledgedGasMissing={userAcknowledgedGasMissing}
setUserAcknowledgedGasMissing={setUserAcknowledgedGasMissing}
renderSimulationFailureWarning={renderSimulationFailureWarning}
updateCustomNonce={(value) => { updateCustomNonce={(value) => {
dispatch(updateCustomNonce(value)); dispatch(updateCustomNonce(value));
}} }}

View File

@ -19,7 +19,7 @@ import CopyRawData from '../../components/app/transaction-decoding/components/ui
import { PRIMARY, SECONDARY } from '../../helpers/constants/common'; import { PRIMARY, SECONDARY } from '../../helpers/constants/common';
import TextField from '../../components/ui/text-field'; import TextField from '../../components/ui/text-field';
import ActionableMessage from '../../components/ui/actionable-message'; import SimulationErrorMessage from '../../components/ui/simulation-error-message';
import Disclosure from '../../components/ui/disclosure'; import Disclosure from '../../components/ui/disclosure';
import { EVENT } from '../../../shared/constants/metametrics'; import { EVENT } from '../../../shared/constants/metametrics';
import { import {
@ -586,18 +586,10 @@ export default class ConfirmTransactionBase extends Component {
const simulationFailureWarning = () => ( const simulationFailureWarning = () => (
<div className="confirm-page-container-content__error-container"> <div className="confirm-page-container-content__error-container">
<ActionableMessage <SimulationErrorMessage
message={t('simulationErrorMessageV2')} userAcknowledgedGasMissing={userAcknowledgedGasMissing}
useIcon setUserAcknowledgedGasMissing={() =>
iconFillColor="var(--color-error-default)" this.setUserAcknowledgedGasMissing()
type="danger"
primaryActionV2={
userAcknowledgedGasMissing === true
? undefined
: {
label: t('proceedWithTransaction'),
onClick: () => this.setUserAcknowledgedGasMissing(),
}
} }
/> />
</div> </div>

View File

@ -57,6 +57,8 @@ import {
NUM_W_OPT_DECIMAL_COMMA_OR_DOT_REGEX, NUM_W_OPT_DECIMAL_COMMA_OR_DOT_REGEX,
} from '../../../shared/constants/tokens'; } from '../../../shared/constants/tokens';
import { ConfirmPageContainerNavigation } from '../../components/app/confirm-page-container'; import { ConfirmPageContainerNavigation } from '../../components/app/confirm-page-container';
import { useSimulationFailureWarning } from '../../hooks/useSimulationFailureWarning';
import SimulationErrorMessage from '../../components/ui/simulation-error-message';
export default function TokenAllowance({ export default function TokenAllowance({
origin, origin,
@ -93,7 +95,12 @@ export default function TokenAllowance({
dappProposedTokenAmount !== '0', dappProposedTokenAmount !== '0',
); );
const [errorText, setErrorText] = useState(''); const [errorText, setErrorText] = useState('');
const [userAcknowledgedGasMissing, setUserAcknowledgedGasMissing] =
useState(false);
const renderSimulationFailureWarning = useSimulationFailureWarning(
userAcknowledgedGasMissing,
);
const currentAccount = useSelector(getCurrentAccountWithSendEtherInfo); const currentAccount = useSelector(getCurrentAccountWithSendEtherInfo);
const networkIdentifier = useSelector(getNetworkIdentifier); const networkIdentifier = useSelector(getNetworkIdentifier);
const rpcPrefs = useSelector(getRpcPrefsForCurrentProvider); const rpcPrefs = useSelector(getRpcPrefsForCurrentProvider);
@ -391,6 +398,21 @@ export default function TokenAllowance({
)} )}
{!isFirstPage && ( {!isFirstPage && (
<Box className="token-allowance-container__card-wrapper"> <Box className="token-allowance-container__card-wrapper">
{renderSimulationFailureWarning && (
<Box
paddingTop={0}
paddingRight={4}
paddingBottom={4}
paddingLeft={4}
>
<SimulationErrorMessage
userAcknowledgedGasMissing={userAcknowledgedGasMissing}
setUserAcknowledgedGasMissing={() =>
setUserAcknowledgedGasMissing(true)
}
/>
</Box>
)}
<ApproveContentCard <ApproveContentCard
symbol={<i className="fa fa-tag" />} symbol={<i className="fa fa-tag" />}
title={t('transactionFee')} title={t('transactionFee')}
@ -404,6 +426,8 @@ export default function TokenAllowance({
ethTransactionTotal={ethTransactionTotal} ethTransactionTotal={ethTransactionTotal}
nativeCurrency={nativeCurrency} nativeCurrency={nativeCurrency}
fullTxData={fullTxData} fullTxData={fullTxData}
userAcknowledgedGasMissing={userAcknowledgedGasMissing}
renderSimulationFailureWarning={renderSimulationFailureWarning}
hexTransactionTotal={hexTransactionTotal} hexTransactionTotal={hexTransactionTotal}
fiatTransactionTotal={fiatTransactionTotal} fiatTransactionTotal={fiatTransactionTotal}
currentCurrency={currentCurrency} currentCurrency={currentCurrency}
@ -449,6 +473,9 @@ export default function TokenAllowance({
noBorder noBorder
supportsEIP1559={supportsEIP1559} supportsEIP1559={supportsEIP1559}
isSetApproveForAll={isSetApproveForAll} isSetApproveForAll={isSetApproveForAll}
fullTxData={fullTxData}
userAcknowledgedGasMissing={userAcknowledgedGasMissing}
renderSimulationFailureWarning={renderSimulationFailureWarning}
isApprovalOrRejection={isApprovalOrRejection} isApprovalOrRejection={isApprovalOrRejection}
data={customTxParamsData || data} data={customTxParamsData || data}
/> />