diff --git a/LittleShop/wwwroot/js/pwa.js b/LittleShop/wwwroot/js/pwa.js index dcb5606..f87fc48 100644 --- a/LittleShop/wwwroot/js/pwa.js +++ b/LittleShop/wwwroot/js/pwa.js @@ -1,537 +1,568 @@ -// Progressive Web App functionality -// Handles service worker registration and PWA features - -class PWAManager { - constructor() { - this.swRegistration = null; - this.vapidPublicKey = null; - this.pushSubscription = null; - this.init(); - } - - async init() { - console.log('PWA: Initializing PWA Manager...'); - - if ('serviceWorker' in navigator) { - try { - this.swRegistration = await navigator.serviceWorker.register('/sw.js'); - console.log('SW: Service Worker registered successfully'); - - // Listen for updates - this.swRegistration.addEventListener('updatefound', () => { - console.log('SW: New version available'); - this.showUpdateNotification(); - }); - - } catch (error) { - console.log('SW: Service Worker registration failed:', error); - } - } - - // Setup PWA install prompt - this.setupInstallPrompt(); - - // Setup notifications (if enabled) - this.setupNotifications(); - - // Setup push notifications - this.setupPushNotifications(); - - // Show manual install option after 5 seconds if no prompt appeared and app not installed - setTimeout(() => { - if (!document.getElementById('pwa-install-btn') && !this.isInstalled()) { - console.log('PWA: No install prompt appeared, showing manual install guide'); - this.showManualInstallButton(); - } - }, 5000); - } - - setupInstallPrompt() { - let deferredPrompt; - - window.addEventListener('beforeinstallprompt', (e) => { - console.log('PWA: beforeinstallprompt event fired'); - // Prevent Chrome 67 and earlier from automatically showing the prompt - e.preventDefault(); - deferredPrompt = e; - - // Show custom install button - this.showInstallButton(deferredPrompt); - }); - - window.addEventListener('appinstalled', () => { - console.log('PWA: App was installed'); - this.hideInstallButton(); - }); - - // Debug: Check if app is already installed - if (this.isInstalled()) { - console.log('PWA: App is already installed (standalone mode)'); - // Hide any existing install buttons - this.hideInstallButton(); - } else { - console.log('PWA: App is not installed, waiting for install prompt...'); - console.log('PWA: Current URL:', window.location.href); - console.log('PWA: Display mode:', window.matchMedia('(display-mode: standalone)').matches ? 'standalone' : 'browser'); - console.log('PWA: User agent:', navigator.userAgent); - } - - // Periodically check if app becomes installed (for cases where user installs via browser menu) - setInterval(() => { - if (this.isInstalled()) { - this.hideInstallButton(); - } - }, 2000); - } - - showInstallButton(deferredPrompt) { - // Don't show install button if app is already installed - if (this.isInstalled()) { - console.log('PWA: App already installed, skipping install button'); - return; - } - - // Create install button - const installBtn = document.createElement('button'); - installBtn.id = 'pwa-install-btn'; - installBtn.className = 'btn btn-primary btn-sm'; - installBtn.innerHTML = ' Install App'; - installBtn.style.cssText = ` - position: fixed; - bottom: 20px; - right: 20px; - z-index: 1000; - box-shadow: 0 4px 12px rgba(37, 99, 235, 0.3); - `; - - installBtn.addEventListener('click', async () => { - if (deferredPrompt) { - deferredPrompt.prompt(); - const { outcome } = await deferredPrompt.userChoice; - console.log('PWA: User response to install prompt:', outcome); - deferredPrompt = null; - this.hideInstallButton(); - } - }); - - document.body.appendChild(installBtn); - } - - hideInstallButton() { - const installBtn = document.getElementById('pwa-install-btn'); - if (installBtn) { - installBtn.remove(); - } - } - - showUpdateNotification() { - // Show update available notification - const notification = document.createElement('div'); - notification.className = 'alert alert-info alert-dismissible'; - notification.style.cssText = ` - position: fixed; - top: 20px; - right: 20px; - z-index: 1050; - max-width: 300px; - `; - - notification.innerHTML = ` - Update Available!
- A new version of the app is ready. - - - `; - - document.body.appendChild(notification); - - // Handle update - document.getElementById('update-btn').addEventListener('click', () => { - if (this.swRegistration && this.swRegistration.waiting) { - this.swRegistration.waiting.postMessage({ type: 'SKIP_WAITING' }); - window.location.reload(); - } - }); - } - - async setupNotifications() { - // Check if notifications are supported and get permission - if ('Notification' in window) { - const permission = await this.requestNotificationPermission(); - console.log('Notifications permission:', permission); - } - } - - async requestNotificationPermission() { - if (Notification.permission === 'default') { - // Only request permission when user interacts with a relevant feature - // For now, just return the current status - return Notification.permission; - } - return Notification.permission; - } - - // Show notification (if permission granted) - showNotification(title, options = {}) { - if (Notification.permission === 'granted') { - const notification = new Notification(title, { - icon: '/icons/icon-192x192.png', - badge: '/icons/icon-72x72.png', - tag: 'littleshop-admin', - ...options - }); - - // Auto-close after 5 seconds - setTimeout(() => { - notification.close(); - }, 5000); - - return notification; - } - } - - // Show manual install button for browsers that don't auto-prompt - showManualInstallButton() { - // Don't show install button if app is already installed - if (this.isInstalled()) { - console.log('PWA: App already installed, skipping manual install button'); - return; - } - - console.log('PWA: Showing manual install button'); - const installBtn = document.createElement('button'); - installBtn.id = 'pwa-install-btn'; - installBtn.className = 'btn btn-primary btn-sm'; - installBtn.innerHTML = ' Install as App'; - installBtn.style.cssText = ` - position: fixed; - bottom: 20px; - right: 20px; - z-index: 1000; - box-shadow: 0 4px 12px rgba(37, 99, 235, 0.3); - `; - - installBtn.addEventListener('click', () => { - const isChrome = navigator.userAgent.includes('Chrome'); - const isEdge = navigator.userAgent.includes('Edge'); - const isFirefox = navigator.userAgent.includes('Firefox'); - - let instructions = 'To install this app:\\n\\n'; - - if (isChrome || isEdge) { - instructions += '1. Look for the install icon (⬇️) in the address bar\\n'; - instructions += '2. Or click the browser menu (⋮) → "Install LittleShop Admin"\\n'; - instructions += '3. Or check if there\'s an "Install app" option in the browser menu'; - } else if (isFirefox) { - instructions += '1. Firefox doesn\'t support PWA installation yet\\n'; - instructions += '2. You can bookmark this page for easy access\\n'; - instructions += '3. Or use Chrome/Edge for the full PWA experience'; - } else { - instructions += '1. Look for an install or "Add to Home Screen" option\\n'; - instructions += '2. Check your browser menu for app installation\\n'; - instructions += '3. Or bookmark this page for quick access'; - } - - alert(instructions); - }); - - document.body.appendChild(installBtn); - } - - // Check if app is installed - isInstalled() { - return window.matchMedia('(display-mode: standalone)').matches || - window.navigator.standalone === true; - } - - // Setup push notifications - async setupPushNotifications() { - if (!('serviceWorker' in navigator) || !('PushManager' in window)) { - console.log('PWA: Push notifications not supported'); - return; - } - - try { - // Get VAPID public key from server - await this.getVapidPublicKey(); - - // Check if user is already subscribed - await this.checkPushSubscription(); - - // Simple logic: only show prompt if user is not subscribed - if (!this.pushSubscription) { - // Check if we've already asked this session - if (!sessionStorage.getItem('pushNotificationPromptShown')) { - this.showPushNotificationSetup(); - sessionStorage.setItem('pushNotificationPromptShown', 'true'); - } - } else { - console.log('PWA: User already subscribed to push notifications'); - } - - } catch (error) { - console.error('PWA: Failed to setup push notifications:', error); - } - } - - async getVapidPublicKey() { - try { - const response = await fetch('/api/push/vapid-key'); - if (response.ok) { - const data = await response.json(); - this.vapidPublicKey = data.publicKey; - console.log('PWA: VAPID public key retrieved'); - } else { - throw new Error('Failed to get VAPID public key'); - } - } catch (error) { - console.error('PWA: Error getting VAPID public key:', error); - throw error; - } - } - - async checkPushSubscription() { - if (!this.swRegistration) { - return; - } - - try { - this.pushSubscription = await this.swRegistration.pushManager.getSubscription(); - if (this.pushSubscription) { - console.log('PWA: User has active push subscription'); - } else { - console.log('PWA: User is not subscribed to push notifications'); - } - } catch (error) { - console.error('PWA: Error checking push subscription:', error); - } - } - - async subscribeToPushNotifications() { - if (!this.swRegistration || !this.vapidPublicKey) { - throw new Error('Service worker or VAPID key not available'); - } - - try { - // Check current permission status - if (Notification.permission === 'denied') { - throw new Error('Notification permission was denied. Please enable notifications in your browser settings.'); - } - - // Request notification permission if not already granted - let permission = Notification.permission; - if (permission === 'default') { - permission = await Notification.requestPermission(); - } - - if (permission !== 'granted') { - throw new Error('Notification permission is required for push notifications. Please allow notifications and try again.'); - } - - // Subscribe to push notifications - const subscription = await this.swRegistration.pushManager.subscribe({ - userVisibleOnly: true, - applicationServerKey: this.urlBase64ToUint8Array(this.vapidPublicKey) - }); - - // Send subscription to server with timeout - console.log('PWA: Sending subscription to server...'); - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout - - const response = await fetch('/api/push/subscribe', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - endpoint: subscription.endpoint, - p256dh: btoa(String.fromCharCode(...new Uint8Array(subscription.getKey('p256dh')))), - auth: btoa(String.fromCharCode(...new Uint8Array(subscription.getKey('auth')))) - }), - credentials: 'same-origin', - signal: controller.signal - }); - - clearTimeout(timeoutId); - console.log('PWA: Server response received:', response.status, response.statusText); - - if (response.ok) { - this.pushSubscription = subscription; - console.log('PWA: Successfully subscribed to push notifications'); - this.hidePushNotificationSetup(); - return true; - } else { - throw new Error('Failed to save push subscription to server'); - } - } catch (error) { - console.error('PWA: Failed to subscribe to push notifications:', error); - throw error; - } - } - - async unsubscribeFromPushNotifications() { - if (!this.pushSubscription) { - return true; - } - - try { - // Unsubscribe from push manager - await this.pushSubscription.unsubscribe(); - - // Notify server - await fetch('/api/push/unsubscribe', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - endpoint: this.pushSubscription.endpoint - }), - credentials: 'same-origin' - }); - - this.pushSubscription = null; - console.log('PWA: Successfully unsubscribed from push notifications'); - return true; - } catch (error) { - console.error('PWA: Failed to unsubscribe from push notifications:', error); - throw error; - } - } - - showPushNotificationSetup() { - // Check if setup UI already exists - if (document.getElementById('push-notification-setup')) { - return; - } - - const setupDiv = document.createElement('div'); - setupDiv.id = 'push-notification-setup'; - setupDiv.className = 'alert alert-info alert-dismissible'; - setupDiv.style.cssText = ` - position: fixed; - top: 80px; - right: 20px; - z-index: 1050; - max-width: 350px; - `; - - setupDiv.innerHTML = ` -
- -
- Push Notifications
- Get notified of new orders and updates -
-
- -
-
- - `; - - document.body.appendChild(setupDiv); - - // Add event listener for subscribe button - const subscribeBtn = document.getElementById('subscribe-push-btn'); - - if (subscribeBtn) { - subscribeBtn.addEventListener('click', async () => { - subscribeBtn.disabled = true; - subscribeBtn.innerHTML = 'Subscribing...'; - - try { - // Add timeout to prevent infinite hanging - const subscriptionPromise = this.subscribeToPushNotifications(); - const timeoutPromise = new Promise((_, reject) => - setTimeout(() => reject(new Error('Push subscription timed out after 15 seconds. This may be due to network connectivity or browser push service issues.')), 15000) - ); - - await Promise.race([subscriptionPromise, timeoutPromise]); - - this.showNotification('Push notifications enabled!', { - body: 'You will now receive notifications for new orders and updates.' - }); - } catch (error) { - console.error('PWA: Push subscription failed:', error); - - // Provide user-friendly error messages - let userMessage = error.message; - if (error.message.includes('permission')) { - userMessage = 'Please allow notifications when your browser asks, then try again.'; - } else if (error.message.includes('timeout')) { - userMessage = 'Push notification setup timed out. This may be due to network or browser issues. Please try again or check your internet connection.'; - } - - alert('Failed to enable push notifications: ' + userMessage); - subscribeBtn.disabled = false; - subscribeBtn.innerHTML = 'Enable'; - } - }); - } - } - - hidePushNotificationSetup() { - const setupDiv = document.getElementById('push-notification-setup'); - if (setupDiv) { - setupDiv.remove(); - console.log('PWA: Push notification setup hidden'); - } - } - - async sendTestNotification() { - try { - const response = await fetch('/api/push/test', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - title: 'Test Notification', - body: 'This is a test push notification from LittleShop Admin!' - }), - credentials: 'same-origin' - }); - - const result = await response.json(); - if (response.ok) { - console.log('PWA: Test notification sent successfully'); - return true; - } else { - throw new Error(result.error || 'Failed to send test notification'); - } - } catch (error) { - console.error('PWA: Failed to send test notification:', error); - throw error; - } - } - - // Helper function to convert VAPID key - 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; - } -} - -// Initialize PWA Manager -const pwaManager = new PWAManager(); -window.pwaManager = pwaManager; - -// Expose notification functions globally -window.showNotification = (title, options) => pwaManager.showNotification(title, options); -window.sendTestPushNotification = () => pwaManager.sendTestNotification(); -window.subscribeToPushNotifications = () => pwaManager.subscribeToPushNotifications(); +// Progressive Web App functionality +// Handles service worker registration and PWA features + +class PWAManager { + constructor() { + this.swRegistration = null; + this.vapidPublicKey = null; + this.pushSubscription = null; + this.init(); + } + + async init() { + console.log('PWA: Initializing PWA Manager...'); + + if ('serviceWorker' in navigator) { + try { + this.swRegistration = await navigator.serviceWorker.register('/sw.js'); + console.log('SW: Service Worker registered successfully'); + + // Listen for updates + this.swRegistration.addEventListener('updatefound', () => { + console.log('SW: New version available'); + this.showUpdateNotification(); + }); + + } catch (error) { + console.log('SW: Service Worker registration failed:', error); + } + } + + // Setup PWA install prompt + this.setupInstallPrompt(); + + // Setup notifications (if enabled) + this.setupNotifications(); + + // Setup push notifications + this.setupPushNotifications(); + + // Show manual install option after 5 seconds if no prompt appeared and app not installed + setTimeout(() => { + if (!document.getElementById('pwa-install-btn') && !this.isInstalled()) { + console.log('PWA: No install prompt appeared, showing manual install guide'); + this.showManualInstallButton(); + } + }, 5000); + } + + setupInstallPrompt() { + let deferredPrompt; + + window.addEventListener('beforeinstallprompt', (e) => { + console.log('PWA: beforeinstallprompt event fired'); + // Prevent Chrome 67 and earlier from automatically showing the prompt + e.preventDefault(); + deferredPrompt = e; + + // Show custom install button + this.showInstallButton(deferredPrompt); + }); + + window.addEventListener('appinstalled', () => { + console.log('PWA: App was installed'); + this.hideInstallButton(); + }); + + // Debug: Check if app is already installed + if (this.isInstalled()) { + console.log('PWA: App is already installed (standalone mode)'); + // Hide any existing install buttons + this.hideInstallButton(); + } else { + console.log('PWA: App is not installed, waiting for install prompt...'); + console.log('PWA: Current URL:', window.location.href); + console.log('PWA: Display mode:', window.matchMedia('(display-mode: standalone)').matches ? 'standalone' : 'browser'); + console.log('PWA: User agent:', navigator.userAgent); + } + + // Periodically check if app becomes installed (for cases where user installs via browser menu) + setInterval(() => { + if (this.isInstalled()) { + this.hideInstallButton(); + } + }, 2000); + } + + showInstallButton(deferredPrompt) { + // Don't show install button if app is already installed + if (this.isInstalled()) { + console.log('PWA: App already installed, skipping install button'); + return; + } + + // Create install button + const installBtn = document.createElement('button'); + installBtn.id = 'pwa-install-btn'; + installBtn.className = 'btn btn-primary btn-sm'; + installBtn.innerHTML = ' Install App'; + installBtn.style.cssText = ` + position: fixed; + bottom: 20px; + right: 20px; + z-index: 1000; + box-shadow: 0 4px 12px rgba(37, 99, 235, 0.3); + `; + + installBtn.addEventListener('click', async () => { + if (deferredPrompt) { + deferredPrompt.prompt(); + const { outcome } = await deferredPrompt.userChoice; + console.log('PWA: User response to install prompt:', outcome); + deferredPrompt = null; + this.hideInstallButton(); + } + }); + + document.body.appendChild(installBtn); + } + + hideInstallButton() { + const installBtn = document.getElementById('pwa-install-btn'); + if (installBtn) { + installBtn.remove(); + } + } + + showUpdateNotification() { + // Show update available notification + const notification = document.createElement('div'); + notification.className = 'alert alert-info alert-dismissible'; + notification.style.cssText = ` + position: fixed; + top: 20px; + right: 20px; + z-index: 1050; + max-width: 300px; + `; + + notification.innerHTML = ` + Update Available!
+ A new version of the app is ready. + + + `; + + document.body.appendChild(notification); + + // Handle update + document.getElementById('update-btn').addEventListener('click', () => { + if (this.swRegistration && this.swRegistration.waiting) { + this.swRegistration.waiting.postMessage({ type: 'SKIP_WAITING' }); + window.location.reload(); + } + }); + } + + async setupNotifications() { + // Check if notifications are supported and get permission + if ('Notification' in window) { + const permission = await this.requestNotificationPermission(); + console.log('Notifications permission:', permission); + } + } + + async requestNotificationPermission() { + if (Notification.permission === 'default') { + // Only request permission when user interacts with a relevant feature + // For now, just return the current status + return Notification.permission; + } + return Notification.permission; + } + + // Show notification (if permission granted) + showNotification(title, options = {}) { + if (Notification.permission === 'granted') { + const notification = new Notification(title, { + icon: '/icons/icon-192x192.png', + badge: '/icons/icon-72x72.png', + tag: 'littleshop-admin', + ...options + }); + + // Auto-close after 5 seconds + setTimeout(() => { + notification.close(); + }, 5000); + + return notification; + } + } + + // Show manual install button for browsers that don't auto-prompt + showManualInstallButton() { + // Don't show install button if app is already installed + if (this.isInstalled()) { + console.log('PWA: App already installed, skipping manual install button'); + return; + } + + console.log('PWA: Showing manual install button'); + const installBtn = document.createElement('button'); + installBtn.id = 'pwa-install-btn'; + installBtn.className = 'btn btn-primary btn-sm'; + installBtn.innerHTML = ' Install as App'; + installBtn.style.cssText = ` + position: fixed; + bottom: 20px; + right: 20px; + z-index: 1000; + box-shadow: 0 4px 12px rgba(37, 99, 235, 0.3); + `; + + installBtn.addEventListener('click', () => { + const isChrome = navigator.userAgent.includes('Chrome'); + const isEdge = navigator.userAgent.includes('Edge'); + const isFirefox = navigator.userAgent.includes('Firefox'); + + let instructions = 'To install this app:\\n\\n'; + + if (isChrome || isEdge) { + instructions += '1. Look for the install icon (⬇️) in the address bar\\n'; + instructions += '2. Or click the browser menu (⋮) → "Install LittleShop Admin"\\n'; + instructions += '3. Or check if there\'s an "Install app" option in the browser menu'; + } else if (isFirefox) { + instructions += '1. Firefox doesn\'t support PWA installation yet\\n'; + instructions += '2. You can bookmark this page for easy access\\n'; + instructions += '3. Or use Chrome/Edge for the full PWA experience'; + } else { + instructions += '1. Look for an install or "Add to Home Screen" option\\n'; + instructions += '2. Check your browser menu for app installation\\n'; + instructions += '3. Or bookmark this page for quick access'; + } + + alert(instructions); + }); + + document.body.appendChild(installBtn); + } + + // Check if app is installed + isInstalled() { + return window.matchMedia('(display-mode: standalone)').matches || + window.navigator.standalone === true; + } + + // Setup push notifications + async setupPushNotifications() { + if (!('serviceWorker' in navigator) || !('PushManager' in window)) { + console.log('PWA: Push notifications not supported'); + return; + } + + try { + // Get VAPID public key from server + await this.getVapidPublicKey(); + + // Check if user is already subscribed + await this.checkPushSubscription(); + + // Simple logic: only show prompt if user is not subscribed + if (!this.pushSubscription) { + // Check if we've already asked this session + if (!sessionStorage.getItem('pushNotificationPromptShown')) { + this.showPushNotificationSetup(); + sessionStorage.setItem('pushNotificationPromptShown', 'true'); + } + } else { + console.log('PWA: User already subscribed to push notifications'); + } + + } catch (error) { + console.error('PWA: Failed to setup push notifications:', error); + } + } + + async getVapidPublicKey() { + try { + const response = await fetch('/api/push/vapid-key'); + if (response.ok) { + const data = await response.json(); + this.vapidPublicKey = data.publicKey; + console.log('PWA: VAPID public key retrieved'); + } else { + throw new Error('Failed to get VAPID public key'); + } + } catch (error) { + console.error('PWA: Error getting VAPID public key:', error); + throw error; + } + } + + async checkPushSubscription() { + if (!this.swRegistration) { + return; + } + + try { + this.pushSubscription = await this.swRegistration.pushManager.getSubscription(); + if (this.pushSubscription) { + console.log('PWA: User has active push subscription'); + } else { + console.log('PWA: User is not subscribed to push notifications'); + } + } catch (error) { + console.error('PWA: Error checking push subscription:', error); + } + } + + async subscribeToPushNotifications() { + if (!this.swRegistration || !this.vapidPublicKey) { + throw new Error('Service worker or VAPID key not available'); + } + + try { + // Check current permission status + if (Notification.permission === 'denied') { + throw new Error('Notification permission was denied. Please enable notifications in your browser settings.'); + } + + // Request notification permission if not already granted + let permission = Notification.permission; + if (permission === 'default') { + permission = await Notification.requestPermission(); + } + + if (permission !== 'granted') { + throw new Error('Notification permission is required for push notifications. Please allow notifications and try again.'); + } + + // Subscribe to push notifications with enhanced debugging + console.log('PWA: Requesting push subscription from browser...'); + console.log('PWA: VAPID public key (first 32 chars):', this.vapidPublicKey.substring(0, 32) + '...'); + + const subscriptionStartTime = Date.now(); + let subscription; + + try { + subscription = await Promise.race([ + this.swRegistration.pushManager.subscribe({ + userVisibleOnly: true, + applicationServerKey: this.urlBase64ToUint8Array(this.vapidPublicKey) + }), + new Promise((_, reject) => + setTimeout(() => reject(new Error('Browser push subscription timed out after 30 seconds. This may indicate an issue with your browser\'s push service connectivity.')), 30000) + ) + ]); + + const subscriptionTime = Date.now() - subscriptionStartTime; + console.log(`PWA: Browser subscription completed in ${subscriptionTime}ms`); + console.log('PWA: Subscription endpoint:', subscription.endpoint); + + } catch (subscriptionError) { + console.error('PWA: Browser subscription failed:', subscriptionError); + throw new Error(`Failed to subscribe with browser push service: ${subscriptionError.message}`); + } + + // Send subscription to server with timeout + console.log('PWA: Sending subscription to server...'); + const serverStartTime = Date.now(); + const controller = new AbortController(); + const timeoutId = setTimeout(() => { + controller.abort(); + console.error('PWA: Server request timeout after 15 seconds'); + }, 15000); + + const response = await fetch('/api/push/subscribe', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + endpoint: subscription.endpoint, + p256dh: btoa(String.fromCharCode(...new Uint8Array(subscription.getKey('p256dh')))), + auth: btoa(String.fromCharCode(...new Uint8Array(subscription.getKey('auth')))) + }), + credentials: 'same-origin', + signal: controller.signal + }); + + clearTimeout(timeoutId); + const serverTime = Date.now() - serverStartTime; + console.log(`PWA: Server response received in ${serverTime}ms:`, response.status, response.statusText); + + if (response.ok) { + this.pushSubscription = subscription; + console.log('PWA: Successfully subscribed to push notifications'); + this.hidePushNotificationSetup(); + return true; + } else { + throw new Error('Failed to save push subscription to server'); + } + } catch (error) { + console.error('PWA: Failed to subscribe to push notifications:', error); + throw error; + } + } + + async unsubscribeFromPushNotifications() { + if (!this.pushSubscription) { + return true; + } + + try { + // Unsubscribe from push manager + await this.pushSubscription.unsubscribe(); + + // Notify server + await fetch('/api/push/unsubscribe', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + endpoint: this.pushSubscription.endpoint + }), + credentials: 'same-origin' + }); + + this.pushSubscription = null; + console.log('PWA: Successfully unsubscribed from push notifications'); + return true; + } catch (error) { + console.error('PWA: Failed to unsubscribe from push notifications:', error); + throw error; + } + } + + showPushNotificationSetup() { + // Check if setup UI already exists + if (document.getElementById('push-notification-setup')) { + return; + } + + const setupDiv = document.createElement('div'); + setupDiv.id = 'push-notification-setup'; + setupDiv.className = 'alert alert-info alert-dismissible'; + setupDiv.style.cssText = ` + position: fixed; + top: 80px; + right: 20px; + z-index: 1050; + max-width: 350px; + `; + + setupDiv.innerHTML = ` +
+ +
+ Push Notifications
+ Get notified of new orders and updates +
+
+ +
+
+ + `; + + document.body.appendChild(setupDiv); + + // Add event listener for subscribe button + const subscribeBtn = document.getElementById('subscribe-push-btn'); + + if (subscribeBtn) { + subscribeBtn.addEventListener('click', async () => { + subscribeBtn.disabled = true; + subscribeBtn.innerHTML = 'Subscribing...'; + + try { + // Add timeout to prevent infinite hanging + const subscriptionPromise = this.subscribeToPushNotifications(); + const timeoutPromise = new Promise((_, reject) => + setTimeout(() => reject(new Error('Push subscription timed out after 15 seconds. This may be due to network connectivity or browser push service issues.')), 15000) + ); + + await Promise.race([subscriptionPromise, timeoutPromise]); + + this.showNotification('Push notifications enabled!', { + body: 'You will now receive notifications for new orders and updates.' + }); + } catch (error) { + console.error('PWA: Push subscription failed:', error); + + // Provide user-friendly error messages + let userMessage = error.message; + if (error.message.includes('permission')) { + userMessage = 'Please allow notifications when your browser asks, then try again.'; + } else if (error.message.includes('timeout')) { + userMessage = 'Push notification setup timed out. This may be due to network or browser push service issues. Please check your internet connection and try again.'; + } else if (error.message.includes('push service')) { + userMessage = 'Failed to connect to browser push service. This may be a temporary network issue. Please try again in a few moments.'; + } else if (error.message.includes('AbortError')) { + userMessage = 'Request was cancelled due to timeout. Please check your internet connection and try again.'; + } + + console.error('PWA: Full error details:', error); + alert('Failed to enable push notifications: ' + userMessage); + subscribeBtn.disabled = false; + subscribeBtn.innerHTML = 'Enable'; + } + }); + } + } + + hidePushNotificationSetup() { + const setupDiv = document.getElementById('push-notification-setup'); + if (setupDiv) { + setupDiv.remove(); + console.log('PWA: Push notification setup hidden'); + } + } + + async sendTestNotification() { + try { + const response = await fetch('/api/push/test', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + title: 'Test Notification', + body: 'This is a test push notification from LittleShop Admin!' + }), + credentials: 'same-origin' + }); + + const result = await response.json(); + if (response.ok) { + console.log('PWA: Test notification sent successfully'); + return true; + } else { + throw new Error(result.error || 'Failed to send test notification'); + } + } catch (error) { + console.error('PWA: Failed to send test notification:', error); + throw error; + } + } + + // Helper function to convert VAPID key + 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; + } +} + +// Initialize PWA Manager +const pwaManager = new PWAManager(); +window.pwaManager = pwaManager; + +// Expose notification functions globally +window.showNotification = (title, options) => pwaManager.showNotification(title, options); +window.sendTestPushNotification = () => pwaManager.sendTestNotification(); +window.subscribeToPushNotifications = () => pwaManager.subscribeToPushNotifications(); window.unsubscribeFromPushNotifications = () => pwaManager.unsubscribeFromPushNotifications(); \ No newline at end of file