diff --git a/__test__/unit/events/eventsUnique.test.ts b/__test__/unit/events/eventsUnique.test.ts new file mode 100644 index 000000000..e88e50af3 --- /dev/null +++ b/__test__/unit/events/eventsUnique.test.ts @@ -0,0 +1,7 @@ +import { ONESIGNAL_EVENTS } from "../../../src/onesignal/OneSignalEvents"; + +test('Test uniqueness of OneSignal event names', () => { + const events = Object.values(ONESIGNAL_EVENTS); + const uniqueEvents = [...new Set(events)]; + expect(events.length).toEqual(uniqueEvents.length); +}); diff --git a/src/onesignal/NotificationsNamespace.ts b/src/onesignal/NotificationsNamespace.ts index dc7d6dec7..65d2e5595 100644 --- a/src/onesignal/NotificationsNamespace.ts +++ b/src/onesignal/NotificationsNamespace.ts @@ -6,7 +6,7 @@ import OneSignalError from "../../src/shared/errors/OneSignalError"; import OneSignal from "./OneSignal"; import { EventListenerBase } from "../page/userModel/EventListenerBase"; import NotificationEventName from "../page/models/NotificationEventName"; -import { NotificationClicked } from "../../src/shared/models/Notification"; +import { NotificationClickResult, NotificationForegroundWillDisplayEvent } from "../page/models/NotificationEvent"; export default class NotificationsNamespace extends EventListenerBase { constructor(private _permissionNative?: NotificationPermission) { @@ -149,9 +149,9 @@ export default class NotificationsNamespace extends EventListenerBase { } /* Function overloads */ - addEventListener(event: NotificationEventName.Click, listener: (obj: NotificationClicked) => void): void; - addEventListener(event: NotificationEventName.WillDisplay, listener: (obj: StructuredNotification) => void): void; - addEventListener(event: NotificationEventName.Dismiss, listener: (obj: StructuredNotification) => void): void; + addEventListener(event: NotificationEventName.Click, listener: (obj: NotificationClickResult) => void): void; + addEventListener(event: NotificationEventName.ForegroundWillDisplay, listener: (obj: NotificationForegroundWillDisplayEvent) => void): void; + addEventListener(event: NotificationEventName.Dismiss, listener: (obj: OSNotificationDataPayload) => void): void; addEventListener(event: NotificationEventName.PermissionChange, listener: (permission: boolean) => void): void; addEventListener(event: NotificationEventName.PermissionPromptDisplay, listener: () => void): void; @@ -161,9 +161,9 @@ export default class NotificationsNamespace extends EventListenerBase { } /* Function overloads */ - removeEventListener(event: NotificationEventName.Click, listener: (event: NotificationClicked) => void): void; - removeEventListener(event: NotificationEventName.WillDisplay, listener: (obj: StructuredNotification) => void): void; - removeEventListener(event: NotificationEventName.Dismiss, listener: (obj: StructuredNotification) => void): void; + removeEventListener(event: NotificationEventName.Click, listener: (event: NotificationClickResult) => void): void; + removeEventListener(event: NotificationEventName.ForegroundWillDisplay, listener: (obj: OSNotificationDataPayload) => void): void; + removeEventListener(event: NotificationEventName.Dismiss, listener: (obj: OSNotificationDataPayload) => void): void; removeEventListener(event: NotificationEventName.PermissionChange, listener: (permission: boolean) => void): void; removeEventListener(event: NotificationEventName.PermissionPromptDisplay, listener: () => void): void; diff --git a/src/onesignal/OneSignalEvents.ts b/src/onesignal/OneSignalEvents.ts index 1442c9be0..cce2b1a6e 100644 --- a/src/onesignal/OneSignalEvents.ts +++ b/src/onesignal/OneSignalEvents.ts @@ -15,7 +15,7 @@ export const ONESIGNAL_EVENTS = { * Occurs after the user is officially subscribed to push notifications. The service worker is fully registered * and activated and the user is eligible to receive push notifications at any point after this. */ - SUBSCRIPTION_CHANGED: 'subscriptionChange', + SUBSCRIPTION_CHANGED: 'change', /** * Occurs after a POST call to OneSignal's server to send the welcome notification has completed. The actual * notification arrives shortly after. @@ -24,7 +24,7 @@ export const ONESIGNAL_EVENTS = { /** * Occurs when a notification is displayed. */ - NOTIFICATION_WILL_DISPLAY: 'willDisplay', + NOTIFICATION_WILL_DISPLAY: 'foregroundWillDisplay', /** * Occurs when a notification is dismissed by the user either clicking 'X' or clearing all notifications * (available in Android). This event is NOT called if the user clicks the notification's body or any of the diff --git a/src/onesignal/PushSubscriptionNamespace.ts b/src/onesignal/PushSubscriptionNamespace.ts index 9be86d35d..d51ec3397 100644 --- a/src/onesignal/PushSubscriptionNamespace.ts +++ b/src/onesignal/PushSubscriptionNamespace.ts @@ -88,11 +88,11 @@ export default class PushSubscriptionNamespace extends EventListenerBase { await this._enable(false); } - addEventListener(event: "subscriptionChange", listener: (change: SubscriptionChangeEvent) => void): void { + addEventListener(event: "change", listener: (change: SubscriptionChangeEvent) => void): void { OneSignal.emitter.on(event, listener); } - removeEventListener(event: "subscriptionChange", listener: (change: SubscriptionChangeEvent) => void): void { + removeEventListener(event: "change", listener: (change: SubscriptionChangeEvent) => void): void { OneSignal.emitter.off(event, listener); } diff --git a/src/page/models/NotificationEvent.ts b/src/page/models/NotificationEvent.ts new file mode 100644 index 000000000..ffb103ca8 --- /dev/null +++ b/src/page/models/NotificationEvent.ts @@ -0,0 +1,17 @@ +import { OSNotification } from "../../shared/models/OSNotification"; + +// post-user-model +export interface NotificationClickEvent { + notification: OSNotification; + result: NotificationClickResult; +} + +export type NotificationClickResult = { + actionId?: string; + url?: string; +} + +export interface NotificationForegroundWillDisplayEvent { + notification: OSNotification; + preventDefault(): void; +} diff --git a/src/page/models/NotificationEventName.ts b/src/page/models/NotificationEventName.ts index ace206895..2154fb446 100644 --- a/src/page/models/NotificationEventName.ts +++ b/src/page/models/NotificationEventName.ts @@ -1,6 +1,6 @@ enum NotificationEventName { Click = "click", - WillDisplay = "willDisplay", + ForegroundWillDisplay = "foregroundWillDisplay", Dismiss = "dismiss", PermissionChange = "permissionChange", PermissionPromptDisplay = "permissionPromptDisplay" diff --git a/src/shared/helpers/OutcomesHelper.ts b/src/shared/helpers/OutcomesHelper.ts index 7c3bf7188..0aad2ba7f 100644 --- a/src/shared/helpers/OutcomesHelper.ts +++ b/src/shared/helpers/OutcomesHelper.ts @@ -1,5 +1,5 @@ import { OutcomesConfig, OutcomeAttribution, OutcomeAttributionType, SentUniqueOutcome } from '../models/Outcomes'; -import { NotificationClicked, NotificationReceived } from '../models/Notification'; +import { NotificationClicked, NotificationReceived } from '../models/OSNotification'; import Database from "../services/Database"; import Log from '../libraries/Log'; import { Utils } from "../../shared/context/Utils"; diff --git a/src/shared/helpers/ServiceWorkerHelper.ts b/src/shared/helpers/ServiceWorkerHelper.ts index 2efbebc71..fa08d683a 100755 --- a/src/shared/helpers/ServiceWorkerHelper.ts +++ b/src/shared/helpers/ServiceWorkerHelper.ts @@ -6,7 +6,7 @@ import { cancelableTimeout, CancelableTimeoutPromise } from '../../sw/helpers/Ca import Utils from "../context/Utils"; import OutcomesHelper from "./OutcomesHelper"; import { OSServiceWorkerFields } from "../../sw/serviceWorker/types"; -import { NotificationClicked } from "../models/Notification"; +import { NotificationClicked } from "../models/OSNotification"; import { SessionOrigin, initializeNewSession, SessionStatus, Session } from "../models/Session"; import OneSignalApiSW from "../api/OneSignalApiSW"; import Path from "../models/Path"; diff --git a/src/shared/managers/ServiceWorkerManager.ts b/src/shared/managers/ServiceWorkerManager.ts index aaf830822..7df886c65 100644 --- a/src/shared/managers/ServiceWorkerManager.ts +++ b/src/shared/managers/ServiceWorkerManager.ts @@ -305,7 +305,7 @@ export class ServiceWorkerManager { // Least likely to modify, since modifying this property changes the page's URL url = location.href; } - await Database.put('NotificationOpened', { url: url, data: data, timestamp: Date.now() }); + await Database.put('NotificationOpened', { url, data, timestamp: Date.now() }); } else await OneSignalEvent.trigger(OneSignal.EVENTS.NOTIFICATION_CLICKED, data); diff --git a/src/shared/models/Notification.ts b/src/shared/models/OSNotification.ts similarity index 88% rename from src/shared/models/Notification.ts rename to src/shared/models/OSNotification.ts index a96b5c662..0e8f9c0f9 100755 --- a/src/shared/models/Notification.ts +++ b/src/shared/models/OSNotification.ts @@ -2,7 +2,7 @@ import Utils from '../context/Utils'; import { InvalidArgumentError, InvalidArgumentReason } from '../errors/InvalidArgumentError'; import { NotificationActionButton } from '../../page/models/NotificationActionButton'; -export class Notification { +export class OSNotification { public id?: string; public title?: string; public body?: string; @@ -15,7 +15,7 @@ export class Notification { public renotify?: true; public actions?: Array; - constructor(title: string, options?: Notification) { + constructor(title: string, options?: OSNotification) { this.title = title; if (options) { this.id = options.id; @@ -38,7 +38,7 @@ export class Notification { icon = "https://onesignal.com/images/notification_logo.png", data = {} } = {}) { - return new Notification(title, { + return new OSNotification(title, { icon: icon, body: body, url: url, @@ -46,11 +46,11 @@ export class Notification { }); } - static createFromPushPayload(payload: any): Notification { + static createFromPushPayload(payload: any): OSNotification { if (!payload) { throw new InvalidArgumentError('payload', InvalidArgumentReason.Empty); } - const notification = new Notification(payload.title, { + const notification = new OSNotification(payload.title, { id: payload.custom.i, title: payload.title, body: payload.alert, @@ -83,6 +83,7 @@ export interface NotificationReceived { timestamp: number; } +// used to store click info in IndexedDB export interface NotificationClicked { notificationId: string; action: string; diff --git a/src/shared/models/OSNotificationDataPayload.ts b/src/shared/models/OSNotificationDataPayload.ts new file mode 100644 index 000000000..2077741fc --- /dev/null +++ b/src/shared/models/OSNotificationDataPayload.ts @@ -0,0 +1,34 @@ +/** + * the data payload on the notification event notification object + * @see https://developer.mozilla.org/en-US/docs/Web/API/NotificationEvent + * + * @example + * NotificationEvent { + * action: "action" + * notification: Notification { + * actions: (2) [{…}, {…}] + * badge: "" + * body: "Test Message" + * data: {…} <------ this is the OSNotificationDataPayload payload + * } + * } + */ + + type OSNotificationDataPayload = { + id: string; + content: string; + heading?: string; + url?: string; + data?: object; + rr?: string; + icon?: string; + image?: string; + tag?: string; + badge?: string; + vibrate?: VibratePattern; + buttons?: NotificationButtonData[]; +}; + +interface NotificationButtonData extends NotificationAction { + url: string; +}; diff --git a/src/shared/models/RawNotificationPayload.ts b/src/shared/models/RawNotificationPayload.ts new file mode 100644 index 000000000..8f9ebe1a6 --- /dev/null +++ b/src/shared/models/RawNotificationPayload.ts @@ -0,0 +1,18 @@ +export interface RawNotificationPayload { + title: string; // heading + alert: string; // content + custom: ICustomNotificationPayload; + icon: string; + image: string; + tag: string; + badge: string; + vibrate: string; + o: any[]; +} + +interface ICustomNotificationPayload { + a: any; + i: string; + u: string; + rr: string; +} diff --git a/src/shared/models/StructuredNotification.ts b/src/shared/models/StructuredNotification.ts deleted file mode 100644 index 01d72d0e8..000000000 --- a/src/shared/models/StructuredNotification.ts +++ /dev/null @@ -1,21 +0,0 @@ -type StructuredNotification = { - id: string; - content: string; - heading?: string; - url?: string; - data?: object; - rr?: string; - icon?: string; - image?: string; - tag?: string; - badge?: string; - vibrate?: string; - buttons?: NotificationButtonData[]; -}; - -type NotificationButtonData = { - action?: string; - title?: string; - icon?: string; - url?: string; -}; diff --git a/src/shared/services/Database.ts b/src/shared/services/Database.ts index 164dd476d..820463de4 100644 --- a/src/shared/services/Database.ts +++ b/src/shared/services/Database.ts @@ -3,7 +3,7 @@ import IndexedDb from "./IndexedDb"; import { AppConfig } from "../models/AppConfig"; import { AppState, ClickedNotifications } from "../models/AppState"; -import { NotificationReceived, NotificationClicked } from "../models/Notification"; +import { NotificationReceived, NotificationClicked } from "../models/OSNotification"; import { ServiceWorkerState } from "../models/ServiceWorkerState"; import { Subscription } from "../models/Subscription"; import { TestEnvironmentKind } from "../models/TestEnvironmentKind"; diff --git a/src/shared/services/IndexedDb.ts b/src/shared/services/IndexedDb.ts index a939f4080..94a7668f3 100644 --- a/src/shared/services/IndexedDb.ts +++ b/src/shared/services/IndexedDb.ts @@ -111,7 +111,7 @@ export default class IndexedDb { if (event.oldVersion < 2) { db.createObjectStore("Sessions", { keyPath: "sessionKey" }); db.createObjectStore("NotificationReceived", { keyPath: "notificationId" }); - db.createObjectStore("NotificationClicked", { keyPath: "notificationId" }); + db.createObjectStore("NotificationClicked", { keyPath: "notification.id" }); } if (event.oldVersion < 3) { db.createObjectStore("SentUniqueOutcome", { keyPath: "outcomeName" }); diff --git a/src/sw/serviceWorker/ServiceWorker.ts b/src/sw/serviceWorker/ServiceWorker.ts index 863c1cb9d..d4104c1d3 100755 --- a/src/sw/serviceWorker/ServiceWorker.ts +++ b/src/sw/serviceWorker/ServiceWorker.ts @@ -18,7 +18,7 @@ import { import ServiceWorkerHelper from "../../shared/helpers/ServiceWorkerHelper"; import { cancelableTimeout } from "../helpers/CancelableTimeout"; import { awaitableTimeout } from "../../shared/utils/AwaitableTimeout"; -import { NotificationReceived, NotificationClicked } from "../../shared/models/Notification"; +import { NotificationReceived } from "../../shared/models/OSNotification"; import { UpsertOrDeactivateSessionPayload, PageVisibilityResponse, @@ -31,8 +31,10 @@ import { WorkerMessenger, WorkerMessengerMessage, WorkerMessengerCommand } from import { DeviceRecord } from "../../../src/shared/models/DeviceRecord"; import { RawPushSubscription } from "../../../src/shared/models/RawPushSubscription"; import { DeliveryPlatformKind } from "../../shared/models/DeliveryPlatformKind"; +import { NotificationClickEvent } from "../../page/models/NotificationEvent"; +import { RawNotificationPayload } from "../../shared/models/RawNotificationPayload"; -declare var self: ServiceWorkerGlobalScope & OSServiceWorkerFields; +declare const self: ServiceWorkerGlobalScope & OSServiceWorkerFields; const MAX_CONFIRMED_DELIVERY_DELAY = 25; @@ -100,7 +102,7 @@ export class ServiceWorker { self.addEventListener('activate', ServiceWorker.onServiceWorkerActivated); self.addEventListener('push', ServiceWorker.onPushReceived); self.addEventListener('notificationclose', ServiceWorker.onNotificationClosed); - self.addEventListener('notificationclick', event => event.waitUntil(ServiceWorker.onNotificationClicked(event))); + self.addEventListener('notificationclick', (event: NotificationEvent) => event.waitUntil(ServiceWorker.onNotificationClicked(event))); self.addEventListener('pushsubscriptionchange', (event: Event) => { (event as FetchEvent).waitUntil(ServiceWorker.onPushSubscriptionChange(event as unknown as SubscriptionChangeEvent)); }); @@ -245,13 +247,13 @@ export class ServiceWorker { event.waitUntil( ServiceWorker.parseOrFetchNotifications(event) - .then(async (notifications: any) => { + .then(async (rawNotificationsArray: RawNotificationPayload[]) => { //Display push notifications in the order we received them const notificationEventPromiseFns = []; const notificationReceivedPromises: Promise[] = []; const appId = await ServiceWorker.getAppId(); - for (const rawNotification of notifications) { + for (const rawNotification of rawNotificationsArray) { Log.debug('Raw Notification from OneSignal:', rawNotification); const notification = ServiceWorker.buildStructuredNotificationObject(rawNotification); @@ -266,7 +268,7 @@ export class ServiceWorker { // Probably should have it's own error handling but not blocking the rest of the execution? // Never nest the following line in a callback from the point of entering from retrieveNotifications - notificationEventPromiseFns.push((async notif => { + notificationEventPromiseFns.push((async (notif: OSNotificationDataPayload) => { await ServiceWorker.workerMessenger.broadcast(WorkerMessengerCommand.NotificationWillDisplay, notif).catch(e => Log.error(e)); ServiceWorker.executeWebhooks('notification.willDisplay', notif); @@ -526,8 +528,8 @@ export class ServiceWorker { * Constructed in onPushReceived, and passed along to other event handlers. * @param rawNotification The raw notification JSON returned from OneSignal's server. */ - static buildStructuredNotificationObject(rawNotification) { - const notification: StructuredNotification = { + static buildStructuredNotificationObject(rawNotification: RawNotificationPayload): OSNotificationDataPayload { + const notification: OSNotificationDataPayload = { id: rawNotification.custom.i, heading: rawNotification.title, content: rawNotification.alert, @@ -538,7 +540,7 @@ export class ServiceWorker { image: rawNotification.image, tag: rawNotification.tag, badge: rawNotification.badge, - vibrate: rawNotification.vibrate + vibrate: Number(rawNotification.vibrate) }; // Add action buttons @@ -553,7 +555,7 @@ export class ServiceWorker { }); } } - return Utils.trimUndefined(notification); + return Utils.trimUndefined(notification) as OSNotificationDataPayload; } /** @@ -612,13 +614,13 @@ export class ServiceWorker { * Any event needing to display a notification calls this so that all the display options can be centralized here. * @param notification A structured notification object. */ - static async displayNotification(notification, overrides?) { + static async displayNotification(notification: OSNotificationDataPayload, overrides?: object) { Log.debug(`Called %cdisplayNotification(${JSON.stringify(notification, null, 4)}):`, Utils.getConsoleStyle('code'), notification); // Use the default title if one isn't provided - const defaultTitle = await ServiceWorker._getTitle(); + const defaultTitle: string = await ServiceWorker._getTitle(); // Use the default icon if one isn't provided - const defaultIcon = await Database.get('Options', 'defaultIcon'); + const defaultIcon: string = await Database.get('Options', 'defaultIcon'); // Get option of whether we should leave notification displaying indefinitely const persistNotification = await Database.get('Options', 'persistNotification'); // Get app ID for tag value @@ -632,8 +634,9 @@ export class ServiceWorker { extra.persistNotification = persistNotification !== false; // Allow overriding some values - if (!overrides) + if (!overrides) { overrides = {}; + } notification = { ...notification, ...overrides }; ServiceWorker.ensureNotificationResourcesHttps(notification); @@ -769,18 +772,13 @@ export class ServiceWorker { * Occurs when the notification's body or action buttons are clicked. Does not occur if the notification is * dismissed by clicking the 'X' icon. See the notification close event for the dismissal event. */ - static async onNotificationClicked(event: NotificationEventInit) { + static async onNotificationClicked(event: NotificationEvent) { Log.debug(`Called %conNotificationClicked(${JSON.stringify(event, null, 4)}):`, Utils.getConsoleStyle('code'), event); // Close the notification first here, before we do anything that might fail event.notification.close(); - const { data } = event.notification; - - // Chrome 48+: Get the action button that was clicked - if (event.action) { - data.action = event.action; - } + const data = event.notification.data as OSNotificationDataPayload; let notificationClickHandlerMatch = 'exact'; let notificationClickHandlerAction = 'navigate'; @@ -798,14 +796,15 @@ export class ServiceWorker { const appId = await ServiceWorker.getAppId(); const deviceType = DeviceRecord.prototype.getDeliveryPlatform(); - const notificationClicked: NotificationClicked = { - notificationId: data.id, - action: data.action, - appId, - url: launchUrl, - timestamp: new Date().getTime(), - }; - Log.info("NotificationClicked", notificationClicked); + const notificationClickEvent: NotificationClickEvent = { + notification: data, + result: { + url: data.url, + actionId: event.action, // get action id directly from event + } + } + + Log.info("NotificationClicked", notificationClickEvent); const saveNotificationClickedPromise = (async notificationClicked => { try { const existingSession = await Database.getCurrentSession(); @@ -817,13 +816,13 @@ export class ServiceWorker { // upgrade existing session to be directly attributed to the notif // if it results in re-focusing the site if (existingSession) { - existingSession.notificationId = notificationClicked.notificationId; + existingSession.notificationId = notificationClicked.notification.id as string | null; await Database.upsertSession(existingSession); } } catch(e) { Log.error("Failed to save clicked notification.", e); } - })(notificationClicked); + })(notificationClickEvent); // Start making REST API requests BEFORE self.clients.openWindow is called. // It will cause the service worker to stop on Chrome for Android when site is added to the home screen. @@ -867,7 +866,7 @@ export class ServiceWorker { if ((client['isSubdomainIframe'] && clientUrl === launchUrl) || (!client['isSubdomainIframe'] && client.url === launchUrl) || (notificationClickHandlerAction === 'focus' && clientOrigin === launchOrigin)) { - ServiceWorker.workerMessenger.unicast(WorkerMessengerCommand.NotificationClicked, data, client); + ServiceWorker.workerMessenger.unicast(WorkerMessengerCommand.NotificationClicked, notificationClickEvent, client); try { if (client instanceof WindowClient) await client.focus(); @@ -1086,9 +1085,9 @@ export class ServiceWorker { /** * Returns a promise that is fulfilled with either the default title from the database (first priority) or the page title from the database (alternate result). */ - static _getTitle() { + static _getTitle(): Promise { return new Promise(resolve => { - Promise.all([Database.get('Options', 'defaultTitle'), Database.get('Options', 'pageTitle')]) + Promise.all([Database.get('Options', 'defaultTitle'), Database.get('Options', 'pageTitle')]) .then(([defaultTitle, pageTitle]) => { if (defaultTitle !== null) { resolve(defaultTitle); @@ -1109,7 +1108,7 @@ export class ServiceWorker { * @returns An array of notifications. The new web push protocol will only ever contain one notification, however * an array is returned for backwards compatibility with the rest of the service worker plumbing. */ - static parseOrFetchNotifications(event) { + static parseOrFetchNotifications(event: PushEvent): Promise { if (!event || !event.data) { return Promise.reject("Missing event.data on push payload!"); } @@ -1117,7 +1116,8 @@ export class ServiceWorker { const isValidPayload = ServiceWorker.isValidPushPayload(event.data); if (isValidPayload) { Log.debug("Received a valid encrypted push payload."); - return Promise.resolve([event.data.json()]); + const payload: RawNotificationPayload = event.data.json(); + return Promise.resolve([payload]); } /* @@ -1132,9 +1132,9 @@ export class ServiceWorker { * Otherwise returns false. * @param rawData The raw PushMessageData from the push event's event.data, not already parsed to JSON. */ - static isValidPushPayload(rawData) { + static isValidPushPayload(rawData: PushMessageData) { try { - const payload = rawData.json(); + const payload: RawNotificationPayload = rawData.json(); if (payload && payload.custom && payload.custom.i && diff --git a/test/support/tester/OutcomeTestHelper.ts b/test/support/tester/OutcomeTestHelper.ts index 4258974ac..dc6a30d87 100644 --- a/test/support/tester/OutcomeTestHelper.ts +++ b/test/support/tester/OutcomeTestHelper.ts @@ -1,7 +1,7 @@ import Random from '../../support/tester/Random'; import Database from '../../../src/shared/services/Database'; import { initializeNewSession, Session } from '../../../src/shared/models/Session'; -import { NotificationClicked, NotificationReceived } from '../../../src/shared/models/Notification'; +import { NotificationClicked, NotificationReceived } from '../../../src/shared/models/OSNotification'; const TEN_MINUTES_MS = 10 * 60 * 1000;