Skip to content

Commit d546501

Browse files
codingnucleidylhunn
authored andcommitted
feat(service-worker): add openWindow, focusLastFocusedOrOpen and navigateLastFocusedOrOpen (angular#42520)
Add `openWindow`, `focusLastFocusedOrOpen` and `navigateLastFocusedOrOpen` abilty to the notificationclick handler such that when either a notification or notification action is clicked the service-worker can act accordinly without the need for the app to be open PR Close angular#26907 PR Close angular#42520
1 parent 9de65db commit d546501

File tree

10 files changed

+549
-33
lines changed

10 files changed

+549
-33
lines changed

.pullapprove.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -617,6 +617,7 @@ groups:
617617
'aio/content/guide/service-worker-config.md',
618618
'aio/content/guide/service-worker-devops.md',
619619
'aio/content/guide/service-worker-intro.md',
620+
'aio/content/guide/service-worker-notifications.md',
620621
'aio/content/images/guide/service-worker/**'
621622
])
622623
reviewers:

aio/content/guide/service-worker-communications.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,4 +95,4 @@ You can subscribe to `SwUpdate#unrecoverable` to be notified and handle these er
9595
## More on Angular service workers
9696

9797
You may also be interested in the following:
98-
* [Service Worker in Production](guide/service-worker-devops).
98+
* [Service Worker Notifications](guide/service-worker-notifications).

aio/content/guide/service-worker-intro.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ The rest of the articles in this section specifically address the Angular implem
6666

