diff --git a/src/routes/api/sendPushSubscription/+server.ts b/src/routes/api/sendPushSubscription/+server.ts index bbb5a005..75d10f67 100644 --- a/src/routes/api/sendPushSubscription/+server.ts +++ b/src/routes/api/sendPushSubscription/+server.ts @@ -1,4 +1,3 @@ -// server endpoints file import type { RequestHandler } from '@sveltejs/kit'; import webPush from 'web-push'; @@ -13,10 +12,10 @@ webPush.setVapidDetails( export const POST: RequestHandler = async ({ request, locals }) => { const { pb, apiKey } = locals; - const { title, body, key } = await request?.json(); + // Extract 'url' from the request body + const { title, body, key, url } = await request?.json(); if (apiKey !== key) { - console.warn('Invalid API key'); return new Response(JSON.stringify({ success: false, error: 'Invalid API key' }), { status: 401 }); } @@ -24,7 +23,6 @@ export const POST: RequestHandler = async ({ request, locals }) => { const subscriptions = await pb.collection('pushSubscription').getFullList({ sort: '-created' }); if (!subscriptions.length) { - console.warn('No subscriptions found.'); return new Response(JSON.stringify({ success: false, error: 'No subscriptions found' }), { status: 404 }); } @@ -32,23 +30,21 @@ export const POST: RequestHandler = async ({ request, locals }) => { try { const subscriptionData = subRecord.subscription?.subscription; - if (!subscriptionData || !subscriptionData.endpoint) { + if (!subscriptionData?.endpoint) { console.warn(`Skipping invalid subscription: ${subRecord.id}`); return; } - // Send just the body text for Android - const isAndroid = !subscriptionData.endpoint.includes('web.push.apple.com'); - const payload = isAndroid ? body : JSON.stringify({ title, body }); + // Always send JSON payload with title, body, and url + const payload = JSON.stringify({ title, body, url }); await webPush.sendNotification(subscriptionData, payload); - console.log(`Notification sent to: ${subscriptionData.endpoint}`); + //console.log(`Notification sent to: ${subscriptionData.endpoint}`); } catch (error: any) { console.error(`Error sending notification to ${subRecord.id}:`, error); if (error.statusCode === 410 || error.statusCode === 404) { - console.warn(`Deleting invalid subscription: ${subRecord.id}`); await pb.collection('pushSubscription').delete(subRecord.id); } } diff --git a/src/routes/profile/+page.svelte b/src/routes/profile/+page.svelte index 5dc088b4..1833ef6c 100644 --- a/src/routes/profile/+page.svelte +++ b/src/routes/profile/+page.svelte @@ -252,7 +252,7 @@ > - {#if pwaInstalled} + {#if !pwaInstalled}
diff --git a/src/service-worker.ts b/src/service-worker.ts index cf75c382..cb373398 100644 --- a/src/service-worker.ts +++ b/src/service-worker.ts @@ -5,43 +5,78 @@ declare let self: ServiceWorkerGlobalScope; import { build, files, version } from "$service-worker"; +// Define cache name and assets const CACHE = `cache-${version}`; const ASSETS = [...build, ...files]; +// Function to generate icon paths function getIconPath(size: string) { return new URL(`/pwa-${size}.png`, self.location.origin).href; } +// Define icons for notifications const ICONS = { DEFAULT: getIconPath('192x192'), SMALL: getIconPath('64x64'), LARGE: getIconPath('512x512') }; -// Previous cache-related event listeners remain the same... +// Install event: Cache static assets +self.addEventListener('install', (event) => { + event.waitUntil( + caches.open(CACHE) + .then((cache) => cache.addAll(ASSETS)) + .then(() => self.skipWaiting()) + ); +}); +// Activate event: Clean up old caches +self.addEventListener('activate', (event) => { + event.waitUntil( + caches.keys().then((keys) => { + return Promise.all( + keys.map((key) => { + if (key !== CACHE) { + return caches.delete(key); + } + }) + ); + }).then(() => self.clients.claim()) + ); +}); + +// Fetch event: Serve cached assets or fetch from network +self.addEventListener('fetch', (event) => { + if (event.request.method !== 'GET') return; + + event.respondWith( + caches.match(event.request).then((cachedResponse) => { + return cachedResponse || fetch(event.request); + }) + ); +}); + +// Push event: Handle incoming push notifications self.addEventListener('push', (event: PushEvent) => { if (!event.data) return; let title = 'Stocknear'; let body: string; - + let url = '/'; // Default URL + try { - // Try to get the payload as text first const payload = event.data.text(); try { - // Try to parse as JSON const jsonData = JSON.parse(payload); if (jsonData.title) { title = jsonData.title; body = jsonData.body; + url = jsonData.url || '/'; // Extract URL from payload } else { - // If no title in JSON, use the entire payload as body body = payload; } } catch { - // If JSON parsing fails, use the payload as body body = payload; } } catch { @@ -58,56 +93,48 @@ self.addEventListener('push', (event: PushEvent) => { renotify: true, vibrate: [200, 100, 200], data: { - suppressNotificationFrom: true // Custom flag to indicate branding should be suppressed + suppressNotificationFrom: true, // Custom flag to indicate branding should be suppressed + url: url // Store URL in notification data } }; - event.waitUntil( - self.registration.showNotification(title, options) - ); + event.waitUntil(self.registration.showNotification(title, options)); }); -/** - * Notification click handler - * Handles user interaction with notifications - */ + +// Notification click event: Handle user interaction with notifications self.addEventListener('notificationclick', (event) => { event.notification.close(); - const urlToOpen = new URL('/', self.location.origin).href; + // Get the URL from notification data or use default + const urlPath = event.notification.data.url || '/'; + const urlToOpen = new URL(urlPath, self.location.origin).href; - const promiseChain = clients.matchAll({ - type: 'window', - includeUncontrolled: true - }) - .then((windowClients) => { - // Focus existing window if available - for (let i = 0; i < windowClients.length; i++) { - const client = windowClients[i]; - if (client.url === urlToOpen && 'focus' in client) { - return client.focus(); - } - } - // Open new window if necessary - if (clients.openWindow) { - return clients.openWindow(urlToOpen); - } - }); - - event.waitUntil(promiseChain); + event.waitUntil( + clients.matchAll({ type: 'window', includeUncontrolled: true }) + .then((windowClients) => { + // Check for existing matching window + for (const client of windowClients) { + if (client.url === urlToOpen && 'focus' in client) { + return client.focus(); + } + } + // Open new window if none found + if (clients.openWindow) { + return clients.openWindow(urlToOpen); + } + }) + ); }); -/** - * Message handler - * Processes messages from the main thread - */ -self.addEventListener("message", (event) => { +// Message event: Handle messages from the main thread +self.addEventListener('message', (event) => { // Handle skip waiting - if (event.data && event.data.type === "SKIP_WAITING") { + if (event.data && event.data.type === 'SKIP_WAITING') { self.skipWaiting(); } - + // Handle cache update - if (event.data && event.data.type === "CACHE_URLS") { + if (event.data && event.data.type === 'CACHE_URLS') { event.waitUntil( caches.open(CACHE) .then((cache) => cache.addAll(event.data.payload))