diff --git a/app/actions/notification/helpers/index.ts b/app/actions/notification/helpers/index.ts index 7038cb028412..34b6b6ff064e 100644 --- a/app/actions/notification/helpers/index.ts +++ b/app/actions/notification/helpers/index.ts @@ -7,7 +7,7 @@ import { mmStorage, getAllUUIDs, } from '../../../util/notifications'; -import { UserStorage } from '@metamask/notification-services-controller/dist/NotificationServicesController/types/user-storage/index.cjs'; +import type { UserStorage } from '@metamask/notification-services-controller/notification-services'; export type MarkAsReadNotificationsParam = Pick< Notification, diff --git a/app/util/notifications/constants/urls.ts b/app/util/notifications/constants/urls.ts deleted file mode 100644 index e057041e0f7e..000000000000 --- a/app/util/notifications/constants/urls.ts +++ /dev/null @@ -1,11 +0,0 @@ -enum CHAIN_SCANS_URLS { - ETHEREUM = 'https://etherscan.io', - OPTIMISM = 'https://optimistic.etherscan.io', - BSC = 'https://bscscan.com', - POLYGON = 'https://polygonscan.com', - ARBITRUM = 'https://arbiscan.io', - AVALANCHE = 'https://snowtrace.io', - LINEA = 'https://lineascan.build', -} - -export default CHAIN_SCANS_URLS; diff --git a/app/util/notifications/hooks/usePushNotifications.ts b/app/util/notifications/hooks/usePushNotifications.ts index ef356fa968cc..b7e2e6970465 100644 --- a/app/util/notifications/hooks/usePushNotifications.ts +++ b/app/util/notifications/hooks/usePushNotifications.ts @@ -5,7 +5,7 @@ import { enablePushNotifications, } from '../../../actions/notification/helpers'; import { mmStorage } from '../settings'; -import { UserStorage } from '@metamask/notification-services-controller/dist/NotificationServicesController/types/user-storage/index.cjs'; +import { UserStorage } from '@metamask/notification-services-controller/notification-services'; import { isNotificationsFeatureEnabled } from '../constants'; export function usePushNotifications() { diff --git a/app/util/notifications/methods/common.test.ts b/app/util/notifications/methods/common.test.ts index 68c058801311..a60317e5ec12 100644 --- a/app/util/notifications/methods/common.test.ts +++ b/app/util/notifications/methods/common.test.ts @@ -1,28 +1,211 @@ -import dayjs from 'dayjs'; -import { formatMenuItemDate, parseNotification } from './common'; +import { + formatMenuItemDate, + parseNotification, + getLeadingZeroCount, + formatAmount, + getUsdAmount, +} from './common'; import { strings } from '../../../../locales/i18n'; import { FirebaseMessagingTypes } from '@react-native-firebase/messaging'; describe('formatMenuItemDate', () => { + beforeAll(() => { + jest.useFakeTimers(); + jest.setSystemTime(new Date(Date.UTC(2024, 5, 7, 9, 40, 0))); // 2024-06-07T09:40:00Z + }); + + afterAll(() => { + jest.useRealTimers(); + }); + it('returns "No date" if date is not provided', () => { expect(formatMenuItemDate()).toBe(strings('notifications.no_date')); }); - it('formats date to "HH:mm" if it is a current day', () => { - const date = dayjs().toDate(); - expect(formatMenuItemDate(date)).toBe(dayjs().format('HH:mm')); + it('formats date as time if the date is today', () => { + const assertToday = (modifyDate?: (d: Date) => void) => { + const testDate = new Date(); + modifyDate?.(testDate); + expect(formatMenuItemDate(testDate)).toMatch(/^\d{2}:\d{2}$/u); // HH:mm + }; + + // assert current date + assertToday(); + + // assert 1 hour ago + assertToday((testDate) => { + testDate.setUTCHours(testDate.getUTCHours() - 1); + return testDate; + }); + }); + + it('formats date as "yesterday" if the date was yesterday', () => { + const assertYesterday = (modifyDate: (d: Date) => void) => { + const testDate = new Date(); + modifyDate(testDate); + expect(formatMenuItemDate(testDate)).toBe( + strings('notifications.yesterday'), + ); + }; + + // assert exactly 1 day ago + assertYesterday((testDate) => { + testDate.setUTCDate(testDate.getUTCDate() - 1); + }); + + // assert almost a day ago, but was still yesterday + // E.g. if Today way 09:40AM, but date to test was 23 hours ago (yesterday at 10:40AM), we still want to to show yesterday + assertYesterday((testDate) => { + testDate.setUTCDate(testDate.getUTCDate() - 1); + testDate.setUTCHours(testDate.getUTCHours() + 1); + }); + }); + + it('should format date as "Month DD" if the date is this year but not today or yesterday', () => { + const assertMonthsAgo = (modifyDate: (d: Date) => Date | void) => { + let testDate = new Date(); + testDate = modifyDate(testDate) ?? testDate; + expect(formatMenuItemDate(testDate)).toMatch(/^\w{3} \d{1,2}$/u); // E.g. Apr 7 + }; + + // assert exactly 1 month ago + assertMonthsAgo((testDate) => { + testDate.setUTCMonth(testDate.getUTCMonth() - 1); + }); + + // assert 2 months ago + assertMonthsAgo((testDate) => { + testDate.setUTCMonth(testDate.getUTCMonth() - 2); + }); + + // assert almost a month ago (where it is a new month, but not 30 days) + assertMonthsAgo( + () => + // jest mock date is set in july, so we will test with month may + new Date(Date.UTC(2024, 4, 20, 9, 40, 0)), // 2024-05-20T09:40:00Z + ); + }); + + it('should format date as "Mon DD, YYYY" if the date is not this year', () => { + const assertYearsAgo = (modifyDate: (d: Date) => Date | void) => { + let testDate = new Date(); + testDate = modifyDate(testDate) ?? testDate; + expect(formatMenuItemDate(testDate)).toMatch(/^\w{3} \d{1,2}, \d{4}$/u); + }; + + // assert exactly 1 year ago + assertYearsAgo((testDate) => { + testDate.setUTCFullYear(testDate.getUTCFullYear() - 1); + }); + + // assert 2 years ago + assertYearsAgo((testDate) => { + testDate.setUTCFullYear(testDate.getUTCFullYear() - 2); + }); + + // assert almost a year ago (where it is a new year, but not 365 days ago) + assertYearsAgo( + () => + // jest mock date is set in 2024, so we will test with year 2023 + new Date(Date.UTC(2023, 10, 20, 9, 40, 0)), // 2023-11-20T09:40:00Z + ); + }); +}); + +describe('getNotificationData - getLeadingZeroCount() tests', () => { + test('Should handle all test cases', () => { + expect(getLeadingZeroCount(0)).toBe(0); + expect(getLeadingZeroCount(-1)).toBe(0); + expect(getLeadingZeroCount(1e-1)).toBe(0); + + expect(getLeadingZeroCount('1.01')).toBe(1); + expect(getLeadingZeroCount('3e-2')).toBe(1); + expect(getLeadingZeroCount('100.001e1')).toBe(1); + + expect(getLeadingZeroCount('0.00120043')).toBe(2); + }); +}); + +describe('getNotificationData - formatAmount() tests', () => { + test('Should format large numbers', () => { + expect(formatAmount(1000)).toBe('1K'); + expect(formatAmount(1500)).toBe('1.5K'); + expect(formatAmount(1000000)).toBe('1M'); + expect(formatAmount(1000000000)).toBe('1B'); + expect(formatAmount(1000000000000)).toBe('1T'); + expect(formatAmount(1234567)).toBe('1.23M'); + }); + + test('Should format smaller numbers (<1000) with custom decimal place', () => { + const formatOptions = { decimalPlaces: 18 }; + expect(formatAmount(100.0012, formatOptions)).toBe('100.0012'); + expect(formatAmount(100.001200001, formatOptions)).toBe('100.001200001'); + expect(formatAmount(1e-18, formatOptions)).toBe('0.000000000000000001'); + expect(formatAmount(1e-19, formatOptions)).toBe('0'); // number is smaller than decimals given, hence 0 }); - it('formats date to "Yesterday" if it is yesterday', () => { - const date = dayjs().subtract(1, 'day').toDate(); - expect(formatMenuItemDate(date)).toBe(strings('notifications.yesterday')); + test('Should format small numbers (<1000) up to 4 decimals otherwise uses ellipses', () => { + const formatOptions = { shouldEllipse: true }; + expect(formatAmount(100.1, formatOptions)).toBe('100.1'); + expect(formatAmount(100.01, formatOptions)).toBe('100.01'); + expect(formatAmount(100.001, formatOptions)).toBe('100.001'); + expect(formatAmount(100.0001, formatOptions)).toBe('100.0001'); + expect(formatAmount(100.00001, formatOptions)).toBe('100.0000...'); // since number is has >4 decimals, it will be truncated + expect(formatAmount(0.00001, formatOptions)).toBe('0.0000...'); // since number is has >4 decimals, it will be truncated }); + test('Should format small numbers (<1000) to custom decimal places and ellipse', () => { + const formatOptions = { decimalPlaces: 2, shouldEllipse: true }; + expect(formatAmount(100.1, formatOptions)).toBe('100.1'); + expect(formatAmount(100.01, formatOptions)).toBe('100.01'); + expect(formatAmount(100.001, formatOptions)).toBe('100.00...'); + expect(formatAmount(100.0001, formatOptions)).toBe('100.00...'); + expect(formatAmount(100.00001, formatOptions)).toBe('100.00...'); // since number is has >2 decimals, it will be truncated + expect(formatAmount(0.00001, formatOptions)).toBe('0.00...'); // since number is has >2 decimals, it will be truncated + }); +}); + +describe('getUsdAmount', () => { + it('should return formatted USD amount based on token amount, decimals, and USD rate', () => { + const amount = '1000000000000000000'; // 1 Ether (1e18 wei) + const decimals = '18'; + const usdRate = '2000'; // 1 Ether = $2000 + + const result = getUsdAmount(amount, decimals, usdRate); + expect(result).toBe('2K'); // Since 1 Ether * $2000 = $2000, formatted as '2K' + }); + + it('should return an empty string if any of the parameters are missing', () => { + expect(getUsdAmount('', '18', '2000')).toBe(''); + expect(getUsdAmount('1000000000000000000', '', '2000')).toBe(''); + expect(getUsdAmount('1000000000000000000', '18', '')).toBe(''); + }); + + it('should handle small amounts correctly', () => { + const amount = '1000000000000000'; // 0.001 Ether (1e15 wei) + const decimals = '18'; + const usdRate = '1500'; // 1 Ether = $1500 + + const result = getUsdAmount(amount, decimals, usdRate); + expect(result).toBe('1.5'); // Since 0.001 Ether * $1500 = $1.5 + }); + + it('should handle large amounts correctly', () => { + const amount = '5000000000000000000000'; // 5000 Ether + const decimals = '18'; + const usdRate = '1000'; // 1 Ether = $1000 + + const result = getUsdAmount(amount, decimals, usdRate); + expect(result).toBe('5M'); // Since 5000 Ether * $1000 = $5,000,000, formatted as '5M' + }); +}); + +describe('parseNotification', () => { it('parses notification', () => { const notification = { data: { data: { - type: 'eth_received', + type: 'eth_received', data: { kind: 'eth_received' }, }, }, @@ -35,5 +218,3 @@ describe('formatMenuItemDate', () => { }); }); }); - - diff --git a/app/util/notifications/methods/common.ts b/app/util/notifications/methods/common.ts index fcbab88a089f..5c90b4d5b5d2 100644 --- a/app/util/notifications/methods/common.ts +++ b/app/util/notifications/methods/common.ts @@ -1,5 +1,4 @@ import dayjs, { Dayjs } from 'dayjs'; -import isYesterday from 'dayjs/plugin/isYesterday'; import relativeTime from 'dayjs/plugin/relativeTime'; import notifee from '@notifee/react-native'; @@ -7,8 +6,18 @@ import localeData from 'dayjs/plugin/localeData'; import { Web3Provider } from '@ethersproject/providers'; import { toHex } from '@metamask/controller-utils'; import BigNumber from 'bignumber.js'; -import { NotificationServicesController } from '@metamask/notification-services-controller'; -import { UserStorage } from '@metamask/notification-services-controller/dist/NotificationServicesController/types/user-storage/index.cjs'; +import { + UserStorage, + USER_STORAGE_VERSION_KEY, + OnChainRawNotification, + OnChainRawNotificationsWithNetworkFields, +} from '@metamask/notification-services-controller/notification-services'; +import { + NOTIFICATION_CHAINS_ID, + NOTIFICATION_NETWORK_CURRENCY_NAME, + NOTIFICATION_NETWORK_CURRENCY_SYMBOL, + SUPPORTED_NOTIFICATION_BLOCK_EXPLORERS, +} from '@metamask/notification-services-controller/notification-services/ui'; import { FirebaseMessagingTypes } from '@react-native-firebase/messaging'; import Engine from '../../../core/Engine'; import { IconName } from '../../../component-library/components/Icons/Icon'; @@ -17,17 +26,38 @@ import { TRIGGER_TYPES } from '../constants'; import { Notification } from '../types'; import { calcTokenAmount } from '../../transactions'; import images from '../../../images/image-icons'; -import CHAIN_SCANS_URLS from '../constants/urls'; import I18n, { strings } from '../../../../locales/i18n'; +/** + * Checks if 2 date objects are on the same day + * + * @param currentDate + * @param dateToCheck + * @returns boolean if dates are same day. + */ +const isSameDay = (currentDate: Date, dateToCheck: Date) => + currentDate.getFullYear() === dateToCheck.getFullYear() && + currentDate.getMonth() === dateToCheck.getMonth() && + currentDate.getDate() === dateToCheck.getDate(); + +/** + * Checks if a date is "yesterday" from the current date + * + * @param currentDate + * @param dateToCheck + * @returns boolean if dates were "yesterday" + */ +const isYesterday = (currentDate: Date, dateToCheck: Date) => { + const yesterday = new Date(currentDate); + yesterday.setDate(currentDate.getDate() - 1); + return isSameDay(yesterday, dateToCheck); +}; + // Extend dayjs with the plugins -dayjs.extend(isYesterday); dayjs.extend(localeData); dayjs.extend(relativeTime); -const { UI } = NotificationServicesController; -export const USER_STORAGE_VERSION_KEY: unique symbol = 'v' as never; -export function formatRelative( +function formatRelative( date: Dayjs, currentDate: Dayjs, locale: string = 'en', @@ -49,47 +79,32 @@ export function formatMenuItemDate(date?: Date, locale: string = 'en'): string { if (!date) { return strings('notifications.no_date'); } - const currentDate = dayjs(); + + const currentDate = new Date(); + const currentDayjsDate = dayjs(); const dayjsDate = dayjs(date); dayjs.locale(locale); // E.g. 12:21 - if (dayjsDate.isSame(currentDate, 'day')) { + if (dayjsDate.isSame(currentDayjsDate, 'day')) { return dayjsDate.format('HH:mm'); } // E.g. Yesterday - if (dayjs().add(-1, 'day').isYesterday()) { - return formatRelative(dayjsDate, currentDate, I18n.locale); + if (isYesterday(currentDate, date)) { + return formatRelative(dayjsDate, currentDayjsDate, I18n.locale); } - // E.g. 21 Oct - if (dayjsDate.isSame(currentDate, 'year')) { - return dayjsDate.format('D MMM'); + // E.g. Oct 21 + if (dayjsDate.isSame(currentDayjsDate, 'year')) { + return dayjsDate.format('MMM D'); } - // E.g. 21 Oct 2022 - return dayjsDate.format('D MMM YYYY'); + // E.g. Oct 21, 2022 + return dayjsDate.format('MMM D, YYYY'); } -/** - * Generates a unique key based on the provided text, index, and a random string. - * - * @param text - The text to be included in the key. - * @param index - The index to be included in the key. - * @returns The generated unique key. - */ -export const getRandomKey = (text: string, index: number) => { - const key = `${text - .replace(/\s+/gu, '_') - .replace(/[^\w-]/gu, '')}-${index}-${Math.random() - .toString(36) - .substring(2, 15)}`; - - return key; -}; - interface FormatOptions { decimalPlaces?: number; shouldEllipse?: boolean; @@ -165,9 +180,9 @@ export const formatAmount = (numericAmount: number, opts?: FormatOptions) => { return numericAmount.toString(); }; -export function hasNetworkFeeFields( - notification: NotificationServicesController.Types.OnChainRawNotification, -): notification is NotificationServicesController.Types.OnChainRawNotificationsWithNetworkFields { +function hasNetworkFeeFields( + notification: OnChainRawNotification, +): notification is OnChainRawNotificationsWithNetworkFields { return 'network_fee' in notification.data; } @@ -184,9 +199,7 @@ export function getProviderByChainId(chainId: HexChainId) { return provider && new Web3Provider(provider); } -export const getNetworkFees = async ( - notification: NotificationServicesController.Types.OnChainRawNotification, -) => { +export const getNetworkFees = async (notification: OnChainRawNotification) => { if (!hasNetworkFeeFields(notification)) { throw new Error('Invalid notification type'); } @@ -336,52 +349,52 @@ export const sortNotifications = ( */ export function getNativeTokenDetailsByChainId(chainId: number) { const chainIdString = chainId.toString(); - if (chainIdString === UI.NOTIFICATION_CHAINS_ID.ETHEREUM) { + if (chainIdString === NOTIFICATION_CHAINS_ID.ETHEREUM) { return { - name: UI.NOTIFICATION_NETWORK_CURRENCY_NAME[chainIdString], - symbol: UI.NOTIFICATION_NETWORK_CURRENCY_SYMBOL[chainIdString], + name: NOTIFICATION_NETWORK_CURRENCY_NAME[chainIdString], + symbol: NOTIFICATION_NETWORK_CURRENCY_SYMBOL[chainIdString], image: images.ETHEREUM, }; } - if (chainIdString === UI.NOTIFICATION_CHAINS_ID.OPTIMISM) { + if (chainIdString === NOTIFICATION_CHAINS_ID.OPTIMISM) { return { - name: UI.NOTIFICATION_NETWORK_CURRENCY_NAME[chainIdString], - symbol: UI.NOTIFICATION_NETWORK_CURRENCY_SYMBOL[chainIdString], + name: NOTIFICATION_NETWORK_CURRENCY_NAME[chainIdString], + symbol: NOTIFICATION_NETWORK_CURRENCY_SYMBOL[chainIdString], image: images.OPTIMISM, }; } - if (chainIdString === UI.NOTIFICATION_CHAINS_ID.BSC) { + if (chainIdString === NOTIFICATION_CHAINS_ID.BSC) { return { - name: UI.NOTIFICATION_NETWORK_CURRENCY_NAME[chainIdString], - symbol: UI.NOTIFICATION_NETWORK_CURRENCY_SYMBOL[chainIdString], + name: NOTIFICATION_NETWORK_CURRENCY_NAME[chainIdString], + symbol: NOTIFICATION_NETWORK_CURRENCY_SYMBOL[chainIdString], image: images.BNB, }; } - if (chainIdString === UI.NOTIFICATION_CHAINS_ID.POLYGON) { + if (chainIdString === NOTIFICATION_CHAINS_ID.POLYGON) { return { - name: UI.NOTIFICATION_NETWORK_CURRENCY_NAME[chainIdString], - symbol: UI.NOTIFICATION_NETWORK_CURRENCY_SYMBOL[chainIdString], + name: NOTIFICATION_NETWORK_CURRENCY_NAME[chainIdString], + symbol: NOTIFICATION_NETWORK_CURRENCY_SYMBOL[chainIdString], image: images.POL, }; } - if (chainIdString === UI.NOTIFICATION_CHAINS_ID.ARBITRUM) { + if (chainIdString === NOTIFICATION_CHAINS_ID.ARBITRUM) { return { - name: UI.NOTIFICATION_NETWORK_CURRENCY_NAME[chainIdString], - symbol: UI.NOTIFICATION_NETWORK_CURRENCY_SYMBOL[chainIdString], + name: NOTIFICATION_NETWORK_CURRENCY_NAME[chainIdString], + symbol: NOTIFICATION_NETWORK_CURRENCY_SYMBOL[chainIdString], image: images.AETH, }; } - if (chainIdString === UI.NOTIFICATION_CHAINS_ID.AVALANCHE) { + if (chainIdString === NOTIFICATION_CHAINS_ID.AVALANCHE) { return { - name: UI.NOTIFICATION_NETWORK_CURRENCY_NAME[chainIdString], - symbol: UI.NOTIFICATION_NETWORK_CURRENCY_SYMBOL[chainIdString], + name: NOTIFICATION_NETWORK_CURRENCY_NAME[chainIdString], + symbol: NOTIFICATION_NETWORK_CURRENCY_SYMBOL[chainIdString], image: images.AVAX, }; } - if (chainIdString === UI.NOTIFICATION_CHAINS_ID.LINEA) { + if (chainIdString === NOTIFICATION_CHAINS_ID.LINEA) { return { - name: UI.NOTIFICATION_NETWORK_CURRENCY_NAME[chainIdString], - symbol: UI.NOTIFICATION_NETWORK_CURRENCY_SYMBOL[chainIdString], + name: NOTIFICATION_NETWORK_CURRENCY_NAME[chainIdString], + symbol: NOTIFICATION_NETWORK_CURRENCY_SYMBOL[chainIdString], image: images['LINEA-MAINNET'], }; } @@ -389,34 +402,19 @@ export function getNativeTokenDetailsByChainId(chainId: number) { return undefined; } +const isSupportedBlockExplorer = ( + chainId: number, +): chainId is keyof typeof SUPPORTED_NOTIFICATION_BLOCK_EXPLORERS => + chainId in SUPPORTED_NOTIFICATION_BLOCK_EXPLORERS; + /** * Gets block explorer information for the notification chains we support * @param chainId Notification Chain Id. This is a subset of chains that support notifications * @returns some default block explorers for the chains we support. */ export function getBlockExplorerByChainId(chainId: number) { - const chainIdString = chainId.toString(); - - if (chainIdString === UI.NOTIFICATION_CHAINS_ID.ETHEREUM) { - return CHAIN_SCANS_URLS.ETHEREUM; - } - if (chainIdString === UI.NOTIFICATION_CHAINS_ID.OPTIMISM) { - return CHAIN_SCANS_URLS.OPTIMISM; - } - if (chainIdString === UI.NOTIFICATION_CHAINS_ID.BSC) { - return CHAIN_SCANS_URLS.BSC; - } - if (chainIdString === UI.NOTIFICATION_CHAINS_ID.POLYGON) { - return CHAIN_SCANS_URLS.POLYGON; - } - if (chainIdString === UI.NOTIFICATION_CHAINS_ID.ARBITRUM) { - return CHAIN_SCANS_URLS.ARBITRUM; - } - if (chainIdString === UI.NOTIFICATION_CHAINS_ID.AVALANCHE) { - return CHAIN_SCANS_URLS.AVALANCHE; - } - if (chainIdString === UI.NOTIFICATION_CHAINS_ID.LINEA) { - return CHAIN_SCANS_URLS.LINEA; + if (isSupportedBlockExplorer(chainId)) { + return SUPPORTED_NOTIFICATION_BLOCK_EXPLORERS[chainId].url; } return undefined; @@ -485,28 +483,29 @@ export function withTimeout(promise: Promise, ms: number): Promise { } export interface NotificationTrigger { - id: string - chainId: string - kind: string - address: string + id: string; + chainId: string; + kind: string; + address: string; } -type MapTriggerFn = (trigger: NotificationTrigger) => Result +type MapTriggerFn = (trigger: NotificationTrigger) => Result; interface TraverseTriggerOpts { - address?: string - mapTrigger?: MapTriggerFn + address?: string; + mapTrigger?: MapTriggerFn; } const triggerToId = (trigger: NotificationTrigger) => trigger.id; const triggerIdentity = (trigger: NotificationTrigger) => trigger; -export function traverseUserStorageTriggers( +function traverseUserStorageTriggers( userStorage: UserStorage, options?: TraverseTriggerOpts, ) { const triggers: ResultTriggers[] = []; - const mapTrigger = options?.mapTrigger ?? (triggerIdentity as MapTriggerFn); + const mapTrigger = + options?.mapTrigger ?? (triggerIdentity as MapTriggerFn); for (const address in userStorage) { if (address === (USER_STORAGE_VERSION_KEY as unknown as string)) continue; @@ -544,9 +543,12 @@ export function getAllUUIDs(userStorage: UserStorage): string[] { return uuids; } -export function parseNotification(remoteMessage: FirebaseMessagingTypes.RemoteMessage) { +export function parseNotification( + remoteMessage: FirebaseMessagingTypes.RemoteMessage, +) { const notification = remoteMessage.data?.data; - const parsedNotification = typeof notification === 'string' ? JSON.parse(notification) : notification; + const parsedNotification = + typeof notification === 'string' ? JSON.parse(notification) : notification; const notificationData = { type: parsedNotification?.type || parsedNotification?.data?.kind,