Skip to content

Commit

Permalink
Merge pull request #1075 from OneSignal/user-model/fix-notification-p…
Browse files Browse the repository at this point in the history
…ermission-change-event-type

[User Model] [Fix] Notification permission change event type to boolean
  • Loading branch information
jkasten2 committed Aug 4, 2023
2 parents 9e026c2 + b1af11c commit efa51db
Show file tree
Hide file tree
Showing 10 changed files with 113 additions and 66 deletions.
13 changes: 13 additions & 0 deletions __test__/support/managers/PermissionManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { PermissionUtils } from "../../../src/shared/utils/PermissionUtils";
import RealPermissionManager from "../../../src/shared/managers/PermissionManager"

export class PermissionManager {
public static async mockNotificationPermissionChange(
test: jest.It,
nativePermission: NotificationPermission,
): Promise<void> {
test.stub(RealPermissionManager.prototype, 'getPermissionStatus', nativePermission);
// mimick native permission change
await PermissionUtils.triggerNotificationPermissionChanged();
}
}
77 changes: 51 additions & 26 deletions __test__/unit/notifications/permission.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,20 @@
import ModelCache from "../../../src/core/caching/ModelCache";
import PermissionManager from "../../../src/shared/managers/PermissionManager";
import { PermissionUtils } from "../../../src/shared/utils/PermissionUtils";
import { NotificationPermission } from "../../../src/shared/models/NotificationPermission";
import { TestEnvironment } from "../../support/environment/TestEnvironment";
import { PermissionManager } from "../../support/managers/PermissionManager";
import OneSignal from "../../../src/onesignal/OneSignal";

function expectPermissionChangeEvent(expectedPermission: boolean): Promise<void> {
return new Promise((resolver) => {
OneSignal.Notifications.addEventListener(
"permissionChange",
(permission: boolean) => {
expect(permission).toBe(expectedPermission)
resolver();
}
);
});
}

