@@ -7,6 +7,7 @@ import type {
77import type { UserStorageController } from '@metamask/profile-sync-controller' ;
88import { AuthenticationController } from '@metamask/profile-sync-controller' ;
99
10+ import { createMockSnapNotification } from './__fixtures__' ;
1011import {
1112 createMockFeatureAnnouncementAPIResult ,
1213 createMockFeatureAnnouncementRaw ,
@@ -25,6 +26,7 @@ import {
2526 mockMarkNotificationsAsRead ,
2627} from './__fixtures__/mockServices' ;
2728import { waitFor } from './__fixtures__/test-utils' ;
29+ import { TRIGGER_TYPES } from './constants' ;
2830import NotificationServicesController , {
2931 defaultState ,
3032} from './NotificationServicesController' ;
@@ -35,7 +37,9 @@ import type {
3537 NotificationServicesPushControllerDisablePushNotifications ,
3638 NotificationServicesPushControllerUpdateTriggerPushNotifications ,
3739} from './NotificationServicesController' ;
40+ import { processFeatureAnnouncement } from './processors' ;
3841import { processNotification } from './processors/process-notifications' ;
42+ import { processSnapNotification } from './processors/process-snap-notifications' ;
3943import * as OnChainNotifications from './services/onchain-notifications' ;
4044import type { UserStorage } from './types/user-storage/user-storage' ;
4145import * as Utils from './utils/utils' ;
@@ -472,23 +476,30 @@ describe('metamask-notifications - fetchAndUpdateMetamaskNotifications()', () =>
472476 } ;
473477 } ;
474478
475- it ( 'processes and shows feature announcements and wallet notifications' , async ( ) => {
479+ it ( 'processes and shows feature announcements, wallet and snap notifications' , async ( ) => {
476480 const {
477481 messenger,
478482 mockFeatureAnnouncementAPIResult,
479483 mockListNotificationsAPIResult,
480484 } = arrangeMocks ( ) ;
481485
486+ const snapNotification = createMockSnapNotification ( ) ;
487+ const processedSnapNotification = processSnapNotification ( snapNotification ) ;
488+
482489 const controller = new NotificationServicesController ( {
483490 messenger,
484491 env : { featureAnnouncements : featureAnnouncementsEnv } ,
485- state : { ...defaultState , isFeatureAnnouncementsEnabled : true } ,
492+ state : {
493+ ...defaultState ,
494+ isFeatureAnnouncementsEnabled : true ,
495+ metamaskNotificationsList : [ processedSnapNotification ] ,
496+ } ,
486497 } ) ;
487498
488499 const result = await controller . fetchAndUpdateMetamaskNotifications ( ) ;
489500
490501 // Should have 1 feature announcement and 1 wallet notification
491- expect ( result ) . toHaveLength ( 2 ) ;
502+ expect ( result ) . toHaveLength ( 3 ) ;
492503 expect (
493504 result . find (
494505 ( n ) => n . id === mockFeatureAnnouncementAPIResult . items ?. [ 0 ] . fields . id ,
@@ -497,9 +508,10 @@ describe('metamask-notifications - fetchAndUpdateMetamaskNotifications()', () =>
497508 expect (
498509 result . find ( ( n ) => n . id === mockListNotificationsAPIResult [ 0 ] . id ) ,
499510 ) . toBeDefined ( ) ;
511+ expect ( result . find ( ( n ) => n . type === TRIGGER_TYPES . SNAP ) ) . toBeDefined ( ) ;
500512
501513 // State is also updated
502- expect ( controller . state . metamaskNotificationsList ) . toHaveLength ( 2 ) ;
514+ expect ( controller . state . metamaskNotificationsList ) . toHaveLength ( 3 ) ;
503515 } ) ;
504516
505517 it ( 'only fetches and processes feature announcements if not authenticated' , async ( ) => {
@@ -529,6 +541,148 @@ describe('metamask-notifications - fetchAndUpdateMetamaskNotifications()', () =>
529541 } ) ;
530542} ) ;
531543
544+ describe ( 'metamask-notifications - getNotificationsByType' , ( ) => {
545+ it ( 'can fetch notifications by their type' , async ( ) => {
546+ const { messenger } = mockNotificationMessenger ( ) ;
547+ const controller = new NotificationServicesController ( {
548+ messenger,
549+ env : { featureAnnouncements : featureAnnouncementsEnv } ,
550+ } ) ;
551+
552+ const processedSnapNotification = processSnapNotification (
553+ createMockSnapNotification ( ) ,
554+ ) ;
555+ const processedFeatureAnnouncement = processFeatureAnnouncement (
556+ createMockFeatureAnnouncementRaw ( ) ,
557+ ) ;
558+
559+ await controller . updateMetamaskNotificationsList ( processedSnapNotification ) ;
560+ await controller . updateMetamaskNotificationsList (
561+ processedFeatureAnnouncement ,
562+ ) ;
563+
564+ expect ( controller . state . metamaskNotificationsList ) . toHaveLength ( 2 ) ;
565+
566+ const filteredNotifications = controller . getNotificationsByType (
567+ TRIGGER_TYPES . SNAP ,
568+ ) ;
569+
570+ expect ( filteredNotifications ) . toHaveLength ( 1 ) ;
571+ expect ( filteredNotifications ) . toStrictEqual ( [
572+ {
573+ type : TRIGGER_TYPES . SNAP ,
574+ id : expect . any ( String ) ,
575+ createdAt : expect . any ( String ) ,
576+ isRead : false ,
577+ readDate : null ,
578+ data : {
579+ message : 'fooBar' ,
580+ origin : '@metamask/example-snap' ,
581+ detailedView : {
582+ title : 'Detailed View' ,
583+ interfaceId : '1' ,
584+ footerLink : {
585+ text : 'Go Home' ,
586+ href : 'metamask://client/' ,
587+ } ,
588+ } ,
589+ } ,
590+ } ,
591+ ] ) ;
592+ } ) ;
593+ } ) ;
594+
595+ describe ( 'metamask-notifications - deleteNotificationsById' , ( ) => {
596+ it ( 'will delete a notification by its id' , async ( ) => {
597+ const { messenger } = mockNotificationMessenger ( ) ;
598+ const processedSnapNotification = processSnapNotification (
599+ createMockSnapNotification ( ) ,
600+ ) ;
601+ const controller = new NotificationServicesController ( {
602+ messenger,
603+ env : { featureAnnouncements : featureAnnouncementsEnv } ,
604+ state : { metamaskNotificationsList : [ processedSnapNotification ] } ,
605+ } ) ;
606+
607+ await controller . deleteNotificationsById ( [ processedSnapNotification . id ] ) ;
608+
609+ expect ( controller . state . metamaskNotificationsList ) . toHaveLength ( 0 ) ;
610+ } ) ;
611+
612+ it ( 'will batch delete notifications' , async ( ) => {
613+ const { messenger } = mockNotificationMessenger ( ) ;
614+ const processedSnapNotification1 = processSnapNotification (
615+ createMockSnapNotification ( ) ,
616+ ) ;
617+ const processedSnapNotification2 = processSnapNotification (
618+ createMockSnapNotification ( ) ,
619+ ) ;
620+ const controller = new NotificationServicesController ( {
621+ messenger,
622+ env : { featureAnnouncements : featureAnnouncementsEnv } ,
623+ state : {
624+ metamaskNotificationsList : [
625+ processedSnapNotification1 ,
626+ processedSnapNotification2 ,
627+ ] ,
628+ } ,
629+ } ) ;
630+
631+ await controller . deleteNotificationsById ( [
632+ processedSnapNotification1 . id ,
633+ processedSnapNotification2 . id ,
634+ ] ) ;
635+
636+ expect ( controller . state . metamaskNotificationsList ) . toHaveLength ( 0 ) ;
637+ } ) ;
638+
639+ it ( 'will throw if a notification is not found' , async ( ) => {
640+ const { messenger } = mockNotificationMessenger ( ) ;
641+ const processedSnapNotification = processSnapNotification (
642+ createMockSnapNotification ( ) ,
643+ ) ;
644+ const controller = new NotificationServicesController ( {
645+ messenger,
646+ env : { featureAnnouncements : featureAnnouncementsEnv } ,
647+ state : { metamaskNotificationsList : [ processedSnapNotification ] } ,
648+ } ) ;
649+
650+ await expect ( controller . deleteNotificationsById ( [ 'foo' ] ) ) . rejects . toThrow (
651+ 'The notification to be deleted does not exist.' ,
652+ ) ;
653+
654+ expect ( controller . state . metamaskNotificationsList ) . toHaveLength ( 1 ) ;
655+ } ) ;
656+
657+ it ( 'will throw if the notification to be deleted is not locally persisted' , async ( ) => {
658+ const { messenger } = mockNotificationMessenger ( ) ;
659+ const processedSnapNotification = processSnapNotification (
660+ createMockSnapNotification ( ) ,
661+ ) ;
662+ const processedFeatureAnnouncement = processFeatureAnnouncement (
663+ createMockFeatureAnnouncementRaw ( ) ,
664+ ) ;
665+ const controller = new NotificationServicesController ( {
666+ messenger,
667+ env : { featureAnnouncements : featureAnnouncementsEnv } ,
668+ state : {
669+ metamaskNotificationsList : [
670+ processedFeatureAnnouncement ,
671+ processedSnapNotification ,
672+ ] ,
673+ } ,
674+ } ) ;
675+
676+ await expect (
677+ controller . deleteNotificationsById ( [ processedFeatureAnnouncement . id ] ) ,
678+ ) . rejects . toThrow (
679+ 'The notification type of "features_announcement" is not locally persisted, only the following types can use this function: snap.' ,
680+ ) ;
681+
682+ expect ( controller . state . metamaskNotificationsList ) . toHaveLength ( 2 ) ;
683+ } ) ;
684+ } ) ;
685+
532686describe ( 'metamask-notifications - markMetamaskNotificationsAsRead()' , ( ) => {
533687 const arrangeMocks = ( options ?: { onChainMarkAsReadFails : boolean } ) => {
534688 const messengerMocks = mockNotificationMessenger ( ) ;
@@ -576,6 +730,38 @@ describe('metamask-notifications - markMetamaskNotificationsAsRead()', () => {
576730 // We can debate & change implementation if it makes sense to mark as read locally if external APIs fail.
577731 expect ( controller . state . metamaskNotificationsReadList ) . toHaveLength ( 1 ) ;
578732 } ) ;
733+
734+ it ( 'updates snap notifications as read' , async ( ) => {
735+ const { messenger } = arrangeMocks ( ) ;
736+ const processedSnapNotification = processSnapNotification (
737+ createMockSnapNotification ( ) ,
738+ ) ;
739+ const controller = new NotificationServicesController ( {
740+ messenger,
741+ env : { featureAnnouncements : featureAnnouncementsEnv } ,
742+ state : {
743+ metamaskNotificationsList : [ processedSnapNotification ] ,
744+ } ,
745+ } ) ;
746+
747+ await controller . markMetamaskNotificationsAsRead ( [
748+ {
749+ type : TRIGGER_TYPES . SNAP ,
750+ id : processedSnapNotification . id ,
751+ isRead : false ,
752+ } ,
753+ ] ) ;
754+
755+ // Should see 1 item in controller read state
756+ expect ( controller . state . metamaskNotificationsReadList ) . toHaveLength ( 1 ) ;
757+
758+ // The notification should have a read date
759+ expect (
760+ // @ts -expect-error readDate property is guaranteed to exist
761+ // as we're dealing with a snap notification
762+ controller . state . metamaskNotificationsList [ 0 ] . readDate ,
763+ ) . not . toBeNull ( ) ;
764+ } ) ;
579765} ) ;
580766
581767describe ( 'metamask-notifications - enableMetamaskNotifications()' , ( ) => {
@@ -670,6 +856,42 @@ describe('metamask-notifications - disableMetamaskNotifications()', () => {
670856 } ) ;
671857} ) ;
672858
859+ describe ( 'metamask-notifications - updateMetamaskNotificationsList' , ( ) => {
860+ it ( 'can add and process a new notification to the notifications list' , async ( ) => {
861+ const { messenger } = mockNotificationMessenger ( ) ;
862+ const controller = new NotificationServicesController ( {
863+ messenger,
864+ env : { featureAnnouncements : featureAnnouncementsEnv } ,
865+ state : { isNotificationServicesEnabled : true } ,
866+ } ) ;
867+ const processedSnapNotification = processSnapNotification (
868+ createMockSnapNotification ( ) ,
869+ ) ;
870+ await controller . updateMetamaskNotificationsList ( processedSnapNotification ) ;
871+ expect ( controller . state . metamaskNotificationsList ) . toStrictEqual ( [
872+ {
873+ type : TRIGGER_TYPES . SNAP ,
874+ id : expect . any ( String ) ,
875+ createdAt : expect . any ( String ) ,
876+ readDate : null ,
877+ isRead : false ,
878+ data : {
879+ message : 'fooBar' ,
880+ origin : '@metamask/example-snap' ,
881+ detailedView : {
882+ title : 'Detailed View' ,
883+ interfaceId : '1' ,
884+ footerLink : {
885+ text : 'Go Home' ,
886+ href : 'metamask://client/' ,
887+ } ,
888+ } ,
889+ } ,
890+ } ,
891+ ] ) ;
892+ } ) ;
893+ } ) ;
894+
673895// Type-Computation - we are extracting args and parameters from a generic type utility
674896// Thus this `AnyFunc` can be used to help constrain the generic parameters correctly
675897// eslint-disable-next-line @typescript-eslint/no-explicit-any
0 commit comments