@djangocfg/layouts 2.1.36 → 2.1.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) 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 +105 -28
  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/McpChat/context/ChatContext.tsx +9 -0
  23. package/src/snippets/PWAInstall/@docs/README.md +92 -0
  24. package/src/snippets/PWAInstall/@docs/research/ios-android-install-flows.md +576 -0
  25. package/src/snippets/PWAInstall/README.md +185 -0
  26. package/src/snippets/PWAInstall/components/A2HSHint.tsx +227 -0
  27. package/src/snippets/PWAInstall/components/DesktopGuide.tsx +229 -0
  28. package/src/snippets/PWAInstall/components/IOSGuide.tsx +29 -0
  29. package/src/snippets/PWAInstall/components/IOSGuideDrawer.tsx +101 -0
  30. package/src/snippets/PWAInstall/components/IOSGuideModal.tsx +101 -0
  31. package/src/snippets/PWAInstall/context/InstallContext.tsx +102 -0
  32. package/src/snippets/PWAInstall/hooks/useInstallPrompt.ts +167 -0
  33. package/src/snippets/PWAInstall/hooks/useIsPWA.ts +115 -0
  34. package/src/snippets/PWAInstall/index.ts +76 -0
  35. package/src/snippets/PWAInstall/types/components.ts +95 -0
  36. package/src/snippets/PWAInstall/types/config.ts +22 -0
  37. package/src/snippets/PWAInstall/types/index.ts +26 -0
  38. package/src/snippets/PWAInstall/types/install.ts +38 -0
  39. package/src/snippets/PWAInstall/types/platform.ts +29 -0
  40. package/src/snippets/PWAInstall/utils/localStorage.ts +181 -0
  41. package/src/snippets/PWAInstall/utils/logger.ts +149 -0
  42. package/src/snippets/PWAInstall/utils/platform.ts +151 -0
  43. package/src/snippets/PushNotifications/@docs/README.md +191 -0
  44. package/src/snippets/PushNotifications/@docs/guides/django-integration.md +648 -0
  45. package/src/snippets/PushNotifications/@docs/guides/service-worker.md +467 -0
  46. package/src/snippets/PushNotifications/@docs/guides/vapid-setup.md +352 -0
  47. package/src/snippets/PushNotifications/README.md +328 -0
  48. package/src/snippets/PushNotifications/components/PushPrompt.tsx +165 -0
  49. package/src/snippets/PushNotifications/config.ts +20 -0
  50. package/src/snippets/PushNotifications/context/DjangoPushContext.tsx +190 -0
  51. package/src/snippets/PushNotifications/hooks/useDjangoPush.ts +259 -0
  52. package/src/snippets/PushNotifications/hooks/usePushNotifications.ts +209 -0
  53. package/src/snippets/PushNotifications/index.ts +87 -0
  54. package/src/snippets/PushNotifications/types/config.ts +28 -0
  55. package/src/snippets/PushNotifications/types/index.ts +9 -0
  56. package/src/snippets/PushNotifications/types/push.ts +21 -0
  57. package/src/snippets/PushNotifications/utils/localStorage.ts +60 -0
  58. package/src/snippets/PushNotifications/utils/logger.ts +149 -0
  59. package/src/snippets/PushNotifications/utils/platform.ts +151 -0
  60. package/src/snippets/PushNotifications/utils/vapid.ts +226 -0
  61. package/src/snippets/index.ts +55 -0
  62. package/src/layouts/shared/index.ts +0 -21
  63. package/src/layouts/shared/types.ts +0 -211
  64. /package/src/layouts/{shared → types}/README.md +0 -0
