Building PWA Features

Offline Capabilities

Creating resilient experiences that work without a network:

Offline-First Approach:

  • Design assuming no connectivity
  • Cache critical resources during installation
  • Provide meaningful offline experiences
  • Sync data when connectivity returns
  • Clear feedback about connection status

Example Offline Page:

<!-- offline.html -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>You're Offline - Weather PWA</title>
  <link rel="stylesheet" href="/styles/main.css">
  <style>
    .offline-container {
      text-align: center;
      padding: 2rem;
      max-width: 600px;
      margin: 0 auto;
    }
    
    .offline-icon {
      font-size: 4rem;
      margin-bottom: 1rem;
      color: #888;
    }
  </style>
</head>
<body>
  <div class="offline-container">
    <div class="offline-icon">📶</div>
    <h1>You're currently offline</h1>
    <p>The Weather PWA requires an internet connection to show the latest weather data.</p>
    <p>Please check your connection and try again.</p>
    <div id="last-updated-info">
      <p>Last data update: <span id="last-updated">Unknown</span></p>
    </div>
    <button id="retry-button" class="button">Retry Connection</button>
  </div>
  
  <script>
    // Check if we have cached data
    if ('caches' in window) {
      caches.match('/api/last-weather-update')
        .then(response => {
          if (response) {
            return response.json();
          }
          return null;
        })
        .then(data => {
          if (data && data.timestamp) {
            const date = new Date(data.timestamp);
            document.getElementById('last-updated').textContent = date.toLocaleString();
          }
        });
    }
    
    // Retry button
    document.getElementById('retry-button').addEventListener('click', () => {
      window.location.reload();
    });
    
    // Listen for online status changes
    window.addEventListener('online', () => {
      window.location.reload();
    });
  </script>
</body>
</html>

IndexedDB for Offline Data:

// Using IndexedDB for offline data storage
class WeatherDataStore {
  constructor() {
    this.dbPromise = this.initDatabase();
  }
  
  async initDatabase() {
    return new Promise((resolve, reject) => {
      const request = indexedDB.open('weather-pwa-db', 1);
      
      request.onerror = event => {
        reject('IndexedDB error: ' + event.target.errorCode);
      };
      
      request.onsuccess = event => {
        resolve(event.target.result);
      };
      
      request.onupgradeneeded = event => {
        const db = event.target.result;
        
        // Create object stores
        const forecastStore = db.createObjectStore('forecasts', { keyPath: 'locationId' });
        forecastStore.createIndex('timestamp', 'timestamp', { unique: false });
        
        const locationStore = db.createObjectStore('locations', { keyPath: 'id' });
        locationStore.createIndex('name', 'name', { unique: false });
      };
    });
  }
  
  async saveForecast(locationId, forecastData) {
    const db = await this.dbPromise;
    const tx = db.transaction('forecasts', 'readwrite');
    const store = tx.objectStore('forecasts');
    
    const data = {
      locationId,
      forecast: forecastData,
      timestamp: new Date().getTime()
    };
    
    await store.put(data);
    return tx.complete;
  }
  
  async getForecast(locationId) {
    const db = await this.dbPromise;
    const tx = db.transaction('forecasts', 'readonly');
    const store = tx.objectStore('forecasts');
    return store.get(locationId);
  }
}

Push Notifications

Engaging users with timely updates:

Push Notification Workflow:

  1. Request user permission
  2. Subscribe to push service
  3. Send subscription to server
  4. Server sends push message
  5. Service worker receives push event
  6. Service worker shows notification

Example Push Notification Implementation:

// Request permission and subscribe to push notifications
async function subscribeToPushNotifications() {
  try {
    // Request permission
    const permission = await Notification.requestPermission();
    if (permission !== 'granted') {
      throw new Error('Permission not granted for notifications');
    }
    
    // Get service worker registration
    const registration = await navigator.serviceWorker.ready;
    
    // Get push subscription
    let subscription = await registration.pushManager.getSubscription();
    
    // Create new subscription if one doesn't exist
    if (!subscription) {
      // Get server's public key
      const response = await fetch('/api/push/public-key');
      const { publicKey } = await response.json();
      
      // Convert public key to Uint8Array
      const applicationServerKey = urlBase64ToUint8Array(publicKey);
      
      // Subscribe
      subscription = await registration.pushManager.subscribe({
        userVisibleOnly: true,
        applicationServerKey
      });
    }
    
    // Send subscription to server
    await saveSubscription(subscription);
    
    return subscription;
  } catch (error) {
    console.error('Failed to subscribe to push notifications:', error);
    throw error;
  }
}

// In service worker
self.addEventListener('push', event => {
  const data = event.data.json();
  
  const options = {
    body: data.body,
    icon: '/images/notification-icon.png',
    badge: '/images/badge-icon.png',
    vibrate: [100, 50, 100],
    data: {
      url: data.url
    }
  };
  
  event.waitUntil(
    self.registration.showNotification(data.title, options)
  );
});

Best Practices for Push Notifications:

  • Request permission at appropriate times
  • Make notifications relevant and timely
  • Provide clear opt-out mechanisms
  • Use rich notifications when appropriate
  • Respect user preferences and time zones
  • Test across different platforms

App Installation

Enabling home screen installation:

Installation Process:

  • Browser checks if app is installable
  • User triggers installation (browser UI or custom button)
  • Browser shows installation prompt
  • User confirms installation
  • App installed on device

Installation Criteria:

  • Valid web app manifest
  • Served over HTTPS
  • Registered service worker
  • Icon defined in manifest
  • Display mode not “browser”
  • Meets engagement heuristics (varies by browser)

Custom Install Button:

// Track installation status
let deferredPrompt;
const installButton = document.getElementById('install-button');

// Initially hide the install button
installButton.style.display = 'none';

// Listen for beforeinstallprompt event
window.addEventListener('beforeinstallprompt', (event) => {
  // Prevent default browser install prompt
  event.preventDefault();
  
  // Store the event for later use
  deferredPrompt = event;
  
  // Show the install button
  installButton.style.display = 'block';
});

// Handle install button click
installButton.addEventListener('click', async () => {
  if (!deferredPrompt) {
    return;
  }
  
  // Show the install prompt
  deferredPrompt.prompt();
  
  // Wait for user response
  const { outcome } = await deferredPrompt.userChoice;
  console.log(`User response to install prompt: ${outcome}`);
  
  // Clear the deferred prompt
  deferredPrompt = null;
  
  // Hide the install button
  installButton.style.display = 'none';
});