From 8e5fad39d72be2bb818c15fc0cde8c9ef82e964f Mon Sep 17 00:00:00 2001 From: Iryna Trush Date: Mon, 23 Dec 2019 13:58:30 -0800 Subject: [PATCH] Checking if there are any active pages to determine whether to upsert or deactivate the session. Currently done for https only. Http support to follow. Unfortunately, safari is special and would not give the correct focused status for its pages to the service worker as chrome does. Had to work around it with a messaging to the page: 1. sending a request to all active pages to return their focused states 2. setting a timeout of 500ms to wait for responses 3. added a handler on the page and in SW to react to the new events 4. after timeout goes off, check current clientsStatus in SW to trigger session update --- src/libraries/WorkerMessenger.ts | 2 + src/managers/ServiceWorkerManager.ts | 20 ++++++ src/models/Session.ts | 2 + src/service-worker/ServiceWorker.ts | 96 +++++++++++++++++++++++++++- src/service-worker/types.ts | 20 ++++++ 5 files changed, 138 insertions(+), 2 deletions(-) diff --git a/src/libraries/WorkerMessenger.ts b/src/libraries/WorkerMessenger.ts index 25c5e96f4..0b48a8b03 100644 --- a/src/libraries/WorkerMessenger.ts +++ b/src/libraries/WorkerMessenger.ts @@ -25,6 +25,8 @@ export enum WorkerMessengerCommand { RedirectPage = 'command.redirect', SessionUpsert = 'os.session.upsert', SessionDeactivate = 'os.session.deactivate', + AreYouVisible = "os.page_focused_request", + AreYouVisibleResponse = "os.page_focused_response", } export interface WorkerMessengerMessage { diff --git a/src/managers/ServiceWorkerManager.ts b/src/managers/ServiceWorkerManager.ts index c7718f7cc..4b6b8d97c 100644 --- a/src/managers/ServiceWorkerManager.ts +++ b/src/managers/ServiceWorkerManager.ts @@ -404,6 +404,26 @@ export class ServiceWorkerManager { workerMessenger.on(WorkerMessengerCommand.NotificationDismissed, data => { Event.trigger(OneSignal.EVENTS.NOTIFICATION_DISMISSED, data); }); + + const isHttps = !OneSignalUtils.isUsingSubscriptionWorkaround(); + // const isSafari = !!bowser.safari; or typeof window.safari !== "undefined" + const isSafari = false; // TODO: GET REAL VALUE + + // TODO: fix types. Seems like it's "{data: {payload: PageVisibilityRequest;}}" for https + // and "PageVisibilityRequest" for http + workerMessenger.on(WorkerMessengerCommand.AreYouVisible, (event: any) => { + // For https sites in Chrome and Firefox service worker (SW) can get correct value directly. + // For Safari, unfortunately, we need this messaging workaround because SW always gets false. + if (isHttps && isSafari) { + const payload = { + timestamp: event.data.payload.timestamp, + focused: document.hasFocus(), + }; + workerMessenger.directPostMessageToSW(WorkerMessengerCommand.AreYouVisibleResponse, payload); + } else { + // TODO: http + } + }); } /** diff --git a/src/models/Session.ts b/src/models/Session.ts index d579d6102..f670ecdb3 100644 --- a/src/models/Session.ts +++ b/src/models/Session.ts @@ -35,6 +35,8 @@ interface BaseSessionPayload { sessionThreshold: number; enableSessionDuration: boolean; sessionOrigin: SessionOrigin; + isHttps: boolean; + isSafari: boolean; } export interface UpsertSessionPayload extends BaseSessionPayload { diff --git a/src/service-worker/ServiceWorker.ts b/src/service-worker/ServiceWorker.ts index 61a7f0cf4..18e6a4798 100755 --- a/src/service-worker/ServiceWorker.ts +++ b/src/service-worker/ServiceWorker.ts @@ -18,9 +18,12 @@ import Log from "../libraries/Log"; import { ConfigHelper } from "../helpers/ConfigHelper"; import { OneSignalUtils } from "../utils/OneSignalUtils"; import { Utils } from "../context/shared/utils/Utils"; -import { OSWindowClient } from "./types"; +import { + OSWindowClient, OSServiceWorkerFields, PageVisibilityRequest, PageVisibilityResponse +} from "./types"; +import ServiceWorkerHelper from "../helpers/ServiceWorkerHelper"; -declare var self: ServiceWorkerGlobalScope; +declare var self: ServiceWorkerGlobalScope & OSServiceWorkerFields; declare var Notification: Notification; /** @@ -192,6 +195,20 @@ export class ServiceWorker { Log.error("Error in SW.SessionDeactivate handler", e); } }); + ServiceWorker.workerMessenger.on( + WorkerMessengerCommand.AreYouVisibleResponse, async (payload: PageVisibilityResponse) => { + Log.debug('[Service Worker] Received response for AreYouVisible', payload); + if (!self.clientsStatus) { return; } + + const timestamp = payload.timestamp; + if (self.clientsStatus.timestamp !== timestamp) { return; } + + self.clientsStatus.receivedResponsesCount++; + if (payload.focused) { + self.clientsStatus.hasAnyActiveSessions = true; + } + } + ); } /** @@ -353,6 +370,81 @@ export class ServiceWorker { return activeClients; } + static async updateSessionBasedOnHasActive( + hasAnyActiveSessions: boolean, options: DeactivateSessionPayload + ) { + if (hasAnyActiveSessions) { + await ServiceWorkerHelper.upsertSession( + options.sessionThreshold, + options.enableSessionDuration, + options.deviceRecord!, + options.deviceId, + options.sessionOrigin + ); + } else { + self.timerId = await ServiceWorkerHelper.deactivateSession( + options.sessionThreshold, options.enableSessionDuration); + } + } + + static async refreshSession(options: DeactivateSessionPayload): Promise { + Log.debug("[Service Worker] refreshSession"); + /** + * if https -> getActiveClients -> check for the first focused + * unfortunately, not enough for safari, it always returns false for focused state of a client + * have to workaround it with messaging to the client. + * + * if http, also have to workaround with messaging: + * SW to iframe -> iframe to page -> page to iframe -> iframe to SW + */ + if (options.isHttps) { + const windowClients: Client[] = await self.clients.matchAll( + { type: "window", includeUncontrolled: false } + ); + + if (options.isSafari) { + ServiceWorker.checkIfAnyClientsFocusedAndUpdateSession(windowClients, options); + } else { + const hasAnyActiveSessions: boolean = windowClients.some(w => (w as WindowClient).focused); + Log.debug("[Service Worker] isHttps hasAnyActiveSessions", hasAnyActiveSessions); + await ServiceWorker.updateSessionBasedOnHasActive(hasAnyActiveSessions, options); + } + return; + } else { + const osClients = await ServiceWorker.getActiveClients(); + ServiceWorker.checkIfAnyClientsFocusedAndUpdateSession(osClients, options); + } + } + + static checkIfAnyClientsFocusedAndUpdateSession( + windowClients: Client[], sessionInfo: DeactivateSessionPayload + ): void { + const timestamp = new Date().getTime(); + self.clientsStatus = { + timestamp, + sentRequestsCount: 0, + receivedResponsesCount: 0, + hasAnyActiveSessions: false, + }; + const payload: PageVisibilityRequest = { timestamp }; + windowClients.forEach(c => { + if (self.clientsStatus) { + // keeping track of number of sent requests mostly for debugging purposes + self.clientsStatus.sentRequestsCount++; + } + c.postMessage({ command: WorkerMessengerCommand.AreYouVisible, payload }) + }); + self.setTimeout(() => { + if (!self.clientsStatus) { return; } + if (self.clientsStatus.timestamp !== timestamp) { return; } + + Log.debug("updateSessionBasedOnHasActive", self.clientsStatus); + ServiceWorker.updateSessionBasedOnHasActive( + self.clientsStatus.hasAnyActiveSessions, sessionInfo); + self.clientsStatus = undefined; + }, 500); + } + /** * Constructs a structured notification object from the raw notification fetched from OneSignal's server. This * object is passed around from event to event, and is also returned to the host page for notification event details. diff --git a/src/service-worker/types.ts b/src/service-worker/types.ts index 2182181bb..3327b2837 100644 --- a/src/service-worker/types.ts +++ b/src/service-worker/types.ts @@ -1,3 +1,23 @@ export interface OSWindowClient extends WindowClient { isSubdomainIframe: boolean; } + +export interface ClientStatus { + timestamp: number; + sentRequestsCount: number; + receivedResponsesCount: number; + hasAnyActiveSessions: boolean; +} + +export interface PageVisibilityRequest { + timestamp: number; +} + +export interface PageVisibilityResponse extends PageVisibilityRequest { + focused: boolean; +} + +export interface OSServiceWorkerFields { + timerId?: number; + clientsStatus?: ClientStatus; +}