@@ -0,0 +1,101 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * iOS Installation Guide Drawer (Mobile-Optimized)
5
+ *
6
+ * Bottom drawer with swipe gestures for iOS installation guide
7
+ * Better UX for mobile devices
8
+ */
9
+
10
+ import React from 'react';
11
+ import { Share, Check, ArrowUpRight, ArrowDown, CheckCircle } from 'lucide-react';
12
+
13
+ import {
14
+ Drawer,
15
+ DrawerContent,
16
+ DrawerDescription,
17
+ DrawerHeader,
18
+ DrawerTitle,
19
+ Button,
20
+ Card,
21
+ CardContent,
22
+ } from '@djangocfg/ui-nextjs';
23
+
24
+ import type { IOSGuideModalProps, InstallStep } from '../types';
25
+
26
+ const steps: InstallStep[] = [
27
+ {
28
+ number: 1,
29
+ title: 'Tap Share',
30
+ icon: ArrowUpRight,
31
+ description: 'At the bottom of Safari',
32
+ },
33
+ {
34
+ number: 2,
35
+ title: 'Scroll & Tap',
36
+ icon: ArrowDown,
37
+ description: '"Add to Home Screen"',
38
+ },
39
+ {
40
+ number: 3,
41
+ title: 'Confirm',
42
+ icon: CheckCircle,
43
+ description: 'Tap "Add" in top-right',
44
+ },
45
+ ];
46
+
47
+ function StepCard({ step }: { step: InstallStep }) {
48
+ return (
49
+ <Card className="border border-border">
50
+ <CardContent className="p-4">
51
+ <div className="flex items-start gap-3">
52
+ <div
53
+ className="flex items-center justify-center rounded-full bg-primary text-primary-foreground flex-shrink-0"
54
+ style={{ width: '32px', height: '32px' }}
55
+ >
56
+ <span className="text-sm font-semibold">{step.number}</span>
57
+ </div>
58
+
59
+ <div className="flex-1 min-w-0">
60
+ <div className="flex items-center gap-2 mb-1">
61
+ <step.icon className="w-5 h-5 text-primary" />
62
+ <h3 className="font-semibold text-foreground">{step.title}</h3>
63
+ </div>
64
+ <p className="text-sm text-muted-foreground">{step.description}</p>
65
+ </div>
66
+ </div>
67
+ </CardContent>
68
+ </Card>
69
+ );
70
+ }
71
+
72
+ export function IOSGuideDrawer({ onDismiss, open = true }: IOSGuideModalProps) {
73
+ return (
74
+ <Drawer open={open} onOpenChange={(isOpen) => !isOpen && onDismiss()}>
75
+ <DrawerContent>
76
+ <DrawerHeader className="text-left">
77
+ <DrawerTitle className="flex items-center gap-2">
78
+ <Share className="w-5 h-5 text-primary" />
79
+ Add to Home Screen
80
+ </DrawerTitle>
81
+ <DrawerDescription className="text-left">
82
+ Install this app on your iPhone for quick access and a better experience
83
+ </DrawerDescription>
84
+ </DrawerHeader>
85
+
86
+ <div className="space-y-3 p-4">
87
+ {steps.map((step) => (
88
+ <StepCard key={step.number} step={step} />
89
+ ))}
90
+ </div>
91
+
92
+ <div className="p-4 pt-0">
93
+ <Button onClick={onDismiss} variant="default" className="w-full">
94
+ <Check className="w-4 h-4 mr-2" />
95
+ Got It
96
+ </Button>
97
+ </div>
98
+ </DrawerContent>
99
+ </Drawer>
100
+ );
101
+ }
@@ -0,0 +1,101 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * iOS Installation Guide Modal
5
+ *
6
+ * Visual step-by-step guide for adding PWA to home screen on iOS Safari
7
+ */
8
+
9
+ import React from 'react';
10
+ import { Share, Check, ArrowUpRight, ArrowDown, CheckCircle } from 'lucide-react';
11
+
12
+ import {
13
+ Dialog,
14
+ DialogContent,
15
+ DialogDescription,
16
+ DialogFooter,
17
+ DialogHeader,
18
+ DialogTitle,
19
+ Button,
20
+ Card,
21
+ CardContent,
22
+ } from '@djangocfg/ui-nextjs';
23
+
24
+ import type { IOSGuideModalProps, InstallStep } from '../types';
25
+
26
+ const steps: InstallStep[] = [
27
+ {
28
+ number: 1,
29
+ title: 'Tap Share',
30
+ icon: ArrowUpRight,
31
+ description: 'At the bottom of Safari',
32
+ },
33
+ {
34
+ number: 2,
35
+ title: 'Scroll & Tap',
36
+ icon: ArrowDown,
37
+ description: '"Add to Home Screen"',
38
+ },
39
+ {
40
+ number: 3,
41
+ title: 'Confirm',
42
+ icon: CheckCircle,
43
+ description: 'Tap "Add" in top-right',
44
+ },
45
+ ];
46
+
47
+ function StepCard({ step }: { step: InstallStep }) {
48
+ return (
49
+ <Card className="border border-border">
50
+ <CardContent className="p-4">
51
+ <div className="flex items-start gap-3">
52
+ <div
53
+ className="flex items-center justify-center rounded-full bg-primary text-primary-foreground flex-shrink-0"
54
+ style={{ width: '32px', height: '32px' }}
55
+ >
56
+ <span className="text-sm font-semibold">{step.number}</span>
57
+ </div>
58
+
59
+ <div className="flex-1 min-w-0">
60
+ <div className="flex items-center gap-2 mb-1">
61
+ <step.icon className="w-5 h-5 text-primary" />
62
+ <h3 className="font-semibold text-foreground">{step.title}</h3>
63
+ </div>
64
+ <p className="text-sm text-muted-foreground">{step.description}</p>
65
+ </div>
66
+ </div>
67
+ </CardContent>
68
+ </Card>
69
+ );
70
+ }
71
+
72
+ export function IOSGuideModal({ onDismiss, open = true }: IOSGuideModalProps) {
73
+ return (
74
+ <Dialog open={open} onOpenChange={(isOpen) => !isOpen && onDismiss()}>
75
+ <DialogContent className="sm:max-w-md">
76
+ <DialogHeader className="text-left">
77
+ <DialogTitle className="flex items-center gap-2">
78
+ <Share className="w-5 h-5 text-primary" />
79
+ Add to Home Screen
80
+ </DialogTitle>
81
+ <DialogDescription className="text-left">
82
+ Install this app on your iPhone for quick access and a better experience
83
+ </DialogDescription>
84
+ </DialogHeader>
85
+
86
+ <div className="space-y-3 py-4">
87
+ {steps.map((step) => (
88
+ <StepCard key={step.number} step={step} />
89
+ ))}
90
+ </div>
91
+
92
+ <DialogFooter>
93
+ <Button onClick={onDismiss} variant="default" className="w-full">
94
+ <Check className="w-4 h-4 mr-2" />
95
+ Got It
96
+ </Button>
97
+ </DialogFooter>
98
+ </DialogContent>
99
+ </Dialog>
100
+ );
101
+ }
@@ -0,0 +1,102 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * 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 type { InstallOutcome } from '../types';
13
+ import { useInstallPrompt } from '../hooks/useInstallPrompt';
14
+
15
+ export interface PwaContextValue {
16
+ // Platform
17
+ isIOS: boolean;
18
+ isAndroid: boolean;
19
+ isDesktop: boolean;
20
+
21
+ // Browsers
22
+ isSafari: boolean;
23
+ isChrome: boolean;
24
+ isFirefox: boolean;
25
+ isEdge: boolean;
26
+ isOpera: boolean;
27
+ isBrave: boolean;
28
+ isArc: boolean;
29
+ isVivaldi: boolean;
30
+ isYandex: boolean;
31
+ isSamsungBrowser: boolean;
32
+ isUCBrowser: boolean;
33
+ isChromium: boolean; // Any Chromium-based browser
34
+ browserName: string;
35
+
36
+ // State
37
+ isInstalled: boolean;
38
+ canPrompt: boolean;
39
+
40
+ // Actions
41
+ install: () => Promise<InstallOutcome>;
42
+ }
43
+
44
+ const PwaContext = createContext<PwaContextValue | undefined>(undefined);
45
+
46
+ export interface PwaConfig {
47
+ enabled?: boolean;
48
+ }
49
+
50
+ export function PwaProvider({ children, ...config }: PwaConfig & { children: ReactNode }) {
51
+ // If not enabled, acts as a simple pass-through
52
+ if (config.enabled === false) {
53
+ return <>{children}</>;
54
+ }
55
+
56
+ const prompt = useInstallPrompt();
57
+
58
+ const value: PwaContextValue = {
59
+ // Platform
60
+ isIOS: prompt.isIOS,
61
+ isAndroid: prompt.isAndroid,
62
+ isDesktop: !prompt.isIOS && !prompt.isAndroid,
63
+
64
+ // Browsers (from useBrowserDetect)
65
+ isSafari: prompt.browser.isSafari && !prompt.browser.isChromium, // Real Safari only
66
+ isChrome: prompt.browser.isChrome,
67
+ isFirefox: prompt.browser.isFirefox,
68
+ isEdge: prompt.browser.isEdge,
69
+ isOpera: prompt.browser.isOpera,
70
+ isBrave: prompt.browser.isBrave,
71
+ isArc: prompt.browser.isArc,
72
+ isVivaldi: prompt.browser.isVivaldi,
73
+ isYandex: prompt.browser.isYandex,
74
+ isSamsungBrowser: prompt.browser.isSamsungBrowser,
75
+ isUCBrowser: prompt.browser.isUCBrowser,
76
+ isChromium: prompt.browser.isChromium,
77
+ browserName: prompt.browser.browserName,
78
+
79
+ // State
80
+ isInstalled: prompt.isInstalled,
81
+ canPrompt: prompt.canPrompt,
82
+
83
+ // Actions
84
+ install: prompt.promptInstall,
85
+ };
86
+
87
+ return <PwaContext.Provider value={value}>{children}</PwaContext.Provider>;
88
+ }
89
+
90
+ /**
91
+ * Use install context
92
+ * Must be used within <PwaProvider>
93
+ */
94
+ export function useInstall(): PwaContextValue {
95
+ const context = useContext(PwaContext);
96
+
97
+ if (context === undefined) {
98
+ throw new Error('useInstall must be used within <PwaProvider>');
99
+ }
100
+
101
+ return context;
102
+ }
@@ -0,0 +1,167 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * PWA Install Prompt Hook
5
+ *
6
+ * Manages beforeinstallprompt event and installation state
7
+ * Uses existing hooks from @djangocfg/ui-nextjs for platform detection
8
+ */
9
+
10
+ import { useEffect, useState } from 'react';
11
+ import { useBrowserDetect, useDeviceDetect } from '@djangocfg/ui-nextjs';
12
+
13
+ import type { BeforeInstallPromptEvent, InstallPromptState, InstallOutcome } from '../types';
14
+ import { markAppInstalled } from '../utils/localStorage';
15
+ import { isStandalone, onDisplayModeChange } from '../utils/platform';
16
+ import { pwaLogger } from '../utils/logger';
17
+
18
+ export function useInstallPrompt() {
19
+ const browser = useBrowserDetect();
20
+ const device = useDeviceDetect();
21
+
22
+ const [state, setState] = useState<InstallPromptState>(() => {
23
+ if (typeof window === 'undefined') {
24
+ return {
25
+ isIOS: false,
26
+ isAndroid: false,
27
+ isSafari: false,
28
+ isChrome: false,
29
+ isInstalled: false,
30
+ canPrompt: false,
31
+ deferredPrompt: null,
32
+ };
33
+ }
34
+
35
+ // Real Safari = Safari && NOT Chromium (avoid Arc, Brave on iOS)
36
+ const isSafari = browser.isSafari && !browser.isChromium;
37
+
38
+ return {
39
+ isIOS: device.isIOS,
40
+ isAndroid: device.isAndroid,
41
+ isSafari,
42
+ isChrome: browser.isChrome || browser.isChromium,
43
+ isInstalled: isStandalone(),
44
+ canPrompt: false,
45
+ deferredPrompt: null,
46
+ };
47
+ });
48
+
49
+ // Update state when platform info changes
50
+ useEffect(() => {
51
+ const isSafari = browser.isSafari && !browser.isChromium;
52
+
53
+ setState((prev) => ({
54
+ ...prev,
55
+ isIOS: device.isIOS,
56
+ isAndroid: device.isAndroid,
57
+ isSafari,
58
+ isChrome: browser.isChrome || browser.isChromium,
59
+ isInstalled: isStandalone(),
60
+ }));
61
+ }, [browser, device]);
62
+
63
+ // Listen for beforeinstallprompt event (Android Chrome only)
64
+ useEffect(() => {
65
+ if (typeof window === 'undefined') return;
66
+
67
+ const handleBeforeInstallPrompt = (e: Event) => {
68
+ // Prevent the default browser install prompt
69
+ e.preventDefault();
70
+
71
+ const event = e as BeforeInstallPromptEvent;
72
+
73
+ setState((prev) => ({
74
+ ...prev,
75
+ canPrompt: true,
76
+ deferredPrompt: event,
77
+ }));
78
+ };
79
+
80
+ window.addEventListener('beforeinstallprompt', handleBeforeInstallPrompt);
81
+
82
+ return () => {
83
+ window.removeEventListener('beforeinstallprompt', handleBeforeInstallPrompt);
84
+ };
85
+ }, []);
86
+
87
+ // Listen for appinstalled event (Android Chrome)
88
+ useEffect(() => {
89
+ if (typeof window === 'undefined') return;
90
+
91
+ const handleAppInstalled = () => {
92
+ setState((prev) => ({
93
+ ...prev,
94
+ canPrompt: false,
95
+ deferredPrompt: null,
96
+ isInstalled: true,
97
+ }));
98
+
99
+ // Mark as installed in localStorage
100
+ markAppInstalled();
101
+ };
102
+
103
+ window.addEventListener('appinstalled', handleAppInstalled);
104
+
105
+ return () => {
106
+ window.removeEventListener('appinstalled', handleAppInstalled);
107
+ };
108
+ }, []);
109
+
110
+ // Check for display-mode changes (user adds to home screen)
111
+ useEffect(() => {
112
+ const cleanup = onDisplayModeChange((isStandaloneMode) => {
113
+ if (isStandaloneMode) {
114
+ setState((prev) => ({
115
+ ...prev,
116
+ isInstalled: true,
117
+ canPrompt: false,
118
+ deferredPrompt: null,
119
+ }));
120
+
121
+ markAppInstalled();
122
+ }
123
+ });
124
+
125
+ return cleanup;
126
+ }, []);
127
+
128
+ /**
129
+ * Trigger Android native install prompt
130
+ */
131
+ const promptInstall = async (): Promise<InstallOutcome> => {
132
+ if (!state.deferredPrompt) {
133
+ pwaLogger.warn('[PWA Install] No deferred prompt available');
134
+ return null;
135
+ }
136
+
137
+ try {
138
+ // Show the native prompt
139
+ await state.deferredPrompt.prompt();
140
+
141
+ // Wait for user response
142
+ const { outcome } = await state.deferredPrompt.userChoice;
143
+
144
+ pwaLogger.info('[PWA Install] User choice:', outcome);
145
+
146
+ // Clear the deferred prompt
147
+ setState((prev) => ({
148
+ ...prev,
149
+ deferredPrompt: null,
150
+ canPrompt: false,
151
+ }));
152
+
153
+ return outcome;
154
+ } catch (error) {
155
+ pwaLogger.error('[PWA Install] Error showing install prompt:', error);
156
+ return null;
157
+ }
158
+ };
159
+
160
+ return {
161
+ ...state,
162
+ promptInstall,
163
+ // Expose full browser info
164
+ browser,
165
+ device,
166
+ };
167
+ }
@@ -0,0 +1,115 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * Hook to detect if app is running as PWA
5
+ *
6
+ * Checks if the app is running in standalone mode (added to home screen).
7
+ * Results are cached in sessionStorage for fast initialization across page loads.
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * // Basic usage (standard check)
12
+ * const isPWA = useIsPWA();
13
+ *
14
+ * // Reliable check (with manifest validation for desktop)
15
+ * const isPWA = useIsPWA({ reliable: true });
16
+ * ```
17
+ */
18
+
19
+ import { useState, useEffect } from 'react';
20
+ import { isStandalone, isStandaloneReliable, onDisplayModeChange } from '../utils/platform';
21
+
22
+ const CACHE_KEY = 'pwa_is_standalone';
23
+
24
+ /**
25
+ * Options for useIsPWA hook
26
+ */
27
+ export interface UseIsPWAOptions {
28
+ /**
29
+ * Use reliable check with additional validation for desktop browsers
30
+ * This prevents false positives on Safari macOS "Add to Dock"
31
+ * @default false
32
+ */
33
+ reliable?: boolean;
34
+ }
35
+
36
+ /**
37
+ * Hook to detect if app is running as PWA (standalone mode)
38
+ *
39
+ * @param options - Configuration options
40
+ * @returns true if app is running as PWA
41
+ */
42
+ export function useIsPWA(options?: UseIsPWAOptions): boolean {
43
+ const checkFunction = options?.reliable ? isStandaloneReliable : isStandalone;
44
+
45
+ const [isPWA, setIsPWA] = useState<boolean>(() => {
46
+ // Try to restore from cache for fast initialization
47
+ if (typeof window !== 'undefined') {
48
+ try {
49
+ const cached = sessionStorage.getItem(CACHE_KEY);
50
+ if (cached !== null) {
51
+ return cached === 'true';
52
+ }
53
+ } catch {
54
+ // Ignore sessionStorage errors (e.g., in incognito mode)
55
+ }
56
+ }
57
+
58
+ // Initial check
59
+ return checkFunction();
60
+ });
61
+
62
+ useEffect(() => {
63
+ // Check after mount (may differ from SSR)
64
+ const isStandaloneMode = checkFunction();
65
+ setIsPWA(isStandaloneMode);
66
+
67
+ // Update cache
68
+ if (typeof window !== 'undefined') {
69
+ try {
70
+ sessionStorage.setItem(CACHE_KEY, String(isStandaloneMode));
71
+ } catch {
72
+ // Ignore sessionStorage errors
73
+ }
74
+ }
75
+
76
+ // Listen for display-mode changes
77
+ const cleanup = onDisplayModeChange((newValue) => {
78
+ setIsPWA(newValue);
79
+
80
+ // Update cache
81
+ if (typeof window !== 'undefined') {
82
+ try {
83
+ sessionStorage.setItem(CACHE_KEY, String(newValue));
84
+ } catch {
85
+ // Ignore sessionStorage errors
86
+ }
87
+ }
88
+ });
89
+
90
+ return cleanup;
91
+ }, [checkFunction]);
92
+
93
+ return isPWA;
94
+ }
95
+
96
+ /**
97
+ * Clear isPWA cache
98
+ *
99
+ * Useful for testing or when you want to force a re-check
100
+ *
101
+ * @example
102
+ * ```typescript
103
+ * import { clearIsPWACache } from '@djangocfg/layouts/snippets';
104
+ * clearIsPWACache();
105
+ * window.location.reload();
106
+ * ```
107
+ */
108
+ export function clearIsPWACache(): void {
109
+ if (typeof window === 'undefined') return;
110
+ try {
111
+ sessionStorage.removeItem(CACHE_KEY);
112
+ } catch {
113
+ // Ignore sessionStorage errors
114
+ }
115
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * PWA Install Snippet
3
+ *
4
+ * Simplified PWA installation for web apps
5
+ * Handles Add to Home Screen (A2HS) for iOS Safari and Android Chrome
6
+ *
7
+ * @example Basic usage
8
+ * ```tsx
9
+ * import { PwaProvider, A2HSHint } from '@/snippets/PWAInstall';
10
+ *
11
+ * export default function Layout({ children }) {
12
+ * return (
13
+ * <PwaProvider>
14
+ * {children}
15
+ * <A2HSHint resetAfterDays={3} />
16
+ * </PwaProvider>
17
+ * );
18
+ * }
19
+ * ```
20
+ */
21
+
22
+ // Main API
23
+ export { PwaProvider, useInstall } from './context/InstallContext';
24
+ export { A2HSHint } from './components/A2HSHint';
25
+ export { IOSGuide } from './components/IOSGuide';
26
+ export { DesktopGuide } from './components/DesktopGuide';
27
+
28
+ // Hooks
29
+ export { useIsPWA, clearIsPWACache, type UseIsPWAOptions } from './hooks/useIsPWA';
30
+
31
+ // Utilities
32
+ export {
33
+ isStandalone,
34
+ isStandaloneReliable,
35
+ isMobileDevice,
36
+ hasValidManifest,
37
+ getDisplayMode,
38
+ onDisplayModeChange,
39
+ } from './utils/platform';
40
+
41
+ export {
42
+ pwaLogger,
43
+ enablePWADebug,
44
+ disablePWADebug,
45
+ isPWADebugEnabled,
46
+ } from './utils/logger';
47
+
48
+ export {
49
+ STORAGE_KEYS,
50
+ markA2HSDismissed,
51
+ isA2HSDismissedRecently,
52
+ clearAllPWAInstallData,
53
+ } from './utils/localStorage';
54
+
55
+ // Types - Configuration
56
+ export type { PwaInstallConfig } from './types';
57
+
58
+ // Types - Platform
59
+ export type { PlatformInfo } from './types';
60
+
61
+ // Types - Install
62
+ export type {
63
+ InstallPromptState,
64
+ BeforeInstallPromptEvent,
65
+ InstallOutcome,
66
+ IOSGuideState,
67
+ } from './types';
68
+
69
+ // Types - Components
70
+ export type {
71
+ InstallContextType,
72
+ InstallManagerProps,
73
+ AndroidInstallButtonProps,
74
+ IOSGuideModalProps,
75
+ InstallStep,
76
+ } from './types';