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..627368166 --- /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.1.0.0 Safari/537.36', + CHROME_LINUX: + '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: + '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/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.1', + 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/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/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: + '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 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: + '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 6a73c9026..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({ @@ -24,3 +25,8 @@ vi.mock('src/core/operationRepo/constants', () => ({ OP_REPO_EXECUTION_INTERVAL: 5, OP_REPO_POST_CREATE_RETRY_UP_TO: 10, })); + +Object.defineProperty(navigator, 'userAgent', { + 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 627f22ad5..6cf61512d 100644 --- a/__test__/support/environment/TestEnvironment.ts +++ b/__test__/support/environment/TestEnvironment.ts @@ -2,15 +2,13 @@ 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, - mockUserAgent, resetDatabase, stubDomEnvironment, stubNotification, @@ -24,7 +22,7 @@ export interface TestEnvironmentConfig { permission?: NotificationPermission; addPrompts?: boolean; url?: string; - userAgent?: typeof BrowserUserAgent; + userAgent?: string; overrideServerConfig?: RecursivePartial; integration?: ConfigIntegrationKindValue; useMockIdentityModel?: boolean; @@ -33,7 +31,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..40d322e30 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,40 +16,25 @@ 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 { + DEFAULT_USER_AGENT, DEVICE_OS, DUMMY_ONESIGNAL_ID, DUMMY_SUBSCRIPTION_ID_3, -} from '../constants'; +} 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'; 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; @@ -107,7 +91,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/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/__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 7789fc29c..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; Mobile ; 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/core/coreModuleDirector.test.ts b/__test__/unit/core/coreModuleDirector.test.ts index 60a99b5f4..0c73bff27 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(); } @@ -44,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/__test__/unit/core/osModel.test.ts b/__test__/unit/core/osModel.test.ts index ccc691ae7..da3ee3784 100644 --- a/__test__/unit/core/osModel.test.ts +++ b/__test__/unit/core/osModel.test.ts @@ -1,14 +1,9 @@ -import { DEVICE_OS } from '__test__/support/constants'; -import { mockUserAgent } from '__test__/support/environment/TestEnvironmentHelpers'; +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'; 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..4a01d2731 100644 --- a/__test__/unit/http/sdkVersion.test.ts +++ b/__test__/unit/http/sdkVersion.test.ts @@ -1,20 +1,16 @@ -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'; -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', () => { - beforeAll(() => { - nock({}); - mockUserAgent(); - }); + beforeAll(() => nock({})); test('POST /users: SDK-Version header is sent', () => { // @ts-expect-error - partial identity object 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 3fd59e215..822c31823 100644 --- a/__test__/unit/pushSubscription/nativePermissionChange.test.ts +++ b/__test__/unit/pushSubscription/nativePermissionChange.test.ts @@ -4,18 +4,15 @@ 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, - 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 { 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'; @@ -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(); }); @@ -122,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(); }); @@ -140,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-lock.json b/package-lock.json index 46a1d68af..3b3747dd3 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" }, @@ -24,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", @@ -41,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": { @@ -922,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", @@ -1155,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", @@ -1894,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": { @@ -1928,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": { @@ -1945,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" }, @@ -1960,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": { @@ -1972,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": { @@ -1985,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" }, @@ -2000,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" }, @@ -2015,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": { @@ -2212,11 +2057,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", @@ -2259,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", @@ -2310,36 +2140,6 @@ "node": ">=18" } }, - "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/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", @@ -2483,6 +2283,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", @@ -2639,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", @@ -2880,6 +2703,36 @@ "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-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", @@ -2914,6 +2767,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", @@ -2941,6 +2811,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", @@ -3209,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", @@ -3793,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", @@ -4018,6 +3868,36 @@ "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/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", @@ -4153,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", @@ -4343,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", @@ -4406,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", @@ -5285,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", @@ -5314,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", @@ -5427,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", @@ -5575,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", @@ -5888,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", @@ -5978,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": { @@ -6021,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": "*" }, @@ -6181,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 ba5df96b0..610b8e607 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" }, @@ -54,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", @@ -71,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": [ { @@ -81,12 +80,12 @@ }, { "path": "./build/releases/OneSignalSDK.page.es6.js", - "limit": "64 kB", + "limit": "61.1 kB", "gzip": true }, { "path": "./build/releases/OneSignalSDK.sw.js", - "limit": "34 kB", + "limit": "25.1 kB", "gzip": true }, { 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 25c8de811..340dd5dd5 100644 --- a/src/core/executors/LoginUserOperationExecutor.test.ts +++ b/src/core/executors/LoginUserOperationExecutor.test.ts @@ -8,9 +8,8 @@ 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 { 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/LoginUserOperationExecutor.ts b/src/core/executors/LoginUserOperationExecutor.ts index fca198efd..59c3916f8 100644 --- a/src/core/executors/LoginUserOperationExecutor.ts +++ b/src/core/executors/LoginUserOperationExecutor.ts @@ -4,15 +4,15 @@ 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 { getTimeZoneId } from 'src/shared/utils/utils'; import { IdentityConstants, OPERATION_NAME } from '../constants'; import { type IPropertiesModelKeys } from '../models/PropertiesModel'; import { type IdentityModelStore } from '../modelStores/IdentityModelStore'; @@ -250,7 +250,7 @@ export class LoginUserOperationExecutor implements IOperationExecutor { ); } - EventHelper.checkAndTriggerUserChanged(); + checkAndTriggerUserChanged(); const followUp = Object.keys(identity).length > 0 diff --git a/src/core/executors/RefreshUserOperationExecutor.test.ts b/src/core/executors/RefreshUserOperationExecutor.test.ts index 6c0b3370d..39de09ed5 100644 --- a/src/core/executors/RefreshUserOperationExecutor.test.ts +++ b/src/core/executors/RefreshUserOperationExecutor.test.ts @@ -6,8 +6,7 @@ import { DUMMY_PUSH_TOKEN, DUMMY_SUBSCRIPTION_ID, DUMMY_SUBSCRIPTION_ID_2, -} from '__test__/support/constants'; -import { mockUserAgent } from '__test__/support/environment/TestEnvironmentHelpers'; +} from '__test__/constants'; 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..3484a414e 100644 --- a/src/core/executors/SubscriptionOperationExecutor.test.ts +++ b/src/core/executors/SubscriptionOperationExecutor.test.ts @@ -2,11 +2,8 @@ import { APP_ID, DUMMY_ONESIGNAL_ID, DUMMY_SUBSCRIPTION_ID_3, -} from '__test__/support/constants'; -import { - createPushSub, - mockUserAgent, -} from '__test__/support/environment/TestEnvironmentHelpers'; +} from '__test__/constants'; +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/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 83843a379..5ce88564e 100644 --- a/src/core/operationRepo/OperationRepo.test.ts +++ b/src/core/operationRepo/OperationRepo.test.ts @@ -2,8 +2,7 @@ import { APP_ID, DUMMY_ONESIGNAL_ID, DUMMY_SUBSCRIPTION_ID, -} from '__test__/support/constants'; -import { mockUserAgent } from '__test__/support/environment/TestEnvironmentHelpers'; +} 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'; @@ -39,7 +38,6 @@ vi.spyOn(OperationModelStore.prototype, 'create').mockImplementation(() => { }); let mockOperationModelStore: OperationModelStore; -mockUserAgent(); describe('OperationRepo', () => { let opRepo: OperationRepo; 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/entries/pageSdkInit.test.ts b/src/entries/pageSdkInit.test.ts index 3cf6b6403..b9860e02b 100644 --- a/src/entries/pageSdkInit.test.ts +++ b/src/entries/pageSdkInit.test.ts @@ -1,20 +1,10 @@ -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'; 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/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/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.test.ts b/src/onesignal/OneSignal.test.ts index 07986c6c1..8fec77818 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'; @@ -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/onesignal/OneSignal.ts b/src/onesignal/OneSignal.ts index 58461e8af..c9440099e 100644 --- a/src/onesignal/OneSignal.ts +++ b/src/onesignal/OneSignal.ts @@ -5,13 +5,20 @@ import { type AppUserConfig, } from 'src/shared/config'; import { windowEnvString } from 'src/shared/environment'; +import { + _onSubscriptionChanged, + checkAndTriggerSubscriptionChanged, +} from 'src/shared/listeners'; +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'; @@ -20,14 +27,12 @@ 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'; 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 +61,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 +127,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 +138,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 @@ -171,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, @@ -272,7 +274,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/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/onesignal/User.ts b/src/onesignal/User.ts index 366c6dbf2..612b34db7 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; @@ -297,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 3be6aa8f5..cbf45583a 100644 --- a/src/onesignal/UserNamespace.test.ts +++ b/src/onesignal/UserNamespace.test.ts @@ -1,7 +1,4 @@ -import { - DUMMY_ONESIGNAL_ID, - DUMMY_PUSH_TOKEN, -} from '__test__/support/constants'; +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'; 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 03e32b221..28527aa7c 100755 --- a/src/page/bell/Bell.ts +++ b/src/page/bell/Bell.ts @@ -1,22 +1,20 @@ import type { AppUserConfigNotifyButton } from 'src/shared/config'; +import { + addCssClass, + addDomElement, + decodeHtmlEntities, + removeDomElement, +} from 'src/shared/helpers/dom'; +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'; 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, - addDomElement, - contains, - delay, - nothing, - once, - removeDomElement, -} 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'; @@ -272,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; @@ -537,11 +535,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 +551,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/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 54bb4d143..e5f936ec8 100755 --- a/src/page/bell/Dialog.ts +++ b/src/page/bell/Dialog.ts @@ -1,10 +1,12 @@ -import OneSignalEvent from '../../shared/services/OneSignalEvent'; -import { bowserCastle } from '../../shared/utils/bowserCastle'; +import { addDomElement, clearDomElementChildren } from 'src/shared/helpers/dom'; import { - addDomElement, - clearDomElementChildren, - getPlatformNotificationIcon, -} from '../../shared/utils/utils'; + Browser, + getBrowserName, + isMobileBrowser, + isTabletBrowser, +} from 'src/shared/useragent'; +import { getPlatformNotificationIcon } from 'src/shared/utils/utils'; +import OneSignalEvent from '../../shared/services/OneSignalEvent'; import type { NotificationIcons } from '../models/NotificationIcons'; import AnimatedElement from './AnimatedElement'; import Bell from './Bell'; @@ -96,14 +98,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 +117,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/bell/Launcher.ts b/src/page/bell/Launcher.ts index 68a113fef..d10f0e12b 100755 --- a/src/page/bell/Launcher.ts +++ b/src/page/bell/Launcher.ts @@ -1,17 +1,16 @@ -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 { nothing } from 'src/shared/helpers/general'; +import Log from 'src/shared/libraries/Log'; +import type { BellSize } from 'src/shared/prompts'; +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 641d1aaa5..db19b511d 100755 --- a/src/page/bell/Message.ts +++ b/src/page/bell/Message.ts @@ -1,6 +1,6 @@ -import Log from '../../shared/libraries/Log'; -import BrowserUtils from '../../shared/utils/BrowserUtils'; -import { delay, nothing } from '../../shared/utils/utils'; +import { decodeHtmlEntities } from 'src/shared/helpers/dom'; +import { delay, nothing } from 'src/shared/helpers/general'; +import Log from 'src/shared/libraries/Log'; import AnimatedElement from './AnimatedElement'; import Bell from './Bell'; @@ -40,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(() => { @@ -68,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/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..41fbb9875 100644 --- a/src/page/managers/PromptsManager.ts +++ b/src/page/managers/PromptsManager.ts @@ -1,22 +1,27 @@ -import { Browser } from 'src/shared/models/Browser'; +import { delay } from 'src/shared/helpers/general'; import { CONFIG_DEFAULTS_SLIDEDOWN_OPTIONS, DelayedPromptType, + getFirstSlidedownPromptOptionsWithType, SERVER_CONFIG_DEFAULTS_PROMPT_DELAYS, type AppUserConfigPromptOptions, type DelayedPromptOptions, type DelayedPromptTypeValue, type SlidedownPromptOptions, } from 'src/shared/prompts'; +import { + Browser, + getBrowserName, + getBrowserVersion, + 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 +46,12 @@ export class PromptsManager { } private shouldForceSlidedownOverNative(): boolean { - const { environmentInfo } = OneSignal; - const { browserType, browserVersion, requiresUserInteraction } = - environmentInfo!; - + const browserVersion = getBrowserVersion(); return ( - (browserType === Browser.Chrome && - Number(browserVersion) >= 63 && - (bowserCastle().tablet || bowserCastle().mobile)) || - requiresUserInteraction + (getBrowserName() === Browser.Chrome && + browserVersion >= 63 && + (isTabletBrowser() || isMobileBrowser())) || + requiresUserInteraction() ); } @@ -87,11 +89,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( @@ -141,14 +142,12 @@ 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 } if (timeDelaySeconds > 0) { - await awaitableTimeout(timeDelaySeconds * 1_000); + await delay(timeDelaySeconds * 1_000); } switch (type) { @@ -266,10 +265,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) { @@ -377,7 +373,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 2cc134d62..8d848099c 100644 --- a/src/page/managers/slidedownManager/SlidedownManager.ts +++ b/src/page/managers/slidedownManager/SlidedownManager.ts @@ -1,7 +1,9 @@ import type { TagsObjectForApi, TagsObjectWithBoolean } from 'src/page/tags'; +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'; @@ -20,11 +22,9 @@ 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'; -import { awaitableTimeout } from '../../../shared/utils/AwaitableTimeout'; import { OneSignalUtils } from '../../../shared/utils/OneSignalUtils'; import TagUtils from '../../../shared/utils/TagUtils'; import AlreadySubscribedError from '../../errors/AlreadySubscribedError'; @@ -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) { @@ -279,10 +278,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); } @@ -414,11 +413,11 @@ 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 - await awaitableTimeout(1000); + await delay(1000); Slidedown.triggerSlidedownEvent(Slidedown.EVENTS.CLOSED); } 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/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 1a525acc4..17340f0f5 100755 --- a/src/page/slidedown/ConfirmationToast.ts +++ b/src/page/slidedown/ConfirmationToast.ts @@ -1,17 +1,17 @@ -import OneSignalEvent from '../../shared/services/OneSignalEvent'; import { addCssClass, - once, - removeDomElement, getDomElementOrStub, -} from '../../shared/utils/utils'; + 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 { bowserCastle } from '../../shared/utils/bowserCastle'; +} 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; @@ -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..10bf4597d 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 { DelayedPromptType, + isSlidedownPushDependent, SERVER_CONFIG_DEFAULTS_SLIDEDOWN, type SlidedownPromptOptions, } from 'src/shared/prompts'; -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 { bowserCastle } from '../../shared/utils/bowserCastle'; -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, @@ -136,7 +135,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, ); @@ -280,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/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/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/environment/environment.ts b/src/shared/environment/environment.ts index f9af40a8b..a4e3986f6 100644 --- a/src/shared/environment/environment.ts +++ b/src/shared/environment/environment.ts @@ -2,14 +2,12 @@ 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, 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'; @@ -21,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 +28,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; @@ -73,14 +73,14 @@ 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()) { return SubscriptionType.SafariPush; } - if (useSafariLegacyPush) { + if (useSafariLegacyPush()) { return SubscriptionType.SafariLegacyPush; } // Other browsers, like Edge, are Chromium based so we consider them "Chrome". @@ -104,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/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/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/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/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/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 new file mode 100644 index 000000000..c04937db4 --- /dev/null +++ b/src/shared/helpers/dom.ts @@ -0,0 +1,137 @@ +import Log from 'src/shared/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.`, + ); + } +} + +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 af9c2c897..cd26f995e 100644 --- a/src/shared/helpers/general.ts +++ b/src/shared/helpers/general.ts @@ -9,6 +9,19 @@ 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)); +} + +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) { @@ -26,3 +39,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/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..176e5e977 --- /dev/null +++ b/src/shared/listeners/listeners.ts @@ -0,0 +1,342 @@ +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); +} + +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/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/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/ServiceWorkerManager.ts b/src/shared/managers/ServiceWorkerManager.ts index 23c6287a0..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, @@ -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'; @@ -319,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.test.ts b/src/shared/managers/SubscriptionManager.test.ts index e3c07a5e6..8e8b84d8d 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 { 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/shared/managers/SubscriptionManager.ts b/src/shared/managers/SubscriptionManager.ts index 7e2f7df41..404970007 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,11 +40,12 @@ import { } from '../models/UnsubscriptionStrategy'; import Database from '../services/Database'; import OneSignalEvent from '../services/OneSignalEvent'; -import { bowserCastle } from '../utils/bowserCastle'; +import { SessionOrigin } from '../session'; +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; @@ -179,7 +186,7 @@ export class SubscriptionManager { PushPermissionNotGrantedErrorReason.Blocked, ); - if (useSafariLegacyPush) { + if (useSafariLegacyPush()) { rawPushSubscription = await this.subscribeSafari(); await updatePushSubscriptionModelWithRawSubscription( rawPushSubscription, @@ -281,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 @@ -534,7 +541,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 +581,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. @@ -822,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/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..02a2793ec 100644 --- a/src/shared/managers/sessionManager/SessionManager.test.ts +++ b/src/shared/managers/sessionManager/SessionManager.test.ts @@ -1,11 +1,10 @@ +import { DUMMY_EXTERNAL_ID } from '__test__/constants'; import { TestEnvironment } from '__test__/support/environment/TestEnvironment'; -import { SessionManager } from './SessionManager'; - -import { DUMMY_EXTERNAL_ID } from '__test__/support/constants'; 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 a0e1424f2..e3de17f0b 100644 --- a/src/shared/managers/sessionManager/SessionManager.ts +++ b/src/shared/managers/sessionManager/SessionManager.ts @@ -1,5 +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'; @@ -11,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'; @@ -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/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/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/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/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/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/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/detect.ts b/src/shared/useragent/detect.ts new file mode 100644 index 000000000..7633c88d3 --- /dev/null +++ b/src/shared/useragent/detect.ts @@ -0,0 +1,109 @@ +interface BrowserConfig { + name: string; + pattern: RegExp; + versionPattern: RegExp; +} + +interface IBrowserResult { + name: string; + version: string; +} + +// Top popular browsers set +const BROWSER_CONFIGS: BrowserConfig[] = [ + { + name: 'Opera', + pattern: /(?:opera|opr|opios)/i, + versionPattern: /(?:opera|opr|opios)[ /](\d+(?:\.\d+)?)/i, + }, + { + name: 'Facebook', + pattern: /FBAN\//i, + versionPattern: /FBAV\/(\d+(?:\.\d+)?)/i, + }, + { + name: 'Samsung Browser', + pattern: /samsungbrowser/i, + versionPattern: /samsungbrowser[ /](\d+(?:\.\d+)?)/i, + }, + { + name: 'Yandex Browser', + pattern: /yabrowser/i, + versionPattern: /yabrowser[ /](\d+(?:\.\d+)?)/i, + }, + { + name: 'Vivaldi', + pattern: /vivaldi/i, + versionPattern: /vivaldi[ /](\d+(?:\.\d+)?)/i, + }, + { + name: 'UC Browser', + pattern: /ucbrowser/i, + versionPattern: /ucbrowser[ /](\d+(?:\.\d+)?)/i, + }, + { + name: 'Microsoft Edge', + pattern: /edg/i, + versionPattern: /edg[ /](\d+(?:\.\d+)?)/i, + }, + { + name: 'Firefox', + pattern: /firefox|iceweasel|fxios/i, + versionPattern: /(?:firefox|iceweasel|fxios)[ /](\d+(?:\.\d+)?)/i, + }, + { + name: 'Chromium', + pattern: /chromium/i, + versionPattern: /chromium[ /](\d+(?:\.\d+)?)/i, + }, + { + name: 'Chrome', + pattern: /chrome|crios|crmo/i, + versionPattern: /(?:chrome|crios|crmo)[ /](\d+(?:\.\d+)?)/i, + }, + { + name: 'Safari', + pattern: /safari|applewebkit/i, + versionPattern: /version[ /](\d+(?:\.\d+)?)/i, + }, +]; + +export function getBrowser(userAgent: string): IBrowserResult { + for (const config of BROWSER_CONFIGS) { + if (config.pattern.test(userAgent)) { + const version = userAgent.match(config.versionPattern)?.[1] ?? ''; + return { name: config.name, version }; + } + } + return { name: 'Unknown', version: '' }; +} +const isAndroid = (userAgent: string): boolean => + !/like android/i.test(userAgent) && /android/i.test(userAgent); + +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); + + return ( + (/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)) + ); +} + +// Based off detect-ua logic: https://github.com/TimvanScherpenzeel/detect-ua +export function isMobileBrowser(userAgent = navigator.userAgent): boolean { + if (isTabletBrowser(userAgent)) return false; + + const ios = getIOSDeviceType(userAgent); + return ( + /[^-]mobi/i.test(userAgent) || + ios === 'iphone' || + ios === 'ipod' || + isAndroid(userAgent) + ); +} 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..1a636c374 --- /dev/null +++ b/src/shared/useragent/useragent.test.ts @@ -0,0 +1,427 @@ +import { USER_AGENTS } from '__test__/constants'; +import { Browser } from './constants'; +import { + getBrowserName, + getBrowserVersion, + isMobileBrowser, + isTabletBrowser, +} from './useragent'; + +const mockUserAgent = (userAgent: string) => { + Object.defineProperty(navigator, 'userAgent', { + value: userAgent, + writable: true, + }); +}; + +describe('isMobileBrowser()', () => { + [ + // non mobile / is tablet + { + userAgent: USER_AGENTS.CHROME_WINDOWS, + expected: false, + }, + { + userAgent: USER_AGENTS.CHROME_MAC, + expected: false, + }, + { + 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: USER_AGENTS.CHROME_IOS_IPHONE, + expected: true, + }, + { + userAgent: USER_AGENTS.CHROME_IOS_IPOD, + expected: true, + }, + { + userAgent: USER_AGENTS.FIREFOX_ANDROID, + expected: true, + }, + { + userAgent: USER_AGENTS.FIREFOX_IOS, + expected: true, + }, + { + userAgent: USER_AGENTS.SAFARI_IPHONE, + expected: true, + }, + { + userAgent: USER_AGENTS.SAFARI_IPOD, + expected: true, + }, + { + userAgent: USER_AGENTS.EDGE_ANDROID, + expected: true, + }, + { + userAgent: USER_AGENTS.EDGE_IOS, + expected: true, + }, + { + userAgent: USER_AGENTS.FACEBOOK_APP_IOS, + expected: true, + }, + ].forEach(({ userAgent, expected }) => { + test(`"${userAgent}" should ${expected ? 'be' : 'not be'} a mobile browser`, () => { + mockUserAgent(userAgent); + expect(isMobileBrowser()).toBe(expected); + }); + }); + + test('should handle empty user agent', () => { + mockUserAgent(''); + expect(isMobileBrowser()).toBe(false); + }); +}); + +describe('isTabletBrowser()', () => { + [ + // non tablet or is mobile + { + userAgent: USER_AGENTS.CHROME_WINDOWS, + expected: false, + }, + { + userAgent: USER_AGENTS.CHROME_MAC, + expected: false, + }, + { + userAgent: USER_AGENTS.CHROME_LINUX, + expected: false, + }, + { + userAgent: USER_AGENTS.CHROME_IOS_IPHONE, + expected: false, + }, + { + userAgent: USER_AGENTS.FIREFOX_WINDOWS, + expected: false, + }, + { + userAgent: USER_AGENTS.FIREFOX_MAC, + expected: false, + }, + { + userAgent: USER_AGENTS.FIREFOX_IOS, + expected: false, + }, + { + userAgent: USER_AGENTS.SAFARI_MAC, + expected: false, + }, + { + userAgent: USER_AGENTS.SAFARI_IPHONE, + expected: false, + }, + { + userAgent: USER_AGENTS.EDGE_MAC, + expected: false, + }, + { + userAgent: USER_AGENTS.EDGE_WINDOWS, + expected: false, + }, + + // tablet + { + userAgent: USER_AGENTS.CHROME_IOS_IPAD, + expected: true, + }, + { + userAgent: USER_AGENTS.SAFARI_IPAD, + expected: true, + }, + { + userAgent: USER_AGENTS.SAMSUNG_TABLET, + expected: true, + }, + { + userAgent: USER_AGENTS.GOOGLE_TABLET, + expected: true, + }, + ].forEach(({ userAgent, expected }) => { + test(`"${userAgent}" should ${expected ? 'be' : 'not be'} a tablet browser`, () => { + mockUserAgent(userAgent); + expect(isTabletBrowser()).toBe(expected); + }); + }); +}); + +describe('getBrowserName()', () => { + [ + // chrome + { + userAgent: USER_AGENTS.CHROME_WINDOWS, + expected: Browser.Chrome, + }, + { + userAgent: USER_AGENTS.CHROME_MAC, + expected: Browser.Chrome, + }, + { + userAgent: USER_AGENTS.CHROME_LINUX, + expected: Browser.Chrome, + }, + { + userAgent: USER_AGENTS.CHROME_ANDROID, + expected: Browser.Chrome, + }, + { + userAgent: USER_AGENTS.CHROME_IOS_IPHONE, + expected: Browser.Chrome, + }, + { + userAgent: USER_AGENTS.CHROME_IOS_IPAD, + expected: Browser.Chrome, + }, + + // firefox + { + userAgent: USER_AGENTS.FIREFOX_WINDOWS, + expected: Browser.Firefox, + }, + { + userAgent: USER_AGENTS.FIREFOX_MAC, + expected: Browser.Firefox, + }, + { + userAgent: USER_AGENTS.FIREFOX_LINUX, + expected: Browser.Firefox, + }, + { + userAgent: USER_AGENTS.FIREFOX_ANDROID, + expected: Browser.Firefox, + }, + { + userAgent: USER_AGENTS.FIREFOX_IOS, + expected: Browser.Firefox, + }, + + // edge + { + userAgent: USER_AGENTS.EDGE_WINDOWS, + expected: Browser.Edge, + }, + { + userAgent: USER_AGENTS.EDGE_MAC, + expected: Browser.Edge, + }, + { + userAgent: USER_AGENTS.EDGE_IOS, + expected: Browser.Edge, + }, + { + userAgent: USER_AGENTS.EDGE_ANDROID, + expected: Browser.Edge, + }, + + // safari + { + userAgent: USER_AGENTS.SAFARI_MAC, + expected: Browser.Safari, + }, + { + userAgent: USER_AGENTS.SAFARI_IPHONE, + expected: Browser.Safari, + }, + { + userAgent: USER_AGENTS.SAFARI_IPAD, + expected: Browser.Safari, + }, + { + userAgent: USER_AGENTS.SAFARI_IPOD, + expected: Browser.Safari, + }, + + // other + { + userAgent: USER_AGENTS.OPERA_WINDOWS, + expected: Browser.Other, + }, + { + userAgent: USER_AGENTS.YANDEX_WINDOWS, + expected: Browser.Other, + }, + { + 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); + expect(getBrowserName()).toBe(expected); + }); + }); +}); + +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); + }); + }); +}); diff --git a/src/shared/useragent/useragent.ts b/src/shared/useragent/useragent.ts new file mode 100644 index 000000000..ba405b65a --- /dev/null +++ b/src/shared/useragent/useragent.ts @@ -0,0 +1,33 @@ +import { Browser } from './constants'; +import { getBrowser } from './detect'; +import type { BrowserValue } from './types'; + +export { isMobileBrowser, isTabletBrowser } from './detect'; + +const BROWSER_MAP: Record = { + Chrome: Browser.Chrome, + Chromium: Browser.Chrome, + Firefox: Browser.Firefox, + 'Microsoft Edge': Browser.Edge, + Safari: Browser.Safari, +}; + +export function getBrowserName(): BrowserValue { + return BROWSER_MAP[getBrowser(navigator.userAgent).name] || Browser.Other; +} + +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 requiresUserInteraction(): boolean { + const browserName = getBrowserName(); + const version = getBrowserVersion(); + return ( + (browserName === Browser.Firefox && version >= 72) || + (browserName === Browser.Safari && version >= 12.1) + ); +} 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/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/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..5f2142265 100755 --- a/src/shared/utils/utils.ts +++ b/src/shared/utils/utils.ts @@ -1,23 +1,11 @@ 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'; -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. @@ -38,152 +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 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(); -} - /** * Returns true if match is in string; otherwise, returns false. */ @@ -294,9 +140,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 ( @@ -306,33 +153,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/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 afba1310c..b49bbb70a 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'; @@ -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'; @@ -198,11 +198,6 @@ describe('ServiceWorker', () => { }); test('should confirm delivery', async () => { - mockBowser.mockReturnValue({ - name: 'Chrome', - version: '130', - }); - const payload = mockOSMinifiedNotificationPayload({ custom: { rr: 'y', @@ -210,6 +205,8 @@ describe('ServiceWorker', () => { }); await dispatchEvent(new PushEvent('push', payload)); + await vi.runOnlyPendingTimersAsync(); + expect(apiPutSpy).toHaveBeenCalledWith( `notifications/${payload.custom.i}/report_received`, { @@ -715,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; + }, }), ); @@ -731,15 +730,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 b86113b9a..55c110940 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 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 { 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'; -import { - type NotificationClickEventInternal, - type NotificationForegroundWillDisplayEventSerializable, -} from '../../shared/models/NotificationEvent'; -import { - type IMutableOSNotification, - type IOSNotification, -} from '../../shared/models/OSNotification'; +} from 'src/shared/libraries/WorkerMessenger'; +import ContextSW from 'src/shared/models/ContextSW'; +import type { DeliveryPlatformKindValue } from 'src/shared/models/DeliveryPlatformKind'; +import type { + NotificationClickEventInternal, + NotificationForegroundWillDisplayEventSerializable, +} from 'src/shared/models/NotificationEvent'; +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 '../../shared/models/Session'; -import { SubscriptionStrategyKind } from '../../shared/models/SubscriptionStrategyKind'; -import Database from '../../shared/services/Database'; -import { awaitableTimeout } from '../../shared/utils/AwaitableTimeout'; +} from 'src/shared/session'; +import { Browser, getBrowserName } from 'src/shared/useragent'; +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 { 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'; +import type { OSServiceWorkerFields, SubscriptionChangeEvent } from './types'; declare const self: ServiceWorkerGlobalScope & OSServiceWorkerFields; @@ -359,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( @@ -378,7 +374,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; } /** @@ -673,7 +669,7 @@ export class ServiceWorker { ); if (this.requiresMacOS15ChromiumAfterDisplayWorkaround()) { - await awaitableTimeout(1_000); + await delay(1_000); } }