Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ on:

jobs:
test:
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
container:
image: python:2.7.18-buster

Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM node:14.21.1
FROM node:22
WORKDIR /sdk
COPY package.json .
RUN yarn
16 changes: 9 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
"bowser": "github:OneSignal/bowser#fix-android8-opr6-build-detection",
"jsdom": "^9.12.0",
"jsonp": "github:OneSignal/jsonp#onesignal",
"node-sass": "^4.9.0",
"npm-css": "https://registry.npmjs.org/npm-css/-/npm-css-0.2.3.tgz",
"postcss-discard-comments": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-2.0.4.tgz",
"postcss-filter-plugins": "https://registry.npmjs.org/postcss-filter-plugins/-/postcss-filter-plugins-2.0.2.tgz",
"sass": "^1.90.0",
"tslib": "^1.9.0",
"validator": "https://registry.npmjs.org/validator/-/validator-6.0.0.tgz"
},
Expand All @@ -29,7 +29,7 @@
"build:dev": "ENV=development yarn transpile:sources && ENV=development yarn bundle-sw && ENV=development yarn bundle-sdk && ENV=development yarn bundle-page-sdk-es6 && ENV=development build/scripts/publish.sh",
"build:staging": "ENV=staging yarn transpile:sources && ENV=staging yarn bundle-sw && ENV=staging yarn bundle-sdk && ENV=staging yarn bundle-page-sdk-es6 && ENV=staging build/scripts/publish.sh",
"build:prod": "ENV=production yarn transpile:sources && ENV=production yarn bundle-sw && ENV=production yarn bundle-sdk && ENV=production yarn bundle-page-sdk-es6 && bundlesize && ENV=production build/scripts/publish.sh",
"test": "ENV=development yarn transpile:tests && yarn run ava --verbose --color --watch",
"test": "ENV=development yarn transpile:tests && yarn run ava --verbose --color --watch --serial --no-worker-threads",
"test:noWatch": "ENV=development yarn transpile:tests && ava --verbose --color --watch=false",
"test:CI": "ENV=development yarn transpile:tests && ava --config ava-ci-config.js --verbose --color --watch=false",
"publish": "yarn clean && yarn build:prod && yarn",
Expand All @@ -40,7 +40,7 @@
"jest": "jest --coverage"
},
"config": {
"sdkVersion": "151606"
"sdkVersion": "151607"
},
"repository": {
"type": "git",
Expand Down Expand Up @@ -88,17 +88,18 @@
"nyc": "^11.1.0",
"postcss-loader": "^2.0.6",
"prettier": "^2.7.1",
"sass-loader": "^6.0.6",
"sass-loader": "8.0.2",
"sinon": "^2.4.1",
"svgo": "^0.7.2",
"text-encoding": "^0.6.4",
"timemachine": "^0.3.0",
"ts-node": "^9.1.1",
"typescript": "^4.1.2",
"uglifyjs-webpack-plugin": "^1.2.4",
"undici": "^7.16.0",
"webpack": "^4.3.0",
"webpack-bundle-analyzer": "^3.3.2",
"webpack-cli": "^2.0.13"
"webpack-cli": "3.3.12"
},
"ava": {
"extensions": [
Expand Down Expand Up @@ -150,5 +151,6 @@
"maxSize": "9 kB",
"compression": "gzip"
}
]
}
],
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}
4 changes: 2 additions & 2 deletions src/libraries/WorkerMessenger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ export class WorkerMessenger {
Log.debug(`(${location.origin}) [Worker Messenger] Page is now listening for messages.`);
}

