diff --git a/app/components/UI/Notification/BaseNotification/__snapshots__/index.test.jsx.snap b/app/components/UI/Notification/BaseNotification/__snapshots__/index.test.jsx.snap
new file mode 100644
index 00000000000..fc9fc68fc42
--- /dev/null
+++ b/app/components/UI/Notification/BaseNotification/__snapshots__/index.test.jsx.snap
@@ -0,0 +1,1741 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`BaseNotification gets icon correctly for each status 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Testing Title
+
+
+ Testing description
+
+
+
+
+
+
+`;
+
+exports[`BaseNotification gets icon correctly for each status 2`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Testing Title
+
+
+ Testing description
+
+
+
+
+
+
+`;
+
+exports[`BaseNotification gets icon correctly for each status 3`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Testing Title
+
+
+ Testing description
+
+
+
+
+
+
+`;
+
+exports[`BaseNotification gets icon correctly for each status 4`] = `
+
+
+
+
+
+
+
+
+
+
+ Testing Title
+
+
+ Testing description
+
+
+
+
+
+
+`;
+
+exports[`BaseNotification gets icon correctly for each status 5`] = `
+
+
+
+
+
+
+
+
+
+
+ Testing Title
+
+
+ Testing description
+
+
+
+
+
+
+`;
+
+exports[`BaseNotification gets icon correctly for each status 6`] = `
+
+
+
+
+
+
+
+
+
+
+ Testing Title
+
+
+ Testing description
+
+
+
+
+
+
+`;
+
+exports[`BaseNotification gets icon correctly for each status 7`] = `
+
+
+
+
+
+
+
+
+
+
+ Testing Title
+
+
+ Testing description
+
+
+
+
+
+
+`;
+
+exports[`BaseNotification gets icon correctly for each status 8`] = `
+
+
+
+
+
+
+
+
+
+
+ Testing Title
+
+
+ Testing description
+
+
+
+
+
+
+`;
+
+exports[`BaseNotification gets icon correctly for each status 9`] = `
+
+
+
+
+
+
+
+
+
+
+ Testing Title
+
+
+ Testing description
+
+
+
+
+
+
+`;
+
+exports[`BaseNotification gets icon correctly for each status 10`] = `
+
+
+
+
+
+
+
+
+
+
+ Testing Title
+
+
+ Testing description
+
+
+
+
+
+
+`;
diff --git a/app/components/UI/Notification/BaseNotification/index.js b/app/components/UI/Notification/BaseNotification/index.js
index 96a11304d75..79b1a7dfc2e 100644
--- a/app/components/UI/Notification/BaseNotification/index.js
+++ b/app/components/UI/Notification/BaseNotification/index.js
@@ -72,6 +72,7 @@ export const getIcon = (status, colors, styles) => {
case 'success':
case 'received':
case 'received_payment':
+ case 'eth_received':
return (
{
return strings('notifications.cancelled_title');
case 'error':
return strings('notifications.error_title');
+ case 'eth_received':
+ return strings('notifications.eth_received_title');
}
};
-const getDescription = (status, { amount = null }) => {
- if (amount) {
- return strings(`notifications.${status}_message`, { amount });
+export const getDescription = (status, { amount = null, type = null }) => {
+ if (amount && typeof amount !== 'object') {
+ return strings(`notifications.${type}_${status}_message`, { amount });
}
return strings(`notifications.${status}_message`);
};
diff --git a/app/components/UI/Notification/BaseNotification/index.test.jsx b/app/components/UI/Notification/BaseNotification/index.test.jsx
new file mode 100644
index 00000000000..11cb5aa6a17
--- /dev/null
+++ b/app/components/UI/Notification/BaseNotification/index.test.jsx
@@ -0,0 +1,47 @@
+import React from 'react';
+import BaseNotification, { getDescription } from './';
+import renderWithProvider from '../../../../util/test/renderWithProvider';
+import { strings } from '../../../../../locales/i18n';
+
+const defaultProps = [
+ { status: 'pending', data: { description: 'Testing description', title: 'Testing Title' } },
+ { status: 'pending_withdrawal', data: { description: 'Testing description', title: 'Testing Title' } },
+ { status: 'pending_deposit', data: { description: 'Testing description', title: 'Testing Title' } },
+ { status: 'success_deposit', data: { description: 'Testing description', title: 'Testing Title' } },
+ { status: 'success_withdrawal', data: { description: 'Testing description', title: 'Testing Title' } },
+ { status: 'received', data: { description: 'Testing description', title: 'Testing Title' } },
+ { status: 'received_payment', data: { description: 'Testing description', title: 'Testing Title' } },
+ { status: 'eth_received', data: { description: 'Testing description', title: 'Testing Title' } },
+ { status: 'cancelled', data: { description: 'Testing description', title: 'Testing Title' } },
+ { status: 'error', data: { description: 'Testing description', title: 'Testing Title' } },
+ ];
+
+describe('BaseNotification', () => {
+ it('gets icon correctly for each status', () => {
+ defaultProps.forEach(({ status, data}) => {
+ const { toJSON } = renderWithProvider();
+ expect(toJSON()).toMatchSnapshot();
+ });
+ });
+
+ it('gets titles correctly for each status', () => {
+ defaultProps.forEach(({ status }) => {
+ const { getByText } = renderWithProvider();
+ expect(getByText(strings(`notifications.${status}_title`))).toBeTruthy();
+ });
+ });
+
+ it('gets descriptions correctly for if they are provided', () => {
+ defaultProps.forEach(({ status, data }) => {
+ const { getByText } = renderWithProvider();
+ expect(getByText(data.description)).toBeTruthy();
+ });
+ });
+
+ it('constructs the correct description using getDescription when no description is provided', () => {
+ defaultProps.forEach(({ status }) => {
+ const { getByText } = renderWithProvider();
+ expect(getByText(getDescription(status, {}))).toBeTruthy();
+ });
+ });
+});
diff --git a/app/core/NotificationManager.js b/app/core/NotificationManager.js
index 568d3e97d6d..22af910e519 100644
--- a/app/core/NotificationManager.js
+++ b/app/core/NotificationManager.js
@@ -3,18 +3,16 @@
import Engine from './Engine';
import { hexToBN, renderFromWei } from '../util/number';
import Device from '../util/device';
-import notifee from '@notifee/react-native';
-import { STORAGE_IDS } from '../util/notifications/settings/storage/constants';
import { strings } from '../../locales/i18n';
import { AppState } from 'react-native';
+import NotificationsService from '../util/notifications/services/NotificationService';
import {
NotificationTransactionTypes,
- isNotificationsFeatureEnabled,
-
+ ChannelId,
} from '../util/notifications';
-import { safeToChecksumAddress } from '../util/address';
+import { safeToChecksumAddress, formatAddress } from '../util/address';
import ReviewManager from './ReviewManager';
import { selectChainId, selectTicker } from '../selectors/networkController';
import { store } from '../store';
@@ -76,9 +74,17 @@ export const constructTitleAndMessage = (notification) => {
amount: notification.transaction.amount,
});
break;
+ case NotificationTransactionTypes.eth_received:
+ title = strings('notifications.default_message_title');
+ message = strings('notifications.eth_received_message', {
+ amount: notification.transaction.amount.usd,
+ ticker: 'USD',
+ address: formatAddress(notification.transaction.from, 'short'),
+ });
+ break;
default:
- title = notification.data.title || strings('notifications.default_message_title');
- message = notification.data.shortDescription || strings('notifications.default_message_description');
+ title = notification?.data?.title || strings('notifications.default_message_title');
+ message = notification?.data?.shortDescription || strings('notifications.default_message_description');
break;
}
return { title, message };
@@ -141,8 +147,7 @@ class NotificationManager {
);
};
- // TODO: Refactor this method to use notifee's channels in combination with MM auth
- _showNotification(data, channelId = STORAGE_IDS.ANDROID_DEFAULT_CHANNEL_ID) {
+ _showNotification = async (data) => {
if (this._backgroundMode) {
const { title, message } = constructTitleAndMessage(data);
const id = data?.transaction?.id || '';
@@ -151,26 +156,13 @@ class NotificationManager {
}
const pushData = {
+ channelId: ChannelId.DEFAULT_NOTIFICATION_CHANNEL_ID,
title,
body: message,
- android: {
- lightUpScreen: true,
- channelId,
- smallIcon: 'ic_notification_small',
- largeIcon: 'ic_notification',
- pressAction: {
- id: 'default',
- launchActivity: 'com.metamask.ui.MainActivity',
- },
- },
- ios: {
- foregroundPresentationOptions: {
- alert: true,
- sound: true,
- badge: true,
- banner: true,
- list: true,
- },
+ data: {
+ ...data?.transaction,
+ action: 'tx',
+ id,
},
};
@@ -179,9 +171,9 @@ class NotificationManager {
if (Device.isAndroid()) {
pushData.tag = JSON.stringify(extraData);
} else {
- pushData.userInfo = extraData; // check if is still needed
+ pushData.userInfo = extraData;
}
- isNotificationsFeatureEnabled() && notifee.displayNotification(pushData);
+ await NotificationsService.displayNotification(pushData);
} else {
this._showTransactionNotification({
autodismiss: data.duration,
@@ -189,7 +181,7 @@ class NotificationManager {
status: data.type,
});
}
- }
+ };
_failedCallback = (transactionMeta) => {
// If it fails we hide the pending tx notification
diff --git a/app/core/NotificationsManager.test.ts b/app/core/NotificationsManager.test.ts
index 2af79c085b5..fb8e9a47d28 100644
--- a/app/core/NotificationsManager.test.ts
+++ b/app/core/NotificationsManager.test.ts
@@ -9,6 +9,10 @@ interface NavigationMock {
navigate: jest.Mock;
}
+jest.mock('react-redux', () => ({
+ useSelector: jest.fn(),
+}));
+
jest.unmock('./NotificationManager');
const mockNavigate: jest.Mock = jest.fn();
@@ -101,7 +105,7 @@ describe('NotificationManager', () => {
'cancelled',
];
selectedNotificationTypes.forEach((type) => {
- it(`should construct title and message for ${type}`, () => {
+ it(`constructs title and message for ${type}`, () => {
const { title, message } = constructTitleAndMessage({
type: NotificationTransactionTypes[type],
});
@@ -109,5 +113,14 @@ describe('NotificationManager', () => {
expect(title).toBe(strings(`notifications.${type}_title`));
expect(message).toBe(strings(`notifications.${type}_message`));
});
+
+ it('constructs default title and message for unknown type', () => {
+ const { title, message } = constructTitleAndMessage({
+ type: 'unknown',
+ });
+
+ expect(title).toBe(strings('notifications.default_message_title'));
+ expect(message).toBe(strings('notifications.default_message_description'));
+ });
});
});
diff --git a/app/util/notifications/hooks/index.test.ts b/app/util/notifications/hooks/index.test.ts
deleted file mode 100644
index ff4b9fd77aa..00000000000
--- a/app/util/notifications/hooks/index.test.ts
+++ /dev/null
@@ -1,109 +0,0 @@
-import { act, renderHook } from '@testing-library/react-hooks';
-// eslint-disable-next-line import/no-namespace
-import * as constants from '../constants';
-import useNotificationHandler from './index';
-import { NavigationProp, ParamListBase } from '@react-navigation/native';
-import Routes from '../../../constants/navigation/Routes';
-import { Notification } from '../../../util/notifications/types';
-import { TRIGGER_TYPES } from '../constants';
-import NotificationsService from '../services/NotificationService';
-
-jest.mock('@notifee/react-native', () => ({
- setBadgeCount: jest.fn(),
- decrementBadgeCount: jest.fn(),
- onForegroundEvent: jest.fn(),
- createChannel: jest.fn(),
- EventType: {
- DISMISSED: 'dismissed',
- DELIVERED: 'delivered',
- PRESS: 'press',
- },
- AndroidImportance: {
- HIGH: 'high',
- },
-}));
-
-jest.mock('../constants', () => ({
- ...jest.requireActual('../constants'),
- isNotificationsFeatureEnabled: jest.fn(),
-}));
-
-const mockNavigate = jest.fn();
-const mockNavigation = {
- navigate: mockNavigate,
-} as unknown as NavigationProp;
-
-const notification = {
- id: '123',
- type: TRIGGER_TYPES.ERC1155_RECEIVED,
- data: {
- id: '123',
- trigger_id: '1',
- chain_id: 1,
- block_number: 1,
- block_timestamp: '',
- tx_hash: '',
- unread: false,
- created_at: '',
- address: '',
- type: TRIGGER_TYPES.ERC1155_RECEIVED,
- data: {},
- createdAt: '',
- isRead: false,
- },
-} as unknown as Notification;
-
-jest.mock('../services/NotificationService', () => ({
- onForegroundEvent: jest.fn(),
- onBackgroundEvent: jest.fn(),
- handleNotificationEvent: jest.fn(),
-}));
-describe('useNotificationHandler', () => {
- beforeEach(() => {
- jest.clearAllMocks();
- });
-
- it('navigates to NOTIFICATIONS.DETAILS when notification is pressed', async () => {
- const { result } = renderHook(() => useNotificationHandler(mockNavigation));
-
- await act(async () => {
- result.current.handlePressedNotification(notification);
- });
-
- expect(mockNavigation.navigate).toHaveBeenCalledWith(
- Routes.NOTIFICATIONS.DETAILS,
- {
- notificationId: notification.id,
- },
- );
- });
-
- it('does not navigates when notification is null', async () => {
-
-
- const { result } = renderHook(() =>
- useNotificationHandler(mockNavigation),
- );
-
- await act(async () => {
- result.current.handlePressedNotification();
- });
-
- expect(mockNavigation.navigate).not.toHaveBeenCalled();
- });
-
- it('does nothing if the isNotificationsFeatureEnabled is false', async () => {
- jest.spyOn(constants, 'isNotificationsFeatureEnabled').mockReturnValue(false);
-
- const { result } = renderHook(() => useNotificationHandler(mockNavigation));
-
- await act(async () => {
- result.current.handlePressedNotification(notification);
- });
-
- expect(NotificationsService.onForegroundEvent).not.toHaveBeenCalled();
- expect(NotificationsService.onBackgroundEvent).not.toHaveBeenCalled();
-
- jest.restoreAllMocks();
- });
-});
diff --git a/app/util/notifications/hooks/index.test.tsx b/app/util/notifications/hooks/index.test.tsx
new file mode 100644
index 00000000000..59535338cad
--- /dev/null
+++ b/app/util/notifications/hooks/index.test.tsx
@@ -0,0 +1,100 @@
+/* eslint-disable import/no-namespace */
+
+import { renderHook } from '@testing-library/react-hooks';
+import { Provider } from 'react-redux';
+import createMockStore from 'redux-mock-store';
+import React from 'react';
+import * as NotificationUtils from '../../../util/notifications';
+import FCMService from '../services/FCMService';
+import useNotificationHandler from './index';
+import initialRootState from '../../../util/test/initial-root-state';
+import * as Selectors from '../../../selectors/notifications';
+import { NavigationContainerRef } from '@react-navigation/native';
+
+jest.mock('../../../util/notifications', () => ({
+ isNotificationsFeatureEnabled: jest.fn(),
+}));
+
+jest.mock('../services/FCMService', () => ({
+ registerAppWithFCM: jest.fn(),
+ saveFCMToken: jest.fn(),
+ registerTokenRefreshListener: jest.fn(),
+ listenForMessagesForeground: jest.fn(),
+}));
+
+function arrangeMocks(isFeatureEnabled: boolean, isMetaMaskEnabled: boolean) {
+ jest.spyOn(NotificationUtils, 'isNotificationsFeatureEnabled')
+ .mockReturnValue(isFeatureEnabled);
+
+ jest.spyOn(Selectors, 'selectIsMetamaskNotificationsEnabled')
+ .mockReturnValue(isMetaMaskEnabled);
+}
+
+function arrangeStore() {
+ const store = createMockStore()(initialRootState);
+
+ store.dispatch = jest.fn().mockImplementation((action) => {
+ if (typeof action === 'function') {
+ return action(store.dispatch, store.getState);
+ }
+ return Promise.resolve();
+ });
+
+ return store;
+}
+
+const mockNavigate = jest.fn();
+const mockNavigation = {
+ navigate: mockNavigate,
+} as unknown as NavigationContainerRef;
+
+function arrangeHook() {
+ const store = arrangeStore();
+ const hook = renderHook(() => useNotificationHandler(mockNavigation), {
+ wrapper: ({ children }) => {children},
+ });
+
+ return hook;
+}
+
+describe('useNotificationHandler', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('does not register FCM when notifications are disabled', () => {
+ arrangeMocks(false, false);
+
+ arrangeHook();
+
+ expect(FCMService.registerAppWithFCM).not.toHaveBeenCalled();
+ expect(FCMService.saveFCMToken).not.toHaveBeenCalled();
+ expect(FCMService.listenForMessagesForeground).not.toHaveBeenCalled();
+ });
+
+ it('registers FCM when notifications feature is enabled', () => {
+ arrangeMocks(true, true);
+
+ arrangeHook();
+
+ expect(FCMService.registerAppWithFCM).toHaveBeenCalledTimes(1);
+ expect(FCMService.saveFCMToken).toHaveBeenCalledTimes(1);
+ });
+
+ it('registers FCM when MetaMask notifications are enabled', () => {
+ arrangeMocks(true, true);
+
+ arrangeHook();
+
+ expect(FCMService.registerAppWithFCM).toHaveBeenCalledTimes(1);
+ expect(FCMService.saveFCMToken).toHaveBeenCalledTimes(1);
+ });
+
+ it('handleNotificationCallback does nothing when notification is undefined', () => {
+ arrangeMocks(true, true);
+
+ arrangeHook();
+
+ expect(mockNavigate).not.toHaveBeenCalled();
+ });
+});
diff --git a/app/util/notifications/hooks/index.ts b/app/util/notifications/hooks/index.ts
index 3f6bb98c9a3..4b00e90492f 100644
--- a/app/util/notifications/hooks/index.ts
+++ b/app/util/notifications/hooks/index.ts
@@ -1,73 +1,75 @@
import { useCallback, useEffect } from 'react';
-import { NavigationProp, ParamListBase } from '@react-navigation/native';
-import NotificationsService from '../../../util/notifications/services/NotificationService';
-import Routes from '../../../constants/navigation/Routes';
+import { NotificationServicesController } from '@metamask/notification-services-controller';
+
+import { useSelector } from 'react-redux';
import {
isNotificationsFeatureEnabled,
+ Notification,
} from '../../../util/notifications';
-import { Notification } from '../../../util/notifications/types';
-import {
- TRIGGER_TYPES,
-} from '../../../util/notifications/constants';
+
+import FCMService from '../services/FCMService';
+import NotificationsService from '../services/NotificationService';
+import { selectIsMetamaskNotificationsEnabled } from '../../../selectors/notifications';
import { Linking } from 'react-native';
+import { NavigationContainerRef } from '@react-navigation/native';
+import Routes from '../../../constants/navigation/Routes';
+
+const { TRIGGER_TYPES } = NotificationServicesController.Constants;
+
+const useNotificationHandler = (navigation: NavigationContainerRef) => {
+ /**
+ * Handles the action based on the type of notification (sent from the backend & following Notification types) that is opened
+ * @param notification - The notification that is opened
+ */
-const useNotificationHandler = (navigation: NavigationProp) => {
- const performActionBasedOnOpenedNotificationType = useCallback(
+ const isNotificationEnabled = useSelector(
+ selectIsMetamaskNotificationsEnabled,
+ );
+
+ const handleNotificationCallback = useCallback(
async (notification: Notification) => {
+ if (!notification) {
+ return;
+ }
if (
notification.type === TRIGGER_TYPES.FEATURES_ANNOUNCEMENT &&
notification.data.externalLink
) {
Linking.openURL(notification.data.externalLink.externalLinkUrl);
} else {
- navigation.navigate(Routes.NOTIFICATIONS.DETAILS, {
- notificationId: notification.id,
- });
+ navigation.navigate(Routes.NOTIFICATIONS.VIEW);
}
},
[navigation],
);
- const handlePressedNotification = useCallback(
- (notification?: Notification) => {
- if (!notification) {
- return;
- }
- performActionBasedOnOpenedNotificationType(notification);
- },
- [performActionBasedOnOpenedNotificationType],
- );
+ const notificationEnabled = isNotificationsFeatureEnabled() && isNotificationEnabled;
useEffect(() => {
- if (!isNotificationsFeatureEnabled()) return;
+ if (!notificationEnabled) return;
- const unsubscribeForegroundEvent = NotificationsService.onForegroundEvent(
- async ({ type, detail }) =>
- await NotificationsService.handleNotificationEvent({
- type,
- detail,
- callback: handlePressedNotification,
- }),
- );
+ // Firebase Cloud Messaging
+ FCMService.registerAppWithFCM();
+ FCMService.saveFCMToken();
+ FCMService.getFCMToken();
+ FCMService.listenForMessagesBackground();
+ // Notifee
NotificationsService.onBackgroundEvent(
async ({ type, detail }) =>
await NotificationsService.handleNotificationEvent({
type,
detail,
- callback: handlePressedNotification,
+ callback: handleNotificationCallback,
}),
);
+ const unsubscribeForegroundEvent = FCMService.listenForMessagesForeground();
+
return () => {
unsubscribeForegroundEvent();
};
- }, [handlePressedNotification]);
-
- return {
- performActionBasedOnOpenedNotificationType,
- handlePressedNotification,
- };
+ }, [handleNotificationCallback, notificationEnabled]);
};
export default useNotificationHandler;
diff --git a/app/util/notifications/services/NotificationService.test.ts b/app/util/notifications/services/NotificationService.test.ts
index 26ce917f539..6b498592f0f 100644
--- a/app/util/notifications/services/NotificationService.test.ts
+++ b/app/util/notifications/services/NotificationService.test.ts
@@ -7,6 +7,7 @@ import notifee, {
import { Linking } from 'react-native';
import { ChannelId } from '../../../util/notifications/androidChannels';
import NotificationsService from './NotificationService';
+import { LAUNCH_ACTIVITY, PressActionId } from '../types';
jest.mock('@notifee/react-native', () => ({
getNotificationSettings: jest.fn(),
@@ -22,6 +23,7 @@ jest.mock('@notifee/react-native', () => ({
getBadgeCount: jest.fn(),
getInitialNotification: jest.fn(),
openNotificationSettings: jest.fn(),
+ displayNotification: jest.fn(),
AndroidImportance: {
DEFAULT: 'default',
HIGH: 'high',
@@ -66,7 +68,7 @@ describe('NotificationsService', () => {
jest.clearAllMocks();
});
- it('should get blocked notifications', async () => {
+ it('gets blocked notifications', async () => {
(notifee.getNotificationSettings as jest.Mock).mockResolvedValue({
authorizationStatus: AuthorizationStatus.AUTHORIZED,
});
@@ -82,7 +84,7 @@ describe('NotificationsService', () => {
).toBe(true);
});
- it('should handle notification press', async () => {
+ it('handles notification press', async () => {
const detail = {
notification: {
id: 'test-id',
@@ -97,13 +99,13 @@ describe('NotificationsService', () => {
expect(callback).toHaveBeenCalledWith(detail.notification);
});
- it('should open system settings on iOS', () => {
+ it('opens system settings on iOS', () => {
NotificationsService.openSystemSettings();
expect(Linking.openSettings).toHaveBeenCalled();
});
- it('should create notification channels', async () => {
+ it('creates notification channels', async () => {
const channel: AndroidChannel = {
id: ChannelId.DEFAULT_NOTIFICATION_CHANNEL_ID,
name: 'Test Channel',
@@ -115,7 +117,7 @@ describe('NotificationsService', () => {
expect(notifee.createChannel).toHaveBeenCalledWith(channel);
});
- it('should return authorized from getAllPermissions', async () => {
+ it('returns authorized from getAllPermissions', async () => {
(notifee.requestPermission as jest.Mock).mockResolvedValue({
authorizationStatus: AuthorizationStatus.AUTHORIZED,
});
@@ -128,7 +130,7 @@ describe('NotificationsService', () => {
expect(result.permission).toBe('authorized');
});
- it('should return authorized from requestPermission', async () => {
+ it('returns authorized from requestPermission', async () => {
(notifee.requestPermission as jest.Mock).mockResolvedValue({
authorizationStatus: AuthorizationStatus.AUTHORIZED,
});
@@ -136,7 +138,7 @@ describe('NotificationsService', () => {
expect(result).toBe('authorized');
});
- it('should return denied from requestPermission', async () => {
+ it('returns denied from requestPermission', async () => {
(notifee.requestPermission as jest.Mock).mockResolvedValue({
authorizationStatus: AuthorizationStatus.DENIED,
});
@@ -144,7 +146,7 @@ describe('NotificationsService', () => {
expect(result).toBe('denied');
});
- it('should handle notification event', async () => {
+ it('handles notification event', async () => {
const callback = jest.fn();
await NotificationsService.handleNotificationEvent({
@@ -172,4 +174,41 @@ describe('NotificationsService', () => {
expect(notifee.decrementBadgeCount).toHaveBeenCalledWith(1);
expect(notifee.cancelTriggerNotification).toHaveBeenCalledWith('123');
});
+
+ it('displays notification', async () => {
+ const notification = {
+ title: 'Test Title',
+ body: 'Test Body',
+ data: undefined,
+ android: {
+ smallIcon: 'ic_notification_small',
+ largeIcon: 'ic_notification',
+ channelId: ChannelId.DEFAULT_NOTIFICATION_CHANNEL_ID,
+ pressAction: {
+ id: PressActionId.OPEN_NOTIFICATIONS_VIEW,
+ launchActivity: LAUNCH_ACTIVITY,
+ },
+ },
+ ios: {
+ foregroundPresentationOptions: {
+ alert: true,
+ sound: true,
+ badge: true,
+ banner: true,
+ list: true,
+ },
+ interruptionLevel: 'critical',
+ launchImageName: 'Default',
+ sound: 'default',
+ },
+ };
+
+ await NotificationsService.displayNotification({
+ title: 'Test Title',
+ body: 'Test Body',
+ channelId: ChannelId.DEFAULT_NOTIFICATION_CHANNEL_ID,
+ });
+
+ expect(notifee.displayNotification).toHaveBeenCalledWith(notification);
+ });
});
diff --git a/app/util/notifications/services/NotificationService.ts b/app/util/notifications/services/NotificationService.ts
index 6c6f104404f..1cf9c9f4d8f 100644
--- a/app/util/notifications/services/NotificationService.ts
+++ b/app/util/notifications/services/NotificationService.ts
@@ -6,7 +6,7 @@ import notifee, {
AndroidChannel,
} from '@notifee/react-native';
-import { Notification } from '../types';
+import { HandleNotificationCallback, LAUNCH_ACTIVITY, Notification, PressActionId } from '../types';
import { Linking, Platform, Alert as NativeAlert } from 'react-native';
import {
@@ -221,7 +221,14 @@ class NotificationsService {
await notifee.cancelTriggerNotification(id);
};
- getInitialNotification = async () => notifee.getInitialNotification();
+ getInitialNotification = async (
+ callback: HandleNotificationCallback
+ ): Promise => {
+ const event = await notifee.getInitialNotification()
+ if (event) {
+ callback(event.notification.data as Notification['data'])
+ }
+ };
cancelAllNotifications = async () => {
await notifee.cancelAllNotifications();
@@ -229,6 +236,45 @@ class NotificationsService {
createChannel = async (channel: AndroidChannel): Promise =>
notifee.createChannel(channel);
+
+ displayNotification = async ({
+ channelId,
+ title,
+ body,
+ data
+ }: {
+ channelId: ChannelId
+ title: string
+ body?: string
+ data?: Notification['data']
+ }): Promise => {
+ await notifee.displayNotification({
+ title,
+ body,
+ data: data as unknown as Notification['data'],
+ android: {
+ smallIcon: 'ic_notification_small',
+ largeIcon: 'ic_notification',
+ channelId: channelId ?? ChannelId.DEFAULT_NOTIFICATION_CHANNEL_ID,
+ pressAction: {
+ id: PressActionId.OPEN_NOTIFICATIONS_VIEW,
+ launchActivity: LAUNCH_ACTIVITY,
+ }
+ },
+ ios: {
+ launchImageName: 'Default',
+ sound: 'default',
+ interruptionLevel: 'critical',
+ foregroundPresentationOptions: {
+ alert: true,
+ sound: true,
+ badge: true,
+ banner: true,
+ list: true,
+ },
+ },
+ });
+ };
}
export default new NotificationsService();
diff --git a/app/util/notifications/types/notification/index.ts b/app/util/notifications/types/notification/index.ts
index 6b091239414..8c8488dc230 100644
--- a/app/util/notifications/types/notification/index.ts
+++ b/app/util/notifications/types/notification/index.ts
@@ -10,6 +10,17 @@ import { TRIGGER_TYPES } from '../../constants';
*/
export type Notification = NotificationServicesController.Types.INotification;
+export type HandleNotificationCallback = (
+ data: Notification['data'] | undefined
+) => void
+
+export enum PressActionId {
+ OPEN_NOTIFICATIONS_VIEW = 'open-notifications-view-press-action-id',
+ OPEN_TRANSACTIONS_VIEW = 'open-transactions-view-press-action-id'
+}
+
+export const LAUNCH_ACTIVITY = 'com.metamask.ui.MainActivity';
+
/**
* NotificationFC is the shared component interface for all notification components
*/
@@ -84,6 +95,22 @@ export const NotificationTransactionTypes = {
cancelled: 'cancelled',
received: 'received',
received_payment: 'received_payment',
+ eth_received: 'eth_received',
+ features_announcement: 'features_announcement',
+ metamask_swap_completed: 'metamask_swap_completed',
+ erc20_sent: 'erc20_sent',
+ erc20_received: 'erc20_received',
+ eth_sent: 'eth_sent',
+ rocketpool_stake_completed: 'rocketpool_stake_completed',
+ rocketpool_unstake_completed: 'rocketpool_unstake_completed',
+ lido_stake_completed: 'lido_stake_completed',
+ lido_withdrawal_requested: 'lido_withdrawal_requested',
+ lido_withdrawal_completed: 'lido_withdrawal_completed',
+ lido_stake_ready_to_be_withdrawn: 'lido_stake_ready_to_be_withdrawn',
+ erc721_sent: 'erc721_sent',
+ erc721_received: 'erc721_received',
+ erc1155_sent: 'erc1155_sent',
+ erc1155_received: 'erc1155_received',
} as const;
export type NotificationTransactionTypesType =
@@ -96,10 +123,10 @@ export interface MarketingNotificationData {
}
export const STAKING_PROVIDER_MAP: Record<
- | 'lido_stake_completed'
- | 'rocketpool_stake_completed'
- | 'rocketpool_unstake_completed'
- | 'lido_withdrawal_completed',
+NotificationServicesController.Constants.TRIGGER_TYPES.LIDO_STAKE_COMPLETED
+| NotificationServicesController.Constants.TRIGGER_TYPES.ROCKETPOOL_STAKE_COMPLETED
+| NotificationServicesController.Constants.TRIGGER_TYPES.ROCKETPOOL_UNSTAKE_COMPLETED
+| NotificationServicesController.Constants.TRIGGER_TYPES.LIDO_WITHDRAWAL_COMPLETED,
string
> = {
[TRIGGER_TYPES.LIDO_STAKE_COMPLETED]: 'Lido-staked ETH',
diff --git a/locales/languages/en.json b/locales/languages/en.json
index 61291b5d831..0e22f5e0b41 100644
--- a/locales/languages/en.json
+++ b/locales/languages/en.json
@@ -2199,7 +2199,7 @@
"label_swapped": "Swapped",
"label_to": "To"
},
- "received_from": "Received from {{address}}",
+ "received_from": "Received {{amount}} {{ticker}} from {{address}}",
"nft_sent": "Sent NFT to {{address}}",
"erc721_sent": "Sent NFT to {{address}}",
"erc1155_sent": "Sent NFT to {{address}}",
@@ -2216,8 +2216,23 @@
"success_withdrawal_title": "Withdrawal Complete!",
"error_title": "Oops, something went wrong :/",
"received_title": "You received {{amount}} {{assetType}}",
- "default_message_title": "New transaction notification",
- "default_message_description": "Tap to view this transaction",
+ "metamask_swap_completed_title": "Swap completed",
+ "erc20_sent_title": "Funds sent",
+ "erc20_received_title": "Funds received",
+ "eth_sent_title": "Funds sent",
+ "eth_received_title": "Funds received",
+ "rocketpool_stake_completed_title": "Stake complete",
+ "rocketpool_unstake_completed_title": "Unstake complete",
+ "lido_stake_completed_title": "Stake complete",
+ "lido_withdrawal_requested_title": "Withdrawal requested",
+ "lido_withdrawal_completed_title": "Withdrawal completed",
+ "lido_stake_ready_to_be_withdrawn_title": "Stake ready for withdrawal ",
+ "erc721_sent_title": "NFT sent",
+ "erc721_received_title": "NFT received",
+ "erc1155_sent_title": "NFT sent",
+ "erc1155_received_title": "NFT received",
+ "default_message_title": "MetaMask",
+ "default_message_description": "Tap to view",
"received_payment_title": "Instant payment received",
"pending_message": "Waiting for confirmation",
"pending_deposit_message": "Waiting for deposit to complete",
@@ -2233,6 +2248,17 @@
"cancelled_message": "Tap to view this transaction",
"received_message": "Tap to view this transaction",
"received_payment_message": "You received {{amount}} DAI",
+ "eth_received_message": "You received some ETH",
+ "metamask_swap_completed_message": "Swapped {{symbolIn}} for {{symbolOut}}",
+ "erc20_sent_message": "Sent to {{address}}",
+ "erc20_received_message": "Received from {{address}}",
+ "eth_sent_message": "Sent to {{address}}",
+ "rocketpool_stake_completed_message": "Staked",
+ "rocketpool_unstake_completed_message": "Unstaking complete",
+ "lido_stake_completed_message": "Staked",
+ "lido_withdrawal_requested_message": "Unstaking requested",
+ "lido_withdrawal_completed_message": "Unstaking complete",
+ "lido_stake_ready_to_be_withdrawn_message": "Withdrawal requested",
"push_notification_content": {
"metamask_swap_completed_title": "Swap completed",
"metamask_swap_completed_description": "Your MetaMask Swap was successful",