@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
@@ -0,0 +1,190 @@
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, useState, useEffect, useCallback } from 'react';
27
+ import { useDjangoPush } from '../hooks/useDjangoPush';
28
+ import { pwaLogger } from '../utils/logger';
29
+ import type { PushNotificationOptions } from '../types';
30
+
31
+ export interface PushMessage {
32
+ id: string;
33
+ title: string;
34
+ body: string;
35
+ icon?: string;
36
+ badge?: string;
37
+ tag?: string;
38
+ timestamp: number;
39
+ data?: Record<string, unknown>;
40
+ }
41
+
42
+ interface DjangoPushContextValue {
43
+ // State
44
+ isSupported: boolean;
45
+ permission: NotificationPermission;
46
+ isSubscribed: boolean;
47
+ subscription: PushSubscription | null;
48
+ isLoading: boolean;
49
+ error: Error | null;
50
+
51
+ // Push history
52
+ pushes: PushMessage[];
53
+
54
+ // Actions
55
+ subscribe: () => Promise<boolean>;
56
+ unsubscribe: () => Promise<boolean>;
57
+ sendTestPush: (message: { title: string; body: string; url?: string }) => Promise<boolean>;
58
+ sendPush: (message: Omit<PushMessage, 'id' | 'timestamp'>) => Promise<void>;
59
+ clearPushes: () => void;
60
+ removePush: (id: string) => void;
61
+ }
62
+
63
+ const DjangoPushContext = createContext<DjangoPushContextValue | undefined>(undefined);
64
+
65
+ interface DjangoPushProviderProps extends PushNotificationOptions {
66
+ children: React.ReactNode;
67
+
68
+ /**
69
+ * Auto-subscribe on mount if permission granted
70
+ * @default false
71
+ */
72
+ autoSubscribe?: boolean;
73
+
74
+ /**
75
+ * Callback when subscription created
76
+ */
77
+ onSubscribed?: (subscription: PushSubscription) => void;
78
+
79
+ /**
80
+ * Callback when subscription failed
81
+ */
82
+ onSubscribeError?: (error: Error) => void;
83
+
84
+ /**
85
+ * Callback when unsubscribed
86
+ */
87
+ onUnsubscribed?: () => void;
88
+ }
89
+
90
+ /**
91
+ * Provider for Django push notifications
92
+ */
93
+ export function DjangoPushProvider({
94
+ children,
95
+ vapidPublicKey,
96
+ autoSubscribe = false,
97
+ onSubscribed,
98
+ onSubscribeError,
99
+ onUnsubscribed,
100
+ }: DjangoPushProviderProps) {
101
+ const djangoPush = useDjangoPush({
102
+ vapidPublicKey,
103
+ onSubscribed,
104
+ onSubscribeError,
105
+ onUnsubscribed,
106
+ });
107
+
108
+ // Auto-subscribe on mount if permission already granted
109
+ useEffect(() => {
110
+ if (
111
+ autoSubscribe &&
112
+ djangoPush.isSupported &&
113
+ djangoPush.permission === 'granted' &&
114
+ !djangoPush.isSubscribed &&
115
+ !djangoPush.isLoading
116
+ ) {
117
+ pwaLogger.info('[DjangoPushProvider] Auto-subscribing (permission already granted)');
118
+ djangoPush.subscribe();
119
+ }
120
+ }, [autoSubscribe, djangoPush.isSupported, djangoPush.permission, djangoPush.isSubscribed, djangoPush.isLoading]);
121
+
122
+ const [pushes, setPushes] = useState<PushMessage[]>([]);
123
+
124
+ // Listen for push messages from service worker
125
+ useEffect(() => {
126
+ if (typeof window === 'undefined' || !('serviceWorker' in navigator)) {
127
+ return;
128
+ }
129
+
130
+ const handleMessage = (event: MessageEvent) => {
131
+ if (event.data && event.data.type === 'PUSH_RECEIVED') {
132
+ const push: PushMessage = {
133
+ id: crypto.randomUUID(),
134
+ timestamp: Date.now(),
135
+ ...event.data.notification,
136
+ };
137
+
138
+ setPushes(prev => [push, ...prev]);
139
+ pwaLogger.info('[DjangoPushProvider] Push received:', push);
140
+ }
141
+ };
142
+
143
+ navigator.serviceWorker.addEventListener('message', handleMessage);
144
+ return () => navigator.serviceWorker.removeEventListener('message', handleMessage);
145
+ }, []);
146
+
147
+ const sendPush = useCallback(
148
+ async (message: Omit<PushMessage, 'id' | 'timestamp'>) => {
149
+ await djangoPush.sendTestPush({
150
+ title: message.title,
151
+ body: message.body,
152
+ url: message.data?.url as string | undefined,
153
+ });
154
+ },
155
+ [djangoPush]
156
+ );
157
+
158
+ const clearPushes = useCallback(() => {
159
+ setPushes([]);
160
+ pwaLogger.info('[DjangoPushProvider] Push history cleared');
161
+ }, []);
162
+
163
+ const removePush = useCallback((id: string) => {
164
+ setPushes(prev => prev.filter(p => p.id !== id));
165
+ pwaLogger.info('[DjangoPushProvider] Push removed:', id);
166
+ }, []);
167
+
168
+ const value: DjangoPushContextValue = {
169
+ ...djangoPush,
170
+ pushes,
171
+ sendPush,
172
+ clearPushes,
173
+ removePush,
174
+ };
175
+
176
+ return <DjangoPushContext.Provider value={value}>{children}</DjangoPushContext.Provider>;
177
+ }
178
+
179
+ /**
180
+ * Hook to access Django push context
181
+ */
182
+ export function useDjangoPushContext(): DjangoPushContextValue {
183
+ const context = useContext(DjangoPushContext);
184
+
185
+ if (context === undefined) {
186
+ throw new Error('useDjangoPushContext must be used within DjangoPushProvider');
187
+ }
188
+
189
+ return context;
190
+ }
@@ -3,21 +3,26 @@
3
3
  /**
4
4
  * Django Push Notifications Hook
5
5
  *
6
- * Integrates @djangocfg/layouts/PWA with Django-CFG backend API.
6
+ * Integrates usePushNotifications with Django-CFG backend API.
7
7
  * Uses generated TypeScript client from @djangocfg/api for type-safe integration.
8
+ * Requires authentication - opens AuthDialog if user is not logged in.
9
+ *
10
+ * Architecture:
11
+ * - usePushNotifications: handles all browser Push API operations
12
+ * - useDjangoPush: adds Django backend sync on top
8
13
  *
9
14
  * @example
10
15
  * ```tsx
11
- * import { useDjangoPush } from '@djangocfg/layouts/PWA';
16
+ * import { useDjangoPush } from '@djangocfg/layouts/snippets';
12
17
  *
13
18
  * function NotificationsButton() {
14
- * const { subscribe, isSubscribed, permission } = useDjangoPush({
19
+ * const { subscribe, unsubscribe, isSubscribed } = useDjangoPush({
15
20
  * vapidPublicKey: 'YOUR_VAPID_KEY'
16
21
  * });
17
22
  *
18
23
  * return (
19
- * <button onClick={subscribe} disabled={isSubscribed}>
20
- * {isSubscribed ? 'Subscribed' : 'Enable Notifications'}
24
+ * <button onClick={isSubscribed ? unsubscribe : subscribe}>
25
+ * {isSubscribed ? 'Unsubscribe' : 'Subscribe'}
21
26
  * </button>
22
27
  * );
23
28
  * }
@@ -25,21 +30,20 @@
25
30
  */
