1
0
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:
Jyoti Puri 2021-11-24 22:32:53 +05:30 committed by GitHub
parent fb27e170ac
commit 7609841902
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 295 additions and 101 deletions

View File

@ -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"
},

View File

@ -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 (

View File

@ -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);

View File

@ -1,5 +1,5 @@
.edit-gas-fee-popover {
height: 540px;
height: 500px;
&__wrapper {
border-top: 1px solid $ui-grey;

View File

@ -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>

View File

@ -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();
});
});

View File

@ -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>

View File

@ -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;
}
}

View File

@ -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,
};

View File

@ -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();
});
});

View File

@ -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,

View File

@ -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);
});
});

View File

@ -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 {

View File

@ -262,5 +262,6 @@ export function useGasFeeErrors({
gasWarnings,
balanceError,
estimatesUnavailableWarning,
hasSimulationError: Boolean(transaction?.simulationFails),
};
}

View File

@ -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();

View File

@ -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,
};

View File

@ -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}

View File

@ -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,
};

View File

@ -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();
});
});
});

View File

@ -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;

View File

@ -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;
}

View File

@ -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();
});
});