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

Update NotificationServicesController to accommodate for Snaps Notifications #4809

Open
wants to merge 32 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
dc85e1a
add snap notification types
hmalik88 Oct 17, 2024
1939195
add snap processor, mock utility and test
hmalik88 Oct 17, 2024
9961df5
update notification union to include snap notifications
hmalik88 Oct 17, 2024
b269078
update trigger types
hmalik88 Oct 17, 2024
aea9760
update process notification function and tests to include snaps
hmalik88 Oct 17, 2024
1d3266b
fix process notifications execution order
hmalik88 Oct 17, 2024
eb8681b
added getNotificationsByType function, updated other functions to acc…
hmalik88 Oct 17, 2024
773f426
Merge branch 'main' into hm/add-snap-notifications
hmalik88 Oct 17, 2024
cb8e138
fix createMockSnapNotification jsdoc
hmalik88 Oct 17, 2024
bcb2263
fix lint errors
hmalik88 Oct 17, 2024
9b3fa2a
fix jsdoc again
hmalik88 Oct 17, 2024
0870e90
bump @metamask/utils to 9.3.0
hmalik88 Oct 17, 2024
08acf8a
add readDate property to snap notification
hmalik88 Oct 17, 2024
7969de5
remove NotificationController
hmalik88 Oct 17, 2024
70f591a
regenerate lockfile
hmalik88 Oct 17, 2024
d3ba765
run yarn dedupe
hmalik88 Oct 17, 2024
e99b172
update tsconfig
hmalik88 Oct 17, 2024
82b07c2
fix test
hmalik88 Oct 17, 2024
7ab1f78
add deleteNotificationById function
hmalik88 Oct 17, 2024
7cc4106
Revert "remove NotificationController"
hmalik88 Oct 17, 2024
3fdbeee
use older version of utils
hmalik88 Oct 17, 2024
a15b131
regenerate lock file
hmalik88 Oct 17, 2024
5ba67cb
Revert "bump @metamask/utils to 9.3.0"
hmalik88 Oct 17, 2024
225a7de
Revert "update tsconfig"
hmalik88 Oct 17, 2024
2e8c150
fix lockfile
hmalik88 Oct 17, 2024
3cf79b3
add assertion to deleteNotificationById and add more tests
hmalik88 Oct 18, 2024
e59eeb2
more changes
hmalik88 Oct 18, 2024
00922f8
lint fix
hmalik88 Oct 18, 2024
a57d1a7
fix type issue
hmalik88 Oct 19, 2024
9645a91
added deleteNotificationsById for batch deletion
hmalik88 Oct 19, 2024
bd312ed
Merge branch 'main' into hm/add-snap-notifications
hmalik88 Oct 19, 2024
0cba55e
lint fix
hmalik88 Oct 19, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/notification-services-controller/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@
"@contentful/rich-text-html-renderer": "^16.5.2",
"@metamask/base-controller": "^7.0.1",
"@metamask/controller-utils": "^11.3.0",
"@metamask/utils": "^9.1.0",
"bignumber.js": "^4.1.0",
"firebase": "^10.11.0",
"loglevel": "^1.8.1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type {
import type { UserStorageController } from '@metamask/profile-sync-controller';
import { AuthenticationController } from '@metamask/profile-sync-controller';

import { createMockSnapNotification } from './__fixtures__';
import {
createMockFeatureAnnouncementAPIResult,
createMockFeatureAnnouncementRaw,
Expand All @@ -25,6 +26,7 @@ import {
mockMarkNotificationsAsRead,
} from './__fixtures__/mockServices';
import { waitFor } from './__fixtures__/test-utils';
import { TRIGGER_TYPES } from './constants';
import NotificationServicesController, {
defaultState,
} from './NotificationServicesController';
Expand All @@ -35,7 +37,9 @@ import type {
NotificationServicesPushControllerDisablePushNotifications,
NotificationServicesPushControllerUpdateTriggerPushNotifications,
} from './NotificationServicesController';
import { processFeatureAnnouncement } from './processors';
import { processNotification } from './processors/process-notifications';
import { processSnapNotification } from './processors/process-snap-notifications';
import * as OnChainNotifications from './services/onchain-notifications';
import type { UserStorage } from './types/user-storage/user-storage';
import * as Utils from './utils/utils';
Expand Down Expand Up @@ -472,23 +476,30 @@ describe('metamask-notifications - fetchAndUpdateMetamaskNotifications()', () =>
};
};

