// 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) => { let data; try { data = event.data.json(); } catch (error) { // If the data is not JSON, try to get it as text data = { message: event.data ? event.data.text() : "New notification", additionalData: {} }; } // Format transaction type for display let messageText = data.message || "New transaction"; // Default notification options const options = { body: messageText, icon: "/icons/icon-192px.jpg", badge: "/icons/icon-192px.jpg", vibrate: [100, 50, 100], data: data.additionalData || {}, actions: [ { action: 'view', title: 'View Details' } ], // Tag transactions by ID to prevent duplicate notifications tag: data.additionalData?.transactionId || 'transaction' }; // Show different icons based on transaction type if (data.additionalData?.type === 'income') { options.icon = "/icons/income-icon.jpg"; } else if (data.additionalData?.type === 'expense') { options.icon = "/icons/expense-icon.jpg"; } console.log('Showing notification:', messageText); // Forward to any open clients forwardNotificationToClients(data); // Show system notification event.waitUntil(self.registration.showNotification("Budget Manager", options)); }); // Notification click handler self.addEventListener("notificationclick", (event) => { event.notification.close(); // Get transaction ID from notification data const transactionId = event.notification.data?.transactionId; // Handle different notification actions if (event.action === 'view' && transactionId) { // Open transaction details page event.waitUntil( clients.matchAll({type: 'window'}) .then(clientList => { // Check if there is already a window open for (const client of clientList) { if (client.url.includes('/') && 'focus' in client) { client.navigate(`/edit/${transactionId}`); return client.focus(); } } // If no window is open, open a new one return clients.openWindow(`/edit/${transactionId}`); }) ); } else { // Default: open the app event.waitUntil( clients.matchAll({type: 'window'}) .then(clientList => { for (const client of clientList) { if (client.url.includes('/') && 'focus' in client) { return client.focus(); } } return clients.openWindow('/'); }) ); } }); // Forward push notifications to all clients function forwardNotificationToClients(data) { self.clients.matchAll().then(clients => { clients.forEach(client => { // Send message to client client.postMessage({ type: 'PUSH_NOTIFICATION', title: 'Budget Manager', body: data.message, transactionId: data.additionalData?.transactionId, type: data.additionalData?.type }); }); }); } // Listen for messages from clients self.addEventListener('message', (event) => { if (event.data && event.data.type === 'GET_AUTH_TOKEN') { // Token request from service worker self.clients.matchAll().then(clients => { if (clients.length > 0) { const token = localStorage.getItem('token'); event.ports[0].postMessage({ token }); } }); } }); // 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]); }); }