@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.
Files changed (77) hide show
  1. package/README.md +204 -18
  2. package/package.json +5 -5
  3. package/src/components/errors/index.ts +9 -0
  4. package/src/components/errors/types.ts +38 -0
  5. package/src/layouts/AppLayout/AppLayout.tsx +33 -45
  6. package/src/layouts/AppLayout/BaseApp.tsx +104 -33
  7. package/src/layouts/AuthLayout/AuthContext.tsx +7 -1
  8. package/src/layouts/AuthLayout/OAuthProviders.tsx +1 -10
  9. package/src/layouts/AuthLayout/OTPForm.tsx +1 -0
  10. package/src/layouts/PrivateLayout/PrivateLayout.tsx +1 -1
  11. package/src/layouts/PublicLayout/PublicLayout.tsx +1 -1
  12. package/src/layouts/PublicLayout/components/PublicMobileDrawer.tsx +1 -1
  13. package/src/layouts/PublicLayout/components/PublicNavigation.tsx +1 -1
  14. package/src/layouts/_components/UserMenu.tsx +1 -1
  15. package/src/layouts/index.ts +1 -1
  16. package/src/layouts/types/index.ts +47 -0
  17. package/src/layouts/types/layout.types.ts +61 -0
  18. package/src/layouts/types/providers.types.ts +65 -0
  19. package/src/layouts/types/ui.types.ts +103 -0
  20. package/src/snippets/Analytics/index.ts +1 -0
  21. package/src/snippets/Analytics/types.ts +10 -0
  22. package/src/snippets/PWAInstall/@docs/README.md +92 -0
  23. package/src/snippets/PWAInstall/README.md +185 -0
  24. package/src/snippets/{PWA → PWAInstall}/components/A2HSHint.tsx +85 -84
  25. package/src/snippets/PWAInstall/components/DesktopGuide.tsx +229 -0
  26. package/src/snippets/PWAInstall/context/InstallContext.tsx +102 -0
  27. package/src/snippets/{PWA → PWAInstall}/hooks/useInstallPrompt.ts +3 -0
  28. package/src/snippets/{PWA → PWAInstall}/index.ts +12 -31
  29. package/src/snippets/{PWA → PWAInstall}/types/components.ts +0 -6
  30. package/src/snippets/PWAInstall/types/config.ts +22 -0
  31. package/src/snippets/{PWA → PWAInstall}/types/index.ts +4 -4
  32. package/src/snippets/{PWA → PWAInstall}/utils/localStorage.ts +1 -23
  33. package/src/snippets/PushNotifications/@docs/README.md +191 -0
  34. package/src/snippets/PushNotifications/@docs/guides/django-integration.md +648 -0
  35. package/src/snippets/PushNotifications/@docs/guides/service-worker.md +467 -0
  36. package/src/snippets/PushNotifications/@docs/guides/vapid-setup.md +352 -0
  37. package/src/snippets/PushNotifications/README.md +328 -0
  38. package/src/snippets/{PWA → PushNotifications}/config.ts +2 -2
  39. package/src/snippets/PushNotifications/context/DjangoPushContext.tsx +190 -0
  40. package/src/snippets/{PWA → PushNotifications}/hooks/useDjangoPush.ts +63 -81
  41. package/src/snippets/{PWA → PushNotifications}/hooks/usePushNotifications.ts +12 -8
  42. package/src/snippets/PushNotifications/index.ts +87 -0
  43. package/src/snippets/PushNotifications/types/config.ts +28 -0
  44. package/src/snippets/PushNotifications/types/index.ts +9 -0
  45. package/src/snippets/PushNotifications/utils/localStorage.ts +60 -0
  46. package/src/snippets/PushNotifications/utils/logger.ts +149 -0
  47. package/src/snippets/PushNotifications/utils/platform.ts +151 -0
  48. package/src/snippets/index.ts +37 -12
  49. package/src/layouts/shared/index.ts +0 -21
  50. package/src/layouts/shared/types.ts +0 -247
  51. package/src/snippets/PWA/@refactoring/ARCHITECTURE_ANALYSIS.md +0 -1179
  52. package/src/snippets/PWA/@refactoring/EXECUTIVE_SUMMARY.md +0 -271
  53. package/src/snippets/PWA/@refactoring/README.md +0 -204
  54. package/src/snippets/PWA/@refactoring/REFACTORING_PROPOSALS.md +0 -1109
  55. package/src/snippets/PWA/@refactoring2/COMPARISON-WITH-NEXTJS.md +0 -718
  56. package/src/snippets/PWA/@refactoring2/P1-FIXES-COMPLETED.md +0 -188
  57. package/src/snippets/PWA/@refactoring2/POST-P0-ANALYSIS.md +0 -362
  58. package/src/snippets/PWA/@refactoring2/README.md +0 -85
  59. package/src/snippets/PWA/@refactoring2/RECOMMENDATIONS.md +0 -1321
  60. package/src/snippets/PWA/@refactoring2/REMAINING-ISSUES.md +0 -557
  61. package/src/snippets/PWA/README.md +0 -387
  62. package/src/snippets/PWA/context/DjangoPushContext.tsx +0 -105
  63. package/src/snippets/PWA/context/InstallContext.tsx +0 -118
  64. package/src/snippets/PWA/context/PushContext.tsx +0 -156
  65. /package/src/layouts/{shared → types}/README.md +0 -0
  66. /package/src/snippets/{PWA/@docs/research.md → PWAInstall/@docs/research/ios-android-install-flows.md} +0 -0
  67. /package/src/snippets/{PWA → PWAInstall}/components/IOSGuide.tsx +0 -0
  68. /package/src/snippets/{PWA → PWAInstall}/components/IOSGuideDrawer.tsx +0 -0
  69. /package/src/snippets/{PWA → PWAInstall}/components/IOSGuideModal.tsx +0 -0
  70. /package/src/snippets/{PWA → PWAInstall}/hooks/useIsPWA.ts +0 -0
  71. /package/src/snippets/{PWA → PWAInstall}/types/install.ts +0 -0
  72. /package/src/snippets/{PWA → PWAInstall}/types/platform.ts +0 -0
  73. /package/src/snippets/{PWA → PWAInstall}/utils/logger.ts +0 -0
  74. /package/src/snippets/{PWA → PWAInstall}/utils/platform.ts +0 -0
  75. /package/src/snippets/{PWA → PushNotifications}/components/PushPrompt.tsx +0 -0
  76. /package/src/snippets/{PWA → PushNotifications}/types/push.ts +0 -0
  77. /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
- }