← 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
};