@djangocfg/layouts 2.1.37 → 2.1.39
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,648 @@
|
|
|
1
|
+
# Django Integration Guide
|
|
2
|
+
|
|
3
|
+
Complete guide to integrating PushNotifications snippet with Django-CFG backend.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
This guide shows how to:
|
|
8
|
+
1. Set up Django backend for push notifications
|
|
9
|
+
2. Create subscription endpoints
|
|
10
|
+
3. Send push notifications from Django
|
|
11
|
+
4. Connect with DjangoPushProvider
|
|
12
|
+
|
|
13
|
+
## Prerequisites
|
|
14
|
+
|
|
15
|
+
- ✅ Django project setup
|
|
16
|
+
- ✅ VAPID keys generated ([VAPID Setup Guide](./vapid-setup.md))
|
|
17
|
+
- ✅ Service worker configured ([Service Worker Guide](./service-worker.md))
|
|
18
|
+
- ✅ Python 3.8+
|
|
19
|
+
|
|
20
|
+
## Step 1: Install Dependencies
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install pywebpush
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Update requirements:
|
|
27
|
+
```bash
|
|
28
|
+
pip freeze > requirements.txt
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Step 2: Configure Django Settings
|
|
32
|
+
|
|
33
|
+
### settings.py
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
# settings.py
|
|
37
|
+
import os
|
|
38
|
+
from pathlib import Path
|
|
39
|
+
|
|
40
|
+
# VAPID Configuration
|
|
41
|
+
VAPID_PUBLIC_KEY = os.getenv('VAPID_PUBLIC_KEY')
|
|
42
|
+
VAPID_PRIVATE_KEY = os.getenv('VAPID_PRIVATE_KEY')
|
|
43
|
+
VAPID_MAILTO = os.getenv('VAPID_MAILTO', 'mailto:admin@example.com')
|
|
44
|
+
|
|
45
|
+
# Validate VAPID keys (development)
|
|
46
|
+
if DEBUG and not all([VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY]):
|
|
47
|
+
import warnings
|
|
48
|
+
warnings.warn(
|
|
49
|
+
"VAPID keys not configured. "
|
|
50
|
+
"Run: npx web-push generate-vapid-keys"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# CORS (if frontend on different domain)
|
|
54
|
+
CORS_ALLOWED_ORIGINS = [
|
|
55
|
+
'http://localhost:3000', # Next.js dev
|
|
56
|
+
'https://your-domain.com',
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
CORS_ALLOW_CREDENTIALS = True
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Environment Variables
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
# .env
|
|
66
|
+
VAPID_PUBLIC_KEY=BM8xT...
|
|
67
|
+
VAPID_PRIVATE_KEY=abc123...
|
|
68
|
+
VAPID_MAILTO=mailto:your-email@example.com
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Step 3: Create Push Models
|
|
72
|
+
|
|
73
|
+
### models.py
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
# apps/notifications/models.py
|
|
77
|
+
from django.db import models
|
|
78
|
+
from django.contrib.auth.models import User
|
|
79
|
+
from django.utils import timezone
|
|
80
|
+
|
|
81
|
+
class PushSubscription(models.Model):
|
|
82
|
+
"""Store user push subscriptions"""
|
|
83
|
+
|
|
84
|
+
user = models.ForeignKey(
|
|
85
|
+
User,
|
|
86
|
+
on_delete=models.CASCADE,
|
|
87
|
+
related_name='push_subscriptions'
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
endpoint = models.URLField(max_length=500, unique=True)
|
|
91
|
+
p256dh = models.CharField(max_length=255) # Public key
|
|
92
|
+
auth = models.CharField(max_length=255) # Auth secret
|
|
93
|
+
|
|
94
|
+
created_at = models.DateTimeField(auto_now_add=True)
|
|
95
|
+
updated_at = models.DateTimeField(auto_now=True)
|
|
96
|
+
is_active = models.BooleanField(default=True)
|
|
97
|
+
|
|
98
|
+
# Browser info (optional)
|
|
99
|
+
user_agent = models.TextField(blank=True)
|
|
100
|
+
ip_address = models.GenericIPAddressField(null=True, blank=True)
|
|
101
|
+
|
|
102
|
+
class Meta:
|
|
103
|
+
db_table = 'push_subscriptions'
|
|
104
|
+
ordering = ['-created_at']
|
|
105
|
+
indexes = [
|
|
106
|
+
models.Index(fields=['user', 'is_active']),
|
|
107
|
+
models.Index(fields=['endpoint']),
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
def __str__(self):
|
|
111
|
+
return f"{self.user.username} - {self.endpoint[:50]}..."
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def subscription_info(self):
|
|
115
|
+
"""Format for pywebpush"""
|
|
116
|
+
return {
|
|
117
|
+
"endpoint": self.endpoint,
|
|
118
|
+
"keys": {
|
|
119
|
+
"p256dh": self.p256dh,
|
|
120
|
+
"auth": self.auth
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class PushNotification(models.Model):
|
|
126
|
+
"""Log sent notifications"""
|
|
127
|
+
|
|
128
|
+
subscription = models.ForeignKey(
|
|
129
|
+
PushSubscription,
|
|
130
|
+
on_delete=models.CASCADE,
|
|
131
|
+
related_name='notifications'
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
title = models.CharField(max_length=255)
|
|
135
|
+
body = models.TextField()
|
|
136
|
+
icon = models.URLField(blank=True)
|
|
137
|
+
url = models.URLField(blank=True)
|
|
138
|
+
|
|
139
|
+
sent_at = models.DateTimeField(auto_now_add=True)
|
|
140
|
+
success = models.BooleanField(default=True)
|
|
141
|
+
error_message = models.TextField(blank=True)
|
|
142
|
+
|
|
143
|
+
class Meta:
|
|
144
|
+
db_table = 'push_notifications'
|
|
145
|
+
ordering = ['-sent_at']
|
|
146
|
+
|
|
147
|
+
def __str__(self):
|
|
148
|
+
return f"{self.title} - {self.sent_at}"
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Migrate
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
python manage.py makemigrations
|
|
155
|
+
python manage.py migrate
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Step 4: Create Views
|
|
159
|
+
|
|
160
|
+
### views.py
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
# apps/notifications/views.py
|
|
164
|
+
from django.http import JsonResponse
|
|
165
|
+
from django.views.decorators.csrf import csrf_exempt
|
|
166
|
+
from django.views.decorators.http import require_http_methods
|
|
167
|
+
from django.contrib.auth.decorators import login_required
|
|
168
|
+
from django.conf import settings
|
|
169
|
+
import json
|
|
170
|
+
|
|
171
|
+
from .models import PushSubscription
|
|
172
|
+
from .utils import send_push_notification
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@csrf_exempt # Remove in production, use CSRF token
|
|
176
|
+
@require_http_methods(["POST"])
|
|
177
|
+
@login_required
|
|
178
|
+
def subscribe(request):
|
|
179
|
+
"""
|
|
180
|
+
Subscribe user to push notifications
|
|
181
|
+
|
|
182
|
+
Expects JSON body:
|
|
183
|
+
{
|
|
184
|
+
"endpoint": "https://...",
|
|
185
|
+
"keys": {
|
|
186
|
+
"p256dh": "...",
|
|
187
|
+
"auth": "..."
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
"""
|
|
191
|
+
try:
|
|
192
|
+
data = json.loads(request.body)
|
|
193
|
+
|
|
194
|
+
# Validate data
|
|
195
|
+
if not all([
|
|
196
|
+
data.get('endpoint'),
|
|
197
|
+
data.get('keys', {}).get('p256dh'),
|
|
198
|
+
data.get('keys', {}).get('auth')
|
|
199
|
+
]):
|
|
200
|
+
return JsonResponse({
|
|
201
|
+
'success': False,
|
|
202
|
+
'error': 'Invalid subscription data'
|
|
203
|
+
}, status=400)
|
|
204
|
+
|
|
205
|
+
# Create or update subscription
|
|
206
|
+
subscription, created = PushSubscription.objects.update_or_create(
|
|
207
|
+
endpoint=data['endpoint'],
|
|
208
|
+
defaults={
|
|
209
|
+
'user': request.user,
|
|
210
|
+
'p256dh': data['keys']['p256dh'],
|
|
211
|
+
'auth': data['keys']['auth'],
|
|
212
|
+
'is_active': True,
|
|
213
|
+
'user_agent': request.META.get('HTTP_USER_AGENT', ''),
|
|
214
|
+
'ip_address': request.META.get('REMOTE_ADDR'),
|
|
215
|
+
}
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
return JsonResponse({
|
|
219
|
+
'success': True,
|
|
220
|
+
'created': created,
|
|
221
|
+
'subscriptionId': subscription.id
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
except json.JSONDecodeError:
|
|
225
|
+
return JsonResponse({
|
|
226
|
+
'success': False,
|
|
227
|
+
'error': 'Invalid JSON'
|
|
228
|
+
}, status=400)
|
|
229
|
+
except Exception as e:
|
|
230
|
+
return JsonResponse({
|
|
231
|
+
'success': False,
|
|
232
|
+
'error': str(e)
|
|
233
|
+
}, status=500)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
@csrf_exempt
|
|
237
|
+
@require_http_methods(["POST"])
|
|
238
|
+
@login_required
|
|
239
|
+
def unsubscribe(request):
|
|
240
|
+
"""Unsubscribe from push notifications"""
|
|
241
|
+
try:
|
|
242
|
+
data = json.loads(request.body)
|
|
243
|
+
endpoint = data.get('endpoint')
|
|
244
|
+
|
|
245
|
+
if not endpoint:
|
|
246
|
+
return JsonResponse({
|
|
247
|
+
'success': False,
|
|
248
|
+
'error': 'Endpoint required'
|
|
249
|
+
}, status=400)
|
|
250
|
+
|
|
251
|
+
# Deactivate subscription
|
|
252
|
+
updated = PushSubscription.objects.filter(
|
|
253
|
+
endpoint=endpoint,
|
|
254
|
+
user=request.user
|
|
255
|
+
).update(is_active=False)
|
|
256
|
+
|
|
257
|
+
return JsonResponse({
|
|
258
|
+
'success': True,
|
|
259
|
+
'unsubscribed': updated > 0
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
except Exception as e:
|
|
263
|
+
return JsonResponse({
|
|
264
|
+
'success': False,
|
|
265
|
+
'error': str(e)
|
|
266
|
+
}, status=500)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
@require_http_methods(["POST"])
|
|
270
|
+
@login_required
|
|
271
|
+
def send_test_push(request):
|
|
272
|
+
"""Send test notification to user"""
|
|
273
|
+
try:
|
|
274
|
+
data = json.loads(request.body)
|
|
275
|
+
|
|
276
|
+
# Get user's active subscriptions
|
|
277
|
+
subscriptions = PushSubscription.objects.filter(
|
|
278
|
+
user=request.user,
|
|
279
|
+
is_active=True
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
if not subscriptions.exists():
|
|
283
|
+
return JsonResponse({
|
|
284
|
+
'success': False,
|
|
285
|
+
'error': 'No active subscriptions'
|
|
286
|
+
}, status=400)
|
|
287
|
+
|
|
288
|
+
# Notification data
|
|
289
|
+
notification = {
|
|
290
|
+
'title': data.get('title', 'Test Notification'),
|
|
291
|
+
'body': data.get('body', 'This is a test from Django'),
|
|
292
|
+
'icon': data.get('icon', '/icon-192.png'),
|
|
293
|
+
'url': data.get('url', '/'),
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
# Send to all subscriptions
|
|
297
|
+
results = []
|
|
298
|
+
for subscription in subscriptions:
|
|
299
|
+
success = send_push_notification(
|
|
300
|
+
subscription.subscription_info,
|
|
301
|
+
notification
|
|
302
|
+
)
|
|
303
|
+
results.append({
|
|
304
|
+
'subscriptionId': subscription.id,
|
|
305
|
+
'success': success
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
return JsonResponse({
|
|
309
|
+
'success': True,
|
|
310
|
+
'results': results
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
except Exception as e:
|
|
314
|
+
return JsonResponse({
|
|
315
|
+
'success': False,
|
|
316
|
+
'error': str(e)
|
|
317
|
+
}, status=500)
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### utils.py
|
|
321
|
+
|
|
322
|
+
```python
|
|
323
|
+
# apps/notifications/utils.py
|
|
324
|
+
from pywebpush import webpush, WebPushException
|
|
325
|
+
from django.conf import settings
|
|
326
|
+
import json
|
|
327
|
+
import logging
|
|
328
|
+
|
|
329
|
+
logger = logging.getLogger(__name__)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def send_push_notification(subscription_info, notification_data):
|
|
333
|
+
"""
|
|
334
|
+
Send push notification using pywebpush
|
|
335
|
+
|
|
336
|
+
Args:
|
|
337
|
+
subscription_info: dict with 'endpoint' and 'keys'
|
|
338
|
+
notification_data: dict with 'title', 'body', 'icon', etc.
|
|
339
|
+
|
|
340
|
+
Returns:
|
|
341
|
+
bool: True if successful, False otherwise
|
|
342
|
+
"""
|
|
343
|
+
try:
|
|
344
|
+
# Send push
|
|
345
|
+
response = webpush(
|
|
346
|
+
subscription_info=subscription_info,
|
|
347
|
+
data=json.dumps(notification_data),
|
|
348
|
+
vapid_private_key=settings.VAPID_PRIVATE_KEY,
|
|
349
|
+
vapid_claims={
|
|
350
|
+
"sub": settings.VAPID_MAILTO
|
|
351
|
+
}
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
logger.info(f"Push sent successfully: {notification_data.get('title')}")
|
|
355
|
+
return True
|
|
356
|
+
|
|
357
|
+
except WebPushException as e:
|
|
358
|
+
logger.error(f"Push failed: {e}")
|
|
359
|
+
|
|
360
|
+
# Handle specific errors
|
|
361
|
+
if e.response and e.response.status_code == 410:
|
|
362
|
+
# Subscription expired - deactivate it
|
|
363
|
+
logger.warning(f"Subscription expired: {subscription_info['endpoint']}")
|
|
364
|
+
|
|
365
|
+
return False
|
|
366
|
+
|
|
367
|
+
except Exception as e:
|
|
368
|
+
logger.error(f"Unexpected error sending push: {e}")
|
|
369
|
+
return False
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
## Step 5: Configure URLs
|
|
373
|
+
|
|
374
|
+
### urls.py
|
|
375
|
+
|
|
376
|
+
```python
|
|
377
|
+
# apps/notifications/urls.py
|
|
378
|
+
from django.urls import path
|
|
379
|
+
from . import views
|
|
380
|
+
|
|
381
|
+
app_name = 'notifications'
|
|
382
|
+
|
|
383
|
+
urlpatterns = [
|
|
384
|
+
path('push/subscribe/', views.subscribe, name='push_subscribe'),
|
|
385
|
+
path('push/unsubscribe/', views.unsubscribe, name='push_unsubscribe'),
|
|
386
|
+
path('push/test/', views.send_test_push, name='push_test'),
|
|
387
|
+
]
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
### Main urls.py
|
|
391
|
+
|
|
392
|
+
```python
|
|
393
|
+
# project/urls.py
|
|
394
|
+
from django.urls import path, include
|
|
395
|
+
|
|
396
|
+
urlpatterns = [
|
|
397
|
+
# ... other patterns
|
|
398
|
+
path('api/', include('apps.notifications.urls')),
|
|
399
|
+
]
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
## Step 6: Configure Frontend
|
|
403
|
+
|
|
404
|
+
### Using DjangoPushProvider
|
|
405
|
+
|
|
406
|
+
```tsx
|
|
407
|
+
// app/layout.tsx
|
|
408
|
+
import { DjangoPushProvider, PushPrompt } from '@/snippets/PushNotifications';
|
|
409
|
+
|
|
410
|
+
const VAPID_KEY = process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY!;
|
|
411
|
+
|
|
412
|
+
export default function Layout({ children }) {
|
|
413
|
+
return (
|
|
414
|
+
<DjangoPushProvider
|
|
415
|
+
vapidPublicKey={VAPID_KEY}
|
|
416
|
+
autoSubscribe={false}
|
|
417
|
+
onSubscribed={(sub) => console.log('Subscribed:', sub)}
|
|
418
|
+
onSubscribeError={(err) => console.error('Error:', err)}
|
|
419
|
+
>
|
|
420
|
+
{children}
|
|
421
|
+
<PushPrompt requirePWA={true} />
|
|
422
|
+
</DjangoPushProvider>
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
### Send Test Notification
|
|
428
|
+
|
|
429
|
+
```tsx
|
|
430
|
+
// components/TestPushButton.tsx
|
|
431
|
+
'use client';
|
|
432
|
+
|
|
433
|
+
import { useDjangoPushContext } from '@/snippets/PushNotifications';
|
|
434
|
+
|
|
435
|
+
export function TestPushButton() {
|
|
436
|
+
const { sendTestPush, isSubscribed } = useDjangoPushContext();
|
|
437
|
+
|
|
438
|
+
if (!isSubscribed) {
|
|
439
|
+
return null;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const handleTest = async () => {
|
|
443
|
+
const success = await sendTestPush({
|
|
444
|
+
title: 'Test from Frontend',
|
|
445
|
+
body: 'This is a test notification',
|
|
446
|
+
url: '/dashboard',
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
if (success) {
|
|
450
|
+
alert('Test notification sent!');
|
|
451
|
+
} else {
|
|
452
|
+
alert('Failed to send notification');
|
|
453
|
+
}
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
return (
|
|
457
|
+
<button onClick={handleTest}>
|
|
458
|
+
Send Test Notification
|
|
459
|
+
</button>
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
## Step 7: Admin Integration (Optional)
|
|
465
|
+
|
|
466
|
+
### admin.py
|
|
467
|
+
|
|
468
|
+
```python
|
|
469
|
+
# apps/notifications/admin.py
|
|
470
|
+
from django.contrib import admin
|
|
471
|
+
from .models import PushSubscription, PushNotification
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
@admin.register(PushSubscription)
|
|
475
|
+
class PushSubscriptionAdmin(admin.ModelAdmin):
|
|
476
|
+
list_display = ['user', 'endpoint_short', 'is_active', 'created_at']
|
|
477
|
+
list_filter = ['is_active', 'created_at']
|
|
478
|
+
search_fields = ['user__username', 'endpoint']
|
|
479
|
+
readonly_fields = ['created_at', 'updated_at']
|
|
480
|
+
|
|
481
|
+
def endpoint_short(self, obj):
|
|
482
|
+
return obj.endpoint[:50] + '...'
|
|
483
|
+
endpoint_short.short_description = 'Endpoint'
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
@admin.register(PushNotification)
|
|
487
|
+
class PushNotificationAdmin(admin.ModelAdmin):
|
|
488
|
+
list_display = ['title', 'subscription', 'success', 'sent_at']
|
|
489
|
+
list_filter = ['success', 'sent_at']
|
|
490
|
+
search_fields = ['title', 'body']
|
|
491
|
+
readonly_fields = ['sent_at']
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
## Step 8: Testing
|
|
495
|
+
|
|
496
|
+
### Test Flow
|
|
497
|
+
|
|
498
|
+
```bash
|
|
499
|
+
# 1. Start Django
|
|
500
|
+
python manage.py runserver
|
|
501
|
+
|
|
502
|
+
# 2. Start Next.js
|
|
503
|
+
npm run dev
|
|
504
|
+
|
|
505
|
+
# 3. Open browser
|
|
506
|
+
# Navigate to http://localhost:3000
|
|
507
|
+
|
|
508
|
+
# 4. Subscribe to push
|
|
509
|
+
# Click "Enable Notifications" button
|
|
510
|
+
|
|
511
|
+
# 5. Send test notification
|
|
512
|
+
# Click "Test" button or use Django admin
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
### Django Shell Testing
|
|
516
|
+
|
|
517
|
+
```python
|
|
518
|
+
python manage.py shell
|
|
519
|
+
|
|
520
|
+
>>> from apps.notifications.models import PushSubscription
|
|
521
|
+
>>> from apps.notifications.utils import send_push_notification
|
|
522
|
+
|
|
523
|
+
>>> # Get user subscription
|
|
524
|
+
>>> sub = PushSubscription.objects.filter(is_active=True).first()
|
|
525
|
+
|
|
526
|
+
>>> # Send test
|
|
527
|
+
>>> send_push_notification(
|
|
528
|
+
... sub.subscription_info,
|
|
529
|
+
... {
|
|
530
|
+
... 'title': 'Test from Shell',
|
|
531
|
+
... 'body': 'Testing push',
|
|
532
|
+
... 'url': '/dashboard'
|
|
533
|
+
... }
|
|
534
|
+
... )
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
## Production Considerations
|
|
538
|
+
|
|
539
|
+
### Security
|
|
540
|
+
|
|
541
|
+
```python
|
|
542
|
+
# Remove @csrf_exempt in production
|
|
543
|
+
# Use Django's CSRF protection
|
|
544
|
+
|
|
545
|
+
from django.views.decorators.csrf import ensure_csrf_cookie
|
|
546
|
+
|
|
547
|
+
@ensure_csrf_cookie
|
|
548
|
+
@require_http_methods(["POST"])
|
|
549
|
+
@login_required
|
|
550
|
+
def subscribe(request):
|
|
551
|
+
# ... implementation
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
### Rate Limiting
|
|
555
|
+
|
|
556
|
+
```python
|
|
557
|
+
from django.core.cache import cache
|
|
558
|
+
from django.http import JsonResponse
|
|
559
|
+
|
|
560
|
+
def rate_limit(request, key, max_requests=10, window=60):
|
|
561
|
+
"""Simple rate limiting"""
|
|
562
|
+
cache_key = f'rate_limit:{key}:{request.user.id}'
|
|
563
|
+
current = cache.get(cache_key, 0)
|
|
564
|
+
|
|
565
|
+
if current >= max_requests:
|
|
566
|
+
return JsonResponse({
|
|
567
|
+
'error': 'Rate limit exceeded'
|
|
568
|
+
}, status=429)
|
|
569
|
+
|
|
570
|
+
cache.set(cache_key, current + 1, window)
|
|
571
|
+
return None
|
|
572
|
+
|
|
573
|
+
@login_required
|
|
574
|
+
def send_test_push(request):
|
|
575
|
+
# Check rate limit
|
|
576
|
+
rate_limit_response = rate_limit(request, 'push_test', max_requests=5, window=60)
|
|
577
|
+
if rate_limit_response:
|
|
578
|
+
return rate_limit_response
|
|
579
|
+
|
|
580
|
+
# ... rest of implementation
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
### Logging
|
|
584
|
+
|
|
585
|
+
```python
|
|
586
|
+
# settings.py
|
|
587
|
+
LOGGING = {
|
|
588
|
+
'version': 1,
|
|
589
|
+
'disable_existing_loggers': False,
|
|
590
|
+
'handlers': {
|
|
591
|
+
'file': {
|
|
592
|
+
'level': 'INFO',
|
|
593
|
+
'class': 'logging.FileHandler',
|
|
594
|
+
'filename': 'push_notifications.log',
|
|
595
|
+
},
|
|
596
|
+
},
|
|
597
|
+
'loggers': {
|
|
598
|
+
'apps.notifications': {
|
|
599
|
+
'handlers': ['file'],
|
|
600
|
+
'level': 'INFO',
|
|
601
|
+
'propagate': True,
|
|
602
|
+
},
|
|
603
|
+
},
|
|
604
|
+
}
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
## Troubleshooting
|
|
608
|
+
|
|
609
|
+
### Issue: Subscription fails
|
|
610
|
+
|
|
611
|
+
**Check**:
|
|
612
|
+
1. CSRF token (if not using `@csrf_exempt`)
|
|
613
|
+
2. User authentication
|
|
614
|
+
3. CORS settings
|
|
615
|
+
4. VAPID keys match
|
|
616
|
+
|
|
617
|
+
### Issue: Push not received
|
|
618
|
+
|
|
619
|
+
**Check**:
|
|
620
|
+
1. Subscription is active in database
|
|
621
|
+
2. Service worker registered
|
|
622
|
+
3. Notification permission granted
|
|
623
|
+
4. VAPID keys correct on backend
|
|
624
|
+
|
|
625
|
+
### Issue: 410 Gone error
|
|
626
|
+
|
|
627
|
+
**Solution**: Subscription expired, remove from database
|
|
628
|
+
|
|
629
|
+
```python
|
|
630
|
+
# Handle in utils.py
|
|
631
|
+
if e.response and e.response.status_code == 410:
|
|
632
|
+
from .models import PushSubscription
|
|
633
|
+
PushSubscription.objects.filter(
|
|
634
|
+
endpoint=subscription_info['endpoint']
|
|
635
|
+
).update(is_active=False)
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
## Next Steps
|
|
639
|
+
|
|
640
|
+
- [VAPID Setup](./vapid-setup.md) - Configure VAPID keys
|
|
641
|
+
- [Service Worker](./service-worker.md) - Configure SW
|
|
642
|
+
- [Main README](../../README.md) - API reference
|
|
643
|
+
|
|
644
|
+
## References
|
|
645
|
+
|
|
646
|
+
- [pywebpush Documentation](https://github.com/web-push-libs/pywebpush)
|
|
647
|
+
- [Django REST Framework](https://www.django-rest-framework.org/)
|
|
648
|
+
- [Web Push Protocol](https://datatracker.ietf.org/doc/html/rfc8030)
|