Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add MultipleAlertModal component #13683

Open
wants to merge 57 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
14ccb4c
feat: add initial structure for Alert System
vinistevam Feb 6, 2025
64ae2df
fix lint and add initial structure
vinistevam Feb 11, 2025
be720cf
add unit tests
vinistevam Feb 11, 2025
19db33d
add use memo
vinistevam Feb 11, 2025
b971c5b
fix sonar issue
vinistevam Feb 11, 2025
4708c65
Merge branch 'main' into feat/initial-structure-alert-system
vinistevam Feb 11, 2025
863fc3c
Merge branch 'main' into feat/initial-structure-alert-system
vinistevam Feb 13, 2025
e27973a
feat: add AlertModal component
vinistevam Feb 13, 2025
3faf5bb
unit test and clean up
vinistevam Feb 14, 2025
b43b8fb
changed actions to action alert property
vinistevam Feb 14, 2025
d6adb57
add unit test and applied suggestions
vinistevam Feb 14, 2025
0d86662
Add unit tests
vinistevam Feb 17, 2025
4dbcc43
Merge branch 'feat/initial-structure-alert-system' into feat/add-aler…
vinistevam Feb 17, 2025
78d2255
add unit test
vinistevam Feb 17, 2025
834c098
Merge branch 'feat/add-alert-modal' of https://github.com/MetaMask/me…
vinistevam Feb 17, 2025
b71e840
Applied suggestions
vinistevam Feb 17, 2025
c3cc520
Merge branch 'main' into feat/initial-structure-alert-system
vinistevam Feb 17, 2025
de8c503
Merge branch 'feat/initial-structure-alert-system' into feat/add-aler…
vinistevam Feb 17, 2025
8023c78
fix unit tests
vinistevam Feb 18, 2025
929a30d
fix lint
vinistevam Feb 18, 2025
2ce146f
Merge branch 'feat/initial-structure-alert-system' into feat/add-aler…
vinistevam Feb 18, 2025
f638bbd
feat: add Confirm Alert Modal component
vinistevam Feb 19, 2025
545a399
fix unit test
vinistevam Feb 19, 2025
c85efd0
fix sonar issues
vinistevam Feb 19, 2025
dbc25e5
feat: add GeneralAlertBanner component
vinistevam Feb 20, 2025
abecb18
Merge branch 'main' into feat/add-alert-modal
vinistevam Feb 20, 2025
2705250
feat: add Confirm Alert Modal component
vinistevam Feb 19, 2025
4088eb9
fix unit test
vinistevam Feb 19, 2025
e837670
fix sonar issues
vinistevam Feb 19, 2025
59c9407
Merge branch 'feat/add-alert-confirm' into feat/add-general-alert-banner
vinistevam Feb 20, 2025
2c2c0d0
feat: add MultipleAlertModal component
vinistevam Feb 24, 2025
8b5f9e5
Merge branch 'main' into feat/add-alert-modal
vinistevam Feb 24, 2025
0025102
Merge branch 'feat/add-alert-modal' into feat/add-alert-confirm
vinistevam Feb 24, 2025
18e3422
fix: applied suggestions
vinistevam Feb 24, 2025
70f5cf2
Merge branch 'feat/add-alert-confirm' into feat/add-general-alert-banner
vinistevam Feb 24, 2025
c9546c8
fix alertKey state unit tests
vinistevam Feb 24, 2025
9c3790d
Merge branch 'feat/add-general-alert-banner' into feat/add-multiple-a…
vinistevam Feb 24, 2025
549e386
Merge branch 'main' into feat/add-alert-modal
vinistevam Feb 25, 2025
f571f04
Merge branch 'feat/add-alert-modal' into feat/add-alert-confirm
vinistevam Feb 25, 2025
6e0de88
Merge branch 'feat/add-alert-confirm' into feat/add-general-alert-banner
vinistevam Feb 25, 2025
744289d
Merge branch 'feat/add-general-alert-banner' into feat/add-multiple-a…
vinistevam Feb 25, 2025
18e37a7
Merge branch 'main' into feat/add-alert-confirm
vinistevam Feb 25, 2025
1f51aa3
Merge branch 'feat/add-alert-confirm' into feat/add-general-alert-banner
vinistevam Feb 25, 2025
0952d96
Merge branch 'feat/add-general-alert-banner' into feat/add-multiple-a…
vinistevam Feb 25, 2025
c3a2e9e
applied suggestions
vinistevam Feb 26, 2025
6ff6427
Merge branch 'feat/add-general-alert-banner' into feat/add-multiple-a…
vinistevam Feb 26, 2025
f6bf64d
fix: apply suggestions
vinistevam Feb 26, 2025
bdac9b0
Merge branch 'main' into feat/add-alert-confirm
vinistevam Feb 26, 2025
fc8bf8c
Merge branch 'feat/add-alert-confirm' into feat/add-general-alert-banner
vinistevam Feb 26, 2025
7e02b5a
Merge branch 'feat/add-general-alert-banner' into feat/add-multiple-a…
vinistevam Feb 26, 2025
e873de7
Merge branch 'main' into feat/add-general-alert-banner
vinistevam Feb 26, 2025
998fbad
Merge branch 'feat/add-general-alert-banner' into feat/add-multiple-a…
vinistevam Feb 26, 2025
ca9ef5c
Merge branch 'main' into feat/add-general-alert-banner
vinistevam Feb 27, 2025
ffc0e13
Merge branch 'feat/add-general-alert-banner' into feat/add-multiple-a…
vinistevam Feb 27, 2025
d94d72a
Merge branch 'main' into feat/add-general-alert-banner
vinistevam Feb 28, 2025
e71fcc3
Merge branch 'feat/add-general-alert-banner' into feat/add-multiple-a…
vinistevam Feb 28, 2025
5ac825a
Merge branch 'main' into feat/add-multiple-alert-modal
vinistevam Mar 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -55,17 +55,17 @@ const mockAlerts = [

describe('AlertModal', () => {
const baseMockUseAlerts = {
alertKey: 'alert1',
alerts: mockAlerts,
fieldAlerts: mockAlerts,
hideAlertModal: jest.fn(),
alertModalVisible: true,
setAlertKey: jest.fn(),
};

const baseMockUseAlertsConfirmed = {
alertKey: 'alert1',
isAlertConfirmed: jest.fn().mockReturnValue(false),
setAlertConfirmed: jest.fn(),
setAlertKey: jest.fn(),
unconfirmedDangerAlerts: [],
unconfirmedFieldDangerAlerts: [],
hasUnconfirmedDangerAlerts: false,
Expand Down Expand Up @@ -93,8 +93,8 @@ describe('AlertModal', () => {
expect(icon).toBeDefined();
expect(icon.props.name).toBe(IconName.Danger);

(useAlertsConfirmed as jest.Mock).mockReturnValue({
...baseMockUseAlertsConfirmed,
(useAlerts as jest.Mock).mockReturnValue({
...baseMockUseAlerts,
alertKey: 'alert3', // Info
});
const { getByTestId: getByTestIdInfo } = render(<AlertModal />);
Expand All @@ -103,14 +103,13 @@ describe('AlertModal', () => {
});

it('handles checkbox click correctly', async () => {
(useAlertsConfirmed as jest.Mock).mockReturnValue({
...baseMockUseAlertsConfirmed,
alertKey: 'alert2',
});
const setAlertConfirmed = jest.fn();
(useAlertsConfirmed as jest.Mock).mockReturnValueOnce({
...baseMockUseAlertsConfirmed,
setAlertConfirmed,
});
(useAlerts as jest.Mock).mockReturnValue({
...baseMockUseAlerts,
alertKey: 'alert2',
});

Expand Down Expand Up @@ -171,17 +170,17 @@ describe('AlertModal', () => {
});

it('does not render the checkbox if the severity is not Danger', () => {
(useAlertsConfirmed as jest.Mock).mockReturnValue({
...baseMockUseAlertsConfirmed,
(useAlerts as jest.Mock).mockReturnValue({
...baseMockUseAlerts,
alertKey: 'alert1',
});
const { queryByText } = render(<AlertModal />);
expect(queryByText(CHECKBOX_LABEL)).toBeNull();
});

it('renders checkbox if the severity is Danger', async () => {
(useAlertsConfirmed as jest.Mock).mockReturnValue({
...baseMockUseAlertsConfirmed,
(useAlerts as jest.Mock).mockReturnValue({
...baseMockUseAlerts,
alertKey: 'alert2',
});
const { queryByText } = render(<AlertModal />);
Expand Down Expand Up @@ -211,4 +210,45 @@ describe('AlertModal', () => {
const { queryByText } = render(<AlertModal />);
expect(queryByText(CHECKBOX_LABEL)).toBeNull();
});

it('renders header accessory when provided', () => {
const headerAccessory = <Text>Header Accessory</Text>;
const { getByText } = render(<AlertModal headerAccessory={headerAccessory} />);
expect(getByText('Header Accessory')).toBeDefined();
});

it('calls onAcknowledgeClick when modal is closed', async () => {
const onAcknowledgeClick = jest.fn();
const { getByText } = render(<AlertModal onAcknowledgeClick={onAcknowledgeClick} />);
await act(async () => {
fireEvent.press(getByText('Got it'));
});
expect(onAcknowledgeClick).toHaveBeenCalled();
});

it('renders default title when alert title is not provided', () => {
const alertWithoutTitle = {
...mockAlerts[0],
title: undefined,
};
(useAlerts as jest.Mock).mockReturnValue({
...baseMockUseAlerts,
fieldAlerts: [alertWithoutTitle],
});
const { getByText } = render(<AlertModal />);
expect(getByText('Alert')).toBeDefined();
});

it('handles alert without message correctly', () => {
const alertWithoutMessage = {
...mockAlerts[0],
message: undefined,
};
(useAlerts as jest.Mock).mockReturnValue({
...baseMockUseAlerts,
fieldAlerts: [alertWithoutMessage],
});
const { queryByText } = render(<AlertModal />);
expect(queryByText(ALERT_MESSAGE_MOCK)).toBeNull();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -7,49 +7,31 @@ import Button, { ButtonSize, ButtonVariants, ButtonWidthTypes } from '../../../.
import Checkbox from '../../../../../component-library/components/Checkbox';
import Icon, { IconName, IconSize } from '../../../../../component-library/components/Icons/Icon';
import Text, { TextVariant } from '../../../../../component-library/components/Texts/Text';
import { ThemeColors } from '@metamask/design-tokens';
import { useStyles } from '../../../../hooks/useStyles';
import styleSheet from './AlertModal.styles';
import { strings } from '../../../../../../locales/i18n';
import { Alert, Severity } from '../../types/alerts';
import { useAlertsConfirmed } from '../../../../hooks/useAlertsConfirmed';

const getSeverityStyle = (severity: Severity, colors: ThemeColors) => {
switch (severity) {
case Severity.Warning:
return {
background: colors.warning.muted,
icon: colors.warning.default,
};
case Severity.Danger:
return {
background: colors.error.muted,
icon: colors.error.default,
};
default:
return {
background: colors.background.default,
icon: colors.info.default,
};
}
};
import { getSeverityStyle } from '../utils';

interface HeaderProps {
iconColor: string;
selectedAlert: Alert;
styles: Record<string, ViewStyle>;
headerAccessory?: React.ReactNode;
}

const Header: React.FC<HeaderProps> = ({ selectedAlert, iconColor, styles }) => (
const Header: React.FC<HeaderProps> = ({ selectedAlert, iconColor, styles, headerAccessory }) => (
<>
<View style={styles.iconWrapper}>
{headerAccessory ?? <View style={styles.iconWrapper}>
<Icon
name={selectedAlert.severity === Severity.Info ? IconName.Info : IconName.Danger}
size={IconSize.Xl}
color={iconColor}
testID="alert-modal-icon"
/>
</View>
}
<View style={styles.headerContainer}>
<Text style={styles.headerText} variant={TextVariant.BodyMDBold}>
{selectedAlert.title ?? strings('alert_system.alert_modal.title')}
Expand All @@ -65,18 +47,22 @@ interface ContentProps {
}

const Content: React.FC<ContentProps> = ({ backgroundColor, selectedAlert, styles }) => (
<View style={[styles.content, {backgroundColor}]}>
<View style={[styles.content, { backgroundColor }]}>
{selectedAlert.content ?? (
<>
<Text style={styles.message}>{selectedAlert.message}</Text>
<Text style={styles.message} variant={TextVariant.BodyMDBold}>
{strings('alert_system.alert_modal.alert_details')}
</Text>
{selectedAlert.alertDetails?.map((detail, index) => (
<Text key={`details-${index}`} style={styles.detailsText} variant={TextVariant.BodyMD}>
{'• ' + detail}
</Text>
))}
{selectedAlert.alertDetails && (
<>
<Text style={styles.message} variant={TextVariant.BodyMDBold}>
{strings('alert_system.alert_modal.alert_details')}
</Text>
{selectedAlert.alertDetails.map((detail, index) => (
<Text key={`details-${index}`} style={styles.detailsText} variant={TextVariant.BodyMD}>
{'• ' + detail}
</Text>
))}
</>
)}
</>
)}
</View>
Expand Down Expand Up @@ -139,17 +125,26 @@ const Buttons: React.FC<ButtonsProps> = ({ hideAlertModal, action, styles, onHan
</View>
);

const AlertModal = () => {
interface AlertModalProps {
headerAccessory?: React.ReactNode;
onAcknowledgeClick?: () => void;
}

const AlertModal: React.FC<AlertModalProps> = ({headerAccessory, onAcknowledgeClick}) => {
const { colors } = useTheme();
const styles = (useStyles(styleSheet, {})).styles as Record<string, ViewStyle>;
const { hideAlertModal, alertModalVisible, fieldAlerts } = useAlerts();
const { isAlertConfirmed, setAlertConfirmed, alertKey } = useAlertsConfirmed(fieldAlerts);
const { hideAlertModal, alertModalVisible, fieldAlerts, alertKey } = useAlerts();
const { isAlertConfirmed, setAlertConfirmed } = useAlertsConfirmed(fieldAlerts);

const handleClose = useCallback(
() => {
if (onAcknowledgeClick) {
onAcknowledgeClick();
return;
}
hideAlertModal();
},
[hideAlertModal],
[hideAlertModal, onAcknowledgeClick],
);

const handleCheckboxClick = useCallback(
Expand Down Expand Up @@ -177,12 +172,13 @@ const AlertModal = () => {
const severityStyle = getSeverityStyle(selectedAlert.severity, colors);

return (
<BottomModal onClose={handleClose}>
<BottomModal onClose={hideAlertModal}>
<View style={styles.modalContainer}>
<Header
selectedAlert={selectedAlert}
iconColor={severityStyle.icon}
styles={styles}
headerAccessory={headerAccessory}
/>
<View>
<Content
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import { BannerAlertSeverity } from '../../../../../component-library/components
import Text from '../../../../../component-library/components/Texts/Text';
import { Alert, Severity } from '../../types/alerts';
import { useAlerts } from '../context';
import GeneralAlertBanner, { getBannerAlertSeverity } from './GeneralAlertBanner';
import GeneralAlertBanner from './GeneralAlertBanner';
import { getBannerAlertSeverity } from '../utils';

jest.mock('../context', () => ({
useAlerts: jest.fn(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,9 @@ import React from 'react';
import { View } from 'react-native';
import { useAlerts } from '../context';
import BannerAlert from '../../../../../component-library/components/Banners/Banner/variants/BannerAlert';
import { Severity } from '../../types/alerts';
import { BannerAlertSeverity } from '../../../../../component-library/components/Banners/Banner';
import styleSheet from './GeneralAlertBanner.styles';
import { useStyles } from '../../../../hooks/useStyles';

/**
* Converts the severity of a banner alert to the corresponding BannerAlertSeverity.
*
* @param severity - The severity of the banner alert.
* @returns The corresponding BannerAlertSeverity.
*/
export function getBannerAlertSeverity(
severity: Severity,
): BannerAlertSeverity {
switch (severity) {
case Severity.Danger:
return BannerAlertSeverity.Error;
case Severity.Warning:
return BannerAlertSeverity.Warning;
default:
return BannerAlertSeverity.Info;
}
}
import { getBannerAlertSeverity } from '../utils';

const GeneralAlertBanner = () => {
const { generalAlerts } = useAlerts();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { StyleSheet } from 'react-native';

const styleSheet = () => StyleSheet.create({
navAlertHeader: {
padding: 0,
},
pageNavigation: { flexDirection: 'row', alignItems: 'center' },
});

export default styleSheet;
Loading
Loading