8000 feat(service-worker): support notification closes · angular/angular@03427c6 · GitHub
[go: up one dir, main page]

Skip to content

Commit 03427c6

Browse files
committed
feat(service-worker): support notification closes
In this commit, support for `notificationclose` events has been added to the service worker Driver. When a notification is closed (either by user dismissal or programmatically), the Driver now captures the event, extracts the relevant notification options, and broadcasts a `NOTIFICATION_CLOSE` message to clients. This ensures the application is aware of notification lifecycle events and can react accordingly.
1 parent 1e79d47 commit 03427c6

File tree

6 files changed

+124
-2
lines changed

6 files changed

+124
-2
lines changed

goldens/public-api/service-worker/index.api.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,12 @@ export class SwPush {
4646
title: string;
4747
};
4848
}>;
49+
readonly notificationCloses: Observable<{
50+
action: string;
51+
notification: NotificationOptions & {
52+
title: string;
53+
};
54+
}>;
4955
requestSubscription(options: {
5056
serverPublicKey: string;
5157
}): Promise<PushSubscription>;

packages/service-worker/src/push.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,25 @@ export class SwPush {
117117
};
118118
}>;
119119

120+
/**
121+
* Emits the payloads of notifications that were closed, along with the action (if any)
122+
* associated with the close event. If no action was used, the `action` property contains
123+
* an empty string `''`.
124+
*
125+
* Note that the `notification` property does **not** contain a
126+
* [Notification][Mozilla Notification] object but rather a
127+
* [NotificationOptions](https://notifications.spec.whatwg.org/#dictdef-notificationoptions)
128+
* object that also includes the `title` of the [Notification][Mozilla Notification] object.
129+
*
130+
* [Mozilla Notification]: https://developer.mozilla.org/en-US/docs/Web/API/Notification
131+
*/
132+
readonly notificationCloses: Observable<{
133+
action: string;
134+
notification: NotificationOptions & {
135+
title: string;
136+
};
137+
}>;
138+
120139
/**
121140
* Emits the currently active
122141
* [PushSubscription](https://developer.mozilla.org/en-US/docs/Web/API/PushSubscription)
@@ -139,6 +158,7 @@ export class SwPush {
139158
if (!sw.isEnabled) {
140159
this.messages = NEVER;
141160
this.notificationClicks = NEVER;
161+
this.notificationCloses = NEVER;
142162
this.subscription = NEVER;
143163
return;
144164
}
@@ -149,6 +169,10 @@ export class SwPush {
149169
.eventsOfType('NOTIFICATION_CLICK')
150170
.pipe(map((message: any) => message.data));
151171

172+
this.notificationCloses = this.sw
173+
.eventsOfType('NOTIFICATION_CLOSE')
174+
.pipe(map((message: any) => message.data));
175+
152176
this.pushManager = this.sw.registration.pipe(map((registration) => registration.pushManager));
153177

154178
const workerDrivenSubscriptions = this.pushManager.pipe(

packages/service-worker/test/comm_spec.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,22 @@ describe('ServiceWorker library', () => {
353353
});
354354
});
355355

356+
describe('notificationCloses', ( A93C ) => {
357+
it('receives notification closes messages', () => {
358+
const sendMessage = (type: string, action: string) =>
359+
mock.sendMessage({type, data: {action}});
360+
361+
const receivedMessages: string[] = [];
362+
push.notificationCloses.subscribe((msg: {action: string}) =>
363+
receivedMessages.push(msg.action),
364+
);
365+
366+
sendMessage('NOTIFICATION_CLOSE', 'empty_string');
367+
368+
expect(receivedMessages).toEqual(['empty_string']);
369+
});
370+
});
371+
356372
describe('subscription', () => {
357373
let nextSubEmitResolve: () => void;
358374
let nextSubEmitPromise: Promise<void>;

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

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -181,11 +181,12 @@ export class Driver implements Debuggable, UpdateSource {
181181
}
182182
});
183183

184-
// Handle the fetch, message, and push events.
184+
// Handle the fetch, message, and push, notificationclick and notificationclose events.
185185
this.scope.addEventListener('fetch', (event) => this.onFetch(event!));
186186
this.scope.addEventListener('message', (event) => this.onMessage(event!));
187187
this.scope.addEventListener('push', (event) => this.onPush(event!));
188-
this.scope.addEventListener('notificationclick', (event) => this.onClick(event!));
188+
this.scope.addEventListener('notificationclick', (event) => this.onClick(event));
189+
this.scope.addEventListener('notificationclose', (event) => this.onClose(event));
189190

190191
// The debugger generates debug pages in response to debugging requests.
191192
this.debugger = new DebugHandler(this, this.adapter);
@@ -313,6 +314,11 @@ export class Driver implements Debuggable, UpdateSource {
313314
event.waitUntil(this.handleClick(event.notification, event.action));
314315
}
315316

317+
private onClose(event: NotificationEvent): void {
318+
// Handle the close event and keep the SW alive until it's handled.
319+
event.waitUntil(this.handleClose(event.notification, event.action));
320+
}
321+
316322
private async ensureInitialized(event: ExtendableEvent): Promise<void> {
317323
// Since the SW may have just been started, it may or may not have been initialized already.
318324
// `this.initialized` will be `null` if initialization has not yet been attempted, or will be a
@@ -418,6 +424,29 @@ export class Driver implements Debuggable, UpdateSource {
418424
});
419425
}
420426

427+
/**
428+
* Handles the closing of a notification by extracting its options and
429+
* broadcasting a `NOTIFICATION_CLOSE` message.
430+
*
431+
* This is typically called when a notification is dismissed by the user
432+
* or closed programmatically, and it relays that information to clients
433+
* listening for service worker events.
434+
*
435+
* @param notification - The original `Notification` object that was closed.
436+
* @param action - The action string associated with the close event, if any (usually an empty string).
437+
*/
438+
private async handleClose(notification: Notification, action: string): Promise<void> {
439+
const options: {-readonly [K in keyof Notification]?: Notification[K]} = {};
440+
NOTIFICATION_OPTION_NAMES.filter((name) => name in notification).forEach(
441+
(name) => (options[name] = notification[name]),
442+
);
443+
444+
await this.broadcast({
445+
type: 'NOTIFICATION_CLOSE',
446+
data: {action, notification: options},
447+
});
448+
}
449+
421450
private async getLastFocusedMatchingClient(
422451
scope: ServiceWorkerGlobalScope,
423452
): Promise<WindowClient | null> {

packages/service-worker/worker/test/happy_spec.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1193,6 +1193,44 @@ import {envIsSupported} from '../testing/utils';
11931193
});
11941194
});
11951195

