Skip to content

Commit 92924a7

Browse files
feat: update notification services to support mobile. (#5120)
## Explanation This is a large PR that enables support for push notifications on mobile. **Makes Push Notification Controller Platform Agnostic** The `NotificationServicesPushController` was highly tied to web and was not compatible for react-native. We now have isolated the web logic in`web/push-utils.ts`, and allow platforms to overwrite and inject a push service into the controller. E.g. Mobile can inject push services using react-native modules. **Add support to toggle push notifications on/off in isolation** This allows us to decouple in-app notifications from push notifications. It is done by adding a `isPushEnabled` boolean inside the `NotificationServicesPushController`. We have also added public methods `enablePushNotifications` and `disablePushNotifications` inside the `NotificationServicesController` to allow us tie it to UI Actions. ## References ## Changelog ### `@metamask/notification-services-controller` - **ADDED**: `isPushEnabled` state to `NotificationServicesPushControllerState` to enable and disable this controller/ - **ADDED**: `isUpdatingFCMToken` state to `NotificationServicesPushControllerState` to track when the controller is updating for firebase registration token. - **ADDED**: `PushService` interface and default web implementation for push notifciations - **CHANGED (BREAKING)**: `NotificationServicesPushController` config now allows injecting of a `PushService` interface during controller creation. - **ADDED**: `/push-services/web` subpath export to import web specific push services. - **ADDED**: `/shared` folder including some shared utils used in controllers for this package. - **ADDED**: public method `enablePushNotifications` and `disablePushNotifications` in `NotificationServicesController` to enable and disable push notifications in isolation. ## Checklist - [x] I've updated the test suite for new or updated code as appropriate - [x] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [x] I've highlighted breaking changes using the "BREAKING" category above as appropriate - [x] I've prepared draft pull requests for clients and consumer packages to resolve any breaking changes
1 parent a9c6e11 commit 92924a7

File tree

23 files changed

+1238
-425
lines changed

23 files changed

+1238
-425
lines changed

packages/notification-services-controller/package.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,16 @@
6767
"default": "./dist/NotificationServicesPushController/index.cjs"
6868
}
6969
},
70+
"./push-services/web": {
71+
"import": {
72+
"types": "./dist/NotificationServicesPushController/web/index.d.mts",
73+
"default": "./dist/NotificationServicesPushController/web/index.mjs"
74+
},
75+
"require": {
76+
"types": "./dist/NotificationServicesPushController/web/index.d.cts",
77+
"default": "./dist/NotificationServicesPushController/web/index.cjs"
78+
}
79+
},
7080
"./push-services/mocks": {
7181
"import": {
7282
"types": "./dist/NotificationServicesPushController/__fixtures__/index.d.mts",
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"version": "1.0.0",
3+
"private": true,
4+
"description": "",
5+
"license": "MIT",
6+
"sideEffects": false,
7+
"main": "../../dist/NotificationServicesPushController/web/index.cjs",
8+
"types": "../../dist/NotificationServicesPushController/web/index.d.cts"
9+
}

packages/notification-services-controller/src/NotificationServicesController/NotificationServicesController.test.ts

Lines changed: 83 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,8 @@ import NotificationServicesController, {
3636
import type {
3737
AllowedActions,
3838
AllowedEvents,
39-
NotificationServicesPushControllerEnablePushNotifications,
40-
NotificationServicesPushControllerDisablePushNotifications,
41-
NotificationServicesPushControllerUpdateTriggerPushNotifications,
4239
NotificationServicesControllerMessenger,
4340
NotificationServicesControllerState,
44-
NotificationServicesPushControllerSubscribeToNotifications,
4541
} from './NotificationServicesController';
4642
import { processFeatureAnnouncement } from './processors';
4743
import { processNotification } from './processors/process-notifications';
@@ -50,6 +46,12 @@ import * as OnChainNotifications from './services/onchain-notifications';
5046
import type { INotification } from './types';
5147
import type { UserStorage } from './types/user-storage/user-storage';
5248
import * as Utils from './utils/utils';
49+
import type {
50+
NotificationServicesPushControllerDisablePushNotificationsAction,
51+
NotificationServicesPushControllerEnablePushNotificationsAction,
52+
NotificationServicesPushControllerSubscribeToNotificationsAction,
53+
NotificationServicesPushControllerUpdateTriggerPushNotificationsAction,
54+
} from '../NotificationServicesPushController';
5355

5456
// Mock type used for testing purposes
5557
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -206,7 +208,7 @@ describe('metamask-notifications - constructor()', () => {
206208
return mocks;
207209
};
208210

209-
it('initializes push notifications', async () => {
211+
it('initialises push notifications', async () => {
210212
const { mockEnablePushNotifications } =
211213
arrangeActInitialisePushNotifications();
212214

@@ -471,7 +473,7 @@ describe('metamask-notifications - deleteOnChainTriggersByAccount', () => {
471473
const {
472474
messenger,
473475
nockMockDeleteTriggersAPI,
474-
mockDisablePushNotifications,
476+
mockUpdateTriggerPushNotifications,
475477
} = arrangeMocks();
476478
const controller = new NotificationServicesController({
477479
messenger,
@@ -482,7 +484,7 @@ describe('metamask-notifications - deleteOnChainTriggersByAccount', () => {
482484
]);
483485
expect(Utils.traverseUserStorageTriggers(result)).toHaveLength(0);
484486
expect(nockMockDeleteTriggersAPI.isDone()).toBe(true);
485-
expect(mockDisablePushNotifications).toHaveBeenCalled();
487+
expect(mockUpdateTriggerPushNotifications).toHaveBeenCalled();
486488
});
487489

488490
it('does nothing if account does not exist in storage', async () => {
@@ -1017,6 +1019,7 @@ describe('metamask-notifications - disableMetamaskNotifications()', () => {
10171019
// Act - final state
10181020
expect(controller.state.isUpdatingMetamaskNotifications).toBe(false);
10191021
expect(controller.state.isNotificationServicesEnabled).toBe(false);
1022+
expect(controller.state.isFeatureAnnouncementsEnabled).toBe(false);
10201023
expect(controller.state.metamaskNotificationsList).toStrictEqual([
10211024
createMockSnapNotification(),
10221025
]);
@@ -1065,6 +1068,73 @@ describe('metamask-notifications - updateMetamaskNotificationsList', () => {
10651068
});
10661069
});
10671070

1071+
describe('metamask-notifications - enablePushNotifications', () => {
1072+
const arrangeMocks = () => {
1073+
const messengerMocks = mockNotificationMessenger();
1074+
return messengerMocks;
1075+
};
1076+
1077+
it('calls push controller and enables notifications for accounts that have subscribed to notifications', async () => {
1078+
const { messenger, mockPerformGetStorage, mockEnablePushNotifications } =
1079+
arrangeMocks();
1080+
const controller = new NotificationServicesController({
1081+
messenger,
1082+
env: { featureAnnouncements: featureAnnouncementsEnv },
1083+
state: { isNotificationServicesEnabled: true },
1084+
});
1085+
1086+
// Act
1087+
await controller.enablePushNotifications();
1088+
1089+
// Assert
1090+
expect(mockPerformGetStorage).toHaveBeenCalled();
1091+
expect(mockEnablePushNotifications).toHaveBeenCalled();
1092+
});
1093+
1094+
it('throws error if fails to get notification triggers', async () => {
1095+
const { messenger, mockPerformGetStorage, mockEnablePushNotifications } =
1096+
arrangeMocks();
1097+
1098+
// Mock no storage
1099+
mockPerformGetStorage.mockResolvedValue(null);
1100+
1101+
const controller = new NotificationServicesController({
1102+
messenger,
1103+
env: { featureAnnouncements: featureAnnouncementsEnv },
1104+
state: { isNotificationServicesEnabled: true },
1105+
});
1106+
1107+
// Act
1108+
await expect(() => controller.enablePushNotifications()).rejects.toThrow(
1109+
expect.any(Error),
1110+
);
1111+
1112+
expect(mockEnablePushNotifications).not.toHaveBeenCalled();
1113+
});
1114+
});
1115+
1116+
describe('metamask-notifications - disablePushNotifications', () => {
1117+
const arrangeMocks = () => {
1118+
const messengerMocks = mockNotificationMessenger();
1119+
return messengerMocks;
1120+
};
1121+
1122+
it('calls push controller and enables notifications for accounts that have subscribed to notifications', async () => {
1123+
const { messenger, mockDisablePushNotifications } = arrangeMocks();
1124+
const controller = new NotificationServicesController({
1125+
messenger,
1126+
env: { featureAnnouncements: featureAnnouncementsEnv },
1127+
state: { isNotificationServicesEnabled: true },
1128+
});
1129+
1130+
// Act
1131+
await controller.disablePushNotifications();
1132+
1133+
// Assert
1134+
expect(mockDisablePushNotifications).toHaveBeenCalled();
1135+
});
1136+
});
1137+
10681138
// Type-Computation - we are extracting args and parameters from a generic type utility
10691139
// Thus this `AnyFunc` can be used to help constrain the generic parameters correctly
10701140
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -1101,6 +1171,7 @@ function mockNotificationMessenger() {
11011171
'KeyringController:lock',
11021172
'KeyringController:unlock',
11031173
'NotificationServicesPushController:onNewNotifications',
1174+
'NotificationServicesPushController:stateChange',
11041175
],
11051176
});
11061177

@@ -1123,16 +1194,16 @@ function mockNotificationMessenger() {
11231194
);
11241195

11251196
const mockDisablePushNotifications =
1126-
typedMockAction<NotificationServicesPushControllerDisablePushNotifications>();
1197+
typedMockAction<NotificationServicesPushControllerDisablePushNotificationsAction>();
11271198

11281199
const mockEnablePushNotifications =
1129-
typedMockAction<NotificationServicesPushControllerEnablePushNotifications>();
1200+
typedMockAction<NotificationServicesPushControllerEnablePushNotificationsAction>();
11301201

11311202
const mockUpdateTriggerPushNotifications =
1132-
typedMockAction<NotificationServicesPushControllerUpdateTriggerPushNotifications>();
1203+
typedMockAction<NotificationServicesPushControllerUpdateTriggerPushNotificationsAction>();
11331204

11341205
const mockSubscribeToPushNotifications =
1135-
typedMockAction<NotificationServicesPushControllerSubscribeToNotifications>();
1206+
typedMockAction<NotificationServicesPushControllerSubscribeToNotificationsAction>();
11361207

11371208
const mockGetStorageKey =
11381209
typedMockAction<UserStorageController.UserStorageControllerGetStorageKey>().mockResolvedValue(
@@ -1184,7 +1255,7 @@ function mockNotificationMessenger() {
11841255
actionType ===
11851256
'NotificationServicesPushController:disablePushNotifications'
11861257
) {
1187-
return mockDisablePushNotifications(params[0]);
1258+
return mockDisablePushNotifications();
11881259
}
11891260

11901261
if (

0 commit comments

Comments
 (0)