it('processes and shows feature announcements and wallet notifications', async () => {
it('processes and shows feature announcements, wallet and snap notifications', async () => {
const {
messenger,
mockFeatureAnnouncementAPIResult,
mockListNotificationsAPIResult,
} = arrangeMocks();

const snapNotification = createMockSnapNotification();
const processedSnapNotification = processSnapNotification(snapNotification);

const controller = new NotificationServicesController({
messenger,
env: { featureAnnouncements: featureAnnouncementsEnv },
state: { ...defaultState, isFeatureAnnouncementsEnabled: true },
state: {
...defaultState,
isFeatureAnnouncementsEnabled: true,
metamaskNotificationsList: [processedSnapNotification],
},
});

const result = await controller.fetchAndUpdateMetamaskNotifications();

// Should have 1 feature announcement and 1 wallet notification
expect(result).toHaveLength(2);
expect(result).toHaveLength(3);
expect(
result.find(
(n) => n.id === mockFeatureAnnouncementAPIResult.items?.[0].fields.id,
Expand All @@ -497,9 +508,10 @@ describe('metamask-notifications - fetchAndUpdateMetamaskNotifications()', () =>
expect(
result.find((n) => n.id === mockListNotificationsAPIResult[0].id),
).toBeDefined();
expect(result.find((n) => n.type === TRIGGER_TYPES.SNAP)).toBeDefined();

// State is also updated
expect(controller.state.metamaskNotificationsList).toHaveLength(2);
expect(controller.state.metamaskNotificationsList).toHaveLength(3);
});

it('only fetches and processes feature announcements if not authenticated', async () => {
Expand Down Expand Up @@ -529,6 +541,148 @@ describe('metamask-notifications - fetchAndUpdateMetamaskNotifications()', () =>
});
});

describe('metamask-notifications - getNotificationsByType', () => {
it('can fetch notifications by their type', async () => {
const { messenger } = mockNotificationMessenger();
const controller = new NotificationServicesController({
messenger,
env: { featureAnnouncements: featureAnnouncementsEnv },
});

const processedSnapNotification = processSnapNotification(
createMockSnapNotification(),
);
const processedFeatureAnnouncement = processFeatureAnnouncement(
createMockFeatureAnnouncementRaw(),
);

await controller.updateMetamaskNotificationsList(processedSnapNotification);
await controller.updateMetamaskNotificationsList(
processedFeatureAnnouncement,
);

expect(controller.state.metamaskNotificationsList).toHaveLength(2);

const filteredNotifications = controller.getNotificationsByType(
TRIGGER_TYPES.SNAP,
);

expect(filteredNotifications).toHaveLength(1);
expect(filteredNotifications).toStrictEqual([
{
type: TRIGGER_TYPES.SNAP,
id: expect.any(String),
createdAt: expect.any(String),
isRead: false,
readDate: null,
data: {
message: 'fooBar',
origin: '@metamask/example-snap',
detailedView: {
title: 'Detailed View',
interfaceId: '1',
footerLink: {
text: 'Go Home',
href: 'metamask://client/',
},
},
},
},
]);
});
});

describe('metamask-notifications - deleteNotificationsById', () => {
it('will delete a notification by its id', async () => {
const { messenger } = mockNotificationMessenger();
const processedSnapNotification = processSnapNotification(
createMockSnapNotification(),
);
const controller = new NotificationServicesController({
messenger,
env: { featureAnnouncements: featureAnnouncementsEnv },
state: { metamaskNotificationsList: [processedSnapNotification] },
});

await controller.deleteNotificationsById([processedSnapNotification.id]);

expect(controller.state.metamaskNotificationsList).toHaveLength(0);
});

it('will batch delete notifications', async () => {
const { messenger } = mockNotificationMessenger();
const processedSnapNotification1 = processSnapNotification(
createMockSnapNotification(),
);
const processedSnapNotification2 = processSnapNotification(
createMockSnapNotification(),
);
const controller = new NotificationServicesController({
messenger,
env: { featureAnnouncements: featureAnnouncementsEnv },
state: {
metamaskNotificationsList: [
processedSnapNotification1,
processedSnapNotification2,
],
},
});

await controller.deleteNotificationsById([
processedSnapNotification1.id,
processedSnapNotification2.id,
]);

expect(controller.state.metamaskNotificationsList).toHaveLength(0);
});

it('will throw if a notification is not found', async () => {
const { messenger } = mockNotificationMessenger();
const processedSnapNotification = processSnapNotification(
createMockSnapNotification(),
);
const controller = new NotificationServicesController({
messenger,
env: { featureAnnouncements: featureAnnouncementsEnv },
state: { metamaskNotificationsList: [processedSnapNotification] },
});

await expect(controller.deleteNotificationsById(['foo'])).rejects.toThrow(
'The notification to be deleted does not exist.',
);

expect(controller.state.metamaskNotificationsList).toHaveLength(1);
});

it('will throw if the notification to be deleted is not locally persisted', async () => {
const { messenger } = mockNotificationMessenger();
const processedSnapNotification = processSnapNotification(
createMockSnapNotification(),
);
const processedFeatureAnnouncement = processFeatureAnnouncement(
createMockFeatureAnnouncementRaw(),
);
const controller = new NotificationServicesController({
messenger,
env: { featureAnnouncements: featureAnnouncementsEnv },
state: {
metamaskNotificationsList: [
processedFeatureAnnouncement,
processedSnapNotification,
],
},
});

await expect(
controller.deleteNotificationsById([processedFeatureAnnouncement.id]),
).rejects.toThrow(
'The notification type of "features_announcement" is not locally persisted, only the following types can use this function: snap.',
);

expect(controller.state.metamaskNotificationsList).toHaveLength(2);
});
});

describe('metamask-notifications - markMetamaskNotificationsAsRead()', () => {
const arrangeMocks = (options?: { onChainMarkAsReadFails: boolean }) => {
const messengerMocks = mockNotificationMessenger();
Expand Down Expand Up @@ -576,6 +730,38 @@ describe('metamask-notifications - markMetamaskNotificationsAsRead()', () => {
// We can debate & change implementation if it makes sense to mark as read locally if external APIs fail.
expect(controller.state.metamaskNotificationsReadList).toHaveLength(1);
});

it('updates snap notifications as read', async () => {
const { messenger } = arrangeMocks();
const processedSnapNotification = processSnapNotification(
createMockSnapNotification(),
);
const controller = new NotificationServicesController({
messenger,
env: { featureAnnouncements: featureAnnouncementsEnv },
state: {
metamaskNotificationsList: [processedSnapNotification],
},
});

await controller.markMetamaskNotificationsAsRead([
{
type: TRIGGER_TYPES.SNAP,
id: processedSnapNotification.id,
isRead: false,
},
]);

// Should see 1 item in controller read state
expect(controller.state.metamaskNotificationsReadList).toHaveLength(1);

// The notification should have a read date
expect(
// @ts-expect-error readDate property is guaranteed to exist
// as we're dealing with a snap notification
controller.state.metamaskNotificationsList[0].readDate,
).not.toBeNull();
});
});

describe('metamask-notifications - enableMetamaskNotifications()', () => {
Expand Down Expand Up @@ -670,6 +856,42 @@ describe('metamask-notifications - disableMetamaskNotifications()', () => {
});
});

describe('metamask-notifications - updateMetamaskNotificationsList', () => {
it('can add and process a new notification to the notifications list', async () => {
const { messenger } = mockNotificationMessenger();
const controller = new NotificationServicesController({
messenger,
env: { featureAnnouncements: featureAnnouncementsEnv },
state: { isNotificationServicesEnabled: true },
});
const processedSnapNotification = processSnapNotification(
createMockSnapNotification(),
);
await controller.updateMetamaskNotificationsList(processedSnapNotification);
expect(controller.state.metamaskNotificationsList).toStrictEqual([
{
type: TRIGGER_TYPES.SNAP,
id: expect.any(String),
createdAt: expect.any(String),
readDate: null,
isRead: false,
data: {
message: 'fooBar',
origin: '@metamask/example-snap',
detailedView: {
title: 'Detailed View',
interfaceId: '1',
footerLink: {
text: 'Go Home',
href: 'metamask://client/',
},
},
},
},
]);
});
});

// Type-Computation - we are extracting args and parameters from a generic type utility
// Thus this `AnyFunc` can be used to help constrain the generic parameters correctly
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand Down
Loading
Loading