/** * @license * Copyright Google Inc. All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ import {Injectable} from '@angular/core'; import {NEVER, Observable, Subject, merge} from 'rxjs'; import {map, switchMap, take} from 'rxjs/operators'; import {ERR_SW_NOT_SUPPORTED, NgswCommChannel, PushEvent} from './low_level'; /** * Subscribe and listen to * [Web Push Notifications](https://developer.mozilla.org/en-US/docs/Web/API/Push_API/Best_Practices) * through Angular Service Worker. * * @usageNotes * * You can inject a `SwPush` instance into any component or service * as a dependency. * * * * To subscribe, call `SwPush.requestSubscription()`, which asks the user for permission. * The call returns a `Promise` with a new * [`PushSubscription`](https://developer.mozilla.org/en-US/docs/Web/API/PushSubscription) * instance. * * * * A request is rejected if the user denies permission, or if the browser * blocks or does not support the Push API or ServiceWorkers. * Check `SwPush.isEnabled` to confirm status. * * Invoke Push Notifications by pushing a message with the following payload. * * ```ts * { * "notification": { * "actions": NotificationAction[], * "badge": USVString * "body": DOMString, * "data": any, * "dir": "auto"|"ltr"|"rtl", * "icon": USVString, * "image": USVString, * "lang": DOMString, * "renotify": boolean, * "requireInteraction": boolean, * "silent": boolean, * "tag": DOMString, * "timestamp": DOMTimeStamp, * "title": DOMString, * "vibrate": number[] * } * } * ``` * * Only `title` is required. See `Notification` * [instance properties](https://developer.mozilla.org/en-US/docs/Web/API/Notification#Instance_properties). * * While the subscription is active, Service Worker listens for * [PushEvent](https://developer.mozilla.org/en-US/docs/Web/API/PushEvent) * occurrences and creates * [Notification](https://developer.mozilla.org/en-US/docs/Web/API/Notification) * instances in response. * * Unsubscribe using `SwPush.unsubscribe()`. * * An application can subscribe to `SwPush.notificationClicks` observable to be notified when a user * clicks on a notification. For example: * * * * @see [Push Notifications](https://developers.google.com/web/fundamentals/codelabs/push-notifications/) * @see [Angular Push Notifications](https://blog.angular-university.io/angular-push-notifications/) * @see [MDN: Push API](https://developer.mozilla.org/en-US/docs/Web/API/Push_API) * @see [MDN: Notifications API](https://developer.mozilla.org/en-US/docs/Web/API/Notifications_API) * @see [MDN: Web Push API Notifications best practices](https://developer.mozilla.org/en-US/docs/Web/API/Push_API/Best_Practices) * * @publicApi */ @Injectable() export class SwPush { /** * Emits the payloads of the received push notification messages. */ readonly messages: Observable; /** * Emits the payloads of the received push notification messages as well as the action the user * interacted with. If no action was used the `action` property contains an empty string `''`. * * Note that the `notification` property does **not** contain a * [Notification][Mozilla Notification] object but rather a * [NotificationOptions](https://notifications.spec.whatwg.org/#dictdef-notificationoptions) * object that also includes the `title` of the [Notification][Mozilla Notification] object. * * [Mozilla Notification]: https://developer.mozilla.org/en-US/docs/Web/API/Notification */ readonly notificationClicks: Observable < { action: string; notification: NotificationOptions&{ title: string } } > ; /** * Emits the currently active * [PushSubscription](https://developer.mozilla.org/en-US/docs/Web/API/PushSubscription) * associated to the Service Worker registration or `null` if there is no subscription. */ readonly subscription: Observable; /** * True if the Service Worker is enabled (supported by the browser and enabled via * `ServiceWorkerModule`). */ get isEnabled(): boolean { return this.sw.isEnabled; } // TODO(issue/24571): remove '!'. private pushManager !: Observable; private subscriptionChanges = new Subject(); constructor(private sw: NgswCommChannel) { if (!sw.isEnabled) { this.messages = NEVER; this.notificationClicks = NEVER; this.subscription = NEVER; return; } this.messages = this.sw.eventsOfType('PUSH').pipe(map(message => message.data)); this.notificationClicks = this.sw.eventsOfType('NOTIFICATION_CLICK').pipe(map((message: any) => message.data)); this.pushManager = this.sw.registration.pipe(map(registration => registration.pushManager)); const workerDrivenSubscriptions = this.pushManager.pipe(switchMap(pm => pm.getSubscription())); this.subscription = merge(workerDrivenSubscriptions, this.subscriptionChanges); } /** * Subscribes to Web Push Notifications, * after requesting and receiving user permission. * * @param options An object containing the `serverPublicKey` string. * @returns A Promise that resolves to the new subscription object. */ requestSubscription(options: {serverPublicKey: string}): Promise { if (!this.sw.isEnabled) { return Promise.reject(new Error(ERR_SW_NOT_SUPPORTED)); } const pushOptions: PushSubscriptionOptionsInit = {userVisibleOnly: true}; let key = this.decodeBase64(options.serverPublicKey.replace(/_/g, '/').replace(/-/g, '+')); let applicationServerKey = new Uint8Array(new ArrayBuffer(key.length)); for (let i = 0; i < key.length; i++) { applicationServerKey[i] = key.charCodeAt(i); } pushOptions.applicationServerKey = applicationServerKey; return this.pushManager.pipe(switchMap(pm => pm.subscribe(pushOptions)), take(1)) .toPromise() .then(sub => { this.subscriptionChanges.next(sub); return sub; }); } /** * Unsubscribes from Service Worker push notifications. * * @returns A Promise that is resolved when the operation succeeds, or is rejected if there is no * active subscription or the unsubscribe operation fails. */ unsubscribe(): Promise { if (!this.sw.isEnabled) { return Promise.reject(new Error(ERR_SW_NOT_SUPPORTED)); } const doUnsubscribe = (sub: PushSubscription | null) => { if (sub === null) { throw new Error('Not subscribed to push notifications.'); } return sub.unsubscribe().then(success => { if (!success) { throw new Error('Unsubscribe failed!'); } this.subscriptionChanges.next(null); }); }; return this.subscription.pipe(take(1), switchMap(doUnsubscribe)).toPromise(); } private decodeBase64(input: string): string { return atob(input); } }