diff --git a/packages/notification-services-controller/CHANGELOG.md b/packages/notification-services-controller/CHANGELOG.md index 5aa4c3f76c..00f3de76d3 100644 --- a/packages/notification-services-controller/CHANGELOG.md +++ b/packages/notification-services-controller/CHANGELOG.md @@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add `PushAnalyticsPayload` type carrying the first-class push notification fields (`notification_id`, `notification_type`, `notification_subtype`, `profile_id`, `chain_id`, `deeplink`). ([#8944](https://github.com/MetaMask/core/pull/8944)) +- Add `getNotificationSubtype` helper that derives a normalised `notification_subtype` from an `INotification`, so both clients pull the subtype from one place. ([#8944](https://github.com/MetaMask/core/pull/8944)) +- Export `toPushAnalyticsPayload` from `@metamask/notification-services-controller/push-services` so web and mobile clients can parse FCM analytics fields from a shared helper. ([#8944](https://github.com/MetaMask/core/pull/8944)) + +### Changed + +- **BREAKING:** The `NotificationServicesPushController:onNewNotifications` and `NotificationServicesPushController:pushNotificationClicked` messenger events now carry `PushAnalyticsPayload` instead of `INotification`. ([#8944](https://github.com/MetaMask/core/pull/8944)) + - The push payload no longer carries the full notification body; clients construct their analytics events directly from the first-class fields. + - The `onReceivedHandler` / `onClickHandler` callbacks passed to `createSubscribeToPushNotifications` now receive a `PushAnalyticsPayload` instead of an `INotification`. +- On push receive, the controller now re-fetches the notifications list from the API rather than inserting the push payload, since the push payload no longer contains the notification body. ([#8944](https://github.com/MetaMask/core/pull/8944)) + +### Removed + +- **BREAKING:** Remove the nested `data["data"]` / `metadata` FCM payload parsing path; push payloads are now read from the top-level FCM fields written by push-services. ([#8944](https://github.com/MetaMask/core/pull/8944)) + ## [24.1.2] ### Changed diff --git a/packages/notification-services-controller/src/NotificationServicesController/NotificationServicesController.test.ts b/packages/notification-services-controller/src/NotificationServicesController/NotificationServicesController.test.ts index 52514d76ce..6560199f0c 100644 --- a/packages/notification-services-controller/src/NotificationServicesController/NotificationServicesController.test.ts +++ b/packages/notification-services-controller/src/NotificationServicesController/NotificationServicesController.test.ts @@ -1236,6 +1236,7 @@ describe('NotificationServicesController', () => { expect(filteredNotifications).toStrictEqual([ { type: TRIGGER_TYPES.SNAP, + notification_subtype: TRIGGER_TYPES.SNAP, id: expect.any(String), createdAt: expect.any(String), isRead: false, @@ -1605,6 +1606,7 @@ describe('NotificationServicesController', () => { expect(controller.state.metamaskNotificationsList).toStrictEqual([ { type: TRIGGER_TYPES.SNAP, + notification_subtype: TRIGGER_TYPES.SNAP, id: expect.any(String), createdAt: expect.any(String), readDate: null, diff --git a/packages/notification-services-controller/src/NotificationServicesController/NotificationServicesController.ts b/packages/notification-services-controller/src/NotificationServicesController/NotificationServicesController.ts index bbc558c4f3..da0c924632 100644 --- a/packages/notification-services-controller/src/NotificationServicesController/NotificationServicesController.ts +++ b/packages/notification-services-controller/src/NotificationServicesController/NotificationServicesController.ts @@ -517,9 +517,11 @@ export class NotificationServicesController extends BaseController< subscribe: (): void => { this.messenger.subscribe( 'NotificationServicesPushController:onNewNotifications', - (notification): void => { + (): void => { + // The push payload no longer carries the full notification body, so + // re-fetch the inbox from the API to surface the new notification. // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.updateMetamaskNotificationsList(notification); + this.fetchAndUpdateMetamaskNotifications(); }, ); }, diff --git a/packages/notification-services-controller/src/NotificationServicesController/index.ts b/packages/notification-services-controller/src/NotificationServicesController/index.ts index 745d6f723e..64ee6df217 100644 --- a/packages/notification-services-controller/src/NotificationServicesController/index.ts +++ b/packages/notification-services-controller/src/NotificationServicesController/index.ts @@ -12,6 +12,7 @@ export * from './constants'; export * as Mocks from './mocks'; export * from '../shared'; export { isVersionInBounds } from './utils/isVersionInBounds'; +export { getNotificationSubtype } from './utils/get-notification-subtype'; export type { NotificationServicesControllerInitAction, diff --git a/packages/notification-services-controller/src/NotificationServicesController/processors/process-api-notifications.ts b/packages/notification-services-controller/src/NotificationServicesController/processors/process-api-notifications.ts index dac4f2ed38..d46a40b303 100644 --- a/packages/notification-services-controller/src/NotificationServicesController/processors/process-api-notifications.ts +++ b/packages/notification-services-controller/src/NotificationServicesController/processors/process-api-notifications.ts @@ -1,5 +1,6 @@ import type { NormalisedAPINotification } from '../types/notification-api/notification-api'; import type { INotification } from '../types/notification/notification'; +import { getNotificationSubtype } from '../utils/get-notification-subtype'; import { shouldAutoExpire } from '../utils/should-auto-expire'; /** @@ -17,6 +18,7 @@ export function processAPINotifications( return { ...notification, id: notification.id, + notification_subtype: getNotificationSubtype(notification), createdAt: createdAtDate.toISOString(), isRead: expired || !notification.unread, }; diff --git a/packages/notification-services-controller/src/NotificationServicesController/processors/process-feature-announcement.ts b/packages/notification-services-controller/src/NotificationServicesController/processors/process-feature-announcement.ts index 7033e0451f..5457e15f01 100644 --- a/packages/notification-services-controller/src/NotificationServicesController/processors/process-feature-announcement.ts +++ b/packages/notification-services-controller/src/NotificationServicesController/processors/process-feature-announcement.ts @@ -1,5 +1,6 @@ import type { FeatureAnnouncementRawNotification } from '../types/feature-announcement/feature-announcement'; import type { INotification } from '../types/notification/notification'; +import { getNotificationSubtype } from '../utils/get-notification-subtype'; import { shouldAutoExpire } from '../utils/should-auto-expire'; /** @@ -32,6 +33,7 @@ export function processFeatureAnnouncement( return { type: notification.type, id: notification.data.id, + notification_subtype: getNotificationSubtype(notification), createdAt: new Date(notification.createdAt).toISOString(), data: notification.data, isRead: false, diff --git a/packages/notification-services-controller/src/NotificationServicesController/processors/process-snap-notifications.ts b/packages/notification-services-controller/src/NotificationServicesController/processors/process-snap-notifications.ts index 17227edaa6..4aba3c65e2 100644 --- a/packages/notification-services-controller/src/NotificationServicesController/processors/process-snap-notifications.ts +++ b/packages/notification-services-controller/src/NotificationServicesController/processors/process-snap-notifications.ts @@ -2,6 +2,7 @@ import { v4 as uuid } from 'uuid'; import type { INotification } from '../types'; import type { RawSnapNotification } from '../types/snaps'; +import { getNotificationSubtype } from '../utils/get-notification-subtype'; /** * Processes a snap notification into a normalized shape. @@ -15,6 +16,7 @@ export const processSnapNotification = ( const { data, type, readDate } = snapNotification; return { id: uuid(), + notification_subtype: getNotificationSubtype(snapNotification), readDate, createdAt: new Date().toISOString(), isRead: false, diff --git a/packages/notification-services-controller/src/NotificationServicesController/types/notification/notification.ts b/packages/notification-services-controller/src/NotificationServicesController/types/notification/notification.ts index aa7102aa27..66f2700a28 100644 --- a/packages/notification-services-controller/src/NotificationServicesController/types/notification/notification.ts +++ b/packages/notification-services-controller/src/NotificationServicesController/types/notification/notification.ts @@ -5,6 +5,8 @@ import type { Compute } from '../type-utils'; export type BaseNotification = { id: string; + // eslint-disable-next-line @typescript-eslint/naming-convention + notification_subtype: string; createdAt: string; isRead: boolean; }; diff --git a/packages/notification-services-controller/src/NotificationServicesController/utils/get-notification-subtype.test.ts b/packages/notification-services-controller/src/NotificationServicesController/utils/get-notification-subtype.test.ts new file mode 100644 index 0000000000..0fd56d333d --- /dev/null +++ b/packages/notification-services-controller/src/NotificationServicesController/utils/get-notification-subtype.test.ts @@ -0,0 +1,39 @@ +import { TRIGGER_TYPES } from '../constants/notification-schema'; +import { createMockFeatureAnnouncementRaw } from '../mocks/mock-feature-announcements'; +import { + createMockNotificationEthReceived, + createMockPlatformNotification, +} from '../mocks/mock-raw-notifications'; +import { createMockSnapNotification } from '../mocks/mock-snap-notification'; +import { processNotification } from '../processors/process-notifications'; +import { getNotificationSubtype } from './get-notification-subtype'; + +describe('getNotificationSubtype', () => { + it('returns the trigger kind for on-chain notifications', () => { + const notification = processNotification( + createMockNotificationEthReceived(), + ); + expect(getNotificationSubtype(notification)).toBe( + TRIGGER_TYPES.ETH_RECEIVED, + ); + }); + + it('falls back to the type label for platform notifications', () => { + const notification = processNotification(createMockPlatformNotification()); + expect(getNotificationSubtype(notification)).toBe(TRIGGER_TYPES.PLATFORM); + }); + + it('returns the snap subtype for snap notifications', () => { + const notification = processNotification(createMockSnapNotification()); + expect(getNotificationSubtype(notification)).toBe(TRIGGER_TYPES.SNAP); + }); + + it('returns a stable label for feature-announcement notifications', () => { + const notification = processNotification( + createMockFeatureAnnouncementRaw(), + ); + expect(getNotificationSubtype(notification)).toBe( + TRIGGER_TYPES.FEATURES_ANNOUNCEMENT, + ); + }); +}); diff --git a/packages/notification-services-controller/src/NotificationServicesController/utils/get-notification-subtype.ts b/packages/notification-services-controller/src/NotificationServicesController/utils/get-notification-subtype.ts new file mode 100644 index 0000000000..86340d71c1 --- /dev/null +++ b/packages/notification-services-controller/src/NotificationServicesController/utils/get-notification-subtype.ts @@ -0,0 +1,48 @@ +import { TRIGGER_TYPES } from '../constants/notification-schema'; +import type { RawNotificationUnion } from '../types/notification/notification'; + +/** + * Derives the normalised `notification_subtype` for a processed in-app + * notification. This is the team-owned axis (e.g. `eth_received`) and is + * always derivable from an `INotification`, so every consumer (both clients) + * pulls it from one place rather than recomputing a fallback chain. + * + * - on-chain: the trigger kind (`payload.data.kind`, e.g. `eth_received`). + * - snap: the snap notification type (`snap`, already snake_case). + * - feature-announcement: §5.3 calls for a stable per-campaign id, pending + * confirmation from the announcements team that one exists. Until confirmed, + * we use the `features_announcement` label as the single value. + * - platform: the server-set `notification_subtype`. The backend stores it + * (notify-notification-services §4.3), but the `/api/v3/notifications` inbox + * response does not expose it yet, so it is absent from the generated + * `schema.ts` and we fall back to `type` (`platform`). + * + * @param notification - a raw or processed notification. + * @returns the normalised subtype string. + */ +export function getNotificationSubtype( + notification: RawNotificationUnion, +): string { + switch (notification.type) { + case TRIGGER_TYPES.FEATURES_ANNOUNCEMENT: + // §5.3 calls for a stable per-campaign id here, pending confirmation from + // the announcements team that one exists. Until confirmed, use the + // `features_announcement` label as the single value. + return TRIGGER_TYPES.FEATURES_ANNOUNCEMENT; + case TRIGGER_TYPES.SNAP: + // Snap notification type, already snake_case in the existing shape. + return TRIGGER_TYPES.SNAP; + default: + // On-chain: the trigger kind (e.g. `eth_received`). + if (notification.notification_type === 'on-chain') { + return notification.payload.data.kind; + } + // Platform: §5.3 wants the server-set `notification_subtype`. It is + // stored backend-side (§4.3) but not returned on the `/api/v3/notifications` + // inbox response, so it is absent from `schema.ts`. Fall back to `type` + // (`platform`) until the inbox API exposes it. + // TODO: return notification.notification_subtype once the inbox API + // response includes it (needs a notify-notification-services change). + return notification.type; + } +} diff --git a/packages/notification-services-controller/src/NotificationServicesPushController/NotificationServicesPushController.ts b/packages/notification-services-controller/src/NotificationServicesPushController/NotificationServicesPushController.ts index 44842609fa..278b596ab8 100644 --- a/packages/notification-services-controller/src/NotificationServicesPushController/NotificationServicesPushController.ts +++ b/packages/notification-services-controller/src/NotificationServicesPushController/NotificationServicesPushController.ts @@ -8,7 +8,6 @@ import type { Messenger } from '@metamask/messenger'; import type { AuthenticationController } from '@metamask/profile-sync-controller'; import log from 'loglevel'; -import type { Types } from '../NotificationServicesController'; import type { NotificationServicesPushControllerMethodActions } from './NotificationServicesPushController-method-action-types'; import type { ENV } from './services/endpoints'; import type { RegToken } from './services/services'; @@ -18,7 +17,7 @@ import { deactivatePushNotifications, updateLinksAPI, } from './services/services'; -import type { PushNotificationEnv } from './types'; +import type { PushAnalyticsPayload, PushNotificationEnv } from './types'; import type { PushService } from './types/push-service-interface'; const controllerName = 'NotificationServicesPushController'; @@ -59,12 +58,12 @@ export type NotificationServicesPushControllerStateChangeEvent = export type NotificationServicesPushControllerOnNewNotificationEvent = { type: `${typeof controllerName}:onNewNotifications`; - payload: [Types.INotification]; + payload: [PushAnalyticsPayload]; }; export type NotificationServicesPushControllerPushNotificationClickedEvent = { type: `${typeof controllerName}:pushNotificationClicked`; - payload: [Types.INotification]; + payload: [PushAnalyticsPayload]; }; export type Events = diff --git a/packages/notification-services-controller/src/NotificationServicesPushController/types/index.ts b/packages/notification-services-controller/src/NotificationServicesPushController/types/index.ts index 693b8999cb..0a00a055c6 100644 --- a/packages/notification-services-controller/src/NotificationServicesPushController/types/index.ts +++ b/packages/notification-services-controller/src/NotificationServicesPushController/types/index.ts @@ -1,2 +1,3 @@ export type * from './firebase'; +export type * from './push-analytics'; export type * from './push-service-interface'; diff --git a/packages/notification-services-controller/src/NotificationServicesPushController/types/push-analytics.ts b/packages/notification-services-controller/src/NotificationServicesPushController/types/push-analytics.ts new file mode 100644 index 0000000000..cccfc9bd0a --- /dev/null +++ b/packages/notification-services-controller/src/NotificationServicesPushController/types/push-analytics.ts @@ -0,0 +1,22 @@ +// snake_case mirrors the FCM payload and Segment schema keys +/* eslint-disable @typescript-eslint/naming-convention */ + +/** + * Analytics fields carried by the `NotificationServicesPushController` messenger + * events (`onNewNotifications`, `pushNotificationClicked`). Read directly from + * top-level FCM payload keys, so clients build Segment events without fallback + * chains or parsing a `metadata` blob. + */ +export type PushAnalyticsPayload = { + notification_id: string; + /** Free-form snake_case label set by the producer. */ + notification_type: string; + /** Team-owned, open-ended (e.g. `eth_received`). */ + notification_subtype: string; + /** Server-side cross-check; clients prefer their own AuthController source. */ + profile_id?: string; + /** Only present when the notification has a chain context. */ + chain_id?: number; + /** Platform notifications only; the CTA link to route to on tap. */ + deeplink?: string; +}; diff --git a/packages/notification-services-controller/src/NotificationServicesPushController/utils/index.ts b/packages/notification-services-controller/src/NotificationServicesPushController/utils/index.ts index be95219f07..b715997638 100644 --- a/packages/notification-services-controller/src/NotificationServicesPushController/utils/index.ts +++ b/packages/notification-services-controller/src/NotificationServicesPushController/utils/index.ts @@ -1,2 +1,3 @@ export * from './get-notification-data'; export * from './get-notification-message'; +export * from './to-push-analytics-payload'; diff --git a/packages/notification-services-controller/src/NotificationServicesPushController/utils/to-push-analytics-payload.test.ts b/packages/notification-services-controller/src/NotificationServicesPushController/utils/to-push-analytics-payload.test.ts new file mode 100644 index 0000000000..048804b80b --- /dev/null +++ b/packages/notification-services-controller/src/NotificationServicesPushController/utils/to-push-analytics-payload.test.ts @@ -0,0 +1,54 @@ +import type { PushAnalyticsPayload } from '../types'; +import { toPushAnalyticsPayload } from './to-push-analytics-payload'; + +const mockFcmData = { + notification_id: 'test-notification-id', + notification_type: 'wallet_activity', + notification_subtype: 'eth_received', + profile_id: 'test-profile-id', + chain_id: '1', + deeplink: 'https://example.com/deeplink', +}; + +const expectedAnalyticsPayload: PushAnalyticsPayload = { + notification_id: 'test-notification-id', + notification_type: 'wallet_activity', + notification_subtype: 'eth_received', + profile_id: 'test-profile-id', + chain_id: 1, + deeplink: 'https://example.com/deeplink', +}; + +describe('toPushAnalyticsPayload() tests', () => { + it('should build the analytics payload from FCM data', () => { + expect(toPushAnalyticsPayload(mockFcmData)).toStrictEqual( + expectedAnalyticsPayload, + ); + }); + + it('should default notification_subtype to an empty string when absent', () => { + const { notification_subtype: _, ...dataWithoutSubtype } = mockFcmData; + + expect(toPushAnalyticsPayload(dataWithoutSubtype)).toStrictEqual({ + ...expectedAnalyticsPayload, + notification_subtype: '', + }); + }); + + it.each([ + undefined, + null, + 'not an object', + { notification_id: 'test-id' }, + { notification_type: 'wallet_activity' }, + ] as const)( + 'should return null for invalid FCM data payload - %p', + (data) => { + expect( + toPushAnalyticsPayload( + data as unknown as Record | undefined, + ), + ).toBeNull(); + }, + ); +}); diff --git a/packages/notification-services-controller/src/NotificationServicesPushController/utils/to-push-analytics-payload.ts b/packages/notification-services-controller/src/NotificationServicesPushController/utils/to-push-analytics-payload.ts new file mode 100644 index 0000000000..79ee8c4c96 --- /dev/null +++ b/packages/notification-services-controller/src/NotificationServicesPushController/utils/to-push-analytics-payload.ts @@ -0,0 +1,27 @@ +import type { PushAnalyticsPayload } from '../types'; + +/** + * Builds the first-class push analytics payload from the top-level FCM `data` + * keys written by push-services. Returns `null` when the required identity + * fields are missing (e.g. a malformed or legacy payload), so callers can + * safely bail out. + * + * @param data - the top-level FCM `data` map (all values are strings). + * @returns the analytics payload, or `null` if required fields are absent. + */ +export function toPushAnalyticsPayload( + data: Record | undefined, +): PushAnalyticsPayload | null { + if (!data?.notification_id || !data?.notification_type) { + return null; + } + + return { + notification_id: data.notification_id, + notification_type: data.notification_type, + notification_subtype: data.notification_subtype ?? '', + profile_id: data.profile_id || undefined, + chain_id: data.chain_id ? Number(data.chain_id) : undefined, + deeplink: data.deeplink || undefined, + }; +} diff --git a/packages/notification-services-controller/src/NotificationServicesPushController/web/push-utils.test.ts b/packages/notification-services-controller/src/NotificationServicesPushController/web/push-utils.test.ts index 6a09cbccc0..632762dd63 100644 --- a/packages/notification-services-controller/src/NotificationServicesPushController/web/push-utils.test.ts +++ b/packages/notification-services-controller/src/NotificationServicesPushController/web/push-utils.test.ts @@ -2,9 +2,8 @@ import * as FirebaseAppModule from 'firebase/app'; import * as FirebaseMessagingModule from 'firebase/messaging'; import * as FirebaseMessagingSWModule from 'firebase/messaging/sw'; -import { processNotification } from '../../NotificationServicesController'; -import { createMockNotificationEthSent } from '../../NotificationServicesController/mocks/mock-raw-notifications'; import { buildPushPlatformNotificationsControllerMessenger } from '../__fixtures__/mockMessenger'; +import type { PushAnalyticsPayload } from '../types'; import { createRegToken, deleteRegToken, @@ -27,6 +26,24 @@ const mockEnv = { vapidKey: 'test-vapidKey', }; +const mockFcmData = { + notification_id: 'test-notification-id', + notification_type: 'wallet_activity', + notification_subtype: 'eth_received', + profile_id: 'test-profile-id', + chain_id: '1', + deeplink: 'https://example.com/deeplink', +}; + +const expectedAnalyticsPayload: PushAnalyticsPayload = { + notification_id: 'test-notification-id', + notification_type: 'wallet_activity', + notification_subtype: 'eth_received', + profile_id: 'test-profile-id', + chain_id: 1, + deeplink: 'https://example.com/deeplink', +}; + const firebaseApp: FirebaseAppModule.FirebaseApp = { name: '', automaticDataCollectionEnabled: false, @@ -332,9 +349,7 @@ describe('createSubscribeToPushNotifications() tests', () => { const firebaseCallback = mocks.mockOnBackgroundMessage.mock .lastCall[1] as FirebaseMessagingModule.NextFn; const payload = { - data: { - data: testData, - }, + data: testData, } as unknown as FirebaseMessagingSWModule.MessagePayload; firebaseCallback(payload); @@ -342,14 +357,16 @@ describe('createSubscribeToPushNotifications() tests', () => { return mocks; } - it('should invoke handler when notifications are received', async () => { - const mocks = await arrangeActNotificationReceived( - JSON.stringify(createMockNotificationEthSent()), - ); + it('should invoke handler with the parsed analytics payload when notifications are received', async () => { + const mocks = await arrangeActNotificationReceived(mockFcmData); - // Assert New Notification Event & Handler Calls - expect(mocks.onNewNotificationsListener).toHaveBeenCalled(); - expect(mocks.mockOnReceivedHandler).toHaveBeenCalled(); + // Assert New Notification Event & Handler Calls carry the analytics payload + expect(mocks.onNewNotificationsListener).toHaveBeenCalledWith( + expectedAnalyticsPayload, + ); + expect(mocks.mockOnReceivedHandler).toHaveBeenCalledWith( + expectedAnalyticsPayload, + ); // Assert Click Notification Event & Handler Calls expect(mocks.pushNotificationClickedListener).not.toHaveBeenCalled(); @@ -360,7 +377,10 @@ describe('createSubscribeToPushNotifications() tests', () => { { data: undefined }, { data: null }, { data: 'not an object' }, - { data: { id: 'test-id', payload: { data: 'unexpected shape' } } }, + // Missing the required `notification_type` field. + { data: { notification_id: 'test-id' } }, + // Missing the required `notification_id` field. + { data: { notification_type: 'wallet_activity' } }, ]; it.each(invalidNotificationDataPayloadsTests)( @@ -378,23 +398,25 @@ describe('createSubscribeToPushNotifications() tests', () => { await actCreateSubscription(mocks); - const notificationData = processNotification( - createMockNotificationEthSent(), - ); const mockNotificationEvent = new Event( 'notificationclick', ) as NotificationEvent; Object.assign(mockNotificationEvent, { - notification: { data: notificationData }, + notification: { data: expectedAnalyticsPayload }, }); // Act - Testing service worker notification click event // eslint-disable-next-line no-restricted-globals self.dispatchEvent(mockNotificationEvent); - // Assert Click Notification Event & Handler Calls - expect(mocks.pushNotificationClickedListener).toHaveBeenCalled(); - expect(mocks.mockOnClickHandler).toHaveBeenCalled(); + // Assert Click Notification Event & Handler Calls carry the analytics payload + expect(mocks.pushNotificationClickedListener).toHaveBeenCalledWith( + expectedAnalyticsPayload, + ); + expect(mocks.mockOnClickHandler).toHaveBeenCalledWith( + expect.any(Event), + expectedAnalyticsPayload, + ); // Assert New Notification Event & Handler Calls expect(mocks.onNewNotificationsListener).not.toHaveBeenCalled(); diff --git a/packages/notification-services-controller/src/NotificationServicesPushController/web/push-utils.ts b/packages/notification-services-controller/src/NotificationServicesPushController/web/push-utils.ts index f3c0257df6..8bd60b2fc6 100644 --- a/packages/notification-services-controller/src/NotificationServicesPushController/web/push-utils.ts +++ b/packages/notification-services-controller/src/NotificationServicesPushController/web/push-utils.ts @@ -12,14 +12,10 @@ import { import type { Messaging, MessagePayload } from 'firebase/messaging/sw'; import log from 'loglevel'; -import type { Types } from '../../NotificationServicesController'; -import { - isOnChainRawNotification, - safeProcessNotification, -} from '../../NotificationServicesController'; -import { toRawAPINotification } from '../../shared/to-raw-notification'; import type { NotificationServicesPushControllerMessenger } from '../NotificationServicesPushController'; +import type { PushAnalyticsPayload } from '../types'; import type { PushNotificationEnv } from '../types/firebase'; +import { toPushAnalyticsPayload } from '../utils/to-push-analytics-payload'; declare const self: ServiceWorkerGlobalScope; @@ -122,7 +118,7 @@ export async function deleteRegToken( */ async function listenToPushNotificationsReceived( env: PushNotificationEnv, - handler?: (notification: Types.INotification) => void | Promise, + handler?: (payload: PushAnalyticsPayload) => void | Promise, ): Promise<(() => void) | null> { const messaging = await getFirebaseMessaging(env); if (!messaging) { @@ -134,31 +130,17 @@ async function listenToPushNotificationsReceived( // eslint-disable-next-line @typescript-eslint/no-misused-promises async (payload: MessagePayload): Promise => { try { - // MessagePayload shapes are not known - // TODO - provide open-api unfied backend/frontend types - // TODO - we will replace the underlying Data payload with the same Notification payload used by mobile - const data: unknown | null = JSON.parse(payload?.data?.data ?? 'null'); - - if (!data) { - return; - } - - if (!isOnChainRawNotification(data)) { - return; - } - - const notificationData = toRawAPINotification(data); - const notification = safeProcessNotification(notificationData); + const analyticsPayload = toPushAnalyticsPayload(payload?.data); - if (!notification) { + if (!analyticsPayload) { return; } - await handler?.(notification); + await handler?.(analyticsPayload); } catch (error) { - // Do Nothing, cannot parse a bad notification - log.error('Unable to send push notification:', { - notification: payload?.data?.data, + // Do Nothing, cannot handle a bad notification + log.error('Unable to handle push notification:', { + notification: payload?.data, error, }); } @@ -176,11 +158,11 @@ async function listenToPushNotificationsReceived( * @returns unsubscribe handler */ function listenToPushNotificationsClicked( - handler: (e: NotificationEvent, notification: Types.INotification) => void, + handler: (e: NotificationEvent, payload: PushAnalyticsPayload) => void, ): () => void { const clickHandler = (event: NotificationEvent): void => { // Get Data - const data: Types.INotification = event?.notification?.data; + const data: PushAnalyticsPayload = event?.notification?.data; handler(event, data); }; @@ -203,33 +185,28 @@ function listenToPushNotificationsClicked( * @returns a function that can be used by the controller */ export function createSubscribeToPushNotifications(props: { - onReceivedHandler: ( - notification: Types.INotification, - ) => void | Promise; - onClickHandler: ( - e: NotificationEvent, - notification: Types.INotification, - ) => void; + onReceivedHandler: (payload: PushAnalyticsPayload) => void | Promise; + onClickHandler: (e: NotificationEvent, payload: PushAnalyticsPayload) => void; messenger: NotificationServicesPushControllerMessenger; }): (env: PushNotificationEnv) => Promise<() => void> { return async function (env: PushNotificationEnv): Promise<() => void> { const onBackgroundMessageSub = await listenToPushNotificationsReceived( env, - async (notification): Promise => { + async (analyticsPayload): Promise => { props.messenger.publish( 'NotificationServicesPushController:onNewNotifications', - notification, + analyticsPayload, ); - await props.onReceivedHandler(notification); + await props.onReceivedHandler(analyticsPayload); }, ); const onClickSub = listenToPushNotificationsClicked( - (event, notification): void => { + (event, analyticsPayload): void => { props.messenger.publish( 'NotificationServicesPushController:pushNotificationClicked', - notification, + analyticsPayload, ); - props.onClickHandler(event, notification); + props.onClickHandler(event, analyticsPayload); }, );