Implement multi-channel notification system with: - Core notification service with email, push, and in-app channels - Email templates for all notification types (welcome, plant registered, transport alerts, farm alerts, harvest ready, demand matches, weekly digest) - Push notification support with VAPID authentication - In-app notification management with read/unread tracking - Notification scheduler for recurring and scheduled notifications - API endpoints for notifications CRUD, preferences, and subscriptions - UI components (NotificationBell, NotificationList, NotificationItem, PreferencesForm) - Full notifications page with preferences management - Service worker for push notification handling
206 lines
5.5 KiB
JavaScript
206 lines
5.5 KiB
JavaScript
/**
|
|
* Service Worker for Push Notifications
|
|
* LocalGreenChain PWA Support
|
|
*/
|
|
|
|
// Cache name versioning
|
|
const CACHE_NAME = 'lgc-cache-v1';
|
|
|
|
// Files to cache for offline support
|
|
const STATIC_CACHE = [
|
|
'/',
|
|
'/offline',
|
|
'/icons/icon-192x192.png'
|
|
];
|
|
|
|
// Install event - cache static assets
|
|
self.addEventListener('install', (event) => {
|
|
console.log('[SW] Installing service worker...');
|
|
event.waitUntil(
|
|
caches.open(CACHE_NAME)
|
|
.then((cache) => {
|
|
console.log('[SW] Caching static assets');
|
|
return cache.addAll(STATIC_CACHE);
|
|
})
|
|
.catch((error) => {
|
|
console.log('[SW] Failed to cache:', error);
|
|
})
|
|
);
|
|
self.skipWaiting();
|
|
});
|
|
|
|
// Activate event - clean up old caches
|
|
self.addEventListener('activate', (event) => {
|
|
console.log('[SW] Activating service worker...');
|
|
event.waitUntil(
|
|
caches.keys().then((cacheNames) => {
|
|
return Promise.all(
|
|
cacheNames
|
|
.filter((name) => name !== CACHE_NAME)
|
|
.map((name) => {
|
|
console.log('[SW] Deleting old cache:', name);
|
|
return caches.delete(name);
|
|
})
|
|
);
|
|
})
|
|
);
|
|
self.clients.claim();
|
|
});
|
|
|
|
// Push event - handle incoming push notifications
|
|
self.addEventListener('push', (event) => {
|
|
console.log('[SW] Push notification received');
|
|
|
|
let data = {
|
|
title: 'LocalGreenChain',
|
|
body: 'You have a new notification',
|
|
icon: '/icons/icon-192x192.png',
|
|
badge: '/icons/badge-72x72.png',
|
|
data: {}
|
|
};
|
|
|
|
if (event.data) {
|
|
try {
|
|
data = { ...data, ...event.data.json() };
|
|
} catch (e) {
|
|
data.body = event.data.text();
|
|
}
|
|
}
|
|
|
|
const options = {
|
|
body: data.body,
|
|
icon: data.icon,
|
|
badge: data.badge,
|
|
vibrate: [100, 50, 100],
|
|
data: data.data,
|
|
actions: data.actions || [
|
|
{ action: 'view', title: 'View', icon: '/icons/check.png' },
|
|
{ action: 'dismiss', title: 'Dismiss', icon: '/icons/close.png' }
|
|
],
|
|
tag: data.tag || 'lgc-notification',
|
|
renotify: true,
|
|
requireInteraction: data.requireInteraction || false
|
|
};
|
|
|
|
event.waitUntil(
|
|
self.registration.showNotification(data.title, options)
|
|
);
|
|
});
|
|
|
|
// Notification click event - handle user interaction
|
|
self.addEventListener('notificationclick', (event) => {
|
|
console.log('[SW] Notification clicked:', event.action);
|
|
|
|
event.notification.close();
|
|
|
|
if (event.action === 'dismiss') {
|
|
return;
|
|
}
|
|
|
|
// Default action or 'view' action
|
|
const urlToOpen = event.notification.data?.url || '/notifications';
|
|
|
|
event.waitUntil(
|
|
clients.matchAll({ type: 'window', includeUncontrolled: true })
|
|
.then((windowClients) => {
|
|
// Check if there's already a window open
|
|
for (const client of windowClients) {
|
|
if (client.url.includes(self.location.origin) && 'focus' in client) {
|
|
client.navigate(urlToOpen);
|
|
return client.focus();
|
|
}
|
|
}
|
|
// Open new window if none found
|
|
if (clients.openWindow) {
|
|
return clients.openWindow(urlToOpen);
|
|
}
|
|
})
|
|
);
|
|
});
|
|
|
|
// Notification close event
|
|
self.addEventListener('notificationclose', (event) => {
|
|
console.log('[SW] Notification closed');
|
|
|
|
// Track notification dismissal if needed
|
|
const notificationData = event.notification.data;
|
|
if (notificationData?.trackDismissal) {
|
|
// Could send analytics here
|
|
}
|
|
});
|
|
|
|
// Fetch event - network-first with cache fallback
|
|
self.addEventListener('fetch', (event) => {
|
|
// Skip non-GET requests
|
|
if (event.request.method !== 'GET') return;
|
|
|
|
// Skip API requests
|
|
if (event.request.url.includes('/api/')) return;
|
|
|
|
event.respondWith(
|
|
fetch(event.request)
|
|
.then((response) => {
|
|
// Clone the response for caching
|
|
const responseClone = response.clone();
|
|
|
|
caches.open(CACHE_NAME).then((cache) => {
|
|
cache.put(event.request, responseClone);
|
|
});
|
|
|
|
return response;
|
|
})
|
|
.catch(() => {
|
|
// Return cached response if available
|
|
return caches.match(event.request)
|
|
.then((cachedResponse) => {
|
|
if (cachedResponse) {
|
|
return cachedResponse;
|
|
}
|
|
// Return offline page for navigation requests
|
|
if (event.request.mode === 'navigate') {
|
|
return caches.match('/offline');
|
|
}
|
|
return new Response('Offline', { status: 503 });
|
|
});
|
|
})
|
|
);
|
|
});
|
|
|
|
// Message event - handle messages from client
|
|
self.addEventListener('message', (event) => {
|
|
console.log('[SW] Message received:', event.data);
|
|
|
|
if (event.data && event.data.type === 'SKIP_WAITING') {
|
|
self.skipWaiting();
|
|
}
|
|
|
|
if (event.data && event.data.type === 'GET_VERSION') {
|
|
event.ports[0].postMessage({ version: CACHE_NAME });
|
|
}
|
|
});
|
|
|
|
// Periodic sync for background updates (if supported)
|
|
self.addEventListener('periodicsync', (event) => {
|
|
if (event.tag === 'check-notifications') {
|
|
event.waitUntil(checkForNewNotifications());
|
|
}
|
|
});
|
|
|
|
async function checkForNewNotifications() {
|
|
try {
|
|
const response = await fetch('/api/notifications?unreadOnly=true&limit=1');
|
|
const data = await response.json();
|
|
|
|
if (data.success && data.data.unreadCount > 0) {
|
|
self.registration.showNotification('LocalGreenChain', {
|
|
body: `You have ${data.data.unreadCount} unread notification(s)`,
|
|
icon: '/icons/icon-192x192.png',
|
|
badge: '/icons/badge-72x72.png'
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.log('[SW] Failed to check notifications:', error);
|
|
}
|
|
}
|
|
|
|
console.log('[SW] Service worker loaded');
|