/* global importScripts */ /* global idbKeyval */ /* global workbox */ /* global cfgenv */ const VITE_APP_VERSION = '1.2.87'; // Costanti di configurazione const DYNAMIC_CACHE = 'dynamic-cache-v2'; const baseUrl = self.location.origin; const CACHE_VERSION = VITE_APP_VERSION; const CACHE_PREFIX = self.location.hostname || 'app'; function extractDomain(url) { return url.replace(/^https?:\/\//, ''); } function removeTestPrefix(str) { return str.startsWith('test.') ? str.slice(5) : str; } // Funzione per determinare il dominio API function determineApiDomain(appDomain) { if (ISTEST) { return 'testapi.' + removeTestPrefix(appDomain); } return appDomain.includes('localhost') ? 'localhost:3000' : 'api.' + appDomain; } const APP_DOMAIN = extractDomain(baseUrl); const API_DOMAIN = determineApiDomain(APP_DOMAIN); console.log('API_DOMAIN', API_DOMAIN); const CACHE_NAME = 'pwa-cache-' + VITE_APP_VERSION; // Nome della cache importScripts('workbox/workbox-sw.js'); import { clientsClaim } from 'workbox-core'; import { precacheAndRoute, cleanupOutdatedCaches, createHandlerBoundToURL, } from 'workbox-precaching'; import { registerRoute, NavigationRoute } from 'workbox-routing'; import { setCacheNameDetails } from 'workbox-core'; import { NetworkOnly, NetworkFirst, StaleWhileRevalidate, CacheFirst, } from 'workbox-strategies'; import { CacheableResponsePlugin } from 'workbox-cacheable-response'; import { ExpirationPlugin } from 'workbox-expiration'; const debug = false; //process.env.NODE_ENV !== 'production'; if (workbox) { // Imposta configurazione prima di tutto workbox.setConfig({ debug }); workbox.loadModule('workbox-strategies'); console.log('Workbox ESISTE ✅ '); } else { console.error('Workbox NON CARICATO ! ❌'); } setCacheNameDetails({ prefix: self.location.hostname, suffix: 'v2', precache: 'precache', runtime: 'runtime', }); // ✅ SOLUZIONE: Sii più specifico const precacheList = (self.__WB_MANIFEST || []).filter((entry) => { const url = entry.url; // Escludi file grandi, upload, e risorse dinamiche return ( !url.includes('/upload/') && !url.includes('/assets/videos/') && !url.match(/\.(mp4|webm|zip|pdf)$/) ); }); // Precache solo i file filtrati precacheAndRoute(precacheList); cleanupOutdatedCaches(); // Installazione del Service Worker self.addEventListener('install', () => { console.log('[Service Worker] Installing ...'); self.skipWaiting(); clientsClaim(); // Notifica il frontend che c'è un nuovo SW pronto self.clients.matchAll().then((clients) => { clients.forEach((client) => { client.postMessage({ type: 'SW_UPDATED' }); }); }); }); // Attivazione del Service Worker self.addEventListener('activate', (event) => { console.log('[Service Worker] Activating...'); event.waitUntil( caches.keys().then((cacheNames) => { return Promise.all( cacheNames .filter((name) => name !== CACHE_NAME && name !== DYNAMIC_CACHE) .map((name) => caches.delete(name)) ); }) ); self.clients.claim(); }); const USASYNC = false; // PER ATTIVARE IL SYNC TOGLI L'AREA COMMENTATA DI 'FETCH' QUI SOTTO .... /* // Strategia fetch self.addEventListener('fetch', (event) => { const { request } = event; const url = new URL(request.url); // ============================================ // IMPORTANTE: NON cachare API /sync o /loadsite // ============================================ if (url.pathname.includes('/api/') || url.pathname.includes('/sync') || url.pathname.includes('/loadsite')) { // Lascia passare normalmente - IndexedDB gestisce cache return; } // ============================================ // Cache Strategy per assets statici // ============================================ if (request.method === 'GET') { event.respondWith( caches.match(request).then((cachedResponse) => { if (cachedResponse) { return cachedResponse; } return fetch(request).then((response) => { // Non cachare se non è successo if (!response || response.status !== 200 || response.type !== 'basic') { return response; } // Clona e salva in cache const responseToCache = response.clone(); caches.open(CACHE_NAME).then((cache) => { cache.put(request, responseToCache); }); return response; }).catch(() => { // Offline fallback if (request.destination === 'document') { return caches.match('/offline.html'); } }); }) ); } }); */ console.log( ' [ VER-' + VITE_APP_VERSION + ' ] _---------________------ PAO: this is my custom service worker: ' ); try { importScripts('/js/idb.js', '/js/storage.js'); console.log('Local scripts imported successfully.'); } catch (error) { console.error('Failed to import local scripts ❌:', error); } let port = self.location.hostname.startsWith('test') ? 3001 : 3000; let ISTEST = self.location.hostname.startsWith('test'); let ISLOCALE = self.location.hostname.startsWith('localhost'); console.log('SW- app ver ' + VITE_APP_VERSION); // Function helpers async function writeData(table, data) { console.log('writeData', table, data); await idbKeyval.setdata(table, data); } async function readAllData(table) { return idbKeyval.getalldata(table); } async function clearAllData(table) { await idbKeyval.clearalldata(table); } async function deleteItemFromData(table, id) { await idbKeyval.deletedata(table, id); } if (workbox) { /*if (process.env.MODE !== 'ssr' || process.env.PROD) { registerRoute( new NavigationRoute( createHandlerBoundToURL(process.env.PWA_FALLBACK_HTML), { denylist: [new RegExp(process.env.PWA_SERVICE_WORKER_REGEX), /workbox\workbox-(.)*\.js$/] } ) ) }*/ // Static assets (JS, CSS, Fonts) - CacheFirst: caricamento rapidissimo registerRoute( ({ request }) => ['script', 'style', 'font'].includes(request.destination), new CacheFirst({ cacheName: `${CACHE_PREFIX}-static-assets-${CACHE_VERSION}`, plugins: [ new CacheableResponsePlugin({ statuses: [0, 200] }), new ExpirationPlugin({ maxEntries: 50, maxAgeSeconds: 30 * 24 * 60 * 60 }), // 30 giorni ], }) ); // Immagini - CacheFirst con scadenza e limite registerRoute( ({ request }) => request.destination === 'image', new CacheFirst({ cacheName: `${CACHE_PREFIX}-images-${CACHE_VERSION}`, plugins: [ new CacheableResponsePlugin({ statuses: [0, 200] }), new ExpirationPlugin({ maxEntries: 60, maxAgeSeconds: 30 * 24 * 60 * 60 }), // 30 giorni ], }) ); // Google Fonts - StaleWhileRevalidate per aggiornamenti trasparenti registerRoute( /^https:\/\/fonts\.(?:googleapis|gstatic)\.com/, new StaleWhileRevalidate({ cacheName: `${CACHE_PREFIX}-google-fonts-${CACHE_VERSION}`, plugins: [ new CacheableResponsePlugin({ statuses: [0, 200] }), new ExpirationPlugin({ maxEntries: 30 }), ], }) ); // HTML documents - NetworkFirst: garantisce contenuti aggiornati e fallback cache registerRoute( ({ request }) => request.destination === 'document', new NetworkFirst({ cacheName: `${CACHE_PREFIX}-html-cache-${CACHE_VERSION}`, networkTimeoutSeconds: 10, // timeout rapido plugins: [ new CacheableResponsePlugin({ statuses: [0, 200] }), new ExpirationPlugin({ maxEntries: 50, maxAgeSeconds: 24 * 60 * 60 }), // 1 giorno ], }) ); // API calls - NetworkFirst con timeout breve registerRoute( ({ url }) => url.hostname === API_DOMAIN, new NetworkFirst({ cacheName: `${CACHE_PREFIX}-api-cache-${CACHE_VERSION}`, networkTimeoutSeconds: 5, plugins: [ new CacheableResponsePlugin({ statuses: [0, 200] }), new ExpirationPlugin({ maxEntries: 100, maxAgeSeconds: 5 * 60 }), // 5 minuti ], }) ); } // Generate UUID per sync function generateUUID() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { const r = (Math.random() * 16) | 0, v = c === 'x' ? r : (r & 0x3) | 0x8; return v.toString(16); }); } let syncStore = {}; // Message event handler self.addEventListener('message', (event) => { if (event.data && event.data.type === 'SKIP_WAITING') { self.skipWaiting(); // Opzionale: rispondi al client if (event.ports && event.ports[0]) { event.ports[0].postMessage({ success: true }); } } if (event.data.type === 'sync') { console.log('addEventListener - message'); const id = generateUUID(); syncStore[id] = event.data; self.registration.sync.register(id); } console.log(event.data); }); // Funzione per gestire richieste API async function handleApiRequest(request) { try { const response = await fetch(request); // Se la risposta non è valida, restituisci un errore personalizzato if (!response.ok) { console.warn('[SW] API Response Error:', response.status, response.statusText); return new Response( JSON.stringify({ error: 'API error', message: `❌ Invalid response from API: ${response.status} ${response.statusText}`, }), { status: response.status, headers: { 'Content-Type': 'application/json' } } ); } return response; } catch (error) { console.error('[Service Worker] API request error ❌:', error); // Restituisci una risposta di errore personalizzata return new Response( JSON.stringify({ error: 'Network error', message: '❌ Unable to fetch from API: ' + error.message, }), { status: 503, headers: { 'Content-Type': 'application/json' } } ); } } // Funzione per effettuare una richiesta di rete e memorizzare nella cache async function fetchAndCache(request) { const cache = await caches.open(DYNAMIC_CACHE); try { const response = await fetch(request); // Clona e salva la risposta nella cache solo se valida if (response.ok) { const responseClone = response.clone(); cache.put(request, responseClone); } return response; } catch (error) { console.error('[SW] Fetch and cache error ❌:', error); throw error; } } // Strategia di caching: stale-while-revalidate async function cacheWithStaleWhileRevalidate(request, event) { const cache = await caches.open(DYNAMIC_CACHE); // Prova a recuperare la risorsa dalla cache const cachedResponse = await cache.match(request); if (cachedResponse) { // Aggiorna in background mentre restituisci la risposta in cache event.waitUntil( fetchAndCache(request).catch((error) => { console.error('[SW] Background fetch and cache error ❌:', error); }) ); return cachedResponse; } // Se non è in cache, fai la richiesta di rete try { return await fetchAndCache(request); } catch (error) { console.error('[SW] Cache miss and network error ❌:', error); // Restituisci una risposta di fallback personalizzata return new Response( JSON.stringify({ error: 'Network error', message: 'Unable to fetch resource from network or cache.', }), { status: 503, headers: { 'Content-Type': 'application/json' } } ); } } // Listener per gestire tutte le richieste /*self.addEventListener('fetch', (event) => { const { request } = event; const url = new URL(request.url); try { // Ignora richieste non gestibili if (request.method !== 'GET' || url.protocol !== 'https:') { return; } // Ignora richieste per file di sviluppo (es. /src/) if (url.pathname.startsWith('/src/') || url.search.includes('vue&type')) { return; } // Gestione richieste API if (url.hostname === API_DOMAIN) { if (debug) console.log("E' una RICHIESTA API!"); event.respondWith(handleApiRequest(request)); return; } // Gestione risorse statiche e altre richieste if (debug) console.log("E' una RICHIESTA statica..."); event.respondWith(cacheWithStaleWhileRevalidate(request, event)); } catch (error) { console.error('[Service Worker] Fetch error ❌:', error); } }); */ // Gestione degli errori non catturati self.addEventListener('unhandledrejection', (event) => { console.error('[Service Worker] Unhandled rejection ❌:', event.reason); }); // Gestione degli errori globali self.addEventListener('error', (event) => { console.error('[Service Worker] Global error ❌:', event.error); }); // Background Sync event self.addEventListener('sync', (event) => { console.log('[Service Worker V5] Background syncing', event); let mystrparam = event.tag; let multiparams = mystrparam.split('|'); if (multiparams && multiparams.length > 3) { let [cmd, table, method, token, refreshToken, browser_random] = multiparams; if (cmd === 'sync-todos') { console.log('[Service Worker] Syncing', cmd, table, method); const headers = new Headers(); headers.append('content-Type', 'application/json'); headers.append('Accept', 'application/json'); headers.append('x-auth', token); headers.append('x-refrtok', refreshToken); headers.append('x-browser-random', browser_random); event.waitUntil( readAllData(table).then((alldata) => { const myrecs = [...alldata]; let errorfromserver = false; if (myrecs) { let promiseChain = Promise.resolve(); for (let rec of myrecs) { //TODO: Risistemare con calma... per ora non penso venga usato... let link = cfgenv.serverweb + '/todos'; if (method !== 'POST') link += '/' + rec._id; promiseChain = promiseChain.then(() => fetch(link, { method: method, headers: headers, cache: 'no-cache', mode: 'cors', body: JSON.stringify(rec), }) .then(() => { deleteItemFromData(table, rec._id); deleteItemFromData('swmsg', mystrparam); }) .catch((err) => { if (err.message === 'Failed to fetch') { errorfromserver = true; } }) ); } return promiseChain.then(() => { const mystate = !errorfromserver ? 'online' : 'offline'; writeData('config', { _id: 2, stateconn: mystate }); }); } }) ); } } // Sync per notifiche (se necessario) if (event.tag === 'sync-notifications') { event.waitUntil( Promise.resolve().then(() => { console.log('[SW] Syncing notifications'); }) ); } }); // ======================================== // 🔔 PUSH NOTIFICATIONS - BACKWARD COMPATIBLE VERSION // ======================================== // Push event - BACKWARD COMPATIBLE con formato vecchio E nuovo self.addEventListener('push', (event) => { console.log('[Service Worker] 🔔 Push notification received:', event); // Default data - supporta ENTRAMBI i formati (vecchio e nuovo) let data = { title: 'New!', body: 'Something new happened!', content: 'Something new happened!', // VECCHIO formato icon: '/images/android-chrome-192x192.png', // VECCHIO path badge: '/images/badge-96x96.png', // VECCHIO path tag: 'default', url: '/', requireInteraction: false, notificationType: null, rideId: null, fromUser: null, actions: [] }; // Parse del payload se presente if (event.data) { try { const payload = event.data.json(); data = { ...data, ...payload }; // BACKWARD COMPATIBILITY: Se c'è 'content' ma non 'body', usa 'content' if (payload.content && !payload.body) { data.body = payload.content; } // Se c'è 'body' ma non 'content', sincronizza if (payload.body && !payload.content) { data.content = payload.body; } console.log('[SW] 📦 Parsed notification payload:', data); } catch (e) { console.warn('[SW] ⚠️ Failed to parse push payload as JSON, using text'); const textData = event.data.text(); data.body = textData; data.content = textData; } } // Configura le actions SOLO se c'è un notificationType (nuovo formato) if (data.notificationType && (!data.actions || data.actions.length === 0)) { data.actions = getNotificationActions(data.notificationType, data.rideId); } // Opzioni della notifica - supporta ENTRAMBI i path delle icone const options = { body: data.body || data.content, // Fallback a 'content' se 'body' non c'è icon: data.icon, badge: data.badge, tag: data.tag, requireInteraction: data.requireInteraction || false, vibrate: data.vibrate || [200, 100, 200], data: { dateOfArrival: Date.now(), url: data.url || '/', rideId: data.rideId, notificationType: data.notificationType, fromUser: data.fromUser, // BACKWARD COMPATIBILITY: mantieni anche il formato vecchio legacyFormat: !data.notificationType // true se è formato vecchio }, actions: data.actions || [] }; // Mostra la notifica event.waitUntil( self.registration.showNotification(data.title, options).then(() => { console.log('[SW] ✅ Notification displayed successfully'); // Salva la notifica in IndexedDB - BACKWARD COMPATIBLE const myid = data.id || generateUUID(); self.registration.sync.register(myid); // Formato compatibile: se è vecchio formato usa solo _id e tag const notificationData = options.data.legacyFormat ? { _id: myid, tag: options.tag } : { _id: myid, tag: options.tag, type: data.notificationType, timestamp: Date.now() }; writeData('notifications', notificationData).catch(err => { console.error('[SW] Failed to save notification to IndexedDB:', err); }); }) ); }); // Funzione helper per generare actions (SOLO per nuovo formato) function getNotificationActions(notificationType, rideId) { const actions = []; switch (notificationType) { case 'newRideRequest': actions.push( { action: 'view-requests', title: '👀 Visualizza richieste' }, { action: 'dismiss', title: '❌ Ignora' } ); break; case 'requestAccepted': actions.push( { action: 'view-ride', title: '🚗 Vedi viaggio' }, { action: 'message', title: '💬 Messaggio' } ); break; case 'requestRejected': actions.push( { action: 'search-rides', title: '🔍 Cerca altri viaggi' } ); break; case 'newMessage': actions.push( { action: 'open-chat', title: '💬 Apri chat' }, { action: 'dismiss', title: '✓ OK' } ); break; case 'rideReminder': actions.push( { action: 'view-ride', title: '📍 Vedi dettagli' }, { action: 'dismiss', title: '✓ OK' } ); break; case 'rideCancelled': actions.push( { action: 'search-rides', title: '🔍 Cerca alternativa' } ); break; case 'feedbackRequest': actions.push( { action: 'leave-feedback', title: '⭐ Lascia feedback' }, { action: 'dismiss', title: 'Dopo' } ); break; case 'newFeedbackReceived': actions.push( { action: 'view-feedback', title: '⭐ Vedi feedback' } ); break; default: actions.push( { action: 'open-app', title: '📱 Apri app' } ); } return actions; } // Notification click event - BACKWARD COMPATIBLE self.addEventListener('notificationclick', (event) => { console.log('[Service Worker] 🖱️ Notification clicked:', event); console.log('[SW] Action:', event.action); console.log('[SW] Notification data:', event.notification.data); // Chiudi la notifica event.notification.close(); // Estrai i dati dalla notifica const notificationData = event.notification.data || {}; const isLegacyFormat = notificationData.legacyFormat || false; let finalUrl; // BACKWARD COMPATIBILITY: Se è formato vecchio, usa solo notification.data.url if (isLegacyFormat || !notificationData.notificationType) { console.log('[SW] 📜 Using legacy notification format'); finalUrl = notificationData.url || '/'; } else { // NUOVO formato: usa il routing intelligente console.log('[SW] 🆕 Using new notification format with smart routing'); const rideId = notificationData.rideId; const notificationType = notificationData.notificationType; const action = event.action; finalUrl = determineUrlFromNotification(action, notificationType, rideId); } console.log('[SW] 🎯 Navigating to:', finalUrl); // Se finalUrl è null (action 'dismiss'), non fare nulla if (!finalUrl) { console.log('[SW] Action dismissed, no navigation'); return; } // Apri l'URL appropriato event.waitUntil( clients.matchAll({ type: 'window', includeUncontrolled: true }) .then((clientList) => { console.log('[SW] Found', clientList.length, 'open windows'); // Cerca una finestra già aperta con l'URL desiderato for (const client of clientList) { if (client.url.includes(finalUrl.split('?')[0]) && 'focus' in client) { console.log('[SW] ✅ Focusing existing window'); return client.focus(); } } // Cerca una finestra dell'app aperta e naviga lì for (const client of clientList) { if (client.url.includes(self.location.origin) && 'navigate' in client) { console.log('[SW] ✅ Navigating existing window to:', finalUrl); return client.navigate(finalUrl).then(client => client.focus()); } } // Altrimenti apri una nuova finestra if (clients.openWindow) { console.log('[SW] 🆕 Opening new window'); return clients.openWindow(finalUrl); } }) .catch((error) => { console.error('[SW] ❌ Error handling notification click:', error); }) ); }); // Funzione per determinare l'URL (SOLO per nuovo formato) function determineUrlFromNotification(action, notificationType, rideId) { // Gestisci prima le actions specifiche if (action) { switch (action) { case 'view-requests': return '/trasporti/miei-viaggi?tab=requests'; case 'view-ride': return rideId ? `/trasporti/viaggio/${rideId}` : '/trasporti/miei-viaggi'; case 'message': case 'open-chat': return rideId ? `/trasporti/chat?ride=${rideId}` : '/trasporti/chat'; case 'search-rides': return '/trasporti?view=search'; case 'leave-feedback': return rideId ? `/trasporti/feedback/${rideId}` : '/trasporti/feedback'; case 'view-feedback': return '/trasporti/miei-feedback'; case 'open-app': return '/trasporti'; case 'dismiss': return null; // Non fare nulla default: break; } } // Se non c'è action, usa il tipo di notifica if (notificationType) { switch (notificationType) { case 'newRideRequest': return '/trasporti/miei-viaggi?tab=requests'; case 'requestAccepted': case 'requestRejected': case 'rideReminder': return rideId ? `/trasporti/viaggio/${rideId}` : '/trasporti/miei-viaggi'; case 'newMessage': return '/trasporti/chat'; case 'rideCancelled': return '/trasporti/miei-viaggi?tab=cancelled'; case 'feedbackRequest': return rideId ? `/trasporti/feedback/${rideId}` : '/trasporti/feedback'; case 'newFeedbackReceived': return '/trasporti/miei-feedback'; default: return '/trasporti'; } } // Fallback return '/trasporti'; } // Notification close event - tracking opzionale self.addEventListener('notificationclose', (event) => { console.log('[Service Worker] 🔕 Notification closed:', event); const notificationData = event.notification.data || {}; // Solo per nuovo formato, traccia le dismissioni if (!notificationData.legacyFormat && notificationData.notificationType) { writeData('notification-dismissals', { _id: generateUUID(), type: notificationData.notificationType, timestamp: Date.now(), tag: event.notification.tag }).catch(err => { console.error('[SW] Failed to track notification dismissal:', err); }); } }); console.log('***** 🚀 CUSTOM-SERVICE-WORKER.JS - BACKWARD COMPATIBLE VERSION ***** ');