Home Reference Source

js/models/PushNotifications.js

import Data, { EnvKey } from '~/models/Data';
import ErrorManager, { AnyError } from '~/helpers/ErrorManager';
import WebPushNewDevice from '~/models/Request/WebPushNewDevice';
import WebAPNToken from '~/models/Request/WebAPNToken';
import WebPushKey from '~/models/Request/WebPushKey';
import ServiceWorker from '~/helpers/ServiceWorker';

export const PNAPNUnknownResponseState = Symbol('PN.APN.Error.UnknownResponseState');

/**
 * Manages push notifications across different platforms.
 */
export default class PushNotification {
    /**
     * The main PN service
     * @type {PushNotification}
     */
    static shared = new PushNotification({
        webAPNId: Data.shared.envValueForKey(EnvKey.webAPNId) || null
    });

    /**
     * Creates PN but you probably want to use {@link PushNotification.shared}
     * @param {Object} [options={}]
     * @param {?string} options.webAPNId - The web APN ID or null if not supported.
     */
    constructor({ webAPNId = null } = {}) {
        /**
         * Apple Web Push Notification ID
         * @readonly
         * @type {?string}
         */
        this.webAPNId = webAPNId;
    }

    /**
     * Checks if PNs are supported
     * @type {boolean}
     */
    get supportsPNs() {
        // Chrome implementation is buggy, disabled for now.
        return this.usePush || this.useAPN;
    }

    /**
     * If to use Push flow
     * @type {string}
     */
    get usePush() { return !!('PushManager' in window); }

    /**
     * If to use APN flow
     * @type {boolean}
     */
    get useAPN() { return !!global.safari?.pushNotification }

    /**
     * If Push is setup. We will always assume true
     * @type {boolean}
     */
    get backendSupportsPush() { return true; }

    /**
     * If APN is setup
     * @type {boolean}
     */
    get backendSupportsAPN() { return this.webAPNId !== null; }

    /**
     * The APN URL. `null` if we don't support
     * @type {?string}
     */
    get apnURL() {
        if (!this.backendSupportsAPN) return null;
        return `${Data.shared.envValueForKey(EnvKey.host)}/webapn`
    }

    /**
     * If has permission to send notifs
     * @type {boolean}
     */
    get hasPermissions() {
        if (this.useAPN) {
            return global.safari.pushNotification.permission(this.webAPNId).permission === "granted";
        } else if (this.usePush) {
            return Notification.permission === 'granted';
        } else {
            return false;
        }
    }

    /**
     * If we should show a request
     */
    get shouldShowRequest() {
        return this.needsRequest && !this.forbiddenRequest
    }

    /**
     * Gets if the user has expressibly forbidden
     * @type {boolean}
     */
    get forbiddenRequest() { return localStorage.getItem('axtell-pn-forbidden') === 'true'; }

    /**
     * Sets if the user has expressibly forbidden
     * @type {boolean}
     */
    set forbiddenRequest(isForbidden) { localStorage.setItem('axtell-pn-forbidden', String(!!isForbidden)); }

    /**
     * If should ask for permission
     * @type {boolean}
     */
    get needsRequest() {
        if (this.useAPN) {
            return global.safari.pushNotification.permission(this.webAPNId).permission === "default";
        } else if (this.usePush) {
            return Notification.permission === 'default';
        } else {
            return false;
        }
    }

    /**
     * If we have been denied perms
     * @type {boolean}
     */
    get denied() {
        // If we don't have perms and can't ask then yeah...
        if (this.useAPN) {
            return global.safari.pushNotification.permission(this.webAPNId).permission === "denied";
        } else if (this.usePush) {
            return Notification.permission === 'denied';
        } else {
            return false;
        }
    }

    /**
     * Requests permission for *desktop* push notifications. If there is already
     * perms this will request again.
     *
     * @async
     * @return {boolean} If priviledge was obtained or not
     */
    requestPriviledge() {
        return new Promise(async (resolve, reject) => {
            // If we don't have permission then ¯\_(ツ)_/¯
            if (this.denied) { return resolve(false) }

            if (this.useAPN) {
                if (!this.backendSupportsAPN) {
                    alert("Axtell instance is not configured for APN");
                    return resolve(false);
                }

                const authorizationToken = await new WebAPNToken().run();
                safari.pushNotification.requestPermission(
                    this.apnURL,
                    this.webAPNId,
                    {
                        token: authorizationToken
                    },
                    ({ deviceToken, permission }) => {
                        if (this.hasPermissions) { resolve(true); }
                        else { resolve(false) };
                    }
                )
            } else if (this.usePush) {
                const key = await new WebPushKey().run();
                const serviceWorker = await ServiceWorker.global();
                const registration = serviceWorker.registration;

                // Request permission
                const permission = await new Promise((resolve, reject) => {
                    const promise = Notification.requestPermission(resolve);
                    if (promise) promise.then(resolve, reject);
                });

                if (!this.hasPermissions) resolve(false);

                const pushSubscription = await registration.pushManager.subscribe({
                    userVisibleOnly: true,
                    applicationServerKey: key
                });

                // Submit to server
                const deviceId = await new WebPushNewDevice(pushSubscription).run();

                resolve(true);
            } else {
                resolve(false);
            }
        });
    }
}