1196+
describe('notification close events', () => {
1197+
it('broadcasts notification close events', async () => {
1198+
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
1199+
const notification = {title: 'This is a test with action', body: 'Test body with action'};
1200+
await driver.initialized;
1201+
await scope.handleClick(
1202+
{title: 'This is a test with action', body: 'Test body with action'},
1203+
'button',
1204+
);
1205+
await scope.handleClose(notification, '');
1206+
1207+
const {messages} = scope.clients.getMock('default')!;
1208+
1209+
expect(messages).toEqual([
1210+
{
1211+
type: 'NOTIFICATION_CLICK',
1212+
data: {
1213+
action: 'button',
1214+
notification: {
1215+
title: notification.title,
1216+
body: notification.body,
1217+
},
1218+
},
1219+
},
1220+
{
1221+
type: 'NOTIFICATION_CLOSE',
1222+
data: {
1223+
action: '',
1224+
notification: {
1225+
title: notification.title,
1226+
body: notification.body,
1227+
},
1228+
},
1229+
},
1230+
]);
1231+
});
1232+
});
1233+
11961234
it('prefetches updates to lazy cache when set', async () => {
11971235
expect(await makeRequest(scope, '/foo.txt')).toEqual('this is foo');
11981236
await driver.initialized;

packages/service-worker/worker/testing/scope.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,15 @@ export class SwTestHarnessImpl
254254
return event.ready;
255255
}
256256

257+
handleClose(notification: Object, action: string): Promise<void> {
258+
if (!this.eventHandlers.has('notificationclose')) {
259+
throw new Error('No notificationclose handler registered');
260+
}
261+
const event = new MockNotificationEvent(notification, action);
262+
this.eventHandlers.get('notificationclose')!.call(this, event);
263+
return event.ready;
264+
}
265+
257266
override timeout(ms: number): Promise<void> {
258267
const promise = new Promise<void>((resolve) => {
259268
this.timers.push({

0 commit comments

Comments
 (0)
0