@djangocfg/layouts 2.1.35 → 2.1.37

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 (40) hide show
  1. package/package.json +5 -5
  2. package/src/layouts/AppLayout/BaseApp.tsx +31 -25
  3. package/src/layouts/shared/types.ts +36 -0
  4. package/src/snippets/McpChat/context/ChatContext.tsx +9 -0
  5. package/src/snippets/PWA/@docs/research.md +576 -0
  6. package/src/snippets/PWA/@refactoring/ARCHITECTURE_ANALYSIS.md +1179 -0
  7. package/src/snippets/PWA/@refactoring/EXECUTIVE_SUMMARY.md +271 -0
  8. package/src/snippets/PWA/@refactoring/README.md +204 -0
  9. package/src/snippets/PWA/@refactoring/REFACTORING_PROPOSALS.md +1109 -0
  10. package/src/snippets/PWA/@refactoring2/COMPARISON-WITH-NEXTJS.md +718 -0
  11. package/src/snippets/PWA/@refactoring2/P1-FIXES-COMPLETED.md +188 -0
  12. package/src/snippets/PWA/@refactoring2/POST-P0-ANALYSIS.md +362 -0
  13. package/src/snippets/PWA/@refactoring2/README.md +85 -0
  14. package/src/snippets/PWA/@refactoring2/RECOMMENDATIONS.md +1321 -0
  15. package/src/snippets/PWA/@refactoring2/REMAINING-ISSUES.md +557 -0
  16. package/src/snippets/PWA/README.md +387 -0
  17. package/src/snippets/PWA/components/A2HSHint.tsx +226 -0
  18. package/src/snippets/PWA/components/IOSGuide.tsx +29 -0
  19. package/src/snippets/PWA/components/IOSGuideDrawer.tsx +101 -0
  20. package/src/snippets/PWA/components/IOSGuideModal.tsx +101 -0
  21. package/src/snippets/PWA/components/PushPrompt.tsx +165 -0
  22. package/src/snippets/PWA/config.ts +20 -0
  23. package/src/snippets/PWA/context/DjangoPushContext.tsx +105 -0
  24. package/src/snippets/PWA/context/InstallContext.tsx +118 -0
  25. package/src/snippets/PWA/context/PushContext.tsx +156 -0
  26. package/src/snippets/PWA/hooks/useDjangoPush.ts +277 -0
  27. package/src/snippets/PWA/hooks/useInstallPrompt.ts +164 -0
  28. package/src/snippets/PWA/hooks/useIsPWA.ts +115 -0
  29. package/src/snippets/PWA/hooks/usePushNotifications.ts +205 -0
  30. package/src/snippets/PWA/index.ts +95 -0
  31. package/src/snippets/PWA/types/components.ts +101 -0
  32. package/src/snippets/PWA/types/index.ts +26 -0
  33. package/src/snippets/PWA/types/install.ts +38 -0
  34. package/src/snippets/PWA/types/platform.ts +29 -0
  35. package/src/snippets/PWA/types/push.ts +21 -0
  36. package/src/snippets/PWA/utils/localStorage.ts +203 -0
  37. package/src/snippets/PWA/utils/logger.ts +149 -0
  38. package/src/snippets/PWA/utils/platform.ts +151 -0
  39. package/src/snippets/PWA/utils/vapid.ts +226 -0
  40. package/src/snippets/index.ts +30 -0
