
Previously, the `SwPush` API docs were using hard-coded code snippets. This commit switches to using code snippets from an actual example app, which ensures that the code shown in the docs will at least continue to compile successfully. PR Close #32139
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); }
|
|
}
|