From 5958719b542bd6c34c2b8c1cc0f7a61d59b83fd4 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Mon, 28 Jul 2025 15:24:21 -0700 Subject: [PATCH 01/12] replace bowser castle logic for simpler user agent helpers --- .../support/environment/TestEnvironment.ts | 2 - .../environment/TestEnvironmentHelpers.ts | 22 +-- __test__/unit/core/coreModuleDirector.test.ts | 6 - __test__/unit/core/osModel.test.ts | 6 - __test__/unit/http/sdkVersion.test.ts | 6 +- .../nativePermissionChange.test.ts | 6 +- package-lock.json | 6 - package.json | 1 - .../LoginUserOperationExecutor.test.ts | 2 - .../RefreshUserOperationExecutor.test.ts | 2 - .../SubscriptionOperationExecutor.test.ts | 7 +- src/core/operationRepo/OperationRepo.test.ts | 2 - src/onesignal/OneSignal.ts | 16 +- src/page/bell/Bell.ts | 11 +- src/page/bell/Dialog.ts | 23 ++- src/page/helpers/EnvironmentInfoHelper.ts | 74 -------- src/page/managers/PromptsManager.ts | 25 ++- src/page/models/Context.ts | 6 - src/page/models/EnvironmentInfo.ts | 10 -- src/page/slidedown/ConfirmationToast.ts | 16 +- src/page/slidedown/Slidedown.ts | 4 +- .../userModel/FuturePushSubscriptionRecord.ts | 58 ++++++ src/shared/environment/environment.ts | 15 +- src/shared/managers/SubscriptionManager.ts | 6 +- .../managers/sessionManager/SessionManager.ts | 9 +- .../Browser.ts => useragent/constants.ts} | 7 +- src/shared/useragent/index.ts | 3 + src/shared/useragent/types.ts | 3 + src/shared/useragent/useragent.test.ts | 169 ++++++++++++++++++ src/shared/useragent/useragent.ts | 65 +++++++ src/shared/utils/OneSignalUtils.ts | 10 -- src/shared/utils/bowserCastle.ts | 10 -- src/shared/utils/utils.ts | 7 +- src/sw/serviceWorker/ServiceWorker.ts | 4 +- 34 files changed, 368 insertions(+), 251 deletions(-) delete mode 100644 src/page/helpers/EnvironmentInfoHelper.ts delete mode 100644 src/page/models/EnvironmentInfo.ts rename src/shared/{models/Browser.ts => useragent/constants.ts} (60%) create mode 100644 src/shared/useragent/index.ts create mode 100644 src/shared/useragent/types.ts create mode 100644 src/shared/useragent/useragent.test.ts create mode 100644 src/shared/useragent/useragent.ts delete mode 100644 src/shared/utils/bowserCastle.ts diff --git a/__test__/support/environment/TestEnvironment.ts b/__test__/support/environment/TestEnvironment.ts index 627f22ad5..ad88d0948 100644 --- a/__test__/support/environment/TestEnvironment.ts +++ b/__test__/support/environment/TestEnvironment.ts @@ -10,7 +10,6 @@ import { generateNewSubscription } from '../helpers/core'; import BrowserUserAgent from '../models/BrowserUserAgent'; import { initOSGlobals, - mockUserAgent, resetDatabase, stubDomEnvironment, stubNotification, @@ -33,7 +32,6 @@ export interface TestEnvironmentConfig { export class TestEnvironment { static async initialize(config: TestEnvironmentConfig = {}) { - mockUserAgent(config); // reset db & localStorage resetDatabase(); diff --git a/__test__/support/environment/TestEnvironmentHelpers.ts b/__test__/support/environment/TestEnvironmentHelpers.ts index 2044fb501..3cbe4b61b 100644 --- a/__test__/support/environment/TestEnvironmentHelpers.ts +++ b/__test__/support/environment/TestEnvironmentHelpers.ts @@ -1,4 +1,3 @@ -import bowser from 'bowser'; import { type DOMWindow, JSDOM, ResourceLoader } from 'jsdom'; import CoreModule from 'src/core/CoreModule'; import { SubscriptionModel } from 'src/core/models/SubscriptionModel'; @@ -17,12 +16,7 @@ import { getSlidedownElement } from '../../../src/page/slidedown/SlidedownElemen import Emitter from '../../../src/shared/libraries/Emitter'; import Database from '../../../src/shared/services/Database'; import { CUSTOM_LINK_CSS_CLASSES } from '../../../src/shared/slidedown/constants'; -import * as bowerCastleHelpers from '../../../src/shared/utils/bowserCastle'; -import { - DEVICE_OS, - DUMMY_ONESIGNAL_ID, - DUMMY_SUBSCRIPTION_ID_3, -} from '../constants'; +import { DUMMY_ONESIGNAL_ID, DUMMY_SUBSCRIPTION_ID_3 } from '../constants'; import MockNotification from '../mocks/MockNotification'; import BrowserUserAgent from '../models/BrowserUserAgent'; import Random from '../utils/Random'; @@ -31,26 +25,12 @@ import { type TestEnvironmentConfig } from './TestEnvironment'; declare const global: any; -const bowserCastleSpy = vi.spyOn(bowerCastleHelpers, 'bowserCastle'); - export function resetDatabase() { // Erase and reset IndexedDb database name to something random Database.resetInstance(); Database.databaseInstanceName = Random.getRandomString(10); } -export function mockUserAgent(config: TestEnvironmentConfig = {}): void { - // @ts-expect-error - bowser is not typed correctly - const info = bowser._detect(config.userAgent ?? BrowserUserAgent.Default); - // Modify the mock implementation - bowserCastleSpy.mockReturnValue({ - mobile: info.mobile, - tablet: info.tablet, - name: info.name.toLowerCase(), - version: info.version, - }); -} - export async function initOSGlobals(config: TestEnvironmentConfig = {}) { global.OneSignal = OneSignal; global.OneSignal.EVENTS = ONESIGNAL_EVENTS; diff --git a/__test__/unit/core/coreModuleDirector.test.ts b/__test__/unit/core/coreModuleDirector.test.ts index 60a99b5f4..a764037ba 100644 --- a/__test__/unit/core/coreModuleDirector.test.ts +++ b/__test__/unit/core/coreModuleDirector.test.ts @@ -1,4 +1,3 @@ -import { mockUserAgent } from '__test__/support/environment/TestEnvironmentHelpers'; import { CoreModuleDirector } from '../../../src/core/CoreModuleDirector'; import { TestEnvironment } from '../../support/environment/TestEnvironment'; import { @@ -12,11 +11,6 @@ describe('CoreModuleDirector tests', () => { }); describe('getPushSubscriptionModel', () => { - beforeEach(() => { - vi.resetAllMocks(); - mockUserAgent(); - }); - async function getPushSubscriptionModel() { return (await getCoreModuleDirector()).getPushSubscriptionModel(); } diff --git a/__test__/unit/core/osModel.test.ts b/__test__/unit/core/osModel.test.ts index ccc691ae7..acb96dc32 100644 --- a/__test__/unit/core/osModel.test.ts +++ b/__test__/unit/core/osModel.test.ts @@ -1,14 +1,8 @@ -import { DEVICE_OS } from '__test__/support/constants'; -import { mockUserAgent } from '__test__/support/environment/TestEnvironmentHelpers'; import { SubscriptionModel } from 'src/core/models/SubscriptionModel'; import { SubscriptionType } from 'src/core/types/subscription'; import { generateNewSubscription } from '../../support/helpers/core'; describe('Model tests', () => { - beforeAll(() => { - mockUserAgent(); - }); - test('Set function updates data', async () => { const newSub = generateNewSubscription(); expect(newSub.enabled).toBe(undefined); diff --git a/__test__/unit/http/sdkVersion.test.ts b/__test__/unit/http/sdkVersion.test.ts index f400bcc60..e41c171cf 100644 --- a/__test__/unit/http/sdkVersion.test.ts +++ b/__test__/unit/http/sdkVersion.test.ts @@ -1,4 +1,3 @@ -import { mockUserAgent } from '__test__/support/environment/TestEnvironmentHelpers'; import { generateNewSubscription } from '__test__/support/helpers/core'; import { nock } from '__test__/support/helpers/general'; import AliasPair from '../../../src/core/requestService/AliasPair'; @@ -11,10 +10,7 @@ import { import { expectHeaderToBeSent } from '../../support/helpers/sdkVersion'; describe('Sdk Version Header Tests', () => { - beforeAll(() => { - nock({}); - mockUserAgent(); - }); + beforeAll(() => nock({})); test('POST /users: SDK-Version header is sent', () => { // @ts-expect-error - partial identity object diff --git a/__test__/unit/pushSubscription/nativePermissionChange.test.ts b/__test__/unit/pushSubscription/nativePermissionChange.test.ts index 3fd59e215..3e5e088cb 100644 --- a/__test__/unit/pushSubscription/nativePermissionChange.test.ts +++ b/__test__/unit/pushSubscription/nativePermissionChange.test.ts @@ -6,10 +6,7 @@ import { DUMMY_SUBSCRIPTION_ID_3, } from '__test__/support/constants'; import { TestEnvironment } from '__test__/support/environment/TestEnvironment'; -import { - createPushSub, - mockUserAgent, -} from '__test__/support/environment/TestEnvironmentHelpers'; +import { createPushSub } from '__test__/support/environment/TestEnvironmentHelpers'; import { MockServiceWorker } from '__test__/support/mocks/MockServiceWorker'; import Emitter from 'src/shared/libraries/Emitter'; import { AppState } from 'src/shared/models/AppState'; @@ -29,7 +26,6 @@ vi.useFakeTimers(); describe('Notification Types are set correctly on subscription change', () => { beforeEach(async () => { - mockUserAgent(); await TestEnvironment.initialize(); OneSignal.emitter = new Emitter(); }); diff --git a/package-lock.json b/package-lock.json index 46a1d68af..1ea8a13c5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,6 @@ "version": "1.2.0", "license": "SEE LICENSE IN LICENSE", "dependencies": { - "bowser": "github:OneSignal/bowser#fix-android8-opr6-build-detection", "jsonp": "github:OneSignal/jsonp#onesignal", "uuid": "^11.1.0" }, @@ -2212,11 +2211,6 @@ "dev": true, "license": "MIT" }, - "node_modules/bowser": { - "version": "1.7.2", - "resolved": "git+ssh://git@github.com/OneSignal/bowser.git#acdbf27b721d27e7abdb559be1e20773f21d1da5", - "license": "MIT" - }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", diff --git a/package.json b/package.json index ba5df96b0..b71e849c1 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,6 @@ "description": "Web push notifications from OneSignal.", "type": "module", "dependencies": { - "bowser": "github:OneSignal/bowser#fix-android8-opr6-build-detection", "jsonp": "github:OneSignal/jsonp#onesignal", "uuid": "^11.1.0" }, diff --git a/src/core/executors/LoginUserOperationExecutor.test.ts b/src/core/executors/LoginUserOperationExecutor.test.ts index 25c8de811..1858b5a2a 100644 --- a/src/core/executors/LoginUserOperationExecutor.test.ts +++ b/src/core/executors/LoginUserOperationExecutor.test.ts @@ -10,7 +10,6 @@ import { DUMMY_SUBSCRIPTION_ID_2, } from '__test__/support/constants'; import { TestEnvironment } from '__test__/support/environment/TestEnvironment'; -import { mockUserAgent } from '__test__/support/environment/TestEnvironmentHelpers'; import { SomeOperation } from '__test__/support/helpers/executors'; import { createUserFn, @@ -54,7 +53,6 @@ describe('LoginUserOperationExecutor', () => { beforeEach(async () => { await Database.clear(); - mockUserAgent(); identityModelStore = new IdentityModelStore(); propertiesModelStore = new PropertiesModelStore(); subscriptionModelStore = new SubscriptionModelStore(); diff --git a/src/core/executors/RefreshUserOperationExecutor.test.ts b/src/core/executors/RefreshUserOperationExecutor.test.ts index 6c0b3370d..818639a25 100644 --- a/src/core/executors/RefreshUserOperationExecutor.test.ts +++ b/src/core/executors/RefreshUserOperationExecutor.test.ts @@ -7,7 +7,6 @@ import { DUMMY_SUBSCRIPTION_ID, DUMMY_SUBSCRIPTION_ID_2, } from '__test__/support/constants'; -import { mockUserAgent } from '__test__/support/environment/TestEnvironmentHelpers'; import { SomeOperation } from '__test__/support/helpers/executors'; import { setGetUserError, @@ -39,7 +38,6 @@ vi.mock('src/shared/libraries/Log'); describe('RefreshUserOperationExecutor', () => { beforeEach(async () => { await Database.clear(); // in case subscription model (from previous tests) are loaded from db - mockUserAgent(); identityModelStore = new IdentityModelStore(); propertiesModelStore = new PropertiesModelStore(); subscriptionModelStore = new SubscriptionModelStore(); diff --git a/src/core/executors/SubscriptionOperationExecutor.test.ts b/src/core/executors/SubscriptionOperationExecutor.test.ts index b3097f755..2cb31e78b 100644 --- a/src/core/executors/SubscriptionOperationExecutor.test.ts +++ b/src/core/executors/SubscriptionOperationExecutor.test.ts @@ -3,10 +3,7 @@ import { DUMMY_ONESIGNAL_ID, DUMMY_SUBSCRIPTION_ID_3, } from '__test__/support/constants'; -import { - createPushSub, - mockUserAgent, -} from '__test__/support/environment/TestEnvironmentHelpers'; +import { createPushSub } from '__test__/support/environment/TestEnvironmentHelpers'; import { SomeOperation } from '__test__/support/helpers/executors'; import { server } from '__test__/support/mocks/server'; import { http, HttpResponse } from 'msw'; @@ -41,12 +38,10 @@ const BACKEND_SUBSCRIPTION_ID = 'backend-subscription-id'; vi.mock('src/shared/libraries/Log'); -mockUserAgent(); const pushSubscription = createPushSub(); describe('SubscriptionOperationExecutor', () => { beforeEach(async () => { - mockUserAgent(); subscriptionModelStore = new SubscriptionModelStore(); newRecordsState = new NewRecordsState(); diff --git a/src/core/operationRepo/OperationRepo.test.ts b/src/core/operationRepo/OperationRepo.test.ts index 83843a379..1ab91547a 100644 --- a/src/core/operationRepo/OperationRepo.test.ts +++ b/src/core/operationRepo/OperationRepo.test.ts @@ -3,7 +3,6 @@ import { DUMMY_ONESIGNAL_ID, DUMMY_SUBSCRIPTION_ID, } from '__test__/support/constants'; -import { mockUserAgent } from '__test__/support/environment/TestEnvironmentHelpers'; import { fakeWaitForOperations } from '__test__/support/helpers/executors'; import Log from 'src/shared/libraries/Log'; import Database, { type OperationItem } from 'src/shared/services/Database'; @@ -39,7 +38,6 @@ vi.spyOn(OperationModelStore.prototype, 'create').mockImplementation(() => { }); let mockOperationModelStore: OperationModelStore; -mockUserAgent(); describe('OperationRepo', () => { let opRepo: OperationRepo; diff --git a/src/onesignal/OneSignal.ts b/src/onesignal/OneSignal.ts index 58461e8af..5fdf5765c 100644 --- a/src/onesignal/OneSignal.ts +++ b/src/onesignal/OneSignal.ts @@ -5,13 +5,16 @@ import { type AppUserConfig, } from 'src/shared/config'; import { windowEnvString } from 'src/shared/environment'; +import { + Browser, + getBrowserName, + getBrowserVersion, +} from 'src/shared/useragent'; import { VERSION } from 'src/shared/utils/EnvVariables'; import CoreModule from '../core/CoreModule'; import { CoreModuleDirector } from '../core/CoreModuleDirector'; -import { EnvironmentInfoHelper } from '../page/helpers/EnvironmentInfoHelper'; import LoginManager from '../page/managers/LoginManager'; import Context from '../page/models/Context'; -import type { EnvironmentInfo } from '../page/models/EnvironmentInfo'; import type { OneSignalDeferredLoadedCallback } from '../page/models/OneSignalDeferredLoadedCallback'; import TimedLocalStorage from '../page/modules/TimedLocalStorage'; import { ProcessOneSignalPushCalls } from '../page/utils/ProcessOneSignalPushCalls'; @@ -27,7 +30,6 @@ import Emitter from '../shared/libraries/Emitter'; import Log from '../shared/libraries/Log'; import Database from '../shared/services/Database'; import OneSignalEvent from '../shared/services/OneSignalEvent'; -import { bowserCastle } from '../shared/utils/bowserCastle'; import LocalStorage from '../shared/utils/LocalStorage'; import { logMethodCall } from '../shared/utils/utils'; import DebugNamespace from './DebugNamesapce'; @@ -56,11 +58,8 @@ export default class OneSignal { Log.debug('OneSignal: Final web app config:', appConfig); - // TODO: environmentInfo is explicitly dependent on existence of OneSignal.config. Needs refactor. // Workaround to temp assign config so that it can be used in context. OneSignal.config = appConfig; - OneSignal.environmentInfo = EnvironmentInfoHelper.getEnvironmentInfo(); - OneSignal.context = new Context(appConfig); OneSignal.config = OneSignal.context.appConfig; } @@ -125,7 +124,7 @@ export default class OneSignal { static async init(options: AppUserConfig) { logMethodCall('init'); Log.debug( - `Browser Environment: ${bowserCastle().name} ${bowserCastle().version}`, + `Browser Environment: ${getBrowserName()} ${getBrowserVersion()}`, ); LocalStorage.removeLegacySubscriptionOptions(); @@ -136,7 +135,7 @@ export default class OneSignal { throw new Error('OneSignal config not initialized!'); } - if (bowserCastle().name == 'safari' && !OneSignal.config.safariWebId) { + if (getBrowserName() === Browser.Safari && !OneSignal.config.safariWebId) { /** * Don't throw an error for missing Safari config; many users set up * support on Chrome/Firefox and don't intend to support Safari but don't @@ -272,7 +271,6 @@ export default class OneSignal { static __doNotShowWelcomeNotification: boolean; static VERSION = VERSION; - static environmentInfo?: EnvironmentInfo; static config: AppConfig | null = null; static _sessionInitAlreadyRunning = false; static _isNewVisitor = false; diff --git a/src/page/bell/Bell.ts b/src/page/bell/Bell.ts index 03e32b221..2f7e069c5 100755 --- a/src/page/bell/Bell.ts +++ b/src/page/bell/Bell.ts @@ -1,12 +1,12 @@ import type { AppUserConfigNotifyButton } from 'src/shared/config'; import type { BellPosition, BellSize, BellText } from 'src/shared/prompts'; +import { Browser, getBrowserName } from 'src/shared/useragent'; import OneSignal from '../../onesignal/OneSignal'; import { DismissHelper } from '../../shared/helpers/DismissHelper'; import MainHelper from '../../shared/helpers/MainHelper'; import Log from '../../shared/libraries/Log'; import { NotificationPermission } from '../../shared/models/NotificationPermission'; import OneSignalEvent from '../../shared/services/OneSignalEvent'; -import { bowserCastle } from '../../shared/utils/bowserCastle'; import BrowserUtils from '../../shared/utils/BrowserUtils'; import { addCssClass, @@ -537,11 +537,7 @@ export default class Bell { } patchSafariSvgFilterBug() { - if ( - !( - bowserCastle().name == 'safari' && Number(bowserCastle().version) >= 9.1 - ) - ) { + if (getBrowserName() !== Browser.Safari) { const bellShadow = `drop-shadow(0 2px 4px rgba(34,36,38,0.35));`; const badgeShadow = `drop-shadow(0 2px 4px rgba(34,36,38,0));`; const dialogShadow = `drop-shadow(0px 2px 2px rgba(34,36,38,.15));`; @@ -557,8 +553,7 @@ export default class Bell { 'style', `filter: ${dialogShadow}; -webkit-filter: ${dialogShadow};`, ); - } - if (bowserCastle().name == 'safari') { + } else { this.badge.element.setAttribute('style', `display: none;`); } } diff --git a/src/page/bell/Dialog.ts b/src/page/bell/Dialog.ts index 54bb4d143..eb815c3e9 100755 --- a/src/page/bell/Dialog.ts +++ b/src/page/bell/Dialog.ts @@ -1,5 +1,10 @@ +import { + Browser, + getBrowserName, + isMobileBrowser, + isTabletBrowser, +} from 'src/shared/useragent'; import OneSignalEvent from '../../shared/services/OneSignalEvent'; -import { bowserCastle } from '../../shared/utils/bowserCastle'; import { addDomElement, clearDomElementChildren, @@ -96,14 +101,16 @@ export default class Dialog extends AnimatedElement { contents = `

${this.bell.options.text['dialog.main.title']}

${notificationIconHtml}
${buttonHtml}
${footer}`; } else if (this.bell.state === Bell.STATES.BLOCKED) { let imageUrl = null; - if (bowserCastle().name === 'chrome') { - if (!bowserCastle().mobile && !bowserCastle().tablet) + + const browserName = getBrowserName(); + if (browserName === Browser.Chrome) { + if (!isMobileBrowser() && !isTabletBrowser()) imageUrl = '/bell/chrome-unblock.jpg'; - } else if (bowserCastle().name === 'firefox') + } else if (browserName === Browser.Firefox) imageUrl = '/bell/firefox-unblock.jpg'; - else if (bowserCastle().name == 'safari') + else if (browserName === Browser.Safari) imageUrl = '/bell/safari-unblock.jpg'; - else if (bowserCastle().name === 'msedge') + else if (browserName === Browser.Edge) imageUrl = '/bell/edge-unblock.png'; let instructionsHtml = ''; @@ -113,8 +120,8 @@ export default class Dialog extends AnimatedElement { } if ( - (bowserCastle().mobile || bowserCastle().tablet) && - bowserCastle().name === 'chrome' + (isMobileBrowser() || isTabletBrowser()) && + browserName === Browser.Chrome ) { instructionsHtml = `
  1. Access Settings by tapping the three menu dots
  2. Click Site settings under Advanced.
  3. Click Notifications.
  4. Find and click this entry for this website.
  5. Click Notifications and set it to Allow.
`; } diff --git a/src/page/helpers/EnvironmentInfoHelper.ts b/src/page/helpers/EnvironmentInfoHelper.ts deleted file mode 100644 index 221ddeb06..000000000 --- a/src/page/helpers/EnvironmentInfoHelper.ts +++ /dev/null @@ -1,74 +0,0 @@ -import bowser from 'bowser'; -import Utils from '../../shared/context/Utils'; -import { Browser, type BrowserValue } from '../../shared/models/Browser'; -import { bowserCastle } from '../../shared/utils/bowserCastle'; -import type { EnvironmentInfo } from '../models/EnvironmentInfo'; - -/** - * EnvironmentInfoHelper is used to save page ("browser") context environment information to - * the OneSignal object upon initialization - */ - -export class EnvironmentInfoHelper { - public static getEnvironmentInfo(): EnvironmentInfo { - return { - browserType: this.getBrowser(), - browserVersion: this.getBrowserVersion(), - isBrowserAndSupportsServiceWorkers: this.supportsServiceWorkers(), - requiresUserInteraction: this.requiresUserInteraction(), - osVersion: this.getOsVersion(), - }; - } - - private static getBrowser(): BrowserValue { - if (bowserCastle().name === 'chrome') { - return Browser.Chrome; - } - if (bowserCastle().name === 'msedge') { - return Browser.Edge; - } - if (bowserCastle().name === 'opera') { - return Browser.Opera; - } - if (bowserCastle().name === 'firefox') { - return Browser.Firefox; - } - // use existing safari detection to be consistent - if (this.isMacOSSafari()) { - return Browser.Safari; - } - - return Browser.Other; - } - - // NOTE: Returns false in a ServiceWorker context - private static isMacOSSafari(): boolean { - return typeof window.safari !== 'undefined'; - } - - private static getBrowserVersion(): number { - return Utils.parseVersionString(bowserCastle().version); - } - - private static supportsServiceWorkers(): boolean { - return window.navigator && 'serviceWorker' in window.navigator; - } - - private static requiresUserInteraction(): boolean { - // Firefox 72+ requires user-interaction - if (this.getBrowser() === 'firefox' && this.getBrowserVersion() >= 72) { - return true; - } - - // Safari 12.1+ requires user-interaction - if (this.getBrowser() === 'safari' && this.getBrowserVersion() >= 12.1) { - return true; - } - - return false; - } - - private static getOsVersion(): string | number { - return bowser.osversion; - } -} diff --git a/src/page/managers/PromptsManager.ts b/src/page/managers/PromptsManager.ts index 5911a93eb..8d06c76f6 100644 --- a/src/page/managers/PromptsManager.ts +++ b/src/page/managers/PromptsManager.ts @@ -1,4 +1,3 @@ -import { Browser } from 'src/shared/models/Browser'; import { CONFIG_DEFAULTS_SLIDEDOWN_OPTIONS, DelayedPromptType, @@ -8,15 +7,20 @@ import { type DelayedPromptTypeValue, type SlidedownPromptOptions, } from 'src/shared/prompts'; +import { + Browser, + getBrowserName, + isMobileBrowser, + isTabletBrowser, + requiresUserInteraction, +} from 'src/shared/useragent'; import { DismissHelper } from '../../shared/helpers/DismissHelper'; import InitHelper from '../../shared/helpers/InitHelper'; import PromptsHelper from '../../shared/helpers/PromptsHelper'; import Log from '../../shared/libraries/Log'; import OneSignalEvent from '../../shared/services/OneSignalEvent'; import { awaitableTimeout } from '../../shared/utils/AwaitableTimeout'; -import { bowserCastle } from '../../shared/utils/bowserCastle'; import OneSignalUtils from '../../shared/utils/OneSignalUtils'; -import { EnvironmentInfoHelper } from '../helpers/EnvironmentInfoHelper'; import type { ContextInterface } from '../models/Context'; import { DismissPrompt } from '../models/Dismiss'; import { ResourceLoadState } from '../services/DynamicResourceLoader'; @@ -41,15 +45,10 @@ export class PromptsManager { } private shouldForceSlidedownOverNative(): boolean { - const { environmentInfo } = OneSignal; - const { browserType, browserVersion, requiresUserInteraction } = - environmentInfo!; - return ( - (browserType === Browser.Chrome && - Number(browserVersion) >= 63 && - (bowserCastle().tablet || bowserCastle().mobile)) || - requiresUserInteraction + (getBrowserName() === Browser.Chrome && + (isTabletBrowser() || isMobileBrowser())) || + requiresUserInteraction() ); } @@ -141,9 +140,7 @@ export class PromptsManager { return; } - const { requiresUserInteraction } = - EnvironmentInfoHelper.getEnvironmentInfo(); - if (requiresUserInteraction && type === DelayedPromptType.Native) { + if (requiresUserInteraction() && type === DelayedPromptType.Native) { type = DelayedPromptType.Push; // Push Slidedown for cases where user interaction is needed } diff --git a/src/page/models/Context.ts b/src/page/models/Context.ts index ad94bacc0..1291c50ab 100644 --- a/src/page/models/Context.ts +++ b/src/page/models/Context.ts @@ -15,18 +15,15 @@ import type { ISlidedownManager } from '../managers/slidedownManager/types'; import TagManager from '../managers/tagManager/TagManager'; import type { ITagManager } from '../managers/tagManager/types'; import { DynamicResourceLoader } from '../services/DynamicResourceLoader'; -import type { EnvironmentInfo } from './EnvironmentInfo'; export interface ContextInterface extends ContextSWInterface { dynamicResourceLoader: DynamicResourceLoader; - environmentInfo?: EnvironmentInfo; tagManager: ITagManager; slidedownManager: ISlidedownManager; } export default class Context implements ContextInterface { public appConfig: AppConfig; - public environmentInfo?: EnvironmentInfo; public dynamicResourceLoader: DynamicResourceLoader; public subscriptionManager: SubscriptionManager; public serviceWorkerManager: ServiceWorkerManager; @@ -41,9 +38,6 @@ export default class Context implements ContextInterface { constructor(appConfig: AppConfig) { this.appConfig = appConfig; - if (typeof OneSignal !== 'undefined' && !!OneSignal.environmentInfo) { - this.environmentInfo = OneSignal.environmentInfo; - } this.subscriptionManager = ContextHelper.getSubscriptionManager(this); this.serviceWorkerManager = ContextHelper.getServiceWorkerManager(this); this.pageViewManager = new PageViewManager(); diff --git a/src/page/models/EnvironmentInfo.ts b/src/page/models/EnvironmentInfo.ts deleted file mode 100644 index 06a40e99e..000000000 --- a/src/page/models/EnvironmentInfo.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { BrowserValue } from 'src/shared/models/Browser'; - -// for runtime environment info -export interface EnvironmentInfo { - browserType: BrowserValue; - browserVersion: number; - isBrowserAndSupportsServiceWorkers: boolean; - requiresUserInteraction: boolean; - osVersion: string | number; -} diff --git a/src/page/slidedown/ConfirmationToast.ts b/src/page/slidedown/ConfirmationToast.ts index 1a525acc4..9b65e5468 100755 --- a/src/page/slidedown/ConfirmationToast.ts +++ b/src/page/slidedown/ConfirmationToast.ts @@ -1,17 +1,17 @@ +import { isMobileBrowser } from 'src/shared/useragent'; import OneSignalEvent from '../../shared/services/OneSignalEvent'; -import { - addCssClass, - once, - removeDomElement, - getDomElementOrStub, -} from '../../shared/utils/utils'; import { SLIDEDOWN_CSS_CLASSES, SLIDEDOWN_CSS_IDS, TOAST_CLASSES, TOAST_IDS, } from '../../shared/slidedown/constants'; -import { bowserCastle } from '../../shared/utils/bowserCastle'; +import { + addCssClass, + getDomElementOrStub, + once, + removeDomElement, +} from '../../shared/utils/utils'; export default class ConfirmationToast { private message: string; @@ -47,7 +47,7 @@ export default class ConfirmationToast { // Animate it in depending on environment addCssClass( this.container, - bowserCastle().mobile + isMobileBrowser() ? SLIDEDOWN_CSS_CLASSES.slideUp : SLIDEDOWN_CSS_CLASSES.slideDown, ); diff --git a/src/page/slidedown/Slidedown.ts b/src/page/slidedown/Slidedown.ts index c2a05cb6b..931f88ecb 100755 --- a/src/page/slidedown/Slidedown.ts +++ b/src/page/slidedown/Slidedown.ts @@ -4,6 +4,7 @@ import { SERVER_CONFIG_DEFAULTS_SLIDEDOWN, type SlidedownPromptOptions, } from 'src/shared/prompts'; +import { isMobileBrowser } from 'src/shared/useragent'; import MainHelper from '../../shared/helpers/MainHelper'; import PromptsHelper from '../../shared/helpers/PromptsHelper'; import OneSignalEvent from '../../shared/services/OneSignalEvent'; @@ -12,7 +13,6 @@ import { SLIDEDOWN_CSS_CLASSES, SLIDEDOWN_CSS_IDS, } from '../../shared/slidedown/constants'; -import { bowserCastle } from '../../shared/utils/bowserCastle'; import { addCssClass, addDomElement, @@ -136,7 +136,7 @@ export default class Slidedown { // Animate it in depending on environment addCssClass( this.container, - bowserCastle().mobile + isMobileBrowser() ? SLIDEDOWN_CSS_CLASSES.slideUp : SLIDEDOWN_CSS_CLASSES.slideDown, ); diff --git a/src/page/userModel/FuturePushSubscriptionRecord.ts b/src/page/userModel/FuturePushSubscriptionRecord.ts index 0d33a71d6..7deb4f717 100644 --- a/src/page/userModel/FuturePushSubscriptionRecord.ts +++ b/src/page/userModel/FuturePushSubscriptionRecord.ts @@ -9,7 +9,16 @@ import { getSubscriptionType, } from 'src/shared/environment'; import { RawPushSubscription } from 'src/shared/models/RawPushSubscription'; +import { + Browser, + getBrowserName, + getBrowserVersion, +} from 'src/shared/useragent'; import { VERSION } from 'src/shared/utils/EnvVariables'; +import { + DeliveryPlatformKind, + type DeliveryPlatformKindValue, +} from '../../shared/models/DeliveryPlatformKind'; import type { Serializable } from '../models/Serializable'; export default class FuturePushSubscriptionRecord implements Serializable { @@ -55,4 +64,53 @@ export default class FuturePushSubscriptionRecord implements Serializable { web_p256: this.webp256, }; } + + /* S T A T I C */ + + /** + * Get the User Model Subscription type based on browser detection. + */ + public static getSubscriptionType(): SubscriptionTypeValue { + const browserName = getBrowserName(); + if (browserName === Browser.Firefox) { + return SubscriptionType.FirefoxPush; + } + if (useSafariVapidPush()) { + return SubscriptionType.SafariPush; + } + if (useSafariLegacyPush) { + return SubscriptionType.SafariLegacyPush; + } + // Other browsers, like Edge, are Chromium based so we consider them "Chrome". + return SubscriptionType.ChromePush; + } + + /** + * Get the legacy player.device_type + * NOTE: Use getSubscriptionType() instead when possible. + */ + public static getDeviceType(): DeliveryPlatformKindValue { + switch (this.getSubscriptionType()) { + case SubscriptionType.FirefoxPush: + return DeliveryPlatformKind.Firefox; + case SubscriptionType.SafariLegacyPush: + return DeliveryPlatformKind.SafariLegacy; + case SubscriptionType.SafariPush: + return DeliveryPlatformKind.SafariVapid; + } + return DeliveryPlatformKind.ChromeLike; + } + + public static getDeviceOS(): string | number { + const browserVersion = getBrowserVersion(); + return isNaN(browserVersion) ? -1 : browserVersion; + } + + public static getDeviceModel(): string { + return navigator.platform; + } + + public static getSdk(): string { + return String(VERSION); + } } diff --git a/src/shared/environment/environment.ts b/src/shared/environment/environment.ts index f9af40a8b..f0d9692ae 100644 --- a/src/shared/environment/environment.ts +++ b/src/shared/environment/environment.ts @@ -1,13 +1,4 @@ -import { - SubscriptionType, - type SubscriptionTypeValue, -} from 'src/core/types/subscription'; -import { EnvironmentInfoHelper } from 'src/page/helpers/EnvironmentInfoHelper'; -import { - DeliveryPlatformKind, - type DeliveryPlatformKindValue, -} from '../models/DeliveryPlatformKind'; -import { bowserCastle } from '../utils/bowserCastle'; +import { Browser, getBrowserName } from '../useragent'; import { API_ORIGIN, API_TYPE, IS_SERVICE_WORKER } from '../utils/EnvVariables'; import OneSignalUtils from '../utils/OneSignalUtils'; import { EnvironmentKind } from './constants'; @@ -30,7 +21,9 @@ export const supportsVapidPush = PushSubscriptionOptions.prototype.hasOwnProperty('applicationServerKey'); export const useSafariVapidPush = () => - bowserCastle().name == 'safari' && supportsVapidPush && !useSafariLegacyPush; + getBrowserName() === Browser.Safari && + supportsVapidPush && + !useSafariLegacyPush; // for determing the api url const API_URL_PORT = 3000; diff --git a/src/shared/managers/SubscriptionManager.ts b/src/shared/managers/SubscriptionManager.ts index 7e2f7df41..4bd74ac7a 100644 --- a/src/shared/managers/SubscriptionManager.ts +++ b/src/shared/managers/SubscriptionManager.ts @@ -41,7 +41,7 @@ import { } from '../models/UnsubscriptionStrategy'; import Database from '../services/Database'; import OneSignalEvent from '../services/OneSignalEvent'; -import { bowserCastle } from '../utils/bowserCastle'; +import { Browser, getBrowserName } from '../useragent'; import { base64ToUint8Array } from '../utils/Encoding'; import { IS_SERVICE_WORKER } from '../utils/EnvVariables'; import { PermissionUtils } from '../utils/PermissionUtils'; @@ -534,7 +534,7 @@ export class SubscriptionManager { const swRegistration = self.registration; - if (!swRegistration.active && bowserCastle().name !== 'firefox') { + if (!swRegistration.active && getBrowserName() !== Browser.Firefox) { throw new InvalidStateError(InvalidStateReason.ServiceWorkerNotActivated); /* Or should we wait for the service worker to be ready? @@ -574,7 +574,7 @@ export class SubscriptionManager { // Specifically return undefined instead of null if the key isn't available let key = undefined; - if (bowserCastle().name === 'firefox') { + if (getBrowserName() === Browser.Firefox) { /* Firefox uses VAPID for application identification instead of authentication, and so all apps share an identification key. diff --git a/src/shared/managers/sessionManager/SessionManager.ts b/src/shared/managers/sessionManager/SessionManager.ts index a0e1424f2..3eec8b024 100644 --- a/src/shared/managers/sessionManager/SessionManager.ts +++ b/src/shared/managers/sessionManager/SessionManager.ts @@ -1,5 +1,6 @@ import type { IUpdateUser } from 'src/core/types/api'; import { NotificationType } from 'src/core/types/subscription'; +import { supportsServiceWorkers } from 'src/shared/environment'; import AliasPair from '../../../core/requestService/AliasPair'; import { RequestService } from '../../../core/requestService/RequestService'; import { isCompleteSubscriptionObject } from '../../../core/utils/typePredicates'; @@ -42,7 +43,7 @@ export class SessionManager implements ISessionManager { isSafari: OneSignalUtils.isSafari(), outcomesConfig: this.context.appConfig.userConfig.outcomes!, }; - if (this.context.environmentInfo?.isBrowserAndSupportsServiceWorkers) { + if (supportsServiceWorkers()) { Log.debug('Notify SW to upsert session'); await this.context.workerMessenger.unicast( WorkerMessengerCommand.SessionUpsert, @@ -70,7 +71,7 @@ export class SessionManager implements ISessionManager { isSafari: OneSignalUtils.isSafari(), outcomesConfig: this.context.appConfig.userConfig.outcomes!, }; - if (this.context.environmentInfo?.isBrowserAndSupportsServiceWorkers) { + if (supportsServiceWorkers()) { Log.debug('Notify SW to deactivate session'); await this.context.workerMessenger.unicast( WorkerMessengerCommand.SessionDeactivate, @@ -279,7 +280,7 @@ export class SessionManager implements ISessionManager { ); } - if (this.context.environmentInfo?.isBrowserAndSupportsServiceWorkers) { + if (supportsServiceWorkers()) { this.setupSessionEventListeners(); } else { this.onSessionSent = sessionOrigin === SessionOrigin.UserCreate; @@ -289,7 +290,7 @@ export class SessionManager implements ISessionManager { setupSessionEventListeners(): void { // Only want these events if it's using subscription workaround - if (!this.context.environmentInfo?.isBrowserAndSupportsServiceWorkers) { + if (!supportsServiceWorkers()) { Log.debug( 'Not setting session event listeners. No service worker possible.', ); diff --git a/src/shared/models/Browser.ts b/src/shared/useragent/constants.ts similarity index 60% rename from src/shared/models/Browser.ts rename to src/shared/useragent/constants.ts index 01da905fa..74e62877b 100644 --- a/src/shared/models/Browser.ts +++ b/src/shared/useragent/constants.ts @@ -1,10 +1,7 @@ export const Browser = { - Safari: 'safari', - Firefox: 'firefox', Chrome: 'chrome', - Opera: 'opera', Edge: 'edge', + Safari: 'safari', + Firefox: 'firefox', Other: 'other', } as const; - -export type BrowserValue = (typeof Browser)[keyof typeof Browser]; diff --git a/src/shared/useragent/index.ts b/src/shared/useragent/index.ts new file mode 100644 index 000000000..93d404590 --- /dev/null +++ b/src/shared/useragent/index.ts @@ -0,0 +1,3 @@ +export * from './constants'; +export * from './types'; +export * from './useragent'; diff --git a/src/shared/useragent/types.ts b/src/shared/useragent/types.ts new file mode 100644 index 000000000..168120dbf --- /dev/null +++ b/src/shared/useragent/types.ts @@ -0,0 +1,3 @@ +import type { Browser } from './constants'; + +export type BrowserValue = (typeof Browser)[keyof typeof Browser]; diff --git a/src/shared/useragent/useragent.test.ts b/src/shared/useragent/useragent.test.ts new file mode 100644 index 000000000..a64e1857e --- /dev/null +++ b/src/shared/useragent/useragent.test.ts @@ -0,0 +1,169 @@ +import BrowserUserAgent from '__test__/support/models/BrowserUserAgent'; +import { isMobileBrowser } from './useragent'; + +describe('isMobileBrowser', () => { + let originalUserAgent: string; + + beforeEach(() => { + // Store original userAgent + originalUserAgent = navigator.userAgent; + }); + + afterEach(() => { + // Restore original userAgent + Object.defineProperty(navigator, 'userAgent', { + value: originalUserAgent, + writable: true, + }); + }); + + const mockUserAgent = (userAgent: string) => { + Object.defineProperty(navigator, 'userAgent', { + value: userAgent, + writable: true, + }); + }; + + describe('isMobileBrowser()', () => { + [ + { + userAgent: BrowserUserAgent.iPhone, + expected: true, + }, + { + userAgent: BrowserUserAgent.iPad, + expected: true, + }, + { + userAgent: BrowserUserAgent.iPod, + expected: true, + }, + { + userAgent: BrowserUserAgent.ChromeAndroidSupported, + expected: true, + }, + { + userAgent: BrowserUserAgent.FirefoxMobileSupported, + expected: true, + }, + { + userAgent: BrowserUserAgent.OperaAndroidSupported, + expected: true, + }, + { + userAgent: BrowserUserAgent.OperaMiniUnsupported, + expected: true, + }, + { + userAgent: BrowserUserAgent.SamsungBrowserSupported, + expected: true, + }, + { + userAgent: BrowserUserAgent.UcBrowserSupported, + expected: true, + }, + { + userAgent: BrowserUserAgent.FacebookBrowseriOS, + expected: true, + }, + { + userAgent: BrowserUserAgent.FacebookBrowserAndroid, + expected: true, + }, + { + userAgent: BrowserUserAgent.YandexMobileSupported, + expected: true, + }, + ].forEach(({ userAgent, expected }) => { + test(`should detect ${userAgent} as a mobile browser`, () => { + mockUserAgent(userAgent); + expect(isMobileBrowser()).toBe(expected); + }); + }); + + [ + { + userAgent: BrowserUserAgent.Default, + expected: false, + }, + { + userAgent: BrowserUserAgent.ChromeWindowsSupported, + expected: false, + }, + { + userAgent: BrowserUserAgent.ChromeMacSupported, + expected: false, + }, + { + userAgent: BrowserUserAgent.ChromeLinuxSupported, + expected: false, + }, + { + userAgent: BrowserUserAgent.SafariSupportedMac, + expected: false, + }, + { + userAgent: BrowserUserAgent.FirefoxWindowsSupported, + expected: false, + }, + { + userAgent: BrowserUserAgent.FirefoxMacSupported, + expected: false, + }, + { + userAgent: BrowserUserAgent.FirefoxLinuxSupported, + expected: false, + }, + { + userAgent: BrowserUserAgent.EdgeSupported, + expected: false, + }, + { + userAgent: BrowserUserAgent.OperaDesktopSupported, + expected: false, + }, + { + userAgent: BrowserUserAgent.YandexDesktopSupportedHigh, + expected: false, + }, + { + userAgent: BrowserUserAgent.VivaldiWindowsSupported, + expected: false, + }, + ].forEach(({ userAgent, expected }) => { + test(`should detect ${userAgent} as a desktop browser`, () => { + mockUserAgent(userAgent); + expect(isMobileBrowser()).toBe(expected); + }); + }); + + [ + { + userAgent: BrowserUserAgent.iPad, + expected: true, + }, + { + userAgent: BrowserUserAgent.ChromeTabletSupported, + expected: true, + }, + { + userAgent: BrowserUserAgent.FirefoxTabletSupported, + expected: true, + }, + { + userAgent: BrowserUserAgent.OperaTabletSupported, + expected: true, + }, + ].forEach(({ userAgent, expected }) => { + test(`should detect ${userAgent} as tablet`, () => { + mockUserAgent(userAgent); + expect(isMobileBrowser()).toBe(expected); + }); + }); + + test('should handle empty user agent', () => { + mockUserAgent(''); + expect(isMobileBrowser()).toBe(false); + }); + }); +}); diff --git a/src/shared/useragent/useragent.ts b/src/shared/useragent/useragent.ts new file mode 100644 index 000000000..ef978926c --- /dev/null +++ b/src/shared/useragent/useragent.ts @@ -0,0 +1,65 @@ +import { Browser } from './constants'; +import type { BrowserValue } from './types'; + +export function getBrowserName(): BrowserValue { + const ua = navigator.userAgent.toLowerCase(); + + if (ua.includes('edg/')) return Browser.Edge; + if (ua.includes('chrome/') && !ua.includes('edg/') && !ua.includes('opr/')) + return Browser.Chrome; + if (ua.includes('safari/') && !ua.includes('chrome/')) return Browser.Safari; + if (ua.includes('firefox/')) return Browser.Firefox; + return Browser.Other; +} + +export function getBrowserVersion(): number { + const ua = navigator.userAgent; + let version = NaN; + const browsers = [ + { name: 'Edge', regex: /edg\/([\d\.]+)/i }, + { name: 'Opera', regex: /(?:opr|opera)\/([\d\.]+)/i }, + { name: 'Chrome', regex: /chrome\/([\d\.]+)/i }, + { name: 'Safari', regex: /version\/([\d\.]+).*safari/i }, + { name: 'Firefox', regex: /firefox\/([\d\.]+)/i }, + ]; + + for (const browser of browsers) { + const match = ua.match(browser.regex); + if (match) { + const [major, minor = '0'] = match[1].split('.'); + version = +`${major}.${minor}`; + } + } + + return version; +} + +export function isMobileBrowser(): boolean { + const ua = navigator.userAgent; + return /android|iphone|ipad|ipod|opera mini|iemobile|mobile/i.test(ua); +} + +export function isTabletBrowser(): boolean { + const ua = navigator.userAgent.toLowerCase(); + + const isIPad = /\bipad\b/.test(ua); + const isAndroidTablet = /android/.test(ua) && !/mobile/.test(ua); // Android tablets don't include "mobile" + const isWindowsTablet = + /windows/.test(ua) && /touch/.test(ua) && !/phone/.test(ua); + const isKindleOrFire = /kindle|silk|kf[a-z]{2,}/.test(ua); // Amazon devices + + return isIPad || isAndroidTablet || isWindowsTablet || isKindleOrFire; +} + +export function requiresUserInteraction(): boolean { + const browserName = getBrowserName(); + const version = getBrowserVersion(); + + // Firefox 72+ requires user-interaction + if (browserName === Browser.Firefox && version >= 72) return true; + + // Safari 12.1+ requires user-interaction + if (browserName === Browser.Safari && version >= 12.1) return true; + + return false; +} diff --git a/src/shared/utils/OneSignalUtils.ts b/src/shared/utils/OneSignalUtils.ts index 30b09cec9..1eedad62b 100644 --- a/src/shared/utils/OneSignalUtils.ts +++ b/src/shared/utils/OneSignalUtils.ts @@ -1,22 +1,12 @@ -import bowser, { type IBowser } from 'bowser'; import { Utils } from '../context/Utils'; import { isBrowser } from '../environment/environment'; import Log from '../libraries/Log'; -import { bowserCastle } from './bowserCastle'; export class OneSignalUtils { public static getBaseUrl() { return location.origin; } - public static redetectBrowserUserAgent(): IBowser { - // During testing, the browser object may be initialized before the userAgent is injected - if (bowserCastle().name === '' && bowserCastle().version === '') { - return bowser._detect(navigator.userAgent); - } - return bowser; - } - /** * Returns true if the UUID is a string of 36 characters; * @param uuid diff --git a/src/shared/utils/bowserCastle.ts b/src/shared/utils/bowserCastle.ts deleted file mode 100644 index c1193bba3..000000000 --- a/src/shared/utils/bowserCastle.ts +++ /dev/null @@ -1,10 +0,0 @@ -import * as bowser from 'bowser'; - -export function bowserCastle() { - return { - mobile: bowser.mobile, - tablet: bowser.tablet, - name: bowser.name.toLowerCase(), - version: bowser.version, - }; -} diff --git a/src/shared/utils/utils.ts b/src/shared/utils/utils.ts index aff90ca90..9c2d8f092 100755 --- a/src/shared/utils/utils.ts +++ b/src/shared/utils/utils.ts @@ -1,7 +1,7 @@ import type { NotificationIcons } from 'src/page/models/NotificationIcons'; import { Utils } from '../context/Utils'; import Log from '../libraries/Log'; -import { bowserCastle } from './bowserCastle'; +import { Browser, getBrowserName } from '../useragent'; import { IS_SERVICE_WORKER } from './EnvVariables'; import { OneSignalUtils } from './OneSignalUtils'; import { PermissionUtils } from './PermissionUtils'; @@ -294,9 +294,10 @@ export function getPlatformNotificationIcon( ): string { if (!notificationIcons) return 'default-icon'; - if (bowserCastle().name == 'safari' && notificationIcons.safari) + const browserName = getBrowserName(); + if (browserName === Browser.Safari && notificationIcons.safari) return notificationIcons.safari; - else if (bowserCastle().name === 'firefox' && notificationIcons.firefox) + else if (browserName === Browser.Firefox && notificationIcons.firefox) return notificationIcons.firefox; return ( diff --git a/src/sw/serviceWorker/ServiceWorker.ts b/src/sw/serviceWorker/ServiceWorker.ts index b86113b9a..f1ed4a92f 100755 --- a/src/sw/serviceWorker/ServiceWorker.ts +++ b/src/sw/serviceWorker/ServiceWorker.ts @@ -44,8 +44,8 @@ import { type AppConfig, getServerAppConfig } from 'src/shared/config'; import { getDeviceType } from 'src/shared/environment'; import ContextSW from 'src/shared/models/ContextSW'; import type { DeliveryPlatformKindValue } from 'src/shared/models/DeliveryPlatformKind'; +import { Browser, getBrowserName } from 'src/shared/useragent'; import { VERSION } from 'src/shared/utils/EnvVariables'; -import { bowserCastle } from '../../shared/utils/bowserCastle'; import { ModelCacheDirectAccess } from '../helpers/ModelCacheDirectAccess'; import { OSNotificationButtonsConverter } from '../models/OSNotificationButtonsConverter'; import { OSWebhookNotificationEventSender } from '../webhooks/notifications/OSWebhookNotificationEventSender'; @@ -378,7 +378,7 @@ export class ServiceWorker { * to be safe we are disabling it for all Safari browsers. */ static browserSupportsConfirmedDelivery(): boolean { - return bowserCastle().name !== 'safari'; + return getBrowserName() !== Browser.Safari; } /** From dc9559656974061c7af6dbf9d0bdfef417b26388 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Mon, 28 Jul 2025 17:34:56 -0700 Subject: [PATCH 02/12] fix tests --- __test__/setupTests.ts | 6 +++ __test__/unit/core/coreModuleDirector.test.ts | 2 +- package.json | 4 +- src/entries/pageSdkInit.test.ts | 10 ---- src/sw/serviceWorker/ServiceWorker.test.ts | 14 ----- src/sw/serviceWorker/ServiceWorker.ts | 52 +++++++++---------- 6 files changed, 33 insertions(+), 55 deletions(-) diff --git a/__test__/setupTests.ts b/__test__/setupTests.ts index 6a73c9026..692b34056 100644 --- a/__test__/setupTests.ts +++ b/__test__/setupTests.ts @@ -24,3 +24,9 @@ vi.mock('src/core/operationRepo/constants', () => ({ OP_REPO_EXECUTION_INTERVAL: 5, OP_REPO_POST_CREATE_RETRY_UP_TO: 10, })); + +Object.defineProperty(navigator, 'userAgent', { + value: + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.0.0 Safari/537.36', + writable: true, +}); diff --git a/__test__/unit/core/coreModuleDirector.test.ts b/__test__/unit/core/coreModuleDirector.test.ts index a764037ba..0c73bff27 100644 --- a/__test__/unit/core/coreModuleDirector.test.ts +++ b/__test__/unit/core/coreModuleDirector.test.ts @@ -38,7 +38,7 @@ describe('CoreModuleDirector tests', () => { 'getPushSubscriptionModelByLastKnownToken', // @ts-expect-error - private method ).mockResolvedValue(pushModelLastKnown); - expect(await getPushSubscriptionModel()).toBe(pushModelLastKnown); + expect(await getPushSubscriptionModel()).toEqual(pushModelLastKnown); }); test('returns current subscription over last known', async () => { diff --git a/package.json b/package.json index b71e849c1..8d3698f21 100644 --- a/package.json +++ b/package.json @@ -80,12 +80,12 @@ }, { "path": "./build/releases/OneSignalSDK.page.es6.js", - "limit": "64 kB", + "limit": "61.2 kB", "gzip": true }, { "path": "./build/releases/OneSignalSDK.sw.js", - "limit": "34 kB", + "limit": "31.3 kB", "gzip": true }, { diff --git a/src/entries/pageSdkInit.test.ts b/src/entries/pageSdkInit.test.ts index 3cf6b6403..093ff1eab 100644 --- a/src/entries/pageSdkInit.test.ts +++ b/src/entries/pageSdkInit.test.ts @@ -5,16 +5,6 @@ import { server } from '__test__/support/mocks/server'; import { http, HttpResponse } from 'msw'; import Log from 'src/shared/libraries/Log'; -// need to mock browsercastle since we resetting modules after each test -vi.mock('src/shared/utils/bowserCastle', () => ({ - bowserCastle: () => ({ - mobile: false, - tablet: false, - name: 'Chrome', - version: '100', - }), -})); - // need to wait for full OperationRepo rework describe('pageSdkInit', () => { beforeEach(async () => { diff --git a/src/sw/serviceWorker/ServiceWorker.test.ts b/src/sw/serviceWorker/ServiceWorker.test.ts index afba1310c..2816a8586 100644 --- a/src/sw/serviceWorker/ServiceWorker.test.ts +++ b/src/sw/serviceWorker/ServiceWorker.test.ts @@ -198,11 +198,6 @@ describe('ServiceWorker', () => { }); test('should confirm delivery', async () => { - mockBowser.mockReturnValue({ - name: 'Chrome', - version: '130', - }); - const payload = mockOSMinifiedNotificationPayload({ custom: { rr: 'y', @@ -731,15 +726,6 @@ vi.mock('../helpers/ModelCacheDirectAccess', () => ({ }, })); -// -- browser info mock -const mockBowser = vi.fn().mockReturnValue({ - name: 'Safari', - version: '18', -}); -vi.mock('../../../src/shared/utils/bowserCastle', () => ({ - bowserCastle: () => mockBowser(), -})); - // -- awaitable timeout mock vi.mock('../../../src/shared/utils/AwaitableTimeout', () => ({ awaitableTimeout: vi.fn().mockResolvedValue(undefined), diff --git a/src/sw/serviceWorker/ServiceWorker.ts b/src/sw/serviceWorker/ServiceWorker.ts index f1ed4a92f..2fb6eb9a4 100755 --- a/src/sw/serviceWorker/ServiceWorker.ts +++ b/src/sw/serviceWorker/ServiceWorker.ts @@ -1,54 +1,50 @@ -import OneSignalApiBase from '../../../src/shared/api/OneSignalApiBase'; -import OneSignalApiSW from '../../../src/shared/api/OneSignalApiSW'; +import { + NotificationType, + type NotificationTypeValue, +} from 'src/core/types/subscription'; +import FuturePushSubscriptionRecord from 'src/page/userModel/FuturePushSubscriptionRecord'; +import OneSignalApiBase from 'src/shared/api/OneSignalApiBase'; +import OneSignalApiSW from 'src/shared/api/OneSignalApiSW'; +import { type AppConfig, getServerAppConfig } from 'src/shared/config'; +import { Utils } from 'src/shared/context/Utils'; +import ServiceWorkerHelper from 'src/shared/helpers/ServiceWorkerHelper'; import { WorkerMessenger, WorkerMessengerCommand, type WorkerMessengerMessage, -} from '../../../src/shared/libraries/WorkerMessenger'; -import { RawPushSubscription } from '../../../src/shared/models/RawPushSubscription'; -import { Utils } from '../../shared/context/Utils'; -import ServiceWorkerHelper from '../../shared/helpers/ServiceWorkerHelper'; +} from 'src/shared/libraries/WorkerMessenger'; +import ContextSW from 'src/shared/models/ContextSW'; +import type { DeliveryPlatformKindValue } from 'src/shared/models/DeliveryPlatformKind'; import { type NotificationClickEventInternal, type NotificationForegroundWillDisplayEventSerializable, -} from '../../shared/models/NotificationEvent'; +} from 'src/shared/models/NotificationEvent'; import { type IMutableOSNotification, type IOSNotification, -} from '../../shared/models/OSNotification'; +} from 'src/shared/models/OSNotification'; +import { RawPushSubscription } from 'src/shared/models/RawPushSubscription'; import { type PageVisibilityRequest, type PageVisibilityResponse, SessionStatus, type UpsertOrDeactivateSessionPayload, -} from '../../shared/models/Session'; -import { SubscriptionStrategyKind } from '../../shared/models/SubscriptionStrategyKind'; -import Database from '../../shared/services/Database'; -import { awaitableTimeout } from '../../shared/utils/AwaitableTimeout'; +} from 'src/shared/models/Session'; +import { SubscriptionStrategyKind } from 'src/shared/models/SubscriptionStrategyKind'; +import Database from 'src/shared/services/Database'; +import { Browser, getBrowserName } from 'src/shared/useragent'; +import { awaitableTimeout } from 'src/shared/utils/AwaitableTimeout'; +import { VERSION } from 'src/shared/utils/EnvVariables'; import { cancelableTimeout } from '../helpers/CancelableTimeout'; +import { ModelCacheDirectAccess } from '../helpers/ModelCacheDirectAccess'; import Log from '../libraries/Log'; import { type OSMinifiedNotificationPayload, OSMinifiedNotificationPayloadHelper, } from '../models/OSMinifiedNotificationPayload'; -import { - type OSServiceWorkerFields, - type SubscriptionChangeEvent, -} from './types'; - -import { - NotificationType, - type NotificationTypeValue, -} from 'src/core/types/subscription'; -import { type AppConfig, getServerAppConfig } from 'src/shared/config'; -import { getDeviceType } from 'src/shared/environment'; -import ContextSW from 'src/shared/models/ContextSW'; -import type { DeliveryPlatformKindValue } from 'src/shared/models/DeliveryPlatformKind'; -import { Browser, getBrowserName } from 'src/shared/useragent'; -import { VERSION } from 'src/shared/utils/EnvVariables'; -import { ModelCacheDirectAccess } from '../helpers/ModelCacheDirectAccess'; import { OSNotificationButtonsConverter } from '../models/OSNotificationButtonsConverter'; import { OSWebhookNotificationEventSender } from '../webhooks/notifications/OSWebhookNotificationEventSender'; +import type { OSServiceWorkerFields, SubscriptionChangeEvent } from './types'; declare const self: ServiceWorkerGlobalScope & OSServiceWorkerFields; From 61930168b72de5bffdf38d82a994cb801448cfd3 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Mon, 28 Jul 2025 17:55:38 -0700 Subject: [PATCH 03/12] move session module to src/shared/session --- src/shared/helpers/ServiceWorkerHelper.ts | 6 +-- src/shared/managers/ServiceWorkerManager.ts | 5 +-- src/shared/managers/SubscriptionManager.ts | 2 +- src/shared/managers/UpdateManager.ts | 2 +- .../sessionManager/SessionManager.test.ts | 7 ++-- .../managers/sessionManager/SessionManager.ts | 10 ++--- src/shared/managers/sessionManager/types.ts | 2 +- src/shared/services/Database.ts | 2 +- src/shared/session/constants.ts | 15 ++++++++ src/shared/session/index.ts | 3 ++ src/shared/session/session.ts | 22 +++++++++++ .../{models/Session.ts => session/types.ts} | 37 +------------------ .../managers/sessionManager/SessionManager.ts | 2 +- src/sw/serviceWorker/ServiceWorker.test.ts | 14 +++---- src/sw/serviceWorker/ServiceWorker.ts | 18 ++++----- 15 files changed, 74 insertions(+), 73 deletions(-) create mode 100644 src/shared/session/constants.ts create mode 100644 src/shared/session/index.ts create mode 100644 src/shared/session/session.ts rename src/shared/{models/Session.ts => session/types.ts} (54%) diff --git a/src/shared/helpers/ServiceWorkerHelper.ts b/src/shared/helpers/ServiceWorkerHelper.ts index 4b5cef614..0385e18f5 100755 --- a/src/shared/helpers/ServiceWorkerHelper.ts +++ b/src/shared/helpers/ServiceWorkerHelper.ts @@ -8,15 +8,15 @@ import OneSignalApiSW from '../api/OneSignalApiSW'; import Utils from '../context/Utils'; import type { OutcomesNotificationClicked } from '../models/OutcomesNotificationEvents'; import Path from '../models/Path'; +import type { OutcomesConfig } from '../outcomes/types'; +import Database from '../services/Database'; import { initializeNewSession, type Session, SessionOrigin, type SessionOriginValue, SessionStatus, -} from '../models/Session'; -import type { OutcomesConfig } from '../outcomes/types'; -import Database from '../services/Database'; +} from '../session'; import { OneSignalUtils } from '../utils/OneSignalUtils'; import OutcomesHelper from './OutcomesHelper'; diff --git a/src/shared/managers/ServiceWorkerManager.ts b/src/shared/managers/ServiceWorkerManager.ts index 23c6287a0..9cd2abb93 100644 --- a/src/shared/managers/ServiceWorkerManager.ts +++ b/src/shared/managers/ServiceWorkerManager.ts @@ -17,12 +17,9 @@ import { type NotificationForegroundWillDisplayEventSerializable, } from '../models/NotificationEvent'; import Path from '../models/Path'; -import { - type PageVisibilityRequest, - type PageVisibilityResponse, -} from '../models/Session'; import Database from '../services/Database'; import OneSignalEvent from '../services/OneSignalEvent'; +import type { PageVisibilityRequest, PageVisibilityResponse } from '../session'; import { VERSION } from '../utils/EnvVariables'; import OneSignalUtils from '../utils/OneSignalUtils'; diff --git a/src/shared/managers/SubscriptionManager.ts b/src/shared/managers/SubscriptionManager.ts index 4bd74ac7a..d094913b4 100644 --- a/src/shared/managers/SubscriptionManager.ts +++ b/src/shared/managers/SubscriptionManager.ts @@ -29,7 +29,6 @@ import type { ContextSWInterface } from '../models/ContextSW'; import { NotificationPermission } from '../models/NotificationPermission'; import type { PushSubscriptionState } from '../models/PushSubscriptionState'; import { RawPushSubscription } from '../models/RawPushSubscription'; -import { SessionOrigin } from '../models/Session'; import { Subscription } from '../models/Subscription'; import { SubscriptionStrategyKind, @@ -41,6 +40,7 @@ import { } from '../models/UnsubscriptionStrategy'; import Database from '../services/Database'; import OneSignalEvent from '../services/OneSignalEvent'; +import { SessionOrigin } from '../session'; import { Browser, getBrowserName } from '../useragent'; import { base64ToUint8Array } from '../utils/Encoding'; import { IS_SERVICE_WORKER } from '../utils/EnvVariables'; diff --git a/src/shared/managers/UpdateManager.ts b/src/shared/managers/UpdateManager.ts index de6a4ff78..fa0510881 100644 --- a/src/shared/managers/UpdateManager.ts +++ b/src/shared/managers/UpdateManager.ts @@ -6,7 +6,7 @@ import OneSignalApiShared from '../api/OneSignalApiShared'; import { getSubscriptionType } from '../environment'; import Log from '../libraries/Log'; import type { ContextSWInterface } from '../models/ContextSW'; -import { SessionOrigin } from '../models/Session'; +import { SessionOrigin } from '../session'; import { logMethodCall } from '../utils/utils'; export class UpdateManager { diff --git a/src/shared/managers/sessionManager/SessionManager.test.ts b/src/shared/managers/sessionManager/SessionManager.test.ts index a0ca804c6..f1ead2392 100644 --- a/src/shared/managers/sessionManager/SessionManager.test.ts +++ b/src/shared/managers/sessionManager/SessionManager.test.ts @@ -1,11 +1,10 @@ -import { TestEnvironment } from '__test__/support/environment/TestEnvironment'; -import { SessionManager } from './SessionManager'; - import { DUMMY_EXTERNAL_ID } from '__test__/support/constants'; +import { TestEnvironment } from '__test__/support/environment/TestEnvironment'; import { setAddAliasResponse } from '__test__/support/helpers/requests'; import LoginManager from 'src/page/managers/LoginManager'; import Log from 'src/shared/libraries/Log'; -import { SessionOrigin } from 'src/shared/models/Session'; +import { SessionOrigin } from 'src/shared/session'; +import { SessionManager } from './SessionManager'; vi.spyOn(Log, 'error').mockImplementation(() => ''); diff --git a/src/shared/managers/sessionManager/SessionManager.ts b/src/shared/managers/sessionManager/SessionManager.ts index 3eec8b024..e3de17f0b 100644 --- a/src/shared/managers/sessionManager/SessionManager.ts +++ b/src/shared/managers/sessionManager/SessionManager.ts @@ -1,6 +1,11 @@ import type { IUpdateUser } from 'src/core/types/api'; import { NotificationType } from 'src/core/types/subscription'; import { supportsServiceWorkers } from 'src/shared/environment'; +import { + SessionOrigin, + type SessionOriginValue, + type UpsertOrDeactivateSessionPayload, +} from 'src/shared/session'; import AliasPair from '../../../core/requestService/AliasPair'; import { RequestService } from '../../../core/requestService/RequestService'; import { isCompleteSubscriptionObject } from '../../../core/utils/typePredicates'; @@ -12,11 +17,6 @@ import OneSignalError from '../../../shared/errors/OneSignalError'; import MainHelper from '../../helpers/MainHelper'; import Log from '../../libraries/Log'; import { WorkerMessengerCommand } from '../../libraries/WorkerMessenger'; -import { - SessionOrigin, - type SessionOriginValue, - type UpsertOrDeactivateSessionPayload, -} from '../../models/Session'; import { OneSignalUtils } from '../../utils/OneSignalUtils'; import type { ISessionManager } from './types'; diff --git a/src/shared/managers/sessionManager/types.ts b/src/shared/managers/sessionManager/types.ts index a23bbb588..0262ad256 100644 --- a/src/shared/managers/sessionManager/types.ts +++ b/src/shared/managers/sessionManager/types.ts @@ -1,4 +1,4 @@ -import type { SessionOriginValue } from '../../models/Session'; +import type { SessionOriginValue } from '../../session'; export interface ISessionManager { setupSessionEventListeners(): void; diff --git a/src/shared/services/Database.ts b/src/shared/services/Database.ts index f121bf5c1..3a7101e0c 100644 --- a/src/shared/services/Database.ts +++ b/src/shared/services/Database.ts @@ -26,9 +26,9 @@ import type { OutcomesNotificationClicked, OutcomesNotificationReceived, } from '../models/OutcomesNotificationEvents'; -import { ONESIGNAL_SESSION_KEY, type Session } from '../models/Session'; import { Subscription } from '../models/Subscription'; import { UserState } from '../models/UserState'; +import { ONESIGNAL_SESSION_KEY, type Session } from '../session'; const DatabaseEventName = { SET: 0, diff --git a/src/shared/session/constants.ts b/src/shared/session/constants.ts new file mode 100644 index 000000000..1172a29a0 --- /dev/null +++ b/src/shared/session/constants.ts @@ -0,0 +1,15 @@ +export const SessionStatus = { + Active: 'active', + Inactive: 'inactive', +} as const; + +export const SessionOrigin = { + UserCreate: 1, + UserNewSession: 2, + VisibilityVisible: 3, + VisibilityHidden: 4, + BeforeUnload: 5, + PageRefresh: 6, + Focus: 7, + Blur: 8, +} as const; diff --git a/src/shared/session/index.ts b/src/shared/session/index.ts new file mode 100644 index 000000000..be20e4cb4 --- /dev/null +++ b/src/shared/session/index.ts @@ -0,0 +1,3 @@ +export * from './constants'; +export * from './session'; +export * from './types'; diff --git a/src/shared/session/session.ts b/src/shared/session/session.ts new file mode 100644 index 000000000..519472514 --- /dev/null +++ b/src/shared/session/session.ts @@ -0,0 +1,22 @@ +import { SessionStatus } from './constants'; +import type { Session } from './types'; + +export const ONESIGNAL_SESSION_KEY = 'oneSignalSession'; + +type NewSessionOptions = Partial & { appId: string }; + +export function initializeNewSession(options: NewSessionOptions): Session { + const currentTimestamp = new Date().getTime(); + const notificationId = (options && options.notificationId) || null; + + return { + accumulatedDuration: 0, + appId: options.appId, + lastActivatedTimestamp: currentTimestamp, + lastDeactivatedTimestamp: null, + notificationId, + sessionKey: ONESIGNAL_SESSION_KEY, + startTimestamp: currentTimestamp, + status: SessionStatus.Active, + }; +} diff --git a/src/shared/models/Session.ts b/src/shared/session/types.ts similarity index 54% rename from src/shared/models/Session.ts rename to src/shared/session/types.ts index 276746cb0..dcb0ea07f 100644 --- a/src/shared/models/Session.ts +++ b/src/shared/session/types.ts @@ -1,24 +1,9 @@ import type { OutcomesConfig } from '../outcomes/types'; - -export const SessionStatus = { - Active: 'active', - Inactive: 'inactive', -} as const; +import type { SessionOrigin, SessionStatus } from './constants'; export type SessionStatusValue = (typeof SessionStatus)[keyof typeof SessionStatus]; -export const SessionOrigin = { - UserCreate: 1, - UserNewSession: 2, - VisibilityVisible: 3, - VisibilityHidden: 4, - BeforeUnload: 5, - PageRefresh: 6, - Focus: 7, - Blur: 8, -} as const; - export type SessionOriginValue = (typeof SessionOrigin)[keyof typeof SessionOrigin]; @@ -33,8 +18,6 @@ export interface Session { lastActivatedTimestamp: number; } -type NewSessionOptions = Partial & { appId: string }; - interface BaseSessionPayload { sessionThreshold: number; enableSessionDuration: boolean; @@ -56,21 +39,3 @@ export interface PageVisibilityRequest { export interface PageVisibilityResponse extends PageVisibilityRequest { focused: boolean; } - -export const ONESIGNAL_SESSION_KEY = 'oneSignalSession'; - -export function initializeNewSession(options: NewSessionOptions): Session { - const currentTimestamp = new Date().getTime(); - const notificationId = (options && options.notificationId) || null; - - return { - accumulatedDuration: 0, - appId: options.appId, - lastActivatedTimestamp: currentTimestamp, - lastDeactivatedTimestamp: null, - notificationId, - sessionKey: ONESIGNAL_SESSION_KEY, - startTimestamp: currentTimestamp, - status: SessionStatus.Active, - }; -} diff --git a/src/sw/managers/sessionManager/SessionManager.ts b/src/sw/managers/sessionManager/SessionManager.ts index 017f10855..7119f9500 100644 --- a/src/sw/managers/sessionManager/SessionManager.ts +++ b/src/sw/managers/sessionManager/SessionManager.ts @@ -1,4 +1,4 @@ -import type { SessionOriginValue } from 'src/shared/models/Session'; +import type { SessionOriginValue } from 'src/shared/session'; import type { ISessionManager } from '../../../shared/managers/sessionManager/types'; export class SessionManager implements ISessionManager { diff --git a/src/sw/serviceWorker/ServiceWorker.test.ts b/src/sw/serviceWorker/ServiceWorker.test.ts index 2816a8586..b866c74f4 100644 --- a/src/sw/serviceWorker/ServiceWorker.test.ts +++ b/src/sw/serviceWorker/ServiceWorker.test.ts @@ -19,13 +19,6 @@ import { } from 'src/shared/managers/SubscriptionManager'; import { DeliveryPlatformKind } from 'src/shared/models/DeliveryPlatformKind'; import { RawPushSubscription } from 'src/shared/models/RawPushSubscription'; -import { - ONESIGNAL_SESSION_KEY, - SessionOrigin, - SessionStatus, - type Session, - type UpsertOrDeactivateSessionPayload, -} from 'src/shared/models/Session'; import { SubscriptionStrategyKind } from 'src/shared/models/SubscriptionStrategyKind'; import Database, { TABLE_NOTIFICATION_OPENED, @@ -33,6 +26,13 @@ import Database, { TABLE_OUTCOMES_NOTIFICATION_RECEIVED, TABLE_SESSIONS, } from 'src/shared/services/Database'; +import { + ONESIGNAL_SESSION_KEY, + SessionOrigin, + SessionStatus, + type Session, + type UpsertOrDeactivateSessionPayload, +} from 'src/shared/session'; import Log from '../libraries/Log'; import { ServiceWorker } from './ServiceWorker'; diff --git a/src/sw/serviceWorker/ServiceWorker.ts b/src/sw/serviceWorker/ServiceWorker.ts index 2fb6eb9a4..f7984b8f0 100755 --- a/src/sw/serviceWorker/ServiceWorker.ts +++ b/src/sw/serviceWorker/ServiceWorker.ts @@ -15,23 +15,23 @@ import { } from 'src/shared/libraries/WorkerMessenger'; import ContextSW from 'src/shared/models/ContextSW'; import type { DeliveryPlatformKindValue } from 'src/shared/models/DeliveryPlatformKind'; -import { - type NotificationClickEventInternal, - type NotificationForegroundWillDisplayEventSerializable, +import type { + NotificationClickEventInternal, + NotificationForegroundWillDisplayEventSerializable, } from 'src/shared/models/NotificationEvent'; -import { - type IMutableOSNotification, - type IOSNotification, +import type { + IMutableOSNotification, + IOSNotification, } from 'src/shared/models/OSNotification'; import { RawPushSubscription } from 'src/shared/models/RawPushSubscription'; +import { SubscriptionStrategyKind } from 'src/shared/models/SubscriptionStrategyKind'; +import Database from 'src/shared/services/Database'; import { type PageVisibilityRequest, type PageVisibilityResponse, SessionStatus, type UpsertOrDeactivateSessionPayload, -} from 'src/shared/models/Session'; -import { SubscriptionStrategyKind } from 'src/shared/models/SubscriptionStrategyKind'; -import Database from 'src/shared/services/Database'; +} from 'src/shared/session'; import { Browser, getBrowserName } from 'src/shared/useragent'; import { awaitableTimeout } from 'src/shared/utils/AwaitableTimeout'; import { VERSION } from 'src/shared/utils/EnvVariables'; From ab6463a9d101d85b11696bd7ff35c2886c6134f1 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Mon, 28 Jul 2025 18:12:07 -0700 Subject: [PATCH 04/12] move from utils/utils to helpers --- __test__/support/helpers/executors.ts | 2 +- .../executors/LoginUserOperationExecutor.ts | 2 +- src/core/operationRepo/OperationRepo.ts | 2 +- src/onesignal/User.ts | 8 +- src/page/bell/ActiveAnimatedElement.ts | 8 +- src/page/bell/AnimatedElement.ts | 8 +- src/page/bell/Bell.ts | 16 +- src/page/bell/Button.ts | 8 +- src/page/bell/Dialog.ts | 7 +- src/page/bell/Launcher.ts | 12 +- src/page/bell/Message.ts | 7 +- src/page/managers/PromptsManager.ts | 4 +- .../slidedownManager/SlidedownManager.ts | 8 +- src/page/slidedown/ChannelCaptureContainer.ts | 10 +- src/page/slidedown/ConfirmationToast.ts | 18 +- src/page/slidedown/Slidedown.ts | 27 ++- src/page/slidedown/SlidedownElement.ts | 4 +- src/page/slidedown/TaggingContainer.ts | 14 +- src/shared/api/OneSignalApiBase.ts | 5 +- src/shared/helpers/dom.ts | 129 +++++++++++++ src/shared/helpers/general.ts | 27 +++ src/shared/managers/CustomLinkManager.ts | 2 +- src/shared/utils/AwaitableTimeout.ts | 4 - src/shared/utils/utils.ts | 170 ------------------ src/sw/serviceWorker/ServiceWorker.test.ts | 2 + src/sw/serviceWorker/ServiceWorker.ts | 6 +- 26 files changed, 237 insertions(+), 273 deletions(-) create mode 100644 src/shared/helpers/dom.ts delete mode 100644 src/shared/utils/AwaitableTimeout.ts diff --git a/__test__/support/helpers/executors.ts b/__test__/support/helpers/executors.ts index 4066e48c8..690d69bff 100644 --- a/__test__/support/helpers/executors.ts +++ b/__test__/support/helpers/executors.ts @@ -1,6 +1,6 @@ import { OP_REPO_EXECUTION_INTERVAL } from 'src/core/operationRepo/constants'; import { GroupComparisonType, Operation } from 'src/core/operations/Operation'; -import { delay } from 'src/shared/utils/utils'; +import { delay } from 'src/shared/helpers/general'; export class SomeOperation extends Operation { constructor() { diff --git a/src/core/executors/LoginUserOperationExecutor.ts b/src/core/executors/LoginUserOperationExecutor.ts index fca198efd..b471c2f14 100644 --- a/src/core/executors/LoginUserOperationExecutor.ts +++ b/src/core/executors/LoginUserOperationExecutor.ts @@ -5,6 +5,7 @@ import { } from 'src/core/types/operation'; import OneSignalError from 'src/shared/errors/OneSignalError'; import EventHelper from 'src/shared/helpers/EventHelper'; +import { getTimeZoneId } from 'src/shared/helpers/general'; import { getResponseStatusType, ResponseStatusType, @@ -12,7 +13,6 @@ import { import Log from 'src/shared/libraries/Log'; import Database from 'src/shared/services/Database'; import LocalStorage from 'src/shared/utils/LocalStorage'; -import { getTimeZoneId } from 'src/shared/utils/utils'; import { IdentityConstants, OPERATION_NAME } from '../constants'; import { type IPropertiesModelKeys } from '../models/PropertiesModel'; import { type IdentityModelStore } from '../modelStores/IdentityModelStore'; diff --git a/src/core/operationRepo/OperationRepo.ts b/src/core/operationRepo/OperationRepo.ts index 248bf172a..389262b4b 100644 --- a/src/core/operationRepo/OperationRepo.ts +++ b/src/core/operationRepo/OperationRepo.ts @@ -4,9 +4,9 @@ import { type IOperationRepo, type IStartableService, } from 'src/core/types/operation'; +import { delay } from 'src/shared/helpers/general'; import Log from 'src/shared/libraries/Log'; import Database from 'src/shared/services/Database'; -import { delay } from 'src/shared/utils/utils'; import { type OperationModelStore } from '../modelRepo/OperationModelStore'; import { GroupComparisonType, type Operation } from '../operations/Operation'; import { ModelName } from '../types/models'; diff --git a/src/onesignal/User.ts b/src/onesignal/User.ts index 366c6dbf2..1ed044572 100644 --- a/src/onesignal/User.ts +++ b/src/onesignal/User.ts @@ -4,18 +4,14 @@ import { SubscriptionType, type SubscriptionTypeValue, } from 'src/core/types/subscription'; +import { isObject, isValidEmail } from 'src/shared/helpers/general'; import Log from 'src/shared/libraries/Log'; import { IDManager } from 'src/shared/managers/IDManager'; import { InvalidArgumentError, InvalidArgumentReason, } from '../shared/errors/InvalidArgumentError'; -import { - isObject, - isObjectSerializable, - isValidEmail, - logMethodCall, -} from '../shared/utils/utils'; +import { logMethodCall } from '../shared/utils/utils'; export default class User { static singletonInstance?: User = undefined; diff --git a/src/page/bell/ActiveAnimatedElement.ts b/src/page/bell/ActiveAnimatedElement.ts index 9854334db..a34c416e5 100755 --- a/src/page/bell/ActiveAnimatedElement.ts +++ b/src/page/bell/ActiveAnimatedElement.ts @@ -1,11 +1,7 @@ +import { addCssClass, removeCssClass } from 'src/shared/helpers/dom'; import Log from '../../shared/libraries/Log'; import OneSignalEvent from '../../shared/services/OneSignalEvent'; -import { - addCssClass, - contains, - once, - removeCssClass, -} from '../../shared/utils/utils'; +import { contains, once } from '../../shared/utils/utils'; import AnimatedElement from './AnimatedElement'; export default class ActiveAnimatedElement extends AnimatedElement { diff --git a/src/page/bell/AnimatedElement.ts b/src/page/bell/AnimatedElement.ts index 04347dafc..051d3747f 100755 --- a/src/page/bell/AnimatedElement.ts +++ b/src/page/bell/AnimatedElement.ts @@ -1,11 +1,7 @@ +import { addCssClass, removeCssClass } from 'src/shared/helpers/dom'; import Log from '../../shared/libraries/Log'; import OneSignalEvent from '../../shared/services/OneSignalEvent'; -import { - addCssClass, - contains, - once, - removeCssClass, -} from '../../shared/utils/utils'; +import { contains, once } from '../../shared/utils/utils'; export default class AnimatedElement { public selector: string; diff --git a/src/page/bell/Bell.ts b/src/page/bell/Bell.ts index 2f7e069c5..cac63905e 100755 --- a/src/page/bell/Bell.ts +++ b/src/page/bell/Bell.ts @@ -1,4 +1,10 @@ import type { AppUserConfigNotifyButton } from 'src/shared/config'; +import { + addCssClass, + addDomElement, + removeDomElement, +} from 'src/shared/helpers/dom'; +import { delay } from 'src/shared/helpers/general'; import type { BellPosition, BellSize, BellText } from 'src/shared/prompts'; import { Browser, getBrowserName } from 'src/shared/useragent'; import OneSignal from '../../onesignal/OneSignal'; @@ -8,15 +14,7 @@ import Log from '../../shared/libraries/Log'; import { NotificationPermission } from '../../shared/models/NotificationPermission'; import OneSignalEvent from '../../shared/services/OneSignalEvent'; import BrowserUtils from '../../shared/utils/BrowserUtils'; -import { - addCssClass, - addDomElement, - contains, - delay, - nothing, - once, - removeDomElement, -} from '../../shared/utils/utils'; +import { contains, nothing, once } from '../../shared/utils/utils'; import { DismissPrompt } from '../models/Dismiss'; import type { SubscriptionChangeEvent } from '../models/SubscriptionChangeEvent'; import { ResourceLoadState } from '../services/DynamicResourceLoader'; diff --git a/src/page/bell/Button.ts b/src/page/bell/Button.ts index ff890c30a..b58de3364 100755 --- a/src/page/bell/Button.ts +++ b/src/page/bell/Button.ts @@ -1,10 +1,10 @@ -import { removeDomElement, addDomElement } from '../../shared/utils/utils'; -import OneSignalEvent from '../../shared/services/OneSignalEvent'; +import { addDomElement, removeDomElement } from 'src/shared/helpers/dom'; +import InitHelper from 'src/shared/helpers/InitHelper'; +import LimitStore from 'src/shared/services/LimitStore'; +import OneSignalEvent from 'src/shared/services/OneSignalEvent'; import ActiveAnimatedElement from './ActiveAnimatedElement'; import Bell from './Bell'; -import LimitStore from '../../shared/services/LimitStore'; import Message from './Message'; -import InitHelper from '../../shared/helpers/InitHelper'; export default class Button extends ActiveAnimatedElement { public events: any; diff --git a/src/page/bell/Dialog.ts b/src/page/bell/Dialog.ts index eb815c3e9..e5f936ec8 100755 --- a/src/page/bell/Dialog.ts +++ b/src/page/bell/Dialog.ts @@ -1,15 +1,12 @@ +import { addDomElement, clearDomElementChildren } from 'src/shared/helpers/dom'; import { Browser, getBrowserName, isMobileBrowser, isTabletBrowser, } from 'src/shared/useragent'; +import { getPlatformNotificationIcon } from 'src/shared/utils/utils'; import OneSignalEvent from '../../shared/services/OneSignalEvent'; -import { - addDomElement, - clearDomElementChildren, - getPlatformNotificationIcon, -} from '../../shared/utils/utils'; import type { NotificationIcons } from '../models/NotificationIcons'; import AnimatedElement from './AnimatedElement'; import Bell from './Bell'; diff --git a/src/page/bell/Launcher.ts b/src/page/bell/Launcher.ts index 68a113fef..c28d83cfe 100755 --- a/src/page/bell/Launcher.ts +++ b/src/page/bell/Launcher.ts @@ -1,17 +1,15 @@ -import type { BellSize } from 'src/shared/prompts'; import { InvalidStateError, InvalidStateReason, -} from '../../shared/errors/InvalidStateError'; -import Log from '../../shared/libraries/Log'; +} from 'src/shared/errors/InvalidStateError'; import { addCssClass, - contains, hasCssClass, - nothing, - once, removeCssClass, -} from '../../shared/utils/utils'; +} from 'src/shared/helpers/dom'; +import Log from 'src/shared/libraries/Log'; +import type { BellSize } from 'src/shared/prompts'; +import { contains, nothing, once } from 'src/shared/utils/utils'; import ActiveAnimatedElement from './ActiveAnimatedElement'; import Bell from './Bell'; diff --git a/src/page/bell/Message.ts b/src/page/bell/Message.ts index 641d1aaa5..1b193caaf 100755 --- a/src/page/bell/Message.ts +++ b/src/page/bell/Message.ts @@ -1,6 +1,7 @@ -import Log from '../../shared/libraries/Log'; -import BrowserUtils from '../../shared/utils/BrowserUtils'; -import { delay, nothing } from '../../shared/utils/utils'; +import { delay } from 'src/shared/helpers/general'; +import Log from 'src/shared/libraries/Log'; +import BrowserUtils from 'src/shared/utils/BrowserUtils'; +import { nothing } from 'src/shared/utils/utils'; import AnimatedElement from './AnimatedElement'; import Bell from './Bell'; diff --git a/src/page/managers/PromptsManager.ts b/src/page/managers/PromptsManager.ts index 8d06c76f6..1e5d75338 100644 --- a/src/page/managers/PromptsManager.ts +++ b/src/page/managers/PromptsManager.ts @@ -1,3 +1,4 @@ +import { delay } from 'src/shared/helpers/general'; import { CONFIG_DEFAULTS_SLIDEDOWN_OPTIONS, DelayedPromptType, @@ -19,7 +20,6 @@ import InitHelper from '../../shared/helpers/InitHelper'; import PromptsHelper from '../../shared/helpers/PromptsHelper'; import Log from '../../shared/libraries/Log'; import OneSignalEvent from '../../shared/services/OneSignalEvent'; -import { awaitableTimeout } from '../../shared/utils/AwaitableTimeout'; import OneSignalUtils from '../../shared/utils/OneSignalUtils'; import type { ContextInterface } from '../models/Context'; import { DismissPrompt } from '../models/Dismiss'; @@ -145,7 +145,7 @@ export class PromptsManager { } if (timeDelaySeconds > 0) { - await awaitableTimeout(timeDelaySeconds * 1_000); + await delay(timeDelaySeconds * 1_000); } switch (type) { diff --git a/src/page/managers/slidedownManager/SlidedownManager.ts b/src/page/managers/slidedownManager/SlidedownManager.ts index 2cc134d62..d8f53a340 100644 --- a/src/page/managers/slidedownManager/SlidedownManager.ts +++ b/src/page/managers/slidedownManager/SlidedownManager.ts @@ -1,4 +1,5 @@ import type { TagsObjectForApi, TagsObjectWithBoolean } from 'src/page/tags'; +import { delay } from 'src/shared/helpers/general'; import { CONFIG_DEFAULTS_SLIDEDOWN_OPTIONS, DelayedPromptType, @@ -24,7 +25,6 @@ import PromptsHelper from '../../../shared/helpers/PromptsHelper'; import Log from '../../../shared/libraries/Log'; import { NotificationPermission } from '../../../shared/models/NotificationPermission'; import type { PushSubscriptionState } from '../../../shared/models/PushSubscriptionState'; -import { awaitableTimeout } from '../../../shared/utils/AwaitableTimeout'; import { OneSignalUtils } from '../../../shared/utils/OneSignalUtils'; import TagUtils from '../../../shared/utils/TagUtils'; import AlreadySubscribedError from '../../errors/AlreadySubscribedError'; @@ -279,10 +279,10 @@ export class SlidedownManager { if (!confirmMessage) { return; } - await awaitableTimeout(1000); + await delay(1000); const confirmationToast = new ConfirmationToast(confirmMessage); await confirmationToast.show(); - await awaitableTimeout(5000); + await delay(5000); confirmationToast.close(); ConfirmationToast.triggerSlidedownEvent(ConfirmationToast.EVENTS.CLOSED); } @@ -418,7 +418,7 @@ export class SlidedownManager { await this.showConfirmationToast(); } // timeout to allow slidedown close animation to finish in case another slidedown is queued - await awaitableTimeout(1000); + await delay(1000); Slidedown.triggerSlidedownEvent(Slidedown.EVENTS.CLOSED); } diff --git a/src/page/slidedown/ChannelCaptureContainer.ts b/src/page/slidedown/ChannelCaptureContainer.ts index 7ce453a15..e489c9855 100644 --- a/src/page/slidedown/ChannelCaptureContainer.ts +++ b/src/page/slidedown/ChannelCaptureContainer.ts @@ -1,3 +1,8 @@ +import { + addCssClass, + getDomElementOrStub, + removeCssClass, +} from 'src/shared/helpers/dom'; import { DelayedPromptType, type DelayedPromptTypeValue, @@ -10,11 +15,6 @@ import { DANGER_ICON, SLIDEDOWN_CSS_IDS, } from '../../shared/slidedown/constants'; -import { - addCssClass, - getDomElementOrStub, - removeCssClass, -} from '../../shared/utils/utils'; import { ItiScriptURLHashes, ItiScriptURLs, diff --git a/src/page/slidedown/ConfirmationToast.ts b/src/page/slidedown/ConfirmationToast.ts index 9b65e5468..17340f0f5 100755 --- a/src/page/slidedown/ConfirmationToast.ts +++ b/src/page/slidedown/ConfirmationToast.ts @@ -1,17 +1,17 @@ -import { isMobileBrowser } from 'src/shared/useragent'; -import OneSignalEvent from '../../shared/services/OneSignalEvent'; +import { + addCssClass, + getDomElementOrStub, + removeDomElement, +} from 'src/shared/helpers/dom'; +import OneSignalEvent from 'src/shared/services/OneSignalEvent'; import { SLIDEDOWN_CSS_CLASSES, SLIDEDOWN_CSS_IDS, TOAST_CLASSES, TOAST_IDS, -} from '../../shared/slidedown/constants'; -import { - addCssClass, - getDomElementOrStub, - once, - removeDomElement, -} from '../../shared/utils/utils'; +} from 'src/shared/slidedown/constants'; +import { isMobileBrowser } from 'src/shared/useragent'; +import { once } from 'src/shared/utils/utils'; export default class ConfirmationToast { private message: string; diff --git a/src/page/slidedown/Slidedown.ts b/src/page/slidedown/Slidedown.ts index 931f88ecb..5ea1b6df4 100755 --- a/src/page/slidedown/Slidedown.ts +++ b/src/page/slidedown/Slidedown.ts @@ -1,27 +1,26 @@ +import { + addCssClass, + addDomElement, + getDomElementOrStub, + removeCssClass, + removeDomElement, +} from 'src/shared/helpers/dom'; import { getValueOrDefault } from 'src/shared/helpers/general'; +import MainHelper from 'src/shared/helpers/MainHelper'; +import PromptsHelper from 'src/shared/helpers/PromptsHelper'; import { DelayedPromptType, SERVER_CONFIG_DEFAULTS_SLIDEDOWN, type SlidedownPromptOptions, } from 'src/shared/prompts'; -import { isMobileBrowser } from 'src/shared/useragent'; -import MainHelper from '../../shared/helpers/MainHelper'; -import PromptsHelper from '../../shared/helpers/PromptsHelper'; -import OneSignalEvent from '../../shared/services/OneSignalEvent'; +import OneSignalEvent from 'src/shared/services/OneSignalEvent'; import { COLORS, SLIDEDOWN_CSS_CLASSES, SLIDEDOWN_CSS_IDS, -} from '../../shared/slidedown/constants'; -import { - addCssClass, - addDomElement, - getDomElementOrStub, - getPlatformNotificationIcon, - once, - removeCssClass, - removeDomElement, -} from '../../shared/utils/utils'; +} from 'src/shared/slidedown/constants'; +import { isMobileBrowser } from 'src/shared/useragent'; +import { getPlatformNotificationIcon, once } from 'src/shared/utils/utils'; import { InvalidChannelInputField, type InvalidChannelInputFieldValue, diff --git a/src/page/slidedown/SlidedownElement.ts b/src/page/slidedown/SlidedownElement.ts index ec8ef2ef8..222762877 100644 --- a/src/page/slidedown/SlidedownElement.ts +++ b/src/page/slidedown/SlidedownElement.ts @@ -1,10 +1,10 @@ +import { addCssClass } from 'src/shared/helpers/dom'; import { DEFAULT_ICON, SLIDEDOWN_BUTTON_CLASSES, SLIDEDOWN_CSS_CLASSES, SLIDEDOWN_CSS_IDS, -} from '../../shared/slidedown/constants'; -import { addCssClass } from '../../shared/utils/utils'; +} from 'src/shared/slidedown/constants'; import type { SlidedownHtmlProps } from './types'; export function getSlidedownElement(dialogProps: SlidedownHtmlProps): Element { diff --git a/src/page/slidedown/TaggingContainer.ts b/src/page/slidedown/TaggingContainer.ts index 5fbd66b38..b6140c6af 100644 --- a/src/page/slidedown/TaggingContainer.ts +++ b/src/page/slidedown/TaggingContainer.ts @@ -1,4 +1,11 @@ import type { TagCategory } from 'src/page/tags'; +import { + addCssClass, + addDomElement, + getDomElementOrStub, + removeCssClass, + removeDomElement, +} from 'src/shared/helpers/dom'; import { COLORS, SLIDEDOWN_CSS_CLASSES, @@ -8,13 +15,6 @@ import { TAGGING_CONTAINER_STRINGS, } from '../../shared/slidedown/constants'; import TagUtils from '../../shared/utils/TagUtils'; -import { - addCssClass, - addDomElement, - getDomElementOrStub, - removeCssClass, - removeDomElement, -} from '../../shared/utils/utils'; import type { TagsObjectWithBoolean } from '../tags'; import { getLoadingIndicatorWithColor } from './LoadingIndicator'; diff --git a/src/shared/api/OneSignalApiBase.ts b/src/shared/api/OneSignalApiBase.ts index f9cbe3f77..1f6fc8762 100644 --- a/src/shared/api/OneSignalApiBase.ts +++ b/src/shared/api/OneSignalApiBase.ts @@ -4,10 +4,9 @@ import { OneSignalApiErrorKind, } from '../errors/OneSignalApiError'; import OneSignalError from '../errors/OneSignalError'; -import { isValidUuid } from '../helpers/general'; +import { delay, isValidUuid } from '../helpers/general'; import Log from '../libraries/Log'; import type { APIHeaders } from '../models/APIHeaders'; -import { awaitableTimeout } from '../utils/AwaitableTimeout'; import { IS_SERVICE_WORKER, VERSION } from '../utils/EnvVariables'; import type OneSignalApiBaseResponse from './OneSignalApiBaseResponse'; import { RETRY_BACKOFF } from './RetryBackoff'; @@ -122,7 +121,7 @@ export class OneSignalApiBase { }; } catch (e) { if (e instanceof Error && e.name === 'TypeError') { - await awaitableTimeout(RETRY_BACKOFF[retry]); + await delay(RETRY_BACKOFF[retry]); Log.error( `OneSignal: Network timed out while calling ${url}. Retrying...`, ); diff --git a/src/shared/helpers/dom.ts b/src/shared/helpers/dom.ts new file mode 100644 index 000000000..ecf1e61c2 --- /dev/null +++ b/src/shared/helpers/dom.ts @@ -0,0 +1,129 @@ +import Log from 'src/sw/libraries/Log'; + +export function addDomElement( + targetSelectorOrElement: string | Element, + addOrder: InsertPosition, + elementHtml: string, +) { + let targetElement: Element | null; + if (typeof targetSelectorOrElement === 'string') { + targetElement = document.querySelector(targetSelectorOrElement); + } else { + targetElement = targetSelectorOrElement; + } + + if (targetElement) { + targetElement.insertAdjacentHTML(addOrder, elementHtml); + return; + } + throw new Error( + `${targetSelectorOrElement} must be a CSS selector string or DOM Element object.`, + ); +} + +export function removeDomElement(selector: string) { + const els = document.querySelectorAll(selector); + if (els.length > 0) { + for (let i = 0; i < els.length; i++) { + const parentNode = els[i].parentNode; + if (parentNode) { + parentNode.removeChild(els[i]); + } + } + } +} + +export function clearDomElementChildren( + targetSelectorOrElement: Element | string, +) { + if (typeof targetSelectorOrElement === 'string') { + const element = document.querySelector(targetSelectorOrElement); + if (element === null) { + throw new Error( + `Cannot find element with selector "${targetSelectorOrElement}"`, + ); + } + while (element.firstChild) { + element.removeChild(element.firstChild); + } + } else if (typeof targetSelectorOrElement === 'object') { + while (targetSelectorOrElement.firstChild) { + targetSelectorOrElement.removeChild(targetSelectorOrElement.firstChild); + } + } else + throw new Error( + `${targetSelectorOrElement} must be a CSS selector string or DOM Element object.`, + ); +} + +export function getDomElementOrStub(selector: string): Element { + const foundElement = document.querySelector(selector); + if (!foundElement) { + Log.debug(`No instance of ${selector} found. Returning stub.`); + return document.createElement('div'); + } + return foundElement; +} + +export function addCssClass( + targetSelectorOrElement: Element | string, + cssClass: string, +) { + if (typeof targetSelectorOrElement === 'string') { + const element = document.querySelector(targetSelectorOrElement); + if (element === null) { + throw new Error( + `Cannot find element with selector "${targetSelectorOrElement}"`, + ); + } + element.classList.add(cssClass); + } else if (typeof targetSelectorOrElement === 'object') { + targetSelectorOrElement.classList.add(cssClass); + } else { + throw new Error( + `${targetSelectorOrElement} must be a CSS selector string or DOM Element object.`, + ); + } +} + +export function removeCssClass( + targetSelectorOrElement: Element | string, + cssClass: string, +) { + if (typeof targetSelectorOrElement === 'string') { + const element = document.querySelector(targetSelectorOrElement); + if (element === null) { + throw new Error( + `Cannot find element with selector "${targetSelectorOrElement}"`, + ); + } + element.classList.remove(cssClass); + } else if (typeof targetSelectorOrElement === 'object') { + targetSelectorOrElement.classList.remove(cssClass); + } else { + throw new Error( + `${targetSelectorOrElement} must be a CSS selector string or DOM Element object.`, + ); + } +} + +export function hasCssClass( + targetSelectorOrElement: Element | string, + cssClass: string, +) { + if (typeof targetSelectorOrElement === 'string') { + const element = document.querySelector(targetSelectorOrElement); + if (element === null) { + throw new Error( + `Cannot find element with selector "${targetSelectorOrElement}"`, + ); + } + return element.classList.contains(cssClass); + } else if (typeof targetSelectorOrElement === 'object') { + return targetSelectorOrElement.classList.contains(cssClass); + } else { + throw new Error( + `${targetSelectorOrElement} must be a CSS selector string or DOM Element object.`, + ); + } +} diff --git a/src/shared/helpers/general.ts b/src/shared/helpers/general.ts index af9c2c897..1a55072dd 100644 --- a/src/shared/helpers/general.ts +++ b/src/shared/helpers/general.ts @@ -9,6 +9,15 @@ export function isValidUuid(uuid: string) { ); } +/** + * Returns a promise for the setTimeout() method. + * @param durationMs + * @returns {Promise} Returns a promise that resolves when the timeout is complete. + */ +export function delay(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + type Nullable = undefined | null; export function valueOrDefault(value: T | Nullable, defaultValue: T): T { if (typeof value === 'undefined' || value === null) { @@ -26,3 +35,21 @@ export function getValueOrDefault( } return defaultValue; } + +export function getTimeZoneId() { + return Intl.DateTimeFormat().resolvedOptions().timeZone; +} + +export function isObject(value: unknown) { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + +export function isValidEmail(email: string | undefined | null) { + return ( + !!email && + !!email.match( + // eslint-disable-next-line no-control-regex + /^([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x22([^\x0d\x22\x5c\x80-\xff]|\x5c[\x00-\x7f])*\x22)(\x2e([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x22([^\x0d\x22\x5c\x80-\xff]|\x5c[\x00-\x7f])*\x22))*\x40([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x5b([^\x0d\x5b-\x5d\x80-\xff]|\x5c[\x00-\x7f])*\x5d)(\x2e([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x5b([^\x0d\x5b-\x5d\x80-\xff]|\x5c[\x00-\x7f])*\x5d))*$/, + ) + ); +} diff --git a/src/shared/managers/CustomLinkManager.ts b/src/shared/managers/CustomLinkManager.ts index a0a49585d..fb6b808e5 100644 --- a/src/shared/managers/CustomLinkManager.ts +++ b/src/shared/managers/CustomLinkManager.ts @@ -1,11 +1,11 @@ import { ResourceLoadState } from '../../page/services/DynamicResourceLoader'; +import { addCssClass } from '../helpers/dom'; import Log from '../libraries/Log'; import type { AppUserConfigCustomLinkOptions } from '../prompts'; import { CUSTOM_LINK_CSS_CLASSES, CUSTOM_LINK_CSS_SELECTORS, } from '../slidedown/constants'; -import { addCssClass } from '../utils/utils'; export class CustomLinkManager { private config: AppUserConfigCustomLinkOptions | undefined; diff --git a/src/shared/utils/AwaitableTimeout.ts b/src/shared/utils/AwaitableTimeout.ts deleted file mode 100644 index 389774d24..000000000 --- a/src/shared/utils/AwaitableTimeout.ts +++ /dev/null @@ -1,4 +0,0 @@ -// timeout with option to await on it -export function awaitableTimeout(ms: number): Promise { - return new Promise((resolve) => setTimeout(resolve, ms)); -} diff --git a/src/shared/utils/utils.ts b/src/shared/utils/utils.ts index 9c2d8f092..49276e88a 100755 --- a/src/shared/utils/utils.ts +++ b/src/shared/utils/utils.ts @@ -6,18 +6,6 @@ import { IS_SERVICE_WORKER } from './EnvVariables'; import { OneSignalUtils } from './OneSignalUtils'; import { PermissionUtils } from './PermissionUtils'; -export function removeDomElement(selector: string) { - const els = document.querySelectorAll(selector); - if (els.length > 0) { - for (let i = 0; i < els.length; i++) { - const parentNode = els[i].parentNode; - if (parentNode) { - parentNode.removeChild(els[i]); - } - } - } -} - /** * Helper method for public APIs that waits until OneSignal is initialized, rejects if push notifications are * not supported, and wraps these tasks in a Promise. @@ -52,134 +40,6 @@ export function logMethodCall(methodName: string, ...args: any[]) { return OneSignalUtils.logMethodCall(methodName, ...args); } -export function isValidEmail(email: string | undefined | null) { - return ( - !!email && - !!email.match( - // eslint-disable-next-line no-control-regex - /^([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x22([^\x0d\x22\x5c\x80-\xff]|\x5c[\x00-\x7f])*\x22)(\x2e([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x22([^\x0d\x22\x5c\x80-\xff]|\x5c[\x00-\x7f])*\x22))*\x40([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x5b([^\x0d\x5b-\x5d\x80-\xff]|\x5c[\x00-\x7f])*\x5d)(\x2e([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x5b([^\x0d\x5b-\x5d\x80-\xff]|\x5c[\x00-\x7f])*\x5d))*$/, - ) - ); -} - -export function addDomElement( - targetSelectorOrElement: string | Element, - addOrder: InsertPosition, - elementHtml: string, -) { - let targetElement: Element | null; - if (typeof targetSelectorOrElement === 'string') { - targetElement = document.querySelector(targetSelectorOrElement); - } else { - targetElement = targetSelectorOrElement; - } - - if (targetElement) { - targetElement.insertAdjacentHTML(addOrder, elementHtml); - return; - } - throw new Error( - `${targetSelectorOrElement} must be a CSS selector string or DOM Element object.`, - ); -} - -export function clearDomElementChildren( - targetSelectorOrElement: Element | string, -) { - if (typeof targetSelectorOrElement === 'string') { - const element = document.querySelector(targetSelectorOrElement); - if (element === null) { - throw new Error( - `Cannot find element with selector "${targetSelectorOrElement}"`, - ); - } - while (element.firstChild) { - element.removeChild(element.firstChild); - } - } else if (typeof targetSelectorOrElement === 'object') { - while (targetSelectorOrElement.firstChild) { - targetSelectorOrElement.removeChild(targetSelectorOrElement.firstChild); - } - } else - throw new Error( - `${targetSelectorOrElement} must be a CSS selector string or DOM Element object.`, - ); -} - -export function addCssClass( - targetSelectorOrElement: Element | string, - cssClass: string, -) { - if (typeof targetSelectorOrElement === 'string') { - const element = document.querySelector(targetSelectorOrElement); - if (element === null) { - throw new Error( - `Cannot find element with selector "${targetSelectorOrElement}"`, - ); - } - element.classList.add(cssClass); - } else if (typeof targetSelectorOrElement === 'object') { - targetSelectorOrElement.classList.add(cssClass); - } else { - throw new Error( - `${targetSelectorOrElement} must be a CSS selector string or DOM Element object.`, - ); - } -} - -export function removeCssClass( - targetSelectorOrElement: Element | string, - cssClass: string, -) { - if (typeof targetSelectorOrElement === 'string') { - const element = document.querySelector(targetSelectorOrElement); - if (element === null) { - throw new Error( - `Cannot find element with selector "${targetSelectorOrElement}"`, - ); - } - element.classList.remove(cssClass); - } else if (typeof targetSelectorOrElement === 'object') { - targetSelectorOrElement.classList.remove(cssClass); - } else { - throw new Error( - `${targetSelectorOrElement} must be a CSS selector string or DOM Element object.`, - ); - } -} - -export function hasCssClass( - targetSelectorOrElement: Element | string, - cssClass: string, -) { - if (typeof targetSelectorOrElement === 'string') { - const element = document.querySelector(targetSelectorOrElement); - if (element === null) { - throw new Error( - `Cannot find element with selector "${targetSelectorOrElement}"`, - ); - } - return element.classList.contains(cssClass); - } else if (typeof targetSelectorOrElement === 'object') { - return targetSelectorOrElement.classList.contains(cssClass); - } else { - throw new Error( - `${targetSelectorOrElement} must be a CSS selector string or DOM Element object.`, - ); - } -} - -/** - * Returns a promise for the setTimeout() method. - * @param durationMs - * @returns {Promise} Returns a promise that resolves when the timeout is complete. - */ -export function delay(durationMs: number) { - return new Promise((resolve) => { - setTimeout(resolve, durationMs); - }); -} - export function nothing(): Promise { return Promise.resolve(); } @@ -307,33 +167,3 @@ export function getPlatformNotificationIcon( 'default-icon' ); } - -export function getDomElementOrStub(selector: string): Element { - const foundElement = document.querySelector(selector); - if (!foundElement) { - Log.debug(`No instance of ${selector} found. Returning stub.`); - return document.createElement('div'); - } - return foundElement; -} - -export function getTimeZoneId() { - return Intl.DateTimeFormat().resolvedOptions().timeZone; -} - -export function isObject(value: unknown) { - return typeof value === 'object' && value !== null && !Array.isArray(value); -} - -/** - * Returns true if the value is a JSON-serializable object. - */ -export function isObjectSerializable(value: unknown): boolean { - if (!isObject(value)) return false; - try { - JSON.stringify(value); - return true; - } catch (e) { - return false; - } -} diff --git a/src/sw/serviceWorker/ServiceWorker.test.ts b/src/sw/serviceWorker/ServiceWorker.test.ts index b866c74f4..8b31cacd7 100644 --- a/src/sw/serviceWorker/ServiceWorker.test.ts +++ b/src/sw/serviceWorker/ServiceWorker.test.ts @@ -205,6 +205,8 @@ describe('ServiceWorker', () => { }); await dispatchEvent(new PushEvent('push', payload)); + await vi.runOnlyPendingTimersAsync(); + expect(apiPutSpy).toHaveBeenCalledWith( `notifications/${payload.custom.i}/report_received`, { diff --git a/src/sw/serviceWorker/ServiceWorker.ts b/src/sw/serviceWorker/ServiceWorker.ts index f7984b8f0..ed211e59b 100755 --- a/src/sw/serviceWorker/ServiceWorker.ts +++ b/src/sw/serviceWorker/ServiceWorker.ts @@ -7,6 +7,7 @@ import OneSignalApiBase from 'src/shared/api/OneSignalApiBase'; import OneSignalApiSW from 'src/shared/api/OneSignalApiSW'; import { type AppConfig, getServerAppConfig } from 'src/shared/config'; import { Utils } from 'src/shared/context/Utils'; +import { delay } from 'src/shared/helpers/general'; import ServiceWorkerHelper from 'src/shared/helpers/ServiceWorkerHelper'; import { WorkerMessenger, @@ -33,7 +34,6 @@ import { type UpsertOrDeactivateSessionPayload, } from 'src/shared/session'; import { Browser, getBrowserName } from 'src/shared/useragent'; -import { awaitableTimeout } from 'src/shared/utils/AwaitableTimeout'; import { VERSION } from 'src/shared/utils/EnvVariables'; import { cancelableTimeout } from '../helpers/CancelableTimeout'; import { ModelCacheDirectAccess } from '../helpers/ModelCacheDirectAccess'; @@ -355,7 +355,7 @@ export class ServiceWorker { `Called sendConfirmedDelivery(${JSON.stringify(notification, null, 4)})`, ); - await awaitableTimeout( + await delay( Math.floor(Math.random() * MAX_CONFIRMED_DELIVERY_DELAY * 1_000), ); await OneSignalApiBase.put( @@ -669,7 +669,7 @@ export class ServiceWorker { ); if (this.requiresMacOS15ChromiumAfterDisplayWorkaround()) { - await awaitableTimeout(1_000); + await delay(1_000); } } From ec09b100a0f95c6f96d6858bc392a370e46ce0a8 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Mon, 28 Jul 2025 21:03:13 -0700 Subject: [PATCH 05/12] move events helpers to shared/listeners --- .../nativePermissionChange.test.ts | 6 +- package.json | 4 +- .../executors/LoginUserOperationExecutor.ts | 4 +- src/onesignal/NotificationsNamespace.ts | 4 +- src/onesignal/OneSignal.ts | 9 +- src/onesignal/PushSubscriptionNamespace.ts | 15 +- src/page/bell/Bell.ts | 8 +- src/page/bell/Launcher.ts | 3 +- src/page/bell/Message.ts | 9 +- src/page/managers/PromptsManager.ts | 18 +- .../slidedownManager/SlidedownManager.ts | 11 +- src/page/slidedown/Slidedown.ts | 4 +- src/shared/helpers/EventHelper.ts | 358 ------------------ src/shared/helpers/PromptsHelper.ts | 40 -- src/shared/helpers/SubscriptionHelper.ts | 4 +- src/shared/helpers/dom.ts | 8 + src/shared/helpers/general.ts | 4 + src/shared/listeners/index.ts | 1 + .../shared/listeners/listeners.test.ts | 8 +- src/shared/listeners/listeners.ts | 347 +++++++++++++++++ src/shared/managers/ServiceWorkerManager.ts | 6 +- src/shared/managers/SubscriptionManager.ts | 9 +- src/shared/prompts/index.ts | 1 + .../prompts.test.ts} | 8 +- src/shared/prompts/prompts.ts | 35 ++ src/shared/utils/BrowserUtils.ts | 13 - src/shared/utils/utils.ts | 14 - 27 files changed, 464 insertions(+), 487 deletions(-) delete mode 100755 src/shared/helpers/EventHelper.ts delete mode 100644 src/shared/helpers/PromptsHelper.ts create mode 100644 src/shared/listeners/index.ts rename __test__/unit/notifications/eventListeners.test.ts => src/shared/listeners/listeners.test.ts (58%) create mode 100644 src/shared/listeners/listeners.ts rename src/shared/{helpers/PromptsHelper.test.ts => prompts/prompts.test.ts} (81%) create mode 100644 src/shared/prompts/prompts.ts delete mode 100644 src/shared/utils/BrowserUtils.ts diff --git a/__test__/unit/pushSubscription/nativePermissionChange.test.ts b/__test__/unit/pushSubscription/nativePermissionChange.test.ts index 3e5e088cb..513d6a2c0 100644 --- a/__test__/unit/pushSubscription/nativePermissionChange.test.ts +++ b/__test__/unit/pushSubscription/nativePermissionChange.test.ts @@ -9,10 +9,10 @@ import { TestEnvironment } from '__test__/support/environment/TestEnvironment'; import { createPushSub } from '__test__/support/environment/TestEnvironmentHelpers'; import { MockServiceWorker } from '__test__/support/mocks/MockServiceWorker'; import Emitter from 'src/shared/libraries/Emitter'; +import { checkAndTriggerSubscriptionChanged } from 'src/shared/listeners'; import { AppState } from 'src/shared/models/AppState'; import Database from 'src/shared/services/Database'; import { PermissionUtils } from 'src/shared/utils/PermissionUtils'; -import EventHelper from '../../../src/shared/helpers/EventHelper'; import MainHelper from '../../../src/shared/helpers/MainHelper'; import { NotificationPermission } from '../../../src/shared/models/NotificationPermission'; @@ -118,7 +118,7 @@ describe('Notification Types are set correctly on subscription change', () => { }); OneSignal.coreDirector.addSubscriptionModel(pushModel); - await EventHelper.checkAndTriggerSubscriptionChanged(); + await checkAndTriggerSubscriptionChanged(); expect(changeListener).not.toHaveBeenCalled(); }); @@ -136,7 +136,7 @@ describe('Notification Types are set correctly on subscription change', () => { }); OneSignal.coreDirector.subscriptionModelStore.add(pushModel); - await EventHelper.checkAndTriggerSubscriptionChanged(); + await checkAndTriggerSubscriptionChanged(); expect(changeListener).toHaveBeenCalledWith({ current: { id: DUMMY_SUBSCRIPTION_ID_2, diff --git a/package.json b/package.json index 8d3698f21..3e7866500 100644 --- a/package.json +++ b/package.json @@ -80,12 +80,12 @@ }, { "path": "./build/releases/OneSignalSDK.page.es6.js", - "limit": "61.2 kB", + "limit": "60.5 kB", "gzip": true }, { "path": "./build/releases/OneSignalSDK.sw.js", - "limit": "31.3 kB", + "limit": "25.1 kB", "gzip": true }, { diff --git a/src/core/executors/LoginUserOperationExecutor.ts b/src/core/executors/LoginUserOperationExecutor.ts index b471c2f14..59c3916f8 100644 --- a/src/core/executors/LoginUserOperationExecutor.ts +++ b/src/core/executors/LoginUserOperationExecutor.ts @@ -4,13 +4,13 @@ import { type IOperationExecutor, } from 'src/core/types/operation'; import OneSignalError from 'src/shared/errors/OneSignalError'; -import EventHelper from 'src/shared/helpers/EventHelper'; import { getTimeZoneId } from 'src/shared/helpers/general'; import { getResponseStatusType, ResponseStatusType, } from 'src/shared/helpers/NetworkUtils'; import Log from 'src/shared/libraries/Log'; +import { checkAndTriggerUserChanged } from 'src/shared/listeners'; import Database from 'src/shared/services/Database'; import LocalStorage from 'src/shared/utils/LocalStorage'; import { IdentityConstants, OPERATION_NAME } from '../constants'; @@ -250,7 +250,7 @@ export class LoginUserOperationExecutor implements IOperationExecutor { ); } - EventHelper.checkAndTriggerUserChanged(); + checkAndTriggerUserChanged(); const followUp = Object.keys(identity).length > 0 diff --git a/src/onesignal/NotificationsNamespace.ts b/src/onesignal/NotificationsNamespace.ts index 949e04ffd..702a359f4 100644 --- a/src/onesignal/NotificationsNamespace.ts +++ b/src/onesignal/NotificationsNamespace.ts @@ -1,3 +1,4 @@ +import { fireStoredNotificationClicks } from 'src/shared/listeners'; import type { NotificationEventName } from '../page/models/NotificationEventName'; import type { NotificationEventTypeMap } from '../page/models/NotificationEventTypeMap'; import { EventListenerBase } from '../page/userModel/EventListenerBase'; @@ -6,7 +7,6 @@ import { InvalidArgumentError, InvalidArgumentReason, } from '../shared/errors/InvalidArgumentError'; -import EventHelper from '../shared/helpers/EventHelper'; import { NotificationPermission } from '../shared/models/NotificationPermission'; import Database from '../shared/services/Database'; import { @@ -151,7 +151,7 @@ export default class NotificationsNamespace extends EventListenerBase { OneSignal.emitter.on(event, listener); if (event === 'click') { - EventHelper.fireStoredNotificationClicks(); + fireStoredNotificationClicks(); } } diff --git a/src/onesignal/OneSignal.ts b/src/onesignal/OneSignal.ts index 5fdf5765c..c9440099e 100644 --- a/src/onesignal/OneSignal.ts +++ b/src/onesignal/OneSignal.ts @@ -5,6 +5,10 @@ import { type AppUserConfig, } from 'src/shared/config'; import { windowEnvString } from 'src/shared/environment'; +import { + _onSubscriptionChanged, + checkAndTriggerSubscriptionChanged, +} from 'src/shared/listeners'; import { Browser, getBrowserName, @@ -23,7 +27,6 @@ import { InvalidArgumentReason, } from '../shared/errors/InvalidArgumentError'; import { SdkInitError, SdkInitErrorKind } from '../shared/errors/SdkInitError'; -import EventHelper from '../shared/helpers/EventHelper'; import InitHelper from '../shared/helpers/InitHelper'; import MainHelper from '../shared/helpers/MainHelper'; import Emitter from '../shared/libraries/Emitter'; @@ -170,11 +173,11 @@ export default class OneSignal { OneSignal.emitter.on( OneSignal.EVENTS.NOTIFICATION_PERMISSION_CHANGED_AS_STRING, - EventHelper.onNotificationPermissionChange, + checkAndTriggerSubscriptionChanged, ); OneSignal.emitter.on( OneSignal.EVENTS.SUBSCRIPTION_CHANGED, - EventHelper._onSubscriptionChanged, + _onSubscriptionChanged, ); OneSignal.emitter.on( OneSignal.EVENTS.SDK_INITIALIZED, diff --git a/src/onesignal/PushSubscriptionNamespace.ts b/src/onesignal/PushSubscriptionNamespace.ts index 1f53dc1f1..05119d20e 100644 --- a/src/onesignal/PushSubscriptionNamespace.ts +++ b/src/onesignal/PushSubscriptionNamespace.ts @@ -1,3 +1,7 @@ +import { + checkAndTriggerSubscriptionChanged, + onInternalSubscriptionSet, +} from 'src/shared/listeners'; import { isCompleteSubscriptionObject } from '../core/utils/typePredicates'; import type { SubscriptionChangeEvent } from '../page/models/SubscriptionChangeEvent'; import { EventListenerBase } from '../page/userModel/EventListenerBase'; @@ -10,7 +14,6 @@ import { InvalidStateError, InvalidStateReason, } from '../shared/errors/InvalidStateError'; -import EventHelper from '../shared/helpers/EventHelper'; import Log from '../shared/libraries/Log'; import { Subscription } from '../shared/models/Subscription'; import Database from '../shared/services/Database'; @@ -139,12 +142,10 @@ export default class PushSubscriptionNamespace extends EventListenerBase { subscriptionFromDb.optedOut = !enabled; await Database.setSubscription(subscriptionFromDb); - EventHelper.onInternalSubscriptionSet(subscriptionFromDb.optedOut).catch( - (e) => { - Log.error(e); - }, - ); - EventHelper.checkAndTriggerSubscriptionChanged().catch((e) => { + onInternalSubscriptionSet(subscriptionFromDb.optedOut).catch((e) => { + Log.error(e); + }); + checkAndTriggerSubscriptionChanged().catch((e) => { Log.error(e); }); } diff --git a/src/page/bell/Bell.ts b/src/page/bell/Bell.ts index cac63905e..28527aa7c 100755 --- a/src/page/bell/Bell.ts +++ b/src/page/bell/Bell.ts @@ -2,9 +2,10 @@ import type { AppUserConfigNotifyButton } from 'src/shared/config'; import { addCssClass, addDomElement, + decodeHtmlEntities, removeDomElement, } from 'src/shared/helpers/dom'; -import { delay } from 'src/shared/helpers/general'; +import { delay, nothing } from 'src/shared/helpers/general'; import type { BellPosition, BellSize, BellText } from 'src/shared/prompts'; import { Browser, getBrowserName } from 'src/shared/useragent'; import OneSignal from '../../onesignal/OneSignal'; @@ -13,8 +14,7 @@ import MainHelper from '../../shared/helpers/MainHelper'; import Log from '../../shared/libraries/Log'; import { NotificationPermission } from '../../shared/models/NotificationPermission'; import OneSignalEvent from '../../shared/services/OneSignalEvent'; -import BrowserUtils from '../../shared/utils/BrowserUtils'; -import { contains, nothing, once } from '../../shared/utils/utils'; +import { contains, once } from '../../shared/utils/utils'; import { DismissPrompt } from '../models/Dismiss'; import type { SubscriptionChangeEvent } from '../models/SubscriptionChangeEvent'; import { ResourceLoadState } from '../services/DynamicResourceLoader'; @@ -270,7 +270,7 @@ export default class Bell { resolve(); }); } else { - this.message.content = BrowserUtils.decodeHtmlEntities( + this.message.content = decodeHtmlEntities( this.message.getTipForState(), ); this.message.contentType = Message.TYPES.TIP; diff --git a/src/page/bell/Launcher.ts b/src/page/bell/Launcher.ts index c28d83cfe..d10f0e12b 100755 --- a/src/page/bell/Launcher.ts +++ b/src/page/bell/Launcher.ts @@ -7,9 +7,10 @@ import { hasCssClass, removeCssClass, } from 'src/shared/helpers/dom'; +import { nothing } from 'src/shared/helpers/general'; import Log from 'src/shared/libraries/Log'; import type { BellSize } from 'src/shared/prompts'; -import { contains, nothing, once } from 'src/shared/utils/utils'; +import { contains, once } from 'src/shared/utils/utils'; import ActiveAnimatedElement from './ActiveAnimatedElement'; import Bell from './Bell'; diff --git a/src/page/bell/Message.ts b/src/page/bell/Message.ts index 1b193caaf..db19b511d 100755 --- a/src/page/bell/Message.ts +++ b/src/page/bell/Message.ts @@ -1,7 +1,6 @@ -import { delay } from 'src/shared/helpers/general'; +import { decodeHtmlEntities } from 'src/shared/helpers/dom'; +import { delay, nothing } from 'src/shared/helpers/general'; import Log from 'src/shared/libraries/Log'; -import BrowserUtils from 'src/shared/utils/BrowserUtils'; -import { nothing } from 'src/shared/utils/utils'; import AnimatedElement from './AnimatedElement'; import Bell from './Bell'; @@ -41,7 +40,7 @@ export default class Message extends AnimatedElement { Log.debug(`Calling display(${type}, ${content}, ${duration}).`); return (this.shown ? this.hide() : nothing()) .then(() => { - this.content = BrowserUtils.decodeHtmlEntities(content); + this.content = decodeHtmlEntities(content); this.contentType = type; }) .then(() => { @@ -69,7 +68,7 @@ export default class Message extends AnimatedElement { } enqueue(message: string) { - this.queued.push(BrowserUtils.decodeHtmlEntities(message)); + this.queued.push(decodeHtmlEntities(message)); return new Promise((resolve) => { if (this.bell.badge.shown) { this.bell.badge diff --git a/src/page/managers/PromptsManager.ts b/src/page/managers/PromptsManager.ts index 1e5d75338..11ef6660f 100644 --- a/src/page/managers/PromptsManager.ts +++ b/src/page/managers/PromptsManager.ts @@ -2,6 +2,7 @@ import { delay } from 'src/shared/helpers/general'; import { CONFIG_DEFAULTS_SLIDEDOWN_OPTIONS, DelayedPromptType, + getFirstSlidedownPromptOptionsWithType, SERVER_CONFIG_DEFAULTS_PROMPT_DELAYS, type AppUserConfigPromptOptions, type DelayedPromptOptions, @@ -17,7 +18,6 @@ import { } from 'src/shared/useragent'; import { DismissHelper } from '../../shared/helpers/DismissHelper'; import InitHelper from '../../shared/helpers/InitHelper'; -import PromptsHelper from '../../shared/helpers/PromptsHelper'; import Log from '../../shared/libraries/Log'; import OneSignalEvent from '../../shared/services/OneSignalEvent'; import OneSignalUtils from '../../shared/utils/OneSignalUtils'; @@ -86,11 +86,10 @@ export class PromptsManager { } // if slidedown not configured, condition met with native options, & should force slidedown over native: - const isPushSlidedownConfigured = - !!PromptsHelper.getFirstSlidedownPromptOptionsWithType( - userPromptOptions?.slidedown?.prompts, - DelayedPromptType.Push, - ); + const isPushSlidedownConfigured = !!getFirstSlidedownPromptOptionsWithType( + userPromptOptions?.slidedown?.prompts, + DelayedPromptType.Push, + ); if (forceSlidedownWithNativeOptions && !isPushSlidedownConfigured) { this.internalShowDelayedPrompt( @@ -263,10 +262,7 @@ export class PromptsManager { this.context.appConfig.userConfig.promptOptions?.slidedown?.prompts; const slidedownPromptOptions = options?.slidedownPromptOptions || - PromptsHelper.getFirstSlidedownPromptOptionsWithType( - prompts, - typeToPullFromConfig, - ); + getFirstSlidedownPromptOptionsWithType(prompts, typeToPullFromConfig); if (!slidedownPromptOptions) { if (typeToPullFromConfig !== DelayedPromptType.Push) { @@ -374,7 +370,7 @@ export class PromptsManager { case DelayedPromptType.Sms: case DelayedPromptType.SmsAndEmail: { const { userConfig } = this.context.appConfig; - const options = PromptsHelper.getFirstSlidedownPromptOptionsWithType( + const options = getFirstSlidedownPromptOptionsWithType( userConfig.promptOptions?.slidedown?.prompts || [], type, ); diff --git a/src/page/managers/slidedownManager/SlidedownManager.ts b/src/page/managers/slidedownManager/SlidedownManager.ts index d8f53a340..8d848099c 100644 --- a/src/page/managers/slidedownManager/SlidedownManager.ts +++ b/src/page/managers/slidedownManager/SlidedownManager.ts @@ -3,6 +3,7 @@ import { delay } from 'src/shared/helpers/general'; import { CONFIG_DEFAULTS_SLIDEDOWN_OPTIONS, DelayedPromptType, + isSlidedownPushDependent, type DelayedPromptTypeValue, } from 'src/shared/prompts'; import { CoreModuleDirector } from '../../../core/CoreModuleDirector'; @@ -21,7 +22,6 @@ import PushPermissionNotGrantedError, { } from '../../../shared/errors/PushPermissionNotGrantedError'; import { DismissHelper } from '../../../shared/helpers/DismissHelper'; import InitHelper from '../../../shared/helpers/InitHelper'; -import PromptsHelper from '../../../shared/helpers/PromptsHelper'; import Log from '../../../shared/libraries/Log'; import { NotificationPermission } from '../../../shared/models/NotificationPermission'; import type { PushSubscriptionState } from '../../../shared/models/PushSubscriptionState'; @@ -67,15 +67,14 @@ export class SlidedownManager { const slidedownType = options.slidedownPromptOptions?.type; - let isSlidedownPushDependent = false; + let _isSlidedownPushDependent = false; if (!!slidedownType) { - isSlidedownPushDependent = - PromptsHelper.isSlidedownPushDependent(slidedownType); + _isSlidedownPushDependent = isSlidedownPushDependent(slidedownType); } // applies to both push and category slidedown types - if (isSlidedownPushDependent) { + if (_isSlidedownPushDependent) { if (subscribed) { // applies to category slidedown type only if (options.isInUpdateMode) { @@ -414,7 +413,7 @@ export class SlidedownManager { if (this.slidedown) { this.slidedown.close(); - if (!PromptsHelper.isSlidedownPushDependent(slidedownType)) { + if (!isSlidedownPushDependent(slidedownType)) { await this.showConfirmationToast(); } // timeout to allow slidedown close animation to finish in case another slidedown is queued diff --git a/src/page/slidedown/Slidedown.ts b/src/page/slidedown/Slidedown.ts index 5ea1b6df4..10bf4597d 100755 --- a/src/page/slidedown/Slidedown.ts +++ b/src/page/slidedown/Slidedown.ts @@ -7,9 +7,9 @@ import { } from 'src/shared/helpers/dom'; import { getValueOrDefault } from 'src/shared/helpers/general'; import MainHelper from 'src/shared/helpers/MainHelper'; -import PromptsHelper from 'src/shared/helpers/PromptsHelper'; import { DelayedPromptType, + isSlidedownPushDependent, SERVER_CONFIG_DEFAULTS_SLIDEDOWN, type SlidedownPromptOptions, } from 'src/shared/prompts'; @@ -279,7 +279,7 @@ export default class Slidedown { removeDomElement('#onesignal-button-indicator-holder'); removeCssClass(this.allowButton, 'onesignal-error-state-button'); - if (!PromptsHelper.isSlidedownPushDependent(this.options.type)) { + if (!isSlidedownPushDependent(this.options.type)) { ChannelCaptureContainer.resetInputErrorStates(this.options.type); } diff --git a/src/shared/helpers/EventHelper.ts b/src/shared/helpers/EventHelper.ts deleted file mode 100755 index 7c2defcde..000000000 --- a/src/shared/helpers/EventHelper.ts +++ /dev/null @@ -1,358 +0,0 @@ -import UserNamespace from '../../onesignal/UserNamespace'; -import type { SubscriptionChangeEvent } from '../../page/models/SubscriptionChangeEvent'; -import type { UserChangeEvent } from '../../page/models/UserChangeEvent'; -import Log from '../libraries/Log'; -import { CustomLinkManager } from '../managers/CustomLinkManager'; -import type { ContextSWInterface } from '../models/ContextSW'; -import type { - NotificationClickEvent, - NotificationClickEventInternal, -} from '../models/NotificationEvent'; -import Database from '../services/Database'; -import LimitStore from '../services/LimitStore'; -import OneSignalEvent from '../services/OneSignalEvent'; -import BrowserUtils from '../utils/BrowserUtils'; -import OneSignalUtils from '../utils/OneSignalUtils'; -import { awaitOneSignalInitAndSupported } from '../utils/utils'; -import MainHelper from './MainHelper'; -import PromptsHelper from './PromptsHelper'; - -export default class EventHelper { - static onNotificationPermissionChange() { - EventHelper.checkAndTriggerSubscriptionChanged(); - } - - static async onInternalSubscriptionSet(optedOut: boolean) { - LimitStore.put('subscription.optedOut', optedOut); - } - - static async checkAndTriggerSubscriptionChanged() { - OneSignalUtils.logMethodCall('checkAndTriggerSubscriptionChanged'); - const context: ContextSWInterface = OneSignal.context; - // isPushEnabled = subscribed && is not opted out - const isPushEnabled: boolean = - await OneSignal.context.subscriptionManager.isPushNotificationsEnabled(); - // isOptedIn = native permission granted && is not opted out - const isOptedIn: boolean = - await OneSignal.context.subscriptionManager.isOptedIn(); - - const appState = await Database.getAppState(); - const { - lastKnownPushEnabled, - lastKnownPushId, - lastKnownPushToken, - lastKnownOptedIn, - } = appState; - - const currentPushToken = await MainHelper.getCurrentPushToken(); - - const pushModel = await OneSignal.coreDirector.getPushSubscriptionModel(); - const pushSubscriptionId = pushModel?.id; - - const didStateChange = - lastKnownPushEnabled === null || - isPushEnabled !== lastKnownPushEnabled || - currentPushToken !== lastKnownPushToken || - pushSubscriptionId !== lastKnownPushId; - - if (!didStateChange) { - return; - } - - // update notification_types via core module - await context.subscriptionManager.updateNotificationTypes(); - - appState.lastKnownPushEnabled = isPushEnabled; - appState.lastKnownPushToken = currentPushToken; - appState.lastKnownPushId = pushSubscriptionId; - appState.lastKnownOptedIn = isOptedIn; - await Database.setAppState(appState); - - const change: SubscriptionChangeEvent = { - previous: { - id: lastKnownPushId, - token: lastKnownPushToken, - // default to true if not stored yet - optedIn: lastKnownOptedIn ?? true, - }, - current: { - id: pushSubscriptionId, - token: currentPushToken, - optedIn: isOptedIn, - }, - }; - Log.info('Push Subscription state changed: ', change); - EventHelper.triggerSubscriptionChanged(change); - } - - static async _onSubscriptionChanged( - change: SubscriptionChangeEvent | undefined, - ) { - EventHelper.onSubscriptionChanged_showWelcomeNotification( - change?.current?.optedIn, - change?.current?.id, - ); - EventHelper.onSubscriptionChanged_sendCategorySlidedownTags( - change?.current?.optedIn, - ); - EventHelper.onSubscriptionChanged_evaluateNotifyButtonDisplayPredicate(); - EventHelper.onSubscriptionChanged_updateCustomLink(); - } - - private static async onSubscriptionChanged_sendCategorySlidedownTags( - isSubscribed?: boolean | null, - ) { - if (isSubscribed !== true) { - return; - } - - const prompts = - OneSignal.context.appConfig.userConfig.promptOptions?.slidedown?.prompts; - if (PromptsHelper.isCategorySlidedownConfigured(prompts)) { - await OneSignal.context.tagManager.sendTags(); - } - } - - /** - * NOTE: This uses the OneSignal REST API POST /notifications with - * include_player_ids. This field will be dropped by 2025 so a - * replacement will needed by then. - */ - private static async onSubscriptionChanged_showWelcomeNotification( - isSubscribed: boolean | undefined, - pushSubscriptionId: string | undefined | null, - ) { - if (OneSignal.__doNotShowWelcomeNotification) { - Log.debug( - 'Not showing welcome notification because user has previously subscribed.', - ); - return; - } - const welcome_notification_opts = - OneSignal.config?.userConfig.welcomeNotification; - const welcome_notification_disabled = - welcome_notification_opts !== undefined && - welcome_notification_opts['disable'] === true; - - if (welcome_notification_disabled) { - return; - } - - if (isSubscribed !== true) { - return; - } - - if (!pushSubscriptionId) { - return; - } - - let title = - welcome_notification_opts !== undefined && - welcome_notification_opts['title'] !== undefined && - welcome_notification_opts['title'] !== null - ? welcome_notification_opts['title'] - : ''; - let message = - welcome_notification_opts !== undefined && - welcome_notification_opts['message'] !== undefined && - welcome_notification_opts['message'] !== null && - welcome_notification_opts['message'].length > 0 - ? welcome_notification_opts['message'] - : 'Thanks for subscribing!'; - const unopenableWelcomeNotificationUrl = - new URL(location.href).origin + '?_osp=do_not_open'; - const url = - welcome_notification_opts && - welcome_notification_opts['url'] && - welcome_notification_opts['url'].length > 0 - ? welcome_notification_opts['url'] - : unopenableWelcomeNotificationUrl; - title = BrowserUtils.decodeHtmlEntities(title); - message = BrowserUtils.decodeHtmlEntities(message); - - Log.debug('Sending welcome notification.'); - MainHelper.showLocalNotification( - title, - message, - url, - undefined, - { __isOneSignalWelcomeNotification: true }, - undefined, - ); - OneSignalEvent.trigger(OneSignal.EVENTS.WELCOME_NOTIFICATION_SENT, { - title: title, - message: message, - url: url, - }); - } - - private static async onSubscriptionChanged_evaluateNotifyButtonDisplayPredicate() { - if (!OneSignal.config?.userConfig.notifyButton) return; - - const displayPredicate = - OneSignal.config.userConfig.notifyButton.displayPredicate; - if ( - displayPredicate && - typeof displayPredicate === 'function' && - OneSignal.notifyButton - ) { - const predicateResult = await displayPredicate(); - if (predicateResult !== false) { - Log.debug( - 'Showing notify button because display predicate returned true.', - ); - OneSignal.notifyButton.launcher.show(); - } else { - Log.debug( - 'Hiding notify button because display predicate returned false.', - ); - OneSignal.notifyButton.launcher.hide(); - } - } - } - - private static async onSubscriptionChanged_updateCustomLink() { - if (OneSignal.config?.userConfig.promptOptions) { - new CustomLinkManager( - OneSignal.config?.userConfig.promptOptions.customlink, - ).initialize(); - } - } - - static triggerSubscriptionChanged(change: SubscriptionChangeEvent) { - OneSignalEvent.trigger(OneSignal.EVENTS.SUBSCRIPTION_CHANGED, change); - } - - static triggerUserChanged(change: UserChangeEvent) { - OneSignalEvent.trigger( - OneSignal.EVENTS.SUBSCRIPTION_CHANGED, - change, - UserNamespace.emitter, - ); - } - - static triggerNotificationClick( - event: NotificationClickEventInternal, - ): Promise { - const publicEvent: NotificationClickEvent = { - notification: event.notification, - result: event.result, - }; - return OneSignalEvent.trigger( - OneSignal.EVENTS.NOTIFICATION_CLICKED, - publicEvent, - ); - } - - /** - * When notifications are clicked, because the site isn't open, the notification is stored in the database. The next - * time the page opens, the event is triggered if its less than 5 minutes (usually page opens instantly from click). - */ - static async fireStoredNotificationClicks() { - await awaitOneSignalInitAndSupported(); - const url = - OneSignal.config?.pageUrl || - OneSignal.config?.userConfig.pageUrl || - document.URL; - - async function fireEventWithNotification( - selectedEvent: NotificationClickEventInternal, - ) { - // Remove the notification from the recently clicked list - // Once this page processes this retroactively provided clicked event, nothing should get the same event - const appState = await Database.getAppState(); - // @ts-expect-error - TODO: address this is a workaround to fix the type error - appState.pendingNotificationClickEvents![selectedEvent.result.url!] = - null; - await Database.setAppState(appState); - - const timestamp = selectedEvent.timestamp; - if (timestamp) { - const minutesSinceNotificationClicked = - (Date.now() - timestamp) / 1000 / 60; - if (minutesSinceNotificationClicked > 5) return; - } - - EventHelper.triggerNotificationClick(selectedEvent); - } - - const appState = await Database.getAppState(); - - /* Is the flag notificationClickHandlerMatch: origin enabled? - - If so, this means we should provide a retroactive notification.clicked event as long as there exists any recently clicked - notification that matches this site's origin. - - Otherwise, the default behavior is to only provide a retroactive notification.clicked event if this page's URL exactly - matches the notification's URL. - */ - const notificationClickHandlerMatch = await Database.get( - 'Options', - 'notificationClickHandlerMatch', - ); - if (notificationClickHandlerMatch === 'origin') { - for (const clickedNotificationUrl of Object.keys( - appState.pendingNotificationClickEvents!, - )) { - // Using notificationClickHandlerMatch: 'origin', as long as the notification's URL's origin matches our current tab's origin, - // fire the clicked event - if (new URL(clickedNotificationUrl).origin === location.origin) { - const clickedNotification = - appState.pendingNotificationClickEvents![clickedNotificationUrl]; - await fireEventWithNotification(clickedNotification); - } - } - } else { - /* - If a user is on https://site.com, document.URL and location.href both report the page's URL as https://site.com/. - This causes checking for notifications for the current URL to fail, since there is a notification for https://site.com, - but there is no notification for https://site.com/. - - As a workaround, if there are no notifications for https://site.com/, we'll do a check for https://site.com. - */ - let pageClickedNotifications = - appState.pendingNotificationClickEvents?.[url]; - if (pageClickedNotifications) { - await fireEventWithNotification(pageClickedNotifications); - } else if (!pageClickedNotifications && url.endsWith('/')) { - const urlWithoutTrailingSlash = url.substring(0, url.length - 1); - pageClickedNotifications = - appState.pendingNotificationClickEvents?.[urlWithoutTrailingSlash]; - if (pageClickedNotifications) { - await fireEventWithNotification(pageClickedNotifications); - } - } - } - } - - static async checkAndTriggerUserChanged() { - OneSignalUtils.logMethodCall('checkAndTriggerUserChanged'); - - const userState = await Database.getUserState(); - const { previousOneSignalId, previousExternalId } = userState; - - const identityModel = await OneSignal.coreDirector.getIdentityModel(); - const currentOneSignalId = identityModel?.onesignalId; - const currentExternalId = identityModel?.externalId; - - const didStateChange = - currentOneSignalId !== previousOneSignalId || - currentExternalId !== previousExternalId; - if (!didStateChange) { - return; - } - - userState.previousOneSignalId = currentOneSignalId; - userState.previousExternalId = currentExternalId; - await Database.setUserState(userState); - - const change: UserChangeEvent = { - current: { - onesignalId: currentOneSignalId, - externalId: currentExternalId, - }, - }; - Log.info('User state changed: ', change); - EventHelper.triggerUserChanged(change); - } -} diff --git a/src/shared/helpers/PromptsHelper.ts b/src/shared/helpers/PromptsHelper.ts deleted file mode 100644 index 15da5fa2f..000000000 --- a/src/shared/helpers/PromptsHelper.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { - DelayedPromptType, - type DelayedPromptTypeValue, - type SlidedownPromptOptions, -} from '../prompts'; - -export default class PromptsHelper { - static isCategorySlidedownConfigured( - prompts?: SlidedownPromptOptions[], - ): boolean { - if (!prompts) return false; - - const options = PromptsHelper.getFirstSlidedownPromptOptionsWithType( - prompts, - DelayedPromptType.Category, - ); - if (!!options) { - return !!options.categories && options.categories.length > 0; - } - return false; - } - - static getFirstSlidedownPromptOptionsWithType( - prompts: SlidedownPromptOptions[] | undefined, - type: DelayedPromptTypeValue, - ): SlidedownPromptOptions | undefined { - return prompts - ? prompts.filter((options) => options.type === type)[0] - : undefined; - } - - static isSlidedownPushDependent( - slidedownType: DelayedPromptTypeValue, - ): boolean { - return ( - slidedownType === DelayedPromptType.Push || - slidedownType === DelayedPromptType.Category - ); - } -} diff --git a/src/shared/helpers/SubscriptionHelper.ts b/src/shared/helpers/SubscriptionHelper.ts index efce06d42..9288069dd 100755 --- a/src/shared/helpers/SubscriptionHelper.ts +++ b/src/shared/helpers/SubscriptionHelper.ts @@ -5,11 +5,11 @@ import { type SubscriptionTypeValue, } from 'src/core/types/subscription'; import Log from '../libraries/Log'; +import { checkAndTriggerSubscriptionChanged } from '../listeners'; import { Subscription } from '../models/Subscription'; import { SubscriptionStrategyKind } from '../models/SubscriptionStrategyKind'; import { IS_SERVICE_WORKER } from '../utils/EnvVariables'; import { PermissionUtils } from '../utils/PermissionUtils'; -import EventHelper from './EventHelper'; export default class SubscriptionHelper { public static async registerForPush(): Promise { @@ -29,7 +29,7 @@ export default class SubscriptionHelper { await context.subscriptionManager.registerSubscription(rawSubscription); context.pageViewManager.incrementPageViewCount(); await PermissionUtils.triggerNotificationPermissionChanged(); - await EventHelper.checkAndTriggerSubscriptionChanged(); + await checkAndTriggerSubscriptionChanged(); } catch (e) { Log.error(e); } diff --git a/src/shared/helpers/dom.ts b/src/shared/helpers/dom.ts index ecf1e61c2..54a6a68f4 100644 --- a/src/shared/helpers/dom.ts +++ b/src/shared/helpers/dom.ts @@ -127,3 +127,11 @@ export function hasCssClass( ); } } + +export function decodeHtmlEntities(text: string): string { + if (typeof DOMParser === 'undefined') { + return text; + } + const doc = new DOMParser().parseFromString(text, 'text/html'); + return doc.documentElement.textContent || ''; +} diff --git a/src/shared/helpers/general.ts b/src/shared/helpers/general.ts index 1a55072dd..cd26f995e 100644 --- a/src/shared/helpers/general.ts +++ b/src/shared/helpers/general.ts @@ -18,6 +18,10 @@ export function delay(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)); } +export function nothing(): Promise { + return Promise.resolve(); +} + type Nullable = undefined | null; export function valueOrDefault(value: T | Nullable, defaultValue: T): T { if (typeof value === 'undefined' || value === null) { diff --git a/src/shared/listeners/index.ts b/src/shared/listeners/index.ts new file mode 100644 index 000000000..52f405aef --- /dev/null +++ b/src/shared/listeners/index.ts @@ -0,0 +1 @@ +export * from './listeners'; diff --git a/__test__/unit/notifications/eventListeners.test.ts b/src/shared/listeners/listeners.test.ts similarity index 58% rename from __test__/unit/notifications/eventListeners.test.ts rename to src/shared/listeners/listeners.test.ts index b4a50bc23..ee693236c 100644 --- a/__test__/unit/notifications/eventListeners.test.ts +++ b/src/shared/listeners/listeners.test.ts @@ -1,7 +1,7 @@ -import EventHelper from '../../../src/shared/helpers/EventHelper'; -import { TestEnvironment } from '../../support/environment/TestEnvironment'; +import { TestEnvironment } from '__test__/support/environment/TestEnvironment'; +import * as eventListeners from 'src/shared/listeners'; -describe('Notification Events', () => { +describe('Notification Listeners', () => { beforeEach(async () => { await TestEnvironment.initialize(); }); @@ -11,7 +11,7 @@ describe('Notification Events', () => { }); test('Adding click listener fires internal EventHelper', async () => { - const stub = vi.spyOn(EventHelper, 'fireStoredNotificationClicks'); + const stub = vi.spyOn(eventListeners, 'fireStoredNotificationClicks'); // @ts-expect-error - listener doesnt matter OneSignal.Notifications.addEventListener('click', null); expect(stub).toHaveBeenCalledTimes(1); diff --git a/src/shared/listeners/listeners.ts b/src/shared/listeners/listeners.ts new file mode 100644 index 000000000..587f44912 --- /dev/null +++ b/src/shared/listeners/listeners.ts @@ -0,0 +1,347 @@ +import UserNamespace from 'src/onesignal/UserNamespace'; +import type { SubscriptionChangeEvent } from 'src/page/models/SubscriptionChangeEvent'; +import type { UserChangeEvent } from 'src/page/models/UserChangeEvent'; +import { decodeHtmlEntities } from '../helpers/dom'; +import MainHelper from '../helpers/MainHelper'; +import Log from '../libraries/Log'; +import { CustomLinkManager } from '../managers/CustomLinkManager'; +import type { ContextSWInterface } from '../models/ContextSW'; +import type { + NotificationClickEvent, + NotificationClickEventInternal, +} from '../models/NotificationEvent'; +import { isCategorySlidedownConfigured } from '../prompts'; +import Database from '../services/Database'; +import LimitStore from '../services/LimitStore'; +import OneSignalEvent from '../services/OneSignalEvent'; +import OneSignalUtils from '../utils/OneSignalUtils'; +import { awaitOneSignalInitAndSupported } from '../utils/utils'; + +export async function checkAndTriggerSubscriptionChanged() { + OneSignalUtils.logMethodCall('checkAndTriggerSubscriptionChanged'); + const context: ContextSWInterface = OneSignal.context; + // isPushEnabled = subscribed && is not opted out + const isPushEnabled: boolean = + await OneSignal.context.subscriptionManager.isPushNotificationsEnabled(); + // isOptedIn = native permission granted && is not opted out + const isOptedIn: boolean = + await OneSignal.context.subscriptionManager.isOptedIn(); + + const appState = await Database.getAppState(); + const { + lastKnownPushEnabled, + lastKnownPushId, + lastKnownPushToken, + lastKnownOptedIn, + } = appState; + + const currentPushToken = await MainHelper.getCurrentPushToken(); + + const pushModel = await OneSignal.coreDirector.getPushSubscriptionModel(); + const pushSubscriptionId = pushModel?.id; + + const didStateChange = + lastKnownPushEnabled === null || + isPushEnabled !== lastKnownPushEnabled || + currentPushToken !== lastKnownPushToken || + pushSubscriptionId !== lastKnownPushId; + + if (!didStateChange) { + return; + } + + // update notification_types via core module + await context.subscriptionManager.updateNotificationTypes(); + + appState.lastKnownPushEnabled = isPushEnabled; + appState.lastKnownPushToken = currentPushToken; + appState.lastKnownPushId = pushSubscriptionId; + appState.lastKnownOptedIn = isOptedIn; + await Database.setAppState(appState); + + const change: SubscriptionChangeEvent = { + previous: { + id: lastKnownPushId, + token: lastKnownPushToken, + // default to true if not stored yet + optedIn: lastKnownOptedIn ?? true, + }, + current: { + id: pushSubscriptionId, + token: currentPushToken, + optedIn: isOptedIn, + }, + }; + Log.info('Push Subscription state changed: ', change); + triggerSubscriptionChanged(change); +} + +function triggerSubscriptionChanged(change: SubscriptionChangeEvent) { + OneSignalEvent.trigger(OneSignal.EVENTS.SUBSCRIPTION_CHANGED, change); +} + +export function triggerNotificationClick( + event: NotificationClickEventInternal, +): Promise { + const publicEvent: NotificationClickEvent = { + notification: event.notification, + result: event.result, + }; + return OneSignalEvent.trigger( + OneSignal.EVENTS.NOTIFICATION_CLICKED, + publicEvent, + ); +} + +export async function checkAndTriggerUserChanged() { + OneSignalUtils.logMethodCall('checkAndTriggerUserChanged'); + + const userState = await Database.getUserState(); + const { previousOneSignalId, previousExternalId } = userState; + + const identityModel = await OneSignal.coreDirector.getIdentityModel(); + const currentOneSignalId = identityModel?.onesignalId; + const currentExternalId = identityModel?.externalId; + + const didStateChange = + currentOneSignalId !== previousOneSignalId || + currentExternalId !== previousExternalId; + if (!didStateChange) { + return; + } + + userState.previousOneSignalId = currentOneSignalId; + userState.previousExternalId = currentExternalId; + await Database.setUserState(userState); + + const change: UserChangeEvent = { + current: { + onesignalId: currentOneSignalId, + externalId: currentExternalId, + }, + }; + Log.info('User state changed: ', change); + triggerUserChanged(change); +} + +function triggerUserChanged(change: UserChangeEvent) { + OneSignalEvent.trigger( + OneSignal.EVENTS.SUBSCRIPTION_CHANGED, + change, + UserNamespace.emitter, + ); +} + +/** + * When notifications are clicked, because the site isn't open, the notification is stored in the database. The next + * time the page opens, the event is triggered if its less than 5 minutes (usually page opens instantly from click). + */ +export async function fireStoredNotificationClicks() { + await awaitOneSignalInitAndSupported(); + const url = + OneSignal.config?.pageUrl || + OneSignal.config?.userConfig.pageUrl || + document.URL; + + async function fireEventWithNotification( + selectedEvent: NotificationClickEventInternal, + ) { + // Remove the notification from the recently clicked list + // Once this page processes this retroactively provided clicked event, nothing should get the same event + const appState = await Database.getAppState(); + // @ts-expect-error - TODO: address this is a workaround to fix the type error + appState.pendingNotificationClickEvents![selectedEvent.result.url!] = null; + await Database.setAppState(appState); + + const timestamp = selectedEvent.timestamp; + if (timestamp) { + const minutesSinceNotificationClicked = + (Date.now() - timestamp) / 1000 / 60; + if (minutesSinceNotificationClicked > 5) return; + } + + triggerNotificationClick(selectedEvent); + } + + const appState = await Database.getAppState(); + + /* Is the flag notificationClickHandlerMatch: origin enabled? + + If so, this means we should provide a retroactive notification.clicked event as long as there exists any recently clicked + notification that matches this site's origin. + + Otherwise, the default behavior is to only provide a retroactive notification.clicked event if this page's URL exactly + matches the notification's URL. + */ + const notificationClickHandlerMatch = await Database.get( + 'Options', + 'notificationClickHandlerMatch', + ); + if (notificationClickHandlerMatch === 'origin') { + for (const clickedNotificationUrl of Object.keys( + appState.pendingNotificationClickEvents!, + )) { + // Using notificationClickHandlerMatch: 'origin', as long as the notification's URL's origin matches our current tab's origin, + // fire the clicked event + if (new URL(clickedNotificationUrl).origin === location.origin) { + const clickedNotification = + appState.pendingNotificationClickEvents![clickedNotificationUrl]; + await fireEventWithNotification(clickedNotification); + } + } + } else { + /* + If a user is on https://site.com, document.URL and location.href both report the page's URL as https://site.com/. + This causes checking for notifications for the current URL to fail, since there is a notification for https://site.com, + but there is no notification for https://site.com/. + + As a workaround, if there are no notifications for https://site.com/, we'll do a check for https://site.com. + */ + let pageClickedNotifications = + appState.pendingNotificationClickEvents?.[url]; + if (pageClickedNotifications) { + await fireEventWithNotification(pageClickedNotifications); + } else if (!pageClickedNotifications && url.endsWith('/')) { + const urlWithoutTrailingSlash = url.substring(0, url.length - 1); + pageClickedNotifications = + appState.pendingNotificationClickEvents?.[urlWithoutTrailingSlash]; + if (pageClickedNotifications) { + await fireEventWithNotification(pageClickedNotifications); + } + } + } +} + +async function onSubscriptionChanged_evaluateNotifyButtonDisplayPredicate() { + if (!OneSignal.config?.userConfig.notifyButton) return; + + const displayPredicate = + OneSignal.config.userConfig.notifyButton.displayPredicate; + if ( + displayPredicate && + typeof displayPredicate === 'function' && + OneSignal.notifyButton + ) { + const predicateResult = await displayPredicate(); + if (predicateResult !== false) { + Log.debug( + 'Showing notify button because display predicate returned true.', + ); + OneSignal.notifyButton.launcher.show(); + } else { + Log.debug( + 'Hiding notify button because display predicate returned false.', + ); + OneSignal.notifyButton.launcher.hide(); + } + } +} + +function onSubscriptionChanged_updateCustomLink() { + if (OneSignal.config?.userConfig.promptOptions) { + new CustomLinkManager( + OneSignal.config?.userConfig.promptOptions.customlink, + ).initialize(); + } +} + +export async function onInternalSubscriptionSet(optedOut: boolean) { + LimitStore.put('subscription.optedOut', optedOut); +} + +/** + * NOTE: This uses the OneSignal REST API POST /notifications with + * include_player_ids. This field will be dropped by 2025 so a + * replacement will needed by then. + */ +async function onSubscriptionChanged_showWelcomeNotification( + isSubscribed: boolean | undefined, + pushSubscriptionId: string | undefined | null, +) { + if (OneSignal.__doNotShowWelcomeNotification) { + Log.debug( + 'Not showing welcome notification because user has previously subscribed.', + ); + return; + } + const welcome_notification_opts = + OneSignal.config?.userConfig.welcomeNotification; + const welcome_notification_disabled = + welcome_notification_opts !== undefined && + welcome_notification_opts['disable'] === true; + + if (welcome_notification_disabled) { + return; + } + + if (isSubscribed !== true) { + return; + } + + if (!pushSubscriptionId) { + return; + } + + let title = + welcome_notification_opts !== undefined && + welcome_notification_opts['title'] !== undefined && + welcome_notification_opts['title'] !== null + ? welcome_notification_opts['title'] + : ''; + let message = + welcome_notification_opts !== undefined && + welcome_notification_opts['message'] !== undefined && + welcome_notification_opts['message'] !== null && + welcome_notification_opts['message'].length > 0 + ? welcome_notification_opts['message'] + : 'Thanks for subscribing!'; + const unopenableWelcomeNotificationUrl = + new URL(location.href).origin + '?_osp=do_not_open'; + const url = + welcome_notification_opts && + welcome_notification_opts['url'] && + welcome_notification_opts['url'].length > 0 + ? welcome_notification_opts['url'] + : unopenableWelcomeNotificationUrl; + title = decodeHtmlEntities(title); + message = decodeHtmlEntities(message); + + Log.debug('Sending welcome notification.'); + MainHelper.showLocalNotification( + title, + message, + url, + undefined, + { __isOneSignalWelcomeNotification: true }, + undefined, + ); + OneSignalEvent.trigger(OneSignal.EVENTS.WELCOME_NOTIFICATION_SENT, { + title: title, + message: message, + url: url, + }); +} + +async function onSubscriptionChanged_sendCategorySlidedownTags( + isSubscribed?: boolean | null, +) { + if (isSubscribed !== true) return; + + const prompts = + OneSignal.context.appConfig.userConfig.promptOptions?.slidedown?.prompts; + if (isCategorySlidedownConfigured(prompts)) { + await OneSignal.context.tagManager.sendTags(); + } +} + +export function _onSubscriptionChanged( + change: SubscriptionChangeEvent | undefined, +) { + onSubscriptionChanged_showWelcomeNotification( + change?.current?.optedIn, + change?.current?.id, + ); + onSubscriptionChanged_sendCategorySlidedownTags(change?.current?.optedIn); + onSubscriptionChanged_evaluateNotifyButtonDisplayPredicate(); + onSubscriptionChanged_updateCustomLink(); +} diff --git a/src/shared/managers/ServiceWorkerManager.ts b/src/shared/managers/ServiceWorkerManager.ts index 9cd2abb93..1aa8c0dad 100644 --- a/src/shared/managers/ServiceWorkerManager.ts +++ b/src/shared/managers/ServiceWorkerManager.ts @@ -1,8 +1,7 @@ -import ServiceWorkerUtilHelper from '../../sw/helpers/ServiceWorkerUtilHelper'; +import ServiceWorkerUtilHelper from 'src/sw/helpers/ServiceWorkerUtilHelper'; import { Utils } from '../context/Utils'; import { supportsServiceWorkers } from '../environment/environment'; import ServiceWorkerRegistrationError from '../errors/ServiceWorkerRegistrationError'; -import EventHelper from '../helpers/EventHelper'; import ServiceWorkerHelper, { ServiceWorkerActiveState, type ServiceWorkerActiveStateValue, @@ -10,6 +9,7 @@ import ServiceWorkerHelper, { } from '../helpers/ServiceWorkerHelper'; import Log from '../libraries/Log'; import { WorkerMessengerCommand } from '../libraries/WorkerMessenger'; +import { triggerNotificationClick } from '../listeners'; import type { ContextSWInterface } from '../models/ContextSW'; import { type NotificationClickEventInternal, @@ -316,7 +316,7 @@ export class ServiceWorkerManager { } await Database.putNotificationClickedEventPendingUrlOpening(event); } else { - await EventHelper.triggerNotificationClick(event); + await triggerNotificationClick(event); } }, ); diff --git a/src/shared/managers/SubscriptionManager.ts b/src/shared/managers/SubscriptionManager.ts index d094913b4..ef8372082 100644 --- a/src/shared/managers/SubscriptionManager.ts +++ b/src/shared/managers/SubscriptionManager.ts @@ -45,7 +45,7 @@ import { Browser, getBrowserName } from '../useragent'; import { base64ToUint8Array } from '../utils/Encoding'; import { IS_SERVICE_WORKER } from '../utils/EnvVariables'; import { PermissionUtils } from '../utils/PermissionUtils'; -import { executeCallback, logMethodCall } from '../utils/utils'; +import { logMethodCall } from '../utils/utils'; import { IDManager } from './IDManager'; export const DEFAULT_DEVICE_ID = '99999999-9999-9999-9999-999999999999'; @@ -99,6 +99,13 @@ export const updatePushSubscriptionModelWithRawSubscription = async ( } }; +function executeCallback(callback?: (...args: any[]) => T, ...args: any[]) { + if (callback) { + // eslint-disable-next-line prefer-spread + return callback.apply(null, args); + } +} + export class SubscriptionManager { private context: ContextSWInterface; private config: SubscriptionManagerConfig; diff --git a/src/shared/prompts/index.ts b/src/shared/prompts/index.ts index 4fbce9564..c5ffa8b79 100644 --- a/src/shared/prompts/index.ts +++ b/src/shared/prompts/index.ts @@ -1,2 +1,3 @@ export * from './constants'; +export * from './prompts'; export * from './types'; diff --git a/src/shared/helpers/PromptsHelper.test.ts b/src/shared/prompts/prompts.test.ts similarity index 81% rename from src/shared/helpers/PromptsHelper.test.ts rename to src/shared/prompts/prompts.test.ts index 41dcc9959..df86ede95 100644 --- a/src/shared/helpers/PromptsHelper.test.ts +++ b/src/shared/prompts/prompts.test.ts @@ -1,10 +1,10 @@ import { DelayedPromptType, + getFirstSlidedownPromptOptionsWithType, type SlidedownPromptOptions, } from 'src/shared/prompts'; -import PromptsHelper from './PromptsHelper'; -describe('PromptsHelper', () => { +describe('Prompt Helpers', () => { test('should return true if the category slidedown is configured', () => { const prompts: SlidedownPromptOptions[] = [ { @@ -29,14 +29,14 @@ describe('PromptsHelper', () => { ]; // should return matching prompt - const result = PromptsHelper.getFirstSlidedownPromptOptionsWithType( + const result = getFirstSlidedownPromptOptionsWithType( prompts, DelayedPromptType.Push, ); expect(result).toBe(prompts[1]); // if no prompts are provided, it should return undefined - const result2 = PromptsHelper.getFirstSlidedownPromptOptionsWithType( + const result2 = getFirstSlidedownPromptOptionsWithType( undefined, DelayedPromptType.Category, ); diff --git a/src/shared/prompts/prompts.ts b/src/shared/prompts/prompts.ts new file mode 100644 index 000000000..e9d22ec07 --- /dev/null +++ b/src/shared/prompts/prompts.ts @@ -0,0 +1,35 @@ +import { DelayedPromptType } from './constants'; +import type { DelayedPromptTypeValue, SlidedownPromptOptions } from './types'; + +export function isCategorySlidedownConfigured( + prompts?: SlidedownPromptOptions[], +): boolean { + if (!prompts) return false; + + const options = getFirstSlidedownPromptOptionsWithType( + prompts, + DelayedPromptType.Category, + ); + if (!!options) { + return !!options.categories && options.categories.length > 0; + } + return false; +} + +export function getFirstSlidedownPromptOptionsWithType( + prompts: SlidedownPromptOptions[] | undefined, + type: DelayedPromptTypeValue, +): SlidedownPromptOptions | undefined { + return prompts + ? prompts.filter((options) => options.type === type)[0] + : undefined; +} + +export function isSlidedownPushDependent( + slidedownType: DelayedPromptTypeValue, +): boolean { + return ( + slidedownType === DelayedPromptType.Push || + slidedownType === DelayedPromptType.Category + ); +} diff --git a/src/shared/utils/BrowserUtils.ts b/src/shared/utils/BrowserUtils.ts deleted file mode 100644 index 2398c02b8..000000000 --- a/src/shared/utils/BrowserUtils.ts +++ /dev/null @@ -1,13 +0,0 @@ -export class BrowserUtils { - // Decodes HTML encoded characters into their displayed value. - // Example: "<b>test</b>" becomes "test" - public static decodeHtmlEntities(text: string): string { - if (typeof DOMParser === 'undefined') { - return text; - } - const doc = new DOMParser().parseFromString(text, 'text/html'); - return doc.documentElement.textContent || ''; - } -} - -export default BrowserUtils; diff --git a/src/shared/utils/utils.ts b/src/shared/utils/utils.ts index 49276e88a..5f2142265 100755 --- a/src/shared/utils/utils.ts +++ b/src/shared/utils/utils.ts @@ -26,24 +26,10 @@ export async function triggerNotificationPermissionChanged( ); } -export function executeCallback( - callback?: (...args: any[]) => T, - ...args: any[] -) { - if (callback) { - // eslint-disable-next-line prefer-spread - return callback.apply(null, args); - } -} - export function logMethodCall(methodName: string, ...args: any[]) { return OneSignalUtils.logMethodCall(methodName, ...args); } -export function nothing(): Promise { - return Promise.resolve(); -} - /** * Returns true if match is in string; otherwise, returns false. */ From ee12190561231c6448afee404e6576577ff9c9f6 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Tue, 29 Jul 2025 15:43:33 -0700 Subject: [PATCH 06/12] add tests for browser name --- __test__/support/models/BrowserUserAgent.ts | 2 +- package-lock.json | 70 +++- src/shared/useragent/useragent.test.ts | 430 +++++++++++++------- src/shared/useragent/useragent.ts | 32 +- 4 files changed, 364 insertions(+), 170 deletions(-) diff --git a/__test__/support/models/BrowserUserAgent.ts b/__test__/support/models/BrowserUserAgent.ts index 7789fc29c..ff01bd396 100644 --- a/__test__/support/models/BrowserUserAgent.ts +++ b/__test__/support/models/BrowserUserAgent.ts @@ -19,7 +19,7 @@ const BrowserUserAgent = { FirefoxMobileSupported: 'Mozilla/5.0 (Android 4.4; Mobile; rv:44.0) Gecko/48.0 Firefox/48.0', FirefoxTabletSupported: - 'Mozilla/5.0 (Android 4.4; Mobile ; rv:44.0) Gecko/48.0 Firefox/48.0', + 'Mozilla/5.0 (Android 4.4; Tablet; rv:44.0) Gecko/48.0 Firefox/48.0', FirefoxWindowsUnSupported: 'Mozilla/5.0 (Windows NT x.y; WOW64; rv:44.0) Gecko/20100101 Firefox/44.0', FirefoxWindowsSupported: diff --git a/package-lock.json b/package-lock.json index 1ea8a13c5..86a2ad922 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2211,6 +2211,13 @@ "dev": true, "license": "MIT" }, + "node_modules/bowser": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz", + "integrity": "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==", + "dev": true, + "license": "MIT" + }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -2305,17 +2312,13 @@ } }, "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", "dev": true, "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, "engines": { - "node": ">=10" + "node": "^12.17.0 || ^14.13 || >=16.0.0" }, "funding": { "url": "https://github.com/chalk/chalk?sponsor=1" @@ -2874,6 +2877,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/eslint-formatter-pretty/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/eslint-rule-docs": { "version": "1.1.235", "resolved": "https://registry.npmjs.org/eslint-rule-docs/-/eslint-rule-docs-1.1.235.tgz", @@ -2908,6 +2928,23 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/eslint/node_modules/eslint-scope": { "version": "7.2.2", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", @@ -4012,6 +4049,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/log-symbols/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/loupe": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.0.tgz", diff --git a/src/shared/useragent/useragent.test.ts b/src/shared/useragent/useragent.test.ts index a64e1857e..61437a495 100644 --- a/src/shared/useragent/useragent.test.ts +++ b/src/shared/useragent/useragent.test.ts @@ -1,169 +1,291 @@ import BrowserUserAgent from '__test__/support/models/BrowserUserAgent'; -import { isMobileBrowser } from './useragent'; +import Bowser from 'bowser'; +import { Browser } from './constants'; +import { getBrowserName, isMobileBrowser, isTabletBrowser } from './useragent'; -describe('isMobileBrowser', () => { - let originalUserAgent: string; - - beforeEach(() => { - // Store original userAgent - originalUserAgent = navigator.userAgent; +const mockUserAgent = (userAgent: string) => { + Object.defineProperty(navigator, 'userAgent', { + value: userAgent, + writable: true, }); +}; - afterEach(() => { - // Restore original userAgent - Object.defineProperty(navigator, 'userAgent', { - value: originalUserAgent, - writable: true, - }); - }); +// using third party library to validate our simpler logic +const checkBrowserIsMobile = (userAgent: string) => { + const browser = Bowser.getParser(userAgent); + return browser.getPlatformType() === 'mobile'; +}; - const mockUserAgent = (userAgent: string) => { - Object.defineProperty(navigator, 'userAgent', { - value: userAgent, - writable: true, - }); - }; +const checkBrowserIsTablet = (userAgent: string) => { + const browser = Bowser.getParser(userAgent); + return browser.getPlatformType() === 'tablet'; +}; - describe('isMobileBrowser()', () => { - [ - { - userAgent: BrowserUserAgent.iPhone, - expected: true, - }, - { - userAgent: BrowserUserAgent.iPad, - expected: true, - }, - { - userAgent: BrowserUserAgent.iPod, - expected: true, - }, - { - userAgent: BrowserUserAgent.ChromeAndroidSupported, - expected: true, - }, - { - userAgent: BrowserUserAgent.FirefoxMobileSupported, - expected: true, - }, - { - userAgent: BrowserUserAgent.OperaAndroidSupported, - expected: true, - }, - { - userAgent: BrowserUserAgent.OperaMiniUnsupported, - expected: true, - }, - { - userAgent: BrowserUserAgent.SamsungBrowserSupported, - expected: true, - }, - { - userAgent: BrowserUserAgent.UcBrowserSupported, - expected: true, - }, - { - userAgent: BrowserUserAgent.FacebookBrowseriOS, - expected: true, - }, - { - userAgent: BrowserUserAgent.FacebookBrowserAndroid, - expected: true, - }, - { - userAgent: BrowserUserAgent.YandexMobileSupported, - expected: true, - }, - ].forEach(({ userAgent, expected }) => { - test(`should detect ${userAgent} as a mobile browser`, () => { - mockUserAgent(userAgent); - expect(isMobileBrowser()).toBe(expected); - }); +const checkBrowserName = (userAgent: string) => { + const browser = Bowser.getParser(userAgent); + const name = browser.getBrowser().name; + + switch (name) { + case 'Chrome': + return Browser.Chrome; + case 'Firefox': + return Browser.Firefox; + case 'Microsoft Edge': + return Browser.Edge; + case 'Safari': + return Browser.Safari; + default: + return Browser.Other; + } +}; + +describe.skip('isMobileBrowser()', () => { + [ + { + userAgent: BrowserUserAgent.iPhone, + expected: true, + }, + { + userAgent: BrowserUserAgent.iPad, + expected: false, + }, + { + userAgent: BrowserUserAgent.iPod, + expected: true, + }, + { + userAgent: BrowserUserAgent.ChromeAndroidSupported, + expected: true, + }, + { + userAgent: BrowserUserAgent.FirefoxMobileSupported, + expected: true, + }, + { + userAgent: BrowserUserAgent.OperaAndroidSupported, + expected: true, + }, + { + userAgent: BrowserUserAgent.OperaMiniUnsupported, + expected: true, + }, + { + userAgent: BrowserUserAgent.SamsungBrowserSupported, + expected: true, + }, + { + userAgent: BrowserUserAgent.UcBrowserSupported, + expected: true, + }, + { + userAgent: BrowserUserAgent.FacebookBrowseriOS, + expected: true, + }, + { + userAgent: BrowserUserAgent.FacebookBrowserAndroid, + expected: true, + }, + { + userAgent: BrowserUserAgent.YandexMobileSupported, + expected: true, + }, + ].forEach(({ userAgent, expected }) => { + test(`should detect as a mobile browser "${userAgent}"`, () => { + mockUserAgent(userAgent); + expect(checkBrowserIsMobile(userAgent)).toBe(expected); + expect(isMobileBrowser()).toBe(expected); }); + }); - [ - { - userAgent: BrowserUserAgent.Default, - expected: false, - }, - { - userAgent: BrowserUserAgent.ChromeWindowsSupported, - expected: false, - }, - { - userAgent: BrowserUserAgent.ChromeMacSupported, - expected: false, - }, - { - userAgent: BrowserUserAgent.ChromeLinuxSupported, - expected: false, - }, - { - userAgent: BrowserUserAgent.SafariSupportedMac, - expected: false, - }, - { - userAgent: BrowserUserAgent.FirefoxWindowsSupported, - expected: false, - }, - { - userAgent: BrowserUserAgent.FirefoxMacSupported, - expected: false, - }, - { - userAgent: BrowserUserAgent.FirefoxLinuxSupported, - expected: false, - }, - { - userAgent: BrowserUserAgent.EdgeSupported, - expected: false, - }, - { - userAgent: BrowserUserAgent.OperaDesktopSupported, - expected: false, - }, - { - userAgent: BrowserUserAgent.YandexDesktopSupportedHigh, - expected: false, - }, - { - userAgent: BrowserUserAgent.VivaldiWindowsSupported, - expected: false, - }, - ].forEach(({ userAgent, expected }) => { - test(`should detect ${userAgent} as a desktop browser`, () => { - mockUserAgent(userAgent); - expect(isMobileBrowser()).toBe(expected); - }); + [ + { + userAgent: BrowserUserAgent.Default, + expected: false, + }, + { + userAgent: BrowserUserAgent.ChromeWindowsSupported, + expected: false, + }, + { + userAgent: BrowserUserAgent.ChromeMacSupported, + expected: false, + }, + { + userAgent: BrowserUserAgent.ChromeLinuxSupported, + expected: false, + }, + { + userAgent: BrowserUserAgent.SafariSupportedMac, + expected: false, + }, + { + userAgent: BrowserUserAgent.FirefoxWindowsSupported, + expected: false, + }, + { + userAgent: BrowserUserAgent.FirefoxMacSupported, + expected: false, + }, + { + userAgent: BrowserUserAgent.FirefoxLinuxSupported, + expected: false, + }, + { + userAgent: BrowserUserAgent.EdgeSupported, + expected: false, + }, + { + userAgent: BrowserUserAgent.OperaDesktopSupported, + expected: false, + }, + { + userAgent: BrowserUserAgent.YandexDesktopSupportedHigh, + expected: false, + }, + { + userAgent: BrowserUserAgent.VivaldiWindowsSupported, + expected: false, + }, + ].forEach(({ userAgent, expected }) => { + test(`should detect ${userAgent} as a desktop browser`, () => { + mockUserAgent(userAgent); + expect(checkBrowserIsMobile(userAgent)).toBe(expected); + expect(isMobileBrowser()).toBe(expected); }); + }); + + test('should handle empty user agent', () => { + mockUserAgent(''); + expect(isMobileBrowser()).toBe(false); + }); +}); - [ - { - userAgent: BrowserUserAgent.iPad, - expected: true, - }, - { - userAgent: BrowserUserAgent.ChromeTabletSupported, - expected: true, - }, - { - userAgent: BrowserUserAgent.FirefoxTabletSupported, - expected: true, - }, - { - userAgent: BrowserUserAgent.OperaTabletSupported, - expected: true, - }, - ].forEach(({ userAgent, expected }) => { - test(`should detect ${userAgent} as tablet`, () => { - mockUserAgent(userAgent); - expect(isMobileBrowser()).toBe(expected); - }); +describe.skip('isTabletBrowser()', () => { + [ + { + userAgent: BrowserUserAgent.iPad, + expected: true, + }, + { + userAgent: BrowserUserAgent.ChromeTabletSupported, + expected: true, + }, + { + userAgent: BrowserUserAgent.FirefoxTabletSupported, + expected: true, + }, + { + userAgent: BrowserUserAgent.OperaTabletSupported, + expected: true, + }, + ].forEach(({ userAgent, expected }) => { + test(`should detect as tablet for "${userAgent}"`, () => { + mockUserAgent(userAgent); + expect(checkBrowserIsTablet(userAgent)).toBe(expected); + expect(isTabletBrowser()).toBe(expected); }); + }); +}); - test('should handle empty user agent', () => { - mockUserAgent(''); - expect(isMobileBrowser()).toBe(false); +describe('getBrowserName', () => { + [ + { + userAgent: BrowserUserAgent.Default, + expected: Browser.Chrome, + }, + { + userAgent: BrowserUserAgent.ChromeWindowsSupported, + expected: Browser.Chrome, + }, + { + userAgent: BrowserUserAgent.FirefoxWindowsSupported, + expected: Browser.Firefox, + }, + { + userAgent: BrowserUserAgent.EdgeSupported, + expected: Browser.Edge, + }, + { + userAgent: BrowserUserAgent.OperaDesktopSupported, + expected: Browser.Other, + }, + { + userAgent: BrowserUserAgent.YandexDesktopSupportedHigh, + expected: Browser.Other, + }, + { + userAgent: BrowserUserAgent.VivaldiWindowsSupported, + expected: Browser.Other, + }, + { + userAgent: BrowserUserAgent.SafariSupportedMac, + expected: Browser.Safari, + }, + { + userAgent: BrowserUserAgent.ChromeAndroidSupported, + expected: Browser.Chrome, + }, + { + userAgent: BrowserUserAgent.FirefoxMobileSupported, + expected: Browser.Firefox, + }, + { + userAgent: BrowserUserAgent.OperaAndroidSupported, + expected: Browser.Other, + }, + { + userAgent: BrowserUserAgent.OperaMiniUnsupported, + expected: Browser.Other, + }, + { + userAgent: BrowserUserAgent.SamsungBrowserSupported, + expected: Browser.Other, + }, + { + userAgent: BrowserUserAgent.UcBrowserSupported, + expected: Browser.Other, + }, + { + userAgent: BrowserUserAgent.FacebookBrowseriOS, + expected: Browser.Safari, + }, + { + userAgent: BrowserUserAgent.FacebookBrowserAndroid, + expected: Browser.Chrome, + }, + { + userAgent: BrowserUserAgent.YandexMobileSupported, + expected: Browser.Other, + }, + { + userAgent: BrowserUserAgent.ChromeTabletSupported, + expected: Browser.Chrome, + }, + { + userAgent: BrowserUserAgent.FirefoxTabletSupported, + expected: Browser.Firefox, + }, + { + userAgent: BrowserUserAgent.OperaTabletSupported, + expected: Browser.Other, + }, + { + userAgent: BrowserUserAgent.iPad, + expected: Browser.Safari, + }, + { + userAgent: BrowserUserAgent.iPhone, + expected: Browser.Safari, + }, + { + userAgent: BrowserUserAgent.iPod, + expected: Browser.Safari, + }, + ].forEach(({ userAgent, expected }) => { + test(`should return the correct browser name for "${userAgent}"`, () => { + mockUserAgent(userAgent); + expect(checkBrowserName(userAgent)).toBe(expected); + expect(getBrowserName()).toBe(expected); }); }); }); diff --git a/src/shared/useragent/useragent.ts b/src/shared/useragent/useragent.ts index ef978926c..62016a71f 100644 --- a/src/shared/useragent/useragent.ts +++ b/src/shared/useragent/useragent.ts @@ -4,11 +4,28 @@ import type { BrowserValue } from './types'; export function getBrowserName(): BrowserValue { const ua = navigator.userAgent.toLowerCase(); - if (ua.includes('edg/')) return Browser.Edge; - if (ua.includes('chrome/') && !ua.includes('edg/') && !ua.includes('opr/')) + if (ua.includes('edg/') || ua.includes('edge/')) return Browser.Edge; + if ( + (ua.includes('safari/') || ua.includes('applewebkit/')) && + !ua.includes('chrome/') + ) + return Browser.Safari; + + if ( + ua.includes('chrome/') && + !ua.includes('edg/') && + !ua.includes('edge/') && + !ua.includes('opr/') && + !ua.includes('opera/') && + !ua.includes('yabrowser/') && + !ua.includes('samsungbrowser/') && + !ua.includes('ucbrowser/') && + !ua.includes('vivaldi/') + ) return Browser.Chrome; - if (ua.includes('safari/') && !ua.includes('chrome/')) return Browser.Safari; + if (ua.includes('firefox/')) return Browser.Firefox; + return Browser.Other; } @@ -35,12 +52,13 @@ export function getBrowserVersion(): number { } export function isMobileBrowser(): boolean { - const ua = navigator.userAgent; - return /android|iphone|ipad|ipod|opera mini|iemobile|mobile/i.test(ua); + const ua = navigator.userAgent.toLowerCase(); + if (isTabletBrowser(ua)) return false; + return /android|ipad|iphone|ipod|opera mini|iemobile|mobile/.test(ua); } -export function isTabletBrowser(): boolean { - const ua = navigator.userAgent.toLowerCase(); +export function isTabletBrowser(ua: string = navigator.userAgent): boolean { + ua = ua.toLowerCase(); const isIPad = /\bipad\b/.test(ua); const isAndroidTablet = /android/.test(ua) && !/mobile/.test(ua); // Android tablets don't include "mobile" From 2576937bdb6bb1d67b826ab01ab52f841c41f739 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Wed, 30 Jul 2025 12:01:39 -0700 Subject: [PATCH 07/12] add newer user agent constants --- __test__/{support => constants}/constants.ts | 0 __test__/constants/index.ts | 2 + __test__/constants/useragent.ts | 66 ++++ __test__/setupTests.ts | 4 +- __test__/support/environment/TestContext.ts | 2 +- .../support/environment/TestEnvironment.ts | 11 +- .../environment/TestEnvironmentHelpers.ts | 9 +- __test__/support/helpers/requests.ts | 2 +- __test__/support/mocks/MockServiceWorker.ts | 2 +- __test__/support/models/BrowserUserAgent.ts | 97 ------ __test__/unit/http/sdkVersion.test.ts | 10 +- __test__/unit/push/registerForPush.test.ts | 3 - .../nativePermissionChange.test.ts | 2 +- package-lock.json | 102 ++++-- .../IdentityOperationExecutor.test.ts | 2 +- .../LoginUserOperationExecutor.test.ts | 2 +- .../RefreshUserOperationExecutor.test.ts | 2 +- .../SubscriptionOperationExecutor.test.ts | 2 +- .../UpdateUserOperationExecutor.test.ts | 2 +- src/core/operationRepo/OperationRepo.test.ts | 2 +- src/entries/pageSdkInit.test.ts | 2 +- src/entries/pageSdkInit2.test.ts | 2 +- src/onesignal/OneSignal.test.ts | 2 +- src/onesignal/UserNamespace.test.ts | 7 +- src/shared/api/OneSignalApiSW.test.ts | 2 +- src/shared/api/OneSignalApiShared.test.ts | 2 +- src/shared/config/config.test.ts | 2 +- .../managers/SubscriptionManager.test.ts | 2 +- .../sessionManager/SessionManager.test.ts | 2 +- src/shared/services/indexedDb.test.ts | 9 +- src/shared/useragent/detect.ts | 292 ++++++++++++++++++ src/shared/useragent/useragent.test.ts | 268 ++++++++-------- src/shared/useragent/useragent.ts | 81 ++--- src/sw/serviceWorker/ServiceWorker.test.ts | 2 +- 34 files changed, 633 insertions(+), 366 deletions(-) rename __test__/{support => constants}/constants.ts (100%) create mode 100644 __test__/constants/index.ts create mode 100644 __test__/constants/useragent.ts delete mode 100644 __test__/support/models/BrowserUserAgent.ts create mode 100644 src/shared/useragent/detect.ts diff --git a/__test__/support/constants.ts b/__test__/constants/constants.ts similarity index 100% rename from __test__/support/constants.ts rename to __test__/constants/constants.ts diff --git a/__test__/constants/index.ts b/__test__/constants/index.ts new file mode 100644 index 000000000..266f28736 --- /dev/null +++ b/__test__/constants/index.ts @@ -0,0 +1,2 @@ +export * from './constants'; +export * from './useragent'; diff --git a/__test__/constants/useragent.ts b/__test__/constants/useragent.ts new file mode 100644 index 000000000..b016666cf --- /dev/null +++ b/__test__/constants/useragent.ts @@ -0,0 +1,66 @@ +export const DEFAULT_USER_AGENT = + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.0.0 Safari/537.36'; + +// reference: https://explore.whatismybrowser.com/useragents/explore/ +export const USER_AGENTS = { + CHROME_WINDOWS: + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', + CHROME_MAC: + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', + CHROME_LINUX: + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', + CHROME_IOS_IPHONE: + 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/139.0.7258.60 Mobile/15E148 Safari/604.1', + CHROME_IOS_IPAD: + 'Mozilla/5.0 (iPad; CPU OS 18_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/139.0.7258.60 Mobile/15E148 Safari/604.1', + CHROME_IOS_IPOD: + 'Mozilla/5.0 (iPod; CPU iPhone OS 18_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/139.0.7258.60 Mobile/15E148 Safari/604.1', + CHROME_ANDROID: + 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.180 Mobile Safari/537.36', + FIREFOX_WINDOWS: + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:141.0) Gecko/20100101 Firefox/141.0', + FIREFOX_MAC: + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 15.6; rv:141.0) Gecko/20100101 Firefox/141.0', + FIREFOX_LINUX: + 'Mozilla/5.0 (X11; Linux i686; rv:141.0) Gecko/20100101 Firefox/141.0', + FIREFOX_IOS: + 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) FxiOS/141.0 Mobile/15E148 Safari/605.1.15', + FIREFOX_ANDROID: + 'Mozilla/5.0 (Android 16; Mobile; rv:141.0) Gecko/141.0 Firefox/141.0', + SAFARI_MAC: + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 15_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.4 Safari/605.1.15', + SAFARI_IPHONE: + 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.4 Mobile/15E148 Safari/604.1', + SAFARI_IPAD: + 'Mozilla/5.0 (iPad; CPU OS 18_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.4 Mobile/15E148 Safari/604.1', + SAFARI_IPOD: + 'Mozilla/5.0 (iPod touch; CPU iPhone 18_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.4 Mobile/15E148 Safari/604.1', + EDGE_WINDOWS: + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36 Edg/138.0.3351.109', + EDGE_MAC: + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36 Edg/138.0.3351.109', + EDGE_ANDROID: + 'Mozilla/5.0 (Linux; Android 10; HD1913) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.180 Mobile Safari/537.36 EdgA/138.0.3351.98', + EDGE_IOS: + 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 EdgiOS/138.3351.109 Mobile/15E148 Safari/605.1.15', + OPERA_WINDOWS: + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36 OPR/120.0.0.0', + VIVALDI_WINDOWS: + 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36 Vivaldi/7.5.3735.47', + YANDEX_WINDOWS: + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 YaBrowser/25.6.3.357 Yowser/2.5 Safari/537.36', + FACEBOOK_APP_IOS: + 'Mozilla/5.0 (iPhone; CPU iPhone OS 12_4_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 [FBAN/FBIOS;FBAV/238.0.0.50.115;FBBV/171859800;FBDV/iPhone9,3;FBMD/iPhone;FBSN/iOS;FBSV/12.4.1;FBSS/2;FBID/phone;FBLC/en_US;FBOP/5;FBRV/172564136;FBCR/AT&T]', + FACEBOOK_APP_ANDROID: + 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.180 Mobile Safari/537.36', + SAMSUNG_TABLET: + 'Dalvik/2.1.0 (Linux; U; Android 14; SM-X306B Build/UP1A.231005.007)', + GOOGLE_TABLET: + 'Mozilla/5.0 (Linux; Android 7.0; Pixel C Build/NRD90M; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/52.0.2743.98 Safari/537.36', + // AMAZON_FIRE_TV: + // 'Mozilla/5.0 (Linux; Android 11; AFTKRT Build/RS8101.1849N; wv)PlexTV/10.0.0.4149', + // APPLE_TV: 'AppleTV14,1/16.1', + // PS5: 'Mozilla/5.0 (PlayStation; PlayStation 5/2.26) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0 Safari/605.1.15', + // FACEBOOK_BOT: + // 'Mozilla/5.0 (compatible; FacebookBot/1.0; +https://developers.facebook.com/docs/sharing/webmasters/facebookbot/)', +}; diff --git a/__test__/setupTests.ts b/__test__/setupTests.ts index 692b34056..3ecf68f80 100644 --- a/__test__/setupTests.ts +++ b/__test__/setupTests.ts @@ -1,3 +1,4 @@ +import { DEFAULT_USER_AGENT } from './constants'; import { server } from './support/mocks/server'; beforeAll(() => server.listen({ @@ -26,7 +27,6 @@ vi.mock('src/core/operationRepo/constants', () => ({ })); Object.defineProperty(navigator, 'userAgent', { - value: - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.0.0 Safari/537.36', + value: DEFAULT_USER_AGENT, writable: true, }); diff --git a/__test__/support/environment/TestContext.ts b/__test__/support/environment/TestContext.ts index dbc05bbc0..3ff9f0358 100644 --- a/__test__/support/environment/TestContext.ts +++ b/__test__/support/environment/TestContext.ts @@ -13,7 +13,7 @@ import { NotificationClickMatchBehavior, } from '../../../src/shared/config/constants'; import type { RecursivePartial } from '../../../src/shared/context/Utils'; -import { APP_ID } from '../constants'; +import { APP_ID } from '../../constants'; import type { TestEnvironmentConfig } from './TestEnvironment'; export default class TestContext { diff --git a/__test__/support/environment/TestEnvironment.ts b/__test__/support/environment/TestEnvironment.ts index ad88d0948..6cf61512d 100644 --- a/__test__/support/environment/TestEnvironment.ts +++ b/__test__/support/environment/TestEnvironment.ts @@ -2,12 +2,11 @@ import type { AppUserConfig, ConfigIntegrationKindValue, ServerAppConfig, -} from '../../../src/shared/config'; -import type { RecursivePartial } from '../../../src/shared/context/Utils'; -import MainHelper from '../../../src/shared/helpers/MainHelper'; -import { DUMMY_ONESIGNAL_ID, DUMMY_PUSH_TOKEN } from '../constants'; +} from 'src/shared/config'; +import type { RecursivePartial } from 'src/shared/context/Utils'; +import MainHelper from 'src/shared/helpers/MainHelper'; +import { DUMMY_ONESIGNAL_ID, DUMMY_PUSH_TOKEN } from '../../constants'; import { generateNewSubscription } from '../helpers/core'; -import BrowserUserAgent from '../models/BrowserUserAgent'; import { initOSGlobals, resetDatabase, @@ -23,7 +22,7 @@ export interface TestEnvironmentConfig { permission?: NotificationPermission; addPrompts?: boolean; url?: string; - userAgent?: typeof BrowserUserAgent; + userAgent?: string; overrideServerConfig?: RecursivePartial; integration?: ConfigIntegrationKindValue; useMockIdentityModel?: boolean; diff --git a/__test__/support/environment/TestEnvironmentHelpers.ts b/__test__/support/environment/TestEnvironmentHelpers.ts index 3cbe4b61b..d135b28c6 100644 --- a/__test__/support/environment/TestEnvironmentHelpers.ts +++ b/__test__/support/environment/TestEnvironmentHelpers.ts @@ -16,9 +16,12 @@ import { getSlidedownElement } from '../../../src/page/slidedown/SlidedownElemen import Emitter from '../../../src/shared/libraries/Emitter'; import Database from '../../../src/shared/services/Database'; import { CUSTOM_LINK_CSS_CLASSES } from '../../../src/shared/slidedown/constants'; -import { DUMMY_ONESIGNAL_ID, DUMMY_SUBSCRIPTION_ID_3 } from '../constants'; +import { + DEFAULT_USER_AGENT, + DUMMY_ONESIGNAL_ID, + DUMMY_SUBSCRIPTION_ID_3, +} from '../../constants'; import MockNotification from '../mocks/MockNotification'; -import BrowserUserAgent from '../models/BrowserUserAgent'; import Random from '../utils/Random'; import TestContext from './TestContext'; import { type TestEnvironmentConfig } from './TestEnvironment'; @@ -87,7 +90,7 @@ export async function stubDomEnvironment(config: TestEnvironmentConfig) { const resourceLoader = new ResourceLoader({ userAgent: config.userAgent ? config.userAgent.toString() - : BrowserUserAgent.Default.toString(), + : DEFAULT_USER_AGENT.toString(), }); // global document object must be defined for `getSlidedownElement` to work correctly. diff --git a/__test__/support/helpers/requests.ts b/__test__/support/helpers/requests.ts index 6b34993b6..45d2367b5 100644 --- a/__test__/support/helpers/requests.ts +++ b/__test__/support/helpers/requests.ts @@ -5,7 +5,7 @@ import { APP_ID, DUMMY_ONESIGNAL_ID, DUMMY_SUBSCRIPTION_ID, -} from '../constants'; +} from '../../constants'; import TestContext from '../environment/TestContext'; import { server } from '../mocks/server'; diff --git a/__test__/support/mocks/MockServiceWorker.ts b/__test__/support/mocks/MockServiceWorker.ts index 50f7bacda..d70951047 100644 --- a/__test__/support/mocks/MockServiceWorker.ts +++ b/__test__/support/mocks/MockServiceWorker.ts @@ -1,4 +1,4 @@ -import { DUMMY_PUSH_TOKEN } from '../constants'; +import { DUMMY_PUSH_TOKEN } from '../../constants'; export const getSubscriptionFn = vi .fn<() => Promise>>() diff --git a/__test__/support/models/BrowserUserAgent.ts b/__test__/support/models/BrowserUserAgent.ts deleted file mode 100644 index ff01bd396..000000000 --- a/__test__/support/models/BrowserUserAgent.ts +++ /dev/null @@ -1,97 +0,0 @@ -const BrowserUserAgent = { - Default: - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36', - iPad: 'Mozilla/5.0 (iPad; U; CPU OS 3_2 like Mac OS X; en-us) AppleWebKit/531.21.10 (KHTML, like Gecko) Version/4.0.4 Mobile/7B334b Safari/531.21.10', - iPhone: - 'Mozilla/5.0 (iPhone; CPU iPhone OS 6_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/6.0 Mobile/10A5376e Safari/8536.25', - iPod: 'Mozilla/5.0 (iPod touch; CPU iPhone OS 7_0_3 like Mac OS X) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11B511 Safari/9537.53', - EdgeUnsupported: - 'Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.10136', - EdgeUnsupported2: - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36 Edge/17.17062', - EdgeSupported: - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36 Edge/17.17074', - IE11: 'Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko', - FirefoxMobileUnsupported: - 'Mozilla/5.0 (Android 4.4; Mobile; rv:47.0) Gecko/47.0 Firefox/47.0', - FirefoxTabletUnsupported: - 'Mozilla/5.0 (Android 4.4; Mobile ; rv:47.0) Gecko/47.0 Firefox/47.0', - FirefoxMobileSupported: - 'Mozilla/5.0 (Android 4.4; Mobile; rv:44.0) Gecko/48.0 Firefox/48.0', - FirefoxTabletSupported: - 'Mozilla/5.0 (Android 4.4; Tablet; rv:44.0) Gecko/48.0 Firefox/48.0', - FirefoxWindowsUnSupported: - 'Mozilla/5.0 (Windows NT x.y; WOW64; rv:44.0) Gecko/20100101 Firefox/44.0', - FirefoxWindowsSupported: - 'Mozilla/5.0 (Windows NT x.y; WOW64; rv:47.0) Gecko/20100101 Firefox/47.0', - FirefoxMacSupported: - 'Mozilla/5.0 (Macintosh; Intel Mac OS X x.y; rv:47.0) Gecko/20100101 Firefox/47.0', - FirefoxLinuxSupported: - 'Mozilla/5.0 (X11; Linux i686 on x86_64; rv:47.0) Gecko/20100101 Firefox/47.0', - SafariUnsupportedMac: - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10) AppleWebKit/538.32 (KHTML, like Gecko) Version/7.0 Safari/538.4', - SafariSupportedMac: - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10) AppleWebKit/538.32 (KHTML, like Gecko) Version/7.1 Safari/538.4', - SafariSupportedMac121: - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1 Safari/605.1.15', - FacebookBrowseriOS: - 'Mozilla/5.0 (iPhone; CPU iPhone OS 8_2 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Mobile/12D508 [FBAN/FBIOS;FBAV/27.0.0.10.12;FBBV/8291884;FBDV/iPhone7,1;FBMD/iPhone;FBSN/iPhone OS;FBSV/8.2;FBSS/3;]', - FacebookBrowserAndroid: - 'Mozilla/5.0 (Linux; Android 5.1; Archos Diamond S Build/LMY47D; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/51.0.2704.81 Mobile Safari/537.36 [FB_IAB/FB4A;FBAV/87.0.0.17.79;]', - ChromeAndroidSupported: - 'Mozilla/5.0 (Linux; Android 4.0.4; Galaxy Nexus Build/IMM76B) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/54 Mobile Safari/535.19', - ChromeWindowsSupported: - 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2228.0 Safari/537.36', - ChromeMacSupported: - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.1636.0 Safari/537.36', - ChromeMac10_15: - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.70 Safari/537.36', - ChromeMacSupported69: - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.81 Safari/537.36', - ChromeLinuxSupported: - 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.1636.0 Safari/537.36', - ChromeTabletSupported: - 'Mozilla/5.0 (Linux; Android 4.3; Nexus 10 Build/JWR66Y) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.1547.72 Safari/537.36', - ChromeAndroidUnsupported: - 'Mozilla/5.0 (Linux; Android 4.0.4; Galaxy Nexus Build/IMM76B) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/41 Mobile Safari/535.19', - ChromeWindowsUnsupported: - 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2228.0 Safari/537.36', - ChromeMacUnsupported: - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.1636.0 Safari/537.36', - ChromeLinuxUnsupported: - 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.1636.0 Safari/537.36', - ChromeTabletUnsupported: - 'Mozilla/5.0 (Linux; Android 4.3; Nexus 10 Build/JWR66Y) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.1547.72 Safari/537.36', - YandexDesktopSupportedHigh: - 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/30.0.1599.12785 YaBrowser/17.1.0.2036 Safari/537.36', - YandexDesktopSupportedLow: - 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/30.0.1599.12785 YaBrowser/15.12.1.6475 Safari/537.36', - YandexMobileSupported: - 'Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6P Build/N4F26T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.95 YaBrowser/17.1.2.339.00 Mobile Safari/537.36', - OperaDesktopSupported: - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.76 Safari/537.36 OPR/42.0.2442.806', - OperaMac10_14: - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.76 Safari/537.36 OPR/42.0.2442.806', - OperaAndroidSupported: - 'Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6P Build/N4F26T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.91 Mobile Safari/537.36 OPR/37.5.2246.114172', - OperaTabletSupported: - 'Mozilla/5.0 (Linux; Android 4.1.2; GT-N8000 Build/JZO54K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.166 Safari/537.36 OPR/37.0.1396.73172', - OperaMiniUnsupported: - 'Mozilla/5.0 (Linux; U; Android 7.1.2; Nexus 6P Build/N2G47H; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/57.0.2987.132 Mobile Safari/537.36 OPR/24.0.2254.115784', - VivaldiWindowsSupported: - 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.89 Vivaldi/1.0.94.2 Safari/537.36', - VivaldiLinuxSupported: - 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.105 Safari/537.36 Vivaldi/1.0.162.2', - VivaldiMacSupported: - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.99 Safari/537.36 Vivaldi/1.0.303.52', - SamsungBrowserSupported: - 'Mozilla/5.0 (Linux; Android 6.0.1; SAMSUNG SM-N910F Build/MMB29M) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/4.0 Chrome/44.0.2403.133 Mobile Safari/537.36', - SamsungBrowserUnsupported: - 'Mozilla/5.0 (Linux; Android 6.0.1; SAMSUNG SM-N910F Build/MMB29M) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/3.0 Chrome/44.0.2403.133 Mobile Safari/537.36', - UcBrowserSupported: - 'Mozilla/5.0 (Linux; U; Android 9; en-US; LM-G710 Build/PKQ1.181105.001) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/57.0.2987.108 UCBrowser/12.9.10.1159 Mobile Safari/537.36', - UcBrowserUnsupported: - 'Mozilla/5.0 (Linux; U; Android 6.0.1; zh-CN; F5121 Build/34.0.A.1.247) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/40.0.2214.89 UCBrowser/11.5.1.944 Mobile Safari/537.36', -}; - -export default BrowserUserAgent; diff --git a/__test__/unit/http/sdkVersion.test.ts b/__test__/unit/http/sdkVersion.test.ts index e41c171cf..4a01d2731 100644 --- a/__test__/unit/http/sdkVersion.test.ts +++ b/__test__/unit/http/sdkVersion.test.ts @@ -1,12 +1,12 @@ -import { generateNewSubscription } from '__test__/support/helpers/core'; -import { nock } from '__test__/support/helpers/general'; -import AliasPair from '../../../src/core/requestService/AliasPair'; -import { RequestService } from '../../../src/core/requestService/RequestService'; import { APP_ID, DUMMY_EXTERNAL_ID, DUMMY_SUBSCRIPTION_ID, -} from '../../support/constants'; +} from '__test__/constants'; +import { generateNewSubscription } from '__test__/support/helpers/core'; +import { nock } from '__test__/support/helpers/general'; +import AliasPair from '../../../src/core/requestService/AliasPair'; +import { RequestService } from '../../../src/core/requestService/RequestService'; import { expectHeaderToBeSent } from '../../support/helpers/sdkVersion'; describe('Sdk Version Header Tests', () => { diff --git a/__test__/unit/push/registerForPush.test.ts b/__test__/unit/push/registerForPush.test.ts index 3a5d15290..b0fd7d926 100644 --- a/__test__/unit/push/registerForPush.test.ts +++ b/__test__/unit/push/registerForPush.test.ts @@ -2,7 +2,6 @@ import InitHelper from '../../../src/shared/helpers/InitHelper'; import Log from '../../../src/shared/libraries/Log'; import OneSignalEvent from '../../../src/shared/services/OneSignalEvent'; import { TestEnvironment } from '../../support/environment/TestEnvironment'; -import BrowserUserAgent from '../../support/models/BrowserUserAgent'; //stub dismisshelper vi.mock('../../../src/shared/helpers/DismissHelper'); @@ -14,8 +13,6 @@ describe('Register for push', () => { beforeEach(async () => { await TestEnvironment.initialize({ addPrompts: true, - // @ts-expect-error - default user agent - userAgent: BrowserUserAgent.Default, }); }); diff --git a/__test__/unit/pushSubscription/nativePermissionChange.test.ts b/__test__/unit/pushSubscription/nativePermissionChange.test.ts index 513d6a2c0..822c31823 100644 --- a/__test__/unit/pushSubscription/nativePermissionChange.test.ts +++ b/__test__/unit/pushSubscription/nativePermissionChange.test.ts @@ -4,7 +4,7 @@ import { DUMMY_SUBSCRIPTION_ID, DUMMY_SUBSCRIPTION_ID_2, DUMMY_SUBSCRIPTION_ID_3, -} from '__test__/support/constants'; +} from '__test__/constants'; import { TestEnvironment } from '__test__/support/environment/TestEnvironment'; import { createPushSub } from '__test__/support/environment/TestEnvironmentHelpers'; import { MockServiceWorker } from '__test__/support/mocks/MockServiceWorker'; diff --git a/package-lock.json b/package-lock.json index 86a2ad922..d9ffc6ba4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2211,13 +2211,6 @@ "dev": true, "license": "MIT" }, - "node_modules/bowser": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz", - "integrity": "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==", - "dev": true, - "license": "MIT" - }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -2311,32 +2304,6 @@ "node": ">=18" } }, - "node_modules/chalk": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", - "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/chalk/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/check-error": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", @@ -2480,6 +2447,36 @@ "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" } }, + "node_modules/concurrently/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/cookie": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", @@ -2894,6 +2891,19 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/eslint-formatter-pretty/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/eslint-rule-docs": { "version": "1.1.235", "resolved": "https://registry.npmjs.org/eslint-rule-docs/-/eslint-rule-docs-1.1.235.tgz", @@ -2972,6 +2982,19 @@ "node": ">=4.0" } }, + "node_modules/eslint/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/espree": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", @@ -4066,6 +4089,19 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/log-symbols/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/loupe": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.0.tgz", diff --git a/src/core/executors/IdentityOperationExecutor.test.ts b/src/core/executors/IdentityOperationExecutor.test.ts index 5d25d114a..bd4bd947e 100644 --- a/src/core/executors/IdentityOperationExecutor.test.ts +++ b/src/core/executors/IdentityOperationExecutor.test.ts @@ -1,4 +1,4 @@ -import { APP_ID, DUMMY_ONESIGNAL_ID } from '__test__/support/constants'; +import { APP_ID, DUMMY_ONESIGNAL_ID } from '__test__/constants'; import { SomeOperation } from '__test__/support/helpers/executors'; import { setAddAliasError, diff --git a/src/core/executors/LoginUserOperationExecutor.test.ts b/src/core/executors/LoginUserOperationExecutor.test.ts index 1858b5a2a..340dd5dd5 100644 --- a/src/core/executors/LoginUserOperationExecutor.test.ts +++ b/src/core/executors/LoginUserOperationExecutor.test.ts @@ -8,7 +8,7 @@ import { DUMMY_PUSH_TOKEN_2, DUMMY_SUBSCRIPTION_ID, DUMMY_SUBSCRIPTION_ID_2, -} from '__test__/support/constants'; +} from '__test__/constants'; import { TestEnvironment } from '__test__/support/environment/TestEnvironment'; import { SomeOperation } from '__test__/support/helpers/executors'; import { diff --git a/src/core/executors/RefreshUserOperationExecutor.test.ts b/src/core/executors/RefreshUserOperationExecutor.test.ts index 818639a25..39de09ed5 100644 --- a/src/core/executors/RefreshUserOperationExecutor.test.ts +++ b/src/core/executors/RefreshUserOperationExecutor.test.ts @@ -6,7 +6,7 @@ import { DUMMY_PUSH_TOKEN, DUMMY_SUBSCRIPTION_ID, DUMMY_SUBSCRIPTION_ID_2, -} from '__test__/support/constants'; +} from '__test__/constants'; import { SomeOperation } from '__test__/support/helpers/executors'; import { setGetUserError, diff --git a/src/core/executors/SubscriptionOperationExecutor.test.ts b/src/core/executors/SubscriptionOperationExecutor.test.ts index 2cb31e78b..3484a414e 100644 --- a/src/core/executors/SubscriptionOperationExecutor.test.ts +++ b/src/core/executors/SubscriptionOperationExecutor.test.ts @@ -2,7 +2,7 @@ import { APP_ID, DUMMY_ONESIGNAL_ID, DUMMY_SUBSCRIPTION_ID_3, -} from '__test__/support/constants'; +} from '__test__/constants'; import { createPushSub } from '__test__/support/environment/TestEnvironmentHelpers'; import { SomeOperation } from '__test__/support/helpers/executors'; import { server } from '__test__/support/mocks/server'; diff --git a/src/core/executors/UpdateUserOperationExecutor.test.ts b/src/core/executors/UpdateUserOperationExecutor.test.ts index c8fbfbe5d..f9d0e663b 100644 --- a/src/core/executors/UpdateUserOperationExecutor.test.ts +++ b/src/core/executors/UpdateUserOperationExecutor.test.ts @@ -1,4 +1,4 @@ -import { APP_ID, DUMMY_ONESIGNAL_ID } from '__test__/support/constants'; +import { APP_ID, DUMMY_ONESIGNAL_ID } from '__test__/constants'; import { SomeOperation } from '__test__/support/helpers/executors'; import { setUpdateUserError, diff --git a/src/core/operationRepo/OperationRepo.test.ts b/src/core/operationRepo/OperationRepo.test.ts index 1ab91547a..5ce88564e 100644 --- a/src/core/operationRepo/OperationRepo.test.ts +++ b/src/core/operationRepo/OperationRepo.test.ts @@ -2,7 +2,7 @@ import { APP_ID, DUMMY_ONESIGNAL_ID, DUMMY_SUBSCRIPTION_ID, -} from '__test__/support/constants'; +} from '__test__/constants'; import { fakeWaitForOperations } from '__test__/support/helpers/executors'; import Log from 'src/shared/libraries/Log'; import Database, { type OperationItem } from 'src/shared/services/Database'; diff --git a/src/entries/pageSdkInit.test.ts b/src/entries/pageSdkInit.test.ts index 093ff1eab..b9860e02b 100644 --- a/src/entries/pageSdkInit.test.ts +++ b/src/entries/pageSdkInit.test.ts @@ -1,4 +1,4 @@ -import { APP_ID } from '__test__/support/constants'; +import { APP_ID } from '__test__/constants'; import { TestEnvironment } from '__test__/support/environment/TestEnvironment'; import { mockServerConfig } from '__test__/support/helpers/requests'; import { server } from '__test__/support/mocks/server'; diff --git a/src/entries/pageSdkInit2.test.ts b/src/entries/pageSdkInit2.test.ts index 95dda65fa..87eee2ad1 100644 --- a/src/entries/pageSdkInit2.test.ts +++ b/src/entries/pageSdkInit2.test.ts @@ -6,7 +6,7 @@ import { DUMMY_PUSH_TOKEN, DUMMY_SUBSCRIPTION_ID, DUMMY_SUBSCRIPTION_ID_2, -} from '__test__/support/constants'; +} from '__test__/constants'; import { TestEnvironment } from '__test__/support/environment/TestEnvironment'; import { setupSubModelStore } from '__test__/support/environment/TestEnvironmentHelpers'; import { diff --git a/src/onesignal/OneSignal.test.ts b/src/onesignal/OneSignal.test.ts index 07986c6c1..0e7900319 100644 --- a/src/onesignal/OneSignal.test.ts +++ b/src/onesignal/OneSignal.test.ts @@ -7,7 +7,7 @@ import { DUMMY_SUBSCRIPTION_ID, DUMMY_SUBSCRIPTION_ID_2, DUMMY_SUBSCRIPTION_ID_3, -} from '__test__/support/constants'; +} from '__test__/constants'; import { TestEnvironment } from '__test__/support/environment/TestEnvironment'; import { setupSubModelStore } from '__test__/support/environment/TestEnvironmentHelpers'; import { waitForOperations } from '__test__/support/helpers/executors'; diff --git a/src/onesignal/UserNamespace.test.ts b/src/onesignal/UserNamespace.test.ts index 3be6aa8f5..16947d9fe 100644 --- a/src/onesignal/UserNamespace.test.ts +++ b/src/onesignal/UserNamespace.test.ts @@ -1,9 +1,4 @@ -import { - DUMMY_ONESIGNAL_ID, - DUMMY_PUSH_TOKEN, -} from '__test__/support/constants'; -import { ModelChangeTags } from 'src/core/types/models'; -import Log from 'src/shared/libraries/Log'; +import { DUMMY_ONESIGNAL_ID, DUMMY_PUSH_TOKEN } from '__test__/constants'; import { IDManager } from 'src/shared/managers/IDManager'; import { TestEnvironment } from '../../__test__/support/environment/TestEnvironment'; import type { UserChangeEvent } from '../page/models/UserChangeEvent'; diff --git a/src/shared/api/OneSignalApiSW.test.ts b/src/shared/api/OneSignalApiSW.test.ts index a81fa7f34..2f21e463e 100644 --- a/src/shared/api/OneSignalApiSW.test.ts +++ b/src/shared/api/OneSignalApiSW.test.ts @@ -1,4 +1,4 @@ -import { APP_ID } from '__test__/support/constants'; +import { APP_ID } from '__test__/constants'; import { nock } from '__test__/support/helpers/general'; import { OneSignalApiSW } from './OneSignalApiSW'; diff --git a/src/shared/api/OneSignalApiShared.test.ts b/src/shared/api/OneSignalApiShared.test.ts index 5c2702f52..095e3109d 100644 --- a/src/shared/api/OneSignalApiShared.test.ts +++ b/src/shared/api/OneSignalApiShared.test.ts @@ -1,4 +1,4 @@ -import { APP_ID } from '__test__/support/constants'; +import { APP_ID } from '__test__/constants'; import { server } from '__test__/support/mocks/server'; import { http, HttpResponse } from 'msw'; import Log from '../libraries/Log'; diff --git a/src/shared/config/config.test.ts b/src/shared/config/config.test.ts index 96217c282..b7fb35adc 100644 --- a/src/shared/config/config.test.ts +++ b/src/shared/config/config.test.ts @@ -1,4 +1,4 @@ -import { APP_ID } from '__test__/support/constants'; +import { APP_ID } from '__test__/constants'; import TestContext from '__test__/support/environment/TestContext'; import { TestEnvironment } from '__test__/support/environment/TestEnvironment'; import { getFinalAppConfig } from '__test__/support/helpers/configHelper'; diff --git a/src/shared/managers/SubscriptionManager.test.ts b/src/shared/managers/SubscriptionManager.test.ts index e3c07a5e6..5432a0fd1 100644 --- a/src/shared/managers/SubscriptionManager.test.ts +++ b/src/shared/managers/SubscriptionManager.test.ts @@ -1,4 +1,4 @@ -import { DEVICE_OS, DUMMY_EXTERNAL_ID } from '__test__/support/constants'; +import { DUMMY_EXTERNAL_ID } from '__test__/constants'; import { TestEnvironment } from '__test__/support/environment/TestEnvironment'; import { setupSubModelStore } from '__test__/support/environment/TestEnvironmentHelpers'; import { diff --git a/src/shared/managers/sessionManager/SessionManager.test.ts b/src/shared/managers/sessionManager/SessionManager.test.ts index f1ead2392..02a2793ec 100644 --- a/src/shared/managers/sessionManager/SessionManager.test.ts +++ b/src/shared/managers/sessionManager/SessionManager.test.ts @@ -1,4 +1,4 @@ -import { DUMMY_EXTERNAL_ID } from '__test__/support/constants'; +import { DUMMY_EXTERNAL_ID } from '__test__/constants'; import { TestEnvironment } from '__test__/support/environment/TestEnvironment'; import { setAddAliasResponse } from '__test__/support/helpers/requests'; import LoginManager from 'src/page/managers/LoginManager'; diff --git a/src/shared/services/indexedDb.test.ts b/src/shared/services/indexedDb.test.ts index 204cd6eb8..e7c25fb87 100644 --- a/src/shared/services/indexedDb.test.ts +++ b/src/shared/services/indexedDb.test.ts @@ -1,11 +1,8 @@ +import { DUMMY_EXTERNAL_ID, DUMMY_ONESIGNAL_ID } from '__test__/constants'; +import Random from '__test__/support/utils/Random'; import { ModelName } from 'src/core/types/models'; import { SubscriptionType } from 'src/core/types/subscription'; -import { - DUMMY_EXTERNAL_ID, - DUMMY_ONESIGNAL_ID, -} from '../../../__test__/support/constants'; -import Random from '../../../__test__/support/utils/Random'; -import Log from '../../shared/libraries/Log'; +import Log from 'src/shared/libraries/Log'; import IndexedDb, { LegacyModelName } from './IndexedDb'; function newOSIndexedDb( diff --git a/src/shared/useragent/detect.ts b/src/shared/useragent/detect.ts new file mode 100644 index 000000000..871fbcce3 --- /dev/null +++ b/src/shared/useragent/detect.ts @@ -0,0 +1,292 @@ +// types.ts +export interface IDeviceResult { + version: string; +} + +export interface IBrowserResult { + name: string; + version: string; +} + +// utils.ts +const isSSR = typeof window === 'undefined'; + +/** + * Get user agent string with SSR support + */ +export function getUserAgent(forceUserAgent?: string): string { + return forceUserAgent + ? forceUserAgent + : !isSSR && window.navigator + ? window.navigator.userAgent + : ''; +} + +/** + * Match entry based on position found in the user-agent string + */ +export function matchUserAgent( + userAgent: string, + position: number, + pattern: RegExp, +): string { + const match = userAgent.match(pattern); + return (match && match.length > 1 && match[position]) || ''; +} + +// device-detection.ts +/** + * Check if device is Android + */ +export function isAndroidDevice(userAgent: string): boolean { + return !/like android/i.test(userAgent) && /android/i.test(userAgent); +} + +/** + * Get iOS device type (iphone, ipod, ipad, or empty string) + */ +export function getIOSDeviceType(userAgent: string): string { + let deviceType = matchUserAgent( + userAgent, + 1, + /(iphone|ipod|ipad)/i, + ).toLowerCase(); + + // Workaround for ipadOS, force detection as tablet + // SEE: https://github.com/lancedikson/bowser/issues/329 + // SEE: https://stackoverflow.com/questions/58019463/how-to-detect-device-name-in-safari-on-ios-13-while-it-doesnt-show-the-correct + if ( + !isSSR && + navigator.platform === 'MacIntel' && + navigator.maxTouchPoints > 2 && + !(window as any).MSStream + ) { + deviceType = 'ipad'; + } + + return deviceType; +} + +/** + * Check if device is a tablet + */ +export function isTablet(userAgent: string): boolean { + const isAndroid = isAndroidDevice(userAgent); + const iOSDevice = getIOSDeviceType(userAgent); + + return ( + // Default tablet + (/tablet/i.test(userAgent) && !/tablet pc/i.test(userAgent)) || + // iPad + iOSDevice === 'ipad' || + // Android tablet + (isAndroid && !/[^-]mobi/i.test(userAgent)) || + // Nexus tablet + (!/nexus\s*[0-6]\s*/i.test(userAgent) && /nexus\s*[0-9]+/i.test(userAgent)) + ); +} + +/** + * Check if device is mobile + */ +export function isMobile(userAgent: string): boolean { + const isTabletDevice = isTablet(userAgent); + const isAndroid = isAndroidDevice(userAgent); + const iOSDevice = getIOSDeviceType(userAgent); + + return ( + // Default mobile + !isTabletDevice && + (/[^-]mobi/i.test(userAgent) || + // iPhone / iPod + iOSDevice === 'iphone' || + iOSDevice === 'ipod' || + // Android mobile + isAndroid || + // Nexus mobile + /nexus\s*[0-6]\s*/i.test(userAgent)) + ); +} + +/** + * Check if device is desktop + */ +export function isDesktop(userAgent: string): boolean { + return !isMobile(userAgent) && !isTablet(userAgent); +} + +// os-detection.ts +/** + * Check if device is running macOS and return version info + */ +export function isMacOS(userAgent: string): IDeviceResult | false { + if (!/macintosh/i.test(userAgent)) { + return false; + } + + const version = matchUserAgent(userAgent, 1, /mac os x (\d+(\.?_?\d+)+)/i) + .replace(/[_\s]/g, '.') + .split('.') + .map((versionNumber: string): string => versionNumber)[1]; + + return { version }; +} + +/** + * Check if device is running Windows and return version info + */ +export function isWindows(userAgent: string): IDeviceResult | false { + if (!/windows /i.test(userAgent)) { + return false; + } + + const version = matchUserAgent( + userAgent, + 1, + /Windows ((NT|XP)( \d\d?.\d)?)/i, + ); + return { version }; +} + +/** + * Check if device is running iOS and return version info + */ +export function isiOS(userAgent: string): IDeviceResult | false { + const iOSDevice = getIOSDeviceType(userAgent); + + if (!iOSDevice) { + return false; + } + + const version = + matchUserAgent(userAgent, 1, /os (\d+([_\s]\d+)*) like mac os x/i).replace( + /[_\s]/g, + '.', + ) || matchUserAgent(userAgent, 1, /version\/(\d+(\.\d+)?)/i); + + return { version }; +} + +/** + * Check if device is running Android and return version info + */ +export function isAndroid(userAgent: string): IDeviceResult | false { + if (!isAndroidDevice(userAgent)) { + return false; + } + + const version = matchUserAgent(userAgent, 1, /android[ \/-](\d+(\.\d+)*)/i); + return { version }; +} + +// browser-detection.ts +/** + * Detect browser name and version + */ +export function getBrowser(userAgent: string): IBrowserResult { + const versionIdentifier = matchUserAgent( + userAgent, + 1, + /version\/(\d+(\.\d+)?)/i, + ); + + if (/opera/i.test(userAgent)) { + // Opera + return { + name: 'Opera', + version: + versionIdentifier || + matchUserAgent(userAgent, 1, /(?:opera|opr|opios)[\s\/](\d+(\.\d+)?)/i), + }; + } else if (/opr\/|opios/i.test(userAgent)) { + // Opera + return { + name: 'Opera', + version: + matchUserAgent(userAgent, 1, /(?:opr|opios)[\s\/](\d+(\.\d+)?)/i) || + versionIdentifier, + }; + } else if (/SamsungBrowser/i.test(userAgent)) { + // Samsung Browser + return { + name: 'Samsung Internet for Android', + version: + versionIdentifier || + matchUserAgent(userAgent, 1, /(?:SamsungBrowser)[\s\/](\d+(\.\d+)?)/i), + }; + } else if (/yabrowser/i.test(userAgent)) { + // Yandex Browser + return { + name: 'Yandex Browser', + version: + versionIdentifier || + matchUserAgent(userAgent, 1, /(?:yabrowser)[\s\/](\d+(\.\d+)?)/i), + }; + } else if (/ucbrowser/i.test(userAgent)) { + // UC Browser + return { + name: 'UC Browser', + version: matchUserAgent( + userAgent, + 1, + /(?:ucbrowser)[\s\/](\d+(\.\d+)?)/i, + ), + }; + } else if (/msie|trident/i.test(userAgent)) { + // Internet Explorer + return { + name: 'Internet Explorer', + version: matchUserAgent(userAgent, 1, /(?:msie |rv:)(\d+(\.\d+)?)/i), + }; + } else if (/(edge|edgios|edga|edg)/i.test(userAgent)) { + // Edge + return { + name: 'Microsoft Edge', + version: matchUserAgent( + userAgent, + 2, + /(edge|edgios|edga|edg)\/(\d+(\.\d+)?)/i, + ), + }; + } else if (/firefox|iceweasel|fxios/i.test(userAgent)) { + // Firefox + return { + name: 'Firefox', + version: matchUserAgent( + userAgent, + 1, + /(?:firefox|iceweasel|fxios)[ \/](\d+(\.\d+)?)/i, + ), + }; + } else if (/chromium/i.test(userAgent)) { + // Chromium + return { + name: 'Chromium', + version: + matchUserAgent(userAgent, 1, /(?:chromium)[\s\/](\d+(?:\.\d+)?)/i) || + versionIdentifier, + }; + } else if (/chrome|crios|crmo/i.test(userAgent)) { + // Chrome + return { + name: 'Chrome', + version: matchUserAgent( + userAgent, + 1, + /(?:chrome|crios|crmo)\/(\d+(\.\d+)?)/i, + ), + }; + } else if (/safari|applewebkit/i.test(userAgent)) { + // Safari + return { + name: 'Safari', + version: versionIdentifier, + }; + } else { + // Everything else + return { + name: matchUserAgent(userAgent, 1, /^(.*)\/(.*) /), + version: matchUserAgent(userAgent, 2, /^(.*)\/(.*) /), + }; + } +} diff --git a/src/shared/useragent/useragent.test.ts b/src/shared/useragent/useragent.test.ts index 61437a495..006d71237 100644 --- a/src/shared/useragent/useragent.test.ts +++ b/src/shared/useragent/useragent.test.ts @@ -1,5 +1,4 @@ -import BrowserUserAgent from '__test__/support/models/BrowserUserAgent'; -import Bowser from 'bowser'; +import { USER_AGENTS } from '__test__/constants'; import { Browser } from './constants'; import { getBrowserName, isMobileBrowser, isTabletBrowser } from './useragent'; @@ -10,281 +9,292 @@ const mockUserAgent = (userAgent: string) => { }); }; -// using third party library to validate our simpler logic -const checkBrowserIsMobile = (userAgent: string) => { - const browser = Bowser.getParser(userAgent); - return browser.getPlatformType() === 'mobile'; -}; - -const checkBrowserIsTablet = (userAgent: string) => { - const browser = Bowser.getParser(userAgent); - return browser.getPlatformType() === 'tablet'; -}; - -const checkBrowserName = (userAgent: string) => { - const browser = Bowser.getParser(userAgent); - const name = browser.getBrowser().name; - - switch (name) { - case 'Chrome': - return Browser.Chrome; - case 'Firefox': - return Browser.Firefox; - case 'Microsoft Edge': - return Browser.Edge; - case 'Safari': - return Browser.Safari; - default: - return Browser.Other; - } -}; - -describe.skip('isMobileBrowser()', () => { +describe('isMobileBrowser()', () => { [ + // non mobile / is tablet { - userAgent: BrowserUserAgent.iPhone, - expected: true, + userAgent: USER_AGENTS.CHROME_WINDOWS, + expected: false, }, { - userAgent: BrowserUserAgent.iPad, + userAgent: USER_AGENTS.CHROME_MAC, expected: false, }, { - userAgent: BrowserUserAgent.iPod, + userAgent: USER_AGENTS.CHROME_LINUX, + expected: false, + }, + { + userAgent: USER_AGENTS.CHROME_IOS_IPAD, + expected: false, + }, + + { + userAgent: USER_AGENTS.FIREFOX_WINDOWS, + expected: false, + }, + { + userAgent: USER_AGENTS.FIREFOX_MAC, + expected: false, + }, + { + userAgent: USER_AGENTS.SAFARI_IPAD, + expected: false, + }, + { + userAgent: USER_AGENTS.EDGE_MAC, + expected: false, + }, + { + userAgent: USER_AGENTS.EDGE_WINDOWS, + expected: false, + }, + { + userAgent: USER_AGENTS.OPERA_WINDOWS, + expected: false, + }, + { + userAgent: USER_AGENTS.YANDEX_WINDOWS, + expected: false, + }, + { + userAgent: USER_AGENTS.VIVALDI_WINDOWS, + expected: false, + }, + + // mobile + { + userAgent: USER_AGENTS.CHROME_ANDROID, expected: true, }, { - userAgent: BrowserUserAgent.ChromeAndroidSupported, + userAgent: USER_AGENTS.CHROME_IOS_IPHONE, expected: true, }, { - userAgent: BrowserUserAgent.FirefoxMobileSupported, + userAgent: USER_AGENTS.CHROME_IOS_IPOD, expected: true, }, { - userAgent: BrowserUserAgent.OperaAndroidSupported, + userAgent: USER_AGENTS.FIREFOX_ANDROID, expected: true, }, { - userAgent: BrowserUserAgent.OperaMiniUnsupported, + userAgent: USER_AGENTS.FIREFOX_IOS, expected: true, }, { - userAgent: BrowserUserAgent.SamsungBrowserSupported, + userAgent: USER_AGENTS.SAFARI_IPHONE, expected: true, }, { - userAgent: BrowserUserAgent.UcBrowserSupported, + userAgent: USER_AGENTS.SAFARI_IPOD, expected: true, }, { - userAgent: BrowserUserAgent.FacebookBrowseriOS, + userAgent: USER_AGENTS.EDGE_ANDROID, expected: true, }, { - userAgent: BrowserUserAgent.FacebookBrowserAndroid, + userAgent: USER_AGENTS.EDGE_IOS, expected: true, }, { - userAgent: BrowserUserAgent.YandexMobileSupported, + userAgent: USER_AGENTS.FACEBOOK_APP_IOS, expected: true, }, ].forEach(({ userAgent, expected }) => { - test(`should detect as a mobile browser "${userAgent}"`, () => { + test(`"${userAgent}" should ${expected ? 'be' : 'not be'} a mobile browser`, () => { mockUserAgent(userAgent); - expect(checkBrowserIsMobile(userAgent)).toBe(expected); expect(isMobileBrowser()).toBe(expected); }); }); + test('should handle empty user agent', () => { + mockUserAgent(''); + expect(isMobileBrowser()).toBe(false); + }); +}); + +describe('isTabletBrowser()', () => { [ + // non tablet or is mobile { - userAgent: BrowserUserAgent.Default, + userAgent: USER_AGENTS.CHROME_WINDOWS, expected: false, }, { - userAgent: BrowserUserAgent.ChromeWindowsSupported, + userAgent: USER_AGENTS.CHROME_MAC, expected: false, }, { - userAgent: BrowserUserAgent.ChromeMacSupported, + userAgent: USER_AGENTS.CHROME_LINUX, expected: false, }, { - userAgent: BrowserUserAgent.ChromeLinuxSupported, + userAgent: USER_AGENTS.CHROME_IOS_IPHONE, expected: false, }, { - userAgent: BrowserUserAgent.SafariSupportedMac, + userAgent: USER_AGENTS.FIREFOX_WINDOWS, expected: false, }, { - userAgent: BrowserUserAgent.FirefoxWindowsSupported, + userAgent: USER_AGENTS.FIREFOX_MAC, expected: false, }, { - userAgent: BrowserUserAgent.FirefoxMacSupported, + userAgent: USER_AGENTS.FIREFOX_IOS, expected: false, }, { - userAgent: BrowserUserAgent.FirefoxLinuxSupported, + userAgent: USER_AGENTS.SAFARI_MAC, expected: false, }, { - userAgent: BrowserUserAgent.EdgeSupported, + userAgent: USER_AGENTS.SAFARI_IPHONE, expected: false, }, { - userAgent: BrowserUserAgent.OperaDesktopSupported, + userAgent: USER_AGENTS.EDGE_MAC, expected: false, }, { - userAgent: BrowserUserAgent.YandexDesktopSupportedHigh, + userAgent: USER_AGENTS.EDGE_WINDOWS, expected: false, }, - { - userAgent: BrowserUserAgent.VivaldiWindowsSupported, - expected: false, - }, - ].forEach(({ userAgent, expected }) => { - test(`should detect ${userAgent} as a desktop browser`, () => { - mockUserAgent(userAgent); - expect(checkBrowserIsMobile(userAgent)).toBe(expected); - expect(isMobileBrowser()).toBe(expected); - }); - }); - - test('should handle empty user agent', () => { - mockUserAgent(''); - expect(isMobileBrowser()).toBe(false); - }); -}); -describe.skip('isTabletBrowser()', () => { - [ + // tablet { - userAgent: BrowserUserAgent.iPad, + userAgent: USER_AGENTS.CHROME_IOS_IPAD, expected: true, }, { - userAgent: BrowserUserAgent.ChromeTabletSupported, + userAgent: USER_AGENTS.SAFARI_IPAD, expected: true, }, { - userAgent: BrowserUserAgent.FirefoxTabletSupported, + userAgent: USER_AGENTS.SAMSUNG_TABLET, expected: true, }, { - userAgent: BrowserUserAgent.OperaTabletSupported, + userAgent: USER_AGENTS.GOOGLE_TABLET, expected: true, }, ].forEach(({ userAgent, expected }) => { - test(`should detect as tablet for "${userAgent}"`, () => { + test(`"${userAgent}" should ${expected ? 'be' : 'not be'} a tablet browser`, () => { mockUserAgent(userAgent); - expect(checkBrowserIsTablet(userAgent)).toBe(expected); expect(isTabletBrowser()).toBe(expected); }); }); }); -describe('getBrowserName', () => { +describe('getBrowserName()', () => { [ + // chrome { - userAgent: BrowserUserAgent.Default, + userAgent: USER_AGENTS.CHROME_WINDOWS, expected: Browser.Chrome, }, { - userAgent: BrowserUserAgent.ChromeWindowsSupported, + userAgent: USER_AGENTS.CHROME_MAC, expected: Browser.Chrome, }, { - userAgent: BrowserUserAgent.FirefoxWindowsSupported, - expected: Browser.Firefox, + userAgent: USER_AGENTS.CHROME_LINUX, + expected: Browser.Chrome, }, { - userAgent: BrowserUserAgent.EdgeSupported, - expected: Browser.Edge, + userAgent: USER_AGENTS.CHROME_ANDROID, + expected: Browser.Chrome, }, { - userAgent: BrowserUserAgent.OperaDesktopSupported, - expected: Browser.Other, + userAgent: USER_AGENTS.CHROME_IOS_IPHONE, + expected: Browser.Chrome, }, { - userAgent: BrowserUserAgent.YandexDesktopSupportedHigh, - expected: Browser.Other, + userAgent: USER_AGENTS.CHROME_IOS_IPAD, + expected: Browser.Chrome, }, + + // firefox { - userAgent: BrowserUserAgent.VivaldiWindowsSupported, - expected: Browser.Other, + userAgent: USER_AGENTS.FIREFOX_WINDOWS, + expected: Browser.Firefox, }, { - userAgent: BrowserUserAgent.SafariSupportedMac, - expected: Browser.Safari, + userAgent: USER_AGENTS.FIREFOX_MAC, + expected: Browser.Firefox, }, { - userAgent: BrowserUserAgent.ChromeAndroidSupported, - expected: Browser.Chrome, + userAgent: USER_AGENTS.FIREFOX_LINUX, + expected: Browser.Firefox, }, { - userAgent: BrowserUserAgent.FirefoxMobileSupported, + userAgent: USER_AGENTS.FIREFOX_ANDROID, expected: Browser.Firefox, }, { - userAgent: BrowserUserAgent.OperaAndroidSupported, - expected: Browser.Other, + userAgent: USER_AGENTS.FIREFOX_IOS, + expected: Browser.Firefox, }, + + // edge { - userAgent: BrowserUserAgent.OperaMiniUnsupported, - expected: Browser.Other, + userAgent: USER_AGENTS.EDGE_WINDOWS, + expected: Browser.Edge, }, { - userAgent: BrowserUserAgent.SamsungBrowserSupported, - expected: Browser.Other, + userAgent: USER_AGENTS.EDGE_MAC, + expected: Browser.Edge, }, { - userAgent: BrowserUserAgent.UcBrowserSupported, - expected: Browser.Other, + userAgent: USER_AGENTS.EDGE_IOS, + expected: Browser.Edge, }, { - userAgent: BrowserUserAgent.FacebookBrowseriOS, - expected: Browser.Safari, + userAgent: USER_AGENTS.EDGE_ANDROID, + expected: Browser.Edge, }, + + // safari { - userAgent: BrowserUserAgent.FacebookBrowserAndroid, - expected: Browser.Chrome, + userAgent: USER_AGENTS.SAFARI_MAC, + expected: Browser.Safari, }, { - userAgent: BrowserUserAgent.YandexMobileSupported, - expected: Browser.Other, + userAgent: USER_AGENTS.SAFARI_IPHONE, + expected: Browser.Safari, }, { - userAgent: BrowserUserAgent.ChromeTabletSupported, - expected: Browser.Chrome, + userAgent: USER_AGENTS.SAFARI_IPAD, + expected: Browser.Safari, }, { - userAgent: BrowserUserAgent.FirefoxTabletSupported, - expected: Browser.Firefox, + userAgent: USER_AGENTS.SAFARI_IPOD, + expected: Browser.Safari, }, { - userAgent: BrowserUserAgent.OperaTabletSupported, - expected: Browser.Other, + userAgent: USER_AGENTS.FACEBOOK_APP_IOS, + expected: Browser.Safari, }, + + // other { - userAgent: BrowserUserAgent.iPad, - expected: Browser.Safari, + userAgent: USER_AGENTS.OPERA_WINDOWS, + expected: Browser.Other, }, { - userAgent: BrowserUserAgent.iPhone, - expected: Browser.Safari, + userAgent: USER_AGENTS.YANDEX_WINDOWS, + expected: Browser.Other, }, { - userAgent: BrowserUserAgent.iPod, - expected: Browser.Safari, + userAgent: USER_AGENTS.VIVALDI_WINDOWS, + expected: Browser.Other, }, ].forEach(({ userAgent, expected }) => { - test(`should return the correct browser name for "${userAgent}"`, () => { + test(`"${userAgent}" should be ${expected}`, () => { mockUserAgent(userAgent); - expect(checkBrowserName(userAgent)).toBe(expected); expect(getBrowserName()).toBe(expected); }); }); diff --git a/src/shared/useragent/useragent.ts b/src/shared/useragent/useragent.ts index 62016a71f..63817ee29 100644 --- a/src/shared/useragent/useragent.ts +++ b/src/shared/useragent/useragent.ts @@ -1,72 +1,39 @@ import { Browser } from './constants'; +import { getBrowser, isMobile, isTablet } from './detect'; import type { BrowserValue } from './types'; -export function getBrowserName(): BrowserValue { - const ua = navigator.userAgent.toLowerCase(); - - if (ua.includes('edg/') || ua.includes('edge/')) return Browser.Edge; - if ( - (ua.includes('safari/') || ua.includes('applewebkit/')) && - !ua.includes('chrome/') - ) - return Browser.Safari; - - if ( - ua.includes('chrome/') && - !ua.includes('edg/') && - !ua.includes('edge/') && - !ua.includes('opr/') && - !ua.includes('opera/') && - !ua.includes('yabrowser/') && - !ua.includes('samsungbrowser/') && - !ua.includes('ucbrowser/') && - !ua.includes('vivaldi/') - ) - return Browser.Chrome; +// Reference: https://github.com/TimvanScherpenzeel/detect-ua/blob/master/src/index.ts - if (ua.includes('firefox/')) return Browser.Firefox; - - return Browser.Other; +export function getBrowserName(): BrowserValue { + const name = getBrowser(navigator.userAgent).name; + switch (name) { + case 'Chrome': + case 'Chromium': + return Browser.Chrome; + case 'Firefox': + return Browser.Firefox; + case 'Microsoft Edge': + return Browser.Edge; + case 'Safari': + return Browser.Safari; + default: + return Browser.Other; + } } export function getBrowserVersion(): number { - const ua = navigator.userAgent; - let version = NaN; - const browsers = [ - { name: 'Edge', regex: /edg\/([\d\.]+)/i }, - { name: 'Opera', regex: /(?:opr|opera)\/([\d\.]+)/i }, - { name: 'Chrome', regex: /chrome\/([\d\.]+)/i }, - { name: 'Safari', regex: /version\/([\d\.]+).*safari/i }, - { name: 'Firefox', regex: /firefox\/([\d\.]+)/i }, - ]; - - for (const browser of browsers) { - const match = ua.match(browser.regex); - if (match) { - const [major, minor = '0'] = match[1].split('.'); - version = +`${major}.${minor}`; - } - } - - return version; + const version = getBrowser(navigator.userAgent).version; + if (!version) return -1; + const [major, minor = '0'] = version.split('.'); + return +`${major}.${minor}`; } export function isMobileBrowser(): boolean { - const ua = navigator.userAgent.toLowerCase(); - if (isTabletBrowser(ua)) return false; - return /android|ipad|iphone|ipod|opera mini|iemobile|mobile/.test(ua); + return isMobile(navigator.userAgent); } -export function isTabletBrowser(ua: string = navigator.userAgent): boolean { - ua = ua.toLowerCase(); - - const isIPad = /\bipad\b/.test(ua); - const isAndroidTablet = /android/.test(ua) && !/mobile/.test(ua); // Android tablets don't include "mobile" - const isWindowsTablet = - /windows/.test(ua) && /touch/.test(ua) && !/phone/.test(ua); - const isKindleOrFire = /kindle|silk|kf[a-z]{2,}/.test(ua); // Amazon devices - - return isIPad || isAndroidTablet || isWindowsTablet || isKindleOrFire; +export function isTabletBrowser(): boolean { + return isTablet(navigator.userAgent); } export function requiresUserInteraction(): boolean { diff --git a/src/sw/serviceWorker/ServiceWorker.test.ts b/src/sw/serviceWorker/ServiceWorker.test.ts index 8b31cacd7..5fbaf5f7f 100644 --- a/src/sw/serviceWorker/ServiceWorker.test.ts +++ b/src/sw/serviceWorker/ServiceWorker.test.ts @@ -2,7 +2,7 @@ import { APP_ID, DUMMY_ONESIGNAL_ID, DUMMY_SUBSCRIPTION_ID, -} from '__test__/support/constants'; +} from '__test__/constants'; import TestContext from '__test__/support/environment/TestContext'; import { TestEnvironment } from '__test__/support/environment/TestEnvironment'; import { MockServiceWorker } from '__test__/support/mocks/MockServiceWorker'; From c895ec118a86c7a8f3f02929a44e6dcea6233ed5 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Wed, 30 Jul 2025 14:30:13 -0700 Subject: [PATCH 08/12] simplify user agent logic --- __test__/constants/useragent.ts | 14 +- src/shared/useragent/detect.ts | 381 +++++++++---------------- src/shared/useragent/useragent.test.ts | 136 ++++++++- 3 files changed, 276 insertions(+), 255 deletions(-) diff --git a/__test__/constants/useragent.ts b/__test__/constants/useragent.ts index b016666cf..627368166 100644 --- a/__test__/constants/useragent.ts +++ b/__test__/constants/useragent.ts @@ -6,9 +6,9 @@ export const USER_AGENTS = { CHROME_WINDOWS: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', CHROME_MAC: - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.1.0.0 Safari/537.36', CHROME_LINUX: - 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.2.0.0 Safari/537.36', CHROME_IOS_IPHONE: 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/139.0.7258.60 Mobile/15E148 Safari/604.1', CHROME_IOS_IPAD: @@ -16,9 +16,9 @@ export const USER_AGENTS = { CHROME_IOS_IPOD: 'Mozilla/5.0 (iPod; CPU iPhone OS 18_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/139.0.7258.60 Mobile/15E148 Safari/604.1', CHROME_ANDROID: - 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.180 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.7204.180 Mobile Safari/537.36', FIREFOX_WINDOWS: - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:141.0) Gecko/20100101 Firefox/141.0', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:141.0) Gecko/20100101 Firefox/141.1', FIREFOX_MAC: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 15.6; rv:141.0) Gecko/20100101 Firefox/141.0', FIREFOX_LINUX: @@ -36,9 +36,9 @@ export const USER_AGENTS = { SAFARI_IPOD: 'Mozilla/5.0 (iPod touch; CPU iPhone 18_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.4 Mobile/15E148 Safari/604.1', EDGE_WINDOWS: - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36 Edg/138.0.3351.109', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36 Edg/137.0.3351.109', EDGE_MAC: - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36 Edg/138.0.3351.109', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36 Edg/136.3351.109', EDGE_ANDROID: 'Mozilla/5.0 (Linux; Android 10; HD1913) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.180 Mobile Safari/537.36 EdgA/138.0.3351.98', EDGE_IOS: @@ -52,7 +52,7 @@ export const USER_AGENTS = { FACEBOOK_APP_IOS: 'Mozilla/5.0 (iPhone; CPU iPhone OS 12_4_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 [FBAN/FBIOS;FBAV/238.0.0.50.115;FBBV/171859800;FBDV/iPhone9,3;FBMD/iPhone;FBSN/iOS;FBSV/12.4.1;FBSS/2;FBID/phone;FBLC/en_US;FBOP/5;FBRV/172564136;FBCR/AT&T]', FACEBOOK_APP_ANDROID: - 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.180 Mobile Safari/537.36', + 'Mozilla/5.0 (Linux; Android 8.1.0; SM-J410G Build/M1AJB; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/131.0.6778.81 Mobile Safari/537.36[FBAN/EMA;FBLC/es_LA;FBAV/457.0.0.12.82;FBCX/modulariab;]', SAMSUNG_TABLET: 'Dalvik/2.1.0 (Linux; U; Android 14; SM-X306B Build/UP1A.231005.007)', GOOGLE_TABLET: diff --git a/src/shared/useragent/detect.ts b/src/shared/useragent/detect.ts index 871fbcce3..8ef1fe03f 100644 --- a/src/shared/useragent/detect.ts +++ b/src/shared/useragent/detect.ts @@ -1,65 +1,152 @@ -// types.ts -export interface IDeviceResult { - version: string; +interface BrowserConfig { + name: string; + pattern: RegExp; + versionPattern?: RegExp; } -export interface IBrowserResult { +interface IBrowserResult { name: string; version: string; } -// utils.ts -const isSSR = typeof window === 'undefined'; - -/** - * Get user agent string with SSR support - */ -export function getUserAgent(forceUserAgent?: string): string { - return forceUserAgent - ? forceUserAgent - : !isSSR && window.navigator - ? window.navigator.userAgent - : ''; -} - -/** - * Match entry based on position found in the user-agent string - */ -export function matchUserAgent( +const PATTERNS = { + LIKE_ANDROID: /like android/i, + ANDROID: /android/i, + IOS_DEVICES: /(iphone|ipod|ipad)/i, + TABLET: /tablet/i, + TABLET_PC: /tablet pc/i, + MOBILE: /[^-]mobi/i, + NEXUS_MOBILE: /nexus\s*[0-6]\s*/i, + NEXUS_TABLET: /nexus\s*[0-9]+/i, + GENERIC_VERSION: /version\/(\d+(?:\.\d+)?)/i, +} as const; + +// Ordered from most specific to least specific +const BROWSER_CONFIGS: BrowserConfig[] = [ + { + name: 'Opera', + pattern: /(?:opera|opr|opios)/i, + versionPattern: /(?:opera|opr|opios)[\s\/](\d+(?:\.\d+)?)/i, + }, + { + name: 'Facebook', + pattern: /FBAN\//i, + versionPattern: /FBAV\/(\d+(?:\.\d+)?)/i, + }, + { + name: 'Samsung Internet for Android', + pattern: /samsungbrowser/i, + versionPattern: /samsungbrowser[\s\/](\d+(?:\.\d+)?)/i, + }, + { + name: 'Yandex Browser', + pattern: /yabrowser/i, + versionPattern: /yabrowser[\s\/](\d+(?:\.\d+)?)/i, + }, + { + name: 'Vivaldi', + pattern: /vivaldi/i, + versionPattern: /vivaldi[\s\/](\d+(?:\.\d+)?)/i, + }, + { + name: 'UC Browser', + pattern: /ucbrowser/i, + versionPattern: /ucbrowser[\s\/](\d+(?:\.\d+)?)/i, + }, + { + name: 'Microsoft Edge', + pattern: /(edge|edgios|edga|edg)/i, + versionPattern: /(edge|edgios|edga|edg)[\s\/](\d+(?:\.\d+)?)/i, + }, + { + name: 'Firefox', + pattern: /firefox|iceweasel|fxios/i, + versionPattern: /(?:firefox|iceweasel|fxios)[\s\/](\d+(?:\.\d+)?)/i, + }, + { + name: 'Chromium', + pattern: /chromium/i, + versionPattern: /chromium[\s\/](\d+(?:\.\d+)?)/i, + }, + { + name: 'Chrome', + pattern: /chrome|crios|crmo/i, + versionPattern: /(?:chrome|crios|crmo)[\s\/](\d+(?:\.\d+)?)/i, + }, + { + name: 'Safari', + pattern: /safari|applewebkit/i, + versionPattern: /version[\s\/](\d+(?:\.\d+)?)/i, + }, +]; + +const matchUserAgent = ( userAgent: string, position: number, pattern: RegExp, -): string { +): string => { const match = userAgent.match(pattern); - return (match && match.length > 1 && match[position]) || ''; + return match?.[position] || ''; +}; + +const safeNavigator = () => + typeof navigator !== 'undefined' ? navigator : null; +const safeWindow = () => (typeof window !== 'undefined' ? window : null); + +function extractVersion(userAgent: string, config: BrowserConfig): string { + if (config.versionPattern) { + const match = userAgent.match(config.versionPattern); + if (match) { + const index = config.versionPattern.source.includes('(') + ? config.name === 'Microsoft Edge' + ? 2 + : 1 + : 1; + return match[index] || ''; + } + } + return matchUserAgent(userAgent, 1, PATTERNS.GENERIC_VERSION); } -// device-detection.ts -/** - * Check if device is Android - */ -export function isAndroidDevice(userAgent: string): boolean { - return !/like android/i.test(userAgent) && /android/i.test(userAgent); +export function getBrowser(userAgent: string): IBrowserResult { + if (!userAgent) return { name: 'Unknown', version: '' }; + + for (const config of BROWSER_CONFIGS) { + if (config.pattern.test(userAgent)) { + return { name: config.name, version: extractVersion(userAgent, config) }; + } + } + return { + name: matchUserAgent(userAgent, 1, /^(.*?)[\s\/]/) || 'Unknown', + version: matchUserAgent(userAgent, 2, /^(.*?)[\s\/](.+?)[\s]/) || '', + }; +} + +function isAndroidDevice(userAgent: string): boolean { + return ( + !!userAgent && + !PATTERNS.LIKE_ANDROID.test(userAgent) && + PATTERNS.ANDROID.test(userAgent) + ); } -/** - * Get iOS device type (iphone, ipod, ipad, or empty string) - */ export function getIOSDeviceType(userAgent: string): string { + if (!userAgent) return ''; + let deviceType = matchUserAgent( userAgent, 1, - /(iphone|ipod|ipad)/i, + PATTERNS.IOS_DEVICES, ).toLowerCase(); - // Workaround for ipadOS, force detection as tablet - // SEE: https://github.com/lancedikson/bowser/issues/329 - // SEE: https://stackoverflow.com/questions/58019463/how-to-detect-device-name-in-safari-on-ios-13-while-it-doesnt-show-the-correct + // iPadOS workaround + const nav = safeNavigator(); + const win = safeWindow(); if ( - !isSSR && - navigator.platform === 'MacIntel' && - navigator.maxTouchPoints > 2 && - !(window as any).MSStream + !deviceType && + nav?.platform === 'MacIntel' && + nav?.maxTouchPoints > 2 && + !(win as { MSStream?: unknown })?.MSStream ) { deviceType = 'ipad'; } @@ -67,226 +154,34 @@ export function getIOSDeviceType(userAgent: string): string { return deviceType; } -/** - * Check if device is a tablet - */ export function isTablet(userAgent: string): boolean { + if (!userAgent) return false; + const isAndroid = isAndroidDevice(userAgent); const iOSDevice = getIOSDeviceType(userAgent); return ( - // Default tablet - (/tablet/i.test(userAgent) && !/tablet pc/i.test(userAgent)) || - // iPad + (PATTERNS.TABLET.test(userAgent) && !PATTERNS.TABLET_PC.test(userAgent)) || iOSDevice === 'ipad' || - // Android tablet - (isAndroid && !/[^-]mobi/i.test(userAgent)) || - // Nexus tablet - (!/nexus\s*[0-6]\s*/i.test(userAgent) && /nexus\s*[0-9]+/i.test(userAgent)) + (isAndroid && !PATTERNS.MOBILE.test(userAgent)) || + (!PATTERNS.NEXUS_MOBILE.test(userAgent) && + PATTERNS.NEXUS_TABLET.test(userAgent)) ); } -/** - * Check if device is mobile - */ export function isMobile(userAgent: string): boolean { + if (!userAgent) return false; + const isTabletDevice = isTablet(userAgent); const isAndroid = isAndroidDevice(userAgent); const iOSDevice = getIOSDeviceType(userAgent); return ( - // Default mobile !isTabletDevice && - (/[^-]mobi/i.test(userAgent) || - // iPhone / iPod + (PATTERNS.MOBILE.test(userAgent) || iOSDevice === 'iphone' || iOSDevice === 'ipod' || - // Android mobile isAndroid || - // Nexus mobile - /nexus\s*[0-6]\s*/i.test(userAgent)) + PATTERNS.NEXUS_MOBILE.test(userAgent)) ); } - -/** - * Check if device is desktop - */ -export function isDesktop(userAgent: string): boolean { - return !isMobile(userAgent) && !isTablet(userAgent); -} - -// os-detection.ts -/** - * Check if device is running macOS and return version info - */ -export function isMacOS(userAgent: string): IDeviceResult | false { - if (!/macintosh/i.test(userAgent)) { - return false; - } - - const version = matchUserAgent(userAgent, 1, /mac os x (\d+(\.?_?\d+)+)/i) - .replace(/[_\s]/g, '.') - .split('.') - .map((versionNumber: string): string => versionNumber)[1]; - - return { version }; -} - -/** - * Check if device is running Windows and return version info - */ -export function isWindows(userAgent: string): IDeviceResult | false { - if (!/windows /i.test(userAgent)) { - return false; - } - - const version = matchUserAgent( - userAgent, - 1, - /Windows ((NT|XP)( \d\d?.\d)?)/i, - ); - return { version }; -} - -/** - * Check if device is running iOS and return version info - */ -export function isiOS(userAgent: string): IDeviceResult | false { - const iOSDevice = getIOSDeviceType(userAgent); - - if (!iOSDevice) { - return false; - } - - const version = - matchUserAgent(userAgent, 1, /os (\d+([_\s]\d+)*) like mac os x/i).replace( - /[_\s]/g, - '.', - ) || matchUserAgent(userAgent, 1, /version\/(\d+(\.\d+)?)/i); - - return { version }; -} - -/** - * Check if device is running Android and return version info - */ -export function isAndroid(userAgent: string): IDeviceResult | false { - if (!isAndroidDevice(userAgent)) { - return false; - } - - const version = matchUserAgent(userAgent, 1, /android[ \/-](\d+(\.\d+)*)/i); - return { version }; -} - -// browser-detection.ts -/** - * Detect browser name and version - */ -export function getBrowser(userAgent: string): IBrowserResult { - const versionIdentifier = matchUserAgent( - userAgent, - 1, - /version\/(\d+(\.\d+)?)/i, - ); - - if (/opera/i.test(userAgent)) { - // Opera - return { - name: 'Opera', - version: - versionIdentifier || - matchUserAgent(userAgent, 1, /(?:opera|opr|opios)[\s\/](\d+(\.\d+)?)/i), - }; - } else if (/opr\/|opios/i.test(userAgent)) { - // Opera - return { - name: 'Opera', - version: - matchUserAgent(userAgent, 1, /(?:opr|opios)[\s\/](\d+(\.\d+)?)/i) || - versionIdentifier, - }; - } else if (/SamsungBrowser/i.test(userAgent)) { - // Samsung Browser - return { - name: 'Samsung Internet for Android', - version: - versionIdentifier || - matchUserAgent(userAgent, 1, /(?:SamsungBrowser)[\s\/](\d+(\.\d+)?)/i), - }; - } else if (/yabrowser/i.test(userAgent)) { - // Yandex Browser - return { - name: 'Yandex Browser', - version: - versionIdentifier || - matchUserAgent(userAgent, 1, /(?:yabrowser)[\s\/](\d+(\.\d+)?)/i), - }; - } else if (/ucbrowser/i.test(userAgent)) { - // UC Browser - return { - name: 'UC Browser', - version: matchUserAgent( - userAgent, - 1, - /(?:ucbrowser)[\s\/](\d+(\.\d+)?)/i, - ), - }; - } else if (/msie|trident/i.test(userAgent)) { - // Internet Explorer - return { - name: 'Internet Explorer', - version: matchUserAgent(userAgent, 1, /(?:msie |rv:)(\d+(\.\d+)?)/i), - }; - } else if (/(edge|edgios|edga|edg)/i.test(userAgent)) { - // Edge - return { - name: 'Microsoft Edge', - version: matchUserAgent( - userAgent, - 2, - /(edge|edgios|edga|edg)\/(\d+(\.\d+)?)/i, - ), - }; - } else if (/firefox|iceweasel|fxios/i.test(userAgent)) { - // Firefox - return { - name: 'Firefox', - version: matchUserAgent( - userAgent, - 1, - /(?:firefox|iceweasel|fxios)[ \/](\d+(\.\d+)?)/i, - ), - }; - } else if (/chromium/i.test(userAgent)) { - // Chromium - return { - name: 'Chromium', - version: - matchUserAgent(userAgent, 1, /(?:chromium)[\s\/](\d+(?:\.\d+)?)/i) || - versionIdentifier, - }; - } else if (/chrome|crios|crmo/i.test(userAgent)) { - // Chrome - return { - name: 'Chrome', - version: matchUserAgent( - userAgent, - 1, - /(?:chrome|crios|crmo)\/(\d+(\.\d+)?)/i, - ), - }; - } else if (/safari|applewebkit/i.test(userAgent)) { - // Safari - return { - name: 'Safari', - version: versionIdentifier, - }; - } else { - // Everything else - return { - name: matchUserAgent(userAgent, 1, /^(.*)\/(.*) /), - version: matchUserAgent(userAgent, 2, /^(.*)\/(.*) /), - }; - } -} diff --git a/src/shared/useragent/useragent.test.ts b/src/shared/useragent/useragent.test.ts index 006d71237..1a636c374 100644 --- a/src/shared/useragent/useragent.test.ts +++ b/src/shared/useragent/useragent.test.ts @@ -1,6 +1,11 @@ import { USER_AGENTS } from '__test__/constants'; import { Browser } from './constants'; -import { getBrowserName, isMobileBrowser, isTabletBrowser } from './useragent'; +import { + getBrowserName, + getBrowserVersion, + isMobileBrowser, + isTabletBrowser, +} from './useragent'; const mockUserAgent = (userAgent: string) => { Object.defineProperty(navigator, 'userAgent', { @@ -274,10 +279,6 @@ describe('getBrowserName()', () => { userAgent: USER_AGENTS.SAFARI_IPOD, expected: Browser.Safari, }, - { - userAgent: USER_AGENTS.FACEBOOK_APP_IOS, - expected: Browser.Safari, - }, // other { @@ -292,6 +293,14 @@ describe('getBrowserName()', () => { userAgent: USER_AGENTS.VIVALDI_WINDOWS, expected: Browser.Other, }, + { + userAgent: USER_AGENTS.FACEBOOK_APP_ANDROID, + expected: Browser.Other, + }, + { + userAgent: USER_AGENTS.FACEBOOK_APP_IOS, + expected: Browser.Other, + }, ].forEach(({ userAgent, expected }) => { test(`"${userAgent}" should be ${expected}`, () => { mockUserAgent(userAgent); @@ -299,3 +308,120 @@ describe('getBrowserName()', () => { }); }); }); + +describe('getBrowserVersion()', () => { + [ + // chrome + { + userAgent: USER_AGENTS.CHROME_WINDOWS, + expected: 138, + }, + { + userAgent: USER_AGENTS.CHROME_MAC, + expected: 138.1, + }, + { + userAgent: USER_AGENTS.CHROME_LINUX, + expected: 138.2, + }, + { + userAgent: USER_AGENTS.CHROME_ANDROID, + expected: 137.0, + }, + { + userAgent: USER_AGENTS.CHROME_IOS_IPHONE, + expected: 139.0, + }, + { + userAgent: USER_AGENTS.CHROME_IOS_IPAD, + expected: 139.0, + }, + { + userAgent: USER_AGENTS.CHROME_IOS_IPOD, + expected: 139.0, + }, + + // firefox + { + userAgent: USER_AGENTS.FIREFOX_WINDOWS, + expected: 141.1, + }, + { + userAgent: USER_AGENTS.FIREFOX_MAC, + expected: 141.0, + }, + { + userAgent: USER_AGENTS.FIREFOX_LINUX, + expected: 141.0, + }, + { + userAgent: USER_AGENTS.FIREFOX_ANDROID, + expected: 141.0, + }, + { + userAgent: USER_AGENTS.FIREFOX_IOS, + expected: 141.0, + }, + + // edge + { + userAgent: USER_AGENTS.EDGE_WINDOWS, + expected: 137.0, + }, + { + userAgent: USER_AGENTS.EDGE_MAC, + expected: 136.3351, + }, + + // safari + { + userAgent: USER_AGENTS.SAFARI_MAC, + expected: 18.4, + }, + { + userAgent: USER_AGENTS.SAFARI_IPHONE, + expected: 18.4, + }, + { + userAgent: USER_AGENTS.SAFARI_IPAD, + expected: 18.4, + }, + { + userAgent: USER_AGENTS.SAFARI_IPOD, + expected: 18.4, + }, + + // opera + { + userAgent: USER_AGENTS.OPERA_WINDOWS, + expected: 120.0, + }, + + // yandex + { + userAgent: USER_AGENTS.YANDEX_WINDOWS, + expected: 25.6, + }, + + // vivaldi + { + userAgent: USER_AGENTS.VIVALDI_WINDOWS, + expected: 7.5, + }, + + // facebook app + { + userAgent: USER_AGENTS.FACEBOOK_APP_IOS, + expected: 238.0, + }, + { + userAgent: USER_AGENTS.FACEBOOK_APP_ANDROID, + expected: 457.0, + }, + ].forEach(({ userAgent, expected }) => { + test(`"${userAgent}" should be ${expected}`, () => { + mockUserAgent(userAgent); + expect(getBrowserVersion()).toBe(expected); + }); + }); +}); From 78e312c3159e8d473b3c85cf872ce168b86fbae4 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Wed, 30 Jul 2025 14:49:09 -0700 Subject: [PATCH 09/12] more improvements to user agent logic --- src/shared/useragent/detect.ts | 59 +++++++------------------------ src/shared/useragent/useragent.ts | 48 +++++++++---------------- 2 files changed, 28 insertions(+), 79 deletions(-) diff --git a/src/shared/useragent/detect.ts b/src/shared/useragent/detect.ts index 8ef1fe03f..206005706 100644 --- a/src/shared/useragent/detect.ts +++ b/src/shared/useragent/detect.ts @@ -21,7 +21,6 @@ const PATTERNS = { GENERIC_VERSION: /version\/(\d+(?:\.\d+)?)/i, } as const; -// Ordered from most specific to least specific const BROWSER_CONFIGS: BrowserConfig[] = [ { name: 'Opera', @@ -55,8 +54,8 @@ const BROWSER_CONFIGS: BrowserConfig[] = [ }, { name: 'Microsoft Edge', - pattern: /(edge|edgios|edga|edg)/i, - versionPattern: /(edge|edgios|edga|edg)[\s\/](\d+(?:\.\d+)?)/i, + pattern: /(?:edge|edgios|edga|edg)/i, + versionPattern: /(?:edge|edgios|edga|edg)[\s\/](\d+(?:\.\d+)?)/i, }, { name: 'Firefox', @@ -84,33 +83,16 @@ const matchUserAgent = ( userAgent: string, position: number, pattern: RegExp, -): string => { - const match = userAgent.match(pattern); - return match?.[position] || ''; -}; - -const safeNavigator = () => - typeof navigator !== 'undefined' ? navigator : null; -const safeWindow = () => (typeof window !== 'undefined' ? window : null); +): string => userAgent.match(pattern)?.[position] || ''; function extractVersion(userAgent: string, config: BrowserConfig): string { - if (config.versionPattern) { - const match = userAgent.match(config.versionPattern); - if (match) { - const index = config.versionPattern.source.includes('(') - ? config.name === 'Microsoft Edge' - ? 2 - : 1 - : 1; - return match[index] || ''; - } - } - return matchUserAgent(userAgent, 1, PATTERNS.GENERIC_VERSION); + if (!config.versionPattern) + return matchUserAgent(userAgent, 1, PATTERNS.GENERIC_VERSION); + const match = userAgent.match(config.versionPattern); + return match?.[1] || ''; } export function getBrowser(userAgent: string): IBrowserResult { - if (!userAgent) return { name: 'Unknown', version: '' }; - for (const config of BROWSER_CONFIGS) { if (config.pattern.test(userAgent)) { return { name: config.name, version: extractVersion(userAgent, config) }; @@ -122,44 +104,30 @@ export function getBrowser(userAgent: string): IBrowserResult { }; } -function isAndroidDevice(userAgent: string): boolean { - return ( - !!userAgent && - !PATTERNS.LIKE_ANDROID.test(userAgent) && - PATTERNS.ANDROID.test(userAgent) - ); -} +const isAndroidDevice = (userAgent: string): boolean => + !PATTERNS.LIKE_ANDROID.test(userAgent) && PATTERNS.ANDROID.test(userAgent); export function getIOSDeviceType(userAgent: string): string { - if (!userAgent) return ''; - let deviceType = matchUserAgent( userAgent, 1, PATTERNS.IOS_DEVICES, ).toLowerCase(); - // iPadOS workaround - const nav = safeNavigator(); - const win = safeWindow(); if ( !deviceType && - nav?.platform === 'MacIntel' && - nav?.maxTouchPoints > 2 && - !(win as { MSStream?: unknown })?.MSStream + navigator.platform === 'MacIntel' && + navigator.maxTouchPoints > 2 && + !(window as { MSStream?: unknown })?.MSStream ) { deviceType = 'ipad'; } - return deviceType; } export function isTablet(userAgent: string): boolean { - if (!userAgent) return false; - const isAndroid = isAndroidDevice(userAgent); const iOSDevice = getIOSDeviceType(userAgent); - return ( (PATTERNS.TABLET.test(userAgent) && !PATTERNS.TABLET_PC.test(userAgent)) || iOSDevice === 'ipad' || @@ -170,12 +138,9 @@ export function isTablet(userAgent: string): boolean { } export function isMobile(userAgent: string): boolean { - if (!userAgent) return false; - const isTabletDevice = isTablet(userAgent); const isAndroid = isAndroidDevice(userAgent); const iOSDevice = getIOSDeviceType(userAgent); - return ( !isTabletDevice && (PATTERNS.MOBILE.test(userAgent) || diff --git a/src/shared/useragent/useragent.ts b/src/shared/useragent/useragent.ts index 63817ee29..81ed5fa4e 100644 --- a/src/shared/useragent/useragent.ts +++ b/src/shared/useragent/useragent.ts @@ -2,49 +2,33 @@ import { Browser } from './constants'; import { getBrowser, isMobile, isTablet } from './detect'; import type { BrowserValue } from './types'; -// Reference: https://github.com/TimvanScherpenzeel/detect-ua/blob/master/src/index.ts +const BROWSER_MAP: Record = { + Chrome: Browser.Chrome, + Chromium: Browser.Chrome, + Firefox: Browser.Firefox, + 'Microsoft Edge': Browser.Edge, + Safari: Browser.Safari, +}; export function getBrowserName(): BrowserValue { - const name = getBrowser(navigator.userAgent).name; - switch (name) { - case 'Chrome': - case 'Chromium': - return Browser.Chrome; - case 'Firefox': - return Browser.Firefox; - case 'Microsoft Edge': - return Browser.Edge; - case 'Safari': - return Browser.Safari; - default: - return Browser.Other; - } + return BROWSER_MAP[getBrowser(navigator.userAgent).name] || Browser.Other; } -export function getBrowserVersion(): number { +export const getBrowserVersion = (): number => { const version = getBrowser(navigator.userAgent).version; if (!version) return -1; const [major, minor = '0'] = version.split('.'); return +`${major}.${minor}`; -} - -export function isMobileBrowser(): boolean { - return isMobile(navigator.userAgent); -} +}; -export function isTabletBrowser(): boolean { - return isTablet(navigator.userAgent); -} +export const isMobileBrowser = (): boolean => isMobile(navigator.userAgent); +export const isTabletBrowser = (): boolean => isTablet(navigator.userAgent); export function requiresUserInteraction(): boolean { const browserName = getBrowserName(); const version = getBrowserVersion(); - - // Firefox 72+ requires user-interaction - if (browserName === Browser.Firefox && version >= 72) return true; - - // Safari 12.1+ requires user-interaction - if (browserName === Browser.Safari && version >= 12.1) return true; - - return false; + return ( + (browserName === Browser.Firefox && version >= 72) || + (browserName === Browser.Safari && version >= 12.1) + ); } From 2f56b2e9cab536d8f97c3b0dfc4ad61807d60104 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Wed, 30 Jul 2025 18:30:46 -0700 Subject: [PATCH 10/12] more improvements --- package.json | 4 +- src/shared/useragent/detect.ts | 114 +++++++++--------------------- src/shared/useragent/useragent.ts | 7 +- 3 files changed, 39 insertions(+), 86 deletions(-) diff --git a/package.json b/package.json index 3e7866500..9dbc6284a 100644 --- a/package.json +++ b/package.json @@ -80,12 +80,12 @@ }, { "path": "./build/releases/OneSignalSDK.page.es6.js", - "limit": "60.5 kB", + "limit": "60.55 kB", "gzip": true }, { "path": "./build/releases/OneSignalSDK.sw.js", - "limit": "25.1 kB", + "limit": "25.25 kB", "gzip": true }, { diff --git a/src/shared/useragent/detect.ts b/src/shared/useragent/detect.ts index 206005706..05da99ee0 100644 --- a/src/shared/useragent/detect.ts +++ b/src/shared/useragent/detect.ts @@ -1,7 +1,7 @@ interface BrowserConfig { name: string; pattern: RegExp; - versionPattern?: RegExp; + versionPattern: RegExp; } interface IBrowserResult { @@ -9,23 +9,11 @@ interface IBrowserResult { version: string; } -const PATTERNS = { - LIKE_ANDROID: /like android/i, - ANDROID: /android/i, - IOS_DEVICES: /(iphone|ipod|ipad)/i, - TABLET: /tablet/i, - TABLET_PC: /tablet pc/i, - MOBILE: /[^-]mobi/i, - NEXUS_MOBILE: /nexus\s*[0-6]\s*/i, - NEXUS_TABLET: /nexus\s*[0-9]+/i, - GENERIC_VERSION: /version\/(\d+(?:\.\d+)?)/i, -} as const; - const BROWSER_CONFIGS: BrowserConfig[] = [ { name: 'Opera', pattern: /(?:opera|opr|opios)/i, - versionPattern: /(?:opera|opr|opios)[\s\/](\d+(?:\.\d+)?)/i, + versionPattern: /(?:opera|opr|opios)[ /](\d+(?:\.\d+)?)/i, }, { name: 'Facebook', @@ -33,120 +21,86 @@ const BROWSER_CONFIGS: BrowserConfig[] = [ versionPattern: /FBAV\/(\d+(?:\.\d+)?)/i, }, { - name: 'Samsung Internet for Android', + name: 'Samsung Browser', pattern: /samsungbrowser/i, - versionPattern: /samsungbrowser[\s\/](\d+(?:\.\d+)?)/i, + versionPattern: /samsungbrowser[ /](\d+(?:\.\d+)?)/i, }, { name: 'Yandex Browser', pattern: /yabrowser/i, - versionPattern: /yabrowser[\s\/](\d+(?:\.\d+)?)/i, + versionPattern: /yabrowser[ /](\d+(?:\.\d+)?)/i, }, { name: 'Vivaldi', pattern: /vivaldi/i, - versionPattern: /vivaldi[\s\/](\d+(?:\.\d+)?)/i, + versionPattern: /vivaldi[ /](\d+(?:\.\d+)?)/i, }, { name: 'UC Browser', pattern: /ucbrowser/i, - versionPattern: /ucbrowser[\s\/](\d+(?:\.\d+)?)/i, + versionPattern: /ucbrowser[ /](\d+(?:\.\d+)?)/i, }, { name: 'Microsoft Edge', - pattern: /(?:edge|edgios|edga|edg)/i, - versionPattern: /(?:edge|edgios|edga|edg)[\s\/](\d+(?:\.\d+)?)/i, + pattern: /edg/i, + versionPattern: /edg[ /](\d+(?:\.\d+)?)/i, }, { name: 'Firefox', pattern: /firefox|iceweasel|fxios/i, - versionPattern: /(?:firefox|iceweasel|fxios)[\s\/](\d+(?:\.\d+)?)/i, + versionPattern: /(?:firefox|iceweasel|fxios)[ /](\d+(?:\.\d+)?)/i, }, { name: 'Chromium', pattern: /chromium/i, - versionPattern: /chromium[\s\/](\d+(?:\.\d+)?)/i, + versionPattern: /chromium[ /](\d+(?:\.\d+)?)/i, }, { name: 'Chrome', pattern: /chrome|crios|crmo/i, - versionPattern: /(?:chrome|crios|crmo)[\s\/](\d+(?:\.\d+)?)/i, + versionPattern: /(?:chrome|crios|crmo)[ /](\d+(?:\.\d+)?)/i, }, { name: 'Safari', pattern: /safari|applewebkit/i, - versionPattern: /version[\s\/](\d+(?:\.\d+)?)/i, + versionPattern: /version[ /](\d+(?:\.\d+)?)/i, }, ]; -const matchUserAgent = ( - userAgent: string, - position: number, - pattern: RegExp, -): string => userAgent.match(pattern)?.[position] || ''; - -function extractVersion(userAgent: string, config: BrowserConfig): string { - if (!config.versionPattern) - return matchUserAgent(userAgent, 1, PATTERNS.GENERIC_VERSION); - const match = userAgent.match(config.versionPattern); - return match?.[1] || ''; -} - export function getBrowser(userAgent: string): IBrowserResult { for (const config of BROWSER_CONFIGS) { if (config.pattern.test(userAgent)) { - return { name: config.name, version: extractVersion(userAgent, config) }; + const version = userAgent.match(config.versionPattern)?.[1] ?? ''; + return { name: config.name, version }; } } - return { - name: matchUserAgent(userAgent, 1, /^(.*?)[\s\/]/) || 'Unknown', - version: matchUserAgent(userAgent, 2, /^(.*?)[\s\/](.+?)[\s]/) || '', - }; + return { name: 'Unknown', version: '' }; } +const isAndroid = (userAgent: string): boolean => + !/like android/i.test(userAgent) && /android/i.test(userAgent); -const isAndroidDevice = (userAgent: string): boolean => - !PATTERNS.LIKE_ANDROID.test(userAgent) && PATTERNS.ANDROID.test(userAgent); +const getIOSDeviceType = (userAgent: string): string => + userAgent.match(/(iphone|ipod|ipad)/i)?.[1]?.toLowerCase() || ''; -export function getIOSDeviceType(userAgent: string): string { - let deviceType = matchUserAgent( - userAgent, - 1, - PATTERNS.IOS_DEVICES, - ).toLowerCase(); +export function isTabletBrowser(userAgent = navigator.userAgent): boolean { + const ios = getIOSDeviceType(userAgent); - if ( - !deviceType && - navigator.platform === 'MacIntel' && - navigator.maxTouchPoints > 2 && - !(window as { MSStream?: unknown })?.MSStream - ) { - deviceType = 'ipad'; - } - return deviceType; -} - -export function isTablet(userAgent: string): boolean { - const isAndroid = isAndroidDevice(userAgent); - const iOSDevice = getIOSDeviceType(userAgent); return ( - (PATTERNS.TABLET.test(userAgent) && !PATTERNS.TABLET_PC.test(userAgent)) || - iOSDevice === 'ipad' || - (isAndroid && !PATTERNS.MOBILE.test(userAgent)) || - (!PATTERNS.NEXUS_MOBILE.test(userAgent) && - PATTERNS.NEXUS_TABLET.test(userAgent)) + (/tablet/i.test(userAgent) && !/tablet pc/i.test(userAgent)) || + ios === 'ipad' || + (isAndroid(userAgent) && !/[^-]mobi/i.test(userAgent)) || + (!/nexus\s*[0-6]\s*/i.test(userAgent) && /nexus\s*\d+/i.test(userAgent)) ); } -export function isMobile(userAgent: string): boolean { - const isTabletDevice = isTablet(userAgent); - const isAndroid = isAndroidDevice(userAgent); - const iOSDevice = getIOSDeviceType(userAgent); +export function isMobileBrowser(userAgent = navigator.userAgent): boolean { + if (isTabletBrowser(userAgent)) return false; + + const ios = getIOSDeviceType(userAgent); return ( - !isTabletDevice && - (PATTERNS.MOBILE.test(userAgent) || - iOSDevice === 'iphone' || - iOSDevice === 'ipod' || - isAndroid || - PATTERNS.NEXUS_MOBILE.test(userAgent)) + /[^-]mobi/i.test(userAgent) || + ios === 'iphone' || + ios === 'ipod' || + isAndroid(userAgent) ); } diff --git a/src/shared/useragent/useragent.ts b/src/shared/useragent/useragent.ts index 81ed5fa4e..ba405b65a 100644 --- a/src/shared/useragent/useragent.ts +++ b/src/shared/useragent/useragent.ts @@ -1,7 +1,9 @@ import { Browser } from './constants'; -import { getBrowser, isMobile, isTablet } from './detect'; +import { getBrowser } from './detect'; import type { BrowserValue } from './types'; +export { isMobileBrowser, isTabletBrowser } from './detect'; + const BROWSER_MAP: Record = { Chrome: Browser.Chrome, Chromium: Browser.Chrome, @@ -21,9 +23,6 @@ export const getBrowserVersion = (): number => { return +`${major}.${minor}`; }; -export const isMobileBrowser = (): boolean => isMobile(navigator.userAgent); -export const isTabletBrowser = (): boolean => isTablet(navigator.userAgent); - export function requiresUserInteraction(): boolean { const browserName = getBrowserName(); const version = getBrowserVersion(); From b7667b033830766ae333b1bef7321d3ff28d89cd Mon Sep 17 00:00:00 2001 From: Fadi George Date: Thu, 31 Jul 2025 16:23:18 -0700 Subject: [PATCH 11/12] address merge conflicts --- .../environment/TestEnvironmentHelpers.ts | 1 + __test__/unit/core/osModel.test.ts | 1 + package.json | 4 ++-- src/onesignal/User.ts | 13 ++++++++++++ src/onesignal/UserNamespace.test.ts | 2 ++ .../userModel/FuturePushSubscriptionRecord.ts | 7 ++++++- src/shared/environment/environment.ts | 20 +++++++++++-------- .../managers/SubscriptionManager.test.ts | 2 +- src/sw/serviceWorker/ServiceWorker.ts | 2 +- 9 files changed, 39 insertions(+), 13 deletions(-) diff --git a/__test__/support/environment/TestEnvironmentHelpers.ts b/__test__/support/environment/TestEnvironmentHelpers.ts index d135b28c6..40d322e30 100644 --- a/__test__/support/environment/TestEnvironmentHelpers.ts +++ b/__test__/support/environment/TestEnvironmentHelpers.ts @@ -18,6 +18,7 @@ import Database from '../../../src/shared/services/Database'; import { CUSTOM_LINK_CSS_CLASSES } from '../../../src/shared/slidedown/constants'; import { DEFAULT_USER_AGENT, + DEVICE_OS, DUMMY_ONESIGNAL_ID, DUMMY_SUBSCRIPTION_ID_3, } from '../../constants'; diff --git a/__test__/unit/core/osModel.test.ts b/__test__/unit/core/osModel.test.ts index acb96dc32..da3ee3784 100644 --- a/__test__/unit/core/osModel.test.ts +++ b/__test__/unit/core/osModel.test.ts @@ -1,3 +1,4 @@ +import { DEVICE_OS } from '__test__/constants'; import { SubscriptionModel } from 'src/core/models/SubscriptionModel'; import { SubscriptionType } from 'src/core/types/subscription'; import { generateNewSubscription } from '../../support/helpers/core'; diff --git a/package.json b/package.json index 9dbc6284a..a44c8de43 100644 --- a/package.json +++ b/package.json @@ -80,12 +80,12 @@ }, { "path": "./build/releases/OneSignalSDK.page.es6.js", - "limit": "60.55 kB", + "limit": "61.1 kB", "gzip": true }, { "path": "./build/releases/OneSignalSDK.sw.js", - "limit": "25.25 kB", + "limit": "25.1 kB", "gzip": true }, { diff --git a/src/onesignal/User.ts b/src/onesignal/User.ts index 1ed044572..612b34db7 100644 --- a/src/onesignal/User.ts +++ b/src/onesignal/User.ts @@ -293,3 +293,16 @@ export default class User { }); } } + +/** + * Returns true if the value is a JSON-serializable object. + */ +function isObjectSerializable(value: unknown): boolean { + if (!isObject(value)) return false; + try { + JSON.stringify(value); + return true; + } catch (e) { + return false; + } +} diff --git a/src/onesignal/UserNamespace.test.ts b/src/onesignal/UserNamespace.test.ts index 16947d9fe..cbf45583a 100644 --- a/src/onesignal/UserNamespace.test.ts +++ b/src/onesignal/UserNamespace.test.ts @@ -1,4 +1,6 @@ import { DUMMY_ONESIGNAL_ID, DUMMY_PUSH_TOKEN } from '__test__/constants'; +import { ModelChangeTags } from 'src/core/types/models'; +import Log from 'src/shared/libraries/Log'; import { IDManager } from 'src/shared/managers/IDManager'; import { TestEnvironment } from '../../__test__/support/environment/TestEnvironment'; import type { UserChangeEvent } from '../page/models/UserChangeEvent'; diff --git a/src/page/userModel/FuturePushSubscriptionRecord.ts b/src/page/userModel/FuturePushSubscriptionRecord.ts index 7deb4f717..70ea04f41 100644 --- a/src/page/userModel/FuturePushSubscriptionRecord.ts +++ b/src/page/userModel/FuturePushSubscriptionRecord.ts @@ -2,11 +2,16 @@ import type { NotificationTypeValue, SubscriptionTypeValue, } from 'src/core/types/subscription'; -import { NotificationType } from 'src/core/types/subscription'; +import { + NotificationType, + SubscriptionType, +} from 'src/core/types/subscription'; import { getDeviceModel, getDeviceOS, getSubscriptionType, + useSafariLegacyPush, + useSafariVapidPush, } from 'src/shared/environment'; import { RawPushSubscription } from 'src/shared/models/RawPushSubscription'; import { diff --git a/src/shared/environment/environment.ts b/src/shared/environment/environment.ts index f0d9692ae..5ff6c837a 100644 --- a/src/shared/environment/environment.ts +++ b/src/shared/environment/environment.ts @@ -1,6 +1,13 @@ -import { Browser, getBrowserName } from '../useragent'; +import { + SubscriptionType, + type SubscriptionTypeValue, +} from 'src/core/types/subscription'; +import { + DeliveryPlatformKind, + type DeliveryPlatformKindValue, +} from '../models/DeliveryPlatformKind'; +import { Browser, getBrowserName, getBrowserVersion } from '../useragent'; import { API_ORIGIN, API_TYPE, IS_SERVICE_WORKER } from '../utils/EnvVariables'; -import OneSignalUtils from '../utils/OneSignalUtils'; import { EnvironmentKind } from './constants'; export const isBrowser = typeof window !== 'undefined'; @@ -66,8 +73,8 @@ const isTurbineEndpoint = (action?: string): boolean => { }; export const getSubscriptionType = (): SubscriptionTypeValue => { - const browser = OneSignalUtils.redetectBrowserUserAgent(); - if (browser.firefox) { + const isFirefox = getBrowserName() === Browser.Firefox; + if (isFirefox) { return SubscriptionType.FirefoxPush; } if (useSafariVapidPush()) { @@ -97,10 +104,7 @@ export function getDeviceType(): DeliveryPlatformKindValue { } export function getDeviceOS(): string { - const environment = EnvironmentInfoHelper.getEnvironmentInfo(); - return String( - isNaN(environment.browserVersion) ? -1 : environment.browserVersion, - ); + return String(getBrowserVersion()); } export function getDeviceModel(): string { diff --git a/src/shared/managers/SubscriptionManager.test.ts b/src/shared/managers/SubscriptionManager.test.ts index 5432a0fd1..8e8b84d8d 100644 --- a/src/shared/managers/SubscriptionManager.test.ts +++ b/src/shared/managers/SubscriptionManager.test.ts @@ -1,4 +1,4 @@ -import { DUMMY_EXTERNAL_ID } from '__test__/constants'; +import { DEVICE_OS, DUMMY_EXTERNAL_ID } from '__test__/constants'; import { TestEnvironment } from '__test__/support/environment/TestEnvironment'; import { setupSubModelStore } from '__test__/support/environment/TestEnvironmentHelpers'; import { diff --git a/src/sw/serviceWorker/ServiceWorker.ts b/src/sw/serviceWorker/ServiceWorker.ts index ed211e59b..55c110940 100755 --- a/src/sw/serviceWorker/ServiceWorker.ts +++ b/src/sw/serviceWorker/ServiceWorker.ts @@ -2,11 +2,11 @@ import { NotificationType, type NotificationTypeValue, } from 'src/core/types/subscription'; -import FuturePushSubscriptionRecord from 'src/page/userModel/FuturePushSubscriptionRecord'; import OneSignalApiBase from 'src/shared/api/OneSignalApiBase'; import OneSignalApiSW from 'src/shared/api/OneSignalApiSW'; import { type AppConfig, getServerAppConfig } from 'src/shared/config'; import { Utils } from 'src/shared/context/Utils'; +import { getDeviceType } from 'src/shared/environment'; import { delay } from 'src/shared/helpers/general'; import ServiceWorkerHelper from 'src/shared/helpers/ServiceWorkerHelper'; import { From b069192f03078b725fd225d881dfa861ee2cf3a7 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Wed, 6 Aug 2025 13:31:48 -0700 Subject: [PATCH 12/12] address pr comments - remove duplicate code - add version check for prompts manager --- package-lock.json | 492 ++---------------- package.json | 4 +- src/onesignal/OneSignal.test.ts | 7 +- src/page/managers/PromptsManager.ts | 3 + .../userModel/FuturePushSubscriptionRecord.ts | 65 +-- src/shared/environment/environment.ts | 6 +- src/shared/helpers/MainHelper.ts | 2 +- src/shared/helpers/dom.ts | 2 +- src/shared/listeners/listeners.ts | 5 - src/shared/managers/PermissionManager.ts | 2 +- src/shared/managers/SubscriptionManager.ts | 6 +- src/shared/useragent/detect.ts | 3 + src/sw/serviceWorker/ServiceWorker.test.ts | 8 +- 13 files changed, 81 insertions(+), 524 deletions(-) diff --git a/package-lock.json b/package-lock.json index d9ffc6ba4..3b3747dd3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,7 +23,7 @@ "@types/uuid": "^10.0.0", "@typescript-eslint/eslint-plugin": "^5.36.1", "@typescript-eslint/parser": "^5.36.1", - "@vitest/coverage-v8": "3.2.4", + "@vitest/coverage-v8": "4.0.0-beta.6", "concurrently": "^9.2.0", "deepmerge": "^4.2.2", "eslint": "^8.23.0", @@ -40,21 +40,7 @@ "vite-bundle-analyzer": "^1.1.0", "vite-plugin-mkcert": "^1.17.8", "vite-tsconfig-paths": "^5.1.4", - "vitest": "3.2.4" - } - }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" + "vitest": "4.0.0-beta.6" } }, "node_modules/@asamuzakjp/css-color": { @@ -921,130 +907,6 @@ } } }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.12", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", - "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -1154,17 +1016,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.46.2", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.46.2.tgz", @@ -1893,32 +1744,30 @@ "license": "ISC" }, "node_modules/@vitest/coverage-v8": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz", - "integrity": "sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==", + "version": "4.0.0-beta.6", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.0.0-beta.6.tgz", + "integrity": "sha512-ktfxAmkue/yDq8mri5GUuDBq+KNWrQfDge/U2S2i5OYM/h4sqy3o4UjmxcP3/thS+4pLJfhk/e84OFfdQksJ0g==", "dev": true, "license": "MIT", "dependencies": { - "@ampproject/remapping": "^2.3.0", "@bcoe/v8-coverage": "^1.0.2", + "@vitest/utils": "4.0.0-beta.6", "ast-v8-to-istanbul": "^0.3.3", "debug": "^4.4.1", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-lib-source-maps": "^5.0.6", "istanbul-reports": "^3.1.7", - "magic-string": "^0.30.17", "magicast": "^0.3.5", "std-env": "^3.9.0", - "test-exclude": "^7.0.1", "tinyrainbow": "^2.0.0" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "3.2.4", - "vitest": "3.2.4" + "@vitest/browser": "4.0.0-beta.6", + "vitest": "4.0.0-beta.6" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -1927,16 +1776,16 @@ } }, "node_modules/@vitest/expect": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", - "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "version": "4.0.0-beta.6", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.0-beta.6.tgz", + "integrity": "sha512-dirPYot23Y+oeTSkWZj1Xv6iPQ+jytpkv6OcaSPWuBmdCX5dg//N3U2VGmI1osbnKcu/Ko2cV+Uxx5I5545CTg==", "dev": true, "license": "MIT", "dependencies": { "@types/chai": "^5.2.2", - "@vitest/spy": "3.2.4", - "@vitest/utils": "3.2.4", - "chai": "^5.2.0", + "@vitest/spy": "4.0.0-beta.6", + "@vitest/utils": "4.0.0-beta.6", + "chai": "^5.2.1", "tinyrainbow": "^2.0.0" }, "funding": { @@ -1944,13 +1793,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", - "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "version": "4.0.0-beta.6", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.0-beta.6.tgz", + "integrity": "sha512-FVWcdkAdVvpa804Ukpd2MaQQ+6Rto5YlUFpZol/DuQgXM6VXHUUEVfZeJpyv0EF8quOmjwlPWJAnmzAl/hL8Jw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.2.4", + "@vitest/spy": "4.0.0-beta.6", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, @@ -1959,7 +1808,7 @@ }, "peerDependencies": { "msw": "^2.4.9", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + "vite": "^6.0.0 || ^7.0.0-0" }, "peerDependenciesMeta": { "msw": { @@ -1971,9 +1820,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", - "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "version": "4.0.0-beta.6", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.0-beta.6.tgz", + "integrity": "sha512-7pk/LkA2pE2O5dYLy+H7E9F6lNxmnWi+nHVGmd/JotCIbUy4Y0Q9wcSQatUKSRY4xkKrO/Ymb7jU8fSYcZ9Skw==", "dev": true, "license": "MIT", "dependencies": { @@ -1984,13 +1833,13 @@ } }, "node_modules/@vitest/runner": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", - "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "version": "4.0.0-beta.6", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.0-beta.6.tgz", + "integrity": "sha512-n350+NMd9HC1yXpuEASgABLgpRTc7CO3jOKbfRw/IX5rQbHxqpa1lpNliWrb5b89ZiK/V1DNSEfqOho9qQjeTw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "3.2.4", + "@vitest/utils": "4.0.0-beta.6", "pathe": "^2.0.3", "strip-literal": "^3.0.0" }, @@ -1999,13 +1848,13 @@ } }, "node_modules/@vitest/snapshot": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", - "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "version": "4.0.0-beta.6", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.0-beta.6.tgz", + "integrity": "sha512-ciqDHmA71fSuO6eB4imTci5g2BwO+nDIuozeu1sXp06WP869xIZ644a83puWthXIv/+2LliHaHx0qQiJ/yYIRg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.2.4", + "@vitest/pretty-format": "4.0.0-beta.6", "magic-string": "^0.30.17", "pathe": "^2.0.3" }, @@ -2014,27 +1863,24 @@ } }, "node_modules/@vitest/spy": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", - "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "version": "4.0.0-beta.6", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.0-beta.6.tgz", + "integrity": "sha512-zKGpj93bqs2BkHB4clcXngfu7VOSieIL+dk/afqSZ4ylYKYyMT2b6QCw7DkWn9a7bW9bC9IrizPzJrAVKnkZKQ==", "dev": true, "license": "MIT", - "dependencies": { - "tinyspy": "^4.0.3" - }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/utils": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", - "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "version": "4.0.0-beta.6", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.0-beta.6.tgz", + "integrity": "sha512-v5cwVYZyB7Vp5T1hipS7wpKg0S18ksSSB7fOGv22iDwQJLGxlYKwuMvPgoQaWFMNWRVsuBgjEjQLY8KhGhTlXw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.2.4", - "loupe": "^3.1.4", + "@vitest/pretty-format": "4.0.0-beta.6", + "loupe": "^3.2.0", "tinyrainbow": "^2.0.0" }, "funding": { @@ -2253,16 +2099,6 @@ "node": ">= 0.8" } }, - "node_modules/cac": { - "version": "6.7.14", - "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -2633,13 +2469,6 @@ "node": ">= 0.4" } }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, - "license": "MIT" - }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -3263,23 +3092,6 @@ } } }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "dev": true, - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/form-data": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", @@ -3847,22 +3659,6 @@ "node": ">=8" } }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, "node_modules/jiti": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz", @@ -4237,16 +4033,6 @@ "node": "*" } }, - "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -4427,13 +4213,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, - "license": "BlueOak-1.0.0" - }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -4490,23 +4269,6 @@ "node": ">=8" } }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/path-to-regexp": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", @@ -5369,22 +5131,6 @@ "node": ">=8" } }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -5398,20 +5144,6 @@ "node": ">=8" } }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -5511,68 +5243,6 @@ "node": ">=16.0.0" } }, - "node_modules/test-exclude": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", - "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==", - "dev": true, - "license": "ISC", - "dependencies": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^10.4.1", - "minimatch": "^9.0.4" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/test-exclude/node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/test-exclude/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -5659,16 +5329,6 @@ "node": ">=14.0.0" } }, - "node_modules/tinyspy": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.3.tgz", - "integrity": "sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/tldts": { "version": "6.1.86", "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", @@ -5972,29 +5632,6 @@ "analyze": "dist/bin.js" } }, - "node_modules/vite-node": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", - "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cac": "^6.7.14", - "debug": "^4.4.1", - "es-module-lexer": "^1.7.0", - "pathe": "^2.0.3", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" - }, - "bin": { - "vite-node": "vite-node.mjs" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, "node_modules/vite-plugin-mkcert": { "version": "1.17.8", "resolved": "https://registry.npmjs.org/vite-plugin-mkcert/-/vite-plugin-mkcert-1.17.8.tgz", @@ -6062,34 +5699,34 @@ } }, "node_modules/vitest": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", - "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "version": "4.0.0-beta.6", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.0-beta.6.tgz", + "integrity": "sha512-vCmDHLkvmshANLjl2DkwdlwLsZDu99/831HjqHWtWkqhSPooUHAuBDFBbKrZeGFfv8I9qn0WhuXPuzItfhNSJg==", "dev": true, "license": "MIT", "dependencies": { "@types/chai": "^5.2.2", - "@vitest/expect": "3.2.4", - "@vitest/mocker": "3.2.4", - "@vitest/pretty-format": "^3.2.4", - "@vitest/runner": "3.2.4", - "@vitest/snapshot": "3.2.4", - "@vitest/spy": "3.2.4", - "@vitest/utils": "3.2.4", - "chai": "^5.2.0", + "@vitest/expect": "4.0.0-beta.6", + "@vitest/mocker": "4.0.0-beta.6", + "@vitest/pretty-format": "^4.0.0-beta.6", + "@vitest/runner": "4.0.0-beta.6", + "@vitest/snapshot": "4.0.0-beta.6", + "@vitest/spy": "4.0.0-beta.6", + "@vitest/utils": "4.0.0-beta.6", + "chai": "^5.2.1", "debug": "^4.4.1", - "expect-type": "^1.2.1", + "es-module-lexer": "^1.7.0", + "expect-type": "^1.2.2", "magic-string": "^0.30.17", "pathe": "^2.0.3", - "picomatch": "^4.0.2", + "picomatch": "^4.0.3", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.14", "tinypool": "^1.1.1", "tinyrainbow": "^2.0.0", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", - "vite-node": "3.2.4", + "vite": "^6.0.0 || ^7.0.0-0", "why-is-node-running": "^2.3.0" }, "bin": { @@ -6105,8 +5742,8 @@ "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.2.4", - "@vitest/ui": "3.2.4", + "@vitest/browser": "4.0.0-beta.6", + "@vitest/ui": "4.0.0-beta.6", "happy-dom": "*", "jsdom": "*" }, @@ -6265,25 +5902,6 @@ "node": ">=8" } }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", diff --git a/package.json b/package.json index a44c8de43..610b8e607 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "@types/uuid": "^10.0.0", "@typescript-eslint/eslint-plugin": "^5.36.1", "@typescript-eslint/parser": "^5.36.1", - "@vitest/coverage-v8": "3.2.4", + "@vitest/coverage-v8": "4.0.0-beta.6", "concurrently": "^9.2.0", "deepmerge": "^4.2.2", "eslint": "^8.23.0", @@ -70,7 +70,7 @@ "vite-bundle-analyzer": "^1.1.0", "vite-plugin-mkcert": "^1.17.8", "vite-tsconfig-paths": "^5.1.4", - "vitest": "3.2.4" + "vitest": "4.0.0-beta.6" }, "size-limit": [ { diff --git a/src/onesignal/OneSignal.test.ts b/src/onesignal/OneSignal.test.ts index 0e7900319..8fec77818 100644 --- a/src/onesignal/OneSignal.test.ts +++ b/src/onesignal/OneSignal.test.ts @@ -98,6 +98,7 @@ describe('OneSignal', () => { afterEach(async () => { window.OneSignal.coreDirector.operationRepo.queue = []; await Database.remove('operations'); + await waitForOperations(); window.OneSignal.coreDirector.subscriptionModelStore.replaceAll( [], ModelChangeTags.HYDRATE, @@ -270,9 +271,8 @@ describe('OneSignal', () => { await waitForOperations(6); window.OneSignal.User.removeEmail(email); - await waitForOperations(4); - expect(deleteSubscriptionFn).toHaveBeenCalled(); + await vi.waitUntil(() => deleteSubscriptionFn.mock.calls.length === 1); dbSubscriptions = await getEmailSubscriptionDbItems(); expect(dbSubscriptions).toHaveLength(0); }); @@ -977,7 +977,7 @@ describe('OneSignal', () => { OneSignal.coreDirector .getIdentityModel() .setProperty('external_id', 'some-id', ModelChangeTags.NO_PROPOGATE); - window.OneSignal.User.trackEvent(name, properties); + window.OneSignal.User.trackEvent(name); await vi.waitUntil(() => sendCustomEventFn.mock.calls.length === 1); @@ -989,7 +989,6 @@ describe('OneSignal', () => { onesignal_id: DUMMY_ONESIGNAL_ID, payload: { os_sdk: OS_SDK, - test_property: 'test_value', }, timestamp: expect.any(String), }, diff --git a/src/page/managers/PromptsManager.ts b/src/page/managers/PromptsManager.ts index 11ef6660f..41fbb9875 100644 --- a/src/page/managers/PromptsManager.ts +++ b/src/page/managers/PromptsManager.ts @@ -12,6 +12,7 @@ import { import { Browser, getBrowserName, + getBrowserVersion, isMobileBrowser, isTabletBrowser, requiresUserInteraction, @@ -45,8 +46,10 @@ export class PromptsManager { } private shouldForceSlidedownOverNative(): boolean { + const browserVersion = getBrowserVersion(); return ( (getBrowserName() === Browser.Chrome && + browserVersion >= 63 && (isTabletBrowser() || isMobileBrowser())) || requiresUserInteraction() ); diff --git a/src/page/userModel/FuturePushSubscriptionRecord.ts b/src/page/userModel/FuturePushSubscriptionRecord.ts index 70ea04f41..0d33a71d6 100644 --- a/src/page/userModel/FuturePushSubscriptionRecord.ts +++ b/src/page/userModel/FuturePushSubscriptionRecord.ts @@ -2,28 +2,14 @@ import type { NotificationTypeValue, SubscriptionTypeValue, } from 'src/core/types/subscription'; -import { - NotificationType, - SubscriptionType, -} from 'src/core/types/subscription'; +import { NotificationType } from 'src/core/types/subscription'; import { getDeviceModel, getDeviceOS, getSubscriptionType, - useSafariLegacyPush, - useSafariVapidPush, } from 'src/shared/environment'; import { RawPushSubscription } from 'src/shared/models/RawPushSubscription'; -import { - Browser, - getBrowserName, - getBrowserVersion, -} from 'src/shared/useragent'; import { VERSION } from 'src/shared/utils/EnvVariables'; -import { - DeliveryPlatformKind, - type DeliveryPlatformKindValue, -} from '../../shared/models/DeliveryPlatformKind'; import type { Serializable } from '../models/Serializable'; export default class FuturePushSubscriptionRecord implements Serializable { @@ -69,53 +55,4 @@ export default class FuturePushSubscriptionRecord implements Serializable { web_p256: this.webp256, }; } - - /* S T A T I C */ - - /** - * Get the User Model Subscription type based on browser detection. - */ - public static getSubscriptionType(): SubscriptionTypeValue { - const browserName = getBrowserName(); - if (browserName === Browser.Firefox) { - return SubscriptionType.FirefoxPush; - } - if (useSafariVapidPush()) { - return SubscriptionType.SafariPush; - } - if (useSafariLegacyPush) { - return SubscriptionType.SafariLegacyPush; - } - // Other browsers, like Edge, are Chromium based so we consider them "Chrome". - return SubscriptionType.ChromePush; - } - - /** - * Get the legacy player.device_type - * NOTE: Use getSubscriptionType() instead when possible. - */ - public static getDeviceType(): DeliveryPlatformKindValue { - switch (this.getSubscriptionType()) { - case SubscriptionType.FirefoxPush: - return DeliveryPlatformKind.Firefox; - case SubscriptionType.SafariLegacyPush: - return DeliveryPlatformKind.SafariLegacy; - case SubscriptionType.SafariPush: - return DeliveryPlatformKind.SafariVapid; - } - return DeliveryPlatformKind.ChromeLike; - } - - public static getDeviceOS(): string | number { - const browserVersion = getBrowserVersion(); - return isNaN(browserVersion) ? -1 : browserVersion; - } - - public static getDeviceModel(): string { - return navigator.platform; - } - - public static getSdk(): string { - return String(VERSION); - } } diff --git a/src/shared/environment/environment.ts b/src/shared/environment/environment.ts index 5ff6c837a..a4e3986f6 100644 --- a/src/shared/environment/environment.ts +++ b/src/shared/environment/environment.ts @@ -19,7 +19,7 @@ export const supportsServiceWorkers = () => { export const windowEnvString = IS_SERVICE_WORKER ? 'Service Worker' : 'Browser'; -export const useSafariLegacyPush = +export const useSafariLegacyPush = () => isBrowser && window.safari?.pushNotification != undefined; export const supportsVapidPush = @@ -30,7 +30,7 @@ export const supportsVapidPush = export const useSafariVapidPush = () => getBrowserName() === Browser.Safari && supportsVapidPush && - !useSafariLegacyPush; + !useSafariLegacyPush(); // for determing the api url const API_URL_PORT = 3000; @@ -80,7 +80,7 @@ export const getSubscriptionType = (): SubscriptionTypeValue => { if (useSafariVapidPush()) { return SubscriptionType.SafariPush; } - if (useSafariLegacyPush) { + if (useSafariLegacyPush()) { return SubscriptionType.SafariLegacyPush; } // Other browsers, like Edge, are Chromium based so we consider them "Chrome". diff --git a/src/shared/helpers/MainHelper.ts b/src/shared/helpers/MainHelper.ts index 343fe8135..eaedade7a 100755 --- a/src/shared/helpers/MainHelper.ts +++ b/src/shared/helpers/MainHelper.ts @@ -239,7 +239,7 @@ export default class MainHelper { // TO DO: unit test static async getCurrentPushToken(): Promise { - if (useSafariLegacyPush) { + if (useSafariLegacyPush()) { const safariToken = window.safari?.pushNotification?.permission( OneSignal.config?.safariWebId, ).deviceToken; diff --git a/src/shared/helpers/dom.ts b/src/shared/helpers/dom.ts index 54a6a68f4..c04937db4 100644 --- a/src/shared/helpers/dom.ts +++ b/src/shared/helpers/dom.ts @@ -1,4 +1,4 @@ -import Log from 'src/sw/libraries/Log'; +import Log from 'src/shared/libraries/Log'; export function addDomElement( targetSelectorOrElement: string | Element, diff --git a/src/shared/listeners/listeners.ts b/src/shared/listeners/listeners.ts index 587f44912..176e5e977 100644 --- a/src/shared/listeners/listeners.ts +++ b/src/shared/listeners/listeners.ts @@ -249,11 +249,6 @@ export async function onInternalSubscriptionSet(optedOut: boolean) { LimitStore.put('subscription.optedOut', optedOut); } -/** - * NOTE: This uses the OneSignal REST API POST /notifications with - * include_player_ids. This field will be dropped by 2025 so a - * replacement will needed by then. - */ async function onSubscriptionChanged_showWelcomeNotification( isSubscribed: boolean | undefined, pushSubscriptionId: string | undefined | null, diff --git a/src/shared/managers/PermissionManager.ts b/src/shared/managers/PermissionManager.ts index 6354a82b3..26c60cb41 100644 --- a/src/shared/managers/PermissionManager.ts +++ b/src/shared/managers/PermissionManager.ts @@ -39,7 +39,7 @@ export default class PermissionManager { public async getNotificationPermission( safariWebId?: string, ): Promise { - if (useSafariLegacyPush) { + if (useSafariLegacyPush()) { return PermissionManager.getLegacySafariNotificationPermission( safariWebId, ); diff --git a/src/shared/managers/SubscriptionManager.ts b/src/shared/managers/SubscriptionManager.ts index ef8372082..404970007 100644 --- a/src/shared/managers/SubscriptionManager.ts +++ b/src/shared/managers/SubscriptionManager.ts @@ -186,7 +186,7 @@ export class SubscriptionManager { PushPermissionNotGrantedErrorReason.Blocked, ); - if (useSafariLegacyPush) { + if (useSafariLegacyPush()) { rawPushSubscription = await this.subscribeSafari(); await updatePushSubscriptionModelWithRawSubscription( rawPushSubscription, @@ -288,7 +288,7 @@ export class SubscriptionManager { subscription.deviceId = DEFAULT_DEVICE_ID; subscription.optedOut = false; if (pushSubscription) { - if (useSafariLegacyPush) { + if (useSafariLegacyPush()) { subscription.subscriptionToken = pushSubscription.safariDeviceToken; } else { subscription.subscriptionToken = pushSubscription.w3cEndpoint @@ -829,7 +829,7 @@ export class SubscriptionManager { pushSubscriptionModel, ); - if (useSafariLegacyPush) { + if (useSafariLegacyPush()) { const subscriptionState = window.safari?.pushNotification?.permission( this.config.safariWebId, ); diff --git a/src/shared/useragent/detect.ts b/src/shared/useragent/detect.ts index 05da99ee0..7633c88d3 100644 --- a/src/shared/useragent/detect.ts +++ b/src/shared/useragent/detect.ts @@ -9,6 +9,7 @@ interface IBrowserResult { version: string; } +// Top popular browsers set const BROWSER_CONFIGS: BrowserConfig[] = [ { name: 'Opera', @@ -82,6 +83,7 @@ const isAndroid = (userAgent: string): boolean => const getIOSDeviceType = (userAgent: string): string => userAgent.match(/(iphone|ipod|ipad)/i)?.[1]?.toLowerCase() || ''; +// Based off detect-ua logic: https://github.com/TimvanScherpenzeel/detect-ua export function isTabletBrowser(userAgent = navigator.userAgent): boolean { const ios = getIOSDeviceType(userAgent); @@ -93,6 +95,7 @@ export function isTabletBrowser(userAgent = navigator.userAgent): boolean { ); } +// Based off detect-ua logic: https://github.com/TimvanScherpenzeel/detect-ua export function isMobileBrowser(userAgent = navigator.userAgent): boolean { if (isTabletBrowser(userAgent)) return false; diff --git a/src/sw/serviceWorker/ServiceWorker.test.ts b/src/sw/serviceWorker/ServiceWorker.test.ts index 5fbaf5f7f..b49bbb70a 100644 --- a/src/sw/serviceWorker/ServiceWorker.test.ts +++ b/src/sw/serviceWorker/ServiceWorker.test.ts @@ -712,9 +712,11 @@ const mockSender = { vi.mock( 'src/sw/webhooks/notifications/OSWebhookNotificationEventSender', () => ({ - OSWebhookNotificationEventSender: vi - .fn() - .mockImplementation(() => mockSender), + OSWebhookNotificationEventSender: class { + willDisplay = mockSender.willDisplay; + dismiss = mockSender.dismiss; + click = mockSender.click; + }, }), );