@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.
- 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 +105 -28
- 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/McpChat/context/ChatContext.tsx +9 -0
- package/src/snippets/PWAInstall/@docs/README.md +92 -0
- package/src/snippets/PWAInstall/@docs/research/ios-android-install-flows.md +576 -0
- package/src/snippets/PWAInstall/README.md +185 -0
- package/src/snippets/PWAInstall/components/A2HSHint.tsx +227 -0
- package/src/snippets/PWAInstall/components/DesktopGuide.tsx +229 -0
- package/src/snippets/PWAInstall/components/IOSGuide.tsx +29 -0
- package/src/snippets/PWAInstall/components/IOSGuideDrawer.tsx +101 -0
- package/src/snippets/PWAInstall/components/IOSGuideModal.tsx +101 -0
- package/src/snippets/PWAInstall/context/InstallContext.tsx +102 -0
- package/src/snippets/PWAInstall/hooks/useInstallPrompt.ts +167 -0
- package/src/snippets/PWAInstall/hooks/useIsPWA.ts +115 -0
- package/src/snippets/PWAInstall/index.ts +76 -0
- package/src/snippets/PWAInstall/types/components.ts +95 -0
- package/src/snippets/PWAInstall/types/config.ts +22 -0
- package/src/snippets/PWAInstall/types/index.ts +26 -0
- package/src/snippets/PWAInstall/types/install.ts +38 -0
- package/src/snippets/PWAInstall/types/platform.ts +29 -0
- package/src/snippets/PWAInstall/utils/localStorage.ts +181 -0
- package/src/snippets/PWAInstall/utils/logger.ts +149 -0
- package/src/snippets/PWAInstall/utils/platform.ts +151 -0
- 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/PushNotifications/components/PushPrompt.tsx +165 -0
- package/src/snippets/PushNotifications/config.ts +20 -0
- package/src/snippets/PushNotifications/context/DjangoPushContext.tsx +190 -0
- package/src/snippets/PushNotifications/hooks/useDjangoPush.ts +259 -0
- package/src/snippets/PushNotifications/hooks/usePushNotifications.ts +209 -0
- 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/types/push.ts +21 -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/PushNotifications/utils/vapid.ts +226 -0
- package/src/snippets/index.ts +55 -0
- package/src/layouts/shared/index.ts +0 -21
- package/src/layouts/shared/types.ts +0 -211
- /package/src/layouts/{shared → types}/README.md +0 -0
|
@@ -0,0 +1,209 @@
|
|
|
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
|
+
/**
|
|
53
|
+
* Subscribe to push notifications
|
|
54
|
+
* @returns subscription on success, null on failure
|
|
55
|
+
*/
|
|
56
|
+
const subscribe = async (): Promise<PushSubscription | null> => {
|
|
57
|
+
if (!state.isSupported) {
|
|
58
|
+
pwaLogger.warn('[usePushNotifications] Push notifications not supported');
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (!options?.vapidPublicKey) {
|
|
63
|
+
pwaLogger.error('[usePushNotifications] VAPID public key required');
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
// Pre-flight check: Test network connectivity
|
|
69
|
+
pwaLogger.debug('[usePushNotifications] Running pre-flight checks...');
|
|
70
|
+
|
|
71
|
+
// Check if we're online
|
|
72
|
+
if (!navigator.onLine) {
|
|
73
|
+
pwaLogger.error('[usePushNotifications] No internet connection');
|
|
74
|
+
throw new Error('No internet connection. Please check your network and try again.');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Request permission
|
|
78
|
+
const permission = await Notification.requestPermission();
|
|
79
|
+
setState((prev) => ({ ...prev, permission }));
|
|
80
|
+
|
|
81
|
+
if (permission !== 'granted') {
|
|
82
|
+
pwaLogger.warn('[usePushNotifications] Permission not granted:', permission);
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Subscribe to push
|
|
87
|
+
const registration = await navigator.serviceWorker.ready;
|
|
88
|
+
|
|
89
|
+
// Convert and validate VAPID key
|
|
90
|
+
let applicationServerKey: Uint8Array;
|
|
91
|
+
try {
|
|
92
|
+
pwaLogger.debug('[usePushNotifications] Converting VAPID key...');
|
|
93
|
+
applicationServerKey = urlBase64ToUint8Array(options.vapidPublicKey);
|
|
94
|
+
pwaLogger.info('[usePushNotifications] VAPID key validated successfully');
|
|
95
|
+
} catch (e) {
|
|
96
|
+
if (e instanceof VapidKeyError) {
|
|
97
|
+
pwaLogger.error(`[usePushNotifications] Invalid VAPID key: ${e.message} (code: ${e.code})`);
|
|
98
|
+
} else {
|
|
99
|
+
pwaLogger.error('[usePushNotifications] Failed to convert VAPID key:', e);
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Diagnostic logging (only in debug mode)
|
|
105
|
+
pwaLogger.debug('[usePushNotifications] Service Worker state:', {
|
|
106
|
+
controller: navigator.serviceWorker.controller ? 'active' : 'none',
|
|
107
|
+
registrationActive: registration.active ? 'yes' : 'no',
|
|
108
|
+
permission: Notification.permission,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Check for existing subscription and unsubscribe
|
|
112
|
+
const existingSub = await registration.pushManager.getSubscription();
|
|
113
|
+
if (existingSub) {
|
|
114
|
+
pwaLogger.debug('[usePushNotifications] Unsubscribing from existing subscription...');
|
|
115
|
+
await existingSub.unsubscribe();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Prepare subscription options
|
|
119
|
+
const subscribeOptions = {
|
|
120
|
+
userVisibleOnly: true,
|
|
121
|
+
applicationServerKey: applicationServerKey as unknown as BufferSource,
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
pwaLogger.debug('[usePushNotifications] Subscribing with VAPID key...');
|
|
125
|
+
|
|
126
|
+
// Attempt subscribe
|
|
127
|
+
const subscription = await registration.pushManager.subscribe(subscribeOptions);
|
|
128
|
+
|
|
129
|
+
// Send subscription to server (legacy endpoint support)
|
|
130
|
+
if (options.subscribeEndpoint) {
|
|
131
|
+
await fetch(options.subscribeEndpoint, {
|
|
132
|
+
method: 'POST',
|
|
133
|
+
headers: { 'Content-Type': 'application/json' },
|
|
134
|
+
body: JSON.stringify(subscription),
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
setState((prev) => ({
|
|
139
|
+
...prev,
|
|
140
|
+
isSubscribed: true,
|
|
141
|
+
subscription,
|
|
142
|
+
}));
|
|
143
|
+
|
|
144
|
+
pwaLogger.success('[usePushNotifications] Successfully subscribed to push notifications');
|
|
145
|
+
return subscription;
|
|
146
|
+
} catch (error: any) {
|
|
147
|
+
pwaLogger.error('[usePushNotifications] Subscribe failed:', error);
|
|
148
|
+
|
|
149
|
+
// Specific diagnostic for push service errors
|
|
150
|
+
if (error.name === 'AbortError' || error.message?.includes('push service error')) {
|
|
151
|
+
pwaLogger.error('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
152
|
+
pwaLogger.error('❌ PUSH SERVICE ERROR - Cannot connect to FCM');
|
|
153
|
+
pwaLogger.error('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
154
|
+
pwaLogger.error('');
|
|
155
|
+
pwaLogger.error('🔍 This is NOT a code bug - it\'s a network/security block.');
|
|
156
|
+
pwaLogger.error('');
|
|
157
|
+
pwaLogger.error('✅ Quick Fixes (try in order):');
|
|
158
|
+
pwaLogger.error(' 1. Disable VPN/Proxy and refresh page');
|
|
159
|
+
pwaLogger.error(' 2. Open Incognito window (Cmd+Shift+N)');
|
|
160
|
+
pwaLogger.error(' 3. Try different browser (Safari, Firefox)');
|
|
161
|
+
pwaLogger.error(' 4. Use mobile hotspot instead of WiFi');
|
|
162
|
+
pwaLogger.error('');
|
|
163
|
+
pwaLogger.error('🔧 Technical Details:');
|
|
164
|
+
pwaLogger.error(' • Browser tries to connect to FCM (ports 5228-5230)');
|
|
165
|
+
pwaLogger.error(' • VPN/Firewall/Privacy settings may block these ports');
|
|
166
|
+
pwaLogger.error(' • Check browser console for "AbortError" details');
|
|
167
|
+
pwaLogger.error('');
|
|
168
|
+
pwaLogger.error('📚 Learn more: https://web.dev/push-notifications-overview/');
|
|
169
|
+
pwaLogger.error('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
170
|
+
} else if (error.message?.includes('No internet connection')) {
|
|
171
|
+
pwaLogger.error('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
172
|
+
pwaLogger.error('❌ NO INTERNET CONNECTION');
|
|
173
|
+
pwaLogger.error('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
174
|
+
pwaLogger.error('Please check your network connection and try again.');
|
|
175
|
+
} else {
|
|
176
|
+
pwaLogger.error('Unknown error:', error.name, error.message);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const unsubscribe = async (): Promise<boolean> => {
|
|
184
|
+
if (!state.subscription) {
|
|
185
|
+
pwaLogger.warn('[usePushNotifications] No active subscription to unsubscribe');
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
await state.subscription.unsubscribe();
|
|
191
|
+
setState((prev) => ({
|
|
192
|
+
...prev,
|
|
193
|
+
isSubscribed: false,
|
|
194
|
+
subscription: null,
|
|
195
|
+
}));
|
|
196
|
+
pwaLogger.info('[usePushNotifications] Successfully unsubscribed from push notifications');
|
|
197
|
+
return true;
|
|
198
|
+
} catch (error) {
|
|
199
|
+
pwaLogger.error('[usePushNotifications] Unsubscribe failed:', error);
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
...state,
|
|
206
|
+
subscribe,
|
|
207
|
+
unsubscribe,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Push Notifications Snippet
|
|
3
|
+
*
|
|
4
|
+
* Web Push Notifications management with Django-CFG integration
|
|
5
|
+
* Includes push history, subscription management, and Django API integration
|
|
6
|
+
*
|
|
7
|
+
* @example Usage
|
|
8
|
+
* ```tsx
|
|
9
|
+
* import { DjangoPushProvider, useDjangoPushContext } from '@/snippets/PushNotifications';
|
|
10
|
+
*
|
|
11
|
+
* // In layout
|
|
12
|
+
* export default function Layout({ children }) {
|
|
13
|
+
* return (
|
|
14
|
+
* <DjangoPushProvider
|
|
15
|
+
* vapidPublicKey={process.env.NEXT_PUBLIC_VAPID_KEY}
|
|
16
|
+
* autoSubscribe={false}
|
|
17
|
+
* >
|
|
18
|
+
* {children}
|
|
19
|
+
* <PushPrompt requirePWA={true} />
|
|
20
|
+
* </DjangoPushProvider>
|
|
21
|
+
* );
|
|
22
|
+
* }
|
|
23
|
+
*
|
|
24
|
+
* // In component
|
|
25
|
+
* function PushDemo() {
|
|
26
|
+
* const { isSubscribed, pushes, subscribe, sendPush } = useDjangoPushContext();
|
|
27
|
+
*
|
|
28
|
+
* return (
|
|
29
|
+
* <div>
|
|
30
|
+
* {!isSubscribed && <button onClick={subscribe}>Subscribe</button>}
|
|
31
|
+
* <div>Received {pushes.length} notifications</div>
|
|
32
|
+
* </div>
|
|
33
|
+
* );
|
|
34
|
+
* }
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* @note For backwards compatibility, you can also use `PushProvider` and `usePush`
|
|
38
|
+
* aliases which point to the same Django implementation.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
// Main API - Django Push Integration
|
|
42
|
+
export { DjangoPushProvider, useDjangoPushContext } from './context/DjangoPushContext';
|
|
43
|
+
export type { PushMessage } from './context/DjangoPushContext';
|
|
44
|
+
|
|
45
|
+
// Backwards compatibility aliases
|
|
46
|
+
export { DjangoPushProvider as PushProvider } from './context/DjangoPushContext';
|
|
47
|
+
export { useDjangoPushContext as usePush } from './context/DjangoPushContext';
|
|
48
|
+
|
|
49
|
+
// Components
|
|
50
|
+
export { PushPrompt } from './components/PushPrompt';
|
|
51
|
+
|
|
52
|
+
// Hooks
|
|
53
|
+
export { usePushNotifications } from './hooks/usePushNotifications';
|
|
54
|
+
export { useDjangoPush } from './hooks/useDjangoPush';
|
|
55
|
+
|
|
56
|
+
// Config
|
|
57
|
+
export { DEFAULT_VAPID_PUBLIC_KEY } from './config';
|
|
58
|
+
|
|
59
|
+
// Utilities
|
|
60
|
+
export {
|
|
61
|
+
urlBase64ToUint8Array,
|
|
62
|
+
isValidVapidKey,
|
|
63
|
+
getVapidKeyInfo,
|
|
64
|
+
safeUrlBase64ToUint8Array,
|
|
65
|
+
VapidKeyError,
|
|
66
|
+
type VapidKeyErrorCode,
|
|
67
|
+
} from './utils/vapid';
|
|
68
|
+
|
|
69
|
+
export {
|
|
70
|
+
pwaLogger,
|
|
71
|
+
enablePWADebug,
|
|
72
|
+
disablePWADebug,
|
|
73
|
+
isPWADebugEnabled,
|
|
74
|
+
} from './utils/logger';
|
|
75
|
+
|
|
76
|
+
export {
|
|
77
|
+
STORAGE_KEYS,
|
|
78
|
+
markPushDismissed,
|
|
79
|
+
isPushDismissedRecently,
|
|
80
|
+
clearAllPushData,
|
|
81
|
+
} from './utils/localStorage';
|
|
82
|
+
|
|
83
|
+
// Types - Configuration
|
|
84
|
+
export type { PushNotificationsConfig } from './types';
|
|
85
|
+
|
|
86
|
+
// Types
|
|
87
|
+
export type { PushNotificationState, PushNotificationOptions } from './types';
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Push Notifications Configuration Types
|
|
3
|
+
*
|
|
4
|
+
* Configuration for Web Push Notifications with VAPID authentication
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface PushNotificationsConfig {
|
|
8
|
+
/** Enable push notifications */
|
|
9
|
+
enabled?: boolean;
|
|
10
|
+
|
|
11
|
+
/** VAPID public key for push notifications (required when enabled) */
|
|
12
|
+
vapidPublicKey: string;
|
|
13
|
+
|
|
14
|
+
/** API endpoint for subscription (default: '/api/push/subscribe') */
|
|
15
|
+
subscribeEndpoint?: string;
|
|
16
|
+
|
|
17
|
+
/** Only show push prompt if PWA is installed (default: true) */
|
|
18
|
+
requirePWA?: boolean;
|
|
19
|
+
|
|
20
|
+
/** Delay before showing push prompt after PWA install (ms) */
|
|
21
|
+
delayMs?: number;
|
|
22
|
+
|
|
23
|
+
/** Number of days before re-showing dismissed push prompt */
|
|
24
|
+
resetAfterDays?: number;
|
|
25
|
+
|
|
26
|
+
/** Auto-subscribe on mount if permission already granted */
|
|
27
|
+
autoSubscribe?: boolean;
|
|
28
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LocalStorage utilities for Push Notifications state persistence
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Storage keys
|
|
7
|
+
*/
|
|
8
|
+
export const STORAGE_KEYS = {
|
|
9
|
+
PUSH_DISMISSED: 'pwa_push_dismissed_at',
|
|
10
|
+
} as const;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Check if push prompt was dismissed recently
|
|
14
|
+
* @param resetDays Number of days before re-showing (default: 7)
|
|
15
|
+
*/
|
|
16
|
+
export function isPushDismissedRecently(resetDays: number = 7): boolean {
|
|
17
|
+
return isDismissedRecentlyHelper(resetDays, STORAGE_KEYS.PUSH_DISMISSED);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Mark push prompt as dismissed
|
|
22
|
+
*/
|
|
23
|
+
export function markPushDismissed(): void {
|
|
24
|
+
if (typeof window === 'undefined') return;
|
|
25
|
+
try {
|
|
26
|
+
localStorage.setItem(STORAGE_KEYS.PUSH_DISMISSED, Date.now().toString());
|
|
27
|
+
} catch {
|
|
28
|
+
// Fail silently
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Helper: Check if a key was dismissed recently
|
|
34
|
+
* @internal
|
|
35
|
+
*/
|
|
36
|
+
function isDismissedRecentlyHelper(resetDays: number, key: string): boolean {
|
|
37
|
+
if (typeof window === 'undefined') return false;
|
|
38
|
+
try {
|
|
39
|
+
const dismissed = localStorage.getItem(key);
|
|
40
|
+
if (!dismissed) return false;
|
|
41
|
+
const dismissedAt = parseInt(dismissed, 10);
|
|
42
|
+
const daysSince = (Date.now() - dismissedAt) / (1000 * 60 * 60 * 24);
|
|
43
|
+
return daysSince < resetDays;
|
|
44
|
+
} catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Clear all push notification data
|
|
51
|
+
*/
|
|
52
|
+
export function clearAllPushData(): void {
|
|
53
|
+
if (typeof window === 'undefined') return;
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
localStorage.removeItem(STORAGE_KEYS.PUSH_DISMISSED);
|
|
57
|
+
} catch {
|
|
58
|
+
// Fail silently
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PWA Logger with Conditional Logging
|
|
3
|
+
*
|
|
4
|
+
* Provides logging utilities that respect environment and debug settings:
|
|
5
|
+
* - In production: Only errors are logged
|
|
6
|
+
* - In development: All levels are logged
|
|
7
|
+
* - Debug mode: Can be enabled in production via localStorage
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* import { pwaLogger } from '../utils/logger';
|
|
12
|
+
*
|
|
13
|
+
* pwaLogger.info('Info message'); // Only in dev
|
|
14
|
+
* pwaLogger.warn('Warning message'); // Only in dev
|
|
15
|
+
* pwaLogger.error('Error message'); // Always logged
|
|
16
|
+
* pwaLogger.debug('Debug message'); // Only when debug enabled
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* Enable debug mode in production:
|
|
20
|
+
* ```typescript
|
|
21
|
+
* import { enablePWADebug } from '../utils/logger';
|
|
22
|
+
* enablePWADebug();
|
|
23
|
+
* // or in console:
|
|
24
|
+
* localStorage.setItem('pwa_debug', 'true');
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { consola } from 'consola';
|
|
29
|
+
|
|
30
|
+
const isDevelopment = process.env.NODE_ENV === 'development';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Check if debug mode is enabled via localStorage
|
|
34
|
+
*/
|
|
35
|
+
function isDebugEnabled(): boolean {
|
|
36
|
+
if (typeof window === 'undefined') return false;
|
|
37
|
+
try {
|
|
38
|
+
return localStorage.getItem('pwa_debug') === 'true';
|
|
39
|
+
} catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* PWA Logger instance with conditional logging
|
|
46
|
+
*/
|
|
47
|
+
export const pwaLogger = {
|
|
48
|
+
/**
|
|
49
|
+
* Info level logging
|
|
50
|
+
* Only logs in development or when debug is enabled
|
|
51
|
+
*/
|
|
52
|
+
info: (...args: Parameters<typeof consola.info>): void => {
|
|
53
|
+
if (isDevelopment || isDebugEnabled()) {
|
|
54
|
+
consola.info(...args);
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Warning level logging
|
|
60
|
+
* Only logs in development or when debug is enabled
|
|
61
|
+
*/
|
|
62
|
+
warn: (...args: Parameters<typeof consola.warn>): void => {
|
|
63
|
+
if (isDevelopment || isDebugEnabled()) {
|
|
64
|
+
consola.warn(...args);
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Error level logging
|
|
70
|
+
* Always logs (production + development)
|
|
71
|
+
*/
|
|
72
|
+
error: (...args: Parameters<typeof consola.error>): void => {
|
|
73
|
+
consola.error(...args);
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Debug level logging
|
|
78
|
+
* Only logs when debug is explicitly enabled
|
|
79
|
+
*/
|
|
80
|
+
debug: (...args: Parameters<typeof consola.debug>): void => {
|
|
81
|
+
if (isDebugEnabled()) {
|
|
82
|
+
consola.debug(...args);
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Success level logging
|
|
88
|
+
* Only logs in development or when debug is enabled
|
|
89
|
+
*/
|
|
90
|
+
success: (...args: Parameters<typeof consola.success>): void => {
|
|
91
|
+
if (isDevelopment || isDebugEnabled()) {
|
|
92
|
+
consola.success(...args);
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Enable debug mode
|
|
99
|
+
*
|
|
100
|
+
* This allows seeing debug logs in production.
|
|
101
|
+
* Call this function or set localStorage manually:
|
|
102
|
+
* `localStorage.setItem('pwa_debug', 'true')`
|
|
103
|
+
*
|
|
104
|
+
* @example
|
|
105
|
+
* ```typescript
|
|
106
|
+
* import { enablePWADebug } from '@djangocfg/layouts/snippets';
|
|
107
|
+
* enablePWADebug();
|
|
108
|
+
* // Reload page to see debug logs
|
|
109
|
+
* ```
|
|
110
|
+
*/
|
|
111
|
+
export function enablePWADebug(): void {
|
|
112
|
+
if (typeof window !== 'undefined') {
|
|
113
|
+
try {
|
|
114
|
+
localStorage.setItem('pwa_debug', 'true');
|
|
115
|
+
consola.info('[PWA] Debug mode enabled. Reload page to see debug logs.');
|
|
116
|
+
} catch (e) {
|
|
117
|
+
consola.error('[PWA] Failed to enable debug mode:', e);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Disable debug mode
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* ```typescript
|
|
127
|
+
* import { disablePWADebug } from '@djangocfg/layouts/snippets';
|
|
128
|
+
* disablePWADebug();
|
|
129
|
+
* ```
|
|
130
|
+
*/
|
|
131
|
+
export function disablePWADebug(): void {
|
|
132
|
+
if (typeof window !== 'undefined') {
|
|
133
|
+
try {
|
|
134
|
+
localStorage.removeItem('pwa_debug');
|
|
135
|
+
consola.info('[PWA] Debug mode disabled.');
|
|
136
|
+
} catch (e) {
|
|
137
|
+
consola.error('[PWA] Failed to disable debug mode:', e);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Check if debug mode is currently active
|
|
144
|
+
*
|
|
145
|
+
* @returns true if debug mode is enabled
|
|
146
|
+
*/
|
|
147
|
+
export function isPWADebugEnabled(): boolean {
|
|
148
|
+
return isDebugEnabled();
|
|
149
|
+
}
|