feat: implement complete web push notification system

- Add self-hosted push notifications using VAPID (no external services)
- Create PushSubscription model in database
- Implement server-side push logic with web-push library
- Add notification helpers for list events (items, invitations, collaborators)
- Create NotificationSettings UI component for profile page
- Add usePushNotifications React hook for subscription management
- Create API routes for subscription management and VAPID key
- Update service worker with push event handlers
- Integrate notifications into item actions (add, remove, complete)
- Integrate notifications into sharing actions (invite, accept, remove)
- Add VAPID key generation script
- Add comprehensive documentation and setup guides

Notifications are sent when:
- Items are added/removed/completed in shared lists
- Users are invited to lists
- Invitations are accepted
- Collaborators are removed from lists

Works on iOS 16.4+ (when installed to home screen), Android, and desktop browsers.
This commit is contained in:
2025-12-07 01:22:16 +00:00
parent 625ca40e48
commit 7cda58440f
21 changed files with 2144 additions and 8 deletions

View File

@@ -280,3 +280,53 @@ self.addEventListener('message', (event) => {
}
}
});
// Push notification event - receive and display notifications
self.addEventListener('push', (event) => {
if (!event.data) {
return;
}
try {
const data = event.data.json();
const { title, body, icon, badge, tag, data: notificationData } = data;
const options = {
body,
icon: icon || '/icon-192.svg',
badge: badge || '/icon-192.svg',
tag: tag || 'default',
data: notificationData,
vibrate: [200, 100, 200],
requireInteraction: false,
};
event.waitUntil(
self.registration.showNotification(title, options)
);
} catch (error) {
console.error('Failed to show notification:', error);
}
});
// Notification click event - handle user interaction
self.addEventListener('notificationclick', (event) => {
event.notification.close();
const urlToOpen = event.notification.data?.url || '/';
event.waitUntil(
clients.matchAll({ type: 'window', includeUncontrolled: true }).then((clientList) => {
// Check if there's already a window open
for (const client of clientList) {
if (client.url === urlToOpen && 'focus' in client) {
return client.focus();
}
}
// Open a new window if none exists
if (clients.openWindow) {
return clients.openWindow(urlToOpen);
}
})
);
});