mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-23 09:52:26 +01:00
Confirm transaction page - onchain failure handling (#12743)
This commit is contained in:
parent
fb27e170ac
commit
7609841902
@ -1382,7 +1382,8 @@
|
||||
"message": "Want to $1 about gas?"
|
||||
},
|
||||
"learnCancelSpeeedup": {
|
||||
"message": "Learn how to $1"
|
||||
"message": "Learn how to $1",
|
||||
"description": "$1 is link to cancel or speed up transactions"
|
||||
},
|
||||
"learnMore": {
|
||||
"message": "learn more"
|
||||
@ -1969,12 +1970,16 @@
|
||||
"pending": {
|
||||
"message": "Pending"
|
||||
},
|
||||
"pendingTransaction": {
|
||||
"message": "You have ($1) pending transaction(s)."
|
||||
},
|
||||
"pendingTransactionInfo": {
|
||||
"message": "This transaction will not process until that one is complete."
|
||||
},
|
||||
"pendingTransactionMultiple": {
|
||||
"message": "You have ($1) pending transactions."
|
||||
},
|
||||
"pendingTransactionSingle": {
|
||||
"message": "You have (1) pending transaction.",
|
||||
"description": "$1 is count of pending transactions"
|
||||
},
|
||||
"permissionCheckedIconDescription": {
|
||||
"message": "You have approved this permission"
|
||||
},
|
||||
@ -2020,6 +2025,9 @@
|
||||
"privateNetwork": {
|
||||
"message": "Private Network"
|
||||
},
|
||||
"proceedWithTransaction": {
|
||||
"message": "I want to proceed anyway"
|
||||
},
|
||||
"proposedApprovalLimit": {
|
||||
"message": "Proposed Approval Limit"
|
||||
},
|
||||
@ -2407,6 +2415,9 @@
|
||||
"simulationErrorMessage": {
|
||||
"message": "This transaction is expected to fail. Trying to execute it is expected to be expensive but fail, and is not recommended."
|
||||
},
|
||||
"simulationErrorMessageV2": {
|
||||
"message": "We were not able to estimate gas. There might be an error in the contract and this transaction may fail."
|
||||
},
|
||||
"skip": {
|
||||
"message": "Skip"
|
||||
},
|
||||
|
@ -36,10 +36,10 @@ export default class ConfirmPageContainerContent extends Component {
|
||||
onCancel: PropTypes.func,
|
||||
cancelText: PropTypes.string,
|
||||
onSubmit: PropTypes.func,
|
||||
onConfirmAnyways: PropTypes.func,
|
||||
setUserAcknowledgedGasMissing: PropTypes.func,
|
||||
submitText: PropTypes.string,
|
||||
disabled: PropTypes.bool,
|
||||
hideConfirmAnyways: PropTypes.bool,
|
||||
hideUserAcknowledgedGasMissing: PropTypes.bool,
|
||||
unapprovedTxCount: PropTypes.number,
|
||||
rejectNText: PropTypes.string,
|
||||
hideTitle: PropTypes.boolean,
|
||||
@ -99,15 +99,15 @@ export default class ConfirmPageContainerContent extends Component {
|
||||
origin,
|
||||
ethGasPriceWarning,
|
||||
hideTitle,
|
||||
onConfirmAnyways,
|
||||
hideConfirmAnyways,
|
||||
setUserAcknowledgedGasMissing,
|
||||
hideUserAcknowledgedGasMissing,
|
||||
} = this.props;
|
||||
|
||||
const primaryAction = hideConfirmAnyways
|
||||
const primaryAction = hideUserAcknowledgedGasMissing
|
||||
? null
|
||||
: {
|
||||
label: this.context.t('tryAnywayOption'),
|
||||
onClick: onConfirmAnyways,
|
||||
onClick: setUserAcknowledgedGasMissing,
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -22,7 +22,7 @@ describe('Confirm Page Container Content', () => {
|
||||
const mockOnCancel = jest.fn();
|
||||
const mockOnCancelAll = jest.fn();
|
||||
const mockOnSubmit = jest.fn();
|
||||
const mockOnConfirmAnyways = jest.fn();
|
||||
const mockSetUserAcknowledgedGasMissing = jest.fn();
|
||||
props = {
|
||||
action: ' Withdraw Stake',
|
||||
errorMessage: null,
|
||||
@ -32,7 +32,7 @@ describe('Confirm Page Container Content', () => {
|
||||
onCancel: mockOnCancel,
|
||||
cancelText: 'Reject',
|
||||
onSubmit: mockOnSubmit,
|
||||
onConfirmAnyways: mockOnConfirmAnyways,
|
||||
setUserAcknowledgedGasMissing: mockSetUserAcknowledgedGasMissing,
|
||||
submitText: 'Confirm',
|
||||
disabled: true,
|
||||
origin: 'http://localhost:4200',
|
||||
@ -63,7 +63,7 @@ describe('Confirm Page Container Content', () => {
|
||||
|
||||
const iWillTryButton = getByText('I will try anyway');
|
||||
fireEvent.click(iWillTryButton);
|
||||
expect(props.onConfirmAnyways).toHaveBeenCalledTimes(1);
|
||||
expect(props.setUserAcknowledgedGasMissing).toHaveBeenCalledTimes(1);
|
||||
|
||||
const cancelButton = getByText('Reject');
|
||||
fireEvent.click(cancelButton);
|
||||
|
@ -1,5 +1,5 @@
|
||||
.edit-gas-fee-popover {
|
||||
height: 540px;
|
||||
height: 500px;
|
||||
|
||||
&__wrapper {
|
||||
border-top: 1px solid $ui-grey;
|
||||
|
@ -1,6 +1,5 @@
|
||||
import React from 'react';
|
||||
|
||||
import { toBigNumber } from '../../../../../shared/modules/conversion.utils';
|
||||
import { COLORS } from '../../../../helpers/constants/design-system';
|
||||
import { useGasFeeContext } from '../../../../contexts/gasFee';
|
||||
import I18nValue from '../../../ui/i18n-value';
|
||||
@ -10,13 +9,6 @@ import StatusSlider from './status-slider';
|
||||
|
||||
const NetworkStatus = () => {
|
||||
const { gasFeeEstimates } = useGasFeeContext();
|
||||
let estBaseFee = null;
|
||||
if (gasFeeEstimates?.estimatedBaseFee) {
|
||||
// estimatedBaseFee is not likely to be below 1, value .01 is used as test networks sometimes
|
||||
// show have small values for it and more decimal places may cause UI to look broken.
|
||||
estBaseFee = toBigNumber.dec(gasFeeEstimates?.estimatedBaseFee);
|
||||
estBaseFee = estBaseFee.lessThan(0.01) ? 0.01 : estBaseFee.toFixed(2);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="network-status">
|
||||
@ -32,7 +24,8 @@ const NetworkStatus = () => {
|
||||
<div className="network-status__info">
|
||||
<div className="network-status__info__field">
|
||||
<span className="network-status__info__field-data">
|
||||
{estBaseFee !== null && `${estBaseFee} GWEI`}
|
||||
{gasFeeEstimates?.estimatedBaseFee &&
|
||||
`${gasFeeEstimates?.estimatedBaseFee} GWEI`}
|
||||
</span>
|
||||
<span className="network-status__info__field-label">Base fee</span>
|
||||
</div>
|
||||
|
@ -57,21 +57,10 @@ describe('NetworkStatus', () => {
|
||||
expect(screen.queryByText('Priority fee')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should renders current base fee value rounded to 2 decimal places', () => {
|
||||
it('should renders current base fee value', () => {
|
||||
renderComponent();
|
||||
expect(
|
||||
screen.queryByText(
|
||||
`${parseFloat(MOCK_FEE_ESTIMATE.estimatedBaseFee).toFixed(2)} GWEI`,
|
||||
),
|
||||
screen.queryByText(`${MOCK_FEE_ESTIMATE.estimatedBaseFee} GWEI`),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should .01 as estimates base fee if estimated base fee is < .01', () => {
|
||||
renderComponent({
|
||||
gasFeeEstimates: {
|
||||
estimatedBaseFee: '0.0012',
|
||||
},
|
||||
});
|
||||
expect(screen.queryByText('0.01 GWEI')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
@ -36,7 +36,7 @@ export default function TransactionDetailItem({
|
||||
color={COLORS.BLACK}
|
||||
fontWeight={FONT_WEIGHT.BOLD}
|
||||
variant={TYPOGRAPHY.H6}
|
||||
margin={[1, 1]}
|
||||
margin={[1, 0, 1, 1]}
|
||||
>
|
||||
{detailTotal}
|
||||
</Typography>
|
||||
|
@ -17,7 +17,6 @@
|
||||
}
|
||||
|
||||
&-edit-V2 {
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: flex-end;
|
||||
@ -66,4 +65,8 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-rows {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
@ -11,13 +11,18 @@ import { COLORS } from '../../../helpers/constants/design-system';
|
||||
import { PRIORITY_LEVEL_ICON_MAP } from '../../../helpers/constants/gas';
|
||||
import { useI18nContext } from '../../../hooks/useI18nContext';
|
||||
|
||||
export default function TransactionDetail({ rows = [], onEdit }) {
|
||||
export default function TransactionDetail({
|
||||
rows = [],
|
||||
onEdit,
|
||||
userAcknowledgedGasMissing,
|
||||
}) {
|
||||
// eslint-disable-next-line prefer-destructuring
|
||||
const EIP_1559_V2 = process.env.EIP_1559_V2;
|
||||
|
||||
const t = useI18nContext();
|
||||
const {
|
||||
gasLimit,
|
||||
hasSimulationError,
|
||||
estimateUsed,
|
||||
maxFeePerGas,
|
||||
maxPriorityFeePerGas,
|
||||
@ -26,6 +31,9 @@ export default function TransactionDetail({ rows = [], onEdit }) {
|
||||
const { openModal } = useTransactionModalContext();
|
||||
|
||||
if (EIP_1559_V2 && estimateUsed) {
|
||||
const editEnabled = !hasSimulationError || userAcknowledgedGasMissing;
|
||||
if (!editEnabled) return null;
|
||||
|
||||
return (
|
||||
<div className="transaction-detail">
|
||||
<div className="transaction-detail-edit-V2">
|
||||
@ -88,4 +96,5 @@ export default function TransactionDetail({ rows = [], onEdit }) {
|
||||
TransactionDetail.propTypes = {
|
||||
rows: PropTypes.arrayOf(TransactionDetailItem).isRequired,
|
||||
onEdit: PropTypes.func,
|
||||
userAcknowledgedGasMissing: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
@ -16,7 +16,7 @@ jest.mock('../../../store/actions', () => ({
|
||||
addPollingTokenToAppState: jest.fn(),
|
||||
}));
|
||||
|
||||
const render = (props) => {
|
||||
const render = ({ componentProps, contextProps } = {}) => {
|
||||
const store = configureStore({
|
||||
metamask: {
|
||||
nativeCurrency: ETH,
|
||||
@ -37,13 +37,13 @@ const render = (props) => {
|
||||
});
|
||||
|
||||
return renderWithProvider(
|
||||
<GasFeeContextProvider {...props}>
|
||||
<GasFeeContextProvider {...contextProps}>
|
||||
<TransactionDetail
|
||||
onEdit={() => {
|
||||
console.log('on edit');
|
||||
}}
|
||||
rows={[]}
|
||||
{...props}
|
||||
{...componentProps}
|
||||
/>
|
||||
</GasFeeContextProvider>,
|
||||
store,
|
||||
@ -54,41 +54,79 @@ describe('TransactionDetail', () => {
|
||||
beforeEach(() => {
|
||||
process.env.EIP_1559_V2 = true;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.env.EIP_1559_V2 = false;
|
||||
});
|
||||
|
||||
it('should render edit link with text low if low gas estimates are selected', () => {
|
||||
render({ transaction: { userFeeLevel: 'low' } });
|
||||
render({ contextProps: { transaction: { userFeeLevel: 'low' } } });
|
||||
expect(screen.queryByText('🐢')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Low')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render edit link with text markey if medium gas estimates are selected', () => {
|
||||
render({ transaction: { userFeeLevel: 'medium' } });
|
||||
render({ contextProps: { transaction: { userFeeLevel: 'medium' } } });
|
||||
expect(screen.queryByText('🦊')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Market')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render edit link with text agressive if high gas estimates are selected', () => {
|
||||
render({ transaction: { userFeeLevel: 'high' } });
|
||||
render({ contextProps: { transaction: { userFeeLevel: 'high' } } });
|
||||
expect(screen.queryByText('🦍')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Aggressive')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render edit link with text Site suggested if site suggested estimated are used', () => {
|
||||
render({
|
||||
transaction: {
|
||||
dappSuggestedGasFees: { maxFeePerGas: 1, maxPriorityFeePerGas: 1 },
|
||||
txParams: { maxFeePerGas: 1, maxPriorityFeePerGas: 1 },
|
||||
contextProps: {
|
||||
transaction: {
|
||||
dappSuggestedGasFees: { maxFeePerGas: 1, maxPriorityFeePerGas: 1 },
|
||||
txParams: { maxFeePerGas: 1, maxPriorityFeePerGas: 1 },
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(screen.queryByText('🌐')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Site suggested')).toBeInTheDocument();
|
||||
expect(document.getElementsByClassName('info-tooltip')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should render edit link with text advance if custom gas estimates are used', () => {
|
||||
render({
|
||||
defaultEstimateToUse: 'custom',
|
||||
contextProps: {
|
||||
defaultEstimateToUse: 'custom',
|
||||
},
|
||||
});
|
||||
expect(screen.queryByText('⚙')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Advanced')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Edit')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not render edit link if transaction has simulation error and prop userAcknowledgedGasMissing is false', () => {
|
||||
render({
|
||||
contextProps: {
|
||||
transaction: {
|
||||
simulationFails: true,
|
||||
userFeeLevel: 'low',
|
||||
},
|
||||
},
|
||||
componentProps: { userAcknowledgedGasMissing: false },
|
||||
});
|
||||
expect(screen.queryByRole('button')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Low')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render edit link if userAcknowledgedGasMissing is true even if transaction has simulation error', () => {
|
||||
render({
|
||||
contextProps: {
|
||||
transaction: {
|
||||
simulationFails: true,
|
||||
userFeeLevel: 'low',
|
||||
},
|
||||
},
|
||||
componentProps: { userAcknowledgedGasMissing: true },
|
||||
});
|
||||
expect(screen.queryByRole('button')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Low')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
@ -19,6 +19,7 @@ const typeHash = {
|
||||
export default function ActionableMessage({
|
||||
message = '',
|
||||
primaryAction = null,
|
||||
primaryActionV2 = null,
|
||||
secondaryAction = null,
|
||||
className = '',
|
||||
infoTooltipText = '',
|
||||
@ -50,6 +51,14 @@ export default function ActionableMessage({
|
||||
/>
|
||||
)}
|
||||
<div className="actionable-message__message">{message}</div>
|
||||
{primaryActionV2 && (
|
||||
<button
|
||||
className="actionable-message__action-v2"
|
||||
onClick={primaryActionV2.onClick}
|
||||
>
|
||||
{primaryActionV2.label}
|
||||
</button>
|
||||
)}
|
||||
{(primaryAction || secondaryAction) && (
|
||||
<div
|
||||
className={classnames('actionable-message__actions', {
|
||||
@ -61,6 +70,7 @@ export default function ActionableMessage({
|
||||
className={classnames(
|
||||
'actionable-message__action',
|
||||
'actionable-message__action--primary',
|
||||
`actionable-message__action-${type}`,
|
||||
{
|
||||
'actionable-message__action--rounded': roundedButtons,
|
||||
},
|
||||
@ -75,6 +85,7 @@ export default function ActionableMessage({
|
||||
className={classnames(
|
||||
'actionable-message__action',
|
||||
'actionable-message__action--secondary',
|
||||
`actionable-message__action-${type}`,
|
||||
{
|
||||
'actionable-message__action--rounded': roundedButtons,
|
||||
},
|
||||
@ -96,6 +107,10 @@ ActionableMessage.propTypes = {
|
||||
label: PropTypes.string,
|
||||
onClick: PropTypes.func,
|
||||
}),
|
||||
primaryActionV2: PropTypes.shape({
|
||||
label: PropTypes.string,
|
||||
onClick: PropTypes.func,
|
||||
}),
|
||||
secondaryAction: PropTypes.shape({
|
||||
label: PropTypes.string,
|
||||
onClick: PropTypes.func,
|
||||
|
@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import { fireEvent } from '@testing-library/react';
|
||||
|
||||
import { renderWithProvider } from '../../../../test/jest';
|
||||
import ActionableMessage from '.';
|
||||
@ -19,4 +20,28 @@ describe('ActionableMessage', () => {
|
||||
expect(getByText(props.message)).toBeInTheDocument();
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders button for primaryActionV2 prop', () => {
|
||||
const props = createProps();
|
||||
const { getByRole } = renderWithProvider(
|
||||
<ActionableMessage
|
||||
{...props}
|
||||
primaryActionV2={{ label: 'primary-action-v2' }}
|
||||
/>,
|
||||
);
|
||||
expect(getByRole('button')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders primaryActionV2.onClick is callen when primaryActionV2 button is clicked', () => {
|
||||
const props = createProps();
|
||||
const onClick = jest.fn();
|
||||
const { getByRole } = renderWithProvider(
|
||||
<ActionableMessage
|
||||
{...props}
|
||||
primaryActionV2={{ label: 'primary-action-v2', onClick }}
|
||||
/>,
|
||||
);
|
||||
fireEvent.click(getByRole('button'));
|
||||
expect(onClick).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
@ -54,6 +54,21 @@
|
||||
&--rounded {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
&-danger {
|
||||
background: $Red-500;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
&__action-v2 {
|
||||
color: $primary-1;
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 12px;
|
||||
align-self: baseline;
|
||||
padding: 0;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
&__info-tooltip-wrapper {
|
||||
@ -86,11 +101,6 @@
|
||||
color: $Black-100;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
button {
|
||||
background: $Red-500;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
&--info {
|
||||
|
@ -262,5 +262,6 @@ export function useGasFeeErrors({
|
||||
gasWarnings,
|
||||
balanceError,
|
||||
estimatesUnavailableWarning,
|
||||
hasSimulationError: Boolean(transaction?.simulationFails),
|
||||
};
|
||||
}
|
||||
|
@ -280,6 +280,21 @@ describe('useGasFeeErrors', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Simulation Error', () => {
|
||||
it('is false if transaction has falsy values for simulationFails', () => {
|
||||
configureEIP1559();
|
||||
const { result } = renderUseGasFeeErrorsHook();
|
||||
expect(result.current.hasSimulationError).toBe(false);
|
||||
});
|
||||
it('is true if transaction.simulationFails is true', () => {
|
||||
configureEIP1559();
|
||||
const { result } = renderUseGasFeeErrorsHook({
|
||||
transaction: { simulationFails: true },
|
||||
});
|
||||
expect(result.current.hasSimulationError).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('estimatesUnavailableWarning', () => {
|
||||
it('is false if supportsEIP1559 and gasEstimateType is fee-market', () => {
|
||||
configureEIP1559();
|
||||
|
@ -186,6 +186,7 @@ export function useGasFeeInputs(
|
||||
gasErrors,
|
||||
gasWarnings,
|
||||
hasGasErrors,
|
||||
hasSimulationError,
|
||||
} = useGasFeeErrors({
|
||||
gasEstimateType,
|
||||
gasFeeEstimates,
|
||||
@ -301,6 +302,7 @@ export function useGasFeeInputs(
|
||||
gasErrors,
|
||||
gasWarnings,
|
||||
hasGasErrors,
|
||||
hasSimulationError,
|
||||
supportsEIP1559,
|
||||
updateTransactionUsingGasFeeEstimates,
|
||||
};
|
||||
|
@ -145,7 +145,7 @@ export default class ConfirmTransactionBase extends Component {
|
||||
submitWarning: '',
|
||||
ethGasPriceWarning: '',
|
||||
editingGas: false,
|
||||
confirmAnyways: false,
|
||||
userAcknowledgedGasMissing: false,
|
||||
};
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
@ -290,8 +290,8 @@ export default class ConfirmTransactionBase extends Component {
|
||||
this.setState({ editingGas: false });
|
||||
}
|
||||
|
||||
handleConfirmAnyways() {
|
||||
this.setState({ confirmAnyways: true });
|
||||
setUserAcknowledgedGasMissing() {
|
||||
this.setState({ userAcknowledgedGasMissing: true });
|
||||
}
|
||||
|
||||
renderDetails() {
|
||||
@ -318,15 +318,16 @@ export default class ConfirmTransactionBase extends Component {
|
||||
nativeCurrency,
|
||||
} = this.props;
|
||||
const { t } = this.context;
|
||||
const { userAcknowledgedGasMissing } = this.state;
|
||||
|
||||
const { valid } = this.getErrorKey();
|
||||
const isDisabled = () => {
|
||||
return this.state.confirmAnyways ? false : !valid;
|
||||
return userAcknowledgedGasMissing ? false : !valid;
|
||||
};
|
||||
|
||||
const hasSimulationError = Boolean(txData.simulationFails);
|
||||
const renderSimulationFailureWarning =
|
||||
hasSimulationError && !this.state.confirmAnyways;
|
||||
hasSimulationError && !userAcknowledgedGasMissing;
|
||||
|
||||
const renderTotalMaxAmount = () => {
|
||||
if (
|
||||
@ -432,6 +433,7 @@ export default class ConfirmTransactionBase extends Component {
|
||||
useNativeCurrencyAsPrimaryCurrency={
|
||||
useNativeCurrencyAsPrimaryCurrency
|
||||
}
|
||||
userAcknowledgedGasMissing={userAcknowledgedGasMissing}
|
||||
/>
|
||||
) : (
|
||||
<TransactionDetailItem
|
||||
@ -559,7 +561,7 @@ export default class ConfirmTransactionBase extends Component {
|
||||
type="danger"
|
||||
primaryAction={{
|
||||
label: this.context.t('tryAnywayOption'),
|
||||
onClick: () => this.handleConfirmAnyways(),
|
||||
onClick: () => this.setUserAcknowledgedGasMissing(),
|
||||
}}
|
||||
message={this.context.t('simulationErrorMessage')}
|
||||
roundedButtons
|
||||
@ -569,9 +571,17 @@ export default class ConfirmTransactionBase extends Component {
|
||||
|
||||
return (
|
||||
<div className="confirm-page-container-content__details">
|
||||
{EIP_1559_V2 && <TransactionAlerts />}
|
||||
{EIP_1559_V2 && (
|
||||
<TransactionAlerts
|
||||
setUserAcknowledgedGasMissing={() =>
|
||||
this.setUserAcknowledgedGasMissing()
|
||||
}
|
||||
userAcknowledgedGasMissing={userAcknowledgedGasMissing}
|
||||
/>
|
||||
)}
|
||||
<TransactionDetail
|
||||
disabled={isDisabled()}
|
||||
userAcknowledgedGasMissing={userAcknowledgedGasMissing}
|
||||
onEdit={
|
||||
renderSimulationFailureWarning ? null : () => this.handleEditGas()
|
||||
}
|
||||
@ -946,14 +956,14 @@ export default class ConfirmTransactionBase extends Component {
|
||||
submitWarning,
|
||||
ethGasPriceWarning,
|
||||
editingGas,
|
||||
confirmAnyways,
|
||||
userAcknowledgedGasMissing,
|
||||
} = this.state;
|
||||
|
||||
const { name } = methodData;
|
||||
const { valid, errorKey } = this.getErrorKey();
|
||||
const hasSimulationError = Boolean(txData.simulationFails);
|
||||
const renderSimulationFailureWarning =
|
||||
hasSimulationError && !confirmAnyways;
|
||||
hasSimulationError && !userAcknowledgedGasMissing;
|
||||
const {
|
||||
totalTx,
|
||||
positionOfCurrentTx,
|
||||
@ -967,7 +977,7 @@ export default class ConfirmTransactionBase extends Component {
|
||||
} = this.getNavigateTxData();
|
||||
|
||||
const isDisabled = () => {
|
||||
return confirmAnyways ? false : !valid;
|
||||
return userAcknowledgedGasMissing ? false : !valid;
|
||||
};
|
||||
|
||||
let functionType = getMethodName(name);
|
||||
@ -1017,7 +1027,7 @@ export default class ConfirmTransactionBase extends Component {
|
||||
lastTx={lastTx}
|
||||
ofText={ofText}
|
||||
requestsWaitingText={requestsWaitingText}
|
||||
hideConfirmAnyways={!isDisabled()}
|
||||
hideUserAcknowledgedGasMissing={!isDisabled()}
|
||||
disabled={
|
||||
renderSimulationFailureWarning ||
|
||||
!valid ||
|
||||
@ -1029,7 +1039,7 @@ export default class ConfirmTransactionBase extends Component {
|
||||
onCancelAll={() => this.handleCancelAll()}
|
||||
onCancel={() => this.handleCancel()}
|
||||
onSubmit={() => this.handleSubmit()}
|
||||
onConfirmAnyways={() => this.handleConfirmAnyways()}
|
||||
setUserAcknowledgedGasMissing={this.setUserAcknowledgedGasMissing}
|
||||
hideSenderToRecipient={hideSenderToRecipient}
|
||||
origin={txData.origin}
|
||||
ethGasPriceWarning={ethGasPriceWarning}
|
||||
|
@ -26,11 +26,14 @@ const GasDetailsItem = ({
|
||||
isMainnet,
|
||||
maxFeePerGas,
|
||||
maxPriorityFeePerGas,
|
||||
userAcknowledgedGasMissing,
|
||||
txData,
|
||||
useNativeCurrencyAsPrimaryCurrency,
|
||||
}) => {
|
||||
const t = useI18nContext();
|
||||
const { estimateUsed } = useGasFeeContext();
|
||||
const { estimateUsed, hasSimulationError } = useGasFeeContext();
|
||||
|
||||
if (hasSimulationError && !userAcknowledgedGasMissing) return null;
|
||||
|
||||
return (
|
||||
<TransactionDetailItem
|
||||
@ -138,6 +141,7 @@ GasDetailsItem.propTypes = {
|
||||
isMainnet: PropTypes.bool,
|
||||
maxFeePerGas: PropTypes.string,
|
||||
maxPriorityFeePerGas: PropTypes.string,
|
||||
userAcknowledgedGasMissing: PropTypes.bool.isRequired,
|
||||
txData: PropTypes.object,
|
||||
useNativeCurrencyAsPrimaryCurrency: PropTypes.bool,
|
||||
};
|
||||
|
@ -17,7 +17,7 @@ jest.mock('../../../store/actions', () => ({
|
||||
getGasFeeTimeEstimate: jest.fn().mockImplementation(() => Promise.resolve()),
|
||||
}));
|
||||
|
||||
const render = (props) => {
|
||||
const render = ({ componentProps, contextProps } = {}) => {
|
||||
const store = configureStore({
|
||||
metamask: {
|
||||
nativeCurrency: ETH,
|
||||
@ -37,8 +37,12 @@ const render = (props) => {
|
||||
});
|
||||
|
||||
return renderWithProvider(
|
||||
<GasFeeContextProvider {...props}>
|
||||
<GasDetailsItem txData={{ txParams: {} }} {...props} />
|
||||
<GasFeeContextProvider {...contextProps}>
|
||||
<GasDetailsItem
|
||||
txData={{ txParams: {} }}
|
||||
userAcknowledgedGasMissing={false}
|
||||
{...componentProps}
|
||||
/>
|
||||
</GasFeeContextProvider>,
|
||||
store,
|
||||
);
|
||||
@ -56,16 +60,47 @@ describe('GasDetailsItem', () => {
|
||||
});
|
||||
|
||||
it('should show warning icon if estimates are high', async () => {
|
||||
render({ defaultEstimateToUse: 'high' });
|
||||
render({ contextProps: { defaultEstimateToUse: 'high' } });
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('⚠ Max fee:')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not show warning icon if estimates are not high', async () => {
|
||||
render({ defaultEstimateToUse: 'low' });
|
||||
render({ contextProps: { defaultEstimateToUse: 'low' } });
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Max fee:')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return null if there is simulationError and user has not acknowledged gasMissing warning', () => {
|
||||
const { container } = render({
|
||||
contextProps: {
|
||||
defaultEstimateToUse: 'low',
|
||||
transaction: { simulationFails: true },
|
||||
},
|
||||
});
|
||||
expect(container.innerHTML).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should not return null even if there is simulationError if user acknowledged gasMissing warning', async () => {
|
||||
render();
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Gas')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should should render gas fee details', async () => {
|
||||
render({
|
||||
componentProps: {
|
||||
hexMinimumTransactionFee: '0x1ca62a4f7800',
|
||||
hexMaximumTransactionFee: '0x290ee75e3d900',
|
||||
},
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTitle('0.0000315 ETH')).toBeInTheDocument();
|
||||
expect(screen.queryByText('ETH')).toBeInTheDocument();
|
||||
expect(screen.queryByTitle('0.0007223')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,26 +1,59 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { PRIORITY_LEVELS } from '../../../../shared/constants/gas';
|
||||
import { INSUFFICIENT_FUNDS_ERROR_KEY } from '../../../helpers/constants/error-keys';
|
||||
import { submittedPendingTransactionsSelector } from '../../../selectors/transactions';
|
||||
import { useGasFeeContext } from '../../../contexts/gasFee';
|
||||
import { useI18nContext } from '../../../hooks/useI18nContext';
|
||||
import ActionableMessage from '../../../components/ui/actionable-message/actionable-message';
|
||||
import ErrorMessage from '../../../components/ui/error-message';
|
||||
import I18nValue from '../../../components/ui/i18n-value';
|
||||
import Typography from '../../../components/ui/typography';
|
||||
|
||||
const TransactionAlerts = () => {
|
||||
const { balanceError, estimateUsed } = useGasFeeContext();
|
||||
const TransactionAlerts = ({
|
||||
userAcknowledgedGasMissing,
|
||||
setUserAcknowledgedGasMissing,
|
||||
}) => {
|
||||
const { balanceError, estimateUsed, hasSimulationError } = useGasFeeContext();
|
||||
const pendingTransactions = useSelector(submittedPendingTransactionsSelector);
|
||||
const t = useI18nContext();
|
||||
|
||||
return (
|
||||
<div className="transaction-alerts">
|
||||
{hasSimulationError && (
|
||||
<ActionableMessage
|
||||
message={<I18nValue messageKey="simulationErrorMessageV2" />}
|
||||
useIcon
|
||||
iconFillColor="#d73a49"
|
||||
type="danger"
|
||||
primaryActionV2={
|
||||
userAcknowledgedGasMissing === true
|
||||
? undefined
|
||||
: {
|
||||
label: t('proceedWithTransaction'),
|
||||
onClick: setUserAcknowledgedGasMissing,
|
||||
}
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{pendingTransactions?.length > 0 && (
|
||||
<ActionableMessage
|
||||
message={
|
||||
<div className="transaction-alerts__pending-transactions">
|
||||
<Typography
|
||||
className="transaction-alerts__pending-transactions"
|
||||
align="left"
|
||||
fontSize="12px"
|
||||
margin={[0, 0]}
|
||||
>
|
||||
<strong>
|
||||
<I18nValue
|
||||
messageKey="pendingTransaction"
|
||||
messageKey={
|
||||
pendingTransactions?.length === 1
|
||||
? 'pendingTransactionSingle'
|
||||
: 'pendingTransactionMultiple'
|
||||
}
|
||||
options={[pendingTransactions?.length]}
|
||||
/>
|
||||
</strong>{' '}
|
||||
@ -38,36 +71,33 @@ const TransactionAlerts = () => {
|
||||
</a>,
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</Typography>
|
||||
}
|
||||
useIcon
|
||||
iconFillColor="#f8c000"
|
||||
type="warning"
|
||||
/>
|
||||
)}
|
||||
{balanceError && (
|
||||
<>
|
||||
{pendingTransactions?.length > 0 && (
|
||||
<div className="transaction-alerts--separator" />
|
||||
)}
|
||||
<ErrorMessage errorKey={INSUFFICIENT_FUNDS_ERROR_KEY} />
|
||||
</>
|
||||
)}
|
||||
{estimateUsed === 'low' && (
|
||||
<>
|
||||
{balanceError && (
|
||||
<div className="transaction-alerts-message--separator" />
|
||||
)}
|
||||
<ActionableMessage
|
||||
message={<I18nValue messageKey="lowPriorityMessage" />}
|
||||
useIcon
|
||||
iconFillColor="#f8c000"
|
||||
type="warning"
|
||||
/>
|
||||
</>
|
||||
{balanceError && <ErrorMessage errorKey={INSUFFICIENT_FUNDS_ERROR_KEY} />}
|
||||
{estimateUsed === PRIORITY_LEVELS.LOW && (
|
||||
<ActionableMessage
|
||||
message={
|
||||
<Typography align="left" fontSize="12px" margin={[0, 0]}>
|
||||
<I18nValue messageKey="lowPriorityMessage" />
|
||||
</Typography>
|
||||
}
|
||||
useIcon
|
||||
iconFillColor="#f8c000"
|
||||
type="warning"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
TransactionAlerts.propTypes = {
|
||||
userAcknowledgedGasMissing: PropTypes.bool,
|
||||
setUserAcknowledgedGasMissing: PropTypes.func,
|
||||
};
|
||||
|
||||
export default TransactionAlerts;
|
||||
|
@ -1,7 +1,11 @@
|
||||
.transaction-alerts {
|
||||
margin-top: 20px;
|
||||
text-align: left;
|
||||
|
||||
&--separator {
|
||||
& > *:first-of-type {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
& > *:not(:first-of-type) {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
|
@ -17,7 +17,7 @@ jest.mock('../../../store/actions', () => ({
|
||||
addPollingTokenToAppState: jest.fn(),
|
||||
}));
|
||||
|
||||
const render = ({ props, state }) => {
|
||||
const render = ({ props, state } = {}) => {
|
||||
const store = configureStore({
|
||||
metamask: {
|
||||
nativeCurrency: ETH,
|
||||
@ -95,7 +95,7 @@ describe('TransactionAlerts', () => {
|
||||
},
|
||||
});
|
||||
expect(
|
||||
screen.queryByText('You have (1) pending transaction(s).'),
|
||||
screen.queryByText('You have (1) pending transaction.'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user