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: update FCM and Notification Services to better support push impl #13441

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
14 changes: 13 additions & 1 deletion app/selectors/notifications/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { TRIGGER_TYPES } from '@metamask/notification-services-controller/notification-services';
import {
selectIsMetamaskNotificationsEnabled,
selectIsMetaMaskPushNotificationsEnabled,
selectIsMetamaskNotificationsFeatureSeen,
selectIsUpdatingMetamaskNotifications,
selectIsFetchingMetamaskNotifications,
Expand All @@ -14,13 +15,18 @@ import {
getOnChainMetamaskNotificationsUnreadCount,
} from './index';
import { RootState } from '../../reducers';
import { MOCK_NOTIFICATION_SERVICES_CONTROLLER } from './testUtils';
import {
MOCK_NOTIFICATION_SERVICES_CONTROLLER,
MOCK_NOTIFICATION_SERVICES_PUSH_CONTROLLER,
} from './testUtils';

describe('Notification Selectors', () => {
const mockState = {
engine: {
backgroundState: {
NotificationServicesController: MOCK_NOTIFICATION_SERVICES_CONTROLLER,
NotificationServicesPushController:
MOCK_NOTIFICATION_SERVICES_PUSH_CONTROLLER,
},
},
} as unknown as RootState;
Expand All @@ -31,6 +37,12 @@ describe('Notification Selectors', () => {
);
});

it('selectIsMetaMaskPushNotificationsEnabled returns correct value', () => {
expect(selectIsMetaMaskPushNotificationsEnabled(mockState)).toEqual(
MOCK_NOTIFICATION_SERVICES_PUSH_CONTROLLER.isPushEnabled,
);
});

it('selectIsMetamaskNotificationsFeatureSeen returns correct value', () => {
expect(selectIsMetamaskNotificationsFeatureSeen(mockState)).toEqual(
MOCK_NOTIFICATION_SERVICES_CONTROLLER.isMetamaskNotificationsFeatureSeen,
Expand Down
21 changes: 19 additions & 2 deletions app/selectors/notifications/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@ import { createSelector } from 'reselect';
import {
NotificationServicesControllerState,
TRIGGER_TYPES,
defaultState,
defaultState as notificationControllerServiceDefaultState,
INotification,
} from '@metamask/notification-services-controller/notification-services';
import {
NotificationServicesPushControllerState,
defaultState as pushControllerDefaultState,
} from '@metamask/notification-services-controller/push-services';

import { createDeepEqualSelector } from '../util';
import { RootState } from '../../reducers';
Expand All @@ -13,13 +17,26 @@ type NotificationServicesState = NotificationServicesControllerState;

const selectNotificationServicesControllerState = (state: RootState) =>
state?.engine?.backgroundState?.NotificationServicesController ??
defaultState;
notificationControllerServiceDefaultState;

const selectNotificationServicesPushControllerState = (state: RootState) =>
state?.engine?.backgroundState?.NotificationServicesPushController ??
pushControllerDefaultState;

export const selectIsMetamaskNotificationsEnabled = createSelector(
selectNotificationServicesControllerState,
(notificationServicesControllerState: NotificationServicesState) =>
notificationServicesControllerState.isNotificationServicesEnabled,
);
/**
* TEMP - update controller to get up-to-date state
*/
export const selectIsMetaMaskPushNotificationsEnabled = createSelector(
selectNotificationServicesPushControllerState,
(state: NotificationServicesPushControllerState) =>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Boolean((state as any)?.isPushEnabled),
);
export const selectIsMetamaskNotificationsFeatureSeen = createSelector(
selectNotificationServicesControllerState,
(notificationServicesControllerState: NotificationServicesState) =>
Expand Down
6 changes: 6 additions & 0 deletions app/selectors/notifications/testUtils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { defaultState } from '@metamask/notification-services-controller/push-services';
import MOCK_NOTIFICATIONS from '../../components/UI/Notification/__mocks__/mock_notifications';

export const MOCK_NOTIFICATION_SERVICES_CONTROLLER = {
Expand All @@ -11,3 +12,8 @@ export const MOCK_NOTIFICATION_SERVICES_CONTROLLER = {
metamaskNotificationsReadList: [],
metamaskNotificationsList: MOCK_NOTIFICATIONS,
};

export const MOCK_NOTIFICATION_SERVICES_PUSH_CONTROLLER = {
...defaultState,
isPushEnabled: true,
};
106 changes: 19 additions & 87 deletions app/util/notifications/hooks/index.test.tsx
Original file line number Diff line number Diff line change
@@ -1,100 +1,32 @@
/* 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 }) => <Provider store={store}>{children}</Provider>,
});

return hook;
}
import { renderHook } from '@testing-library/react-hooks';
import useNotificationHandler from './index';
// eslint-disable-next-line import/no-namespace
import * as UseRegisterPushNotificationsEffect from './useRegisterPushNotificationsEffect';

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);
const arrangeMocks = () => {
const mockNavigate = jest.fn();
const mockNavigation = {
navigate: mockNavigate,
} as unknown as NavigationContainerRef;

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);
});
const mockUseRegisterPushNotificationsEffect = jest.spyOn(
UseRegisterPushNotificationsEffect,
'useRegisterPushNotificationsEffect',
);

it('handleNotificationCallback does nothing when notification is undefined', () => {
arrangeMocks(true, true);
return { mockNavigation, mockUseRegisterPushNotificationsEffect };
};

arrangeHook();
it('invokes the push notifications effect', () => {
const mocks = arrangeMocks();
renderHook(() => useNotificationHandler(mocks.mockNavigation));

expect(mockNavigate).not.toHaveBeenCalled();
expect(mocks.mockUseRegisterPushNotificationsEffect).toHaveBeenCalled();
});
});
79 changes: 9 additions & 70 deletions app/util/notifications/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,74 +1,13 @@
import { useCallback, useEffect } from 'react';
import {
INotification,
TRIGGER_TYPES,
} from '@metamask/notification-services-controller/notification-services';

import { useSelector } from 'react-redux';
import { isNotificationsFeatureEnabled } from '../../../util/notifications';

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 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 isNotificationEnabled = useSelector(
selectIsMetamaskNotificationsEnabled,
);

const handleNotificationCallback = useCallback(
async (notification: INotification) => {
if (!notification) {
return;
}
if (
notification.type === TRIGGER_TYPES.FEATURES_ANNOUNCEMENT &&
notification.data.externalLink
) {
Linking.openURL(notification.data.externalLink.externalLinkUrl);
} else {
navigation.navigate(Routes.NOTIFICATIONS.VIEW);
}
},
[navigation],
);

const notificationEnabled =
isNotificationsFeatureEnabled() && isNotificationEnabled;

useEffect(() => {
if (!notificationEnabled) return;

// Firebase Cloud Messaging
FCMService.registerAppWithFCM();
FCMService.saveFCMToken();
FCMService.getFCMToken();
FCMService.listenForMessagesBackground();

// Notifee
NotificationsService.onBackgroundEvent(
async ({ type, detail }) =>
await NotificationsService.handleNotificationEvent({
type,
detail,
callback: handleNotificationCallback,
}),
);

const unsubscribeForegroundEvent = FCMService.listenForMessagesForeground();

return () => {
unsubscribeForegroundEvent();
};
}, [handleNotificationCallback, notificationEnabled]);
import { useRegisterPushNotificationsEffect } from './useRegisterPushNotificationsEffect';

/**
* Registers Push Notifications
* TEMP - clean up props once we finalise integration
* @param navigation - page navigation prop
*/
const useNotificationHandler = (_navigation: NavigationContainerRef) => {
useRegisterPushNotificationsEffect();
};

export default useNotificationHandler;
Loading
Loading