6767
* [App Shell](guide/app-shell)
6868
* [Service Worker Communication](guide/service-worker-communications)
69+
* [Service Worker Notifications](guide/service-worker-notifications)
6970
* [Service Worker in Production](guide/service-worker-devops)
7071
* [Service Worker Configuration](guide/service-worker-config)
7172

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# Service worker notifications
2+
3+
Push notifications are a compelling way to engage users. Through the power of service workers, notifications can be delivered to a device even when your application is not in focus.
4+
5+
The Angular service worker enables the display of push notifications and the handling of notification click events.
6+
7+
<div class="alert is-helpful">
8+
9+
When using the Angular service worker, push notification interactions are handled using the `SwPush` service.
10+
To learn more about the native APIs involved see [Push API](https://developer.mozilla.org/en-US/docs/Web/API/Push_API) and [Using the Notifications API](https://developer.mozilla.org/en-US/docs/Web/API/Notifications_API/Using_the_Notifications_API).
11+
12+
</div>
13+
14+
#### Prerequisites
15+
16+
We recommend you have a basic understanding of the following:
17+
18+
- [Getting Started with Service Workers](guide/service-worker-getting-started).
19+
20+
## Notification payload
21+
22+
Invoke push notifications by pushing a message with a valid payload. See `SwPush` for guidance.
23+
24+
<div class="alert is-helpful">
25+
26+
In Chrome, you can test push notifications without a backend.
27+
Open Devtools -> Application -> Service Workers and use the `Push` input to send a JSON notification payload.
28+
29+
</div>
30+
31+
## Notification click handling
32+
33+
The default behavior for the `notificationclick` event is to close the notification and notify `SwPush.notificationClicks`.
34+
35+
You can specify an additional operation to be executed on `notificationclick` by adding an `onActionClick` property to the `data` object, and providing a `default` entry. This is especially useful for when there are no open clients when a notification is clicked.
36+
37+
```json
38+
{
39+
"notification": {
40+
"title": "New Notification!",
41+
"data": {
42+
"onActionClick": {
43+
"default": {"operation": "openWindow", "url": "foo"}
44+
}
45+
}
46+
}
47+
}
48+
```
49+
50+
### Operations
51+
52+
The Angular service worker supports the following operations:
53+
54+
- `openWindow`: Opens a new tab at the specified URL, which is resolved relative to the service worker scope.
55+
56+
- `focusLastFocusedOrOpen`: Focuses the last focused client. If there is no client open, then it opens a new tab at the specified URL, which is resolved relative to the service worker scope.
57+
58+
- `navigateLastFocusedOrOpen`: Focuses the last focused client and navigates it to the specified URL, which is resolved relative to the service worker scope. If there is no client open, then it opens a new tab at the specified URL.
59+
60+
<div class="alert is-important">
61+
62+
If an `onActionClick` item does not define a `url`, then the service worker's registration scope is used.
63+
64+
</div>
65+
66+
### Actions
67+
68+
Actions offer a way to customize how the user can interact with a notification.
69+
70+
Using the `actions` property, you can define a set of available actions. Each action is represented as an action button that the user can click to interact with the notification.
71+
72+
In addition, using the `onActionClick` property on the `data` object, you can tie each action to an operation to be performed when the corresponding action button is clicked:
73+
74+
```ts
75+
{
76+
"notification": {
77+
"title": "New Notification!",
78+
"actions": [
79+
{"action": "foo", "title": "Open new tab"},
80+
{"action": "bar", "title": "Focus last"},
81+
{"action": "baz", "title": "Navigate last"},
82+
{"action": "qux", "title": "Just notify existing clients"}
83+
],
84+
"data": {
85+
"onActionClick": {
86+
"default": {"operation": "openWindow"},
87+
"foo": {"operation": "openWindow", "url": "/absolute/path"},
88+
"bar": {"operation": "focusLastFocusedOrOpen", "url": "relative/path"},
89+
"baz": {"operation": "navigateLastFocusedOrOpen", "url": "https://other.domain.com/"}
90+
}
91+
}
92+
}
93+
}
94+
```
95+
96+
<div class="alert is-important">
97+
98+
If an action does not have a corresponding `onActionClick` entry, then the notification is closed and `SwPush.notificationClicks` is notified on existing clients.
99+
100+
</div>
101+
102+
## More on Angular service workers
103+
104+
You may also be interested in the following:
105+
106+
- [Service Worker in Production](guide/service-worker-devops).

aio/content/navigation.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,11 @@
435435
"title": "Service Worker Communication",
436436
"tooltip": "Services that enable you to interact with an Angular service worker."
437437
},
438+
{
439+
"url": "guide/service-worker-notifications",
440+
"title": "Service Worker Notifications",
441+
"tooltip": "Configuring service worker notification behavior."
442+
},
438443
{
439444
"url": "guide/service-worker-devops",
440445
"title": "Service Worker in Production",

packages/service-worker/src/push.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,9 @@ import {ERR_SW_NOT_SUPPORTED, NgswCommChannel, PushEvent} from './low_level';
8181
* <code-example path="service-worker/push/module.ts" region="subscribe-to-notification-clicks"
8282
* header="app.component.ts"></code-example>
8383
*
84+
* You can read more on handling notification clicks in the [Service worker notifications
85+
* guide](guide/service-worker-notifications).
86+
*
8487
* @see [Push Notifications](https://developers.google.com/web/fundamentals/codelabs/push-notifications/)
8588
* @see [Angular Push Notifications](https://blog.angular-university.io/angular-push-notifications/)
8689
* @see [MDN: Push API](https://developer.mozilla.org/en-US/docs/Web/API/Push_API)

packages/service-worker/worker/src/driver.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,12 +348,52 @@ export class Driver implements Debuggable, UpdateSource {
348348
NOTIFICATION_OPTION_NAMES.filter(name => name in notification)
349349
.forEach(name => options[name] = notification[name]);
350350

351+
const notificationAction = action === '' || action === undefined ? 'default' : action;
352+
353+
const onActionClick = notification?.data?.onActionClick[notificationAction];
354+
355+
const urlToOpen = new URL(onActionClick?.url ?? '', this.scope.registration.scope).href;
356+
357+
switch (onActionClick?.operation) {
358+
case 'openWindow':
359+
await this.scope.clients.openWindow(urlToOpen);
360+
break;
361+
case 'focusLastFocusedOrOpen': {
362+
let matchingClient = await this.getLastFocusedMatchingClient(this.scope);
363+
if (matchingClient) {
364+
await matchingClient?.focus();
365+
} else {
366+
await this.scope.clients.openWindow(urlToOpen);
367+
}
368+
break;
369+
}
370+
case 'navigateLastFocusedOrOpen': {
371+
let matchingClient = await this.getLastFocusedMatchingClient(this.scope);
372+
if (matchingClient) {
373+
matchingClient = await matchingClient.navigate(urlToOpen);
374+
await matchingClient?.focus();
375+
} else {
376+
await this.scope.clients.openWindow(urlToOpen);
377+
}
378+
break;
379+
}
380+
default:
381+
break;
382+
}
383+
351384
await this.broadcast({
352385
type: 'NOTIFICATION_CLICK',
353386
data: {action, notification: options},
354387
});
355388
}
356389

390+
private async getLastFocusedMatchingClient(scope: ServiceWorkerGlobalScope):
391+
Promise<WindowClient|null> {
392+
const windowClients = await scope.clients.matchAll({type: 'window'});
393+
394+
// As per the spec windowClients are `sorted in the most recently focused order`
395+
return windowClients[0];
396+
}
357397
private async reportStatus(client: Client, promise: Promise<void>, nonce: number): Promise<void> {
358398
const response = {type: 'STATUS', nonce, status: true};
359399
try {

packages/service-worker/worker/src/service-worker.d.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,21 +36,25 @@ declare class Client {
3636
}
3737

3838
interface Clients {
39-
claim(): Promise<any>;
39+
claim(): Promise<void>;
4040
get(id: string): Promise<Client>;
41-
matchAll(options?: ClientMatchOptions): Promise<Array<Client>>;
41+
matchAll<T extends ClientMatchOptions>(
42+
options?: T
43+
): Promise<ReadonlyArray<T['type'] extends 'window' ? WindowClient : Client>>;
44+
openWindow(url: string): Promise<WindowClient | null>;
4245
}
4346

4447
interface ClientMatchOptions {
4548
includeUncontrolled?: boolean;
4649
type?: ClientMatchTypes;
4750
}
4851

49-
interface WindowClient {
50-
focused: boolean;
51-
visibilityState: WindowClientState;
52+
interface WindowClient extends Client {
53+
readonly ancestorOrigins: ReadonlyArray<string>;
54+
readonly focused: boolean;
55+
readonly visibilityState: VisibilityState;
5256
focus(): Promise<WindowClient>;
53-
navigate(url: string): Promise<WindowClient>;
57+
navigate(url: string): Promise<WindowClient | null>;
5458
}
5559

5660
type ClientFrameType = 'auxiliary'|'top-level'|'nested'|'none';

0 commit comments

Comments
 (0)