diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json
index bac2c66e7234..14ba0f25b085 100644
--- a/app/_locales/en/messages.json
+++ b/app/_locales/en/messages.json
@@ -352,6 +352,9 @@
"alertModalDetails": {
"message": "Alert Details"
},
+ "alertModalReviewAllAlerts": {
+ "message": "Review all alerts"
+ },
"alertSettingsUnconnectedAccount": {
"message": "Browsing a website with an unconnected account selected"
},
@@ -818,6 +821,15 @@
"confirm": {
"message": "Confirm"
},
+ "confirmAlertModalAcknowledge": {
+ "message": "I have acknowledged the alerts and still want to proceed"
+ },
+ "confirmAlertModalDetails": {
+ "message": "If you sign in, a third party known for scams might take all your assets. Please review the alerts before you proceed."
+ },
+ "confirmAlertModalTitle": {
+ "message": "Your assets may be at risk"
+ },
"confirmConnectCustodianRedirect": {
"message": "We will redirect you to $1 upon clicking continue."
},
@@ -4160,6 +4172,9 @@
"revealTheSeedPhrase": {
"message": "Reveal seed phrase"
},
+ "reviewAlerts": {
+ "message": "Review alerts"
+ },
"revokeAllTokensTitle": {
"message": "Revoke permission to access and transfer all of your $1?",
"description": "$1 is the symbol of the token for which the user is revoking approval"
diff --git a/test/data/mock-state.json b/test/data/mock-state.json
index d4d6555504f8..5bad1276c21f 100644
--- a/test/data/mock-state.json
+++ b/test/data/mock-state.json
@@ -30,6 +30,10 @@
"customTokenAmount": "10"
},
"confirm": {},
+ "confirmAlerts": {
+ "alerts": [],
+ "confirmed": []
+ },
"confirmTransaction": {
"txData": {
"txParams": {
diff --git a/ui/components/app/confirmations/alerts/alert-modal/alert-modal.stories.tsx b/ui/components/app/confirmations/alerts/alert-modal/alert-modal.stories.tsx
index 180c1e7e2b5e..df79b51cbca0 100644
--- a/ui/components/app/confirmations/alerts/alert-modal/alert-modal.stories.tsx
+++ b/ui/components/app/confirmations/alerts/alert-modal/alert-modal.stories.tsx
@@ -1,10 +1,12 @@
import React from 'react';
import { AlertModal } from './alert-modal';
import { Severity } from '../../../../../helpers/constants/design-system';
-import { Meta } from '@storybook/react';
+import { Meta, StoryFn } from '@storybook/react';
import configureStore from '../../../../../store/store';
import { Provider } from 'react-redux';
import { Alert } from '../../../../../ducks/confirm-alerts/confirm-alerts';
+import { useArgs } from '@storybook/client-api';
+import { Box, Button } from '../../../../component-library';
const alertsMock: Alert[] = [
{ key: 'from', severity: Severity.Danger, message: 'Description of what may happen if this alert was ignored', reason: 'Reason for the alert 1', alertDetails: ['We found the contract Petname 0xEqT3b9773b1763efa556f55ccbeb20441962d82x to be malicious',
@@ -47,8 +49,27 @@ export default {
decorators: [(story) => {story()}],
} as Meta;
-export const DefaultStory = (args) => {
- return ;
+export const DefaultStory: StoryFn = (args) => {
+ const [{ isOpen }, updateArgs] = useArgs();
+ const handleOnClick = () => {
+ updateArgs({ isOpen: true });
+ };
+ const handleOnClose = () => {
+ updateArgs({ isOpen: false });
+ };
+ return (
+
+ {isOpen && (
+
+ )}
+
+
+ );
};
DefaultStory.storyName = 'Critical Alert';
diff --git a/ui/components/app/confirmations/alerts/alert-modal/alert-modal.tsx b/ui/components/app/confirmations/alerts/alert-modal/alert-modal.tsx
index 086eb7e3411b..bb03914c9642 100644
--- a/ui/components/app/confirmations/alerts/alert-modal/alert-modal.tsx
+++ b/ui/components/app/confirmations/alerts/alert-modal/alert-modal.tsx
@@ -22,6 +22,7 @@ import {
BlockSize,
BorderRadius,
Display,
+ FlexDirection,
IconColor,
Severity,
TextAlign,
@@ -33,22 +34,46 @@ import useAlerts from '../../../../../hooks/useAlerts';
import { Alert } from '../../../../../ducks/confirm-alerts/confirm-alerts';
export type AlertModalProps = {
- /** The unique key representing the specific alert field. */
+ /**
+ * The unique key representing the specific alert field.
+ */
alertKey: string;
+ /**
+ * The custom button component for acknowledging the alert.
+ */
+ customAcknowledgeButton?: React.ReactNode;
+ /**
+ * The custom checkbox component for acknowledging the alert.
+ */
+ customAcknowledgeCheckbox?: React.ReactNode;
+ /**
+ * The custom details component for the alert.
+ */
+ customAlertDetails?: React.ReactNode;
+ /**
+ * The custom title for the alert.
+ */
+ customAlertTitle?: string;
/**
* The start (left) content area of ModalHeader.
- * It override `startAccessory` of ModalHeaderDefault and by default no content is present.
+ * It overrides `startAccessory` of ModalHeaderDefault and by default no content is present.
*/
headerStartAccessory?: React.ReactNode;
- /** The owner ID of the relevant alert from the `confirmAlerts` reducer. */
+ /**
+ * The owner ID of the relevant alert from the `confirmAlerts` reducer.
+ */
ownerId: string;
- /** The function invoked when the user acknowledges the alert. */
+ /**
+ * The function invoked when the user acknowledges the alert.
+ */
onAcknowledgeClick: () => void;
- /** The function to be executed when the modal needs to be closed. */
+ /**
+ * The function to be executed when the modal needs to be closed.
+ */
onClose: () => void;
};
-function getSeverityStyle(severity: Severity) {
+export function getSeverityStyle(severity?: Severity) {
switch (severity) {
case Severity.Warning:
return {
@@ -68,7 +93,13 @@ function getSeverityStyle(severity: Severity) {
}
}
-function AlertHeader({ selectedAlert }: { selectedAlert: Alert }) {
+function AlertHeader({
+ selectedAlert,
+ customAlertTitle,
+}: {
+ selectedAlert: Alert;
+ customAlertTitle?: string;
+}) {
const t = useI18nContext();
const severityStyle = getSeverityStyle(selectedAlert.severity);
return (
@@ -95,58 +126,74 @@ function AlertHeader({ selectedAlert }: { selectedAlert: Alert }) {
marginTop={3}
marginBottom={4}
>
- {selectedAlert.reason ?? t('alert')}
+ {customAlertTitle ?? selectedAlert.reason ?? t('alert')}
>
);
}
-function AlertDetails({ selectedAlert }: { selectedAlert: Alert }) {
+function AlertDetails({
+ selectedAlert,
+ customAlertDetails,
+}: {
+ selectedAlert: Alert;
+ customAlertDetails?: React.ReactNode;
+}) {
const t = useI18nContext();
const severityStyle = getSeverityStyle(selectedAlert.severity);
return (
-
- {selectedAlert.message}
- {selectedAlert.alertDetails?.length ? (
-
- {t('alertModalDetails')}
-
- ) : null}
-
-
- {selectedAlert.alertDetails?.map((detail, index) => (
-
- {detail}
+ <>
+
+ {customAlertDetails ?? (
+
+ {selectedAlert.message}
+ {selectedAlert.alertDetails?.length ? (
+
+ {t('alertModalDetails')}
+
+ ) : null}
+
+ {selectedAlert.alertDetails?.map((detail, index) => (
+
+ {detail}
+
+ ))}
+
- ))}
+ )}
-
+ >
);
}
-function AcknowledgeCheckbox({
+export function AcknowledgeCheckboxBase({
selectedAlert,
- setAlertConfirmed,
+ onCheckboxClick,
isConfirmed,
+ label,
}: {
selectedAlert: Alert;
- setAlertConfirmed: (alertKey: string, isConfirmed: boolean) => void;
+ onCheckboxClick: () => void;
isConfirmed: boolean;
+ label?: string;
}) {
const t = useI18nContext();
const severityStyle = getSeverityStyle(selectedAlert.severity);
- const handleCheckboxClick = () => {
- return setAlertConfirmed(selectedAlert.key, !isConfirmed);
- };
return (
@@ -179,18 +226,16 @@ function AcknowledgeButton({
const t = useI18nContext();
return (
- <>
-
- >
+
);
}
@@ -200,6 +245,10 @@ export function AlertModal({
alertKey,
onClose,
headerStartAccessory,
+ customAlertTitle,
+ customAlertDetails,
+ customAcknowledgeCheckbox,
+ customAcknowledgeButton,
}: AlertModalProps) {
const { alerts, isAlertConfirmed, setAlertConfirmed } = useAlerts(ownerId);
@@ -214,6 +263,10 @@ export function AlertModal({
}
const isConfirmed = isAlertConfirmed(selectedAlert.key);
+ const handleCheckboxClick = useCallback(() => {
+ return setAlertConfirmed(selectedAlert.key, !isConfirmed);
+ }, [isConfirmed, selectedAlert.key]);
+
return (
@@ -225,20 +278,37 @@ export function AlertModal({
borderWidth={1}
display={headerStartAccessory ? Display.InlineFlex : Display.Block}
/>
-
+
-
-
+ {customAcknowledgeCheckbox ?? (
+
+ )}
-
+
+ {customAcknowledgeButton ?? (
+
+ )}
+
diff --git a/ui/components/app/confirmations/alerts/confirm-alert-modal/confirm-alert-modal.stories.tsx b/ui/components/app/confirmations/alerts/confirm-alert-modal/confirm-alert-modal.stories.tsx
new file mode 100644
index 000000000000..9e7723660a96
--- /dev/null
+++ b/ui/components/app/confirmations/alerts/confirm-alert-modal/confirm-alert-modal.stories.tsx
@@ -0,0 +1,115 @@
+import React from 'react';
+import { ConfirmAlertModal } from './confirm-alert-modal';
+import { Severity } from '../../../../../helpers/constants/design-system';
+import { Meta, StoryFn } from '@storybook/react';
+import configureStore from '../../../../../store/store';
+import { Provider } from 'react-redux';
+import { Alert } from '../../../../../ducks/confirm-alerts/confirm-alerts';
+import { Box, Button } from '../../../../component-library';
+import { useArgs } from '@storybook/client-api';
+
+const ALERTS_MOCK: Alert[] = [
+ {
+ key: 'from',
+ severity: Severity.Danger,
+ message: 'Description of what may happen if this alert was ignored',
+ reason: 'Reason for the alert 1',
+ alertDetails: [
+ 'We found the contract Petname 0xEqT3b9773b1763efa556f55ccbeb20441962d82x to be malicious',
+ 'Operator is an externally owned account (EOA) ',
+ 'Operator is untrusted according to previous activity',
+ ],
+ },
+ {
+ key: 'data',
+ severity: Severity.Warning,
+ message: 'Alert 2',
+ alertDetails: ['detail 1 warning', 'detail 2 warning'],
+ },
+ {
+ key: 'contract',
+ severity: Severity.Info,
+ message: 'Alert Info',
+ alertDetails: ['detail 1 info', 'detail info'],
+ },
+];
+const OWNER_ID_MOCK = '123';
+const storeMock = configureStore({
+ confirmAlerts: {
+ alerts: { [OWNER_ID_MOCK]: ALERTS_MOCK },
+ confirmed: {
+ [OWNER_ID_MOCK]: { from: false, data: false, contract: false },
+ },
+ },
+});
+
+export default {
+ title: 'Confirmations/Components/Alerts/ConfirmAlertModal',
+ component: ConfirmAlertModal,
+ argTypes: {
+ alertKey: {
+ control: 'text',
+ description: 'The unique key identifying the specific alert.',
+ },
+ onAcknowledgeClick: {
+ action: 'acknowledged',
+ description:
+ 'Callback function invoked when the acknowledge action is triggered.',
+ },
+ onClose: {
+ action: 'closed',
+ description:
+ 'Callback function invoked when the modal is requested to close.',
+ },
+ ownerId: {
+ control: 'text',
+ description:
+ 'The owner ID of the relevant alert from the `confirmAlerts` reducer.',
+ },
+ onAlertLinkClick: {
+ action: 'alertLinkClicked',
+ description: 'Callback function called when the alert link is clicked.',
+ },
+ onCancel: {
+ action: 'cancelled',
+ description:
+ 'Callback function called when the cancel button is clicked.',
+ },
+ onSubmit: {
+ action: 'submitted',
+ description:
+ 'Callback function called when the submit button is clicked.',
+ },
+ },
+ args: {
+ onAcknowledgeClick: () => {},
+ onClose: () => {},
+ ownerId: OWNER_ID_MOCK,
+ },
+ decorators: [(story) => {story()}],
+} as Meta;
+
+export const TemplateStory: StoryFn = (args) => {
+ const [{ isOpen }, updateArgs] = useArgs();
+ const handleOnClick = () => {
+ updateArgs({ isOpen: true });
+ };
+ const handleOnClose = () => {
+ updateArgs({ isOpen: false });
+ };
+ return (
+
+ {isOpen && (
+
+ )}
+
+
+ );
+};
+TemplateStory.storyName = 'Confirm Critical Alert Modal';
diff --git a/ui/components/app/confirmations/alerts/confirm-alert-modal/confirm-alert-modal.test.tsx b/ui/components/app/confirmations/alerts/confirm-alert-modal/confirm-alert-modal.test.tsx
new file mode 100644
index 000000000000..2bed8323b453
--- /dev/null
+++ b/ui/components/app/confirmations/alerts/confirm-alert-modal/confirm-alert-modal.test.tsx
@@ -0,0 +1,94 @@
+import React from 'react';
+import configureMockStore from 'redux-mock-store';
+import { fireEvent } from '@testing-library/react';
+import { Severity } from '../../../../../helpers/constants/design-system';
+import { renderWithProvider } from '../../../../../../test/lib/render-helpers';
+import {
+ ConfirmAlertModalProps,
+ ConfirmAlertModal,
+} from './confirm-alert-modal';
+
+describe('ConfirmAlertModal', () => {
+ const OWNER_ID_MOCK = '123';
+ const FROM_ALERT_KEY_MOCK = 'from';
+ const onCloseMock = jest.fn();
+ const onCancelMock = jest.fn();
+ const onSubmitMock = jest.fn();
+ const alertsMock = [
+ {
+ key: FROM_ALERT_KEY_MOCK,
+ severity: Severity.Warning,
+ message: 'Alert 1',
+ reason: 'Reason 1',
+ alertDetails: ['Detail 1', 'Detail 2'],
+ },
+ { key: 'data', severity: Severity.Danger, message: 'Alert 2' },
+ ];
+
+ const STATE_MOCK = {
+ confirmAlerts: {
+ alerts: { [OWNER_ID_MOCK]: alertsMock },
+ confirmed: {
+ [OWNER_ID_MOCK]: { [FROM_ALERT_KEY_MOCK]: false, data: false },
+ },
+ },
+ };
+ const mockStore = configureMockStore([])(STATE_MOCK);
+
+ const defaultProps: ConfirmAlertModalProps = {
+ ownerId: OWNER_ID_MOCK,
+ alertKey: FROM_ALERT_KEY_MOCK,
+ onClose: onCloseMock,
+ onCancel: onCancelMock,
+ onSubmit: onSubmitMock,
+ };
+
+ it('renders the confirm alert modal', () => {
+ const { getByText } = renderWithProvider(
+ ,
+ mockStore,
+ );
+
+ expect(getByText('Your assets may be at risk')).toBeInTheDocument();
+ });
+
+ it('disables submit button when confirm modal is not acknowledged', () => {
+ const { getByTestId } = renderWithProvider(
+ ,
+ mockStore,
+ );
+
+ expect(getByTestId('confirm-alert-modal-submit-button')).toBeDisabled();
+ });
+
+ it('calls onCancel when the button is clicked', () => {
+ const { getByTestId } = renderWithProvider(
+ ,
+ mockStore,
+ );
+
+ fireEvent.click(getByTestId('confirm-alert-modal-cancel-button'));
+ expect(onCancelMock).toHaveBeenCalledTimes(1);
+ });
+
+ it('calls onSubmit when the button is clicked', () => {
+ const { getByTestId } = renderWithProvider(
+ ,
+ mockStore,
+ );
+
+ fireEvent.click(getByTestId('alert-modal-acknowledge-checkbox'));
+ fireEvent.click(getByTestId('confirm-alert-modal-submit-button'));
+ expect(onSubmitMock).toHaveBeenCalledTimes(1);
+ });
+
+ it('calls open multiple alert modal when review alerts link is clicked', () => {
+ const { getByTestId } = renderWithProvider(
+ ,
+ mockStore,
+ );
+
+ fireEvent.click(getByTestId('confirm-alert-modal-review-all-alerts'));
+ expect(getByTestId('alert-modal-button')).toBeInTheDocument();
+ });
+});
diff --git a/ui/components/app/confirmations/alerts/confirm-alert-modal/confirm-alert-modal.tsx b/ui/components/app/confirmations/alerts/confirm-alert-modal/confirm-alert-modal.tsx
new file mode 100644
index 000000000000..51126238c429
--- /dev/null
+++ b/ui/components/app/confirmations/alerts/confirm-alert-modal/confirm-alert-modal.tsx
@@ -0,0 +1,186 @@
+import React, { useCallback, useState } from 'react';
+import {
+ Box,
+ Button,
+ ButtonLink,
+ ButtonLinkSize,
+ ButtonSize,
+ ButtonVariant,
+ Icon,
+ IconName,
+ IconSize,
+ Text,
+} from '../../../../component-library';
+import {
+ AlignItems,
+ TextAlign,
+ TextVariant,
+} from '../../../../../helpers/constants/design-system';
+import { useI18nContext } from '../../../../../hooks/useI18nContext';
+import useAlerts from '../../../../../hooks/useAlerts';
+import { AlertModal } from '../alert-modal';
+import { AcknowledgeCheckboxBase } from '../alert-modal/alert-modal';
+import { MultipleAlertModal } from '../multiple-alert-modal';
+
+export type ConfirmAlertModalProps = {
+ /** The unique key representing the specific alert field. */
+ alertKey: string;
+ /** Callback function that is called when the cancel button is clicked. */
+ onCancel: () => void;
+ /** The function to be executed when the modal needs to be closed. */
+ onClose: () => void;
+ /** Callback function that is called when the submit button is clicked. */
+ onSubmit: () => void;
+ /** The owner ID of the relevant alert from the `confirmAlerts` reducer. */
+ ownerId: string;
+};
+
+function ConfirmButtons({
+ onCancel,
+ onSubmit,
+ isConfirmed,
+}: {
+ onCancel: () => void;
+ onSubmit: () => void;
+ isConfirmed: boolean;
+}) {
+ const t = useI18nContext();
+ return (
+ <>
+
+
+ >
+ );
+}
+
+function ConfirmDetails({
+ onAlertLinkClick,
+}: {
+ onAlertLinkClick?: () => void;
+}) {
+ const t = useI18nContext();
+ return (
+ <>
+
+
+ {t('confirmAlertModalDetails')}
+
+
+
+ {t('alertModalReviewAllAlerts')}
+
+
+ >
+ );
+}
+
+export function ConfirmAlertModal({
+ alertKey,
+ onCancel,
+ onClose,
+ onSubmit,
+ ownerId,
+}: ConfirmAlertModalProps) {
+ const t = useI18nContext();
+ const { alerts, isAlertConfirmed } = useAlerts(ownerId);
+
+ const unconfirmedAlerts = alerts.filter(
+ (alert) => alert.field && !isAlertConfirmed(alert.key),
+ );
+ const selectedAlert = alerts.find((alert) => alert.key === alertKey);
+ const hasUnconfirmedAlerts = unconfirmedAlerts.length > 0;
+
+ const [confirmCheckbox, setConfirmCheckbox] = useState(false);
+ // if there are multiple alerts, show the multiple alert modal
+ const [multipleAlertModalVisible, setMultipleAlertModalVisible] =
+ useState(hasUnconfirmedAlerts);
+
+ const handleCloseMultipleAlertModal = useCallback(() => {
+ setMultipleAlertModalVisible(false);
+ }, []);
+
+ const handleOpenMultipleAlertModal = useCallback(() => {
+ setMultipleAlertModalVisible(true);
+ }, []);
+
+ const handleConfirmCheckbox = useCallback(() => {
+ setConfirmCheckbox(!confirmCheckbox);
+ }, [confirmCheckbox, selectedAlert]);
+
+ if (!selectedAlert) {
+ return null;
+ }
+
+ if (multipleAlertModalVisible) {
+ return (
+
+ );
+ }
+
+ return (
+
+ }
+ customAcknowledgeCheckbox={
+
+ }
+ customAcknowledgeButton={
+
+ }
+ />
+ );
+}
diff --git a/ui/components/app/confirmations/alerts/confirm-alert-modal/index.tsx b/ui/components/app/confirmations/alerts/confirm-alert-modal/index.tsx
new file mode 100644
index 000000000000..688d056d4c67
--- /dev/null
+++ b/ui/components/app/confirmations/alerts/confirm-alert-modal/index.tsx
@@ -0,0 +1 @@
+export { ConfirmAlertModal } from './confirm-alert-modal';
diff --git a/ui/components/app/confirmations/alerts/inline-alert/inline-alert.tsx b/ui/components/app/confirmations/alerts/inline-alert/inline-alert.tsx
index 28b41a0f7060..9da5372a8509 100644
--- a/ui/components/app/confirmations/alerts/inline-alert/inline-alert.tsx
+++ b/ui/components/app/confirmations/alerts/inline-alert/inline-alert.tsx
@@ -9,7 +9,6 @@ import {
} from '../../../../component-library';
import {
AlignItems,
- BackgroundColor,
BorderRadius,
Display,
Severity,
@@ -17,6 +16,7 @@ import {
TextVariant,
} from '../../../../../helpers/constants/design-system';
import { useI18nContext } from '../../../../../hooks/useI18nContext';
+import { getSeverityBackground } from '../utils';
export type InlineAlertProps = {
/** The onClick handler for the inline alerts */
@@ -27,18 +27,6 @@ export type InlineAlertProps = {
style?: React.CSSProperties;
};
-function getSeverityBackground(severity: Severity): BackgroundColor {
- switch (severity) {
- case Severity.Danger:
- return BackgroundColor.errorMuted;
- case Severity.Warning:
- return BackgroundColor.warningMuted;
- // Defaults to Severity.Info
- default:
- return BackgroundColor.primaryMuted;
- }
-}
-
export default function InlineAlert({
onClick,
severity = Severity.Info,
diff --git a/ui/components/app/confirmations/alerts/multiple-alert-modal/multiple-alert-modal.stories.tsx b/ui/components/app/confirmations/alerts/multiple-alert-modal/multiple-alert-modal.stories.tsx
index c8dd56b98d91..b3108b7d6832 100644
--- a/ui/components/app/confirmations/alerts/multiple-alert-modal/multiple-alert-modal.stories.tsx
+++ b/ui/components/app/confirmations/alerts/multiple-alert-modal/multiple-alert-modal.stories.tsx
@@ -1,23 +1,47 @@
import React from 'react';
import { MultipleAlertModal } from './multiple-alert-modal';
import { Severity } from '../../../../../helpers/constants/design-system';
-import { Meta } from '@storybook/react';
+import { Meta, StoryFn } from '@storybook/react';
import configureStore from '../../../../../store/store';
import { Provider } from 'react-redux';
import { Alert } from '../../../../../ducks/confirm-alerts/confirm-alerts';
+import { useArgs } from '@storybook/client-api';
+import { Box, Button } from '../../../../component-library';
const ALERTS_MOCK: Alert[] = [
- { key: 'from', severity: Severity.Danger, message: 'Description of what may happen if this alert was ignored', reason: 'Reason for the alert 1', alertDetails: ['We found the contract Petname 0xEqT3b9773b1763efa556f55ccbeb20441962d82x to be malicious',
- 'Operator is an externally owned account (EOA) ',
- 'Operator is untrusted according to previous activity',]},
- { key: 'data', severity: Severity.Warning, message: 'Alert 2', alertDetails:['detail 1 warning', 'detail 2 warning'] },
- { key: 'contract', severity: Severity.Info, message: 'Alert Info', alertDetails:['detail 1 info', 'detail info'] },
+ {
+ key: 'from',
+ severity: Severity.Danger,
+ message: 'Description of what may happen if this alert was ignored',
+ reason: 'Reason for the alert 1',
+ alertDetails: [
+ 'We found the contract Petname 0xEqT3b9773b1763efa556f55ccbeb20441962d82x to be malicious',
+ 'Operator is an externally owned account (EOA) ',
+ 'Operator is untrusted according to previous activity',
+ ],
+ },
+ {
+ key: 'data',
+ severity: Severity.Warning,
+ message: 'Alert 2',
+ alertDetails: ['detail 1 warning', 'detail 2 warning'],
+ },
+ {
+ key: 'contract',
+ severity: Severity.Info,
+ message: 'Alert Info',
+ alertDetails: ['detail 1 info', 'detail info'],
+ },
];
const OWNER_ID_MOCK = '123';
-const storeMock = configureStore({ confirmAlerts: {
- alerts: {[OWNER_ID_MOCK]: ALERTS_MOCK},
- confirmed: {[OWNER_ID_MOCK]: {'from': false, 'data': false, 'contract': false}},
- } });
+const storeMock = configureStore({
+ confirmAlerts: {
+ alerts: { [OWNER_ID_MOCK]: ALERTS_MOCK },
+ confirmed: {
+ [OWNER_ID_MOCK]: { from: false, data: false, contract: false },
+ },
+ },
+});
export default {
title: 'Confirmations/Components/Alerts/MultipleAlertModal',
@@ -33,7 +57,8 @@ export default {
},
onClose: {
action: 'onClick',
- description: 'The function to be executed when the modal needs to be closed.',
+ description:
+ 'The function to be executed when the modal needs to be closed.',
},
ownerId: {
control: 'text',
@@ -41,62 +66,34 @@ export default {
},
},
args: {
- onAcknowledgeClick: () => {},
- onClose: () => {},
ownerId: OWNER_ID_MOCK,
},
decorators: [(story) => {story()}],
} as Meta;
-export const TemplateStory = (args) => {
- return ;
-};
-TemplateStory.storyName = 'Multiple Critical Alert Modal';
-
-/**
- * Single Critical Alert Modal.
- */
-export const SingleCriticalAlertModal = TemplateStory.bind({});
-SingleCriticalAlertModal.storyName = 'Single Critical Alert Modal';
-SingleCriticalAlertModal.args = {
- alertKey: 'from',
-};
-SingleCriticalAlertModal.decorators = [
- (story) => {
- const singleAlertStore = configureStore({
- confirmAlerts: {
- alerts: { [OWNER_ID_MOCK]: [ALERTS_MOCK[0]] },
- confirmed: { [OWNER_ID_MOCK]: { 'from': false } },
- }
- });
- return {story()}
- }
-];
-
-/**
- * Multiple Warning Alert Modal.
- */
-export const MultipleWarningAlertModal = TemplateStory.bind({});
-
-MultipleWarningAlertModal.storyName = 'Multiple Warning Alert Modal';
-MultipleWarningAlertModal.args = {
- alertKey: 'data',
-};
-
-/**
- * Multiple Info Alert Modal.
- */
-export const MultipleInfoAlertModal = TemplateStory.bind({});
-MultipleInfoAlertModal.storyName = 'Multiple Info Alert Modal';
-MultipleInfoAlertModal.args = {
- alertKey: 'contract',
-};
-
/**
* Multiple Critical Alert Modal.
*/
-export const MultipleCriticalAlertModal = TemplateStory.bind({});
-MultipleCriticalAlertModal.storyName = 'Multiple Critical Alert Modal';
-MultipleCriticalAlertModal.args = {
- alertKey: 'from',
-};
\ No newline at end of file
+export const TemplateStory: StoryFn = (args) => {
+ const [{ isOpen }, updateArgs] = useArgs();
+ const handleOnClick = () => {
+ updateArgs({ isOpen: true });
+ };
+ const handleOnClose = () => {
+ updateArgs({ isOpen: false });
+ };
+ return (
+
+ {isOpen && (
+
+ )}
+
+
+ );
+};
+TemplateStory.storyName = 'Multiple Critical Alert Modal';
diff --git a/ui/components/app/confirmations/alerts/multiple-alert-modal/multiple-alert-modal.tsx b/ui/components/app/confirmations/alerts/multiple-alert-modal/multiple-alert-modal.tsx
index 86321779e947..dca3b9fff8cb 100644
--- a/ui/components/app/confirmations/alerts/multiple-alert-modal/multiple-alert-modal.tsx
+++ b/ui/components/app/confirmations/alerts/multiple-alert-modal/multiple-alert-modal.tsx
@@ -168,8 +168,8 @@ export function MultipleAlertModal({
return;
}
- handleBackButtonClick();
- }, [onFinalAcknowledgeClick, handleBackButtonClick, selectedIndex, alerts]);
+ handleNextButtonClick();
+ }, [onFinalAcknowledgeClick, handleNextButtonClick, selectedIndex, alerts]);
return (
{
+ it('returns the correct background color for Danger severity', () => {
+ const result = getSeverityBackground(Severity.Danger);
+ expect(result).toBe(BackgroundColor.errorMuted);
+ });
+
+ it('returns the correct background color for Warning severity', () => {
+ const result = getSeverityBackground(Severity.Warning);
+ expect(result).toBe(BackgroundColor.warningMuted);
+ });
+
+ it('returns the default background color for other severity levels', () => {
+ const result = getSeverityBackground(Severity.Info);
+ expect(result).toBe(BackgroundColor.primaryMuted);
+ });
+});
diff --git a/ui/components/app/confirmations/alerts/utils.ts b/ui/components/app/confirmations/alerts/utils.ts
new file mode 100644
index 000000000000..582c43d2d6c2
--- /dev/null
+++ b/ui/components/app/confirmations/alerts/utils.ts
@@ -0,0 +1,22 @@
+import {
+ BackgroundColor,
+ Severity,
+} from '../../../../helpers/constants/design-system';
+
+/**
+ * Returns the background color based on the severity level.
+ *
+ * @param severity - The severity level.
+ * @returns The background color corresponding to the severity level.
+ */
+export function getSeverityBackground(severity: Severity): BackgroundColor {
+ switch (severity) {
+ case Severity.Danger:
+ return BackgroundColor.errorMuted;
+ case Severity.Warning:
+ return BackgroundColor.warningMuted;
+ // Defaults to Severity.Info
+ default:
+ return BackgroundColor.primaryMuted;
+ }
+}
diff --git a/ui/pages/confirmations/components/confirm/footer/footer.test.tsx b/ui/pages/confirmations/components/confirm/footer/footer.test.tsx
index 7e3d629595de..5deb6021ca69 100644
--- a/ui/pages/confirmations/components/confirm/footer/footer.test.tsx
+++ b/ui/pages/confirmations/components/confirm/footer/footer.test.tsx
@@ -11,6 +11,7 @@ import { fireEvent, renderWithProvider } from '../../../../../../test/jest';
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 Footer from './footer';
jest.mock('react-redux', () => ({
@@ -159,4 +160,52 @@ describe('ConfirmFooter', () => {
fireEvent.click(submitButton);
expect(mockFn).toHaveBeenCalledTimes(1);
});
+
+ describe('ConfirmButton', () => {
+ const OWNER_ID_MOCK = '123';
+ const KEY_ALERT_KEY_MOCK = 'Key';
+ const ALERT_MESSAGE_MOCK = 'Alert 1';
+ const alertsMock = [
+ {
+ key: KEY_ALERT_KEY_MOCK,
+ field: KEY_ALERT_KEY_MOCK,
+ severity: Severity.Warning,
+ message: ALERT_MESSAGE_MOCK,
+ reason: 'Reason 1',
+ alertDetails: ['Detail 1', 'Detail 2'],
+ },
+ ];
+ const stateWithAlertsMock = {
+ ...mockState,
+ confirmAlerts: {
+ alerts: { [OWNER_ID_MOCK]: alertsMock },
+ confirmed: {
+ [OWNER_ID_MOCK]: { [KEY_ALERT_KEY_MOCK]: false },
+ },
+ },
+ confirm: {
+ currentConfirmation: {
+ id: OWNER_ID_MOCK,
+ msgParams: {
+ from: '0xc42edfcc21ed14dda456aa0756c153f7985d8813',
+ },
+ },
+ },
+ };
+ it('renders the review alerts button when there are unconfirmed alerts', () => {
+ const { getByText } = render(stateWithAlertsMock);
+ expect(getByText('Review alerts')).toBeInTheDocument();
+ });
+
+ it('renders the confirm button when there are no unconfirmed alerts', () => {
+ const { getByText } = render();
+ expect(getByText('Confirm')).toBeInTheDocument();
+ });
+
+ it('sets the alert modal visible when the review alerts button is clicked', () => {
+ const { getByTestId } = render(stateWithAlertsMock);
+ fireEvent.click(getByTestId('confirm-footer-confirm-button'));
+ expect(getByTestId('alert-modal-button')).toBeDefined();
+ });
+ });
});
diff --git a/ui/pages/confirmations/components/confirm/footer/footer.tsx b/ui/pages/confirmations/components/confirm/footer/footer.tsx
index fcf1ae4ae591..60011c7f648c 100644
--- a/ui/pages/confirmations/components/confirm/footer/footer.tsx
+++ b/ui/pages/confirmations/components/confirm/footer/footer.tsx
@@ -1,10 +1,11 @@
import { ethErrors, serializeError } from 'eth-rpc-errors';
-import React, { useCallback } from 'react';
+import React, { useCallback, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import {
Button,
ButtonSize,
ButtonVariant,
+ IconName,
} from '../../../../../components/component-library';
import { Footer as PageFooter } from '../../../../../components/multichain/pages/page';
import { useI18nContext } from '../../../../../hooks/useI18nContext';
@@ -19,13 +20,80 @@ import {
} from '../../../../../store/actions';
import { confirmSelector } from '../../../selectors';
import { getConfirmationSender } from '../utils';
+import useAlerts from '../../../../../hooks/useAlerts';
+import { ConfirmAlertModal } from '../../../../../components/app/confirmations/alerts/confirm-alert-modal';
import useIsDangerButton from './useIsDangerButton';
+function getIconName(hasUnconfirmedAlerts: boolean): IconName {
+ if (hasUnconfirmedAlerts) {
+ return IconName.SecuritySearch;
+ }
+ return IconName.Danger;
+}
+
+function ConfirmButton({
+ alertOwnerId = '',
+ disabled,
+ onSubmit,
+ onCancel,
+}: {
+ alertOwnerId?: string;
+ disabled: boolean;
+ onSubmit: () => void;
+ onCancel: () => void;
+}) {
+ const isDangerButton = useIsDangerButton();
+ const t = useI18nContext();
+
+ const [confirmModalVisible, setConfirmModalVisible] =
+ useState(false);
+ const { alerts, isAlertConfirmed } = useAlerts(alertOwnerId);
+ const unconfirmedAlerts = alerts.filter(
+ (alert) => alert.field && !isAlertConfirmed(alert.key),
+ );
+ const hasAlerts = alerts.length > 0;
+ const hasUnconfirmedAlerts = unconfirmedAlerts.length > 0;
+
+ const handleCloseConfirmModal = useCallback(() => {
+ setConfirmModalVisible(false);
+ }, []);
+
+ const handleOpenConfirmModal = useCallback(() => {
+ setConfirmModalVisible(true);
+ }, [hasUnconfirmedAlerts]);
+
+ return (
+ <>
+ {confirmModalVisible && (
+
+ )}
+
+ >
+ );
+}
+
const Footer = () => {
const dispatch = useDispatch();
const t = useI18nContext();
const confirm = useSelector(confirmSelector);
- const isDangerButton = useIsDangerButton();
const { currentConfirmation, isScrollToBottomNeeded } = confirm;
const { from } = getConfirmationSender(currentConfirmation);
@@ -58,6 +126,7 @@ const Footer = () => {
if (!currentConfirmation) {
return;
}
+
dispatch(resolvePendingApproval(currentConfirmation.id, undefined));
///: BEGIN:ONLY_INCLUDE_IF(build-mmi)
@@ -75,12 +144,9 @@ const Footer = () => {
>
{t('cancel')}
-
+ onCancel={onCancel}
+ />
);
};