describe('Notifications namespace permission properties', () => {
beforeEach(async () => {
Expand All @@ -15,46 +27,59 @@ describe('Notifications namespace permission properties', () => {
jest.resetAllMocks();
});

test('When permission changes to granted, ensure permissionChange fires with true', async () => {
const expectedPromise = expectPermissionChangeEvent(true);
await PermissionManager.mockNotificationPermissionChange(test, NotificationPermission.Granted);
await expectedPromise;
});

test('When permission changes to granted, we update the permission properties on the Notifications namespace', async () => {
test.stub(PermissionManager.prototype, 'getPermissionStatus', Promise.resolve(NotificationPermission.Granted));
const permissionChangeEventFiredPromise = new Promise(resolve => {
OneSignal.emitter.on(OneSignal.EVENTS.NATIVE_PROMPT_PERMISSIONCHANGED, resolve);
});
test('When permission changes to Denied, ensure permissionChange fires with false', async () => {
await PermissionManager.mockNotificationPermissionChange(test, NotificationPermission.Granted);

// mimick native permission change
PermissionUtils.triggerNotificationPermissionChanged();
const expectedPromise = expectPermissionChangeEvent(false);
await PermissionManager.mockNotificationPermissionChange(test, NotificationPermission.Denied);
await expectedPromise;
});

test('When permission changes to Default, ensure permissionChange fires with false', async () => {
await PermissionManager.mockNotificationPermissionChange(test, NotificationPermission.Granted);

const expectedPromise = expectPermissionChangeEvent(false);
await PermissionManager.mockNotificationPermissionChange(test, NotificationPermission.Default);
await expectedPromise;
});

test('When permission changes to granted, we update the permission properties on the Notifications namespace', async () => {
await PermissionManager.mockNotificationPermissionChange(test, NotificationPermission.Granted);

await permissionChangeEventFiredPromise
expect(OneSignal.Notifications.permission).toBe(true);
expect(OneSignal.Notifications.permissionNative).toBe(NotificationPermission.Granted);
});

test('When permission changes to default, we update the permission properties on the Notifications namespace', async () => {
test.stub(PermissionManager.prototype, 'getPermissionStatus', Promise.resolve(NotificationPermission.Default));
const permissionChangeEventFiredPromise = new Promise(resolve => {
OneSignal.emitter.on(OneSignal.EVENTS.NATIVE_PROMPT_PERMISSIONCHANGED, resolve);
});
await PermissionManager.mockNotificationPermissionChange(test, NotificationPermission.Default);

// mimick native permission change
PermissionUtils.triggerNotificationPermissionChanged();

await permissionChangeEventFiredPromise
expect(OneSignal.Notifications.permission).toBe(false);
expect(OneSignal.Notifications.permissionNative).toBe(NotificationPermission.Default);
});

test('When permission changes to denied, we update the permission properties on the Notifications namespace', async () => {
test.stub(PermissionManager.prototype, 'getPermissionStatus', Promise.resolve(NotificationPermission.Denied));
const permissionChangeEventFiredPromise = new Promise(resolve => {
OneSignal.emitter.on(OneSignal.EVENTS.NATIVE_PROMPT_PERMISSIONCHANGED, resolve);
});

// mimick native permission change
PermissionUtils.triggerNotificationPermissionChanged();
await PermissionManager.mockNotificationPermissionChange(test, NotificationPermission.Denied);

await permissionChangeEventFiredPromise
expect(OneSignal.Notifications.permission).toBe(false);
expect(OneSignal.Notifications.permissionNative).toBe(NotificationPermission.Denied);
});

test('When permission changes, removeEventListener should stop callback from firing', async () => {
const callback = (_permission: boolean) => {
throw new Error("Should never be call since removeEventListener should prevent this.");
};
OneSignal.Notifications.addEventListener("permissionChange", callback);
OneSignal.Notifications.removeEventListener("permissionChange", callback);

// Change permissions through all possible states to ensure the event has had a chance to fire
await PermissionManager.mockNotificationPermissionChange(test, NotificationPermission.Granted);
await PermissionManager.mockNotificationPermissionChange(test, NotificationPermission.Default);
await PermissionManager.mockNotificationPermissionChange(test, NotificationPermission.Denied);
});
});
19 changes: 4 additions & 15 deletions __test__/unit/pushSubscription/nativePermissionChange.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import MainHelper from "../../../src/shared/helpers/MainHelper";
import ModelCache from "../../../src/core/caching/ModelCache";
import { OSModel } from '../../../src/core/modelRepo/OSModel'
import PermissionManager from "../../../src/shared/managers/PermissionManager";
import EventHelper from "../../../src/shared/helpers/EventHelper";
import { DUMMY_PUSH_TOKEN } from "../../support/constants";
import { initializeWithPermission } from "../../support/helpers/pushSubscription";
import { PermissionManager } from "../../support/managers/PermissionManager";
import { NotificationPermission } from "../../../src/shared/models/NotificationPermission";

describe('Notification Types are set correctly on subscription change', () => {
beforeEach(async () => {
Expand All @@ -19,39 +20,27 @@ describe('Notification Types are set correctly on subscription change', () => {

test('When native permission is rejected, we update notification_types to -2 & enabled to false', async () => {
await initializeWithPermission('granted');
test.stub(PermissionManager.prototype, 'getNotificationPermission', Promise.resolve('denied'));
const osModelSetSpy = jest.spyOn(OSModel.prototype, 'set');

const permissionChangeEventFiredPromise = new Promise(resolve => {
OneSignal.emitter.on(OneSignal.EVENTS.NATIVE_PROMPT_PERMISSIONCHANGED, resolve);
});

// mimick native permission change
await PermissionManager.mockNotificationPermissionChange(test, NotificationPermission.Denied);
await MainHelper.checkAndTriggerNotificationPermissionChanged();
await EventHelper.checkAndTriggerSubscriptionChanged();

await permissionChangeEventFiredPromise;

// check that the set function was called at least once with the correct value
expect(osModelSetSpy).toHaveBeenCalledWith('notification_types', -2);
expect(osModelSetSpy).toHaveBeenCalledWith('enabled', false);
});

test('When native permission is accepted, we update notification_types to 1 & enabled to true', async () => {
await initializeWithPermission('denied');
test.stub(PermissionManager.prototype, 'getNotificationPermission', Promise.resolve('granted'));
const osModelSetSpy = jest.spyOn(OSModel.prototype, 'set');

const permissionChangeEventFiredPromise = new Promise(resolve => {
OneSignal.emitter.on(OneSignal.EVENTS.NATIVE_PROMPT_PERMISSIONCHANGED, resolve);
});

// mimick native permission change
await PermissionManager.mockNotificationPermissionChange(test, NotificationPermission.Granted);
await MainHelper.checkAndTriggerNotificationPermissionChanged();
await EventHelper.checkAndTriggerSubscriptionChanged();

await permissionChangeEventFiredPromise;

// check that the set function was called at least once with the correct value
expect(osModelSetSpy).toHaveBeenCalledWith('notification_types', 1);
expect(osModelSetSpy).toHaveBeenCalledWith('enabled', true);
Expand Down
4 changes: 2 additions & 2 deletions src/onesignal/NotificationsNamespace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import Database from "../shared/services/Database";
import { awaitOneSignalInitAndSupported, logMethodCall } from "../shared/utils/utils";
import OneSignal from "./OneSignal";
import { EventListenerBase } from "../page/userModel/EventListenerBase";
import NotificationEventName from "../page/models/NotificationEventName";
import { NotificationEventName } from "../page/models/NotificationEventName";
import { NotificationPermission } from "../shared/models/NotificationPermission";
import NotificationEventTypeMap from "../page/models/NotificationEventTypeMap";

Expand All @@ -16,7 +16,7 @@ export default class NotificationsNamespace extends EventListenerBase {

this._permission = _permissionNative === NotificationPermission.Granted;

OneSignal.emitter.on(OneSignal.EVENTS.NATIVE_PROMPT_PERMISSIONCHANGED, (permissionNative: NotificationPermission) => {
OneSignal.emitter.on(OneSignal.EVENTS.NOTIFICATION_PERMISSION_CHANGED_AS_STRING, (permissionNative: NotificationPermission) => {
this._permissionNative = permissionNative;
this._permission = permissionNative === NotificationPermission.Granted;
});
Expand Down
2 changes: 1 addition & 1 deletion src/onesignal/OneSignal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ export default class OneSignal {

OneSignal.__initAlreadyCalled = true;

OneSignal.emitter.on(OneSignal.EVENTS.NATIVE_PROMPT_PERMISSIONCHANGED,
OneSignal.emitter.on(OneSignal.EVENTS.NOTIFICATION_PERMISSION_CHANGED_AS_STRING,
EventHelper.onNotificationPermissionChange);
OneSignal.emitter.on(OneSignal.EVENTS.SUBSCRIPTION_CHANGED, EventHelper._onSubscriptionChanged);
OneSignal.emitter.on(OneSignal.EVENTS.SDK_INITIALIZED, InitHelper.onSdkInitialized);
Expand Down
15 changes: 11 additions & 4 deletions src/onesignal/OneSignalEvents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,18 @@ export const ONESIGNAL_EVENTS = {
*/
CUSTOM_PROMPT_CLICKED: 'customPromptClick',
/**
* Occurs when the user clicks "Allow" or "Block" on the native permission prompt on Chrome, Firefox, or Safari.
* This event is used for both HTTP and HTTPS sites and occurs after the user actually grants notification
* permissions for the site. Occurs before the user is actually subscribed to push notifications.
* Occurs immediately when the notification permission changes for the domain at the browser level.
* This normally happens when the user clicks "Allow" or "Block" on the native permission prompt
* on Chrome, Firefox, etc, however it also changes if the end-user clicks on the lock icon and
* manually changes it.
* Occurs BEFORE the actual push subscription is created on on the backend.
*/
NATIVE_PROMPT_PERMISSIONCHANGED: 'permissionChange',
NOTIFICATION_PERMISSION_CHANGED_AS_STRING: 'permissionChangeAsString',
/**
* Same as NOTIFICATION_PERMISSION_CHANGED_AS_STRING, expect a boolean and will be used to fire
* events to the public API OneSignal.Notification.addEventListener("permissionChange", function....)
*/
NOTIFICATION_PERMISSION_CHANGED_AS_BOOLEAN: 'permissionChange',
/**
* Occurs after the user is officially subscribed to push notifications. The service worker is fully registered
* and activated and the user is eligible to receive push notifications at any point after this.
Expand Down
2 changes: 1 addition & 1 deletion src/onesignal/PushSubscriptionNamespace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export default class PushSubscriptionNamespace extends EventListenerBase {
this._token = change?.current.token;
});

OneSignal.emitter.on(OneSignal.EVENTS.NATIVE_PROMPT_PERMISSIONCHANGED, async (permission: NotificationPermission) => {
OneSignal.emitter.on(OneSignal.EVENTS.NOTIFICATION_PERMISSION_CHANGED_AS_STRING, async (permission: NotificationPermission) => {
this._permission = permission;
});
}
Expand Down
2 changes: 1 addition & 1 deletion src/page/bell/Bell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,7 @@ export default class Bell {
}
});

OneSignal.emitter.on(OneSignal.EVENTS.NATIVE_PROMPT_PERMISSIONCHANGED, () => {
OneSignal.emitter.on(OneSignal.EVENTS.NOTIFICATION_PERMISSION_CHANGED_AS_STRING, () => {
this.updateState();
});

Expand Down
15 changes: 6 additions & 9 deletions src/page/models/NotificationEventName.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
enum NotificationEventName {
Click = "click",
ForegroundWillDisplay = "foregroundWillDisplay",
Dismiss = "dismiss",
PermissionChange = "permissionChange",
PermissionPromptDisplay = "permissionPromptDisplay"
}

export default NotificationEventName;
export type NotificationEventName =
"click" |
"foregroundWillDisplay" |
"dismiss" |
"permissionChange" |
"permissionPromptDisplay"
30 changes: 23 additions & 7 deletions src/shared/utils/PermissionUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,37 +3,53 @@ import OneSignalEvent from '../services/OneSignalEvent';

export class PermissionUtils {

// This flag prevents firing the NATIVE_PROMPT_PERMISSIONCHANGED event twice
// This flag prevents firing the NOTIFICATION_PERMISSION_CHANGED_AS_STRING event twice
// We use multiple APIs:
// 1. Notification.requestPermission callback
// 2. navigator.permissions.query({ name: 'notifications' }`).onchange
// Some browsers support both, while others only support Notification.requestPermission
private static executing = false;

public static async triggerNotificationPermissionChanged(updateIfIdentical = false) {
public static async triggerNotificationPermissionChanged(force = false) {
if (PermissionUtils.executing) {
return;
}

PermissionUtils.executing = true;
try {
await PermissionUtils.privateTriggerNotificationPermissionChanged(updateIfIdentical);
await PermissionUtils.privateTriggerNotificationPermissionChanged(force);
}
finally {
PermissionUtils.executing = false;
}
}

private static async privateTriggerNotificationPermissionChanged(updateIfIdentical: boolean) {
private static async privateTriggerNotificationPermissionChanged(force: boolean) {
const newPermission: NotificationPermission = await OneSignal.context.permissionManager.getPermissionStatus();
const previousPermission: NotificationPermission = await Database.get('Options', 'notificationPermission');

const shouldBeUpdated = newPermission !== previousPermission || updateIfIdentical;
if (!shouldBeUpdated) {
const triggerEvent = newPermission !== previousPermission || force;
if (!triggerEvent) {
return;
}

await Database.put('Options', { key: 'notificationPermission', value: newPermission });
OneSignalEvent.trigger(OneSignal.EVENTS.NATIVE_PROMPT_PERMISSIONCHANGED, newPermission);

OneSignalEvent.trigger(OneSignal.EVENTS.NOTIFICATION_PERMISSION_CHANGED_AS_STRING, newPermission);
this.triggerBooleanPermissionChangeEvent(previousPermission, newPermission, force);
}

private static triggerBooleanPermissionChangeEvent(
previousPermission: NotificationPermission,
newPermission: NotificationPermission,
force: boolean,
): void {
const newPermissionBoolean = newPermission === 'granted';
const previousPermissionBoolean = previousPermission === 'granted';
const triggerEvent = newPermissionBoolean !== previousPermissionBoolean || force;
if (!triggerEvent) {
return;
}
OneSignalEvent.trigger(OneSignal.EVENTS.NOTIFICATION_PERMISSION_CHANGED_AS_BOOLEAN, newPermissionBoolean);
}
}

0 comments on commit efa51db

Please sign in to comment.