diff --git a/test/e2e/tests/confirmations/alerts/insufficient-funds.spec.ts b/test/e2e/tests/confirmations/alerts/insufficient-funds.spec.ts new file mode 100644 index 000000000000..e600ed920c9b --- /dev/null +++ b/test/e2e/tests/confirmations/alerts/insufficient-funds.spec.ts @@ -0,0 +1,85 @@ +import { strict as assert } from 'assert'; +import FixtureBuilder from '../../../fixture-builder'; +import { + PRIVATE_KEY, + convertETHToHexGwei, + withFixtures, + WINDOW_TITLES, +} from '../../../helpers'; +import { SMART_CONTRACTS } from '../../../seeder/smart-contracts'; +import { + TestSuiteArguments, + openDAppWithContract, +} from '../transactions/shared'; +import { Driver } from '../../../webdriver/driver'; + +describe('Alert for insufficient funds @no-mmi', function () { + it('Shows an alert when the user tries to send a transaction with insufficient funds', async function () { + const nftSmartContract = SMART_CONTRACTS.NFTS; + const ganacheOptions = { + accounts: [ + { + secretKey: PRIVATE_KEY, + balance: convertETHToHexGwei(0.0053), // Low balance only to create the contract and then trigger the alert for insufficient funds + }, + ], + }; + await withFixtures( + { + dapp: true, + fixtures: new FixtureBuilder() + .withPermissionControllerConnectedToTestDapp() + .withPreferencesController({ + preferences: { + redesignedConfirmationsEnabled: true, + isRedesignedConfirmationsDeveloperEnabled: true, + }, + }) + .build(), + ganacheOptions, + smartContract: nftSmartContract, + title: this.test?.fullTitle(), + }, + async ({ driver, contractRegistry }: TestSuiteArguments) => { + await openDAppWithContract(driver, contractRegistry, nftSmartContract); + + await mintNft(driver); + + await verifyAlertForInsufficientBalance(driver); + + await verifyConfirmationIsDisabled(driver); + }, + ); + }); +}); + +async function verifyConfirmationIsDisabled(driver: Driver) { + const confirmButton = await driver.findElement( + '[data-testid="confirm-footer-button"]', + ); + assert.equal(await confirmButton.isEnabled(), false); +} + +async function verifyAlertForInsufficientBalance(driver: Driver) { + const alert = await driver.findElement('[data-testid="inline-alert"]'); + assert.equal(await alert.getText(), 'Alert'); + await driver.clickElementSafe('.confirm-scroll-to-bottom__button'); + await driver.clickElement('[data-testid="inline-alert"]'); + + const alertDescription = await driver.findElement( + '[data-testid="alert-modal__selected-alert"]', + ); + const alertDescriptionText = await alertDescription.getText(); + assert.equal( + alertDescriptionText, + 'You do not have enough ETH in your account to pay for transaction fees.', + ); +} + +async function mintNft(driver: Driver) { + await driver.switchToWindowWithTitle(WINDOW_TITLES.TestDApp); + await driver.clickElement(`#mintButton`); + + await driver.waitUntilXWindowHandles(3); + await driver.switchToWindowWithTitle(WINDOW_TITLES.Dialog); +} diff --git a/ui/components/app/alert-system/alert-modal/alert-modal.tsx b/ui/components/app/alert-system/alert-modal/alert-modal.tsx index 2d4ec4bb112c..fa31cb018091 100644 --- a/ui/components/app/alert-system/alert-modal/alert-modal.tsx +++ b/ui/components/app/alert-system/alert-modal/alert-modal.tsx @@ -166,7 +166,12 @@ function AlertDetails({ > {customDetails ?? ( - {selectedAlert.message} + + {selectedAlert.message} + {selectedAlert.alertDetails?.length ? ( {t('alertModalDetails')} diff --git a/ui/pages/confirmations/components/confirm/footer/footer.test.tsx b/ui/pages/confirmations/components/confirm/footer/footer.test.tsx index 9fd033fb90ff..5fb6b7e5ac76 100644 --- a/ui/pages/confirmations/components/confirm/footer/footer.test.tsx +++ b/ui/pages/confirmations/components/confirm/footer/footer.test.tsx @@ -12,6 +12,8 @@ import * as MMIConfirmations from '../../../../../hooks/useMMIConfirmations'; import * as Actions from '../../../../../store/actions'; import configureStore from '../../../../../store/store'; import { Severity } from '../../../../../helpers/constants/design-system'; + +import { Alert } from '../../../../../ducks/confirm-alerts/confirm-alerts'; import Footer from './footer'; jest.mock('react-redux', () => ({ @@ -184,7 +186,8 @@ describe('ConfirmFooter', () => { const OWNER_ID_MOCK = '123'; const KEY_ALERT_KEY_MOCK = 'Key'; const ALERT_MESSAGE_MOCK = 'Alert 1'; - const alertsMock = [ + + const alertsMock: Alert[] = [ { key: KEY_ALERT_KEY_MOCK, field: KEY_ALERT_KEY_MOCK, @@ -213,12 +216,31 @@ describe('ConfirmFooter', () => { }; it('renders the review alerts button when there are unconfirmed alerts', () => { const { getByText } = render(stateWithAlertsMock); - expect(getByText('Confirm')).toBeInTheDocument(); + expect(getByText('Review alerts')).toBeInTheDocument(); }); - it('renders the confirm button when there are no unconfirmed alerts', () => { - const { getByText } = render(); - expect(getByText('Confirm')).toBeInTheDocument(); + it('renders the "review alerts" button disabled when there are blocking alerts', () => { + const stateWithMultipleDangerAlerts = { + ...stateWithAlertsMock, + confirmAlerts: { + alerts: { + [OWNER_ID_MOCK]: [ + alertsMock[0], + { + ...alertsMock[0], + key: 'From', + isBlocking: true, + }, + ], + }, + confirmed: { + [OWNER_ID_MOCK]: { [KEY_ALERT_KEY_MOCK]: false }, + }, + }, + }; + const { getByText } = render(stateWithMultipleDangerAlerts); + expect(getByText('Review alerts')).toBeInTheDocument(); + expect(getByText('Review alerts')).toBeDisabled(); }); it('sets the alert modal visible when the review alerts button is clicked', () => { @@ -226,5 +248,10 @@ describe('ConfirmFooter', () => { fireEvent.click(getByTestId('confirm-footer-button')); expect(getByTestId('confirm-alert-modal-submit-button')).toBeDefined(); }); + + it('renders the "confirm" button when there are no alerts', () => { + const { getByText } = render(); + expect(getByText('Confirm')).toBeInTheDocument(); + }); }); }); diff --git a/ui/pages/confirmations/components/confirm/footer/footer.tsx b/ui/pages/confirmations/components/confirm/footer/footer.tsx index a3d3e1bbc71d..7c36c31c164e 100644 --- a/ui/pages/confirmations/components/confirm/footer/footer.tsx +++ b/ui/pages/confirmations/components/confirm/footer/footer.tsx @@ -27,6 +27,30 @@ import { import { confirmSelector } from '../../../selectors'; import { REDESIGN_TRANSACTION_TYPES } from '../../../utils'; import { getConfirmationSender } from '../utils'; +import { MetaMetricsEventLocation } from '../../../../../../shared/constants/metametrics'; +import { Severity } from '../../../../../helpers/constants/design-system'; + +export type OnCancelHandler = ({ + location, +}: { + location: MetaMetricsEventLocation; +}) => void; + +function getButtonDisabledState( + hasUnconfirmedDangerAlerts: boolean, + hasBlockingAlerts: boolean, + disabled: boolean, +) { + if (hasBlockingAlerts) { + return true; + } + + if (hasUnconfirmedDangerAlerts) { + return false; + } + + return disabled; +} const ConfirmButton = ({ alertOwnerId = '', @@ -47,6 +71,10 @@ const ConfirmButton = ({ const { dangerAlerts, hasDangerAlerts, hasUnconfirmedDangerAlerts } = useAlerts(alertOwnerId); + const hasDangerBlockingAlerts = dangerAlerts.some( + (alert) => alert.severity === Severity.Danger && alert.isBlocking, + ); + const handleCloseConfirmModal = useCallback(() => { setConfirmModalVisible(false); }, []); @@ -70,12 +98,16 @@ const ConfirmButton = ({ block danger data-testid="confirm-footer-button" - disabled={hasUnconfirmedDangerAlerts ? false : disabled} + disabled={getButtonDisabledState( + hasUnconfirmedDangerAlerts, + hasDangerBlockingAlerts, + disabled, + )} onClick={handleOpenConfirmModal} size={ButtonSize.Lg} startIconName={IconName.Danger} > - {dangerAlerts?.length > 1 ? t('reviewAlerts') : t('confirm')} + {dangerAlerts?.length > 0 ? t('reviewAlerts') : t('confirm')} ) : (