/** * Composable for Web Push Notifications * Handles permission requests and showing notifications for new orders */ export const usePushNotifications = () => { const isSupported = ref(false) const permission = ref('default') const isEnabled = ref(false) const swRegistration = ref(null) // Check if Push Notifications are supported const checkSupport = async () => { if (process.client) { isSupported.value = 'Notification' in window && 'serviceWorker' in navigator if (isSupported.value && Notification.permission) { permission.value = Notification.permission isEnabled.value = Notification.permission === 'granted' } // Use existing service worker registration (registered globally by plugin) if ('serviceWorker' in navigator) { try { // Wait for existing registration or get current one const registration = await navigator.serviceWorker.ready swRegistration.value = registration console.log('[Push] ✅ Using existing Service Worker registration:', registration.scope) // If permission already granted, ensure subscription is active if (Notification.permission === 'granted') { const existingSub = await registration.pushManager.getSubscription() if (!existingSub) { console.log('[Push] Permission granted but no subscription - resubscribing...') await subscribeToPush() } else { console.log('[Push] ✅ Existing push subscription found') } } } catch (error) { console.error('[Push] ❌ Service Worker not available:', error) } } } } // Helper to convert base64 to Uint8Array for VAPID key const urlBase64ToUint8Array = (base64String: string) => { 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 } // Subscribe user to push notifications with VAPID const subscribeToPush = async () => { if (!swRegistration.value) { console.error('[Push] Service Worker not registered') return null } try { // Get VAPID public key from runtime config const config = useRuntimeConfig() const vapidPublicKey = config.public.vapidPublicKey if (!vapidPublicKey) { console.error('[Push] VAPID public key not configured') return null } // Subscribe to push notifications const subscription = await swRegistration.value.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: urlBase64ToUint8Array(vapidPublicKey) }) console.log('[Push] ✅ Push subscription created:', subscription) // Save subscription to server const response = await $fetch('/api/push/subscribe', { method: 'POST', body: { subscription: subscription.toJSON() } }) if (response.success) { console.log('[Push] ✅ Subscription saved to server') } else { console.error('[Push] ❌ Failed to save subscription:', response.error) } return subscription } catch (error) { console.error('[Push] ❌ Error subscribing to push:', error) return null } } // Request permission for notifications const requestPermission = async () => { if (!isSupported.value) { console.warn('[Push] Notifications not supported in this browser') return false } try { const result = await Notification.requestPermission() permission.value = result isEnabled.value = result === 'granted' if (result === 'granted') { console.log('[Push] Notification permission granted') // Subscribe to push notifications with VAPID await subscribeToPush() } else if (result === 'denied') { console.warn('[Push] Notification permission denied') } return result === 'granted' } catch (error) { console.error('[Push] Error requesting notification permission:', error) return false } } // Show a notification using Service Worker (works even when page is hidden) const showNotification = async (title: string, options?: NotificationOptions) => { if (!isEnabled.value) { console.warn('[Push] Notifications not enabled, permission:', permission.value) return } try { // Check if page is visible const isPageVisible = document.visibilityState === 'visible' console.log('[Push] Attempting to show notification:', { title, isPageVisible, swRegistered: !!swRegistration.value }) // Always show notification using Service Worker (works in background) if (swRegistration.value && 'showNotification' in swRegistration.value) { await swRegistration.value.showNotification(title, { icon: '/favicon.ico', badge: '/favicon.ico', vibrate: [200, 100, 200], tag: 'order-notification', renotify: true, requireInteraction: false, silent: false, ...options, data: { url: '/sales/orders', timestamp: Date.now() } }) console.log('[Push] ✅ Service Worker notification shown:', title) } else { // Fallback to regular notification if SW not available console.log('[Push] Using fallback Notification API') const notification = new Notification(title, { icon: '/favicon.ico', badge: '/favicon.ico', vibrate: [200, 100, 200], tag: 'order-notification', renotify: true, requireInteraction: false, ...options }) notification.onclick = () => { window.focus() notification.close() } console.log('[Push] ✅ Fallback notification shown:', title) } } catch (error) { console.error('[Push] ❌ Error showing notification:', error) } } // Show notification for new orders const notifyNewOrders = (count: number) => { if (count === 0) return const title = count === 1 ? '🔔 New Order Received' : `🔔 ${count} New Orders Received` const body = count === 1 ? 'You have 1 new order waiting to be processed' : `You have ${count} new orders waiting to be processed` showNotification(title, { body, icon: '/favicon.ico', badge: '/favicon.ico', // Add action buttons (supported on Android Chrome/Desktop) actions: [ { action: 'view', title: '👁️ View Orders', icon: '/favicon.ico' }, { action: 'dismiss', title: '✕ Dismiss', icon: '/favicon.ico' } ], // Optional: Add a large image (you can replace with order thumbnail) // image: '/images/order-notification-banner.png', dir: 'auto', lang: 'en', timestamp: Date.now() } as any) } // Initialize on mount onMounted(async () => { await checkSupport() }) return { isSupported, permission, isEnabled, requestPermission, showNotification, notifyNewOrders } }