← Back to Tutorials Chapter 23

Capabilities

Push Notifications

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.

Subscribing to Push

// 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';
}

Service Worker - Display Notification

// 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);
    })
  );
});

Background Sync

// 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);
    }
  }
}

Advanced APIs

Badging API

// 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();
  }
}

Media Capabilities

// 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);
}

Payment Request API

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;
  }
}
Exercise: Build a PWA that sends push notifications when new content is available. Implement a "subscribe" button that requests notification permission, subscribes to push (mock the server side), and displays a notification when triggered. Also add a badge showing the count of unread items.