@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,352 @@
|
|
|
1
|
+
# VAPID Setup Guide
|
|
2
|
+
|
|
3
|
+
Complete guide to generating and configuring VAPID keys for Web Push Notifications.
|
|
4
|
+
|
|
5
|
+
## What is VAPID?
|
|
6
|
+
|
|
7
|
+
**VAPID** (Voluntary Application Server Identification) is a standard that authenticates your web push notifications.
|
|
8
|
+
|
|
9
|
+
### Why VAPID?
|
|
10
|
+
|
|
11
|
+
Without VAPID, push services can't verify that push notifications actually come from your server. VAPID provides:
|
|
12
|
+
|
|
13
|
+
- **Authentication** - Proves notifications come from you
|
|
14
|
+
- **Security** - Prevents notification spoofing
|
|
15
|
+
- **Privacy** - Required by modern push services
|
|
16
|
+
|
|
17
|
+
## Key Pairs
|
|
18
|
+
|
|
19
|
+
VAPID uses a **public/private key pair**:
|
|
20
|
+
|
|
21
|
+
| Key | Location | Purpose |
|
|
22
|
+
|-----|----------|---------|
|
|
23
|
+
| **Public** | Frontend + Backend | Identify your app to push service |
|
|
24
|
+
| **Private** | Backend ONLY | Sign push notifications |
|
|
25
|
+
|
|
26
|
+
⚠️ **CRITICAL**: Never expose private key in frontend code!
|
|
27
|
+
|
|
28
|
+
## Step 1: Generate VAPID Keys
|
|
29
|
+
|
|
30
|
+
### Option A: Using web-push (Recommended)
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
# Install web-push globally
|
|
34
|
+
npm install -g web-push
|
|
35
|
+
|
|
36
|
+
# Generate keys
|
|
37
|
+
web-push generate-vapid-keys
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
**Output:**
|
|
41
|
+
```
|
|
42
|
+
=======================================
|
|
43
|
+
|
|
44
|
+
Public Key:
|
|
45
|
+
BM8xTpJKZ1234567890abcdefghijklmnopqrstuvwxyz...
|
|
46
|
+
|
|
47
|
+
Private Key:
|
|
48
|
+
abc123def456ghi789jkl012mno345pqr678stu901vwx...
|
|
49
|
+
|
|
50
|
+
=======================================
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Option B: Using npx (No install)
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
npx web-push generate-vapid-keys
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Option C: Programmatically
|
|
60
|
+
|
|
61
|
+
```javascript
|
|
62
|
+
// generate-vapid.js
|
|
63
|
+
const webpush = require('web-push');
|
|
64
|
+
|
|
65
|
+
const vapidKeys = webpush.generateVAPIDKeys();
|
|
66
|
+
|
|
67
|
+
console.log('Public Key:', vapidKeys.publicKey);
|
|
68
|
+
console.log('Private Key:', vapidKeys.privateKey);
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
node generate-vapid.js
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Step 2: Store Keys Securely
|
|
76
|
+
|
|
77
|
+
### Frontend Environment Variables
|
|
78
|
+
|
|
79
|
+
Create/update `.env.local` (frontend):
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
# .env.local (frontend - Next.js app)
|
|
83
|
+
NEXT_PUBLIC_VAPID_PUBLIC_KEY=BM8xTpJKZ1234567890abcdefghijklmnopqrstuvwxyz...
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**Notes:**
|
|
87
|
+
- ✅ `NEXT_PUBLIC_` prefix makes it available in browser
|
|
88
|
+
- ✅ Public key is safe to expose
|
|
89
|
+
- ✅ Commit `.env.example` with placeholder
|
|
90
|
+
- ❌ Never commit `.env.local` with real keys
|
|
91
|
+
|
|
92
|
+
### Backend Environment Variables
|
|
93
|
+
|
|
94
|
+
Create/update `.env` (backend - Django):
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
# .env (backend - Django)
|
|
98
|
+
VAPID_PUBLIC_KEY=BM8xTpJKZ1234567890abcdefghijklmnopqrstuvwxyz...
|
|
99
|
+
VAPID_PRIVATE_KEY=abc123def456ghi789jkl012mno345pqr678stu901vwx...
|
|
100
|
+
VAPID_MAILTO=mailto:your-email@example.com
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
**Notes:**
|
|
104
|
+
- ⚠️ **CRITICAL**: Private key NEVER in frontend
|
|
105
|
+
- ⚠️ Never commit `.env` to git
|
|
106
|
+
- ✅ Use secure environment (production secrets manager)
|
|
107
|
+
- ✅ `VAPID_MAILTO` should be valid email with `mailto:` prefix
|
|
108
|
+
|
|
109
|
+
### Example .env.example
|
|
110
|
+
|
|
111
|
+
Create `.env.example` (safe to commit):
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
# Frontend (.env.example)
|
|
115
|
+
NEXT_PUBLIC_VAPID_PUBLIC_KEY=your_public_key_here
|
|
116
|
+
|
|
117
|
+
# Backend (.env.example)
|
|
118
|
+
VAPID_PUBLIC_KEY=your_public_key_here
|
|
119
|
+
VAPID_PRIVATE_KEY=your_private_key_here
|
|
120
|
+
VAPID_MAILTO=mailto:your-email@example.com
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Step 3: Configure Frontend
|
|
124
|
+
|
|
125
|
+
### Using PushNotifications Snippet
|
|
126
|
+
|
|
127
|
+
```tsx
|
|
128
|
+
// app/layout.tsx
|
|
129
|
+
import { DjangoPushProvider } from '@/snippets/PushNotifications';
|
|
130
|
+
|
|
131
|
+
const VAPID_KEY = process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY!;
|
|
132
|
+
|
|
133
|
+
export default function Layout({ children }) {
|
|
134
|
+
return (
|
|
135
|
+
<DjangoPushProvider vapidPublicKey={VAPID_KEY}>
|
|
136
|
+
{children}
|
|
137
|
+
</DjangoPushProvider>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Manual Configuration
|
|
143
|
+
|
|
144
|
+
```tsx
|
|
145
|
+
import { usePushNotifications } from '@/snippets/PushNotifications';
|
|
146
|
+
|
|
147
|
+
function Component() {
|
|
148
|
+
const { subscribe } = usePushNotifications({
|
|
149
|
+
vapidPublicKey: process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY!,
|
|
150
|
+
subscribeEndpoint: '/api/push/subscribe',
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
return <button onClick={subscribe}>Subscribe</button>;
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Step 4: Configure Backend (Django)
|
|
158
|
+
|
|
159
|
+
### Install Dependencies
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
pip install pywebpush
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Django Settings
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
# settings.py
|
|
169
|
+
import os
|
|
170
|
+
|
|
171
|
+
# VAPID Configuration
|
|
172
|
+
VAPID_PUBLIC_KEY = os.getenv('VAPID_PUBLIC_KEY')
|
|
173
|
+
VAPID_PRIVATE_KEY = os.getenv('VAPID_PRIVATE_KEY')
|
|
174
|
+
VAPID_MAILTO = os.getenv('VAPID_MAILTO', 'mailto:admin@example.com')
|
|
175
|
+
|
|
176
|
+
# Validate VAPID keys
|
|
177
|
+
if not VAPID_PUBLIC_KEY or not VAPID_PRIVATE_KEY:
|
|
178
|
+
raise ValueError("VAPID keys not configured. Run: web-push generate-vapid-keys")
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Django View (Subscribe Endpoint)
|
|
182
|
+
|
|
183
|
+
```python
|
|
184
|
+
# views.py
|
|
185
|
+
from django.http import JsonResponse
|
|
186
|
+
from django.views.decorators.csrf import csrf_exempt
|
|
187
|
+
from django.conf import settings
|
|
188
|
+
import json
|
|
189
|
+
|
|
190
|
+
@csrf_exempt
|
|
191
|
+
def push_subscribe(request):
|
|
192
|
+
if request.method == 'POST':
|
|
193
|
+
subscription_info = json.loads(request.body)
|
|
194
|
+
|
|
195
|
+
# Store subscription in database
|
|
196
|
+
# (Your model here)
|
|
197
|
+
|
|
198
|
+
return JsonResponse({
|
|
199
|
+
'success': True,
|
|
200
|
+
'publicKey': settings.VAPID_PUBLIC_KEY
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
return JsonResponse({'error': 'Method not allowed'}, status=405)
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Send Push Notification
|
|
207
|
+
|
|
208
|
+
```python
|
|
209
|
+
# utils/push.py
|
|
210
|
+
from pywebpush import webpush, WebPushException
|
|
211
|
+
from django.conf import settings
|
|
212
|
+
import json
|
|
213
|
+
|
|
214
|
+
def send_push_notification(subscription_info, notification_data):
|
|
215
|
+
"""
|
|
216
|
+
Send push notification to a subscriber
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
subscription_info: PushSubscription object from frontend
|
|
220
|
+
notification_data: dict with 'title', 'body', 'icon', etc.
|
|
221
|
+
"""
|
|
222
|
+
try:
|
|
223
|
+
webpush(
|
|
224
|
+
subscription_info=subscription_info,
|
|
225
|
+
data=json.dumps(notification_data),
|
|
226
|
+
vapid_private_key=settings.VAPID_PRIVATE_KEY,
|
|
227
|
+
vapid_claims={
|
|
228
|
+
"sub": settings.VAPID_MAILTO
|
|
229
|
+
}
|
|
230
|
+
)
|
|
231
|
+
return True
|
|
232
|
+
except WebPushException as e:
|
|
233
|
+
print(f"Push failed: {e}")
|
|
234
|
+
return False
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
## Step 5: Verify Configuration
|
|
238
|
+
|
|
239
|
+
### Frontend Verification
|
|
240
|
+
|
|
241
|
+
```tsx
|
|
242
|
+
// Check if VAPID key is loaded
|
|
243
|
+
console.log('VAPID Key:', process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY?.slice(0, 10) + '...');
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Expected output:
|
|
247
|
+
```
|
|
248
|
+
VAPID Key: BM8xTpJKZ1...
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Backend Verification
|
|
252
|
+
|
|
253
|
+
```python
|
|
254
|
+
# Django shell
|
|
255
|
+
python manage.py shell
|
|
256
|
+
|
|
257
|
+
>>> from django.conf import settings
|
|
258
|
+
>>> print(f"Public: {settings.VAPID_PUBLIC_KEY[:10]}...")
|
|
259
|
+
>>> print(f"Private: {settings.VAPID_PRIVATE_KEY[:10]}...")
|
|
260
|
+
>>> print(f"Mailto: {settings.VAPID_MAILTO}")
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Expected output:
|
|
264
|
+
```
|
|
265
|
+
Public: BM8xTpJKZ1...
|
|
266
|
+
Private: abc123def4...
|
|
267
|
+
Mailto: mailto:you@example.com
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
## Security Checklist
|
|
271
|
+
|
|
272
|
+
- [ ] ✅ Public key in `NEXT_PUBLIC_` env var (frontend)
|
|
273
|
+
- [ ] ✅ Private key in backend-only env var (no `NEXT_PUBLIC_`)
|
|
274
|
+
- [ ] ✅ `.env.local` and `.env` in `.gitignore`
|
|
275
|
+
- [ ] ✅ `.env.example` committed with placeholders
|
|
276
|
+
- [ ] ⚠️ Private key NEVER in frontend code
|
|
277
|
+
- [ ] ⚠️ Private key NEVER in git
|
|
278
|
+
- [ ] ⚠️ Production keys in secure secrets manager
|
|
279
|
+
- [ ] ✅ VAPID_MAILTO is valid email with `mailto:` prefix
|
|
280
|
+
|
|
281
|
+
## Troubleshooting
|
|
282
|
+
|
|
283
|
+
### Error: "VAPID key must be base64 URL-safe"
|
|
284
|
+
|
|
285
|
+
**Problem**: Invalid key format
|
|
286
|
+
|
|
287
|
+
**Solution**: Regenerate keys with `web-push generate-vapid-keys`
|
|
288
|
+
|
|
289
|
+
### Error: "Public key not found"
|
|
290
|
+
|
|
291
|
+
**Problem**: Environment variable not loaded
|
|
292
|
+
|
|
293
|
+
**Solution**:
|
|
294
|
+
1. Check `.env.local` exists in frontend root
|
|
295
|
+
2. Restart dev server (`npm run dev`)
|
|
296
|
+
3. Verify: `console.log(process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY)`
|
|
297
|
+
|
|
298
|
+
### Error: "Push notification failed"
|
|
299
|
+
|
|
300
|
+
**Problem**: Private key or mailto incorrect
|
|
301
|
+
|
|
302
|
+
**Solution**:
|
|
303
|
+
1. Check backend `.env` has `VAPID_PRIVATE_KEY` and `VAPID_MAILTO`
|
|
304
|
+
2. Verify mailto format: `mailto:email@example.com`
|
|
305
|
+
3. Restart Django server
|
|
306
|
+
|
|
307
|
+
## Production Deployment
|
|
308
|
+
|
|
309
|
+
### Vercel (Frontend)
|
|
310
|
+
|
|
311
|
+
```bash
|
|
312
|
+
# Add environment variable in Vercel dashboard
|
|
313
|
+
vercel env add NEXT_PUBLIC_VAPID_PUBLIC_KEY production
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
Or via CLI:
|
|
317
|
+
```bash
|
|
318
|
+
vercel env add NEXT_PUBLIC_VAPID_PUBLIC_KEY
|
|
319
|
+
# Paste key when prompted
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
### Heroku (Backend)
|
|
323
|
+
|
|
324
|
+
```bash
|
|
325
|
+
heroku config:set VAPID_PUBLIC_KEY="BM8xT..."
|
|
326
|
+
heroku config:set VAPID_PRIVATE_KEY="abc123..."
|
|
327
|
+
heroku config:set VAPID_MAILTO="mailto:you@example.com"
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
### Docker
|
|
331
|
+
|
|
332
|
+
```yaml
|
|
333
|
+
# docker-compose.yml
|
|
334
|
+
services:
|
|
335
|
+
backend:
|
|
336
|
+
environment:
|
|
337
|
+
- VAPID_PUBLIC_KEY=${VAPID_PUBLIC_KEY}
|
|
338
|
+
- VAPID_PRIVATE_KEY=${VAPID_PRIVATE_KEY}
|
|
339
|
+
- VAPID_MAILTO=${VAPID_MAILTO}
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
## Next Steps
|
|
343
|
+
|
|
344
|
+
- [Django Integration Guide](./django-integration.md) - Connect backend
|
|
345
|
+
- [Service Worker Setup](./service-worker.md) - Configure SW
|
|
346
|
+
- [Main README](../../README.md) - API reference
|
|
347
|
+
|
|
348
|
+
## References
|
|
349
|
+
|
|
350
|
+
- [Web Push Protocol](https://datatracker.ietf.org/doc/html/rfc8030)
|
|
351
|
+
- [VAPID Specification](https://datatracker.ietf.org/doc/html/rfc8292)
|
|
352
|
+
- [web-push Library](https://github.com/web-push-libs/web-push)
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
# Push Notifications
|
|
2
|
+
|
|
3
|
+
Web Push Notifications snippet for web applications.
|
|
4
|
+
|
|
5
|
+
## Responsibility
|
|
6
|
+
|
|
7
|
+
**Web Push Notifications management**
|
|
8
|
+
|
|
9
|
+
- Push subscription/unsubscription
|
|
10
|
+
- Permission management
|
|
11
|
+
- VAPID key handling
|
|
12
|
+
- Django-CFG backend integration
|
|
13
|
+
- Push message accumulation and display
|
|
14
|
+
|
|
15
|
+
## Quick Start
|
|
16
|
+
|
|
17
|
+
### Basic Usage
|
|
18
|
+
|
|
19
|
+
```tsx
|
|
20
|
+
// app/layout.tsx
|
|
21
|
+
import { PushProvider, usePush, PushPrompt } from '@/snippets/PushNotifications';
|
|
22
|
+
|
|
23
|
+
const VAPID_KEY = process.env.NEXT_PUBLIC_VAPID_KEY;
|
|
24
|
+
|
|
25
|
+
export default function RootLayout({ children }) {
|
|
26
|
+
return (
|
|
27
|
+
<html>
|
|
28
|
+
<body>
|
|
29
|
+
<PushProvider vapidPublicKey={VAPID_KEY}>
|
|
30
|
+
{children}
|
|
31
|
+
<PushPrompt requirePWA={true} />
|
|
32
|
+
</PushProvider>
|
|
33
|
+
</body>
|
|
34
|
+
</html>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Django Integration
|
|
40
|
+
|
|
41
|
+
```tsx
|
|
42
|
+
// app/layout.tsx
|
|
43
|
+
import { DjangoPushProvider } from '@/snippets/PushNotifications';
|
|
44
|
+
|
|
45
|
+
export default function RootLayout({ children }) {
|
|
46
|
+
return (
|
|
47
|
+
<html>
|
|
48
|
+
<body>
|
|
49
|
+
<DjangoPushProvider
|
|
50
|
+
vapidPublicKey={process.env.NEXT_PUBLIC_VAPID_KEY}
|
|
51
|
+
autoSubscribe={false}
|
|
52
|
+
>
|
|
53
|
+
{children}
|
|
54
|
+
</DjangoPushProvider>
|
|
55
|
+
</body>
|
|
56
|
+
</html>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## API
|
|
62
|
+
|
|
63
|
+
### `<PushProvider>`
|
|
64
|
+
|
|
65
|
+
Generic push notifications provider:
|
|
66
|
+
|
|
67
|
+
```tsx
|
|
68
|
+
<PushProvider
|
|
69
|
+
vapidPublicKey={VAPID_KEY}
|
|
70
|
+
subscribeEndpoint="/api/push/subscribe" // Optional
|
|
71
|
+
sendEndpoint="/api/push/send" // Optional
|
|
72
|
+
onPushReceived={(push) => console.log(push)}
|
|
73
|
+
>
|
|
74
|
+
{children}
|
|
75
|
+
</PushProvider>
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### `<DjangoPushProvider>`
|
|
79
|
+
|
|
80
|
+
Django-CFG specific provider:
|
|
81
|
+
|
|
82
|
+
```tsx
|
|
83
|
+
<DjangoPushProvider
|
|
84
|
+
vapidPublicKey={VAPID_KEY}
|
|
85
|
+
autoSubscribe={false}
|
|
86
|
+
onSubscribed={(subscription) => console.log('Subscribed!', subscription)}
|
|
87
|
+
onSubscribeError={(error) => console.error('Error:', error)}
|
|
88
|
+
onUnsubscribed={() => console.log('Unsubscribed')}
|
|
89
|
+
>
|
|
90
|
+
{children}
|
|
91
|
+
</DjangoPushProvider>
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### `<PushPrompt />`
|
|
95
|
+
|
|
96
|
+
Push notification permission prompt:
|
|
97
|
+
|
|
98
|
+
```tsx
|
|
99
|
+
<PushPrompt
|
|
100
|
+
vapidPublicKey={VAPID_KEY}
|
|
101
|
+
requirePWA={true} // Only show if PWA installed (default: true)
|
|
102
|
+
delayMs={5000} // Delay before showing (default: 5000)
|
|
103
|
+
resetAfterDays={7} // Auto-reset after days (default: 7)
|
|
104
|
+
onEnabled={() => console.log('Enabled!')}
|
|
105
|
+
onDismissed={() => console.log('Dismissed')}
|
|
106
|
+
/>
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
**Behavior:**
|
|
110
|
+
- Shows after PWA is installed (if `requirePWA={true}`)
|
|
111
|
+
- Waits for delay before showing
|
|
112
|
+
- Dismissible (saved to localStorage)
|
|
113
|
+
- Auto-resets after configured days
|
|
114
|
+
- Respects user's permission choice
|
|
115
|
+
|
|
116
|
+
### Hooks
|
|
117
|
+
|
|
118
|
+
#### `usePush()`
|
|
119
|
+
|
|
120
|
+
Access push state and actions:
|
|
121
|
+
|
|
122
|
+
```tsx
|
|
123
|
+
import { usePush } from '@/snippets/PushNotifications';
|
|
124
|
+
|
|
125
|
+
function NotifyButton() {
|
|
126
|
+
const {
|
|
127
|
+
isSupported,
|
|
128
|
+
permission,
|
|
129
|
+
isSubscribed,
|
|
130
|
+
subscription,
|
|
131
|
+
pushes,
|
|
132
|
+
subscribe,
|
|
133
|
+
unsubscribe,
|
|
134
|
+
sendPush,
|
|
135
|
+
clearPushes,
|
|
136
|
+
removePush,
|
|
137
|
+
} = usePush();
|
|
138
|
+
|
|
139
|
+
return (
|
|
140
|
+
<div>
|
|
141
|
+
{!isSubscribed && (
|
|
142
|
+
<button onClick={subscribe}>Enable Notifications</button>
|
|
143
|
+
)}
|
|
144
|
+
{pushes.map(push => (
|
|
145
|
+
<div key={push.id}>{push.title}</div>
|
|
146
|
+
))}
|
|
147
|
+
</div>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
#### `useDjangoPushContext()`
|
|
153
|
+
|
|
154
|
+
Django-specific push context:
|
|
155
|
+
|
|
156
|
+
```tsx
|
|
157
|
+
import { useDjangoPushContext } from '@/snippets/PushNotifications';
|
|
158
|
+
|
|
159
|
+
function NotifyButton() {
|
|
160
|
+
const {
|
|
161
|
+
isSupported,
|
|
162
|
+
permission,
|
|
163
|
+
isSubscribed,
|
|
164
|
+
subscription,
|
|
165
|
+
isLoading,
|
|
166
|
+
error,
|
|
167
|
+
subscribe,
|
|
168
|
+
unsubscribe,
|
|
169
|
+
sendTestPush,
|
|
170
|
+
} = useDjangoPushContext();
|
|
171
|
+
|
|
172
|
+
const handleTest = async () => {
|
|
173
|
+
await sendTestPush({
|
|
174
|
+
title: 'Test',
|
|
175
|
+
body: 'This is a test notification',
|
|
176
|
+
url: '/dashboard',
|
|
177
|
+
});
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
return (
|
|
181
|
+
<div>
|
|
182
|
+
{!isSubscribed && (
|
|
183
|
+
<button onClick={subscribe} disabled={isLoading}>
|
|
184
|
+
Enable Notifications
|
|
185
|
+
</button>
|
|
186
|
+
)}
|
|
187
|
+
{isSubscribed && (
|
|
188
|
+
<button onClick={handleTest}>Send Test</button>
|
|
189
|
+
)}
|
|
190
|
+
</div>
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
#### `usePushNotifications()`
|
|
196
|
+
|
|
197
|
+
Low-level push hook (used internally):
|
|
198
|
+
|
|
199
|
+
```tsx
|
|
200
|
+
import { usePushNotifications } from '@/snippets/PushNotifications';
|
|
201
|
+
|
|
202
|
+
function Component() {
|
|
203
|
+
const {
|
|
204
|
+
isSupported,
|
|
205
|
+
permission,
|
|
206
|
+
isSubscribed,
|
|
207
|
+
subscription,
|
|
208
|
+
subscribe,
|
|
209
|
+
unsubscribe,
|
|
210
|
+
} = usePushNotifications({
|
|
211
|
+
vapidPublicKey: VAPID_KEY,
|
|
212
|
+
subscribeEndpoint: '/api/push/subscribe',
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
return <div>{/* ... */}</div>;
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## Usage with PWA Install
|
|
220
|
+
|
|
221
|
+
Use together with the PWAInstall snippet:
|
|
222
|
+
|
|
223
|
+
```tsx
|
|
224
|
+
import { PwaProvider, A2HSHint } from '@/snippets/PWAInstall';
|
|
225
|
+
import { DjangoPushProvider, PushPrompt } from '@/snippets/PushNotifications';
|
|
226
|
+
|
|
227
|
+
export default function Layout({ children }) {
|
|
228
|
+
return (
|
|
229
|
+
<PwaProvider>
|
|
230
|
+
<DjangoPushProvider vapidPublicKey={VAPID_KEY}>
|
|
231
|
+
{children}
|
|
232
|
+
|
|
233
|
+
{/* PWA Install hint */}
|
|
234
|
+
<A2HSHint resetAfterDays={3} />
|
|
235
|
+
|
|
236
|
+
{/* Push notification prompt (shown after PWA install) */}
|
|
237
|
+
<PushPrompt
|
|
238
|
+
requirePWA={true}
|
|
239
|
+
delayMs={5000}
|
|
240
|
+
resetAfterDays={7}
|
|
241
|
+
/>
|
|
242
|
+
</DjangoPushProvider>
|
|
243
|
+
</PwaProvider>
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
## Browser Support
|
|
249
|
+
|
|
250
|
+
| Platform | Browser | Support |
|
|
251
|
+
|----------|---------|---------|
|
|
252
|
+
| iOS | Safari 16.4+ | ✅ Push supported |
|
|
253
|
+
| iOS | Chrome/Firefox | ❌ No push support |
|
|
254
|
+
| Android | Chrome | ✅ Full support |
|
|
255
|
+
| Android | Firefox | ✅ Full support |
|
|
256
|
+
| Desktop | Chrome/Edge/Firefox | ✅ Full support |
|
|
257
|
+
|
|
258
|
+
## Architecture
|
|
259
|
+
|
|
260
|
+
```
|
|
261
|
+
PushNotifications/
|
|
262
|
+
├── context/
|
|
263
|
+
│ ├── PushContext.tsx # Generic push state
|
|
264
|
+
│ └── DjangoPushContext.tsx # Django integration
|
|
265
|
+
├── components/
|
|
266
|
+
│ └── PushPrompt.tsx # Permission prompt
|
|
267
|
+
├── hooks/
|
|
268
|
+
│ ├── usePushNotifications.ts # Core push logic
|
|
269
|
+
│ └── useDjangoPush.ts # Django push logic
|
|
270
|
+
├── utils/
|
|
271
|
+
│ ├── vapid.ts # VAPID key utils
|
|
272
|
+
│ ├── localStorage.ts # Persistence
|
|
273
|
+
│ └── logger.ts # Logging
|
|
274
|
+
└── types/
|
|
275
|
+
└── push.ts
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
## Separation from PWA Install
|
|
279
|
+
|
|
280
|
+
This snippet is **completely independent** from PWA installation:
|
|
281
|
+
|
|
282
|
+
- **PWAInstall** → Handles device installation
|
|
283
|
+
- **PushNotifications** → Handles web push subscriptions
|
|
284
|
+
|
|
285
|
+
Both can be used together or separately.
|
|
286
|
+
|
|
287
|
+
## VAPID Keys
|
|
288
|
+
|
|
289
|
+
Generate VAPID keys using:
|
|
290
|
+
|
|
291
|
+
```bash
|
|
292
|
+
npx web-push generate-vapid-keys
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
Store public key in environment:
|
|
296
|
+
```env
|
|
297
|
+
NEXT_PUBLIC_VAPID_KEY=BM8xT...
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
## Service Worker
|
|
301
|
+
|
|
302
|
+
Ensure your service worker handles push events:
|
|
303
|
+
|
|
304
|
+
```js
|
|
305
|
+
// public/sw.js
|
|
306
|
+
self.addEventListener('push', (event) => {
|
|
307
|
+
const data = event.data.json();
|
|
308
|
+
|
|
309
|
+
event.waitUntil(
|
|
310
|
+
self.registration.showNotification(data.title, {
|
|
311
|
+
body: data.body,
|
|
312
|
+
icon: data.icon || '/icon-192.png',
|
|
313
|
+
badge: data.badge || '/badge-72.png',
|
|
314
|
+
data: data.data,
|
|
315
|
+
})
|
|
316
|
+
);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
self.addEventListener('notificationclick', (event) => {
|
|
320
|
+
event.notification.close();
|
|
321
|
+
|
|
322
|
+
if (event.notification.data?.url) {
|
|
323
|
+
event.waitUntil(
|
|
324
|
+
clients.openWindow(event.notification.data.url)
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
```
|