Push notifications allow your PWA to send messages to users even when the browser is not open. They require two steps: subscribing to push notifications and displaying them.
// Convert VAPID public key to Uint8Array
function urlBase64ToUint8Array(base64String) {
const padding = '='.repeat((4 - base64String.length % 4) % 4);
const base64 = (base64String + padding)
.replace(/-/g, '+')
.replace(/_/g, '/');
const rawData = window.atob(base64);
const output = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
output[i] = rawData.charCodeAt(i);
}
return output;
}
// Subscribe to push
async function subscribeToPush() {
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(
'YOUR_VAPID_PUBLIC_KEY'
)
});
// Send subscription to your server
await fetch('/api/push/subscribe', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(subscription)
});
return subscription;
}
// Check and request permission
async function enableNotifications() {
if (!('Notification' in window)) {
console.log('Notifications not supported');
return false;
}
const permission = await Notification.requestPermission();
return permission === 'granted';
}
// sw.js - Handle push events
self.addEventListener('push', (event) => {
let data = { title: 'New Message', body: 'You have a new notification', icon: '/icon-192.png' };
if (event.data) {
try {
data = event.data.json();
} catch {
data.body = event.data.text();
}
}
event.waitUntil(
self.registration.showNotification(data.title, {
body: data.body,
icon: data.icon,
badge: '/badge-72.png',
vibrate: [200, 100, 200],
data: data,
actions: [
{ action: 'open', title: 'Open' },
{ action: 'dismiss', title: 'Dismiss' }
]
})
);
});
// Handle notification clicks
self.addEventListener('notificationclick', (event) => {
event.notification.close();
if (event.action === 'dismiss') return;
const urlToOpen = event.notification.data?.url || '/';
event.waitUntil(
self.clients.matchAll({ type: 'window' }).then(clients => {
for (const client of clients) {
if (client.url === urlToOpen && 'focus' in client) {
return client.focus();
}
}
return self.clients.openWindow(urlToOpen);
})
);
});
// Register sync for pending data
async function queueAction(actionData) {
await saveToIndexedDB('pending-actions', actionData);
const registration = await navigator.serviceWorker.ready;
await registration.sync.register('sync-actions');
}
// In Service Worker
self.addEventListener('sync', (event) => {
if (event.tag === 'sync-actions') {
event.waitUntil(processPendingActions());
}
});
async function processPendingActions() {
const actions = await getAllFromIndexedDB('pending-actions');
for (const action of actions) {
try {
await fetch(action.url, {
method: action.method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(action.data)
});
await deleteFromIndexedDB('pending-actions', action.id);
} catch (err) {
console.error('Sync failed for action', action.id, err);
}
}
}
// Set app badge
async function setBadge(count) {
if (navigator.setAppBadge) {
await navigator.setAppBadge(count);
} else if (navigator.setExperimentalAppBadge) {
await navigator.setExperimentalAppBadge(count);
}
}
// Clear badge
async function clearBadge() {
if (navigator.clearAppBadge) {
await navigator.clearAppBadge();
}
}
// Update badge when unread count changes
function updateBadge(unreadCount) {
if (unreadCount > 0) {
setBadge(unreadCount);
} else {
clearBadge();
}
}
// Check media decoding capabilities
async function checkMediaSupport() {
const config = {
type: 'file',
video: {
contentType: 'video/webm; codecs="vp9"',
width: 1920,
height: 1080,
bitrate: 5000000,
framerate: 30
}
};
const support = await navigator.mediaCapabilities.decodingInfo(config);
console.log('Supported:', support.supported);
console.log('Smooth:', support.smooth);
console.log('Power efficient:', support.powerEfficient);
}
async function processPayment(items) {
if (!window.PaymentRequest) {
showFallbackPayment();
return;
}
const methodData = [
{ supportedMethods: 'basic-card' },
{ supportedMethods: 'https://google.com/pay' }
];
const details = {
total: {
label: 'Total',
amount: { currency: 'USD', value: items.reduce((sum, i) => sum + i.price, 0).toFixed(2) }
},
displayItems: items.map(item => ({
label: item.name,
amount: { currency: 'USD', value: item.price.toFixed(2) }
}))
};
try {
const request = new PaymentRequest(methodData, details);
const response = await request.show();
await response.complete('success');
return response.details;
} catch (err) {
console.error('Payment failed:', err);
return null;
}
}