← back to Watches

src/utils/pwaUtils.js

443 lines

/**
 * PWA Utilities for Omega Watch Price History
 * Handles service worker registration, installation prompt, and offline detection
 */

let deferredPrompt = null;
let isOnline = navigator.onLine;

/**
 * Register the service worker
 */
export async function registerServiceWorker() {
  if ('serviceWorker' in navigator) {
    try {
      const registration = await navigator.serviceWorker.register('/service-worker.js', {
        scope: '/'
      });

      console.log('Service Worker registered:', registration.scope);

      // Check for updates periodically
      setInterval(() => {
        registration.update();
      }, 60000); // Check every minute

      // Handle updates
      registration.addEventListener('updatefound', () => {
        const newWorker = registration.installing;

        newWorker.addEventListener('statechange', () => {
          if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
            // New service worker available
            showUpdateNotification();
          }
        });
      });

      return registration;
    } catch (error) {
      console.error('Service Worker registration failed:', error);
      return null;
    }
  }

  console.log('Service Workers not supported');
  return null;
}

/**
 * Unregister service worker (for debugging)
 */
export async function unregisterServiceWorker() {
  if ('serviceWorker' in navigator) {
    const registrations = await navigator.serviceWorker.getRegistrations();
    for (const registration of registrations) {
      await registration.unregister();
    }
    console.log('Service Worker unregistered');
  }
}

/**
 * Show notification when update is available
 */
function showUpdateNotification() {
  const updateBanner = document.createElement('div');
  updateBanner.className = 'update-banner';
  updateBanner.innerHTML = `
    <div class="update-content">
      <span>A new version is available!</span>
      <button onclick="window.location.reload()" class="update-button">Update Now</button>
    </div>
  `;
  document.body.appendChild(updateBanner);
}

/**
 * Setup install prompt handler
 */
export function setupInstallPrompt(onInstallable, onInstalled) {
  // Capture the install prompt event
  window.addEventListener('beforeinstallprompt', (e) => {
    e.preventDefault();
    deferredPrompt = e;

    // Notify that app is installable
    if (onInstallable) {
      onInstallable(true);
    }

    console.log('App is installable');
  });

  // Detect when app was installed
  window.addEventListener('appinstalled', () => {
    console.log('App installed successfully');
    deferredPrompt = null;

    if (onInstalled) {
      onInstalled();
    }
  });
}

/**
 * Show the install prompt
 */
export async function showInstallPrompt() {
  if (!deferredPrompt) {
    console.log('Install prompt not available');
    return false;
  }

  // Show the install prompt
  deferredPrompt.prompt();

  // Wait for user response
  const { outcome } = await deferredPrompt.userChoice;

  console.log('Install prompt outcome:', outcome);

  // Clear the prompt
  deferredPrompt = null;

  return outcome === 'accepted';
}

/**
 * Check if app is installed
 */
export function isAppInstalled() {
  // Check if running as PWA
  const isStandalone = window.matchMedia('(display-mode: standalone)').matches;
  const isIOSInstalled = window.navigator.standalone === true;

  return isStandalone || isIOSInstalled;
}

/**
 * Check if device is iOS
 */
export function isIOS() {
  return /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
}

/**
 * Check if device is Android
 */
export function isAndroid() {
  return /Android/.test(navigator.userAgent);
}

/**
 * Setup online/offline detection
 */
export function setupOnlineDetection(onStatusChange) {
  window.addEventListener('online', () => {
    isOnline = true;
    console.log('App is online');

    if (onStatusChange) {
      onStatusChange(true);
    }

    // Trigger background sync if available
    triggerBackgroundSync();
  });

  window.addEventListener('offline', () => {
    isOnline = false;
    console.log('App is offline');

    if (onStatusChange) {
      onStatusChange(false);
    }
  });

  return isOnline;
}

/**
 * Get current online status
 */
export function getOnlineStatus() {
  return isOnline;
}

/**
 * Trigger background sync
 */
export async function triggerBackgroundSync() {
  if ('serviceWorker' in navigator && 'sync' in window.ServiceWorkerRegistration.prototype) {
    try {
      const registration = await navigator.serviceWorker.ready;
      await registration.sync.register('sync-watchlist');
      await registration.sync.register('sync-favorites');
      console.log('Background sync registered');
    } catch (error) {
      console.error('Background sync failed:', error);
    }
  }
}

/**
 * Request notification permission
 */
export async function requestNotificationPermission() {
  if (!('Notification' in window)) {
    console.log('Notifications not supported');
    return false;
  }

  if (Notification.permission === 'granted') {
    return true;
  }

  if (Notification.permission !== 'denied') {
    const permission = await Notification.requestPermission();
    return permission === 'granted';
  }

  return false;
}

