From 48fb523a2e75d3cfd9f695c29710d0bca8900e85 Mon Sep 17 00:00:00 2001 From: Judy Bogart Date: Wed, 14 Aug 2019 10:00:56 -0700 Subject: [PATCH 1/3] docs: add push notification usage to api doc --- packages/service-worker/src/push.ts | 104 ++++++++++++++++++++++++++-- 1 file changed, 100 insertions(+), 4 deletions(-) diff --git a/packages/service-worker/src/push.ts b/packages/service-worker/src/push.ts index cc222ef948f2a..8f91466e78d96 100644 --- a/packages/service-worker/src/push.ts +++ b/packages/service-worker/src/push.ts @@ -14,7 +14,92 @@ 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. + * + * ```ts + * import {Optional} from '@angular/core'; + * import {SwPush} from '@angular/service-worker'; + * ... + * constructor(@Optional() private swPush: SwPush) {} + * ... + * ``` + * 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. + * + * ```ts + * async subscribeToPush() { + * try { + * const sub = await this.swPush.requestSubscription({ + * serverPublicKey: PUBLIC_VAPID_KEY_OF_SERVER + * }); + * // todo send to server + * } + * catch (e) { + * console.error('Could not subscribe', e); + * } + * } + * ``` + * + * 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: + * ```ts + * swPush.notificationClicks.subscribe(({action, notification}) => { + * // TODO: Do something in response to notification click. + * }); + * ``` + * + * @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 */ @@ -27,10 +112,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. * @@ -78,6 +163,12 @@ 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 { if (!this.sw.isEnabled) { return Promise.reject(new Error(ERR_SW_NOT_SUPPORTED)); @@ -98,6 +189,11 @@ 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. + */ unsubscribe(): Promise { if (!this.sw.isEnabled) { return Promise.reject(new Error(ERR_SW_NOT_SUPPORTED)); From 79ac73af952868b32296deaf88df1415c2c24b6e Mon Sep 17 00:00:00 2001 From: George Kalpakas Date: Tue, 27 Aug 2019 22:29:48 +0300 Subject: [PATCH 2/3] fixup! docs: add push notification usage to api doc --- packages/service-worker/src/push.ts | 69 +++++++++++++++-------------- 1 file changed, 36 insertions(+), 33 deletions(-) diff --git a/packages/service-worker/src/push.ts b/packages/service-worker/src/push.ts index 8f91466e78d96..61e37129b9869 100644 --- a/packages/service-worker/src/push.ts +++ b/packages/service-worker/src/push.ts @@ -24,12 +24,12 @@ import {ERR_SW_NOT_SUPPORTED, NgswCommChannel, PushEvent} from './low_level'; * as a dependency. * * ```ts - * import {Optional} from '@angular/core'; - * import {SwPush} from '@angular/service-worker'; - * ... - * constructor(@Optional() private swPush: SwPush) {} - * ... + * import {SwPush} from '@angular/service-worker'; + * ... + * constructor(private swPush: SwPush) {} + * ... * ``` + * * 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) @@ -39,14 +39,13 @@ import {ERR_SW_NOT_SUPPORTED, NgswCommChannel, PushEvent} from './low_level'; * async subscribeToPush() { * try { * const sub = await this.swPush.requestSubscription({ - * serverPublicKey: PUBLIC_VAPID_KEY_OF_SERVER - * }); - * // todo send to server - * } - * catch (e) { - * console.error('Could not subscribe', e); - * } - * } + * serverPublicKey: PUBLIC_VAPID_KEY_OF_SERVER, + * }); + * // TODO: Send to server. + * } catch (e) { + * console.error('Could not subscribe:', e); + * } + * } * ``` * * A request is rejected if the user denies permission, or if the browser @@ -57,25 +56,26 @@ import {ERR_SW_NOT_SUPPORTED, NgswCommChannel, PushEvent} from './low_level'; * * ```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[] + * "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). * @@ -87,8 +87,9 @@ import {ERR_SW_NOT_SUPPORTED, NgswCommChannel, PushEvent} from './low_level'; * * Unsubscribe using `SwPush.unsubscribe()`. * - * An application can subscribe to `SwPush.notificationClicks` observable - * to be notified when a user clicks on a notification. For example: + * An application can subscribe to `SwPush.notificationClicks` observable to be notified when a user + * clicks on a notification. For example: + * * ```ts * swPush.notificationClicks.subscribe(({action, notification}) => { * // TODO: Do something in response to notification click. @@ -166,6 +167,7 @@ export class SwPush { /** * 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. */ @@ -191,8 +193,9 @@ 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. + * + * @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) { From ee622db241a91ff9e00612b39a5a2d1f9eeced53 Mon Sep 17 00:00:00 2001 From: George Kalpakas Date: Tue, 27 Aug 2019 23:17:51 +0300 Subject: [PATCH 3/3] docs(service-worker): add example app for `SwPush` API docs 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. --- .../examples/service-worker/push/BUILD.bazel | 62 +++++++++++++++++ .../service-worker/push/e2e_test/push_spec.ts | 22 +++++++ packages/examples/service-worker/push/main.ts | 12 ++++ .../examples/service-worker/push/module.ts | 66 +++++++++++++++++++ .../service-worker/push/ngsw-worker.js | 14 ++++ .../service-worker/push/start-server.js | 17 +++++ packages/service-worker/src/push.ts | 26 +------- 7 files changed, 196 insertions(+), 23 deletions(-) create mode 100644 packages/examples/service-worker/push/BUILD.bazel create mode 100644 packages/examples/service-worker/push/e2e_test/push_spec.ts create mode 100644 packages/examples/service-worker/push/main.ts create mode 100644 packages/examples/service-worker/push/module.ts create mode 100644 packages/examples/service-worker/push/ngsw-worker.js create mode 100644 packages/examples/service-worker/push/start-server.js diff --git a/packages/examples/service-worker/push/BUILD.bazel b/packages/examples/service-worker/push/BUILD.bazel new file mode 100644 index 0000000000000..3a8460d255364 --- /dev/null +++ b/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", + ], +) diff --git a/packages/examples/service-worker/push/e2e_test/push_spec.ts b/packages/examples/service-worker/push/e2e_test/push_spec.ts new file mode 100644 index 0000000000000..bf01e83a4963b --- /dev/null +++ b/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'); + }); +}); diff --git a/packages/examples/service-worker/push/main.ts b/packages/examples/service-worker/push/main.ts new file mode 100644 index 0000000000000..5ac1a58e47e74 --- /dev/null +++ b/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); diff --git a/packages/examples/service-worker/push/module.ts b/packages/examples/service-worker/push/module.ts new file mode 100644 index 0000000000000..e0c06440eef9f --- /dev/null +++ b/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 { +} diff --git a/packages/examples/service-worker/push/ngsw-worker.js b/packages/examples/service-worker/push/ngsw-worker.js new file mode 100644 index 0000000000000..45b1769cca5cd --- /dev/null +++ b/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()))); diff --git a/packages/examples/service-worker/push/start-server.js b/packages/examples/service-worker/push/start-server.js new file mode 100644 index 0000000000000..cd47c54c057ae --- /dev/null +++ b/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; +}; diff --git a/packages/service-worker/src/push.ts b/packages/service-worker/src/push.ts index 61e37129b9869..38b8930f87b55 100644 --- a/packages/service-worker/src/push.ts +++ b/packages/service-worker/src/push.ts @@ -23,30 +23,14 @@ import {ERR_SW_NOT_SUPPORTED, NgswCommChannel, PushEvent} from './low_level'; * You can inject a `SwPush` instance into any component or service * as a dependency. * - * ```ts - * import {SwPush} from '@angular/service-worker'; - * ... - * constructor(private swPush: SwPush) {} - * ... - * ``` + * * * 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. * - * ```ts - * async subscribeToPush() { - * try { - * const sub = await this.swPush.requestSubscription({ - * serverPublicKey: PUBLIC_VAPID_KEY_OF_SERVER, - * }); - * // TODO: Send to server. - * } catch (e) { - * console.error('Could not subscribe:', e); - * } - * } - * ``` + * * * A request is rejected if the user denies permission, or if the browser * blocks or does not support the Push API or ServiceWorkers. @@ -90,11 +74,7 @@ import {ERR_SW_NOT_SUPPORTED, NgswCommChannel, PushEvent} from './low_level'; * An application can subscribe to `SwPush.notificationClicks` observable to be notified when a user * clicks on a notification. For example: * - * ```ts - * swPush.notificationClicks.subscribe(({action, notification}) => { - * // TODO: Do something in response to notification click. - * }); - * ``` + * * * @see [Push Notifications](https://developers.google.com/web/fundamentals/codelabs/push-notifications/) * @see [Angular Push Notifications](https://blog.angular-university.io/angular-push-notifications/)