diff --git a/__test__/unit/push/registerForPush.test.ts b/__test__/unit/push/registerForPush.test.ts index 68978fcc7..a46648210 100644 --- a/__test__/unit/push/registerForPush.test.ts +++ b/__test__/unit/push/registerForPush.test.ts @@ -1,5 +1,8 @@ import * as InitHelper from '../../../src/shared/helpers/init'; +import { registerForPush } from '../../../src/shared/helpers/subscription'; import OneSignalEvent from '../../../src/shared/services/OneSignalEvent'; +import { NotificationType } from '../../../src/shared/subscriptions/constants'; +import { SubscriptionStrategyKind } from '../../../src/shared/models/SubscriptionStrategyKind'; import { TestEnvironment } from '../../support/environment/TestEnvironment'; //stub dismisshelper @@ -48,4 +51,30 @@ describe('Register for push', () => { ); expect(spy).toHaveBeenCalledTimes(1); }); + + test('registerForPush passes notificationTypesOverride to _subscribe', async () => { + const subscribeSpy = vi + .spyOn(OneSignal._context._subscriptionManager, '_subscribe') + .mockRejectedValue(new Error('mock stop')); + + await registerForPush(NotificationType._UserOptedOut); + + expect(subscribeSpy).toHaveBeenCalledWith( + SubscriptionStrategyKind._ResubscribeExisting, + NotificationType._UserOptedOut, + ); + }); + + test('registerForPush calls _subscribe without override when none provided', async () => { + const subscribeSpy = vi + .spyOn(OneSignal._context._subscriptionManager, '_subscribe') + .mockRejectedValue(new Error('mock stop')); + + await registerForPush(); + + expect(subscribeSpy).toHaveBeenCalledWith( + SubscriptionStrategyKind._ResubscribeExisting, + undefined, + ); + }); }); diff --git a/package.json b/package.json index 6b057daad..e7446f030 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,7 @@ }, { "path": "./build/releases/OneSignalSDK.page.es6.js", - "limit": "46.44 kB", + "limit": "46.56 kB", "gzip": true }, { diff --git a/src/shared/helpers/init.ts b/src/shared/helpers/init.ts index 2d58c7aab..08462751e 100755 --- a/src/shared/helpers/init.ts +++ b/src/shared/helpers/init.ts @@ -10,6 +10,7 @@ import { CustomLinkManager } from '../managers/CustomLinkManager'; import { SubscriptionStrategyKind } from '../models/SubscriptionStrategyKind'; import { limitStorePut } from '../services/limitStore'; import OneSignalEvent from '../services/OneSignalEvent'; +import { NotificationType } from '../subscriptions/constants'; import { IS_SERVICE_WORKER } from '../utils/env'; import { once } from '../utils/utils'; import { getAppId } from './main'; @@ -17,6 +18,8 @@ import { incrementPageViewCount } from './pageview'; import { triggerNotificationPermissionChanged } from './permissions'; import { registerForPush } from './subscription'; +let _didAppIdMigrate = false; + export async function internalInit() { Log._debug('Called internalInit()'); @@ -83,6 +86,10 @@ async function sessionInit(): Promise { await setSubscription(subscription); await handleAutoResubscribe(isOptedOut); + if (_didAppIdMigrate && isOptedOut) { + await createOptedOutSubscriptionForMigration(); + } + const isSubscribed = await OneSignal._context._subscriptionManager._isPushNotificationsEnabled(); // saves isSubscribed to IndexedDb. used for require user interaction functionality @@ -368,7 +375,9 @@ export async function initSaveState(overridingPageTitle?: string) { const config: AppConfig = OneSignal.config!; const previousAppId = await getIdsValue('appId'); - if (previousAppId && previousAppId !== appId) { + _didAppIdMigrate = !!(previousAppId && previousAppId !== appId); + + if (_didAppIdMigrate) { Log._info( `OneSignal: App ID changed from ${previousAppId} to ${appId}. Clearing stale state for migration.`, ); @@ -390,6 +399,29 @@ export async function initSaveState(overridingPageTitle?: string) { Log._info(`OneSignal: Set pageTitle to be '${pageTitle}'.`); } +async function createOptedOutSubscriptionForMigration() { + const currentPermission: NotificationPermission = + await OneSignal._context._permissionManager._getNotificationPermission( + OneSignal._context._appConfig.safariWebId, + ); + if (currentPermission !== 'granted') { + return; + } + + Log._info( + 'App ID migration: creating unsubscribed subscription for opted-out user.', + ); + + // Call _subscribe directly instead of registerForPush to avoid + // _registerSubscription (which resets optedOut to false) and + // checkAndTriggerSubscriptionChanged (which would read that false + // value and overwrite notification_types back to _Subscribed). + await OneSignal._context._subscriptionManager._subscribe( + SubscriptionStrategyKind._ResubscribeExisting, + NotificationType._UserOptedOut, + ); +} + async function handleAutoResubscribe(isOptedOut: boolean) { Log._info('handleAutoResubscribe', { autoResubscribe: OneSignal.config?.userConfig.autoResubscribe, diff --git a/src/shared/helpers/subscription.ts b/src/shared/helpers/subscription.ts index 32c3ad38d..46267ac2a 100755 --- a/src/shared/helpers/subscription.ts +++ b/src/shared/helpers/subscription.ts @@ -1,5 +1,8 @@ import { SubscriptionType } from 'src/shared/subscriptions/constants'; -import type { SubscriptionTypeValue } from 'src/shared/subscriptions/types'; +import type { + NotificationTypeValue, + SubscriptionTypeValue, +} from 'src/shared/subscriptions/types'; import Log from '../libraries/Log'; import { checkAndTriggerSubscriptionChanged } from '../listeners'; import { Subscription } from '../models/Subscription'; @@ -8,7 +11,9 @@ import { IS_SERVICE_WORKER } from '../utils/env'; import { incrementPageViewCount } from './pageview'; import { triggerNotificationPermissionChanged } from './permissions'; -export async function registerForPush(): Promise { +export async function registerForPush( + notificationTypesOverride?: NotificationTypeValue, +): Promise { const context = OneSignal._context; let subscription: Subscription | null = null; @@ -16,6 +21,7 @@ export async function registerForPush(): Promise { try { const rawSubscription = await context._subscriptionManager._subscribe( SubscriptionStrategyKind._ResubscribeExisting, + notificationTypesOverride, ); subscription = await context._subscriptionManager._registerSubscription(rawSubscription); diff --git a/src/shared/managers/SubscriptionManager.test.ts b/src/shared/managers/SubscriptionManager.test.ts index 3e0ba3bf1..27097a2e3 100644 --- a/src/shared/managers/SubscriptionManager.test.ts +++ b/src/shared/managers/SubscriptionManager.test.ts @@ -18,6 +18,7 @@ import { MockServiceWorker, } from '__test__/support/mocks/MockServiceWorker'; import { setPushToken } from '../database/subscription'; +import { NotificationType } from '../subscriptions/constants'; import { IDManager } from './IDManager'; import { SubscriptionManagerPage, @@ -145,6 +146,59 @@ describe('SubscriptionManager', () => { await vi.waitUntil(() => updateSubscriptionFn.mock.calls.length > 0); }); + + test('should apply notificationTypesOverride when creating a new model', async () => { + setCreateUserResponse(); + const rawSubscription = getRawPushSubscription(); + await setPushToken(rawSubscription.w3cEndpoint?.toString()); + + await updatePushSubscriptionModelWithRawSubscription( + rawSubscription, + NotificationType._UserOptedOut, + ); + + const subModels = OneSignal._coreDirector._subscriptionModelStore._list(); + expect(subModels.length).toBe(1); + expect(subModels[0]._notification_types).toBe( + NotificationType._UserOptedOut, + ); + expect(subModels[0].enabled).toBe(false); + + await vi.waitUntil(() => createUserFn.mock.calls.length > 0); + expect(createUserFn).toHaveBeenCalledWith( + expect.objectContaining({ + subscriptions: [ + expect.objectContaining({ + notification_types: NotificationType._UserOptedOut, + enabled: false, + }), + ], + }), + ); + }); + + test('should ignore notificationTypesOverride for existing models', async () => { + setUpdateSubscriptionResponse({ subscriptionId: '123' }); + const rawSubscription = getRawPushSubscription(); + await setPushToken(rawSubscription.w3cEndpoint?.toString()); + + await setupSubModelStore({ + id: '123', + token: rawSubscription.w3cEndpoint?.toString(), + onesignalId: EXTERNAL_ID, + }); + + await updatePushSubscriptionModelWithRawSubscription( + rawSubscription, + NotificationType._UserOptedOut, + ); + + const updatedPushModel = + (await OneSignal._coreDirector._getPushSubscriptionModel())!; + expect(updatedPushModel._notification_types).not.toBe( + NotificationType._UserOptedOut, + ); + }); }); }); diff --git a/src/shared/managers/subscription/page.ts b/src/shared/managers/subscription/page.ts index 3b6b1c698..11d0b3f1a 100644 --- a/src/shared/managers/subscription/page.ts +++ b/src/shared/managers/subscription/page.ts @@ -50,6 +50,7 @@ function executeCallback(callback?: (...args: any[]) => T, ...args: any[]) { export const updatePushSubscriptionModelWithRawSubscription = async ( rawPushSubscription: RawPushSubscription, + notificationTypesOverride?: NotificationTypeValue, ) => { // incase a login op was called before user accepts the notifcations permissions, we need to wait for it to finish // otherwise there would be two login ops in the same bucket for LoginOperationExecutor which would error @@ -62,6 +63,11 @@ export const updatePushSubscriptionModelWithRawSubscription = async ( OneSignal._coreDirector._generatePushSubscriptionModel( rawPushSubscription, ); + if (notificationTypesOverride !== undefined) { + pushModel._notification_types = notificationTypesOverride; + pushModel.enabled = + notificationTypesOverride === NotificationType._Subscribed; + } return createUserOnServer(); } // for users with data failed to create a user or user + subscription on the server @@ -236,6 +242,7 @@ export class SubscriptionManagerPage extends SubscriptionManagerBase { let rawPushSubscription: RawPushSubscription; @@ -258,7 +265,10 @@ export class SubscriptionManagerPage extends SubscriptionManagerBase