/**
 * Show local notification
 */
export function showNotification(title, options = {}) {
  if (Notification.permission !== 'granted') {
    console.log('Notification permission not granted');
    return;
  }

  const defaultOptions = {
    icon: '/icons/icon-192x192.png',
    badge: '/icons/badge-72x72.png',
    vibrate: [200, 100, 200],
    requireInteraction: false
  };

  new Notification(title, { ...defaultOptions, ...options });
}

/**
 * Subscribe to push notifications
 */
export async function subscribeToPushNotifications() {
  if (!('serviceWorker' in navigator) || !('PushManager' in window)) {
    console.log('Push notifications not supported');
    return null;
  }

  try {
    const registration = await navigator.serviceWorker.ready;

    // Check if already subscribed
    let subscription = await registration.pushManager.getSubscription();

    if (!subscription) {
      // Subscribe to push notifications
      const vapidPublicKey = 'YOUR_VAPID_PUBLIC_KEY'; // Replace with actual key

      subscription = await registration.pushManager.subscribe({
        userVisibleOnly: true,
        applicationServerKey: urlBase64ToUint8Array(vapidPublicKey)
      });

      console.log('Push subscription created:', subscription);
    }

    return subscription;
  } catch (error) {
    console.error('Push subscription failed:', error);
    return null;
  }
}

/**
 * Clear app cache (for debugging)
 */
export async function clearAppCache() {
  if ('caches' in window) {
    const cacheNames = await caches.keys();
    await Promise.all(
      cacheNames.map(name => caches.delete(name))
    );
    console.log('Cache cleared');
  }

  // Also clear service worker cache via message
  if (navigator.serviceWorker.controller) {
    navigator.serviceWorker.controller.postMessage({
      action: 'clearCache'
    });
  }
}

/**
 * Get cache size
 */
export async function getCacheSize() {
  if (!('caches' in window)) {
    return 0;
  }

  let totalSize = 0;

  const cacheNames = await caches.keys();

  for (const name of cacheNames) {
    const cache = await caches.open(name);
    const keys = await cache.keys();

    for (const request of keys) {
      const response = await cache.match(request);
      if (response) {
        const blob = await response.blob();
        totalSize += blob.size;
      }
    }
  }

  return totalSize;
}

/**
 * Format cache size for display
 */
export function formatCacheSize(bytes) {
  if (bytes === 0) return '0 Bytes';

  const k = 1024;
  const sizes = ['Bytes', 'KB', 'MB', 'GB'];
  const i = Math.floor(Math.log(bytes) / Math.log(k));

  return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
}

/**
 * Helper function to convert VAPID key
 */
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 outputArray = new Uint8Array(rawData.length);

  for (let i = 0; i < rawData.length; ++i) {
    outputArray[i] = rawData.charCodeAt(i);
  }

  return outputArray;
}

/**
 * Check if app needs update
 */
export async function checkForUpdates() {
  if ('serviceWorker' in navigator) {
    const registration = await navigator.serviceWorker.getRegistration();
    if (registration) {
      await registration.update();
    }
  }
}

/**
 * Share watch data (Web Share API)
 */
export async function shareWatch(watch) {
  if (!navigator.share) {
    console.log('Web Share API not supported');
    return false;
  }

  try {
    await navigator.share({
      title: `${watch.model} - Omega Watch`,
      text: `Check out this ${watch.series} watch: ${watch.model}`,
      url: `${window.location.origin}?watch=${watch.id}`
    });

    console.log('Watch shared successfully');
    return true;
  } catch (error) {
    if (error.name !== 'AbortError') {
      console.error('Share failed:', error);
    }
    return false;
  }
}

/**
 * Detect if app is running in standalone mode
 */
export function isStandaloneMode() {
  return isAppInstalled();
}

/**
 * Get app install status message
 */
export function getInstallStatusMessage() {
  if (isAppInstalled()) {
    return 'App is installed';
  }

  if (isIOS()) {
    return 'Tap Share > Add to Home Screen to install';
  }

  if (isAndroid()) {
    return 'Tap menu > Install app';
  }

  return 'Install this app for offline access';
}

export default {
  registerServiceWorker,
  unregisterServiceWorker,
  setupInstallPrompt,
  showInstallPrompt,
  isAppInstalled,
  isIOS,
  isAndroid,
  setupOnlineDetection,
  getOnlineStatus,
  requestNotificationPermission,
  showNotification,
  subscribeToPushNotifications,
  clearAppCache,
  getCacheSize,
  formatCacheSize,
  checkForUpdates,
  shareWatch,
  isStandaloneMode,
  getInstallStatusMessage
};