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} + /> ); };