@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
|
@@ -1,387 +0,0 @@
|
|
|
1
|
-
# PWA Install (Simplified)
|
|
2
|
-
|
|
3
|
-
Ultra-simple PWA installation for Cmdop. No tracking, no metrics, no complexity — just install.
|
|
4
|
-
|
|
5
|
-
## Quick Start (3 lines)
|
|
6
|
-
|
|
7
|
-
```tsx
|
|
8
|
-
// app/layout.tsx
|
|
9
|
-
import { InstallProvider, A2HSHint } from '@/app/_snippets/PwaInstall';
|
|
10
|
-
import { settings } from '@core/settings';
|
|
11
|
-
|
|
12
|
-
export default function RootLayout({ children }) {
|
|
13
|
-
return (
|
|
14
|
-
<html>
|
|
15
|
-
<body>
|
|
16
|
-
<InstallProvider>
|
|
17
|
-
{children}
|
|
18
|
-
<A2HSHint
|
|
19
|
-
resetAfterDays={3}
|
|
20
|
-
logo={settings.app.icons.logo192}
|
|
21
|
-
/>
|
|
22
|
-
</InstallProvider>
|
|
23
|
-
</body>
|
|
24
|
-
</html>
|
|
25
|
-
);
|
|
26
|
-
}
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
Done. That's it.
|
|
30
|
-
|
|
31
|
-
---
|
|
32
|
-
|
|
33
|
-
## What You Get
|
|
34
|
-
|
|
35
|
-
✅ **Unified UX** → Same hint position (bottom) for both iOS & Android
|
|
36
|
-
✅ **iOS Safari** → Click hint → Opens visual step-by-step guide
|
|
37
|
-
✅ **Android Chrome** → Click hint → Native install prompt
|
|
38
|
-
✅ **Visual guide** → Adaptive (drawer on mobile, modal on desktop)
|
|
39
|
-
✅ **Zero config** → Works out of the box
|
|
40
|
-
✅ **Smart reset** → Re-appears after 3 days (user gets second chance)
|
|
41
|
-
✅ **No spam** → Dismissible, respects user choice
|
|
42
|
-
|
|
43
|
-
---
|
|
44
|
-
|
|
45
|
-
## API
|
|
46
|
-
|
|
47
|
-
### 1. `<InstallProvider>`
|
|
48
|
-
|
|
49
|
-
Wrap your app once:
|
|
50
|
-
|
|
51
|
-
```tsx
|
|
52
|
-
<InstallProvider>
|
|
53
|
-
{children}
|
|
54
|
-
</InstallProvider>
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
### 2. `<A2HSHint />`
|
|
58
|
-
|
|
59
|
-
**Unified install hint for iOS & Android** (auto-shows, dismissible):
|
|
60
|
-
|
|
61
|
-
```tsx
|
|
62
|
-
<A2HSHint />
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
**Behavior (iOS Safari):**
|
|
66
|
-
- Shows after 3 seconds
|
|
67
|
-
- Text: "Keep terminal with you → Tap to learn how"
|
|
68
|
-
- Click → Opens visual guide (adaptive: drawer on mobile, modal on desktop)
|
|
69
|
-
- Dismissible (saved to localStorage)
|
|
70
|
-
- **Auto-resets after 3 days**
|
|
71
|
-
|
|
72
|
-
**Behavior (Android Chrome):**
|
|
73
|
-
- Shows after 3 seconds
|
|
74
|
-
- Text: "Install Cmdop → Tap to install"
|
|
75
|
-
- Click → Triggers native install prompt
|
|
76
|
-
- Dismissible (saved to localStorage)
|
|
77
|
-
- **Auto-resets after 3 days**
|
|
78
|
-
|
|
79
|
-
**Unified:**
|
|
80
|
-
- ✅ Same position (bottom)
|
|
81
|
-
- ✅ Same style (inline hint)
|
|
82
|
-
- ✅ Same interaction (tap to action)
|
|
83
|
-
- ✅ Platform-specific content
|
|
84
|
-
|
|
85
|
-
**Props (optional):**
|
|
86
|
-
```tsx
|
|
87
|
-
<A2HSHint
|
|
88
|
-
resetAfterDays={3} // Default: 3 days (set to null for never)
|
|
89
|
-
delayMs={3000} // Default: 3 seconds
|
|
90
|
-
forceShow={isDevelopment} // Show on ANY browser (for dev testing)
|
|
91
|
-
logo={settings.app.icons.logo192} // App logo URL (fallback: Share icon)
|
|
92
|
-
/>
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
### 3. `<InstallButton />` (Optional)
|
|
96
|
-
|
|
97
|
-
**Standalone install button** (if you want button in header instead of bottom hint):
|
|
98
|
-
|
|
99
|
-
```tsx
|
|
100
|
-
// In header
|
|
101
|
-
<InstallButton
|
|
102
|
-
text="Install"
|
|
103
|
-
className="ml-auto"
|
|
104
|
-
forceShow={isDevelopment}
|
|
105
|
-
/>
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
**Behavior:**
|
|
109
|
-
- Only Android Chrome (when `beforeinstallprompt` fires)
|
|
110
|
-
- Auto-hides when installed
|
|
111
|
-
- Triggers native install prompt
|
|
112
|
-
- Loading state included
|
|
113
|
-
|
|
114
|
-
**Note:** You don't need this if using `<A2HSHint />` — it already handles Android!
|
|
115
|
-
|
|
116
|
-
### 4. `useInstall()` hook
|
|
117
|
-
|
|
118
|
-
Access install state anywhere:
|
|
119
|
-
|
|
120
|
-
```tsx
|
|
121
|
-
import { useInstall } from '@/app/_snippets/PwaInstall';
|
|
122
|
-
|
|
123
|
-
function Header() {
|
|
124
|
-
const { isIOS, isInstalled, canPrompt, install } = useInstall();
|
|
125
|
-
|
|
126
|
-
return (
|
|
127
|
-
<header>
|
|
128
|
-
{canPrompt && <button onClick={install}>Install</button>}
|
|
129
|
-
{isInstalled && <span>✓ Installed</span>}
|
|
130
|
-
</header>
|
|
131
|
-
);
|
|
132
|
-
}
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
**Returns:**
|
|
136
|
-
```ts
|
|
137
|
-
{
|
|
138
|
-
isIOS: boolean;
|
|
139
|
-
isAndroid: boolean;
|
|
140
|
-
isSafari: boolean; // Real Safari (not Chromium browsers like Arc)
|
|
141
|
-
isChrome: boolean; // Any Chromium browser
|
|
142
|
-
isInstalled: boolean;
|
|
143
|
-
canPrompt: boolean; // Android only
|
|
144
|
-
install: () => Promise<'accepted' | 'dismissed' | null>;
|
|
145
|
-
}
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
**Platform Detection:**
|
|
149
|
-
Uses `useBrowserDetect` and `useDeviceDetect` from `@djangocfg/ui-nextjs` for accurate cross-platform detection.
|
|
150
|
-
|
|
151
|
-
---
|
|
152
|
-
|
|
153
|
-
## Complete Example (Cmdop)
|
|
154
|
-
|
|
155
|
-
```tsx
|
|
156
|
-
// app/layout.tsx
|
|
157
|
-
import { InstallProvider, A2HSHint } from '@/app/_snippets/PwaInstall';
|
|
158
|
-
import { settings } from '@core/settings';
|
|
159
|
-
|
|
160
|
-
export default function RootLayout({ children }) {
|
|
161
|
-
return (
|
|
162
|
-
<html>
|
|
163
|
-
<body>
|
|
164
|
-
<InstallProvider>
|
|
165
|
-
<Header />
|
|
166
|
-
{children}
|
|
167
|
-
<A2HSHint
|
|
168
|
-
resetAfterDays={3}
|
|
169
|
-
logo={settings.app.icons.logo192}
|
|
170
|
-
/>
|
|
171
|
-
</InstallProvider>
|
|
172
|
-
</body>
|
|
173
|
-
</html>
|
|
174
|
-
);
|
|
175
|
-
}
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
```tsx
|
|
179
|
-
// components/Header.tsx
|
|
180
|
-
import { InstallButton } from '@/app/_snippets/PwaInstall';
|
|
181
|
-
|
|
182
|
-
export function Header() {
|
|
183
|
-
return (
|
|
184
|
-
<header className="flex items-center justify-between p-4">
|
|
185
|
-
<h1>Cmdop</h1>
|
|
186
|
-
<InstallButton /> {/* Auto-shows on Android */}
|
|
187
|
-
</header>
|
|
188
|
-
);
|
|
189
|
-
}
|
|
190
|
-
```
|
|
191
|
-
|
|
192
|
-
---
|
|
193
|
-
|
|
194
|
-
## How It Works
|
|
195
|
-
|
|
196
|
-
### Android Chrome
|
|
197
|
-
|
|
198
|
-
1. Browser fires `beforeinstallprompt`
|
|
199
|
-
2. We capture it
|
|
200
|
-
3. Wait 3 seconds (engagement)
|
|
201
|
-
4. Show hint at bottom: "Install Cmdop → Tap to install"
|
|
202
|
-
5. User taps → Native install prompt
|
|
203
|
-
6. User accepts → App installed
|
|
204
|
-
7. Hint auto-dismisses
|
|
205
|
-
|
|
206
|
-
### iOS Safari
|
|
207
|
-
|
|
208
|
-
1. Detect iOS + Safari
|
|
209
|
-
2. Check if installed (`standalone` mode)
|
|
210
|
-
3. Check if dismissed recently (localStorage with timestamp)
|
|
211
|
-
4. If dismissed more than 3 days ago → show again
|
|
212
|
-
5. Wait 3 seconds (engagement)
|
|
213
|
-
6. Show clickable hint at bottom
|
|
214
|
-
7. **User taps hint** → Opens visual guide (drawer on mobile, modal on desktop)
|
|
215
|
-
8. **Visual guide shows 3 steps:**
|
|
216
|
-
- Tap Share button
|
|
217
|
-
- Scroll & tap "Add to Home Screen"
|
|
218
|
-
- Tap "Add" to confirm
|
|
219
|
-
9. User dismisses → Save timestamp
|
|
220
|
-
10. After 3 days → Reset, show again
|
|
221
|
-
|
|
222
|
-
---
|
|
223
|
-
|
|
224
|
-
## Advanced Usage
|
|
225
|
-
|
|
226
|
-
### Configure Reset Behavior
|
|
227
|
-
|
|
228
|
-
```tsx
|
|
229
|
-
// Default: Reset after 3 days
|
|
230
|
-
<A2HSHint />
|
|
231
|
-
|
|
232
|
-
// Never reset (show once forever)
|
|
233
|
-
<A2HSHint resetAfterDays={null} />
|
|
234
|
-
|
|
235
|
-
// Reset after 7 days
|
|
236
|
-
<A2HSHint resetAfterDays={7} />
|
|
237
|
-
|
|
238
|
-
// Show immediately (no delay)
|
|
239
|
-
<A2HSHint delayMs={0} />
|
|
240
|
-
|
|
241
|
-
// Force show for dev testing (shows on ANY browser, ignores localStorage)
|
|
242
|
-
<A2HSHint forceShow={isDevelopment} />
|
|
243
|
-
|
|
244
|
-
// Production setup
|
|
245
|
-
<A2HSHint
|
|
246
|
-
resetAfterDays={3}
|
|
247
|
-
delayMs={3000}
|
|
248
|
-
forceShow={settings.pwa.forceShowInDev}
|
|
249
|
-
/>
|
|
250
|
-
```
|
|
251
|
-
|
|
252
|
-
**Important: `forceShow` behavior**
|
|
253
|
-
- ✅ Shows on ANY browser (not just iOS Safari)
|
|
254
|
-
- ✅ Ignores localStorage (dismissed state)
|
|
255
|
-
- ✅ Doesn't save dismiss to localStorage
|
|
256
|
-
- ✅ Reappears on every refresh (perfect for dev testing)
|
|
257
|
-
```
|
|
258
|
-
|
|
259
|
-
### Custom Install Flow
|
|
260
|
-
|
|
261
|
-
```tsx
|
|
262
|
-
import { useInstall } from '@/app/_snippets/PwaInstall';
|
|
263
|
-
|
|
264
|
-
function CustomFlow() {
|
|
265
|
-
const { isIOS, canPrompt, install } = useInstall();
|
|
266
|
-
|
|
267
|
-
const handleInstall = async () => {
|
|
268
|
-
if (canPrompt) {
|
|
269
|
-
// Android: trigger native prompt
|
|
270
|
-
const outcome = await install();
|
|
271
|
-
console.log('Install outcome:', outcome);
|
|
272
|
-
} else if (isIOS) {
|
|
273
|
-
// iOS: show custom modal or guide
|
|
274
|
-
alert('Tap Share → Add to Home Screen');
|
|
275
|
-
}
|
|
276
|
-
};
|
|
277
|
-
|
|
278
|
-
return <button onClick={handleInstall}>Install App</button>;
|
|
279
|
-
}
|
|
280
|
-
```
|
|
281
|
-
|
|
282
|
-
### Check PWA State
|
|
283
|
-
|
|
284
|
-
```tsx
|
|
285
|
-
import { useInstall } from '@/app/_snippets/PwaInstall';
|
|
286
|
-
|
|
287
|
-
function AppStatus() {
|
|
288
|
-
const { isInstalled } = useInstall();
|
|
289
|
-
|
|
290
|
-
return (
|
|
291
|
-
<div>
|
|
292
|
-
{isInstalled ? (
|
|
293
|
-
<p>✓ Running as PWA</p>
|
|
294
|
-
) : (
|
|
295
|
-
<p>Running in browser</p>
|
|
296
|
-
)}
|
|
297
|
-
</div>
|
|
298
|
-
);
|
|
299
|
-
}
|
|
300
|
-
```
|
|
301
|
-
|
|
302
|
-
---
|
|
303
|
-
|
|
304
|
-
## Why This Approach?
|
|
305
|
-
|
|
306
|
-
### Click-to-Guide Pattern
|
|
307
|
-
|
|
308
|
-
Research shows **visual steps outperform text-only instructions**:
|
|
309
|
-
- 📊 **70% completion rate** with visual guide
|
|
310
|
-
- 📊 **40% completion rate** with text-only
|
|
311
|
-
|
|
312
|
-
**Our approach:**
|
|
313
|
-
1. **Minimal hint** — doesn't overwhelm ("Tap to learn how")
|
|
314
|
-
2. **Click → Visual guide** — for those who want details
|
|
315
|
-
3. **Adaptive UI** — drawer on mobile, modal on desktop
|
|
316
|
-
4. **Device-specific** — iOS Safari gets iOS-specific steps
|
|
317
|
-
|
|
318
|
-
### Why Simplified?
|
|
319
|
-
|
|
320
|
-
The old version had:
|
|
321
|
-
- ❌ Engagement tracking (`actions`, `timeSpent`, `visitCount`)
|
|
322
|
-
- ❌ Complex metrics in localStorage
|
|
323
|
-
- ❌ Too many props (`delayMs`, `resetDays`, `engagementThreshold`)
|
|
324
|
-
- ❌ Multiple components for same job
|
|
325
|
-
- ❌ Over-engineered for Cmdop
|
|
326
|
-
|
|
327
|
-
The new version:
|
|
328
|
-
- ✅ One provider, two components, one hook
|
|
329
|
-
- ✅ Click-to-guide (minimal hint + visual guide on demand)
|
|
330
|
-
- ✅ Zero config
|
|
331
|
-
- ✅ No tracking
|
|
332
|
-
- ✅ Simple localStorage (only dismiss state)
|
|
333
|
-
- ✅ Works out of the box
|
|
334
|
-
|
|
335
|
-
---
|
|
336
|
-
|
|
337
|
-
## Troubleshooting
|
|
338
|
-
|
|
339
|
-
**iOS hint not showing?**
|
|
340
|
-
- Check if iOS Safari (not Chrome on iOS)
|
|
341
|
-
- Check if already installed
|
|
342
|
-
- Check if dismissed recently:
|
|
343
|
-
```js
|
|
344
|
-
const dismissed = localStorage.getItem('cmdop-a2hs-dismissed');
|
|
345
|
-
if (dismissed) {
|
|
346
|
-
const days = (Date.now() - parseInt(dismissed)) / (1000 * 60 * 60 * 24);
|
|
347
|
-
console.log('Dismissed', days.toFixed(1), 'days ago');
|
|
348
|
-
}
|
|
349
|
-
```
|
|
350
|
-
- Force reset: `localStorage.removeItem('cmdop-a2hs-dismissed')`
|
|
351
|
-
- **Hint reappears automatically after 3 days**
|
|
352
|
-
|
|
353
|
-
**Android button not showing?**
|
|
354
|
-
- Check PWA manifest (`/manifest.json`)
|
|
355
|
-
- Check service worker
|
|
356
|
-
- Check HTTPS (required for PWA)
|
|
357
|
-
- Open DevTools → Application → Manifest
|
|
358
|
-
|
|
359
|
-
**Already installed but still showing?**
|
|
360
|
-
- Clear localStorage: `localStorage.clear()`
|
|
361
|
-
- Reload page
|
|
362
|
-
|
|
363
|
-
---
|
|
364
|
-
|
|
365
|
-
## Browser Support
|
|
366
|
-
|
|
367
|
-
| Platform | Browser | Support |
|
|
368
|
-
|----------|---------|---------|
|
|
369
|
-
| iOS | Safari | ✅ Inline hint |
|
|
370
|
-
| iOS | Chrome/Firefox | ❌ No PWA support |
|
|
371
|
-
| Android | Chrome | ✅ Native prompt |
|
|
372
|
-
| Android | Firefox | ⚠️ Manual only |
|
|
373
|
-
| Desktop | Chrome/Edge | ✅ Native prompt |
|
|
374
|
-
|
|
375
|
-
---
|
|
376
|
-
|
|
377
|
-
## That's It
|
|
378
|
-
|
|
379
|
-
Three exports:
|
|
380
|
-
1. `<InstallProvider>` — wrap your app
|
|
381
|
-
2. `<A2HSHint />` — iOS hint
|
|
382
|
-
3. `<InstallButton />` — Android button
|
|
383
|
-
|
|
384
|
-
One hook:
|
|
385
|
-
- `useInstall()` — access state
|
|
386
|
-
|
|
387
|
-
Zero config. Done.
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Django Push Context
|
|
5
|
-
*
|
|
6
|
-
* Provider for Django-CFG push notifications integration.
|
|
7
|
-
* Wraps useDjangoPush hook in React context for easy consumption.
|
|
8
|
-
*
|
|
9
|
-
* @example
|
|
10
|
-
* ```tsx
|
|
11
|
-
* import { DjangoPushProvider, useDjangoPushContext } from '@djangocfg/layouts/PWA';
|
|
12
|
-
*
|
|
13
|
-
* // In layout
|
|
14
|
-
* <DjangoPushProvider vapidPublicKey={process.env.NEXT_PUBLIC_VAPID_KEY}>
|
|
15
|
-
* {children}
|
|
16
|
-
* </DjangoPushProvider>
|
|
17
|
-
*
|
|
18
|
-
* // In component
|
|
19
|
-
* function NotifyButton() {
|
|
20
|
-
* const { subscribe, isSubscribed } = useDjangoPushContext();
|
|
21
|
-
* return <button onClick={subscribe}>Subscribe</button>;
|
|
22
|
-
* }
|
|
23
|
-
* ```
|
|
24
|
-
*/
|
|
25
|
-
|
|
26
|
-
import React, { createContext, useContext } from 'react';
|
|
27
|
-
import { useDjangoPush } from '../hooks/useDjangoPush';
|
|
28
|
-
import type { PushNotificationOptions } from '../types';
|
|
29
|
-
|
|
30
|
-
interface DjangoPushContextValue {
|
|
31
|
-
// State
|
|
32
|
-
isSupported: boolean;
|
|
33
|
-
permission: NotificationPermission;
|
|
34
|
-
isSubscribed: boolean;
|
|
35
|
-
subscription: PushSubscription | null;
|
|
36
|
-
isLoading: boolean;
|
|
37
|
-
error: Error | null;
|
|
38
|
-
|
|
39
|
-
// Actions
|
|
40
|
-
subscribe: () => Promise<boolean>;
|
|
41
|
-
unsubscribe: () => Promise<boolean>;
|
|
42
|
-
sendTestPush: (message: { title: string; body: string; url?: string }) => Promise<boolean>;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const DjangoPushContext = createContext<DjangoPushContextValue | undefined>(undefined);
|
|
46
|
-
|
|
47
|
-
interface DjangoPushProviderProps extends PushNotificationOptions {
|
|
48
|
-
children: React.ReactNode;
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Auto-subscribe on mount if permission granted
|
|
52
|
-
* @default false
|
|
53
|
-
*/
|
|
54
|
-
autoSubscribe?: boolean;
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Callback when subscription created
|
|
58
|
-
*/
|
|
59
|
-
onSubscribed?: (subscription: PushSubscription) => void;
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Callback when subscription failed
|
|
63
|
-
*/
|
|
64
|
-
onSubscribeError?: (error: Error) => void;
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Callback when unsubscribed
|
|
68
|
-
*/
|
|
69
|
-
onUnsubscribed?: () => void;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Provider for Django push notifications
|
|
74
|
-
*/
|
|
75
|
-
export function DjangoPushProvider({
|
|
76
|
-
children,
|
|
77
|
-
vapidPublicKey,
|
|
78
|
-
autoSubscribe = false,
|
|
79
|
-
onSubscribed,
|
|
80
|
-
onSubscribeError,
|
|
81
|
-
onUnsubscribed,
|
|
82
|
-
}: DjangoPushProviderProps) {
|
|
83
|
-
const djangoPush = useDjangoPush({
|
|
84
|
-
vapidPublicKey,
|
|
85
|
-
autoSubscribe,
|
|
86
|
-
onSubscribed,
|
|
87
|
-
onSubscribeError,
|
|
88
|
-
onUnsubscribed,
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
return <DjangoPushContext.Provider value={djangoPush}>{children}</DjangoPushContext.Provider>;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Hook to access Django push context
|
|
96
|
-
*/
|
|
97
|
-
export function useDjangoPushContext(): DjangoPushContextValue {
|
|
98
|
-
const context = useContext(DjangoPushContext);
|
|
99
|
-
|
|
100
|
-
if (context === undefined) {
|
|
101
|
-
throw new Error('useDjangoPushContext must be used within DjangoPushProvider');
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
return context;
|
|
105
|
-
}
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Simplified PWA Install Context
|
|
5
|
-
*
|
|
6
|
-
* Minimal global state for PWA installation
|
|
7
|
-
* No tracking, no metrics, no engagement — just install state
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import React, { createContext, useContext, ReactNode } from 'react';
|
|
11
|
-
|
|
12
|
-
import { A2HSHint } from '../components/A2HSHint';
|
|
13
|
-
import type { InstallOutcome, PushNotificationOptions } from '../types';
|
|
14
|
-
import { useInstallPrompt } from '../hooks/useInstallPrompt';
|
|
15
|
-
import { PushProvider } from './PushContext';
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Conditional PushProvider wrapper
|
|
19
|
-
* Only wraps children if push notifications are enabled
|
|
20
|
-
*/
|
|
21
|
-
function ConditionalPushProvider({
|
|
22
|
-
enabled,
|
|
23
|
-
children,
|
|
24
|
-
...config
|
|
25
|
-
}: PushNotificationOptions & { enabled: boolean; children: ReactNode }) {
|
|
26
|
-
if (!enabled) return <>{children}</>;
|
|
27
|
-
return <PushProvider {...config}>{children}</PushProvider>;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export interface PwaContextValue {
|
|
31
|
-
// Platform
|
|
32
|
-
isIOS: boolean;
|
|
33
|
-
isAndroid: boolean;
|
|
34
|
-
isSafari: boolean;
|
|
35
|
-
isChrome: boolean;
|
|
36
|
-
|
|
37
|
-
// State
|
|
38
|
-
isInstalled: boolean;
|
|
39
|
-
canPrompt: boolean;
|
|
40
|
-
|
|
41
|
-
// Actions
|
|
42
|
-
install: () => Promise<InstallOutcome>;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const PwaContext = createContext<PwaContextValue | undefined>(undefined);
|
|
46
|
-
|
|
47
|
-
export interface PwaConfig {
|
|
48
|
-
enabled?: boolean;
|
|
49
|
-
showInstallHint?: boolean;
|
|
50
|
-
resetAfterDays?: number | null;
|
|
51
|
-
delayMs?: number;
|
|
52
|
-
logo?: string;
|
|
53
|
-
pushNotifications?: PushNotificationOptions & {
|
|
54
|
-
delayMs?: number;
|
|
55
|
-
resetAfterDays?: number;
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export function PwaProvider({ children, ...config }: PwaConfig & { children: React.ReactNode }) {
|
|
60
|
-
// If not enabled, acts as a simple pass-through
|
|
61
|
-
if (config.enabled === false) {
|
|
62
|
-
return <>{children}</>;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const prompt = useInstallPrompt();
|
|
66
|
-
|
|
67
|
-
const value: PwaContextValue = {
|
|
68
|
-
isIOS: prompt.isIOS,
|
|
69
|
-
isAndroid: prompt.isAndroid,
|
|
70
|
-
isSafari: prompt.isSafari,
|
|
71
|
-
isChrome: prompt.isChrome,
|
|
72
|
-
isInstalled: prompt.isInstalled,
|
|
73
|
-
canPrompt: prompt.canPrompt,
|
|
74
|
-
install: prompt.promptInstall,
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
const showHint = config.showInstallHint !== false;
|
|
78
|
-
|
|
79
|
-
// ✅ Explicit composition tree (no magic wrapping)
|
|
80
|
-
// Structure:
|
|
81
|
-
// - PwaContext.Provider (PWA install state)
|
|
82
|
-
// - ConditionalPushProvider (optional push notifications)
|
|
83
|
-
// - children (user content)
|
|
84
|
-
// - A2HSHint (PWA install hint UI)
|
|
85
|
-
return (
|
|
86
|
-
<PwaContext.Provider value={value}>
|
|
87
|
-
<ConditionalPushProvider
|
|
88
|
-
enabled={!!config.pushNotifications}
|
|
89
|
-
vapidPublicKey={config.pushNotifications?.vapidPublicKey || ''}
|
|
90
|
-
subscribeEndpoint={config.pushNotifications?.subscribeEndpoint}
|
|
91
|
-
>
|
|
92
|
-
{children}
|
|
93
|
-
{showHint && (
|
|
94
|
-
<A2HSHint
|
|
95
|
-
resetAfterDays={config.resetAfterDays}
|
|
96
|
-
delayMs={config.delayMs}
|
|
97
|
-
logo={config.logo}
|
|
98
|
-
pushNotifications={config.pushNotifications}
|
|
99
|
-
/>
|
|
100
|
-
)}
|
|
101
|
-
</ConditionalPushProvider>
|
|
102
|
-
</PwaContext.Provider>
|
|
103
|
-
);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Use install context
|
|
108
|
-
* Must be used within <PwaProvider>
|
|
109
|
-
*/
|
|
110
|
-
export function useInstall(): PwaContextValue {
|
|
111
|
-
const context = useContext(PwaContext);
|
|
112
|
-
|
|
113
|
-
if (context === undefined) {
|
|
114
|
-
throw new Error('useInstall must be used within <PwaProvider>');
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
return context;
|
|
118
|
-
}
|