26
31
 
27
32
  import { useState, useCallback } from 'react';
33
+ import { toast } from 'sonner';
28
34
  import { usePushNotifications } from './usePushNotifications';
29
35
  import { pwaLogger } from '../utils/logger';
30
36
  import type { PushNotificationOptions } from '../types';
31
37
 
38
+ // Auth
39
+ import { useAuth } from '@djangocfg/api/auth';
40
+ import { useAuthDialog } from '../../AuthDialog';
41
+
32
42
  // Import Django API client
33
43
  // @ts-ignore - optional peer dependency
34
- import { apiAccounts, apiWebPush } from '@djangocfg/api/clients';
44
+ import { apiWebPush } from '@djangocfg/api/clients';
35
45
 
36
46
  interface UseDjangoPushOptions extends PushNotificationOptions {
37
- /**
38
- * Auto-subscribe on mount if permission granted
39
- * @default false
40
- */
41
- autoSubscribe?: boolean;
42
-
43
47
  /**
44
48
  * Callback when subscription created
45
49
  */
@@ -57,11 +61,13 @@ interface UseDjangoPushOptions extends PushNotificationOptions {
57
61
  }
58
62
 
59
63
  interface UseDjangoPushReturn {
60
- // State
64
+ // State (from usePushNotifications)
61
65
  isSupported: boolean;
62
66
  permission: NotificationPermission;
63
67
  isSubscribed: boolean;
64
68
  subscription: PushSubscription | null;
69
+
70
+ // Local state
65
71
  isLoading: boolean;
66
72
  error: Error | null;
67
73
 
@@ -75,35 +81,27 @@ interface UseDjangoPushReturn {
75
81
  * Hook for Django-CFG push notifications integration
76
82
  */
77
83
  export function useDjangoPush(options: UseDjangoPushOptions): UseDjangoPushReturn {
78
- const {
79
- autoSubscribe = false,
80
- onSubscribed,
81
- onSubscribeError,
82
- onUnsubscribed,
83
- ...pushOptions
84
- } = options;
84
+ const { onSubscribed, onSubscribeError, onUnsubscribed, ...pushOptions } = options;
85
85
 
86
86
  const [isLoading, setIsLoading] = useState(false);
87
87
  const [error, setError] = useState<Error | null>(null);
88
88
 
89
- // Use base push notifications hook
89
+ // Auth check
90
+ const { isAuthenticated } = useAuth();
91
+ const { openAuthDialog } = useAuthDialog();
92
+
93
+ // Use base push notifications hook (handles all browser API)
90
94
  const pushNotifications = usePushNotifications(pushOptions);
91
95
 
92
96
  /**
93
97
  * Subscribe to push notifications and save to Django backend
94
98
  */
95
99
  const subscribe = useCallback(async (): Promise<boolean> => {
96
- if (!pushNotifications.isSupported) {
97
- const err = new Error('Push notifications not supported');
98
- setError(err);
99
- onSubscribeError?.(err);
100
- return false;
101
- }
102
-
103
- if (!options.vapidPublicKey) {
104
- const err = new Error('VAPID public key required');
105
- setError(err);
106
- onSubscribeError?.(err);
100
+ // Auth check - require login for push subscriptions
101
+ if (!isAuthenticated) {
102
+ openAuthDialog({
103
+ message: 'Please sign in to enable push notifications',
104
+ });
107
105
  return false;
108
106
  }
109
107
 
@@ -111,26 +109,20 @@ export function useDjangoPush(options: UseDjangoPushOptions): UseDjangoPushRetur
111
109
  setError(null);
112
110
 
113
111
  try {
114
- // Step 1: Request permission
115
- const permission = await Notification.requestPermission();
116
- if (permission !== 'granted') {
117
- throw new Error(`Permission ${permission}`);
118
- }
112
+ // Step 1: Browser subscription via usePushNotifications
113
+ const subscription = await pushNotifications.subscribe();
119
114
 
120
- // Step 2: Get service worker registration
121
- const registration = await navigator.serviceWorker.ready;
122
-
123
- // Step 3: Subscribe to push manager
124
- const subscription = await registration.pushManager.subscribe({
125
- userVisibleOnly: true,
126
- applicationServerKey: urlBase64ToUint8Array(options.vapidPublicKey) as BufferSource,
127
- });
115
+ if (!subscription) {
116
+ // Permission denied or other browser error
117
+ const err = new Error('Browser subscription failed');
118
+ setError(err);
119
+ onSubscribeError?.(err);
120
+ return false;
121
+ }
128
122
 
129
- pwaLogger.info('[useDjangoPush] Browser subscription created:', {
130
- endpoint: subscription.endpoint.substring(0, 50) + '...',
131
- });
123
+ pwaLogger.info('[useDjangoPush] Browser subscription created');
132
124
 
133
- // Step 4: Save to Django backend using generated API client
125
+ // Step 2: Save to Django backend
134
126
  const result = await apiWebPush.web_push.webpushSubscribeCreate({
135
127
  endpoint: subscription.endpoint,
136
128
  keys: {
@@ -142,43 +134,47 @@ export function useDjangoPush(options: UseDjangoPushOptions): UseDjangoPushRetur
142
134
  pwaLogger.info('[useDjangoPush] Subscription saved to Django:', result);
143
135
 
144
136
  onSubscribed?.(subscription);
137
+ toast.success('Push notifications enabled');
145
138
  return true;
146
139
  } catch (err) {
147
140
  const error = err instanceof Error ? err : new Error(String(err));
148
141
  pwaLogger.error('[useDjangoPush] Subscribe failed:', error);
149
142
  setError(error);
150
143
  onSubscribeError?.(error);
144
+
145
+ toast.error('Subscription Failed', {
146
+ description: 'Could not save subscription to server. Please try again.',
147
+ duration: 5000,
148
+ });
149
+
151
150
  return false;
152
151
  } finally {
153
152
  setIsLoading(false);
154
153
  }
155
- }, [
156
- pushNotifications.isSupported,
157
- options.vapidPublicKey,
158
- onSubscribed,
159
- onSubscribeError,
160
- ]);
154
+ }, [isAuthenticated, openAuthDialog, pushNotifications.subscribe, onSubscribed, onSubscribeError]);
161
155
 
162
156
  /**
163
157
  * Unsubscribe from push notifications
164
158
  */
165
159
  const unsubscribe = useCallback(async (): Promise<boolean> => {
166
- if (!pushNotifications.subscription) {
167
- return false;
168
- }
169
-
170
160
  setIsLoading(true);
171
161
  setError(null);
172
162
 
173
163
  try {
174
- // Unsubscribe from push manager
175
- await pushNotifications.subscription.unsubscribe();
164
+ // Unsubscribe via usePushNotifications (handles browser API)
165
+ const success = await pushNotifications.unsubscribe();
166
+
167
+ if (!success) {
168
+ return false;
169
+ }
170
+
176
171
  pwaLogger.info('[useDjangoPush] Browser unsubscribed');
177
172
 
178
- // Note: Django backend will auto-cleanup inactive subscriptions
179
- // No need to explicitly remove since subscription.endpoint becomes invalid
173
+ // Note: Django backend auto-cleans up inactive subscriptions
174
+ // when push delivery fails (410 Gone response)
180
175
 
181
176
  onUnsubscribed?.();
177
+ toast.success('Push notifications disabled');
182
178
  return true;
183
179
  } catch (err) {
184
180
  const error = err instanceof Error ? err : new Error(String(err));
@@ -188,7 +184,7 @@ export function useDjangoPush(options: UseDjangoPushOptions): UseDjangoPushRetur
188
184
  } finally {
189
185
  setIsLoading(false);
190
186
  }
191
- }, [pushNotifications.subscription, onUnsubscribed]);
187
+ }, [pushNotifications.unsubscribe, onUnsubscribed]);
192
188
 
193
189
  /**
194
190
  * Send test push notification
@@ -227,11 +223,13 @@ export function useDjangoPush(options: UseDjangoPushOptions): UseDjangoPushRetur
227
223
  );
228
224
 
229
225
  return {
230
- // State
226
+ // State from usePushNotifications
231
227
  isSupported: pushNotifications.isSupported,
232
228
  permission: pushNotifications.permission,
233
229
  isSubscribed: pushNotifications.isSubscribed,
234
230
  subscription: pushNotifications.subscription,
231
+
232
+ // Local state
235
233
  isLoading,
236
234
  error,
237
235
 
@@ -246,22 +244,6 @@ export function useDjangoPush(options: UseDjangoPushOptions): UseDjangoPushRetur
246
244
  // Utilities
247
245
  // ============================================================================
248
246
 
249
- /**
250
- * Convert base64 VAPID key to Uint8Array
251
- */
252
- function urlBase64ToUint8Array(base64String: string): Uint8Array {
253
- const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
254
- const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
255
-
256
- const rawData = window.atob(base64);
257
- const outputArray = new Uint8Array(rawData.length);
258
-
259
- for (let i = 0; i < rawData.length; ++i) {
260
- outputArray[i] = rawData.charCodeAt(i);
261
- }
262
- return outputArray;
263
- }
264
-
265
247
  /**
266
248
  * Convert ArrayBuffer to base64 string
267
249
  */
@@ -49,15 +49,19 @@ export function usePushNotifications(options?: PushNotificationOptions) {
49
49
  }
50
50
  }, []);
51
51
 
52
- const subscribe = async (): Promise<boolean> => {
52
+ /**
53
+ * Subscribe to push notifications
54
+ * @returns subscription on success, null on failure
55
+ */
56
+ const subscribe = async (): Promise<PushSubscription | null> => {
53
57
  if (!state.isSupported) {
54
58
  pwaLogger.warn('[usePushNotifications] Push notifications not supported');
55
- return false;
59
+ return null;
56
60
  }
57
61
 
58
62
  if (!options?.vapidPublicKey) {
59
63
  pwaLogger.error('[usePushNotifications] VAPID public key required');
60
- return false;
64
+ return null;
61
65
  }
62
66
 
63
67
  try {
@@ -76,7 +80,7 @@ export function usePushNotifications(options?: PushNotificationOptions) {
76
80
 
77
81
  if (permission !== 'granted') {
78
82
  pwaLogger.warn('[usePushNotifications] Permission not granted:', permission);
79
- return false;
83
+ return null;
80
84
  }
81
85
 
82
86
  // Subscribe to push
@@ -94,7 +98,7 @@ export function usePushNotifications(options?: PushNotificationOptions) {
94
98
  } else {
95
99
  pwaLogger.error('[usePushNotifications] Failed to convert VAPID key:', e);
96
100
  }
97
- return false;
101
+ return null;
98
102
  }
99
103
 
100
104
  // Diagnostic logging (only in debug mode)
@@ -122,7 +126,7 @@ export function usePushNotifications(options?: PushNotificationOptions) {
122
126
  // Attempt subscribe
123
127
  const subscription = await registration.pushManager.subscribe(subscribeOptions);
124
128
 
125
- // Send subscription to server
129
+ // Send subscription to server (legacy endpoint support)
126
130
  if (options.subscribeEndpoint) {
127
131
  await fetch(options.subscribeEndpoint, {
128
132
  method: 'POST',
@@ -138,7 +142,7 @@ export function usePushNotifications(options?: PushNotificationOptions) {
138
142
  }));
139
143
 
140
144
  pwaLogger.success('[usePushNotifications] Successfully subscribed to push notifications');
141
- return true;
145
+ return subscription;
142
146
  } catch (error: any) {
143
147
  pwaLogger.error('[usePushNotifications] Subscribe failed:', error);
144
148
 
@@ -172,7 +176,7 @@ export function usePushNotifications(options?: PushNotificationOptions) {
172
176
  pwaLogger.error('Unknown error:', error.name, error.message);
173
177
  }
174
178
 
175
- return false;
179
+ return null;
176
180
  }
177
181
  };
178
182
 
@@ -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,9 @@
1
+ /**
2
+ * Push Notifications Types - Central Export
3
+ */
4
+
5
+ // Configuration
6
+ export type { PushNotificationsConfig } from './config';
7
+
8
+ // Push Notifications
9
+ export type { PushNotificationState, PushNotificationOptions } from './push';