// Service Worker for Budget Manager PWA const CACHE_NAME = "budget-manager-v1"; // Assets to precache const precacheResources = [ "/", "/index.html", "/src/main.ts", "/src/App.vue", "/styles/tailwind.css", "/styles/transitions.css", "/styles/style.css", "/icons/icon-192px.jpg", "/icons/icon-512px.jpg", ]; // Install event - precache assets self.addEventListener("install", (event) => { console.log("Service worker install event"); event.waitUntil( caches .open(CACHE_NAME) .then((cache) => { return cache.addAll(precacheResources); }) .then(() => { // Skip waiting to activate immediately return self.skipWaiting(); }), ); }); // Activate event - clean up old caches self.addEventListener("activate", (event) => { console.log("Service worker activate event"); event.waitUntil( caches .keys() .then((cacheNames) => { return Promise.all( cacheNames.map((cacheName) => { if (cacheName !== CACHE_NAME) { console.log("Deleting old cache:", cacheName); return caches.delete(cacheName); } }), ); }) .then(() => { // Take control of all clients return self.clients.claim(); }), ); }); // Fetch event - serve from cache first, then network self.addEventListener("fetch", (event) => { // Skip cross-origin requests if ( !event.request.url.startsWith(self.location.origin) || event.request.url.includes("/api/") ) { return; } event.respondWith( caches.match(event.request).then((cachedResponse) => { if (cachedResponse) { return cachedResponse; } return fetch(event.request) .then((response) => { // Cache successful responses if ( response && response.status === 200 && response.type === "basic" ) { const responseToCache = response.clone(); caches.open(CACHE_NAME).then((cache) => { cache.put(event.request, responseToCache); }); } return response; }) .catch(() => { // If fetch fails (offline), return a fallback if (event.request.mode === "navigate") { return caches.match("/"); } return null; }); }), ); }); // Background sync for offline transactions self.addEventListener("sync", (event) => { if (event.tag === "sync-transactions") { event.waitUntil(syncTransactions()); } else if (event.tag === "sync-categories") { event.waitUntil(syncCategories()); } }); // Push notification handler self.addEventListener("push", (event) => { const data = event.data.json(); const options = { body: data.body, icon: "/icons/icon-192px.jpg", badge: "/icons/badge-96x96.png", data: data.data, actions: data.actions || [], }; event.waitUntil(self.registration.showNotification(data.title, options)); }); // Notification click handler self.addEventListener("notificationclick", (event) => { event.notification.close(); event.waitUntil(clients.openWindow("/")); }); // Sync transactions with the server async function syncTransactions() { try { const pendingTransactions = await getPendingItems("pendingTransactions"); const token = await getAuthToken(); if (!token || pendingTransactions.length === 0) return; for (const transaction of pendingTransactions) { const response = await fetch("http://localhost:3000/api/transactions", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, body: JSON.stringify(transaction), }); if (response.ok) { await removePendingItem("pendingTransactions", transaction.id); } } } catch (error) { console.error("Error syncing transactions:", error); } } // Sync categories with the server async function syncCategories() { try { const pendingCategories = await getPendingItems("pendingCategories"); const token = await getAuthToken(); if (!token || pendingCategories.length === 0) return; for (const category of pendingCategories) { const response = await fetch("http://localhost:3000/api/categories", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, body: JSON.stringify(category), }); if (response.ok) { await removePendingItem("pendingCategories", category.id); } } } catch (error) { console.error("Error syncing categories:", error); } } // Helper function to get pending items from IndexedDB async function getPendingItems(storeName) { return []; // Placeholder - in a real app, this would access IndexedDB } // Helper function to remove a pending item from IndexedDB async function removePendingItem(storeName, id) { // Placeholder - in a real app, this would access IndexedDB } // Helper function to get auth token from client async function getAuthToken() { const clients = await self.clients.matchAll(); if (clients.length === 0) { return localStorage.getItem("token"); } // Ask client for token const client = clients[0]; return new Promise((resolve) => { const channel = new MessageChannel(); channel.port1.onmessage = (event) => { resolve(event.data.token); }; client.postMessage({ type: "GET_AUTH_TOKEN" }, [channel.port2]); }); }