@djangocfg/layouts 2.1.37 → 2.1.38
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +204 -18
- package/package.json +5 -5
- package/src/components/errors/index.ts +9 -0
- package/src/components/errors/types.ts +38 -0
- package/src/layouts/AppLayout/AppLayout.tsx +33 -45
- package/src/layouts/AppLayout/BaseApp.tsx +104 -33
- package/src/layouts/AuthLayout/AuthContext.tsx +7 -1
- package/src/layouts/AuthLayout/OAuthProviders.tsx +1 -10
- package/src/layouts/AuthLayout/OTPForm.tsx +1 -0
- package/src/layouts/PrivateLayout/PrivateLayout.tsx +1 -1
- package/src/layouts/PublicLayout/PublicLayout.tsx +1 -1
- package/src/layouts/PublicLayout/components/PublicMobileDrawer.tsx +1 -1
- package/src/layouts/PublicLayout/components/PublicNavigation.tsx +1 -1
- package/src/layouts/_components/UserMenu.tsx +1 -1
- package/src/layouts/index.ts +1 -1
- package/src/layouts/types/index.ts +47 -0
- package/src/layouts/types/layout.types.ts +61 -0
- package/src/layouts/types/providers.types.ts +65 -0
- package/src/layouts/types/ui.types.ts +103 -0
- package/src/snippets/Analytics/index.ts +1 -0
- package/src/snippets/Analytics/types.ts +10 -0
- package/src/snippets/PWAInstall/@docs/README.md +92 -0
- package/src/snippets/PWAInstall/README.md +185 -0
- package/src/snippets/{PWA → PWAInstall}/components/A2HSHint.tsx +85 -84
- package/src/snippets/PWAInstall/components/DesktopGuide.tsx +229 -0
- package/src/snippets/PWAInstall/context/InstallContext.tsx +102 -0
- package/src/snippets/{PWA → PWAInstall}/hooks/useInstallPrompt.ts +3 -0
- package/src/snippets/{PWA → PWAInstall}/index.ts +12 -31
- package/src/snippets/{PWA → PWAInstall}/types/components.ts +0 -6
- package/src/snippets/PWAInstall/types/config.ts +22 -0
- package/src/snippets/{PWA → PWAInstall}/types/index.ts +4 -4
- package/src/snippets/{PWA → PWAInstall}/utils/localStorage.ts +1 -23
- package/src/snippets/PushNotifications/@docs/README.md +191 -0
- package/src/snippets/PushNotifications/@docs/guides/django-integration.md +648 -0
- package/src/snippets/PushNotifications/@docs/guides/service-worker.md +467 -0
- package/src/snippets/PushNotifications/@docs/guides/vapid-setup.md +352 -0
- package/src/snippets/PushNotifications/README.md +328 -0
- package/src/snippets/{PWA → PushNotifications}/config.ts +2 -2
- package/src/snippets/PushNotifications/context/DjangoPushContext.tsx +190 -0
- package/src/snippets/{PWA → PushNotifications}/hooks/useDjangoPush.ts +63 -81
- package/src/snippets/{PWA → PushNotifications}/hooks/usePushNotifications.ts +12 -8
- package/src/snippets/PushNotifications/index.ts +87 -0
- package/src/snippets/PushNotifications/types/config.ts +28 -0
- package/src/snippets/PushNotifications/types/index.ts +9 -0
- package/src/snippets/PushNotifications/utils/localStorage.ts +60 -0
- package/src/snippets/PushNotifications/utils/logger.ts +149 -0
- package/src/snippets/PushNotifications/utils/platform.ts +151 -0
- package/src/snippets/index.ts +37 -12
- package/src/layouts/shared/index.ts +0 -21
- package/src/layouts/shared/types.ts +0 -247
- package/src/snippets/PWA/@refactoring/ARCHITECTURE_ANALYSIS.md +0 -1179
- package/src/snippets/PWA/@refactoring/EXECUTIVE_SUMMARY.md +0 -271
- package/src/snippets/PWA/@refactoring/README.md +0 -204
- package/src/snippets/PWA/@refactoring/REFACTORING_PROPOSALS.md +0 -1109
- package/src/snippets/PWA/@refactoring2/COMPARISON-WITH-NEXTJS.md +0 -718
- package/src/snippets/PWA/@refactoring2/P1-FIXES-COMPLETED.md +0 -188
- package/src/snippets/PWA/@refactoring2/POST-P0-ANALYSIS.md +0 -362
- package/src/snippets/PWA/@refactoring2/README.md +0 -85
- package/src/snippets/PWA/@refactoring2/RECOMMENDATIONS.md +0 -1321
- package/src/snippets/PWA/@refactoring2/REMAINING-ISSUES.md +0 -557
- package/src/snippets/PWA/README.md +0 -387
- package/src/snippets/PWA/context/DjangoPushContext.tsx +0 -105
- package/src/snippets/PWA/context/InstallContext.tsx +0 -118
- package/src/snippets/PWA/context/PushContext.tsx +0 -156
- /package/src/layouts/{shared → types}/README.md +0 -0
- /package/src/snippets/{PWA/@docs/research.md → PWAInstall/@docs/research/ios-android-install-flows.md} +0 -0
- /package/src/snippets/{PWA → PWAInstall}/components/IOSGuide.tsx +0 -0
- /package/src/snippets/{PWA → PWAInstall}/components/IOSGuideDrawer.tsx +0 -0
- /package/src/snippets/{PWA → PWAInstall}/components/IOSGuideModal.tsx +0 -0
- /package/src/snippets/{PWA → PWAInstall}/hooks/useIsPWA.ts +0 -0
- /package/src/snippets/{PWA → PWAInstall}/types/install.ts +0 -0
- /package/src/snippets/{PWA → PWAInstall}/types/platform.ts +0 -0
- /package/src/snippets/{PWA → PWAInstall}/utils/logger.ts +0 -0
- /package/src/snippets/{PWA → PWAInstall}/utils/platform.ts +0 -0
- /package/src/snippets/{PWA → PushNotifications}/components/PushPrompt.tsx +0 -0
- /package/src/snippets/{PWA → PushNotifications}/types/push.ts +0 -0
- /package/src/snippets/{PWA → PushNotifications}/utils/vapid.ts +0 -0
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
# Service Worker Setup for Push Notifications
|
|
2
|
+
|
|
3
|
+
Complete guide to configuring Service Worker for receiving and displaying push notifications.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
Service Workers handle push notifications **even when your app is closed**. They:
|
|
8
|
+
|
|
9
|
+
1. Listen for push events from the browser
|
|
10
|
+
2. Display notifications to the user
|
|
11
|
+
3. Handle notification clicks
|
|
12
|
+
|
|
13
|
+
## Prerequisites
|
|
14
|
+
|
|
15
|
+
- ✅ VAPID keys configured ([VAPID Setup Guide](./vapid-setup.md))
|
|
16
|
+
- ✅ Service worker registered in your app
|
|
17
|
+
- ✅ HTTPS (required for service workers)
|
|
18
|
+
|
|
19
|
+
## Step 1: Create Service Worker File
|
|
20
|
+
|
|
21
|
+
### Location
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
public/
|
|
25
|
+
└── sw.js # Service worker file
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Basic Service Worker
|
|
29
|
+
|
|
30
|
+
```javascript
|
|
31
|
+
// public/sw.js
|
|
32
|
+
|
|
33
|
+
// Service Worker version (increment to force update)
|
|
34
|
+
const SW_VERSION = 'v1.0.0';
|
|
35
|
+
|
|
36
|
+
console.log(`[SW] ${SW_VERSION} loaded`);
|
|
37
|
+
|
|
38
|
+
// Install event
|
|
39
|
+
self.addEventListener('install', (event) => {
|
|
40
|
+
console.log('[SW] Installing...');
|
|
41
|
+
self.skipWaiting(); // Activate immediately
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Activate event
|
|
45
|
+
self.addEventListener('activate', (event) => {
|
|
46
|
+
console.log('[SW] Activating...');
|
|
47
|
+
event.waitUntil(clients.claim()); // Take control immediately
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Push event - receive notification
|
|
51
|
+
self.addEventListener('push', (event) => {
|
|
52
|
+
console.log('[SW] Push received');
|
|
53
|
+
|
|
54
|
+
// Parse notification data
|
|
55
|
+
let data = {
|
|
56
|
+
title: 'Notification',
|
|
57
|
+
body: 'You have a new notification',
|
|
58
|
+
icon: '/icon-192.png',
|
|
59
|
+
badge: '/badge-72.png',
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
if (event.data) {
|
|
63
|
+
try {
|
|
64
|
+
data = event.data.json();
|
|
65
|
+
} catch (e) {
|
|
66
|
+
console.error('[SW] Failed to parse push data:', e);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Show notification
|
|
71
|
+
event.waitUntil(
|
|
72
|
+
self.registration.showNotification(data.title, {
|
|
73
|
+
body: data.body,
|
|
74
|
+
icon: data.icon || '/icon-192.png',
|
|
75
|
+
badge: data.badge || '/badge-72.png',
|
|
76
|
+
data: data.data || {}, // Custom data for click handler
|
|
77
|
+
tag: data.tag || 'default', // Notification grouping
|
|
78
|
+
requireInteraction: data.requireInteraction || false,
|
|
79
|
+
})
|
|
80
|
+
);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Notification click event
|
|
84
|
+
self.addEventListener('notificationclick', (event) => {
|
|
85
|
+
console.log('[SW] Notification clicked');
|
|
86
|
+
|
|
87
|
+
event.notification.close();
|
|
88
|
+
|
|
89
|
+
// Handle click action
|
|
90
|
+
const urlToOpen = event.notification.data?.url || '/';
|
|
91
|
+
|
|
92
|
+
event.waitUntil(
|
|
93
|
+
clients.matchAll({ type: 'window', includeUncontrolled: true })
|
|
94
|
+
.then((clientList) => {
|
|
95
|
+
// Check if app is already open
|
|
96
|
+
for (const client of clientList) {
|
|
97
|
+
if (client.url === urlToOpen && 'focus' in client) {
|
|
98
|
+
return client.focus();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Open new window
|
|
103
|
+
if (clients.openWindow) {
|
|
104
|
+
return clients.openWindow(urlToOpen);
|
|
105
|
+
}
|
|
106
|
+
})
|
|
107
|
+
);
|
|
108
|
+
});
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Step 2: Register Service Worker
|
|
112
|
+
|
|
113
|
+
### Next.js App
|
|
114
|
+
|
|
115
|
+
```tsx
|
|
116
|
+
// app/layout.tsx or components/ServiceWorkerRegistration.tsx
|
|
117
|
+
'use client';
|
|
118
|
+
|
|
119
|
+
import { useEffect } from 'react';
|
|
120
|
+
|
|
121
|
+
export function ServiceWorkerRegistration() {
|
|
122
|
+
useEffect(() => {
|
|
123
|
+
if ('serviceWorker' in navigator) {
|
|
124
|
+
navigator.serviceWorker
|
|
125
|
+
.register('/sw.js')
|
|
126
|
+
.then((registration) => {
|
|
127
|
+
console.log('[App] SW registered:', registration);
|
|
128
|
+
})
|
|
129
|
+
.catch((error) => {
|
|
130
|
+
console.error('[App] SW registration failed:', error);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}, []);
|
|
134
|
+
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
```tsx
|
|
140
|
+
// app/layout.tsx
|
|
141
|
+
import { ServiceWorkerRegistration } from '@/components/ServiceWorkerRegistration';
|
|
142
|
+
|
|
143
|
+
export default function RootLayout({ children }) {
|
|
144
|
+
return (
|
|
145
|
+
<html>
|
|
146
|
+
<body>
|
|
147
|
+
<ServiceWorkerRegistration />
|
|
148
|
+
{children}
|
|
149
|
+
</body>
|
|
150
|
+
</html>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Step 3: Advanced Features
|
|
156
|
+
|
|
157
|
+
### Custom Notification Options
|
|
158
|
+
|
|
159
|
+
```javascript
|
|
160
|
+
// public/sw.js - Enhanced notification options
|
|
161
|
+
|
|
162
|
+
self.addEventListener('push', (event) => {
|
|
163
|
+
const data = event.data ? event.data.json() : {};
|
|
164
|
+
|
|
165
|
+
const options = {
|
|
166
|
+
// Basic
|
|
167
|
+
body: data.body || 'New notification',
|
|
168
|
+
icon: data.icon || '/icon-192.png',
|
|
169
|
+
badge: data.badge || '/badge-72.png',
|
|
170
|
+
|
|
171
|
+
// Visual
|
|
172
|
+
image: data.image, // Large image
|
|
173
|
+
vibrate: [200, 100, 200], // Vibration pattern
|
|
174
|
+
silent: data.silent || false,
|
|
175
|
+
|
|
176
|
+
// Interaction
|
|
177
|
+
requireInteraction: data.important || false,
|
|
178
|
+
tag: data.tag || 'default', // Group notifications
|
|
179
|
+
|
|
180
|
+
// Actions (buttons)
|
|
181
|
+
actions: data.actions || [
|
|
182
|
+
{ action: 'view', title: 'View' },
|
|
183
|
+
{ action: 'dismiss', title: 'Dismiss' },
|
|
184
|
+
],
|
|
185
|
+
|
|
186
|
+
// Custom data
|
|
187
|
+
data: {
|
|
188
|
+
url: data.url || '/',
|
|
189
|
+
timestamp: Date.now(),
|
|
190
|
+
...data.data,
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
event.waitUntil(
|
|
195
|
+
self.registration.showNotification(data.title || 'Notification', options)
|
|
196
|
+
);
|
|
197
|
+
});
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Handle Notification Actions
|
|
201
|
+
|
|
202
|
+
```javascript
|
|
203
|
+
// public/sw.js - Handle action buttons
|
|
204
|
+
|
|
205
|
+
self.addEventListener('notificationclick', (event) => {
|
|
206
|
+
event.notification.close();
|
|
207
|
+
|
|
208
|
+
if (event.action === 'view') {
|
|
209
|
+
// User clicked "View" button
|
|
210
|
+
const url = event.notification.data.url || '/';
|
|
211
|
+
event.waitUntil(clients.openWindow(url));
|
|
212
|
+
} else if (event.action === 'dismiss') {
|
|
213
|
+
// User clicked "Dismiss" button
|
|
214
|
+
// Notification already closed
|
|
215
|
+
} else {
|
|
216
|
+
// User clicked notification body
|
|
217
|
+
const url = event.notification.data.url || '/';
|
|
218
|
+
event.waitUntil(clients.openWindow(url));
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Background Sync
|
|
224
|
+
|
|
225
|
+
```javascript
|
|
226
|
+
// public/sw.js - Sync notifications with server
|
|
227
|
+
|
|
228
|
+
self.addEventListener('sync', (event) => {
|
|
229
|
+
if (event.tag === 'sync-notifications') {
|
|
230
|
+
event.waitUntil(
|
|
231
|
+
fetch('/api/notifications/sync')
|
|
232
|
+
.then(response => response.json())
|
|
233
|
+
.then(data => {
|
|
234
|
+
// Handle synced notifications
|
|
235
|
+
console.log('[SW] Synced notifications:', data);
|
|
236
|
+
})
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
## Step 4: Testing
|
|
243
|
+
|
|
244
|
+
### Local Testing
|
|
245
|
+
|
|
246
|
+
```javascript
|
|
247
|
+
// Browser console - Send test notification
|
|
248
|
+
|
|
249
|
+
// 1. Get service worker registration
|
|
250
|
+
navigator.serviceWorker.ready.then((registration) => {
|
|
251
|
+
// 2. Show test notification
|
|
252
|
+
registration.showNotification('Test', {
|
|
253
|
+
body: 'This is a test notification',
|
|
254
|
+
icon: '/icon-192.png',
|
|
255
|
+
data: { url: '/dashboard' },
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### Backend Testing
|
|
261
|
+
|
|
262
|
+
```python
|
|
263
|
+
# Django - Send test push
|
|
264
|
+
from utils.push import send_push_notification
|
|
265
|
+
|
|
266
|
+
subscription = {
|
|
267
|
+
"endpoint": "https://fcm.googleapis.com/fcm/send/...",
|
|
268
|
+
"keys": {
|
|
269
|
+
"auth": "...",
|
|
270
|
+
"p256dh": "..."
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
notification = {
|
|
275
|
+
"title": "Test Notification",
|
|
276
|
+
"body": "Testing push from backend",
|
|
277
|
+
"icon": "/icon-192.png",
|
|
278
|
+
"data": {"url": "/dashboard"}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
send_push_notification(subscription, notification)
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
## Step 5: Production Optimization
|
|
285
|
+
|
|
286
|
+
### Service Worker Caching
|
|
287
|
+
|
|
288
|
+
```javascript
|
|
289
|
+
// public/sw.js - Add caching for offline support
|
|
290
|
+
|
|
291
|
+
const CACHE_NAME = 'pwa-cache-v1';
|
|
292
|
+
|
|
293
|
+
const urlsToCache = [
|
|
294
|
+
'/',
|
|
295
|
+
'/icon-192.png',
|
|
296
|
+
'/badge-72.png',
|
|
297
|
+
];
|
|
298
|
+
|
|
299
|
+
self.addEventListener('install', (event) => {
|
|
300
|
+
event.waitUntil(
|
|
301
|
+
caches.open(CACHE_NAME)
|
|
302
|
+
.then((cache) => cache.addAll(urlsToCache))
|
|
303
|
+
);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
self.addEventListener('fetch', (event) => {
|
|
307
|
+
event.respondWith(
|
|
308
|
+
caches.match(event.request)
|
|
309
|
+
.then((response) => response || fetch(event.request))
|
|
310
|
+
);
|
|
311
|
+
});
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
### Update Strategy
|
|
315
|
+
|
|
316
|
+
```javascript
|
|
317
|
+
// public/sw.js - Smart update strategy
|
|
318
|
+
|
|
319
|
+
const SW_VERSION = 'v1.0.1'; // Increment on changes
|
|
320
|
+
|
|
321
|
+
self.addEventListener('activate', (event) => {
|
|
322
|
+
event.waitUntil(
|
|
323
|
+
// Delete old caches
|
|
324
|
+
caches.keys().then((cacheNames) => {
|
|
325
|
+
return Promise.all(
|
|
326
|
+
cacheNames.map((cacheName) => {
|
|
327
|
+
if (cacheName !== CACHE_NAME) {
|
|
328
|
+
console.log('[SW] Deleting old cache:', cacheName);
|
|
329
|
+
return caches.delete(cacheName);
|
|
330
|
+
}
|
|
331
|
+
})
|
|
332
|
+
);
|
|
333
|
+
})
|
|
334
|
+
);
|
|
335
|
+
});
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
## Notification Permissions
|
|
339
|
+
|
|
340
|
+
### Check Permission State
|
|
341
|
+
|
|
342
|
+
```tsx
|
|
343
|
+
// Check if user granted permission
|
|
344
|
+
const permission = Notification.permission;
|
|
345
|
+
// 'default' | 'granted' | 'denied'
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
### Request Permission
|
|
349
|
+
|
|
350
|
+
```tsx
|
|
351
|
+
import { useDjangoPushContext } from '@/snippets/PushNotifications';
|
|
352
|
+
|
|
353
|
+
function NotifyButton() {
|
|
354
|
+
const { subscribe, permission } = useDjangoPushContext();
|
|
355
|
+
|
|
356
|
+
if (permission === 'denied') {
|
|
357
|
+
return <p>Notifications blocked</p>;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return (
|
|
361
|
+
<button onClick={subscribe}>
|
|
362
|
+
{permission === 'granted' ? 'Subscribed' : 'Enable Notifications'}
|
|
363
|
+
</button>
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
## Browser Compatibility
|
|
369
|
+
|
|
370
|
+
| Feature | Chrome | Firefox | Safari | Edge |
|
|
371
|
+
|---------|--------|---------|--------|------|
|
|
372
|
+
| Service Workers | ✅ 40+ | ✅ 44+ | ✅ 11.1+ | ✅ 17+ |
|
|
373
|
+
| Push API | ✅ 42+ | ✅ 44+ | ✅ 16.4+ | ✅ 17+ |
|
|
374
|
+
| Notification Actions | ✅ 48+ | ✅ 56+ | ❌ | ✅ 18+ |
|
|
375
|
+
| Badge | ✅ 53+ | ✅ 60+ | ⚠️ Limited | ✅ 18+ |
|
|
376
|
+
|
|
377
|
+
**iOS Note**: Push requires Safari 16.4+ and PWA must be installed.
|
|
378
|
+
|
|
379
|
+
## Troubleshooting
|
|
380
|
+
|
|
381
|
+
### Issue: Service Worker Not Registering
|
|
382
|
+
|
|
383
|
+
**Solution**:
|
|
384
|
+
```javascript
|
|
385
|
+
// Check if HTTPS
|
|
386
|
+
if (location.protocol !== 'https:' && location.hostname !== 'localhost') {
|
|
387
|
+
console.error('Service workers require HTTPS');
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Check browser support
|
|
391
|
+
if (!('serviceWorker' in navigator)) {
|
|
392
|
+
console.error('Service workers not supported');
|
|
393
|
+
}
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
### Issue: Push Not Received
|
|
397
|
+
|
|
398
|
+
**Check**:
|
|
399
|
+
1. Service worker registered: `navigator.serviceWorker.ready`
|
|
400
|
+
2. Push subscription active: `registration.pushManager.getSubscription()`
|
|
401
|
+
3. Notification permission: `Notification.permission === 'granted'`
|
|
402
|
+
4. VAPID keys match between frontend and backend
|
|
403
|
+
|
|
404
|
+
### Issue: Notification Not Showing
|
|
405
|
+
|
|
406
|
+
**Solution**:
|
|
407
|
+
```javascript
|
|
408
|
+
// Ensure notification is shown in push event
|
|
409
|
+
self.addEventListener('push', (event) => {
|
|
410
|
+
// ❌ Wrong - async not awaited
|
|
411
|
+
self.registration.showNotification('Test');
|
|
412
|
+
|
|
413
|
+
// ✅ Correct - use event.waitUntil
|
|
414
|
+
event.waitUntil(
|
|
415
|
+
self.registration.showNotification('Test')
|
|
416
|
+
);
|
|
417
|
+
});
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
## Security
|
|
421
|
+
|
|
422
|
+
### Content Security Policy
|
|
423
|
+
|
|
424
|
+
```html
|
|
425
|
+
<!-- Add to HTML head -->
|
|
426
|
+
<meta http-equiv="Content-Security-Policy"
|
|
427
|
+
content="default-src 'self';
|
|
428
|
+
connect-src 'self' https://*.googleapis.com;
|
|
429
|
+
img-src 'self' data: https:;">
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
### Validate Push Data
|
|
433
|
+
|
|
434
|
+
```javascript
|
|
435
|
+
// public/sw.js - Validate notification data
|
|
436
|
+
|
|
437
|
+
function isValidNotification(data) {
|
|
438
|
+
return (
|
|
439
|
+
data &&
|
|
440
|
+
typeof data.title === 'string' &&
|
|
441
|
+
typeof data.body === 'string'
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
self.addEventListener('push', (event) => {
|
|
446
|
+
const data = event.data ? event.data.json() : null;
|
|
447
|
+
|
|
448
|
+
if (!isValidNotification(data)) {
|
|
449
|
+
console.error('[SW] Invalid notification data');
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Process valid notification...
|
|
454
|
+
});
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
## Next Steps
|
|
458
|
+
|
|
459
|
+
- [VAPID Setup](./vapid-setup.md) - Configure VAPID keys
|
|
460
|
+
- [Django Integration](./django-integration.md) - Backend setup
|
|
461
|
+
- [Main README](../../README.md) - API reference
|
|
462
|
+
|
|
463
|
+
## References
|
|
464
|
+
|
|
465
|
+
- [Service Worker API](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API)
|
|
466
|
+
- [Push API](https://developer.mozilla.org/en-US/docs/Web/API/Push_API)
|
|
467
|
+
- [Notifications API](https://developer.mozilla.org/en-US/docs/Web/API/Notifications_API)
|