diff --git a/__test__/support/environment/TestEnvironmentHelpers.ts b/__test__/support/environment/TestEnvironmentHelpers.ts index 892a6fe96..f55a4b586 100644 --- a/__test__/support/environment/TestEnvironmentHelpers.ts +++ b/__test__/support/environment/TestEnvironmentHelpers.ts @@ -162,8 +162,10 @@ export const setupSubModelStore = async ({ token, onesignalId, }); - await Database.setPushId(pushModel.id); - await Database.setPushToken(pushModel.token); + await Database.setTokenAndId({ + token: pushModel.token, + id: pushModel.id, + }); OneSignal.coreDirector.subscriptionModelStore.replaceAll( [pushModel], ModelChangeTags.NO_PROPOGATE, diff --git a/__test__/unit/models/deliveryPlatformKind.test.ts b/__test__/unit/models/deliveryPlatformKind.test.ts index 8a37d4fad..23ecc6ee6 100644 --- a/__test__/unit/models/deliveryPlatformKind.test.ts +++ b/__test__/unit/models/deliveryPlatformKind.test.ts @@ -3,7 +3,6 @@ import { DeliveryPlatformKind } from '../../../src/shared/models/DeliveryPlatfor describe('DeliveryPlatformKind', () => { test('delivery platform constants should be correct', async () => { expect(DeliveryPlatformKind.ChromeLike).toBe(5); - expect(DeliveryPlatformKind.SafariLegacy).toBe(7); expect(DeliveryPlatformKind.Firefox).toBe(8); expect(DeliveryPlatformKind.Email).toBe(11); expect(DeliveryPlatformKind.Edge).toBe(12); diff --git a/package.json b/package.json index a75329a93..86f2751ae 100644 --- a/package.json +++ b/package.json @@ -65,17 +65,17 @@ "size-limit": [ { "path": "./build/releases/OneSignalSDK.page.js", - "limit": "1 kB", + "limit": "640 B", "gzip": true }, { "path": "./build/releases/OneSignalSDK.page.es6.js", - "limit": "65 kB", + "limit": "64.3 kB", "gzip": true }, { "path": "./build/releases/OneSignalSDK.sw.js", - "limit": "37.5 kB", + "limit": "37 kB", "gzip": true }, { diff --git a/src/core/executors/LoginUserOperationExecutor.ts b/src/core/executors/LoginUserOperationExecutor.ts index 9e6355b1f..cb73eca8b 100644 --- a/src/core/executors/LoginUserOperationExecutor.ts +++ b/src/core/executors/LoginUserOperationExecutor.ts @@ -223,8 +223,10 @@ export class LoginUserOperationExecutor implements IOperationExecutor { const pushSubscriptionId = await Database.getPushId(); if (pushSubscriptionId === localId) { - await Database.setPushId(backendSub.id); - await Database.setPushToken(backendSub.token); + await Database.setTokenAndId({ + token: backendSub.token, + id: backendSub.id, + }); } const model = diff --git a/src/core/executors/SubscriptionOperationExecutor.ts b/src/core/executors/SubscriptionOperationExecutor.ts index f521c774f..1c46b6205 100644 --- a/src/core/executors/SubscriptionOperationExecutor.ts +++ b/src/core/executors/SubscriptionOperationExecutor.ts @@ -124,8 +124,10 @@ export class SubscriptionOperationExecutor implements IOperationExecutor { const pushSubscriptionId = await Database.getPushId(); if (pushSubscriptionId === createOperation.subscriptionId) { - await Database.setPushId(backendSubscriptionId); - await Database.setPushToken(subscription?.token); + await Database.setTokenAndId({ + token: subscription?.token, + id: backendSubscriptionId, + }); } return new ExecutionResponse( diff --git a/src/global.d.ts b/src/global.d.ts index 4eb8bd0a1..795a39ff3 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -1,27 +1,5 @@ import { OneSignalDeferredLoadedCallback } from './page/models/OneSignalDeferredLoadedCallback'; -/** - * Types and names collected from: - * - https://developer.apple.com/documentation/safariextensions/safariremotenotification - * - https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/NotificationProgrammingGuideForWebsites/PushNotifications/PushNotifications.html - */ -interface SafariRemoteNotificationPermission { - readonly deviceToken: string | null; - readonly permission: 'default' | 'granted' | 'denied'; -} - -interface SafariRemoteNotification { - permission( - websitePushID: string | undefined, - ): SafariRemoteNotificationPermission; - requestPermission( - webAPIURL: string, - websitePushID: string | undefined, - queryParameterDictionary: unknown, - callback: (permissionData: SafariRemoteNotificationPermission) => void, - ): void; -} - type _OneSignal = typeof import('./onesignal/OneSignal').default; declare global { @@ -32,7 +10,7 @@ declare global { OneSignalDeferred?: OneSignalDeferredLoadedCallback[]; __oneSignalSdkLoadCount?: number; safari?: { - pushNotification?: SafariRemoteNotification; + pushNotification?: {}; }; } } diff --git a/src/page/bell/Dialog.ts b/src/page/bell/Dialog.ts index 4b50099aa..2c14ed521 100755 --- a/src/page/bell/Dialog.ts +++ b/src/page/bell/Dialog.ts @@ -1,13 +1,14 @@ -import OneSignalEvent from '../../shared/services/OneSignalEvent'; import SdkEnvironment from '../../shared/managers/SdkEnvironment'; +import OneSignalEvent from '../../shared/services/OneSignalEvent'; +import { bowserCastle } from '../../shared/utils/bowserCastle'; import { addDomElement, clearDomElementChildren, getPlatformNotificationIcon, } from '../../shared/utils/utils'; +import type { NotificationIcons } from '../types'; import AnimatedElement from './AnimatedElement'; import Bell from './Bell'; -import { bowserCastle } from '../../shared/utils/bowserCastle'; export default class Dialog extends AnimatedElement { public bell: Bell; diff --git a/src/page/slidedown/Slidedown.ts b/src/page/slidedown/Slidedown.ts index 7509c018a..3d612630e 100755 --- a/src/page/slidedown/Slidedown.ts +++ b/src/page/slidedown/Slidedown.ts @@ -1,33 +1,34 @@ -import OneSignalEvent from '../../shared/services/OneSignalEvent'; +import { SERVER_CONFIG_DEFAULTS_SLIDEDOWN } from '../../shared/config/constants'; +import { Utils } from '../../shared/context/Utils'; import MainHelper from '../../shared/helpers/MainHelper'; +import PromptsHelper from '../../shared/helpers/PromptsHelper'; +import { + DelayedPromptType, + SlidedownPromptOptions, +} from '../../shared/models/Prompts'; +import OneSignalEvent from '../../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, - removeDomElement, removeCssClass, - getDomElementOrStub, + removeDomElement, } from '../../shared/utils/utils'; -import { SERVER_CONFIG_DEFAULTS_SLIDEDOWN } from '../../shared/config/constants'; +import { InvalidChannelInputField } from '../errors/ChannelCaptureError'; +import { TagCategory } from '../models/Tags'; +import type { NotificationIcons } from '../types'; +import ChannelCaptureContainer from './ChannelCaptureContainer'; import { getLoadingIndicatorWithColor } from './LoadingIndicator'; import { getRetryIndicator } from './RetryIndicator'; -import { - SLIDEDOWN_CSS_CLASSES, - SLIDEDOWN_CSS_IDS, - COLORS, -} from '../../shared/slidedown/constants'; -import { TagCategory } from '../models/Tags'; import { getSlidedownElement } from './SlidedownElement'; -import { Utils } from '../../shared/context/Utils'; -import ChannelCaptureContainer from './ChannelCaptureContainer'; -import PromptsHelper from '../../shared/helpers/PromptsHelper'; -import { - SlidedownPromptOptions, - DelayedPromptType, -} from '../../shared/models/Prompts'; -import { InvalidChannelInputField } from '../errors/ChannelCaptureError'; -import { bowserCastle } from '../../shared/utils/bowserCastle'; export default class Slidedown { public options: SlidedownPromptOptions; diff --git a/src/page/models/NotificationIcons.ts b/src/page/types.ts similarity index 61% rename from src/page/models/NotificationIcons.ts rename to src/page/types.ts index a4bf59a30..e5cb1c333 100644 --- a/src/page/models/NotificationIcons.ts +++ b/src/page/types.ts @@ -1,4 +1,4 @@ -interface NotificationIcons { +export interface NotificationIcons { chrome?: string; firefox?: string; safari?: string; diff --git a/src/page/userModel/FuturePushSubscriptionRecord.ts b/src/page/userModel/FuturePushSubscriptionRecord.ts index 04ee1ec00..17bd3e4cc 100644 --- a/src/page/userModel/FuturePushSubscriptionRecord.ts +++ b/src/page/userModel/FuturePushSubscriptionRecord.ts @@ -37,10 +37,7 @@ export default class FuturePushSubscriptionRecord implements Serializable { } private _getToken(subscription: RawPushSubscription): string | undefined { - if (subscription.w3cEndpoint) { - return subscription.w3cEndpoint.toString(); - } - return subscription.safariDeviceToken; + return subscription.w3cEndpoint?.toString(); } serialize() { @@ -70,9 +67,6 @@ export default class FuturePushSubscriptionRecord implements Serializable { if (Environment.useSafariVapidPush()) { return SubscriptionType.SafariPush; } - if (Environment.useSafariLegacyPush()) { - return SubscriptionType.SafariLegacyPush; - } // Other browsers, like Edge, are Chromium based so we consider them "Chrome". return SubscriptionType.ChromePush; } @@ -85,8 +79,6 @@ export default class FuturePushSubscriptionRecord implements Serializable { switch (this.getSubscriptionType()) { case SubscriptionType.FirefoxPush: return DeliveryPlatformKind.Firefox; - case SubscriptionType.SafariLegacyPush: - return DeliveryPlatformKind.SafariLegacy; case SubscriptionType.SafariPush: return DeliveryPlatformKind.SafariVapid; } diff --git a/src/page/utils/BrowserSupportsPush.ts b/src/page/utils/BrowserSupportsPush.ts index 71303dc6e..15e25a687 100644 --- a/src/page/utils/BrowserSupportsPush.ts +++ b/src/page/utils/BrowserSupportsPush.ts @@ -9,7 +9,8 @@ export function isPushNotificationsSupported() { return supportsVapidPush() || supportsSafariLegacyPush(); } -// Does the browser support legacy Safari push? (only available on macOS) +// Allow app to run with legacy safari push notifications. If safari version is newer then +// the subscription will ported in SubscriptionManager _updatePushSubscriptionModelWithRawSubscription export function supportsSafariLegacyPush(): boolean { return ( typeof window.safari !== 'undefined' && diff --git a/src/shared/helpers/Environment.ts b/src/shared/helpers/Environment.ts index 64527ea5c..c51eb7379 100755 --- a/src/shared/helpers/Environment.ts +++ b/src/shared/helpers/Environment.ts @@ -12,20 +12,8 @@ export default class Environment { return typeof window !== 'undefined'; } - // Prefer Legacy Safari if API is available over VAPID until Safari - // fixes issues with it. - public static useSafariLegacyPush(): boolean { - return this.isBrowser() && window.safari?.pushNotification != undefined; - } - - // This is the counter part to useSafariLegacyPush(); as it notes only use - // Safari VAPID if it doesn't have legacy Safari push. public static useSafariVapidPush(): boolean { - return ( - bowserCastle().name == 'safari' && - supportsVapidPush() && - !this.useSafariLegacyPush() - ); + return bowserCastle().name == 'safari' && supportsVapidPush(); } public static version() { diff --git a/src/shared/helpers/MainHelper.ts b/src/shared/helpers/MainHelper.ts index ddcadf283..b0f447d87 100755 --- a/src/shared/helpers/MainHelper.ts +++ b/src/shared/helpers/MainHelper.ts @@ -21,7 +21,6 @@ import { import Database from '../services/Database'; import { PermissionUtils } from '../utils/PermissionUtils'; import { getPlatformNotificationIcon, logMethodCall } from '../utils/utils'; -import Environment from './Environment'; export default class MainHelper { static async showLocalNotification( @@ -234,13 +233,6 @@ export default class MainHelper { // TO DO: unit test static async getCurrentPushToken(): Promise { - if (Environment.useSafariLegacyPush()) { - const safariToken = window.safari?.pushNotification?.permission( - OneSignal.config.safariWebId, - ).deviceToken; - return safariToken?.toLowerCase() || undefined; - } - const registration = await OneSignal.context.serviceWorkerManager.getRegistration(); if (!registration) { diff --git a/src/shared/managers/PermissionManager.ts b/src/shared/managers/PermissionManager.ts index 96ab5ca59..6d7aa9d70 100644 --- a/src/shared/managers/PermissionManager.ts +++ b/src/shared/managers/PermissionManager.ts @@ -1,10 +1,5 @@ -import { - InvalidArgumentError, - InvalidArgumentReason, -} from '../errors/InvalidArgumentError'; -import { NotificationPermission } from '../models/NotificationPermission'; import OneSignalError from '../errors/OneSignalError'; -import Environment from '../helpers/Environment'; +import { NotificationPermission } from '../models/NotificationPermission'; /** * A permission manager to consolidate the different quirks of obtaining and evaluating permissions @@ -26,47 +21,15 @@ export default class PermissionManager { ); } - return await OneSignal.context.permissionManager.getNotificationPermission( - OneSignal.config!.safariWebId, - ); + return await OneSignal.context.permissionManager.getNotificationPermission(); } /** - * Notification permission reported by the browser. - * - * @param safariWebId The Safari web ID necessary to access the permission - * state on Legacy Safari on macOS. - */ - public async getNotificationPermission( - safariWebId?: string, - ): Promise { - if (Environment.useSafariLegacyPush()) { - return PermissionManager.getLegacySafariNotificationPermission( - safariWebId, - ); - } - return this.getW3cNotificationPermission(); - } - - /** - * Returns the Safari browser's notification permission as reported by the browser. - * - * @param safariWebId The Safari web ID necessary for Legacy Safari on macOS. - */ - private static getLegacySafariNotificationPermission( - safariWebId?: string, - ): NotificationPermission { - if (safariWebId) - return window.safari.pushNotification.permission(safariWebId) - .permission as NotificationPermission; - throw new InvalidArgumentError('safariWebId', InvalidArgumentReason.Empty); - } - /** * Returns the notification permission as reported by the browser. * - Expect for legacy Safari on macOS. */ - private getW3cNotificationPermission(): NotificationPermission { + public getNotificationPermission(): NotificationPermission { return Notification.permission as NotificationPermission; } } diff --git a/src/shared/managers/ServiceWorkerManager.ts b/src/shared/managers/ServiceWorkerManager.ts index 29a544f91..c048bade3 100644 --- a/src/shared/managers/ServiceWorkerManager.ts +++ b/src/shared/managers/ServiceWorkerManager.ts @@ -155,9 +155,7 @@ export class ServiceWorkerManager { workerState === ServiceWorkerActiveState.ThirdParty ) { const permission = - await OneSignal.context.permissionManager.getNotificationPermission( - OneSignal.config!.safariWebId, - ); + await OneSignal.context.permissionManager.getNotificationPermission(); const notificationsEnabled = permission === 'granted'; if (notificationsEnabled) { Log.info( diff --git a/src/shared/managers/SubscriptionManager.test.ts b/src/shared/managers/SubscriptionManager.test.ts index facc5ee5d..903053ddd 100644 --- a/src/shared/managers/SubscriptionManager.test.ts +++ b/src/shared/managers/SubscriptionManager.test.ts @@ -1,17 +1,20 @@ import { APP_ID, DUMMY_EXTERNAL_ID } from '__test__/support/constants'; import TestContext from '__test__/support/environment/TestContext'; import { TestEnvironment } from '__test__/support/environment/TestEnvironment'; -import { setupSubModelStore } from '__test__/support/environment/TestEnvironmentHelpers'; import { - createUserFn, - setCreateUserResponse, -} from '__test__/support/helpers/requests'; + mockUserAgent, + setupSubModelStore, +} from '__test__/support/environment/TestEnvironmentHelpers'; import { getSubscriptionFn, MockServiceWorker, } from '__test__/support/mocks/MockServiceWorker'; +import BrowserUserAgent from '__test__/support/models/BrowserUserAgent'; +import { SubscriptionType } from 'src/core/types/subscription'; +import UserDirector from 'src/onesignal/UserDirector'; import ContextSW from '../models/ContextSW'; import { RawPushSubscription } from '../models/RawPushSubscription'; +import Database from '../services/Database'; import { IDManager } from './IDManager'; import { SubscriptionManager, @@ -29,19 +32,24 @@ const getRawSubscription = (): RawPushSubscription => { rawSubscription.w3cAuth = 'auth'; rawSubscription.w3cP256dh = 'p256dh'; rawSubscription.w3cEndpoint = new URL('https://example.com/endpoint'); + // @ts-expect-error - legacy property rawSubscription.safariDeviceToken = 'safariDeviceToken'; return rawSubscription; }; +const createUserOnServerSpy = vi + .spyOn(UserDirector, 'createUserOnServer') + .mockResolvedValue(); + describe('SubscriptionManager', () => { beforeEach(async () => { vi.resetModules(); await TestEnvironment.initialize(); + await Database.clear(); }); describe('updatePushSubscriptionModelWithRawSubscription', () => { test('should create the push subscription model if it does not exist', async () => { - setCreateUserResponse(); const context = new ContextSW(TestContext.getFakeMergedConfig()); const subscriptionManager = new SubscriptionManager(context, subConfig); const rawSubscription = getRawSubscription(); @@ -51,10 +59,11 @@ describe('SubscriptionManager', () => { expect(subModels.length).toBe(0); // mimicing the event helper checkAndTriggerSubscriptionChanged - await OneSignal.database.setPushToken( - rawSubscription.w3cEndpoint?.toString(), - ); + await OneSignal.database.setTokenAndId({ + token: rawSubscription.w3cEndpoint?.toString(), + }); + // @ts-expect-error - private method await subscriptionManager._updatePushSubscriptionModelWithRawSubscription( rawSubscription, ); @@ -62,10 +71,7 @@ describe('SubscriptionManager', () => { subModels = await OneSignal.coreDirector.subscriptionModelStore.list(); expect(subModels.length).toBe(1); - const id = subModels[0].id; - expect(IDManager.isLocalId(id)).toBe(true); expect(subModels[0].toJSON()).toEqual({ - id, device_model: '', device_os: 56, enabled: true, @@ -77,28 +83,7 @@ describe('SubscriptionManager', () => { web_p256: rawSubscription.w3cP256dh, }); - await vi.waitUntil(() => createUserFn.mock.calls.length > 0); - expect(createUserFn).toHaveBeenCalledWith({ - identity: {}, - properties: { - language: 'en', - timezone_id: 'America/Los_Angeles', - }, - refresh_device_metadata: true, - subscriptions: [ - { - device_model: '', - device_os: 56, - enabled: true, - notification_types: 1, - sdk: '1', - token: rawSubscription.w3cEndpoint?.toString(), - type: 'ChromePush', - web_auth: rawSubscription.w3cAuth, - web_p256: rawSubscription.w3cP256dh, - }, - ], - }); + expect(createUserOnServerSpy).toHaveBeenCalled(); }); test('should create user if push subscription model does not have an id', async () => { @@ -110,9 +95,6 @@ describe('SubscriptionManager', () => { getSubscriptionFn.mockResolvedValue({ endpoint: rawSubscription.w3cEndpoint?.toString(), }); - setCreateUserResponse({ - externalId: 'some-external-id', - }); const context = new ContextSW(TestContext.getFakeMergedConfig()); const subscriptionManager = new SubscriptionManager(context, subConfig); @@ -128,45 +110,24 @@ describe('SubscriptionManager', () => { onesignalId: identityModel.onesignalId, }); + // @ts-expect-error - private method await subscriptionManager._updatePushSubscriptionModelWithRawSubscription( rawSubscription, ); // should not call generatePushSubscriptionModelSpy expect(generatePushSubscriptionModelSpy).not.toHaveBeenCalled(); - - expect(createUserFn).toHaveBeenCalledWith({ - identity: { - external_id: 'some-external-id', - }, - properties: { - language: 'en', - timezone_id: 'America/Los_Angeles', - }, - refresh_device_metadata: true, - subscriptions: [ - { - device_model: '', - device_os: 56, - enabled: true, - notification_types: 1, - sdk: '1', - token: rawSubscription.w3cEndpoint?.toString(), - type: 'ChromePush', - }, - ], - }); + expect(createUserOnServerSpy).toHaveBeenCalled(); }); test('should update the push subscription model if it already exists', async () => { - setCreateUserResponse(); const context = new ContextSW(TestContext.getFakeMergedConfig()); const subscriptionManager = new SubscriptionManager(context, subConfig); const rawSubscription = getRawSubscription(); - await OneSignal.database.setPushToken( - rawSubscription.w3cEndpoint?.toString(), - ); + await OneSignal.database.setTokenAndId({ + token: rawSubscription.w3cEndpoint?.toString(), + }); const pushModel = await setupSubModelStore({ id: '123', @@ -176,6 +137,7 @@ describe('SubscriptionManager', () => { pushModel.web_auth = 'old-web-auth'; pushModel.web_p256 = 'old-web-p256'; + // @ts-expect-error - private method await subscriptionManager._updatePushSubscriptionModelWithRawSubscription( rawSubscription, ); @@ -188,6 +150,47 @@ describe('SubscriptionManager', () => { expect(updatedPushModel.web_auth).toBe(rawSubscription.w3cAuth); expect(updatedPushModel.web_p256).toBe(rawSubscription.w3cP256dh); }); + + test('should port legacy safari push to new format', async () => { + const rawSubscription = getRawSubscription(); + getSubscriptionFn.mockResolvedValue({ + endpoint: rawSubscription.w3cEndpoint?.toString(), + }); + + const context = new ContextSW(TestContext.getFakeMergedConfig()); + const subscriptionManager = new SubscriptionManager(context, subConfig); + + // setting up legacy push model + await OneSignal.database.setTokenAndId({ + token: 'old-token', + }); + const pushModel = await setupSubModelStore({ + id: '123', + token: 'old-token', + onesignalId: DUMMY_EXTERNAL_ID, + }); + pushModel.type = SubscriptionType.SafariLegacyPush; + + // mock agent to safari with vapid push support + mockUserAgent({ + userAgent: BrowserUserAgent.SafariSupportedMac121, + }); + + // @ts-expect-error - private method + await subscriptionManager._updatePushSubscriptionModelWithRawSubscription( + rawSubscription, + ); + + // should update push model with new token, type, web_auth, and web_p256 + const updatedPushModel = + (await OneSignal.coreDirector.getPushSubscriptionModel())!; + expect(updatedPushModel.type).toBe(SubscriptionType.SafariPush); + expect(updatedPushModel.token).toBe( + rawSubscription.w3cEndpoint!.toString(), + ); + expect(updatedPushModel.web_auth).toBe(rawSubscription.w3cAuth); + expect(updatedPushModel.web_p256).toBe(rawSubscription.w3cP256dh); + }); }); }); @@ -195,3 +198,12 @@ Object.defineProperty(global.navigator, 'serviceWorker', { value: new MockServiceWorker(), writable: true, }); + +Object.defineProperty(global, 'PushSubscriptionOptions', { + value: { + prototype: { + applicationServerKey: 'test', + }, + }, + writable: true, +}); diff --git a/src/shared/managers/SubscriptionManager.ts b/src/shared/managers/SubscriptionManager.ts index 4797790ac..e6b0a5b64 100644 --- a/src/shared/managers/SubscriptionManager.ts +++ b/src/shared/managers/SubscriptionManager.ts @@ -1,6 +1,7 @@ import { NotificationType, NotificationTypeValue, + SubscriptionType, } from 'src/core/types/subscription'; import { isCompleteSubscriptionObject } from '../../core/utils/typePredicates'; import UserDirector from '../../onesignal/UserDirector'; @@ -13,11 +14,7 @@ import NotImplementedError from '../errors/NotImplementedError'; import PushPermissionNotGrantedError, { PushPermissionNotGrantedErrorReason, } from '../errors/PushPermissionNotGrantedError'; -import { SdkInitError, SdkInitErrorKind } from '../errors/SdkInitError'; import ServiceWorkerRegistrationError from '../errors/ServiceWorkerRegistrationError'; -import SubscriptionError, { - SubscriptionErrorReason, -} from '../errors/SubscriptionError'; import Environment from '../helpers/Environment'; import { ServiceWorkerActiveState } from '../helpers/ServiceWorkerHelper'; import Log from '../libraries/Log'; @@ -61,7 +58,6 @@ export type SubscriptionStateServiceWorkerNotIntalled = Exclude< export class SubscriptionManager { private context: ContextSWInterface; private config: SubscriptionManagerConfig; - private safariPermissionPromptFailed = false; constructor(context: ContextSWInterface, config: SubscriptionManagerConfig) { this.context = context; @@ -142,26 +138,11 @@ export class SubscriptionManager { PushPermissionNotGrantedErrorReason.Blocked, ); - if (Environment.useSafariLegacyPush()) { - rawPushSubscription = await this.subscribeSafari(); - await this._updatePushSubscriptionModelWithRawSubscription( - rawPushSubscription, - ); - /* Now that permissions have been granted, install the service worker */ - Log.info('Installing SW on Safari'); - try { - await this.context.serviceWorkerManager.installWorker(); - Log.info('SW on Safari successfully installed'); - } catch (e) { - Log.error('SW on Safari failed to install.'); - } - } else { - rawPushSubscription = - await this.subscribeFcmFromPage(subscriptionStrategy); - await this._updatePushSubscriptionModelWithRawSubscription( - rawPushSubscription, - ); - } + rawPushSubscription = + await this.subscribeFcmFromPage(subscriptionStrategy); + await this._updatePushSubscriptionModelWithRawSubscription( + rawPushSubscription, + ); break; default: throw new InvalidStateError(InvalidStateReason.UnsupportedEnvironment); @@ -174,23 +155,32 @@ export class SubscriptionManager { rawPushSubscription: RawPushSubscription, ) { const pushModel = await OneSignal.coreDirector.getPushSubscriptionModel(); + // EventHelper checkAndTriggerSubscriptionChanged is called before this function when permission is granted and so // it will save the push token/id to the database so we don't need to save the token afer generating if (!pushModel) { OneSignal.coreDirector.generatePushSubscriptionModel(rawPushSubscription); return UserDirector.createUserOnServer(); - - // Bug w/ v160400 release where isCreatingUser was improperly set and never reset - // so a check if pushModel id is needed to recreate the user - } - if (!pushModel.id) { - return UserDirector.createUserOnServer(); } - // resubscribing. update existing push subscription model + // Bug w/ v160400 release where isCreatingUser was improperly set and never reset + // so a check if pushModel id is needed to recreate the user + if (!pushModel.id) return UserDirector.createUserOnServer(); + const serializedSubscriptionRecord = new FuturePushSubscriptionRecord( rawPushSubscription, ).serialize(); + + // for legacy safari push, switch to new format (e.g. old token 'ebsm3...' to -> https://web.push.apple.com/... with populated web_auth and web_p256) + if (pushModel.type === SubscriptionType.SafariLegacyPush) { + if (!Environment.useSafariVapidPush()) return; + await Database.setTokenAndId({ + token: serializedSubscriptionRecord.token, + id: pushModel.id, + }); + } + + // update existing push subscription model for (const key in serializedSubscriptionRecord) { const modelKey = key as keyof typeof serializedSubscriptionRecord; pushModel.setProperty(modelKey, serializedSubscriptionRecord[modelKey]); @@ -274,13 +264,9 @@ export class SubscriptionManager { subscription.deviceId = DEFAULT_DEVICE_ID; subscription.optedOut = false; if (pushSubscription) { - if (Environment.useSafariLegacyPush()) { - subscription.subscriptionToken = pushSubscription.safariDeviceToken; - } else { - subscription.subscriptionToken = pushSubscription.w3cEndpoint - ? pushSubscription.w3cEndpoint.toString() - : null; - } + subscription.subscriptionToken = pushSubscription.w3cEndpoint + ? pushSubscription.w3cEndpoint.toString() + : null; } else { subscription.subscriptionToken = null; } @@ -339,83 +325,6 @@ export class SubscriptionManager { return !!deviceId; } - private async subscribeSafariPromptPermission(): Promise { - const requestPermission = (url: string) => { - return new Promise((resolve) => { - window.safari?.pushNotification?.requestPermission( - url, - this.config.safariWebId, - { app_id: this.config.appId }, - (response) => { - if (response && response.deviceToken) { - resolve(response.deviceToken.toLowerCase()); - } else { - resolve(null); - } - }, - ); - }); - }; - - if (!this.safariPermissionPromptFailed) { - return requestPermission( - `${SdkEnvironment.getOneSignalApiUrl({ - legacy: true, - }).toString()}safari/apps/${this.config.appId}`, - ); - } else { - // If last attempt failed, retry with the legacy URL - return requestPermission( - `${SdkEnvironment.getOneSignalApiUrl({ - legacy: true, - }).toString()}safari`, - ); - } - } - - private async subscribeSafari(): Promise { - const pushSubscriptionDetails = new RawPushSubscription(); - if (!this.config.safariWebId) { - throw new SdkInitError(SdkInitErrorKind.MissingSafariWebId); - } - - const { deviceToken: existingDeviceToken } = - window.safari?.pushNotification?.permission(this.config.safariWebId) || - {}; - - if (existingDeviceToken) { - pushSubscriptionDetails.setFromSafariSubscription( - existingDeviceToken.toLowerCase(), - ); - return pushSubscriptionDetails; - } - - /* - We're about to show the Safari native permission request. It can fail for a number of - reasons, e.g.: - - Setup-related reasons when developers just starting to get set up - - Address bar URL doesn't match safari certificate allowed origins (case-sensitive) - - Safari web ID doesn't match provided web ID - - Browsing in a Safari private window - - Bad icon DPI - - but shouldn't fail for sites that have already gotten Safari working. - - We'll show the permissionPromptDisplay event if the Safari user isn't already subscribed, - otherwise an already subscribed Safari user would not see the permission request again. - */ - OneSignalEvent.trigger(OneSignal.EVENTS.PERMISSION_PROMPT_DISPLAYED); - const deviceToken = await this.subscribeSafariPromptPermission(); - PermissionUtils.triggerNotificationPermissionChanged(); - if (deviceToken) { - pushSubscriptionDetails.setFromSafariSubscription(deviceToken); - } else { - this.safariPermissionPromptFailed = true; - throw new SubscriptionError(SubscriptionErrorReason.InvalidSafariSetup); - } - return pushSubscriptionDetails; - } - private async subscribeFcmFromPage( subscriptionStrategy: SubscriptionStrategyKind, ): Promise { @@ -823,23 +732,6 @@ export class SubscriptionManager { pushSubscriptionModel, ); - if (Environment.useSafariLegacyPush()) { - const subscriptionState = window.safari?.pushNotification?.permission( - this.config.safariWebId, - ); - const isSubscribedToSafari = !!( - isValidPushSubscription && - subscriptionToken && - subscriptionState?.permission === 'granted' && - subscriptionState?.deviceToken - ); - - return { - subscribed: isSubscribedToSafari, - optedOut: !!optedOut, - }; - } - const workerRegistration = await this.context.serviceWorkerManager.getOneSignalRegistration(); const notificationPermission = diff --git a/src/shared/models/DeliveryPlatformKind.ts b/src/shared/models/DeliveryPlatformKind.ts index 86d4a3876..ec11e00ca 100644 --- a/src/shared/models/DeliveryPlatformKind.ts +++ b/src/shared/models/DeliveryPlatformKind.ts @@ -1,6 +1,5 @@ export enum DeliveryPlatformKind { ChromeLike = 5, - SafariLegacy = 7, Firefox = 8, Email = 11, Edge = 12, diff --git a/src/shared/models/RawPushSubscription.ts b/src/shared/models/RawPushSubscription.ts index 4905ac376..5b2aaf861 100644 --- a/src/shared/models/RawPushSubscription.ts +++ b/src/shared/models/RawPushSubscription.ts @@ -2,16 +2,11 @@ import { Serializable } from '../../page/models/Serializable'; export class RawPushSubscription implements Serializable { /** - * The GCM/FCM registration token, along with the full URL. Not used for Safari. + * The GCM/FCM registration token, along with the full URL.i. */ w3cEndpoint: URL | undefined; w3cP256dh: string | undefined; w3cAuth: string | undefined; - /** - * A Safari-only push subscription device token. Not used for Chrome/Firefox. - */ - safariDeviceToken: string | undefined; - /** * Given a native W3C browser push subscription, takes the endpoint, p256dh, * and auth. @@ -35,7 +30,7 @@ export class RawPushSubscription implements Serializable { } catch (e) { // User is most likely running < Chrome < 50 } - let auth = null; + let auth: ArrayBuffer | null = null; try { auth = pushSubscription.getKey('auth'); } catch (e) { @@ -62,46 +57,29 @@ export class RawPushSubscription implements Serializable { return rawPushSubscription; } - /** - * Given a native browser Safari push subscription, sets the device token - * property. - * - * @param safariDeviceToken A native browser Safari push subscription. - */ - public setFromSafariSubscription(safariDeviceToken?: string | null) { - if (!safariDeviceToken) { - return; - } - this.safariDeviceToken = safariDeviceToken; - } - public serialize() { const serializedBundle = { /* Old Parameters */ - w3cEndpoint: this.w3cEndpoint ? this.w3cEndpoint.toString() : null, + w3cEndpoint: this.w3cEndpoint?.toString(), w3cP256dh: this.w3cP256dh, w3cAuth: this.w3cAuth, - safariDeviceToken: this.safariDeviceToken, }; return serializedBundle; } - // TODO: had a hard to debug bug here due to "any" type bypassing typescript validation. - // Check the usage and maybe change with strict type - public static deserialize(bundle: any): RawPushSubscription { + public static deserialize(bundle?: { + w3cEndpoint: string; + w3cP256dh: string; + w3cAuth: string; + }): RawPushSubscription { const subscription = new RawPushSubscription(); if (!bundle) { return subscription; } - try { - subscription.w3cEndpoint = new URL(bundle.w3cEndpoint); - } catch (e) { - // w3cEndpoint will be null for Safari - } + subscription.w3cEndpoint = new URL(bundle.w3cEndpoint); subscription.w3cP256dh = bundle.w3cP256dh; subscription.w3cAuth = bundle.w3cAuth; - subscription.safariDeviceToken = bundle.safariDeviceToken; return subscription; } } diff --git a/src/shared/services/Database.ts b/src/shared/services/Database.ts index e368eff7e..f26205b28 100644 --- a/src/shared/services/Database.ts +++ b/src/shared/services/Database.ts @@ -534,6 +534,17 @@ export default class Database { static async setPushToken(pushToken: string | undefined): Promise { await this.put('Options', { key: 'lastPushToken', value: pushToken }); } + static async setTokenAndId({ + token, + id, + }: { + token?: string; + id?: string; + }): Promise { + if (token) + await this.put('Options', { key: 'lastPushToken', value: token }); + if (id) await this.put('Options', { key: 'lastPushId', value: id }); + } static async setIsPushEnabled(enabled: boolean): Promise { return Database.singletonInstance.setIsPushEnabled(enabled); diff --git a/src/shared/utils/utils.ts b/src/shared/utils/utils.ts index 8c06aad00..2ef2abe5c 100755 --- a/src/shared/utils/utils.ts +++ b/src/shared/utils/utils.ts @@ -1,3 +1,4 @@ +import type { NotificationIcons } from '../../page/types'; import { Utils } from '../context/Utils'; import Log from '../libraries/Log'; import SdkEnvironment from '../managers/SdkEnvironment'; diff --git a/vitest.config.ts b/vitest.config.ts index eb4498959..e48c42e00 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -4,7 +4,7 @@ import { defineConfig } from 'vitest/config'; export default defineConfig({ define: { __API_ORIGIN__: JSON.stringify('onesignal.com'), - __API_TYPE__: JSON.stringify('staging'), + __API_TYPE__: JSON.stringify('production'), __BUILD_ORIGIN__: JSON.stringify('onesignal.com'), __BUILD_TYPE__: JSON.stringify('production'), __IS_HTTPS__: JSON.stringify(true),