@@ -0,0 +1,205 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * Push Notifications Hook
5
+ *
6
+ * Manages push notification subscription state
7
+ * Integrates with @djangocfg/nextjs/pwa
8
+ */
9
+
10
+ import { useState, useEffect } from 'react';
11
+ import type { PushNotificationState, PushNotificationOptions } from '../types';
12
+ import { pwaLogger } from '../utils/logger';
13
+ import { urlBase64ToUint8Array, VapidKeyError } from '../utils/vapid';
14
+
15
+ export function usePushNotifications(options?: PushNotificationOptions) {
16
+ const [state, setState] = useState<PushNotificationState>({
17
+ isSupported: false,
18
+ permission: 'default',
19
+ isSubscribed: false,
20
+ subscription: null,
21
+ });
22
+
23
+ // Check if push notifications are supported
24
+ useEffect(() => {
25
+ if (typeof window === 'undefined') return;
26
+
27
+ const isSupported = 'serviceWorker' in navigator && 'PushManager' in window && 'Notification' in window;
28
+
29
+ setState((prev) => ({
30
+ ...prev,
31
+ isSupported,
32
+ permission: isSupported ? Notification.permission : 'denied',
33
+ }));
34
+
35
+ // Check existing subscription
36
+ if (isSupported) {
37
+ navigator.serviceWorker.ready
38
+ .then((registration) => registration.pushManager.getSubscription())
39
+ .then((subscription) => {
40
+ setState((prev) => ({
41
+ ...prev,
42
+ isSubscribed: !!subscription,
43
+ subscription,
44
+ }));
45
+ })
46
+ .catch((error) => {
47
+ pwaLogger.error('[usePushNotifications] Failed to get subscription:', error);
48
+ });
49
+ }
50
+ }, []);
51
+
52
+ const subscribe = async (): Promise<boolean> => {
53
+ if (!state.isSupported) {
54
+ pwaLogger.warn('[usePushNotifications] Push notifications not supported');
55
+ return false;
56
+ }
57
+
58
+ if (!options?.vapidPublicKey) {
59
+ pwaLogger.error('[usePushNotifications] VAPID public key required');
60
+ return false;
61
+ }
62
+
63
+ try {
64
+ // Pre-flight check: Test network connectivity
65
+ pwaLogger.debug('[usePushNotifications] Running pre-flight checks...');
66
+
67
+ // Check if we're online
68
+ if (!navigator.onLine) {
69
+ pwaLogger.error('[usePushNotifications] No internet connection');
70
+ throw new Error('No internet connection. Please check your network and try again.');
71
+ }
72
+
73
+ // Request permission
74
+ const permission = await Notification.requestPermission();
75
+ setState((prev) => ({ ...prev, permission }));
76
+
77
+ if (permission !== 'granted') {
78
+ pwaLogger.warn('[usePushNotifications] Permission not granted:', permission);
79
+ return false;
80
+ }
81
+
82
+ // Subscribe to push
83
+ const registration = await navigator.serviceWorker.ready;
84
+
85
+ // Convert and validate VAPID key
86
+ let applicationServerKey: Uint8Array;
87
+ try {
88
+ pwaLogger.debug('[usePushNotifications] Converting VAPID key...');
89
+ applicationServerKey = urlBase64ToUint8Array(options.vapidPublicKey);
90
+ pwaLogger.info('[usePushNotifications] VAPID key validated successfully');
91
+ } catch (e) {
92
+ if (e instanceof VapidKeyError) {
93
+ pwaLogger.error(`[usePushNotifications] Invalid VAPID key: ${e.message} (code: ${e.code})`);
94
+ } else {
95
+ pwaLogger.error('[usePushNotifications] Failed to convert VAPID key:', e);
96
+ }
97
+ return false;
98
+ }
99
+
100
+ // Diagnostic logging (only in debug mode)
101
+ pwaLogger.debug('[usePushNotifications] Service Worker state:', {
102
+ controller: navigator.serviceWorker.controller ? 'active' : 'none',
103
+ registrationActive: registration.active ? 'yes' : 'no',
104
+ permission: Notification.permission,
105
+ });
106
+
107
+ // Check for existing subscription and unsubscribe
108
+ const existingSub = await registration.pushManager.getSubscription();
109
+ if (existingSub) {
110
+ pwaLogger.debug('[usePushNotifications] Unsubscribing from existing subscription...');
111
+ await existingSub.unsubscribe();
112
+ }
113
+
114
+ // Prepare subscription options
115
+ const subscribeOptions = {
116
+ userVisibleOnly: true,
117
+ applicationServerKey: applicationServerKey as unknown as BufferSource,
118
+ };
119
+
120
+ pwaLogger.debug('[usePushNotifications] Subscribing with VAPID key...');
121
+
122
+ // Attempt subscribe
123
+ const subscription = await registration.pushManager.subscribe(subscribeOptions);
124
+
125
+ // Send subscription to server
126
+ if (options.subscribeEndpoint) {
127
+ await fetch(options.subscribeEndpoint, {
128
+ method: 'POST',
129
+ headers: { 'Content-Type': 'application/json' },
130
+ body: JSON.stringify(subscription),
131
+ });
132
+ }
133
+
134
+ setState((prev) => ({
135
+ ...prev,
136
+ isSubscribed: true,
137
+ subscription,
138
+ }));
139
+
140
+ pwaLogger.success('[usePushNotifications] Successfully subscribed to push notifications');
141
+ return true;
142
+ } catch (error: any) {
143
+ pwaLogger.error('[usePushNotifications] Subscribe failed:', error);
144
+
145
+ // Specific diagnostic for push service errors
146
+ if (error.name === 'AbortError' || error.message?.includes('push service error')) {
147
+ pwaLogger.error('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
148
+ pwaLogger.error('❌ PUSH SERVICE ERROR - Cannot connect to FCM');
149
+ pwaLogger.error('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
150
+ pwaLogger.error('');
151
+ pwaLogger.error('🔍 This is NOT a code bug - it\'s a network/security block.');
152
+ pwaLogger.error('');
153
+ pwaLogger.error('✅ Quick Fixes (try in order):');
154
+ pwaLogger.error(' 1. Disable VPN/Proxy and refresh page');
155
+ pwaLogger.error(' 2. Open Incognito window (Cmd+Shift+N)');
156
+ pwaLogger.error(' 3. Try different browser (Safari, Firefox)');
157
+ pwaLogger.error(' 4. Use mobile hotspot instead of WiFi');
158
+ pwaLogger.error('');
159
+ pwaLogger.error('🔧 Technical Details:');
160
+ pwaLogger.error(' • Browser tries to connect to FCM (ports 5228-5230)');
161
+ pwaLogger.error(' • VPN/Firewall/Privacy settings may block these ports');
162
+ pwaLogger.error(' • Check browser console for "AbortError" details');
163
+ pwaLogger.error('');
164
+ pwaLogger.error('📚 Learn more: https://web.dev/push-notifications-overview/');
165
+ pwaLogger.error('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
166
+ } else if (error.message?.includes('No internet connection')) {
167
+ pwaLogger.error('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
168
+ pwaLogger.error('❌ NO INTERNET CONNECTION');
169
+ pwaLogger.error('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
170
+ pwaLogger.error('Please check your network connection and try again.');
171
+ } else {
172
+ pwaLogger.error('Unknown error:', error.name, error.message);
173
+ }
174
+
175
+ return false;
176
+ }
177
+ };
178
+
179
+ const unsubscribe = async (): Promise<boolean> => {
180
+ if (!state.subscription) {
181
+ pwaLogger.warn('[usePushNotifications] No active subscription to unsubscribe');
182
+ return false;
183
+ }
184
+
185
+ try {
186
+ await state.subscription.unsubscribe();
187
+ setState((prev) => ({
188
+ ...prev,
189
+ isSubscribed: false,
190
+ subscription: null,
191
+ }));
192
+ pwaLogger.info('[usePushNotifications] Successfully unsubscribed from push notifications');
193
+ return true;
194
+ } catch (error) {
195
+ pwaLogger.error('[usePushNotifications] Unsubscribe failed:', error);
196
+ return false;
197
+ }
198
+ };
199
+
200
+ return {
201
+ ...state,
202
+ subscribe,
203
+ unsubscribe,
204
+ };
205
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * PWA Install Flow (Simplified)
3
+ *
4
+ * Ultra-simple PWA installation for Cmdop
5
+ * No tracking, no metrics, no complexity — just install
6
+ *
7
+ * @example Basic usage
8
+ * ```tsx
9
+ * import { InstallProvider, A2HSHint } from './PwaInstall';
10
+ * import { DEFAULT_VAPID_PUBLIC_KEY } from './config';
11
+ *
12
+ * export default function Layout({ children }) {
13
+ * return (
14
+ * <InstallProvider>
15
+ * {children}
16
+ * <A2HSHint resetAfterDays={3} />
17
+ * </InstallProvider>
18
+ * );
19
+ * }
20
+ * ```
21
+ */
22
+
23
+ // Main API
24
+ export { PwaProvider, useInstall } from './context/InstallContext';
25
+ export { PushProvider, usePush } from './context/PushContext';
26
+ export type { PushMessage, PushContextValue } from './context/PushContext';
27
+
28
+ // Django Integration
29
+ export { DjangoPushProvider, useDjangoPushContext } from './context/DjangoPushContext';
30
+ export { A2HSHint } from './components/A2HSHint';
31
+ export { PushPrompt } from './components/PushPrompt';
32
+ export { DEFAULT_VAPID_PUBLIC_KEY } from './config';
33
+
34
+ // Hooks
35
+ export { useIsPWA, clearIsPWACache, type UseIsPWAOptions } from './hooks/useIsPWA';
36
+ export { usePushNotifications } from './hooks/usePushNotifications';
37
+ export { useDjangoPush } from './hooks/useDjangoPush';
38
+
39
+ // Utilities
40
+ export {
41
+ isStandalone,
42
+ isStandaloneReliable,
43
+ isMobileDevice,
44
+ hasValidManifest,
45
+ getDisplayMode,
46
+ onDisplayModeChange,
47
+ } from './utils/platform';
48
+
49
+ export {
50
+ pwaLogger,
51
+ enablePWADebug,
52
+ disablePWADebug,
53
+ isPWADebugEnabled,
54
+ } from './utils/logger';
55
+
56
+ export {
57
+ urlBase64ToUint8Array,
58
+ isValidVapidKey,
59
+ getVapidKeyInfo,
60
+ safeUrlBase64ToUint8Array,
61
+ VapidKeyError,
62
+ type VapidKeyErrorCode,
63
+ } from './utils/vapid';
64
+
65
+ export {
66
+ STORAGE_KEYS,
67
+ markA2HSDismissed,
68
+ markPushDismissed,
69
+ isA2HSDismissedRecently,
70
+ isPushDismissedRecently,
71
+ clearAllPWAData,
72
+ } from './utils/localStorage';
73
+
74
+ // Types - Platform
75
+ export type { PlatformInfo } from './types';
76
+
77
+ // Types - Install
78
+ export type {
79
+ InstallPromptState,
80
+ BeforeInstallPromptEvent,
81
+ InstallOutcome,
82
+ IOSGuideState,
83
+ } from './types';
84
+
85
+ // Types - Push Notifications
86
+ export type { PushNotificationState, PushNotificationOptions } from './types';
87
+
88
+ // Types - Components
89
+ export type {
90
+ InstallContextType,
91
+ InstallManagerProps,
92
+ AndroidInstallButtonProps,
93
+ IOSGuideModalProps,
94
+ InstallStep,
95
+ } from './types';
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Component Props Types
3
+ */
4
+
5
+ import type { PlatformInfo } from './platform';
6
+ import type { InstallOutcome } from './install';
7
+ import type { PushNotificationState } from './push';
8
+
9
+ /**
10
+ * Install context type
11
+ */
12
+ export interface InstallContextType {
13
+ // State
14
+ platform: PlatformInfo;
15
+ isInstalled: boolean;
16
+ canPrompt: boolean;
17
+
18
+ // iOS Guide
19
+ showIOSGuide: boolean;
20
+ setShowIOSGuide: (show: boolean) => void;
21
+
22
+ // Actions
23
+ promptInstall: () => Promise<InstallOutcome>;
24
+ dismissIOSGuide: () => void;
25
+
26
+ // Push Notifications
27
+ pushState: PushNotificationState;
28
+ subscribeToPush: () => Promise<boolean>;
29
+ unsubscribeFromPush: () => Promise<boolean>;
30
+ }
31
+
32
+ /**
33
+ * Install manager props
34
+ */
35
+ export interface InstallManagerProps {
36
+ /**
37
+ * Delay before showing iOS guide (ms)
38
+ * @default 2000
39
+ */
40
+ delayMs?: number;
41
+
42
+ /**
43
+ * Number of days before re-showing dismissed guide
44
+ * @default 7
45
+ */
46
+ resetDays?: number;
47
+
48
+ /**
49
+ * Custom class name for install button
50
+ */
51
+ buttonClassName?: string;
52
+
53
+ /**
54
+ * Custom button text
55
+ */
56
+ buttonText?: string;
57
+
58
+ /**
59
+ * Force show install UI (ignores platform detection)
60
+ * Useful for testing on desktop in development
61
+ * @default false
62
+ */
63
+ forceShow?: boolean;
64
+
65
+ /**
66
+ * Callback when install is successful
67
+ */
68
+ onInstallSuccess?: () => void;
69
+
70
+ /**
71
+ * Callback when install is dismissed
72
+ */
73
+ onInstallDismiss?: () => void;
74
+ }
75
+
76
+ /**
77
+ * Android install button props
78
+ */
79
+ export interface AndroidInstallButtonProps {
80
+ onInstall: () => Promise<InstallOutcome>;
81
+ className?: string;
82
+ text?: string;
83
+ }
84
+
85
+ /**
86
+ * iOS guide modal props
87
+ */
88
+ export interface IOSGuideModalProps {
89
+ onDismiss: () => void;
90
+ open?: boolean;
91
+ }
92
+
93
+ /**
94
+ * Install step for iOS guide
95
+ */
96
+ export interface InstallStep {
97
+ number: number;
98
+ title: string;
99
+ icon: React.ComponentType<{ className?: string }>;
100
+ description: string;
101
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * PWA Types - Central Export
3
+ */
4
+
5
+ // Platform
6
+ export type { PlatformInfo } from './platform';
7
+
8
+ // Install
9
+ export type {
10
+ InstallPromptState,
11
+ BeforeInstallPromptEvent,
12
+ InstallOutcome,
13
+ IOSGuideState,
14
+ } from './install';
15
+
16
+ // Push Notifications
17
+ export type { PushNotificationState, PushNotificationOptions } from './push';
18
+
19
+ // Components
20
+ export type {
21
+ InstallContextType,
22
+ InstallManagerProps,
23
+ AndroidInstallButtonProps,
24
+ IOSGuideModalProps,
25
+ InstallStep,
26
+ } from './components';
@@ -0,0 +1,38 @@
1
+ /**
2
+ * PWA Installation Types
3
+ */
4
+
5
+ /**
6
+ * Install prompt state
7
+ */
8
+ export interface InstallPromptState {
9
+ isIOS: boolean;
10
+ isAndroid: boolean;
11
+ isSafari: boolean;
12
+ isChrome: boolean;
13
+ isInstalled: boolean;
14
+ canPrompt: boolean;
15
+ deferredPrompt: BeforeInstallPromptEvent | null;
16
+ }
17
+
18
+ /**
19
+ * BeforeInstallPrompt event (Android Chrome)
20
+ */
21
+ export interface BeforeInstallPromptEvent extends Event {
22
+ prompt: () => Promise<void>;
23
+ userChoice: Promise<{ outcome: 'accepted' | 'dismissed'; platform: string }>;
24
+ }
25
+
26
+ /**
27
+ * Install outcome
28
+ */
29
+ export type InstallOutcome = 'accepted' | 'dismissed' | null;
30
+
31
+ /**
32
+ * iOS guide dismissal state
33
+ */
34
+ export interface IOSGuideState {
35
+ dismissed: boolean;
36
+ dismissedAt: number | null;
37
+ shouldShow: boolean;
38
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Platform Detection Types
3
+ */
4
+
5
+ /**
6
+ * Platform detection result
7
+ */
8
+ export interface PlatformInfo {
9
+ // Operating System
10
+ isIOS: boolean;
11
+ isAndroid: boolean;
12
+ isDesktop: boolean;
13
+
14
+ // Browser
15
+ isSafari: boolean;
16
+ isChrome: boolean;
17
+ isEdge: boolean;
18
+ isFirefox: boolean;
19
+
20
+ // Installation State
21
+ isStandalone: boolean;
22
+
23
+ // Capability
24
+ canPrompt: boolean;
25
+
26
+ // Composite checks
27
+ shouldShowAndroidPrompt: boolean;
28
+ shouldShowIOSGuide: boolean;
29
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Push Notification Types
3
+ */
4
+
5
+ /**
6
+ * Push notification state
7
+ */
8
+ export interface PushNotificationState {
9
+ isSupported: boolean;
10
+ permission: NotificationPermission;
11
+ isSubscribed: boolean;
12
+ subscription: PushSubscription | null;
13
+ }
14
+
15
+ /**
16
+ * Push notification options
17
+ */
18
+ export interface PushNotificationOptions {
19
+ vapidPublicKey: string;
20
+ subscribeEndpoint?: string;
21
+ }