onesignal-web-sdk
Version:
Web push notifications from OneSignal.
217 lines (198 loc) • 9.23 kB
text/typescript
import OneSignalUtils from '../utils/OneSignalUtils';
import bowser from 'bowser';
import { InvalidArgumentError, InvalidArgumentReason } from '../errors/InvalidArgumentError';
import Database from '../services/Database';
import { NotificationPermission } from '../models/NotificationPermission';
import SdkEnvironment from '../managers/SdkEnvironment';
/**
* A permission manager to consolidate the different quirks of obtaining and evaluating permissions
* across Safari, Chrome, and Firefox.
*/
export default class PermissionManager {
static get STORED_PERMISSION_KEY() {
return 'storedNotificationPermission';
}
/**
* Returns an interpreted version of the browser's notification permission.
*
* On some environments, it isn't possible to obtain the actual notification
* permission. For example, starting with Chrome 62+, cross-origin iframes and
* insecure origins can no longer accurately detect the default notification
* permission state.
*
* For cross-origin iframes, returned permissions are correct except that
* "denied" is returned instead of "default".
*
* For insecure origins, returned permissions are always "denied". This
* differs from cross-origin iframes where the cross-origin iframes are
* acurrate if returning "granted", but insecure origins will always return
* "denied" regardless of the actual permission.
*
* This method therefore returns the notification permission best suited for
* our SDK, and it may not always be accurate. On most environments (i.e. not
* Chrome 62+), the returned permission will be accurate.
*
* @param safariWebId The Safari web ID necessary to access the permission
* state on Safari.
*/
public async getNotificationPermission(safariWebId?: string): Promise<NotificationPermission> {
const reportedPermission = await this.getReportedNotificationPermission(safariWebId);
if (await this.isPermissionEnvironmentAmbiguous(reportedPermission))
return await this.getInterpretedAmbiguousPermission(reportedPermission);
return reportedPermission;
}
/**
* Returns the browser's actual notification permission as reported without any modifications.
*
* One challenge is determining the frame context our permission query needs to run in:
*
* - For a regular top-level HTTPS site, query our current top-level frame
*
* - For a custom web push setup in a child HTTPS iframe, query our current child iframe (even
* though the returned permission is ambiguous on Chrome 62+ if our origin is different from
* that of the top-level frame)
*
* - For a regular HTTP site, query OneSignal's child subdomain.os.tc or subdomain.onesignal.com
* iframe
*
* - For a regular HTTP site embedded in a child iframe, still query the nested child's
* OneSignal subdomain.os.tc or subdomain.onesignal.com iframe
*
* This simplifies into determining whether the web push setup is using OneSignal's subdomain. If
* not, we assume the current frame context, regardless of whether it is a child or top-level
* frame, is the current context to run the permission query in.
*
* @param safariWebId The Safari web ID necessary to access the permission state on Safari.
*/
public async getReportedNotificationPermission(safariWebId?: string): Promise<NotificationPermission>{
if (bowser.safari)
return PermissionManager.getSafariNotificationPermission(safariWebId);
// Is this web push setup using subdomain.os.tc or subdomain.onesignal.com?
if (OneSignalUtils.isUsingSubscriptionWorkaround())
return await this.getOneSignalSubdomainNotificationPermission(safariWebId);
else
return this.getW3cNotificationPermission();
}
/**
* Returns the Safari browser's notification permission as reported by the browser.
*
* @param safariWebId The Safari web ID necessary to access the permission state on Safari.
*/
private static getSafariNotificationPermission(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 for non-Safari browsers. This
* includes Chrome, Firefox, Opera, Yandex, and every browser following the Notification API
* standard.
*/
private getW3cNotificationPermission(): NotificationPermission {
return window.Notification.permission as NotificationPermission;
}
/**
* Returns the notification permission as reported by the browser for the OneSignal subdomain
* iframe.
*
* @param safariWebId The Safari web ID necessary to access the permission state on Safari.
*/
public async getOneSignalSubdomainNotificationPermission(safariWebId?: string): Promise<NotificationPermission> {
return new Promise<NotificationPermission>(resolve => {
OneSignal.proxyFrameHost.message(
OneSignal.POSTMAM_COMMANDS.REMOTE_NOTIFICATION_PERMISSION,
{ safariWebId: safariWebId },
(reply: any) => {
let remoteNotificationPermission = reply.data;
resolve(remoteNotificationPermission);
}
);
});
}
/**
* To interpret the browser's reported notification permission, we need to know whether we're in
* an environment where the returned permission should be treated ambiguously.
*
* The reported permission should only be treated ambiguously if:
*
* - We're not on Safari or Firefox (Chromium, Chrome, Opera, and Yandex will all eventually
* share the same Chrome 62+ codebase)
*
* - And the reported permission is "denied"
*
* - And the current frame context is either a cross-origin iframe or insecure
*/
public async isPermissionEnvironmentAmbiguous(permission: NotificationPermission): Promise<boolean> {
// For testing purposes, allows changing the browser user agent
const browser = OneSignalUtils.redetectBrowserUserAgent();
return (!browser.safari &&
!browser.firefox &&
permission === NotificationPermission.Denied &&
(
this.isCurrentFrameContextCrossOrigin() ||
await SdkEnvironment.isFrameContextInsecure() ||
OneSignalUtils.isUsingSubscriptionWorkaround() ||
SdkEnvironment.isInsecureOrigin()
)
);
}
/**
* Returns true if we're a cross-origin iframe.
*
* This means:
*
* - We're not the top-level frame
* - We're unable to access to the top-level frame's origin, or we can access the origin but it
* is different. On most browsers, accessing the top-level origin should throw an exception.
*/
public isCurrentFrameContextCrossOrigin(): boolean {
let topFrameOrigin: string;
try {
// Accessing a cross-origin top-level frame's origin should throw an error
topFrameOrigin = window.top.location.origin;
} catch (e) {
// We're in a cross-origin child iframe
return true;
}
return window.top !== window &&
topFrameOrigin !== window.location.origin;
}
/**
* To workaround Chrome 62+'s permission ambiguity for "denied" permissions,
* we assume the permission is "default" until we actually record the
* permission being "denied" or "granted".
*
* This allows our best-effort approach to subscribe new users, and upon
* subscribing, if we discover the actual permission to be denied, we record
* this for next time.
*
* @param reportedPermission The notification permission as reported by the
* browser without any modifications.
*/
public async getInterpretedAmbiguousPermission(reportedPermission: NotificationPermission) {
switch (reportedPermission) {
case NotificationPermission.Denied:
const storedPermission = await this.getStoredPermission();
if (storedPermission) {
// If we've recorded the last known actual browser permission, return that
return storedPermission;
} else {
// If we don't have any stored permission, assume default
return NotificationPermission.Default;
}
default:
return reportedPermission;
}
}
public async getStoredPermission(): Promise<NotificationPermission> {
return await Database.get<NotificationPermission>('Options', PermissionManager.STORED_PERMISSION_KEY);
}
public async setStoredPermission(permission: NotificationPermission) {
await Database.put('Options', { key: PermissionManager.STORED_PERMISSION_KEY, value: permission });
}
public async updateStoredPermission() {
// TODO verify if `OneSignal.config.safariWebId` should be passed as a parameter
const permission = await this.getNotificationPermission();
return await this.setStoredPermission(permission);
}
}