diff --git a/app/components/Nav/App/index.js b/app/components/Nav/App/index.js
index 577c1afce5c..e7293543910 100644
--- a/app/components/Nav/App/index.js
+++ b/app/components/Nav/App/index.js
@@ -114,6 +114,7 @@ import OnboardingSuccess from '../../Views/OnboardingSuccess';
import DefaultSettings from '../../Views/OnboardingSuccess/DefaultSettings';
import BasicFunctionalityModal from '../../UI/BasicFunctionality/BasicFunctionalityModal/BasicFunctionalityModal';
import SmartTransactionsOptInModal from '../../Views/SmartTransactionsOptInModal/SmartTranactionsOptInModal';
+import ProfileSyncingModal from '../../UI/ProfileSyncing/ProfileSyncingModal/ProfileSyncingModal';
import NFTAutoDetectionModal from '../../../../app/components/Views/NFTAutoDetectionModal/NFTAutoDetectionModal';
///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps)
import { SnapsExecutionWebView } from '../../../lib/snaps';
@@ -656,6 +657,10 @@ const App = ({ userLoggedIn }) => {
name={Routes.SHEET.BASIC_FUNCTIONALITY}
component={BasicFunctionalityModal}
/>
+
+
+
+
+
+`;
diff --git a/app/components/UI/Notification/List/index.test.tsx b/app/components/UI/Notification/List/index.test.tsx
new file mode 100644
index 00000000000..895f1120775
--- /dev/null
+++ b/app/components/UI/Notification/List/index.test.tsx
@@ -0,0 +1,24 @@
+import React from 'react';
+import NotificationsList from './';
+import renderWithProvider from '../../../../util/test/renderWithProvider';
+import MOCK_NOTIFICATIONS from '../__mocks__/mock_notifications';
+import { NavigationProp, ParamListBase } from '@react-navigation/native';
+
+const navigationMock = {
+ navigate: jest.fn(),
+} as unknown as NavigationProp;
+
+describe('NotificationsList', () => {
+ it('should render correctly', () => {
+ const { toJSON } = renderWithProvider(
+ ,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+});
diff --git a/app/components/UI/Notification/List/index.tsx b/app/components/UI/Notification/List/index.tsx
new file mode 100644
index 00000000000..6f58ae2a15e
--- /dev/null
+++ b/app/components/UI/Notification/List/index.tsx
@@ -0,0 +1,239 @@
+import { NavigationProp, ParamListBase } from '@react-navigation/native';
+import React, { useCallback, useMemo } from 'react';
+import { ActivityIndicator, FlatList, FlatListProps, View } from 'react-native';
+import ScrollableTabView, {
+ DefaultTabBar,
+ DefaultTabBarProps,
+ TabBarProps,
+} from 'react-native-scrollable-tab-view';
+import { NotificationsViewSelectorsIDs } from '../../../../../e2e/selectors/NotificationsView.selectors';
+import { strings } from '../../../../../locales/i18n';
+import {
+ hasNotificationComponents,
+ hasNotificationModal,
+ NotificationComponentState,
+} from '../../../../util/notifications/notification-states';
+import Routes from '../../../../constants/navigation/Routes';
+import { MetaMetricsEvents } from '../../../../core/Analytics';
+import { Notification } from '../../../../util/notifications';
+import { useMarkNotificationAsRead } from '../../../../util/notifications/hooks/useNotifications';
+import { useMetrics } from '../../../hooks/useMetrics';
+import Empty from '../Empty';
+import { NotificationMenuItem } from '../NotificationMenuItem';
+import useStyles from './useStyles';
+
+interface NotificationsListProps {
+ navigation: NavigationProp;
+ allNotifications: Notification[];
+ walletNotifications: Notification[];
+ web3Notifications: Notification[];
+ loading: boolean;
+}
+
+interface NotificationsListItemProps {
+ navigation: NavigationProp;
+ notification: Notification;
+}
+interface NotificationsListItemProps {
+ navigation: NavigationProp;
+ notification: Notification;
+}
+
+function Loading() {
+ const {
+ theme: { colors },
+ styles,
+ } = useStyles();
+
+ return (
+
+
+
+ );
+}
+
+function NotificationsListItem(props: NotificationsListItemProps) {
+ const { styles } = useStyles();
+ const { markNotificationAsRead } = useMarkNotificationAsRead();
+
+ const onNotificationClick = useCallback(
+ (item: Notification) => {
+ markNotificationAsRead([
+ {
+ id: item.id,
+ type: item.type,
+ isRead: item.isRead,
+ },
+ ]);
+ if (hasNotificationModal(item.type)) {
+ props.navigation.navigate(Routes.NOTIFICATIONS.DETAILS, {
+ notification: item,
+ });
+ }
+ },
+ [markNotificationAsRead, props.navigation],
+ );
+
+ const menuItemState = useMemo(() => {
+ const notificationState =
+ NotificationComponentState[props.notification.type];
+ return notificationState.createMenuItem(props.notification);
+ }, [props.notification]);
+
+ if (!hasNotificationComponents(props.notification.type)) {
+ return null;
+ }
+
+ return (
+ onNotificationClick(props.notification)}
+ styles={styles}
+ simultaneousHandlers={undefined}
+ >
+
+
+
+ );
+}
+
+function useNotificationListProps(props: {
+ navigation: NavigationProp;
+}) {
+ const { styles } = useStyles();
+
+ const getListProps = useCallback(
+ (data: Notification[], tabLabel?: string) => {
+ const listProps: FlatListProps = {
+ keyExtractor: (item) => item.id,
+ data,
+ ListEmptyComponent: (
+
+ ),
+ contentContainerStyle: styles.list,
+ renderItem: ({ item }) => (
+
+ ),
+ initialNumToRender: 10,
+ maxToRenderPerBatch: 2,
+ onEndReachedThreshold: 0.5,
+ };
+
+ return { ...listProps, tabLabel: tabLabel ?? '' };
+ },
+ [props.navigation, styles.list],
+ );
+
+ return getListProps;
+}
+
+function SingleNotificationList(props: NotificationsListProps) {
+ const getListProps = useNotificationListProps(props);
+
+ return ;
+}
+
+function TabbedNotificationList(props: NotificationsListProps) {
+ const {
+ theme: { colors },
+ styles,
+ } = useStyles();
+ const { trackEvent } = useMetrics();
+
+ const getListProps = useNotificationListProps(props);
+
+ const onTabClick = useCallback(
+ (tabLabel: string) => {
+ switch (tabLabel) {
+ case strings('notifications.list.0'):
+ trackEvent(MetaMetricsEvents.ALL_NOTIFICATIONS);
+ break;
+ case strings('notifications.list.1'):
+ trackEvent(MetaMetricsEvents.WALLET_NOTIFICATIONS);
+ break;
+ case strings('notifications.list.2'):
+ trackEvent(MetaMetricsEvents.WEB3_NOTIFICATIONS);
+ break;
+ default:
+ break;
+ }
+ },
+ [trackEvent],
+ );
+
+ return (
+ ) => (
+
+
+
+ )}
+ onChangeTab={(val) => onTabClick(val.ref.props.tabLabel)}
+ >
+ {/* Tab 1 - All Notifications */}
+
+
+ {/* Tab 2 - Wallet Notifications */}
+
+
+ {/* Tab 3 - Web 3 Notifications */}
+
+
+ );
+}
+
+const Notifications = (props: NotificationsListProps) => {
+ const { styles } = useStyles();
+ if (props.loading) {
+ return (
+
+
+
+ );
+ }
+
+ if (props.web3Notifications.length > 0) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ );
+};
+
+export default Notifications;
diff --git a/app/components/UI/Notification/List/styles.ts b/app/components/UI/Notification/List/styles.ts
new file mode 100644
index 00000000000..53fb50f0cb8
--- /dev/null
+++ b/app/components/UI/Notification/List/styles.ts
@@ -0,0 +1,146 @@
+/* eslint-disable import/prefer-default-export */
+import { StyleSheet, TextStyle } from 'react-native';
+import type { Theme } from '@metamask/design-tokens';
+
+export type NotificationListStyles = ReturnType;
+
+export const createStyles = ({ colors, typography }: Theme) =>
+ StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: colors.background.default,
+ marginHorizontal: 8,
+ },
+ wrapper: {
+ flex: 1,
+ paddingVertical: 10,
+ justifyContent: 'center',
+ borderRadius: 10,
+ },
+ loaderContainer: {
+ position: 'absolute',
+ zIndex: 999,
+ width: '100%',
+ height: '100%',
+ },
+ menuItemContainer: {
+ flexDirection: 'row',
+ gap: 16,
+ },
+
+ tabUnderlineStyle: {
+ height: 2,
+ backgroundColor: colors.primary.default,
+ },
+ tabStyle: {
+ paddingBottom: 0,
+ paddingVertical: 8,
+ },
+ tabBar: {
+ borderColor: colors.background.default,
+ },
+ textStyle: {
+ ...(typography.sBodyMD as TextStyle),
+ fontWeight: '500',
+ },
+ loader: {
+ backgroundColor: colors.background.default,
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ TabWrapper: {
+ backgroundColor: colors.background.default,
+ flex: 1,
+ },
+ list: { flexGrow: 1 },
+ fox: {
+ width: 20,
+ height: 20,
+ },
+ itemLogoSize: {
+ width: 32,
+ height: 32,
+ },
+ containerFill: { flex: 1 },
+ badgeWrapper: {
+ alignItems: 'center',
+ justifyContent: 'center',
+ alignSelf: 'flex-start',
+ position: 'absolute',
+ top: '10%',
+ },
+ circleLogo: {
+ width: 32,
+ height: 32,
+ borderRadius: 16,
+ overflow: 'hidden',
+ borderWidth: 0.5,
+ borderColor: colors.background.alternative,
+ },
+ circleLogoPlaceholder: {
+ backgroundColor: colors.background.alternative,
+ width: 32,
+ height: 32,
+ borderRadius: 16,
+ borderWidth: 0.5,
+ borderColor: colors.background.alternative,
+ },
+ squareLogo: {
+ width: 32,
+ height: 32,
+ borderRadius: 8,
+ overflow: 'hidden',
+ borderWidth: 0.5,
+ borderColor: colors.background.alternative,
+ },
+ squareLogoPlaceholder: {
+ backgroundColor: colors.background.alternative,
+ width: 32,
+ height: 32,
+ borderRadius: 8,
+ borderWidth: 0.5,
+ borderColor: colors.background.alternative,
+ },
+ rowInsider: {
+ flex: 1,
+ flexDirection: 'row',
+ gap: 8,
+ justifyContent: 'space-between',
+ },
+ ethLogo: {
+ width: 32,
+ height: 32,
+ borderRadius: 16,
+ },
+ foxWrapper: {
+ width: 32,
+ height: 32,
+ borderRadius: 16,
+ backgroundColor: colors.background.alternative,
+ alignItems: 'center',
+ justifyContent: 'center',
+ alignSelf: 'flex-start',
+ position: 'absolute',
+ top: '25%',
+ },
+ button: {
+ marginTop: 16,
+ width: '100%',
+ alignSelf: 'center',
+ },
+ trashIconContainer: {
+ position: 'absolute',
+ paddingHorizontal: 24,
+ flex: 1,
+ flexDirection: 'row',
+ backgroundColor: colors.background.hover,
+ justifyContent: 'flex-end',
+ alignItems: 'center',
+ overflow: 'hidden',
+ height: '100%',
+ right: 0,
+ left: 0,
+ zIndex: -1,
+ },
+ });
diff --git a/app/components/UI/Notification/List/useStyles.ts b/app/components/UI/Notification/List/useStyles.ts
new file mode 100644
index 00000000000..f230632a969
--- /dev/null
+++ b/app/components/UI/Notification/List/useStyles.ts
@@ -0,0 +1,9 @@
+import { useMemo } from 'react';
+import { createStyles } from './styles';
+import { useTheme } from '../../../../util/theme';
+
+export default function useStyles() {
+ const theme = useTheme();
+ const styles = useMemo(() => createStyles(theme), [theme]);
+ return { theme, styles };
+}
diff --git a/app/components/UI/Notification/NotificationMenuItem/Content.test.tsx b/app/components/UI/Notification/NotificationMenuItem/Content.test.tsx
new file mode 100644
index 00000000000..1a65102a1ad
--- /dev/null
+++ b/app/components/UI/Notification/NotificationMenuItem/Content.test.tsx
@@ -0,0 +1,40 @@
+import React from 'react';
+import renderWithProvider from '../../../../util/test/renderWithProvider';
+
+import NotificationContent from './Content';
+
+describe('NotificationContent', () => {
+ const title = 'Welcome to the new Test!';
+ const createdAt = '2024-04-26T16:35:03.147606Z';
+ const description = {
+ start:
+ 'We are excited to announce the launch of our brand new website and app!',
+ end: 'Ethereum',
+ };
+
+ it('renders correctly', () => {
+ const { toJSON } = renderWithProvider(
+ ,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('renders title and 1 part of description', () => {
+ const titleWithTo = 'Sent 0.01 ETH to 0x10000';
+ const { getByText } = renderWithProvider(
+ ,
+ );
+
+ expect(getByText(titleWithTo)).toBeTruthy();
+ });
+});
diff --git a/app/components/UI/Notification/NotificationMenuItem/Content.tsx b/app/components/UI/Notification/NotificationMenuItem/Content.tsx
new file mode 100644
index 00000000000..65cb0882e78
--- /dev/null
+++ b/app/components/UI/Notification/NotificationMenuItem/Content.tsx
@@ -0,0 +1,47 @@
+import React from 'react';
+import { View } from 'react-native';
+import Text, {
+ TextColor,
+ TextVariant,
+} from '../../../../component-library/components/Texts/Text';
+import { NotificationMenuItem } from '../../../../util/notifications/notification-states/types/NotificationMenuItem';
+import useStyles from '../List/useStyles';
+import { formatMenuItemDate } from '../../../../util/notifications/methods';
+
+type NotificationContentProps = Pick<
+ NotificationMenuItem,
+ 'title' | 'description' | 'createdAt'
+>;
+
+function NotificationContent(props: NotificationContentProps) {
+ const { styles } = useStyles();
+
+ return (
+
+ {/* Section 1 - Title + Timestamp */}
+
+
+ {props.title}
+
+
+ {formatMenuItemDate(new Date(props.createdAt))}
+
+
+ {/* Section 2 - Left Desc + Right Desc */}
+
+
+ {props.description.start}
+
+ {props.description.end && (
+ {props.description.end}
+ )}
+
+
+ );
+}
+
+export default NotificationContent;
diff --git a/app/components/UI/Notification/NotificationMenuItem/Icon.test.tsx b/app/components/UI/Notification/NotificationMenuItem/Icon.test.tsx
new file mode 100644
index 00000000000..bc8bb54b981
--- /dev/null
+++ b/app/components/UI/Notification/NotificationMenuItem/Icon.test.tsx
@@ -0,0 +1,38 @@
+import React from 'react';
+import { Linking } from 'react-native';
+
+import renderWithProvider from '../../../../util/test/renderWithProvider';
+
+import NotificationIcon from './Icon';
+import { IconName } from '../../../../component-library/components/Icons/Icon';
+import initialBackgroundState from '../../../../util/test/initial-background-state.json';
+
+import SVG_ETH_LOGO_PATH from '../../../../component-library/components/Icons/Icon/assets/ethereum.svg';
+
+Linking.openURL = jest.fn(() => Promise.resolve('opened https://metamask.io!'));
+
+const mockInitialState = {
+ engine: {
+ backgroundState: {
+ ...initialBackgroundState,
+ },
+ },
+};
+
+describe('NotificationIcon', () => {
+ const walletNotification = {
+ badgeIcon: IconName.Send2,
+ imageUrl: SVG_ETH_LOGO_PATH,
+ };
+
+ it('matches snapshot when icon is provided', () => {
+ const { toJSON } = renderWithProvider(
+ ,
+ { state: mockInitialState },
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+});
diff --git a/app/components/UI/Notification/NotificationMenuItem/Icon.tsx b/app/components/UI/Notification/NotificationMenuItem/Icon.tsx
new file mode 100644
index 00000000000..a18b019673d
--- /dev/null
+++ b/app/components/UI/Notification/NotificationMenuItem/Icon.tsx
@@ -0,0 +1,67 @@
+import { NotificationMenuItem } from '../../../../util/notifications/notification-states/types/NotificationMenuItem';
+import React, { useMemo } from 'react';
+import useStyles from '../List/useStyles';
+import BadgeWrapper from '../../../../component-library/components/Badges/BadgeWrapper';
+import Badge, {
+ BadgeVariant,
+} from '../../../../component-library/components/Badges/Badge';
+import { BOTTOM_BADGEWRAPPER_BADGEPOSITION } from '../../../../component-library/components/Badges/BadgeWrapper/BadgeWrapper.constants';
+import RemoteImage from '../../../../components/Base/RemoteImage';
+import METAMASK_FOX from '../../../../images/fox.png';
+import { View } from 'react-native';
+
+type NotificationIconProps = Pick;
+
+function MenuIcon(props: NotificationIconProps) {
+ const { styles } = useStyles();
+
+ const menuIconStyles = {
+ style:
+ props.image?.variant === 'square' ? styles.squareLogo : styles.circleLogo,
+ placeholderStyle:
+ props.image?.variant === 'square'
+ ? styles.squareLogoPlaceholder
+ : styles.circleLogoPlaceholder,
+ };
+
+ const source = useMemo(() => {
+ if (!props.image?.url) {
+ return METAMASK_FOX;
+ }
+ if (typeof props.image.url === 'string') {
+ return { uri: props.image.url };
+ }
+ return props.image.url;
+ }, [props.image?.url]);
+
+ return (
+
+ );
+}
+
+function NotificationIcon(props: NotificationIconProps) {
+ const { styles } = useStyles();
+
+ return (
+
+
+ }
+ style={styles.badgeWrapper}
+ >
+
+
+
+ );
+}
+
+export default NotificationIcon;
diff --git a/app/components/UI/Notification/NotificationMenuItem/Root.test.tsx b/app/components/UI/Notification/NotificationMenuItem/Root.test.tsx
new file mode 100644
index 00000000000..38783f74ede
--- /dev/null
+++ b/app/components/UI/Notification/NotificationMenuItem/Root.test.tsx
@@ -0,0 +1,100 @@
+import React from 'react';
+
+import renderWithProvider from '../../../../util/test/renderWithProvider';
+import { PanGestureHandlerGestureEvent } from 'react-native-gesture-handler';
+import { useSharedValue, runOnJS, withTiming } from 'react-native-reanimated';
+
+import NotificationRoot from './Root';
+
+const children = <>>;
+const styles = {
+ wrapper: {},
+ trashIconContainer: {},
+};
+const handleOnPress = jest.fn();
+jest.mock('react-native-reanimated', () => ({
+ ...jest.requireActual('react-native-reanimated/mock'),
+ useSharedValue: jest.fn().mockImplementation((init) => ({
+ value: init,
+ withTiming: jest.fn((toValue, _, callback) => ({ toValue, callback })),
+ })),
+ runOnJS: jest.fn((callback) => callback()),
+}));
+
+describe('NotificationRoot', () => {
+ const SWIPE_THRESHOLD = -100;
+ const SCREEN_WIDTH = 300; // Assume some screen width
+
+ // Mock callbacks
+ const onDismiss = jest.fn();
+
+ // Shared values setup
+ const transX = useSharedValue(0);
+ const itemHeight = useSharedValue(100); // Assume initial height
+ const paddingVertical = useSharedValue(20); // Assume initial padding
+ const opacity = useSharedValue(1); // Assume full opacity
+ const onActive = (event: PanGestureHandlerGestureEvent) => {
+ const isSwipingLeft = event.translationX > 0;
+
+ if (isSwipingLeft) {
+ transX.value = 0;
+ return;
+ }
+
+ transX.value = event.translationX;
+ };
+ const onEnd = () => {
+ const isDismissed = transX.value < SWIPE_THRESHOLD;
+ if (isDismissed) {
+ transX.value = withTiming(-SCREEN_WIDTH);
+ itemHeight.value = withTiming(0);
+ paddingVertical.value = withTiming(0);
+ opacity.value = withTiming(0, undefined, (isFinished: boolean) => {
+ if (isFinished && onDismiss) {
+ runOnJS(onDismiss);
+ }
+ });
+ } else {
+ transX.value = withTiming(0);
+ }
+ };
+
+ it('matches snapshot', () => {
+ const { toJSON } = renderWithProvider(
+
+ {children}
+ ,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+
+ it('should dismiss the item if transX.value is less than SWIPE_THRESHOLD', () => {
+ transX.value = -150;
+ onEnd();
+ expect(transX.value).toBe(-SCREEN_WIDTH);
+ expect(itemHeight.value).toBe(0);
+ expect(paddingVertical.value).toBe(0);
+ expect(opacity.value).toBe(0);
+ expect(onDismiss).toHaveBeenCalled();
+ });
+
+ it('should set transX.value to 0 if swiping left', () => {
+ const event = {
+ translationX: 10,
+ } as unknown as PanGestureHandlerGestureEvent;
+ onActive(event);
+ expect(transX.value).toBe(0);
+ });
+
+ it('should set transX.value to event.translationX if not swiping left', () => {
+ const event = {
+ translationX: -10,
+ } as unknown as PanGestureHandlerGestureEvent;
+ onActive(event);
+ expect(transX.value).toBe(-10);
+ });
+});
diff --git a/app/components/UI/Notification/NotificationMenuItem/Root.tsx b/app/components/UI/Notification/NotificationMenuItem/Root.tsx
new file mode 100644
index 00000000000..a706da80884
--- /dev/null
+++ b/app/components/UI/Notification/NotificationMenuItem/Root.tsx
@@ -0,0 +1,119 @@
+import React from 'react';
+import { Dimensions, TouchableOpacity } from 'react-native';
+import {
+ PanGestureHandler,
+ PanGestureHandlerGestureEvent,
+ PanGestureHandlerProps,
+} from 'react-native-gesture-handler';
+import Animated, {
+ runOnJS,
+ useAnimatedGestureHandler,
+ useSharedValue,
+ useAnimatedStyle,
+ withTiming,
+} from 'react-native-reanimated';
+import Icon, {
+ IconColor,
+ IconName,
+ IconSize,
+} from '../../../../component-library/components/Icons/Icon';
+import { NotificationListStyles } from '../List/styles';
+
+interface NotificationRootProps
+ extends Pick {
+ children: React.ReactNode;
+ styles: NotificationListStyles;
+ handleOnPress: () => void;
+ onDismiss?: () => void;
+}
+
+const { width: SCREEN_WIDTH } = Dimensions.get('window');
+const DELETE_BUTTON_WIDTH = -SCREEN_WIDTH * 0.3;
+const SWIPE_THRESHOLD = DELETE_BUTTON_WIDTH;
+
+function NotificationRoot({
+ children,
+ handleOnPress,
+ styles,
+ onDismiss,
+ simultaneousHandlers,
+}: NotificationRootProps) {
+ const transX = useSharedValue(0);
+ const itemHeight = useSharedValue();
+ const paddingVertical = useSharedValue(10);
+ const opacity = useSharedValue(1);
+
+ const panGesture = useAnimatedGestureHandler({
+ onActive: (event: PanGestureHandlerGestureEvent) => {
+ const isSwipingLeft = event.translationX > 0;
+
+ if (isSwipingLeft) {
+ transX.value = 0;
+ return;
+ }
+
+ transX.value = event.translationX;
+ },
+ onEnd: () => {
+ const isDismissed = transX.value < SWIPE_THRESHOLD;
+ if (isDismissed) {
+ transX.value = withTiming(-SCREEN_WIDTH);
+ itemHeight.value = withTiming(0);
+ paddingVertical.value = withTiming(0);
+ opacity.value = withTiming(0, undefined, (isFinished: boolean) => {
+ if (isFinished && onDismiss) {
+ runOnJS(onDismiss);
+ }
+ });
+ } else {
+ transX.value = withTiming(0);
+ }
+ },
+ });
+
+ const rChildrenStyle = useAnimatedStyle(() => ({
+ transform: [{ translateX: transX.value }],
+ ...styles.container,
+ }));
+
+ const rIconStyle = useAnimatedStyle(() => {
+ const opct = withTiming(transX.value < SWIPE_THRESHOLD ? 1 : 0, {
+ duration: 300,
+ });
+
+ return { opacity: opct };
+ });
+
+ const rContainerStyle = useAnimatedStyle(() => ({
+ height: itemHeight.value,
+ paddingVertical: paddingVertical.value,
+ opacity: opacity.value,
+ }));
+
+ return (
+
+
+
+
+ {children}
+
+
+
+
+
+
+
+ );
+}
+
+export default NotificationRoot;
diff --git a/app/components/UI/Notification/NotificationMenuItem/__snapshots__/Content.test.tsx.snap b/app/components/UI/Notification/NotificationMenuItem/__snapshots__/Content.test.tsx.snap
new file mode 100644
index 00000000000..9e11e42af79
--- /dev/null
+++ b/app/components/UI/Notification/NotificationMenuItem/__snapshots__/Content.test.tsx.snap
@@ -0,0 +1,96 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`NotificationContent renders correctly 1`] = `
+
+
+
+ Welcome to the new Test!
+
+
+ Apr 26
+
+
+
+
+ We are excited to announce the launch of our brand new website and app!
+
+
+ Ethereum
+
+
+
+`;
diff --git a/app/components/UI/Notification/NotificationMenuItem/__snapshots__/Icon.test.tsx.snap b/app/components/UI/Notification/NotificationMenuItem/__snapshots__/Icon.test.tsx.snap
new file mode 100644
index 00000000000..4f3b4715133
--- /dev/null
+++ b/app/components/UI/Notification/NotificationMenuItem/__snapshots__/Icon.test.tsx.snap
@@ -0,0 +1,110 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`NotificationIcon matches snapshot when icon is provided 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/app/components/UI/Notification/NotificationMenuItem/__snapshots__/Root.test.tsx.snap b/app/components/UI/Notification/NotificationMenuItem/__snapshots__/Root.test.tsx.snap
new file mode 100644
index 00000000000..d055b2e865b
--- /dev/null
+++ b/app/components/UI/Notification/NotificationMenuItem/__snapshots__/Root.test.tsx.snap
@@ -0,0 +1,59 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`NotificationRoot matches snapshot 1`] = `
+
+
+
+
+
+
+
+
+`;
diff --git a/app/components/UI/Notification/NotificationMenuItem/index.tsx b/app/components/UI/Notification/NotificationMenuItem/index.tsx
new file mode 100644
index 00000000000..99596dc7970
--- /dev/null
+++ b/app/components/UI/Notification/NotificationMenuItem/index.tsx
@@ -0,0 +1,10 @@
+/* eslint-disable import/prefer-default-export */
+import NotificationRoot from './Root';
+import NotificationIcon from './Icon';
+import NotificationContent from './Content';
+
+export const NotificationMenuItem = {
+ Root: NotificationRoot,
+ Icon: NotificationIcon,
+ Content: NotificationContent,
+};
diff --git a/app/components/UI/Notification/SwitchLoadingModal/Loader.tsx b/app/components/UI/Notification/SwitchLoadingModal/Loader.tsx
new file mode 100644
index 00000000000..6b4a984b2fa
--- /dev/null
+++ b/app/components/UI/Notification/SwitchLoadingModal/Loader.tsx
@@ -0,0 +1,95 @@
+import React from 'react';
+import { StyleSheet, View } from 'react-native';
+import PropTypes from 'prop-types';
+import Device from '../../../../util/device';
+import { useTheme } from '../../../../util/theme';
+import Text, {
+ TextVariant,
+} from '../../../../component-library/components/Texts/Text';
+import Icon, {
+ IconSize,
+ IconName,
+ IconColor,
+} from '../../../../component-library/components/Icons/Icon';
+
+import Spinner from '../../AnimatedSpinner';
+import Button, {
+ ButtonSize,
+ ButtonVariants,
+ ButtonWidthTypes,
+} from '../../../../component-library/components/Buttons/Button';
+import { strings } from '../../../../../locales/i18n';
+
+const createStyles = (colors) =>
+ StyleSheet.create({
+ root: {
+ backgroundColor: colors.background.default,
+ borderColor: colors.border.default,
+ borderWidth: 2,
+ borderRadius: 16,
+ paddingBottom: Device.isIphoneX() ? 24 : 18,
+ minHeight: 120,
+ margin: 16,
+ },
+ spinnerWrapper: {
+ alignItems: 'center',
+ marginVertical: 12,
+ },
+ text: {
+ lineHeight: 20,
+ paddingHorizontal: 24,
+ marginVertical: 12,
+ width: '100%',
+ },
+ button: {
+ alignSelf: 'center',
+ },
+ });
+
+const Loader = ({
+ loadingText,
+ onDismiss,
+ errorText,
+}: {
+ loadingText: string;
+ onDismiss: () => void;
+ errorText?: string;
+}) => {
+ const { colors } = useTheme();
+ const styles = createStyles(colors);
+
+ return (
+
+
+ {!errorText ? (
+
+ ) : (
+
+ )}
+
+
+ {errorText || loadingText}
+
+ {!!errorText && (
+
+ )}
+
+ );
+};
+
+Loader.propTypes = {
+ loadingText: PropTypes.string,
+};
+
+export default Loader;
diff --git a/app/components/UI/Notification/SwitchLoadingModal/LoaderModal.tsx b/app/components/UI/Notification/SwitchLoadingModal/LoaderModal.tsx
new file mode 100644
index 00000000000..3d4aea72fd0
--- /dev/null
+++ b/app/components/UI/Notification/SwitchLoadingModal/LoaderModal.tsx
@@ -0,0 +1,42 @@
+import React from 'react';
+import { StyleSheet } from 'react-native';
+import Modal from 'react-native-modal';
+import { useTheme } from '../../../../util/theme';
+
+export interface LoaderModalProps {
+ isVisible: boolean;
+ onCancel: () => void;
+ children: React.ReactNode;
+}
+
+const styles = StyleSheet.create({
+ bottomModal: {
+ justifyContent: 'flex-end',
+ marginHorizontal: 0,
+ },
+});
+
+const LoaderModal = (props: LoaderModalProps) => {
+ const { colors } = useTheme();
+
+ return (
+
+ {props.children}
+
+ );
+};
+
+export default LoaderModal;
diff --git a/app/components/UI/Notification/SwitchLoadingModal/SwitchLoadingModal.tsx b/app/components/UI/Notification/SwitchLoadingModal/SwitchLoadingModal.tsx
new file mode 100644
index 00000000000..2c86a38c13b
--- /dev/null
+++ b/app/components/UI/Notification/SwitchLoadingModal/SwitchLoadingModal.tsx
@@ -0,0 +1,35 @@
+import React, { useCallback, useEffect, useState } from 'react';
+import LoaderModal from './LoaderModal';
+import Loader from './Loader';
+
+const SwitchLoadingModal = ({
+ loading,
+ loadingText,
+ error,
+}: {
+ loading: boolean;
+ loadingText: string;
+ error?: string;
+}) => {
+ const [isVisible, setIsVisible] = useState(false);
+
+ useEffect(() => {
+ setIsVisible(loading || !!error);
+ }, [loading, error]);
+
+ const handleVisibility = useCallback(() => {
+ setIsVisible(false);
+ }, []);
+
+ return (
+
+
+
+ );
+};
+
+export default SwitchLoadingModal;
diff --git a/app/components/UI/Notification/SwitchLoadingModal/index.ts b/app/components/UI/Notification/SwitchLoadingModal/index.ts
new file mode 100644
index 00000000000..479aa17d978
--- /dev/null
+++ b/app/components/UI/Notification/SwitchLoadingModal/index.ts
@@ -0,0 +1 @@
+export { default } from './SwitchLoadingModal';
diff --git a/app/components/UI/ProfileSyncing/ProfileSyncing.styles.ts b/app/components/UI/ProfileSyncing/ProfileSyncing.styles.ts
new file mode 100644
index 00000000000..1114594f0de
--- /dev/null
+++ b/app/components/UI/ProfileSyncing/ProfileSyncing.styles.ts
@@ -0,0 +1,14 @@
+import { StyleSheet } from 'react-native';
+const styles = StyleSheet.create({
+ setting: {
+ marginVertical: 16,
+ },
+ heading: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ paddingBottom: 8,
+ },
+});
+
+export default styles;
diff --git a/app/components/UI/ProfileSyncing/ProfileSyncing.test.tsx b/app/components/UI/ProfileSyncing/ProfileSyncing.test.tsx
new file mode 100644
index 00000000000..e0bd337679a
--- /dev/null
+++ b/app/components/UI/ProfileSyncing/ProfileSyncing.test.tsx
@@ -0,0 +1,35 @@
+// Third party dependencies.
+import React from 'react';
+
+// Internal dependencies.
+import ProfileSyncingComponent from './ProfileSyncing';
+import renderWithProvider from '../../../util/test/renderWithProvider';
+
+const MOCK_STORE_STATE = {
+ engine: {
+ backgroundState: {
+ UserStorageController: {
+ isProfileSyncingEnabled: true,
+ },
+ NotificationServicesController: {
+ isNotificationServicesEnabled: true,
+ },
+ },
+ },
+};
+
+jest.mock('react-redux', () => ({
+ ...jest.requireActual('react-redux'),
+ useSelector: (fn: (state: unknown) => unknown) => fn(MOCK_STORE_STATE),
+}));
+
+const handleSwitchToggle = jest.fn();
+
+describe('ProfileSyncing', () => {
+ it('should render correctly', () => {
+ const { toJSON } = renderWithProvider(
+ ,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+});
diff --git a/app/components/UI/ProfileSyncing/ProfileSyncing.tsx b/app/components/UI/ProfileSyncing/ProfileSyncing.tsx
new file mode 100644
index 00000000000..27b7d610e6c
--- /dev/null
+++ b/app/components/UI/ProfileSyncing/ProfileSyncing.tsx
@@ -0,0 +1,69 @@
+import React, { useEffect } from 'react';
+import { useSelector } from 'react-redux';
+
+import { View, Switch, Linking } from 'react-native';
+import { RootState } from '../../../reducers';
+
+import Text, {
+ TextVariant,
+ TextColor,
+} from '../../../component-library/components/Texts/Text';
+import { useTheme } from '../../../util/theme';
+import { strings } from '../../../../locales/i18n';
+import styles from './ProfileSyncing.styles';
+import { ProfileSyncingComponentProps } from './ProfileSyncing.types';
+import AppConstants from '../../../core/AppConstants';
+import { selectIsProfileSyncingEnabled } from '../../../selectors/notifications';
+import { useProfileSyncing } from '../../../util/notifications/hooks/useProfileSyncing';
+
+export default function ProfileSyncingComponent({
+ handleSwitchToggle,
+}: Readonly) {
+ const theme = useTheme();
+ const { colors } = theme;
+ const { disableProfileSyncing } = useProfileSyncing();
+ const isProfileSyncingEnabled = useSelector(selectIsProfileSyncingEnabled);
+ const isBasicFunctionalityEnabled = useSelector(
+ (state: RootState) => state?.settings?.basicFunctionalityEnabled,
+ );
+
+ const handleLink = () => {
+ Linking.openURL(AppConstants.URLS.PRIVACY_POLICY_2024);
+ };
+
+ useEffect(() => {
+ async function disableProfileSyncingOnLogout() {
+ if (!isBasicFunctionalityEnabled) {
+ await disableProfileSyncing();
+ }
+ }
+ disableProfileSyncingOnLogout();
+ }, [disableProfileSyncing, isBasicFunctionalityEnabled]);
+
+ return (
+
+
+
+ {strings('profile_sync.title')}
+
+
+
+
+ {strings('profile_sync.enable_description')}
+
+ {strings('profile_sync.enable_privacy_link')}
+
+
+
+ );
+}
diff --git a/app/components/UI/ProfileSyncing/ProfileSyncing.types.ts b/app/components/UI/ProfileSyncing/ProfileSyncing.types.ts
new file mode 100644
index 00000000000..1fe29f86dcd
--- /dev/null
+++ b/app/components/UI/ProfileSyncing/ProfileSyncing.types.ts
@@ -0,0 +1,3 @@
+export interface ProfileSyncingComponentProps {
+ handleSwitchToggle: () => void;
+}
diff --git a/app/components/UI/ProfileSyncing/ProfileSyncingModal/ProfileSyncingModal.styles.ts b/app/components/UI/ProfileSyncing/ProfileSyncingModal/ProfileSyncingModal.styles.ts
new file mode 100644
index 00000000000..9d2914c4fe4
--- /dev/null
+++ b/app/components/UI/ProfileSyncing/ProfileSyncingModal/ProfileSyncingModal.styles.ts
@@ -0,0 +1,52 @@
+// Third party dependencies.
+import { StyleSheet, TextStyle } from 'react-native';
+import { typography } from '@metamask/design-tokens';
+import { ThemeColors } from '@metamask/design-tokens/dist/types/js/themes/types';
+
+/**
+ * Style sheet function for AmbiguousAddressSheet component.
+ *
+ * @returns StyleSheet object.
+ */
+
+export default (colors: ThemeColors) =>
+ StyleSheet.create({
+ container: {
+ justifyContent: 'center',
+ padding: 16,
+ alignSelf: 'center',
+ },
+ heading: {
+ ...(typography.sHeadingMD as TextStyle),
+ color: colors.text.default,
+ },
+ description: {
+ alignSelf: 'flex-start',
+ paddingTop: 16,
+ paddingBottom: 10,
+ },
+ subtitle: {
+ ...(typography.sBodyMD as TextStyle),
+ color: colors.text.default,
+ alignSelf: 'flex-start',
+ paddingTop: 16,
+ paddingBottom: 10,
+ },
+ buttonsContainer: {
+ flexDirection: 'row',
+ paddingTop: 24,
+ },
+ button: {
+ flex: 1,
+ },
+ bullets: { paddingHorizontal: 8 },
+ bullet: {
+ alignSelf: 'flex-start',
+ },
+ title: {
+ textAlign: 'center',
+ },
+ bottom: { paddingTop: 20 },
+ spacer: { width: 20 },
+ icon: { alignSelf: 'center', marginBottom: 10 },
+ });
diff --git a/app/components/UI/ProfileSyncing/ProfileSyncingModal/ProfileSyncingModal.test.tsx b/app/components/UI/ProfileSyncing/ProfileSyncingModal/ProfileSyncingModal.test.tsx
new file mode 100644
index 00000000000..73c5b4c66ed
--- /dev/null
+++ b/app/components/UI/ProfileSyncing/ProfileSyncingModal/ProfileSyncingModal.test.tsx
@@ -0,0 +1,65 @@
+// Third party dependencies.
+import React from 'react';
+
+// Internal dependencies.
+import ProfileSyncingModal from './ProfileSyncingModal';
+import renderWithProvider from '../../../../util/test/renderWithProvider';
+import { useNavigation } from '@react-navigation/native';
+
+jest.mock('react-native-safe-area-context', () => {
+ const inset = { top: 0, right: 0, bottom: 0, left: 0 };
+ const frame = { width: 0, height: 0, x: 0, y: 0 };
+ return {
+ SafeAreaProvider: jest.fn().mockImplementation(({ children }) => children),
+ SafeAreaConsumer: jest
+ .fn()
+ .mockImplementation(({ children }) => children(inset)),
+ useSafeAreaInsets: jest.fn().mockImplementation(() => inset),
+ useSafeAreaFrame: jest.fn().mockImplementation(() => frame),
+ };
+});
+
+const MOCK_STORE_STATE = {
+ engine: {
+ backgroundState: {
+ NotificationServicesController: {
+ isNotificationServicesEnabled: true,
+ },
+ UserStorageController: {
+ isProfileSyncingEnabled: true,
+ },
+ },
+ },
+};
+
+jest.mock('react-redux', () => ({
+ ...jest.requireActual('react-redux'),
+ useSelector: (fn: (state: unknown) => unknown) => fn(MOCK_STORE_STATE),
+}));
+
+jest.mock('@react-navigation/native', () => {
+ const actualReactNavigation = jest.requireActual('@react-navigation/native');
+ return {
+ ...actualReactNavigation,
+ useNavigation: () => ({
+ navigate: jest.fn(),
+ setOptions: jest.fn(),
+ goBack: jest.fn(),
+ reset: jest.fn(),
+ dangerouslyGetParent: () => ({
+ pop: jest.fn(),
+ }),
+ }),
+ };
+});
+
+describe('ProfileSyncingModal', () => {
+ it('should render correctly', () => {
+ const { toJSON } = renderWithProvider(
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ //@ts-ignore
+ ,
+ );
+ expect(toJSON()).toMatchSnapshot();
+ });
+});
diff --git a/app/components/UI/ProfileSyncing/ProfileSyncingModal/ProfileSyncingModal.tsx b/app/components/UI/ProfileSyncing/ProfileSyncingModal/ProfileSyncingModal.tsx
new file mode 100644
index 00000000000..2f1ba7a0008
--- /dev/null
+++ b/app/components/UI/ProfileSyncing/ProfileSyncingModal/ProfileSyncingModal.tsx
@@ -0,0 +1,132 @@
+// Third party dependencies.
+import React, { useRef } from 'react';
+import { View } from 'react-native';
+import { useSelector } from 'react-redux';
+
+// External dependencies.
+import BottomSheet, {
+ BottomSheetRef,
+} from '../../../../component-library/components/BottomSheets/BottomSheet';
+import { strings } from '../../../../../locales/i18n';
+import Text, {
+ TextVariant,
+} from '../../../../component-library/components/Texts/Text';
+import { useTheme } from '../../../../util/theme';
+import Button, {
+ ButtonSize,
+ ButtonVariants,
+} from '../../../../component-library/components/Buttons/Button';
+import Checkbox from '../../../../component-library/components/Checkbox/Checkbox';
+import createStyles from './ProfileSyncingModal.styles';
+import Icon, {
+ IconColor,
+ IconName,
+ IconSize,
+} from '../../../../component-library/components/Icons/Icon';
+import { selectIsProfileSyncingEnabled } from '../../../../selectors/notifications';
+import { useProfileSyncing } from '../../../../util/notifications/hooks/useProfileSyncing';
+import { useDisableNotifications } from '../../../../util/notifications/hooks/useNotifications';
+
+const ProfileSyncingModal = () => {
+ const { colors } = useTheme();
+ const styles = createStyles(colors);
+ const bottomSheetRef = useRef(null);
+ const [isChecked, setIsChecked] = React.useState(false);
+ const { disableProfileSyncing } = useProfileSyncing();
+ const { disableNotifications } = useDisableNotifications();
+
+ const isProfileSyncingEnabled = useSelector(selectIsProfileSyncingEnabled);
+
+ // TODO: Handle errror/loading states from enabling/disabling profile syncing
+ const closeBottomSheet = () => {
+ bottomSheetRef.current?.onCloseBottomSheet(async () => {
+ if (isProfileSyncingEnabled) {
+ await disableProfileSyncing();
+ await disableNotifications();
+ }
+ });
+ };
+
+ const handleSwitchToggle = () => {
+ closeBottomSheet();
+ };
+
+ const handleCancel = () => {
+ bottomSheetRef.current?.onCloseBottomSheet();
+ };
+
+ const turnContent = !isProfileSyncingEnabled
+ ? {
+ icon: {
+ name: IconName.Check,
+ color: IconColor.Success,
+ },
+ bottomSheetTitle: strings('profile_sync.bottomSheetTurnOn'),
+ bottomSheetMessage: strings('profile_sync.enable_description'),
+ bottomSheetCTA: strings('default_settings.sheet.buttons.turn_on'),
+ }
+ : {
+ icon: {
+ name: IconName.Danger,
+ color: IconColor.Error,
+ },
+ bottomSheetTitle: strings('profile_sync.bottomSheetTurnOff'),
+ bottomSheetMessage: strings('profile_sync.disable_warning'),
+ bottomSheetCTA: strings('default_settings.sheet.buttons.turn_off'),
+ };
+
+ const renderTurnOnOFfContent = () => (
+
+
+
+ {turnContent.bottomSheetTitle}
+
+
+ {turnContent.bottomSheetMessage}
+
+
+ {isProfileSyncingEnabled && (
+ setIsChecked(!isChecked)}
+ />
+ )}
+
+
+
+
+
+
+
+ );
+
+ return (
+ {renderTurnOnOFfContent()}
+ );
+};
+
+export default ProfileSyncingModal;
diff --git a/app/components/UI/ProfileSyncing/ProfileSyncingModal/__snapshots__/ProfileSyncingModal.test.tsx.snap b/app/components/UI/ProfileSyncing/ProfileSyncingModal/__snapshots__/ProfileSyncingModal.test.tsx.snap
new file mode 100644
index 00000000000..5c1600db867
--- /dev/null
+++ b/app/components/UI/ProfileSyncing/ProfileSyncingModal/__snapshots__/ProfileSyncingModal.test.tsx.snap
@@ -0,0 +1,334 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ProfileSyncingModal should render correctly 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+ Turning off Profile Sync
+
+
+ If you turn off profile sync, you won’t be able to receive notifications.
+
+
+
+
+
+
+ I understand and want to continue
+
+
+
+
+
+
+ Cancel
+
+
+
+
+
+ Turn off
+
+
+
+
+
+
+
+
+`;
diff --git a/app/components/UI/ProfileSyncing/__snapshots__/ProfileSyncing.test.tsx.snap b/app/components/UI/ProfileSyncing/__snapshots__/ProfileSyncing.test.tsx.snap
new file mode 100644
index 00000000000..cf1b7ec1739
--- /dev/null
+++ b/app/components/UI/ProfileSyncing/__snapshots__/ProfileSyncing.test.tsx.snap
@@ -0,0 +1,92 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ProfileSyncing should render correctly 1`] = `
+
+
+
+ Profile Sync
+
+
+
+
+ Creates a profile that MetaMask uses to sync some settings among your devices. This is required to get notifications.
+
+ Learn how we protect your privacy
+
+
+
+`;
diff --git a/app/constants/navigation/Routes.ts b/app/constants/navigation/Routes.ts
index 21393b7ef06..82484b34a18 100644
--- a/app/constants/navigation/Routes.ts
+++ b/app/constants/navigation/Routes.ts
@@ -85,6 +85,7 @@ const Routes = {
ACCOUNT_SELECTOR: 'AccountSelector',
AMBIGUOUS_ADDRESS: 'AmbiguousAddress',
BASIC_FUNCTIONALITY: 'BasicFunctionality',
+ PROFILE_SYNCING: 'ProfileSyncing',
SDK_LOADING: 'SDKLoading',
SDK_FEEDBACK: 'SDKFeedback',
DATA_COLLECTION: 'DataCollection',
diff --git a/app/util/notifications/methods/common.ts b/app/util/notifications/methods/common.ts
index d89bb000045..590595b073e 100644
--- a/app/util/notifications/methods/common.ts
+++ b/app/util/notifications/methods/common.ts
@@ -461,3 +461,29 @@ export const getAmount = (
return formatAmount(numericAmount, options);
};
+
+/**
+ * Converts a token amount and its USD conversion rate to a formatted USD string.
+ *
+ * This function first converts the token amount from its smallest unit based on the provided decimals
+ * to a human-readable format. It then multiplies this amount by the USD conversion rate to get the
+ * equivalent amount in USD, and formats this USD amount into a readable string.
+ *
+ * @param amount - The token amount in its smallest unit as a string.
+ * @param decimals - The number of decimals the token uses.
+ * @param usd - The current USD conversion rate for the token.
+ * @returns The formatted USD amount as a string. If any input is invalid, returns an empty string.
+ */
+export const getUsdAmount = (amount: string, decimals: string, usd: string) => {
+ if (!amount || !decimals || !usd) {
+ return '';
+ }
+
+ const amountInEther = calcTokenAmount(
+ amount,
+ parseFloat(decimals),
+ ).toNumber();
+ const numericAmount = parseFloat(`${amountInEther}`) * parseFloat(usd);
+
+ return formatAmount(numericAmount);
+};
diff --git a/app/util/notifications/notification-states/token-amounts.ts b/app/util/notifications/notification-states/token-amounts.ts
index 0be37c8103b..5d4eb730a8d 100644
--- a/app/util/notifications/notification-states/token-amounts.ts
+++ b/app/util/notifications/notification-states/token-amounts.ts
@@ -1,4 +1,4 @@
-import { getAmount, getUsdAmount } from '../notification.util';
+import { getAmount, getUsdAmount } from '../methods/common';
export function getTokenAmount(token: {
amount: string;