Complete push notification system with subscription management, message handling, and notification click events for Angular service worker integration.
Injectable service for managing Web Push Notifications through the Angular Service Worker.
/**
* Service for subscribing to and handling Web Push Notifications
* Provides observables for messages and notification interactions
*/
@Injectable()
class SwPush {
/**
* Observable that emits payloads of received push notification messages
*/
readonly messages: Observable<object>;
/**
* Observable that emits when user interacts with notifications
* Includes both the action taken and notification details
*/
readonly notificationClicks: Observable<{
action: string;
notification: NotificationOptions & { title: string };
}>;
/**
* Observable that emits the current push subscription state
* Emits null when no subscription exists
*/
readonly subscription: Observable<PushSubscription | null>;
/**
* Whether the Service Worker is enabled and push is supported
*/
get isEnabled(): boolean;
/**
* Request push notification subscription from the user
* @param options - Configuration with VAPID public key
* @returns Promise resolving to PushSubscription object
* @throws Error if service worker not supported or user denies permission
*/
requestSubscription(options: {serverPublicKey: string}): Promise<PushSubscription>;
/**
* Unsubscribe from push notifications
* @returns Promise resolving when unsubscribe completes
* @throws Error if not currently subscribed or unsubscribe fails
*/
unsubscribe(): Promise<void>;
}Usage Examples:
import { Component, inject, OnInit } from '@angular/core';
import { SwPush } from '@angular/service-worker';
@Component({...})
export class NotificationComponent implements OnInit {
private swPush = inject(SwPush);
async subscribeToNotifications() {
if (!this.swPush.isEnabled) {
console.log('Push notifications not supported');
return;
}
try {
const subscription = await this.swPush.requestSubscription({
serverPublicKey: 'BEl62iUYgUivxIkv69yViEuiBIa-Ib9-SkvMeAtA3LFgDzkrxZJjSgSnfckjBJuBkr3qBUYIHBQFLXYp5Nksh8U'
});
// Send subscription to your server
await this.sendSubscriptionToServer(subscription);
console.log('Successfully subscribed to push notifications');
} catch (error) {
console.error('Could not subscribe to notifications', error);
}
}
async unsubscribeFromNotifications() {
try {
await this.swPush.unsubscribe();
console.log('Successfully unsubscribed from push notifications');
} catch (error) {
console.error('Could not unsubscribe', error);
}
}
ngOnInit() {
// Listen for push messages
this.swPush.messages.subscribe(message => {
console.log('Received push message:', message);
// Handle incoming push data
});
// Listen for notification clicks
this.swPush.notificationClicks.subscribe(click => {
console.log('Notification clicked:', click);
// Handle user interaction with notification
if (click.action === 'open-url') {
window.open(click.notification.data.url);
}
});
// Monitor subscription state
this.swPush.subscription.subscribe(subscription => {
if (subscription) {
console.log('Currently subscribed to push notifications');
} else {
console.log('Not subscribed to push notifications');
}
});
}
private async sendSubscriptionToServer(subscription: PushSubscription): Promise<void> {
const response = await fetch('/api/push/subscribe', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(subscription.toJSON())
});
if (!response.ok) {
throw new Error('Failed to save subscription');
}
}
}Observable that emits when push messages are received by the service worker.
/**
* Observable that emits the data payload of received push messages
* Messages are delivered even when the app is not in focus
*/
readonly messages: Observable<object>;Usage Examples:
// Simple message handling
this.swPush.messages.subscribe(message => {
console.log('Push message received:', message);
});
// Typed message handling
interface PushMessage {
title: string;
body: string;
data?: any;
}
this.swPush.messages.subscribe((message: PushMessage) => {
this.showNotification(message.title, message.body);
});Observable that emits when users interact with push notifications.
/**
* Observable that emits notification interaction events
* Includes the action taken and full notification details
*/
readonly notificationClicks: Observable<{
action: string;
notification: NotificationOptions & { title: string };
}>;Usage Examples:
// Handle notification clicks
this.swPush.notificationClicks.subscribe(event => {
const { action, notification } = event;
switch (action) {
case 'open-post':
this.router.navigate(['/posts', notification.data.postId]);
break;
case 'mark-read':
this.markAsRead(notification.data.messageId);
break;
case '': // Default click (no action button)
this.openApp();
break;
}
});Request and manage push notification subscriptions.
/**
* Request push notification subscription with VAPID key
* @param options - Object containing serverPublicKey (VAPID public key)
* @returns Promise<PushSubscription> - Browser push subscription
*/
requestSubscription(options: {serverPublicKey: string}): Promise<PushSubscription>;
/**
* Unsubscribe from push notifications
* @returns Promise<void> - Resolves when unsubscription completes
*/
unsubscribe(): Promise<void>;
/**
* Observable of current subscription state
* Emits PushSubscription when subscribed, null when not subscribed
*/
readonly subscription: Observable<PushSubscription | null>;VAPID Key Format:
The serverPublicKey must be a base64url-encoded VAPID public key. Special characters are automatically converted:
_) become forward slashes (/)-) become plus signs (+)Server Integration Example:
async subscribeToNotifications() {
const subscription = await this.swPush.requestSubscription({
serverPublicKey: this.vapidPublicKey
});
// Send subscription to your backend
const response = await fetch('/api/notifications/subscribe', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
subscription: subscription.toJSON(),
userId: this.currentUser.id
})
});
if (!response.ok) {
throw new Error('Failed to save subscription on server');
}
}Push messages sent from your server should follow this structure:
{
"notification": {
"title": "Message Title",
"body": "Message body text",
"icon": "/assets/icon-192x192.png",
"badge": "/assets/badge-72x72.png",
"image": "/assets/notification-image.jpg",
"data": {
"url": "https://example.com/page",
"id": "unique-message-id"
},
"actions": [
{
"action": "open-url",
"title": "Open",
"icon": "/assets/open-icon.png"
},
{
"action": "dismiss",
"title": "Dismiss",
"icon": "/assets/dismiss-icon.png"
}
],
"requireInteraction": false,
"silent": false,
"tag": "message-category",
"renotify": true,
"timestamp": 1640995200000
}
}Required Properties:
title: Notification title (required)Optional Properties:
body: Main notification texticon: Small icon displayed with notificationbadge: Small monochrome icon for mobile status barimage: Large image displayed in notificationdata: Custom data passed to click handlersactions: Array of action buttonsrequireInteraction: Keep notification visible until user interactssilent: Suppress notification sound/vibrationtag: Grouping identifier for replacing notificationsrenotify: Whether to alert user if notification with same tag existstimestamp: When the event occurred// Runtime error codes for push notifications
enum RuntimeErrorCode {
UNKNOWN_REGISTRATION_STRATEGY = 5600,
SERVICE_WORKER_DISABLED_OR_NOT_SUPPORTED_BY_THIS_BROWSER = 5601,
NOT_SUBSCRIBED_TO_PUSH_NOTIFICATIONS = 5602,
PUSH_SUBSCRIPTION_UNSUBSCRIBE_FAILED = 5603,
SERVICE_WORKER_REGISTRATION_FAILED = 5604
}
// Error message constant
const ERR_SW_NOT_SUPPORTED = 'Service workers are disabled or not supported by this browser';Common Error Scenarios:
requestSubscription() rejects with permission errorswPush.isEnabled before operationsunsubscribe() throws NOT_SUBSCRIBED_TO_PUSH_NOTIFICATIONSunsubscribe() throws PUSH_SUBSCRIPTION_UNSUBSCRIBE_FAILEDrequestSubscription() rejects with key format errorError Handling Example:
async handlePushSubscription() {
if (!this.swPush.isEnabled) {
this.showError('Push notifications not supported in this browser');
return;
}
try {
await this.swPush.requestSubscription({
serverPublicKey: this.vapidKey
});
} catch (error) {
if (error.message.includes('permission')) {
this.showError('Please enable notifications in browser settings');
} else {
this.showError('Failed to subscribe to notifications');
}
}
}