204 lines
7.3 KiB
TypeScript
204 lines
7.3 KiB
TypeScript
/**
|
|
* @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.
|
|
*
|
|
* <code-example path="service-worker/push/module.ts" region="inject-sw-push" header="app.component.ts"></code-example>
|
|
*
|
|
* 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.
|
|
*
|
|
* <code-example path="service-worker/push/module.ts" region="subscribe-to-push" header="app.component.ts"></code-example>
|
|
*
|
|
* 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:
|
|
*
|
|
* <code-example path="service-worker/push/module.ts" region="subscribe-to-notification-clicks" header="app.component.ts"></code-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<object>;
|
|
|
|
/**
|
|
* 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<PushSubscription|null>;
|
|
|
|
/**
|
|
* 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<PushManager>;
|
|
private subscriptionChanges = new Subject<PushSubscription|null>();
|
|
|
|
constructor(private sw: NgswCommChannel) {
|
|
if (!sw.isEnabled) {
|
|
this.messages = NEVER;
|
|
this.notificationClicks = NEVER;
|
|
this.subscription = NEVER;
|
|
return;
|
|
}
|
|
|
|
this.messages = this.sw.eventsOfType<PushEvent>('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<PushSubscription> {
|
|
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<void> {
|
|
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); }
|
|
}
|