UNPKG

onesignal-web-sdk

Version:

Web push notifications from OneSignal.

209 lines (183 loc) 7.53 kB
import Environment from '../../Environment'; import Event from '../../Event'; import { MessengerMessageEvent } from '../../models/MessengerMessageEvent'; import Postmam from '../../Postmam'; import { timeoutPromise, triggerNotificationPermissionChanged } from '../../utils'; import { ServiceWorkerActiveState } from "../../helpers/ServiceWorkerHelper"; import Log from '../../libraries/Log'; /** * Manager for an instance of the OneSignal proxy frame, for use from the main * page (not the iFrame itself). * * This is loaded as subdomain.onesignal.com/webPushIFrame or * subdomain.os.tc/webPushIFrame. * */ export default class ProxyFrameHost implements Disposable { public url: URL; private element: HTMLIFrameElement; private messenger: Postmam; // Promise to track whether the frame has finished loading private loadPromise: { promise: Promise<void>, resolver: Function, rejector: Function } /** * How long to wait to load the proxy frame before timing out. */ static get LOAD_TIMEOUT_MS() { return 15000; } /** * * @param origin The URL object describing the origin to load. */ constructor(origin: URL) { this.url = origin; this.url.pathname = 'webPushIframe'; } /** * Creates and loads an iFrame on the DOM, replacing any existing iFrame of * the same URL. * * Rejects with a TimeoutError if the frame doesn't load within a specified time. */ async load(): Promise<void> { /* This class removes existing iFrames with the same URL. This prevents multiple iFrames to the same origin, which can cause issues with cross-origin messaging. */ Log.debug('Opening an iFrame to', this.url.toString()); this.removeFrame(); const iframe = document.createElement("iframe"); iframe.style.display = "none"; iframe.src = this.url.toString(); (iframe as any).sandbox = 'allow-popups allow-popups-to-escape-sandbox allow-same-origin allow-scripts allow-top-navigation'; (this as any).loadPromise = {}; (this as any).loadPromise.promise = new Promise((resolve, reject) => { this.loadPromise.resolver = resolve; this.loadPromise.rejector = reject; }); document.body.appendChild(iframe); iframe.onload = this.onFrameLoad.bind(this); this.element = iframe; // Display a timeout warning if frame doesn't load in time, but don't prevent it from loading if the network is just slow timeoutPromise(this.loadPromise.promise, ProxyFrameHost.LOAD_TIMEOUT_MS).catch(() => { if (window === window.top) { Log.warn(`OneSignal: Loading the required iFrame ${this.url.toString()} timed out. Check that the Site URL onesignal.com dashboard web config is ${location.origin}. Only the Site URL specified there is allowed to use load the iFrame.`); } }); return this.loadPromise.promise; } removeFrame() { if (!Environment.isBrowser()) return; const existingInstance = document.querySelector(`iframe[src='${this.url.toString()}']`); if (existingInstance) existingInstance.remove(); } onFrameLoad(_: UIEvent): void { this.establishCrossOriginMessaging(); } establishCrossOriginMessaging() { if (this.messenger) { // Remove all previous events; window message events should not go to any previous listeners this.messenger.destroy(); } this.messenger = new Postmam(this.element.contentWindow, this.url.toString(), this.url.toString()); this.messenger.on(OneSignal.POSTMAM_COMMANDS.CONNECTED, this.onMessengerConnect.bind(this)); this.messenger.on(OneSignal.POSTMAM_COMMANDS.REMOTE_RETRIGGER_EVENT, this.onRemoteRetriggerEvent.bind(this)); this.messenger.on(OneSignal.POSTMAM_COMMANDS.REMOTE_NOTIFICATION_PERMISSION_CHANGED, this.onRemoteNotificationPermissionChanged.bind(this)); this.messenger.on(OneSignal.POSTMAM_COMMANDS.REQUEST_HOST_URL, this.onRequestHostUrl.bind(this)); this.messenger.on(OneSignal.POSTMAM_COMMANDS.SERVICEWORKER_COMMAND_REDIRECT, this.onServiceWorkerCommandRedirect.bind(this)); this.messenger.on(OneSignal.POSTMAM_COMMANDS.GET_EVENT_LISTENER_COUNT, this.onGetEventListenerCount.bind(this)); this.messenger.connect(); } dispose() { // Removes all events if (this.messenger) { this.messenger.destroy(); } this.removeFrame(); } async onMessengerConnect(_: MessengerMessageEvent) { Log.debug(`Successfully established cross-origin communication for iFrame at ${this.url.toString()}`); this.messenger.message(OneSignal.POSTMAM_COMMANDS.IFRAME_POPUP_INITIALIZE, { hostInitOptions: JSON.parse(JSON.stringify(OneSignal.config)), // Removes functions and unmessageable objects pageUrl: window.location.href, pageTitle: document.title, }, reply => { if (reply.data === OneSignal.POSTMAM_COMMANDS.REMOTE_OPERATION_COMPLETE) { this.loadPromise.resolver(); // This needs to be initialized so that isSubscribed() can be called to // determine whether the user is subscribed to Frame A or B //Event.trigger(OneSignal.EVENTS.SDK_INITIALIZED); } return false; }); } onRemoteRetriggerEvent(message: MessengerMessageEvent) { // e.g. { eventName: 'subscriptionChange', eventData: true} let {eventName, eventData} = (message.data as any); Event.trigger(eventName, eventData, message.source); return false; } onRemoteNotificationPermissionChanged(message: MessengerMessageEvent) { let {forceUpdatePermission} = (message.data as any); triggerNotificationPermissionChanged(forceUpdatePermission); return false; } onRequestHostUrl(message: MessengerMessageEvent) { message.reply(location.href); return false; } onServiceWorkerCommandRedirect(message: MessengerMessageEvent) { const url = (message.data as any); if (url && url.startsWith("http")) { window.location.href = url; } return false; } onGetEventListenerCount(message: MessengerMessageEvent) { const eventName: string = message.data; Log.debug('(Reposted from iFrame -> Host) Getting event listener count for ', eventName); message.reply(OneSignal.emitter.numberOfListeners(eventName)); return false; } isSubscribed(): Promise<boolean> { return new Promise(resolve => { this.messenger.message(OneSignal.POSTMAM_COMMANDS.IS_SUBSCRIBED, null, reply => { resolve(reply.data); }); }); } unsubscribeFromPush(): Promise<void> { return new Promise<void>(resolve => { this.messenger.message(OneSignal.POSTMAM_COMMANDS.UNSUBSCRIBE_PROXY_FRAME, null, _ => { resolve(); }); }); } getProxyServiceWorkerActiveState() { return new Promise<ServiceWorkerActiveState>((resolve, reject) => { this.message(OneSignal.POSTMAM_COMMANDS.SERVICE_WORKER_STATE, null, reply => { resolve(reply.data); }); }); } async runCommand<T>(command: string): Promise<T> { const result = await new Promise<T>((resolve, reject) => { this.message(command, null, reply => { resolve(reply.data); }); }); return result; } /** * Shortcut method to messenger.message(). */ message(..._) { this.messenger.message.apply(this.messenger, arguments); } }