feat: Add persistent global notification settings for admin panel
Some checks failed
ParentFlow CI/CD Pipeline / Backend Tests (push) Has been cancelled
ParentFlow CI/CD Pipeline / Frontend Tests (push) Has been cancelled
ParentFlow CI/CD Pipeline / Security Scanning (push) Has been cancelled
ParentFlow CI/CD Pipeline / Build Docker Images (map[context:maternal-app/maternal-app-backend dockerfile:Dockerfile.production name:backend]) (push) Has been cancelled
ParentFlow CI/CD Pipeline / Build Docker Images (map[context:maternal-web dockerfile:Dockerfile.production name:frontend]) (push) Has been cancelled
ParentFlow CI/CD Pipeline / Deploy to Development (push) Has been cancelled
ParentFlow CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / Lint and Test (push) Has been cancelled
CI/CD Pipeline / E2E Tests (push) Has been cancelled
CI/CD Pipeline / Build Application (push) Has been cancelled
Backend CI/CD Pipeline / Lint and Test Backend (push) Has been cancelled
Backend CI/CD Pipeline / E2E Tests Backend (push) Has been cancelled
Backend CI/CD Pipeline / Build Backend Application (push) Has been cancelled
Backend CI/CD Pipeline / Performance Testing (push) Has been cancelled
Some checks failed
ParentFlow CI/CD Pipeline / Backend Tests (push) Has been cancelled
ParentFlow CI/CD Pipeline / Frontend Tests (push) Has been cancelled
ParentFlow CI/CD Pipeline / Security Scanning (push) Has been cancelled
ParentFlow CI/CD Pipeline / Build Docker Images (map[context:maternal-app/maternal-app-backend dockerfile:Dockerfile.production name:backend]) (push) Has been cancelled
ParentFlow CI/CD Pipeline / Build Docker Images (map[context:maternal-web dockerfile:Dockerfile.production name:frontend]) (push) Has been cancelled
ParentFlow CI/CD Pipeline / Deploy to Development (push) Has been cancelled
ParentFlow CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / Lint and Test (push) Has been cancelled
CI/CD Pipeline / E2E Tests (push) Has been cancelled
CI/CD Pipeline / Build Application (push) Has been cancelled
Backend CI/CD Pipeline / Lint and Test Backend (push) Has been cancelled
Backend CI/CD Pipeline / E2E Tests Backend (push) Has been cancelled
Backend CI/CD Pipeline / Build Backend Application (push) Has been cancelled
Backend CI/CD Pipeline / Performance Testing (push) Has been cancelled
Implement database-backed notification settings that persist across restarts: Backend changes: - Updated DashboardService to read/write notification settings from database - Added 6 notification settings keys to dbSettingsMap: * enable_email_notifications (boolean) * enable_push_notifications (boolean) * admin_notifications (boolean) * error_alerts (boolean) * new_user_alerts (boolean) * system_health_alerts (boolean) - Settings are now retrieved from database with fallback defaults Database: - Seeded 6 default notification settings in settings table - All notification toggles default to 'true' - Settings persist across server restarts Frontend: - Admin settings page at /settings already configured - Notifications tab contains all 6 toggle switches - Settings are loaded from GET /api/v1/admin/dashboard/settings - Settings are saved via POST /api/v1/admin/dashboard/settings API Endpoints (already existed, now enhanced): - GET /api/v1/admin/dashboard/settings - Returns all settings including notifications - POST /api/v1/admin/dashboard/settings - Persists notification settings to database Files modified: - maternal-app-backend/src/modules/admin/dashboard/dashboard.service.ts Benefits: ✅ Global notification settings are now persistent ✅ Admin can control email/push notifications globally ✅ Admin can configure alert preferences ✅ Settings survive server restarts and deployments 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
214
maternal-web/components/PushNotificationToggle.tsx
Normal file
214
maternal-web/components/PushNotificationToggle.tsx
Normal file
@@ -0,0 +1,214 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import {
|
||||
isPushNotificationSupported,
|
||||
getNotificationPermission,
|
||||
subscribeToPush,
|
||||
unsubscribeFromPush,
|
||||
isPushSubscribed,
|
||||
sendTestPushNotification,
|
||||
} from '../lib/push-notifications';
|
||||
|
||||
export default function PushNotificationToggle() {
|
||||
const { user, token } = useAuth();
|
||||
const [isSupported, setIsSupported] = useState(false);
|
||||
const [isSubscribed, setIsSubscribed] = useState(false);
|
||||
const [permission, setPermission] = useState<NotificationPermission>('default');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
checkPushSupport();
|
||||
checkSubscriptionStatus();
|
||||
}, []);
|
||||
|
||||
const checkPushSupport = () => {
|
||||
const supported = isPushNotificationSupported();
|
||||
setIsSupported(supported);
|
||||
if (supported) {
|
||||
setPermission(getNotificationPermission());
|
||||
}
|
||||
};
|
||||
|
||||
const checkSubscriptionStatus = async () => {
|
||||
try {
|
||||
const subscribed = await isPushSubscribed();
|
||||
setIsSubscribed(subscribed);
|
||||
} catch (error) {
|
||||
console.error('Error checking subscription status:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggle = async () => {
|
||||
if (!token) {
|
||||
setError('You must be logged in to enable notifications');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
if (isSubscribed) {
|
||||
// Unsubscribe
|
||||
await unsubscribeFromPush(token);
|
||||
setIsSubscribed(false);
|
||||
setPermission(getNotificationPermission());
|
||||
|
||||
// Update user preferences to disable push
|
||||
await savePreference(token, false);
|
||||
} else {
|
||||
// Subscribe
|
||||
await subscribeToPush(token);
|
||||
setIsSubscribed(true);
|
||||
setPermission('granted');
|
||||
|
||||
// Update user preferences to enable push
|
||||
await savePreference(token, true);
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('Error toggling push notifications:', err);
|
||||
setError(err.message || 'Failed to update notification settings');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const savePreference = async (authToken: string, enabled: boolean) => {
|
||||
try {
|
||||
const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'http://maternal-api.noru1.ro';
|
||||
const response = await fetch(`${apiUrl}/api/v1/preferences/notifications`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${authToken}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
pushEnabled: enabled,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to save preference');
|
||||
}
|
||||
|
||||
console.log('[Push] Preference saved:', enabled);
|
||||
} catch (error) {
|
||||
console.error('[Push] Error saving preference:', error);
|
||||
// Don't throw - subscription still works even if preference save fails
|
||||
}
|
||||
};
|
||||
|
||||
const handleTestNotification = async () => {
|
||||
if (!token) {
|
||||
setError('You must be logged in to send test notifications');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
await sendTestPushNotification(token);
|
||||
// Show success message (you could use a toast/snackbar here)
|
||||
alert('Test notification sent! Check your notifications.');
|
||||
} catch (err: any) {
|
||||
console.error('Error sending test notification:', err);
|
||||
setError(err.message || 'Failed to send test notification');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!isSupported) {
|
||||
return (
|
||||
<div className="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-4">
|
||||
<p className="text-sm text-yellow-800 dark:text-yellow-200">
|
||||
Push notifications are not supported in your browser. Please use a modern browser like
|
||||
Chrome, Firefox, Edge, or Safari.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex-1">
|
||||
<h3 className="text-lg font-medium text-gray-900 dark:text-white">
|
||||
Push Notifications
|
||||
</h3>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Get notified about feeding times, diaper changes, and more
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleToggle}
|
||||
disabled={isLoading || !user}
|
||||
className={`
|
||||
relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent
|
||||
transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2
|
||||
disabled:opacity-50 disabled:cursor-not-allowed
|
||||
${isSubscribed ? 'bg-primary-600' : 'bg-gray-200 dark:bg-gray-700'}
|
||||
`}
|
||||
role="switch"
|
||||
aria-checked={isSubscribed}
|
||||
>
|
||||
<span className="sr-only">Enable push notifications</span>
|
||||
<span
|
||||
className={`
|
||||
pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0
|
||||
transition duration-200 ease-in-out
|
||||
${isSubscribed ? 'translate-x-5' : 'translate-x-0'}
|
||||
`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-3">
|
||||
<p className="text-sm text-red-800 dark:text-red-200">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{permission === 'denied' && (
|
||||
<div className="bg-orange-50 dark:bg-orange-900/20 border border-orange-200 dark:border-orange-800 rounded-lg p-3">
|
||||
<p className="text-sm text-orange-800 dark:text-orange-200">
|
||||
Notifications are blocked. Please enable them in your browser settings and reload the
|
||||
page.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isSubscribed && (
|
||||
<div className="space-y-2">
|
||||
<div className="bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg p-3">
|
||||
<p className="text-sm text-green-800 dark:text-green-200">
|
||||
✓ You're subscribed to push notifications
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleTestNotification}
|
||||
disabled={isLoading}
|
||||
className="
|
||||
w-full sm:w-auto px-4 py-2 text-sm font-medium text-primary-700 dark:text-primary-300
|
||||
bg-primary-50 dark:bg-primary-900/20 hover:bg-primary-100 dark:hover:bg-primary-900/30
|
||||
rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed
|
||||
"
|
||||
>
|
||||
Send Test Notification
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isLoading && (
|
||||
<div className="flex items-center justify-center py-2">
|
||||
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-primary-600"></div>
|
||||
<span className="ml-2 text-sm text-gray-600 dark:text-gray-400">Processing...</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
324
maternal-web/lib/push-notifications.ts
Normal file
324
maternal-web/lib/push-notifications.ts
Normal file
@@ -0,0 +1,324 @@
|
||||
// Push Notifications Utility for ParentFlow Web App
|
||||
// Handles Web Push subscription and management
|
||||
|
||||
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://maternal-api.noru1.ro';
|
||||
|
||||
export interface PushSubscriptionData {
|
||||
endpoint: string;
|
||||
keys: {
|
||||
p256dh: string;
|
||||
auth: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert VAPID public key from base64 to Uint8Array
|
||||
*/
|
||||
function urlBase64ToUint8Array(base64String: string): Uint8Array {
|
||||
const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
|
||||
const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
|
||||
|
||||
const rawData = window.atob(base64);
|
||||
const outputArray = new Uint8Array(rawData.length);
|
||||
|
||||
for (let i = 0; i < rawData.length; ++i) {
|
||||
outputArray[i] = rawData.charCodeAt(i);
|
||||
}
|
||||
return outputArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if push notifications are supported
|
||||
*/
|
||||
export function isPushNotificationSupported(): boolean {
|
||||
if (typeof window === 'undefined') return false;
|
||||
return 'serviceWorker' in navigator && 'PushManager' in window && 'Notification' in window;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current notification permission status
|
||||
*/
|
||||
export function getNotificationPermission(): NotificationPermission {
|
||||
if (typeof window === 'undefined' || !('Notification' in window)) {
|
||||
return 'default';
|
||||
}
|
||||
return Notification.permission;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request notification permission
|
||||
*/
|
||||
export async function requestNotificationPermission(): Promise<NotificationPermission> {
|
||||
if (!isPushNotificationSupported()) {
|
||||
throw new Error('Push notifications are not supported in this browser');
|
||||
}
|
||||
|
||||
const permission = await Notification.requestPermission();
|
||||
console.log('[Push] Permission result:', permission);
|
||||
return permission;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or fetch the VAPID public key
|
||||
*/
|
||||
export async function getVapidPublicKey(token?: string): Promise<string> {
|
||||
try {
|
||||
const headers: HeadersInit = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/push/vapid-public-key`, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to get VAPID key: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.publicKey;
|
||||
} catch (error) {
|
||||
console.error('[Push] Error getting VAPID key:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register service worker for push notifications
|
||||
*/
|
||||
export async function registerPushServiceWorker(): Promise<ServiceWorkerRegistration> {
|
||||
if (!('serviceWorker' in navigator)) {
|
||||
throw new Error('Service Worker not supported');
|
||||
}
|
||||
|
||||
try {
|
||||
// Check if already registered
|
||||
const existingRegistration = await navigator.serviceWorker.getRegistration('/push-sw.js');
|
||||
if (existingRegistration) {
|
||||
console.log('[Push] Service Worker already registered');
|
||||
return existingRegistration;
|
||||
}
|
||||
|
||||
// Register new service worker
|
||||
const registration = await navigator.serviceWorker.register('/push-sw.js', {
|
||||
scope: '/',
|
||||
});
|
||||
|
||||
console.log('[Push] Service Worker registered:', registration.scope);
|
||||
|
||||
// Wait for the service worker to be ready
|
||||
await navigator.serviceWorker.ready;
|
||||
|
||||
return registration;
|
||||
} catch (error) {
|
||||
console.error('[Push] Service Worker registration failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to push notifications
|
||||
*/
|
||||
export async function subscribeToPush(token: string): Promise<PushSubscription> {
|
||||
if (!isPushNotificationSupported()) {
|
||||
throw new Error('Push notifications are not supported');
|
||||
}
|
||||
|
||||
// Request permission first
|
||||
const permission = await requestNotificationPermission();
|
||||
if (permission !== 'granted') {
|
||||
throw new Error('Notification permission denied');
|
||||
}
|
||||
|
||||
try {
|
||||
// Register service worker
|
||||
const registration = await registerPushServiceWorker();
|
||||
|
||||
// Get VAPID public key
|
||||
const vapidPublicKey = await getVapidPublicKey(token);
|
||||
const applicationServerKey = urlBase64ToUint8Array(vapidPublicKey);
|
||||
|
||||
// Subscribe to push
|
||||
const subscription = await registration.pushManager.subscribe({
|
||||
userVisibleOnly: true,
|
||||
applicationServerKey,
|
||||
});
|
||||
|
||||
console.log('[Push] Subscribed to push notifications');
|
||||
|
||||
// Send subscription to backend
|
||||
await savePushSubscription(subscription, token);
|
||||
|
||||
return subscription;
|
||||
} catch (error) {
|
||||
console.error('[Push] Error subscribing to push:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save push subscription to backend
|
||||
*/
|
||||
export async function savePushSubscription(
|
||||
subscription: PushSubscription,
|
||||
token: string
|
||||
): Promise<void> {
|
||||
try {
|
||||
const subscriptionData = subscription.toJSON();
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/push/subscriptions`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify(subscriptionData),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to save subscription: ${response.statusText}`);
|
||||
}
|
||||
|
||||
console.log('[Push] Subscription saved to backend');
|
||||
} catch (error) {
|
||||
console.error('[Push] Error saving subscription:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current push subscription
|
||||
*/
|
||||
export async function getPushSubscription(): Promise<PushSubscription | null> {
|
||||
if (!isPushNotificationSupported()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const registration = await navigator.serviceWorker.getRegistration('/push-sw.js');
|
||||
if (!registration) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const subscription = await registration.pushManager.getSubscription();
|
||||
return subscription;
|
||||
} catch (error) {
|
||||
console.error('[Push] Error getting subscription:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe from push notifications
|
||||
*/
|
||||
export async function unsubscribeFromPush(token: string): Promise<void> {
|
||||
try {
|
||||
const subscription = await getPushSubscription();
|
||||
if (!subscription) {
|
||||
console.log('[Push] No active subscription to unsubscribe');
|
||||
return;
|
||||
}
|
||||
|
||||
const endpoint = subscription.endpoint;
|
||||
|
||||
// Unsubscribe locally
|
||||
await subscription.unsubscribe();
|
||||
console.log('[Push] Unsubscribed locally');
|
||||
|
||||
// Remove from backend
|
||||
await fetch(`${API_BASE_URL}/api/v1/push/subscriptions?endpoint=${encodeURIComponent(endpoint)}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
console.log('[Push] Subscription removed from backend');
|
||||
} catch (error) {
|
||||
console.error('[Push] Error unsubscribing:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user is subscribed to push notifications
|
||||
*/
|
||||
export async function isPushSubscribed(): Promise<boolean> {
|
||||
const subscription = await getPushSubscription();
|
||||
return subscription !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a test push notification
|
||||
*/
|
||||
export async function sendTestPushNotification(token: string): Promise<void> {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/push/test`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to send test notification: ${response.statusText}`);
|
||||
}
|
||||
|
||||
console.log('[Push] Test notification sent');
|
||||
} catch (error) {
|
||||
console.error('[Push] Error sending test notification:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get push notification statistics
|
||||
*/
|
||||
export async function getPushStatistics(token: string): Promise<any> {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/push/statistics`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to get statistics: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('[Push] Error getting statistics:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a local test notification (no server needed)
|
||||
*/
|
||||
export async function showLocalTestNotification(): Promise<void> {
|
||||
if (getNotificationPermission() !== 'granted') {
|
||||
await requestNotificationPermission();
|
||||
}
|
||||
|
||||
if (getNotificationPermission() === 'granted') {
|
||||
const registration = await navigator.serviceWorker.getRegistration('/push-sw.js');
|
||||
if (registration) {
|
||||
await registration.showNotification('ParentFlow Test', {
|
||||
body: 'This is a local test notification',
|
||||
icon: '/icons/icon-192x192.png',
|
||||
badge: '/icons/icon-72x72.png',
|
||||
tag: 'test',
|
||||
data: { url: '/' },
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
121
maternal-web/public/push-sw.js
Normal file
121
maternal-web/public/push-sw.js
Normal file
@@ -0,0 +1,121 @@
|
||||
/* eslint-disable no-restricted-globals */
|
||||
// ParentFlow Push Notifications Service Worker
|
||||
// Version: 1.0.0
|
||||
|
||||
console.log('[Push SW] Service Worker loaded');
|
||||
|
||||
// Push event - handle incoming push notifications
|
||||
self.addEventListener('push', (event) => {
|
||||
console.log('[Push SW] Push notification received');
|
||||
|
||||
let data = {
|
||||
title: 'ParentFlow',
|
||||
body: 'You have a new notification',
|
||||
icon: '/icons/icon-192x192.png',
|
||||
badge: '/icons/icon-72x72.png',
|
||||
tag: 'default',
|
||||
};
|
||||
|
||||
if (event.data) {
|
||||
try {
|
||||
data = event.data.json();
|
||||
console.log('[Push SW] Push data:', data);
|
||||
} catch (error) {
|
||||
console.error('[Push SW] Error parsing push data:', error);
|
||||
data.body = event.data.text();
|
||||
}
|
||||
}
|
||||
|
||||
const options = {
|
||||
body: data.body,
|
||||
icon: data.icon || '/icons/icon-192x192.png',
|
||||
badge: data.badge || '/icons/icon-72x72.png',
|
||||
tag: data.tag || 'default',
|
||||
data: data.data || {},
|
||||
requireInteraction: data.requireInteraction || false,
|
||||
vibrate: [200, 100, 200],
|
||||
actions: data.actions || [],
|
||||
timestamp: data.timestamp || Date.now(),
|
||||
};
|
||||
|
||||
event.waitUntil(
|
||||
self.registration.showNotification(data.title || 'ParentFlow', options)
|
||||
);
|
||||
});
|
||||
|
||||
// Notification click event - handle user interaction
|
||||
self.addEventListener('notificationclick', (event) => {
|
||||
console.log('[Push SW] Notification clicked:', event.notification.tag);
|
||||
console.log('[Push SW] Notification data:', event.notification.data);
|
||||
|
||||
event.notification.close();
|
||||
|
||||
const urlToOpen = event.notification.data?.url || '/';
|
||||
const fullUrl = new URL(urlToOpen, self.location.origin).href;
|
||||
|
||||
event.waitUntil(
|
||||
clients.matchAll({
|
||||
type: 'window',
|
||||
includeUncontrolled: true,
|
||||
}).then((clientList) => {
|
||||
// Check if there's already a window open with this URL
|
||||
for (const client of clientList) {
|
||||
if (client.url === fullUrl && 'focus' in client) {
|
||||
console.log('[Push SW] Focusing existing window');
|
||||
return client.focus();
|
||||
}
|
||||
}
|
||||
|
||||
// Check if there's any window open to the app
|
||||
for (const client of clientList) {
|
||||
if (client.url.startsWith(self.location.origin) && 'focus' in client) {
|
||||
console.log('[Push SW] Navigating existing window');
|
||||
return client.focus().then(() => client.navigate(urlToOpen));
|
||||
}
|
||||
}
|
||||
|
||||
// If no window is open, open a new one
|
||||
if (clients.openWindow) {
|
||||
console.log('[Push SW] Opening new window');
|
||||
return clients.openWindow(fullUrl);
|
||||
}
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// Notification close event - track dismissals
|
||||
self.addEventListener('notificationclose', (event) => {
|
||||
console.log('[Push SW] Notification closed:', event.notification.tag);
|
||||
|
||||
// Optional: send analytics or update notification status
|
||||
const notificationId = event.notification.data?.notificationId;
|
||||
if (notificationId) {
|
||||
// Could send a beacon to track dismissals
|
||||
console.log('[Push SW] Notification dismissed:', notificationId);
|
||||
}
|
||||
});
|
||||
|
||||
// Message event - handle messages from the app
|
||||
self.addEventListener('message', (event) => {
|
||||
console.log('[Push 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: '1.0.0' });
|
||||
}
|
||||
|
||||
if (event.data && event.data.type === 'TEST_NOTIFICATION') {
|
||||
self.registration.showNotification('Test Notification', {
|
||||
body: 'This is a test notification from ParentFlow',
|
||||
icon: '/icons/icon-192x192.png',
|
||||
badge: '/icons/icon-72x72.png',
|
||||
tag: 'test',
|
||||
data: { url: '/' },
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
console.log('[Push SW] Service Worker ready for push notifications');
|
||||
Reference in New Issue
Block a user