onWorkerMessageReceivedFromPage(event: ServiceWorkerMessageEvent) {
onWorkerMessageReceivedFromPage(event: ExtendableMessageEvent) {
const data: WorkerMessengerMessage = event.data;

/* If this message doesn't contain our expected fields, discard the message */
Expand Down Expand Up @@ -235,7 +235,7 @@ export class WorkerMessenger {
message topic. If no one is listening to the message, it is discarded;
otherwise, the listener callback is executed.
*/
onPageMessageReceivedFromServiceWorker(event: ServiceWorkerMessageEvent) {
onPageMessageReceivedFromServiceWorker(event: ExtendableMessageEvent) {
const data: WorkerMessengerMessage = event.data;

/* If this message doesn't contain our expected fields, discard the message */
Expand Down
30 changes: 29 additions & 1 deletion src/service-worker/ServiceWorker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { NotificationReceived, NotificationClicked } from "../models/Notificatio
import { cancelableTimeout } from "../helpers/sw/CancelableTimeout";
import { DeviceRecord } from '../models/DeviceRecord';
import { awaitableTimeout } from "../utils/AwaitableTimeout";
import { AppConfig } from "src/models/AppConfig";

declare var self: ServiceWorkerGlobalScope & OSServiceWorkerFields;

Expand Down Expand Up @@ -156,6 +157,11 @@ export class ServiceWorker {
return appId;
}

static async getAppConfig(): Promise<AppConfig> {
const appId = await ServiceWorker.getAppId();
return await ConfigHelper.getAppConfig({ appId }, OneSignalApiSW.downloadServerAppConfig);
}

static setupMessageListeners() {
ServiceWorker.workerMessenger.on(WorkerMessengerCommand.WorkerVersion, _ => {
Log.debug('[Service Worker] Received worker version message.');
Expand Down Expand Up @@ -750,8 +756,30 @@ export class ServiceWorker {

// Use the user-provided default URL if one exists
const { defaultNotificationUrl: dbDefaultNotificationUrl } = await Database.getAppState();
if (dbDefaultNotificationUrl)
if (dbDefaultNotificationUrl) {
launchUrl = dbDefaultNotificationUrl;
}
else {
// There was an issue with legacy HTTP integrations where the defaultNotificationUrl
// was never saved to the DB. To account for this, we try to get the default URL
// from the app config on notification click and save it to the DB for future use.
// Choosing between notification received and notification clicked to do this logic,
// we decided on notification clicked to avoid doing extra api call when the user
// may never click the notification.
try {
const config = await ServiceWorker.getAppConfig();
const defaultNotificationUrlFromConfig = config.origin;
if (defaultNotificationUrlFromConfig) {
launchUrl = defaultNotificationUrlFromConfig;

if (!dbDefaultNotificationUrl) {
await Database.setAppState({ defaultNotificationUrl: defaultNotificationUrlFromConfig });
}
}
} catch (e) {
Log.error("Failed to update notification in the database", e);
}
}

// If the user clicked an action button, use the URL provided by the action button
// Unless the action button URL is null
Expand Down
4 changes: 2 additions & 2 deletions src/services/Database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ export default class Database {
return state;
}

async setAppState(appState: AppState) {
async setAppState(appState: Partial<AppState>) {
if (appState.defaultNotificationUrl)
await this.put("Options", { key: "defaultUrl", value: appState.defaultNotificationUrl });
if (appState.defaultNotificationTitle || appState.defaultNotificationTitle === "")
Expand Down Expand Up @@ -525,7 +525,7 @@ export default class Database {
return await Database.singletonInstance.getServiceWorkerState();
}

static async setAppState(appState: AppState) {
static async setAppState(appState: Partial<AppState>) {
return await Database.singletonInstance.setAppState(appState);
}

Expand Down
4 changes: 2 additions & 2 deletions test/support/mocks/service-workers/models/MockClients.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ export class MockClients implements Clients {
return Promise.resolve(client || null);
}

async matchAll(_options?: ClientQueryOptions): Promise<Client[]> {
return this.clients;
async matchAll<T extends ClientQueryOptions>(options?: T): Promise<ReadonlyArray<T["type"] extends "window" ? WindowClient : Client>> {
return Object.freeze(this.clients) as ReadonlyArray<T["type"] extends "window" ? WindowClient : Client>;
}

async openWindow(_url: string): Promise<WindowClient | null> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export class MockPushManager implements PushManager {
return this.subscription;
}

public async permissionState(_options?: PushSubscriptionOptionsInit): Promise<PushPermissionState> {
public async permissionState(_options?: PushSubscriptionOptionsInit): Promise<PermissionState> {
return "granted";
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export class MockServiceWorker implements ServiceWorker {
throw new NotImplementedError();
}

postMessage(message: any, transfer: Array<Transferable> | PostMessageOptions): void {
postMessage(message: any, transfer: Array<Transferable> | any): void {
}

removeEventListener<K extends keyof ServiceWorkerEventMap>(type: K, listener: (this: ServiceWorker, ev: ServiceWorkerEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ export class MockServiceWorkerGlobalScope implements ServiceWorkerGlobalScope {
return this.registration;
}

readonly fonts: FontFaceSet;
readonly crossOriginIsolated: boolean;
reportError: (e: any) => void;
structuredClone: (input: any) => any;

readonly caches: CacheStorage;
readonly clients: Clients = new MockClients();
readonly console: Console;
Expand Down Expand Up @@ -51,7 +56,7 @@ export class MockServiceWorkerGlobalScope implements ServiceWorkerGlobalScope {
onrejectionhandled: ((this: WorkerGlobalScope, ev: PromiseRejectionEvent) => any) | null = null;
onpush: ((this: ServiceWorkerGlobalScope, ev: PushEvent) => any) | null = null;
onpushsubscriptionchange: ((this: ServiceWorkerGlobalScope, ev: PushSubscriptionChangeEvent) => any) | null = null;
onsync: ((this: ServiceWorkerGlobalScope, ev: SyncEvent) => any) | null = null;
onsync: ((this: ServiceWorkerGlobalScope, ev: Event) => any) | null = null;
onunhandledrejection: ((this: WorkerGlobalScope, ev: PromiseRejectionEvent) => any) | null = null;
queueMicrotask(callback: VoidFunction): void {}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ export class MockServiceWorkerRegistration implements ServiceWorkerRegistration
waiting: ServiceWorker | null;
onupdatefound: ((this: ServiceWorkerRegistration, ev: Event) => any) | null;
readonly pushManager: PushManager;
readonly sync: SyncManager;
navigationPreload: NavigationPreloadManager;
updateViaCache: ServiceWorkerUpdateViaCache;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@ export class MockWorkerNavigator implements WorkerNavigator {
public readonly userAgent: string
) {
}
mediaCapabilities: MediaCapabilities;
clearAppBadge(): Promise<void> {
throw new Error("Method not implemented.");
}
setAppBadge(contents?: unknown): Promise<void> {
throw new Error("Method not implemented.");
}
locks: LockManager;

readonly hardwareConcurrency: number;
readonly onLine: boolean;
Expand Down
47 changes: 35 additions & 12 deletions test/support/polyfills/polyfills.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import fetch from 'node-fetch';
import { MockServiceWorkerGlobalScope } from "../mocks/service-workers/models/MockServiceWorkerGlobalScope";

// NodeJS.Global
Expand All @@ -8,7 +7,6 @@ global.URL = require('url').URL;
global.indexedDB = require('fake-indexeddb');
global.IDBKeyRange = require("fake-indexeddb/lib/FDBKeyRange");
global.Headers = require('./Headers');
global.fetch = fetch;

global.btoa = function(str: string | Buffer) {
let buffer;
Expand All @@ -24,15 +22,40 @@ global.atob = function(str: string) {
};

// Add any methods from ServiceWorkerGlobalScope to NodeJS's global if they don't exist already
export function addServiceWorkerGlobalScopeToGlobal(serviceWorkerGlobalScope: ServiceWorkerGlobalScope): void {
global = Object.assign(global, serviceWorkerGlobalScope);
const props = Object.getOwnPropertyNames(MockServiceWorkerGlobalScope.prototype);
for(const propName of props) {
// Do NOT replace any existing stubs or default NodeJS global methods
if (!!global[propName])
continue;

// Add method to NodeJS global
global[propName] = (<any>serviceWorkerGlobalScope)[propName];
export function addServiceWorkerGlobalScopeToGlobal(serviceWorkerScope: ServiceWorkerGlobalScope): void {
for (const key of Object.keys(serviceWorkerScope)) {
const descriptor = Object.getOwnPropertyDescriptor(global, key);

// Skip keys that exist and are not writable
if (descriptor && !descriptor.writable) continue;

// Skip non-configurable properties (navigator etc.)
if (descriptor && !descriptor.configurable) continue;

// Safe to define
Object.defineProperty(global, key, {
value: serviceWorkerScope[key],
writable: true,
configurable: true,
enumerable: true
});
}

// Install prototype methods too (but skip existing built-ins)
const protoKeys = Object.getOwnPropertyNames(MockServiceWorkerGlobalScope.prototype);
for (const key of protoKeys) {
if (key === "constructor") continue;

const descriptor = Object.getOwnPropertyDescriptor(global, key);
if (descriptor && !descriptor.configurable) continue;

if (!(key in global)) {
Object.defineProperty(global, key, {
value: serviceWorkerScope[key],
writable: true,
configurable: true,
enumerable: false,
});
}
}
}
31 changes: 16 additions & 15 deletions test/support/sdk/TestEnvironment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ import { NotificationPermission } from "../../../src/models/NotificationPermissi
import jsdom from 'jsdom';
// @ts-ignore
import DOMStorage from "dom-storage";
// @ts-ignore
import fetch from "node-fetch";

import SdkEnvironment from '../../../src/managers/SdkEnvironment';
import { TestEnvironmentKind } from '../../../src/models/TestEnvironmentKind';
Expand Down Expand Up @@ -197,7 +195,6 @@ export class TestEnvironment {
addServiceWorkerGlobalScopeToGlobal(serviceWorkerScope);

global.location = config.url ? config.url : new URL('https://localhost:3001/webpush/sandbox?https=1');
global.fetch = fetch;
global.self = global;
return global;
}
Expand Down Expand Up @@ -265,17 +262,13 @@ export class TestEnvironment {
(windowDef as any).isSecureContext = isSecureContext;
(windowDef as any).location = url;

const realSetTimeout = global.setTimeout.bind(global);
const realClearTimeout = global.clearTimeout.bind(global);

if (config.stubSetTimeout) {
(windowDef as any).setTimeout = async (callback: Function, timeDelay: number) => {
console.log("override setTimeout invoked");
const end = new Date().getTime() + timeDelay;
while (new Date().getTime() < end) {
// wait
}
await callback();
};
(windowDef as any).setTimeout = (cb: Function, ms: number = 0) => realSetTimeout(() => cb(), ms);
(windowDef as any).clearTimeout = (id: any) => realClearTimeout(id);
}

TestEnvironment.addCustomEventPolyfill(windowDef);

const topWindow = config.initializeAsIframe ? {
Expand All @@ -286,7 +279,15 @@ export class TestEnvironment {
}
} as any : windowDef;
jsdom.reconfigureWindow(windowDef, { top: topWindow });
Object.assign(global, windowDef);
const { navigator, ...rest } = windowDef as any;
Object.assign(global, rest);

// then define navigator explicitly in a safe way
Object.defineProperty(global, "navigator", {
value: navigator,
configurable: true
});
// Object.assign(global, windowDef);
return jsdom;
}

Expand Down Expand Up @@ -913,8 +914,8 @@ export class TestEnvironment {
// returnable spys
const sendTagsSpy = sinonSandbox.spy(TagManager.prototype, "sendTags");

// network mocks
mockGetIcon();
// // network mocks
// mockGetIcon();

if (testConfig.httpOrHttps === HttpHttpsEnvironment.Http) {
stubMessageChannel(t);
Expand Down
12 changes: 12 additions & 0 deletions test/support/tester/NockOneSignalHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,18 @@ export class NockOneSignalHelper {
});
}

static nockGetConfig(appId: string, body: any): NockScopeWithResultPromise {
return NockHelper.nockBase({
method: "get",
baseUrl: "https://onesignal.com",
path: `/api/v1/sync/${appId}/web`,
reply: {
status: 200,
body: body
}
});
}

static nockNotificationConfirmedDelivery(notificationId?: string): NockScopeWithResultPromise {
return NockHelper.nockBase({
method: "put",
Expand Down
Loading
Loading