Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add SwPush notification usage to api doc #32139

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
62 changes: 62 additions & 0 deletions packages/examples/service-worker/push/BUILD.bazel
@@ -0,0 +1,62 @@
package(default_visibility = ["//visibility:public"])

load("//tools:defaults.bzl", "ng_module", "ts_library")
load("@npm_bazel_protractor//:index.bzl", "protractor_web_test_suite")
load("@npm_bazel_typescript//:index.bzl", "ts_devserver")

ng_module(
name = "sw_push_examples",
srcs = glob(
["**/*.ts"],
exclude = ["**/*_spec.ts"],
),
# TODO: FW-1004 Type checking is currently not complete.
type_check = False,
deps = [
"//packages/core",
"//packages/platform-browser",
"//packages/platform-browser-dynamic",
"//packages/service-worker",
],
)

ts_library(
name = "sw_push_e2e_tests_lib",
testonly = True,
srcs = glob(["**/e2e_test/*_spec.ts"]),
tsconfig = "//packages/examples:tsconfig-e2e.json",
deps = [
"//packages/examples/test-utils",
"//packages/private/testing",
"@npm//@types/jasminewd2",
"@npm//protractor",
],
)

ts_devserver(
name = "devserver",
entry_module = "@angular/examples/service-worker/push/main",
index_html = "//packages/examples:index.html",
port = 4200,
scripts = [
"//tools/rxjs:rxjs_umd_modules",
"@npm//:node_modules/tslib/tslib.js",
],
static_files = [
"ngsw-worker.js",
"@npm//:node_modules/zone.js/dist/zone.js",
],
deps = [":sw_push_examples"],
)

protractor_web_test_suite(
name = "protractor_tests",
data = ["//packages/bazel/src/protractor/utils"],
on_prepare = "start-server.js",
server = ":devserver",
deps = [
":sw_push_e2e_tests_lib",
"@npm//protractor",
"@npm//selenium-webdriver",
],
)
22 changes: 22 additions & 0 deletions packages/examples/service-worker/push/e2e_test/push_spec.ts
@@ -0,0 +1,22 @@
/**
* @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 {browser, by, element} from 'protractor';
import {verifyNoBrowserErrors} from '../../../test-utils';

describe('SW `SwPush` example', () => {
const pageUrl = '/push';
const appElem = element(by.css('example-app'));

afterEach(verifyNoBrowserErrors);

it('should be enabled', () => {
browser.get(pageUrl);
expect(appElem.getText()).toBe('SW enabled: true');
});
});
12 changes: 12 additions & 0 deletions packages/examples/service-worker/push/main.ts
@@ -0,0 +1,12 @@
/**
* @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 {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
import {AppModuleNgFactory} from './module.ngfactory';

platformBrowserDynamic().bootstrapModuleFactory(AppModuleNgFactory);
66 changes: 66 additions & 0 deletions packages/examples/service-worker/push/module.ts
@@ -0,0 +1,66 @@
/**
* @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
*/
// tslint:disable: no-duplicate-imports
import {Component, NgModule} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
import {ServiceWorkerModule} from '@angular/service-worker';
// #docregion inject-sw-push
import {SwPush} from '@angular/service-worker';
// #enddocregion inject-sw-push
// tslint:enable: no-duplicate-imports

const PUBLIC_VAPID_KEY_OF_SERVER = '...';

@Component({
selector: 'example-app',
template: 'SW enabled: {{ swPush.isEnabled }}',
})
// #docregion inject-sw-push
export class AppComponent {
constructor(readonly swPush: SwPush) {}
// #enddocregion inject-sw-push

// #docregion subscribe-to-push
private async subscribeToPush() {
try {
const sub = await this.swPush.requestSubscription({
serverPublicKey: PUBLIC_VAPID_KEY_OF_SERVER,
});
// TODO: Send to server.
} catch (err) {
console.error('Could not subscribe due to:', err);
}
}
// #enddocregion subscribe-to-push

private subscribeToNotificationClicks() {
// #docregion subscribe-to-notification-clicks
this.swPush.notificationClicks.subscribe(
({action, notification}) => {
// TODO: Do something in response to notification click.
});
// #enddocregion subscribe-to-notification-clicks
}
// #docregion inject-sw-push
}
// #enddocregion inject-sw-push

@NgModule({
bootstrap: [
AppComponent,
],
declarations: [
AppComponent,
],
imports: [
BrowserModule,
ServiceWorkerModule.register('ngsw-worker.js'),
],
})
export class AppModule {
}
14 changes: 14 additions & 0 deletions packages/examples/service-worker/push/ngsw-worker.js
@@ -0,0 +1,14 @@
/**
* @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
*/

// Mock `ngsw-worker.js` used for testing the examples.
// Immediately takes over and unregisters itself.
self.addEventListener('install', evt => evt.waitUntil(self.skipWaiting()));
self.addEventListener(
'activate',
evt => evt.waitUntil(self.clients.claim().then(() => self.registration.unregister())));
17 changes: 17 additions & 0 deletions packages/examples/service-worker/push/start-server.js
@@ -0,0 +1,17 @@
/**
* @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
*/

const protractorUtils = require('@bazel/protractor/protractor-utils');
const protractor = require('protractor');

module.exports = async function(config) {
const {port} = await protractorUtils.runServer(config.workspace, config.server, '-port', []);
const serverUrl = `http://localhost:${port}`;

protractor.browser.baseUrl = serverUrl;
};
87 changes: 83 additions & 4 deletions packages/service-worker/src/push.ts
Expand Up @@ -14,7 +14,73 @@ import {ERR_SW_NOT_SUPPORTED, NgswCommChannel, PushEvent} from './low_level';


/**
* Subscribe and listen to push notifications from the Service Worker.
* 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.
*
jbogarthyde marked this conversation as resolved.
Show resolved Hide resolved
* 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/)
jbogarthyde marked this conversation as resolved.
Show resolved Hide resolved
* @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
*/
Expand All @@ -27,10 +93,10 @@ export class SwPush {

/**
* 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 will be an empty string `''`.
* interacted with. If no action was used the `action` property contains an empty string `''`.
*
* Note that the `notification` property is **not** a [Notification][Mozilla Notification] object
* but rather a
* 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.
*
Expand Down Expand Up @@ -78,6 +144,13 @@ export class SwPush {
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));
Expand All @@ -98,6 +171,12 @@ export class SwPush {
});
}

/**
* 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.
*/
jbogarthyde marked this conversation as resolved.
Show resolved Hide resolved
unsubscribe(): Promise<void> {
if (!this.sw.isEnabled) {
return Promise.reject(new Error(ERR_SW_NOT_SUPPORTED));
Expand Down