Skip to content

Commit

Permalink
Checking if there are any active pages to determine whether to upsert
Browse files Browse the repository at this point in the history
  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
  • Loading branch information
itrush committed Apr 8, 2020
1 parent 8dc041f commit 8e5fad3
Show file tree
Hide file tree
Showing 5 changed files with 138 additions and 2 deletions.
2 changes: 2 additions & 0 deletions src/libraries/WorkerMessenger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
20 changes: 20 additions & 0 deletions src/managers/ServiceWorkerManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
});
}

/**
Expand Down
2 changes: 2 additions & 0 deletions src/models/Session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ interface BaseSessionPayload {
sessionThreshold: number;
enableSessionDuration: boolean;
sessionOrigin: SessionOrigin;
isHttps: boolean;
isSafari: boolean;
}

export interface UpsertSessionPayload extends BaseSessionPayload {
Expand Down
96 changes: 94 additions & 2 deletions src/service-worker/ServiceWorker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -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;
}
}
);
}

/**
Expand Down Expand Up @@ -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<void> {
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 }

This comment has been minimized.

Copy link
@jkasten2

jkasten2 Feb 10, 2021

Member

@itrush Any reason why we used includeUncontrolled false instead of true here?
All other usages of clients.matchAll use includeUncontrolled: true instead.

);

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.
Expand Down
20 changes: 20 additions & 0 deletions src/service-worker/types.ts
Original file line number Diff line number Diff line change
@@ -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;
}

0 comments on commit 8e5fad3

Please sign in to comment.