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: Integrate Snap notification services #27975

Draft
wants to merge 3 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 2 additions & 6 deletions app/scripts/background.js
Original file line number Diff line number Diff line change
Expand Up @@ -1005,11 +1005,6 @@ export function setupController(
updateBadge,
);

controller.controllerMessenger.subscribe(
METAMASK_CONTROLLER_EVENTS.NOTIFICATIONS_STATE_CHANGE,
updateBadge,
);

/**
* Formats a count for display as a badge label.
*
Expand Down Expand Up @@ -1078,7 +1073,8 @@ export function setupController(
controller.notificationServicesController.state;

const snapNotificationCount = Object.values(
controller.notificationController.state.notifications,
controller.notificationServicesController.state
.metamaskNotificationsList,
).filter((notification) => notification.readDate === null).length;

const featureAnnouncementCount = isFeatureAnnouncementsEnabled
Expand Down
3 changes: 0 additions & 3 deletions app/scripts/constants/sentry-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,9 +184,6 @@ export const SENTRY_BACKGROUND_STATE = {
allNfts: false,
ignoredNfts: false,
},
NotificationController: {
notifications: false,
},
OnboardingController: {
completedOnboarding: true,
firstTimeFlowType: true,
Expand Down
83 changes: 39 additions & 44 deletions app/scripts/metamask-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,6 @@ import { LoggingController, LogType } from '@metamask/logging-controller';
import { PermissionLogController } from '@metamask/permission-log-controller';

import { RateLimitController } from '@metamask/rate-limit-controller';
import { NotificationController } from '@metamask/notification-controller';
import {
CronjobController,
JsonSnapsRegistry,
Expand Down Expand Up @@ -369,6 +368,7 @@ import createTracingMiddleware from './lib/createTracingMiddleware';
import { PatchStore } from './lib/PatchStore';
import { sanitizeUIState } from './lib/state-utils';

const { TRIGGER_TYPES } = NotificationServicesController.Constants;
export const METAMASK_CONTROLLER_EVENTS = {
// Fired after state changes that impact the extension badge (unapproved msg count)
// The process of updating the badge happens in app/scripts/background.js.
Expand All @@ -380,7 +380,6 @@ export const METAMASK_CONTROLLER_EVENTS = {
'NotificationServicesController:notificationsListUpdated',
METAMASK_NOTIFICATIONS_MARK_AS_READ:
'NotificationServicesController:markNotificationsAsRead',
NOTIFICATIONS_STATE_CHANGE: 'NotificationController:stateChange',
};

// stream channels
Expand Down Expand Up @@ -1392,13 +1391,6 @@ export default class MetamaskController extends EventEmitter {
},
});

this.notificationController = new NotificationController({
messenger: this.controllerMessenger.getRestricted({
name: 'NotificationController',
}),
state: initState.NotificationController,
});

this.rateLimitController = new RateLimitController({
state: initState.RateLimitController,
messenger: this.controllerMessenger.getRestricted({
Expand Down Expand Up @@ -1426,11 +1418,28 @@ export default class MetamaskController extends EventEmitter {
rateLimitTimeout: 300000,
},
showInAppNotification: {
method: (origin, message) => {
method: (origin, args) => {
const { message, title, footerLink } = args;

const detailedView = {
title,
...(footerLink ? { footerLink } : {}),
interfaceId: args.content,
};

const notification = {
data: {
message,
origin,
...(args.content ? { detailedView } : {}),
},
type: 'snap',
readDate: null,
};

this.controllerMessenger.call(
'NotificationController:show',
origin,
message,
'NotificationServicesController:updateMetamaskNotificationsList',
notification,
);

return null;
Expand Down Expand Up @@ -1488,6 +1497,7 @@ export default class MetamaskController extends EventEmitter {
`${this.approvalController.name}:hasRequest`,
`${this.approvalController.name}:acceptRequest`,
`${this.snapController.name}:get`,
'NotificationServicesController:getNotificationsByType',
],
});

Expand Down Expand Up @@ -2398,7 +2408,6 @@ export default class MetamaskController extends EventEmitter {
SnapController: this.snapController,
CronjobController: this.cronjobController,
SnapsRegistry: this.snapsRegistry,
NotificationController: this.notificationController,
SnapInterfaceController: this.snapInterfaceController,
SnapInsightsController: this.snapInsightsController,
///: BEGIN:ONLY_INCLUDE_IF(build-mmi)
Expand Down Expand Up @@ -2453,7 +2462,6 @@ export default class MetamaskController extends EventEmitter {
SnapController: this.snapController,
CronjobController: this.cronjobController,
SnapsRegistry: this.snapsRegistry,
NotificationController: this.notificationController,
SnapInterfaceController: this.snapInterfaceController,
SnapInsightsController: this.snapInsightsController,
///: BEGIN:ONLY_INCLUDE_IF(build-mmi)
Expand Down Expand Up @@ -2773,14 +2781,15 @@ export default class MetamaskController extends EventEmitter {
origin,
args.message,
),
showInAppNotification: (origin, args) =>
this.controllerMessenger.call(
showInAppNotification: (origin, args) => {
return this.controllerMessenger.call(
'RateLimitController:call',
origin,
'showInAppNotification',
origin,
args.message,
),
args,
);
},
updateSnapState: this.controllerMessenger.call.bind(
this.controllerMessenger,
'SnapController:updateSnapState',
Expand Down Expand Up @@ -2825,24 +2834,6 @@ export default class MetamaskController extends EventEmitter {
};
}

/**
* Deletes the specified notifications from state.
*
* @param {string[]} ids - The notifications ids to delete.
*/
dismissNotifications(ids) {
this.notificationController.dismiss(ids);
}

/**
* Updates the readDate attribute of the specified notifications.
*
* @param {string[]} ids - The notifications ids to mark as read.
*/
markNotificationsAsRead(ids) {
this.notificationController.markRead(ids);
}

/**
* Sets up BaseController V2 event subscriptions. Currently, this includes
* the subscriptions necessary to notify permission subjects of account
Expand Down Expand Up @@ -3042,16 +3033,16 @@ export default class MetamaskController extends EventEmitter {
this.controllerMessenger.subscribe(
`${this.snapController.name}:snapUninstalled`,
(truncatedSnap) => {
const notificationIds = Object.values(
this.notificationController.state.notifications,
const notificationIds = this.getNotificationsByType(
TRIGGER_TYPES.SNAP,
).reduce((idList, notification) => {
if (notification.origin === truncatedSnap.id) {
idList.push(notification.id);
}
return idList;
}, []);

this.dismissNotifications(notificationIds);
this.deleteNotificationsById(notificationIds);

const snapId = truncatedSnap.id;
const snapCategory = this._getSnapMetadata(snapId)?.category;
Expand Down Expand Up @@ -3809,8 +3800,6 @@ export default class MetamaskController extends EventEmitter {
this.controllerMessenger,
'SnapController:revokeDynamicPermissions',
),
dismissNotifications: this.dismissNotifications.bind(this),
markNotificationsAsRead: this.markNotificationsAsRead.bind(this),
disconnectOriginFromSnap: this.controllerMessenger.call.bind(
this.controllerMessenger,
'SnapController:disconnectOrigin',
Expand Down Expand Up @@ -4094,6 +4083,14 @@ export default class MetamaskController extends EventEmitter {
notificationServicesController.fetchAndUpdateMetamaskNotifications.bind(
notificationServicesController,
),
deleteNotificationsById:
notificationServicesController.deleteNotificationsById.bind(
notificationServicesController,
),
getNotificationsByType:
notificationServicesController.getNotificationsByType.bind(
notificationServicesController,
),
markMetamaskNotificationsAsRead:
notificationServicesController.markMetamaskNotificationsAsRead.bind(
notificationServicesController,
Expand Down Expand Up @@ -4322,8 +4319,6 @@ export default class MetamaskController extends EventEmitter {

// Clear snap state
this.snapController.clearState();
// Clear notification state
this.notificationController.clear();

// clear accounts in AccountTrackerController
this.accountTrackerController.clearAccounts();
Expand Down
28 changes: 0 additions & 28 deletions app/scripts/metamask-controller.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -245,17 +245,6 @@ const firstTimeState = {
},
),
},
NotificationController: {
notifications: {
[NOTIFICATION_ID]: {
id: NOTIFICATION_ID,
origin: 'local:http://localhost:8086/',
createdDate: 1652967897732,
readDate: null,
message: 'Hello, http://localhost:8086!',
},
},
},
PhishingController: {
phishingLists: [
{
Expand Down Expand Up @@ -1812,23 +1801,6 @@ describe('MetaMaskController', () => {
});
});

describe('markNotificationsAsRead', () => {
it('marks the notification as read', () => {
metamaskController.markNotificationsAsRead([NOTIFICATION_ID]);
const readNotification =
metamaskController.getState().notifications[NOTIFICATION_ID];
expect(readNotification.readDate).not.toBeNull();
});
});

describe('dismissNotifications', () => {
it('deletes the notification from state', () => {
metamaskController.dismissNotifications([NOTIFICATION_ID]);
const state = metamaskController.getState().notifications;
expect(Object.values(state)).not.toContain(NOTIFICATION_ID);
});
});

describe('getTokenStandardAndDetails', () => {
it('gets token data from the token list if available, and with a balance retrieved by fetchTokenBalance', async () => {
const providerResultStub = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,6 @@
"allNfts": "object",
"ignoredNfts": "object"
},
"NotificationController": { "notifications": "object" },
"NotificationServicesController": {
"subscriptionAccountsSeen": "object",
"isMetamaskNotificationsFeatureSeen": "boolean",
Expand Down
12 changes: 6 additions & 6 deletions ui/hooks/metamask-notifications/useCounter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@ import {
getFeatureAnnouncementsUnreadCount,
getOnChainMetamaskNotificationsReadCount,
getOnChainMetamaskNotificationsUnreadCount,
getSnapNotificationsReadCount,
getSnapNotificationsUnreadCount,
} from '../../selectors/metamask-notifications/metamask-notifications';
import {
getReadNotificationsCount,
getUnreadNotificationsCount,
} from '../../selectors';

const useSnapNotificationdCount = () => {
const unreadSnapNotificationsCount = useSelector(getUnreadNotificationsCount);
const readSnapNotificationsCount = useSelector(getReadNotificationsCount);
const unreadSnapNotificationsCount = useSelector(
getSnapNotificationsUnreadCount,
);
const readSnapNotificationsCount = useSelector(getSnapNotificationsReadCount);
return { unreadSnapNotificationsCount, readSnapNotificationsCount };
};

Expand Down
24 changes: 18 additions & 6 deletions ui/pages/notifications/notification-components/snap/snap.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,38 @@
import React, { useContext } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { useHistory } from 'react-router-dom';
import { NotificationServicesController } from '@metamask/notification-services-controller'; } from '@metamask/notification-services-controller';
import {
MetaMetricsEventCategory,
MetaMetricsEventName,
} from '../../../../../shared/constants/metametrics';
import { MetaMetricsContext } from '../../../../contexts/metametrics';
import { NotificationListItemSnap } from '../../../../components/multichain';
import type { SnapNotification } from '../../snap/types/types';
import { getSnapsMetadata } from '../../../../selectors';
import { markNotificationsAsRead } from '../../../../store/actions';
import { getSnapRoute, getSnapName } from '../../../../helpers/utils/util';
import { useMarkNotificationAsRead } from '../../../../hooks/metamask-notifications/useNotifications';

type SnapComponentProps = {
snapNotification: SnapNotification;
snapNotification: NotificationServicesController.Types.INotification;
};

export const SnapComponent = ({ snapNotification }: SnapComponentProps) => {
const dispatch = useDispatch();
const history = useHistory();
const trackEvent = useContext(MetaMetricsContext);
const { markNotificationAsRead } = useMarkNotificationAsRead();

const snapsMetadata = useSelector(getSnapsMetadata);

const snapsNameGetter = getSnapName(snapsMetadata);

const handleSnapClick = () => {
dispatch(markNotificationsAsRead([snapNotification.id]));
markNotificationAsRead([
{
id: snapNotification.id,
type: snapNotification.type,
isRead: snapNotification.isRead,
},
]);
trackEvent({
category: MetaMetricsEventCategory.NotificationInteraction,
event: MetaMetricsEventName.NotificationClicked,
Expand All @@ -39,7 +45,13 @@ export const SnapComponent = ({ snapNotification }: SnapComponentProps) => {
};

const handleSnapButton = () => {
dispatch(markNotificationsAsRead([snapNotification.id]));
markNotificationAsRead([
{
id: snapNotification.id,
type: snapNotification.type,
isRead: snapNotification.isRead,
},
]);
trackEvent({
category: MetaMetricsEventCategory.NotificationInteraction,
event: MetaMetricsEventName.NotificationClicked,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,13 @@ import { getUnreadNotifications } from '../../selectors';
import { markNotificationsAsRead } from '../../store/actions';
import { Box, Button, ButtonVariant } from '../../components/component-library';
import { BlockSize } from '../../helpers/constants/design-system';
import type { NotificationType } from './notifications';
import { SNAP } from './snap/types/types';

type Notification = NotificationServicesController.Types.INotification;
type MarkAsReadNotificationsParam =
NotificationServicesController.Types.MarkAsReadNotificationsParam;

export type NotificationsListReadAllButtonProps = {
notifications: NotificationType[];
notifications: Notification[];
};

export const NotificationsListReadAllButton = ({
Expand All @@ -40,7 +38,7 @@ export const NotificationsListReadAllButton = ({
.filter(
(notification): notification is Notification =>
(notification as Notification).id !== undefined &&
notification.type !== SNAP,
notification.type !== TRIGGER_TYPES.SNAP,
)
.map((notification: Notification) => ({
id: notification.id,
Expand Down
2 changes: 1 addition & 1 deletion ui/pages/notifications/notifications-list.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { NotificationsList } from './notifications-list';
import { TAB_KEYS } from './notifications';

jest.mock('../../store/actions', () => ({
deleteExpiredNotifications: jest.fn(() => () => Promise.resolve()),
deleteExpiredSnapNotifications: jest.fn(() => () => Promise.resolve()),
fetchAndUpdateMetamaskNotifications: jest.fn(() => () => Promise.resolve()),
}));

Expand Down
Loading
Loading