@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,226 @@
1
+ /**
2
+ * VAPID Key Utilities
3
+ *
4
+ * Provides validation and conversion utilities for VAPID public keys
5
+ * used in push notification subscriptions.
6
+ *
7
+ * VAPID keys must be:
8
+ * - Base64url encoded
9
+ * - 65 bytes after decoding (P-256 uncompressed public key)
10
+ * - Start with 0x04 (uncompressed point indicator)
11
+ */
12
+
13
+ /**
14
+ * Custom error class for VAPID key validation failures
15
+ */
16
+ export class VapidKeyError extends Error {
17
+ constructor(
18
+ message: string,
19
+ public readonly code: VapidKeyErrorCode
20
+ ) {
21
+ super(message);
22
+ this.name = 'VapidKeyError';
23
+ }
24
+ }
25
+
26
+ /**
27
+ * Error codes for VAPID key validation
28
+ */
29
+ export type VapidKeyErrorCode =
30
+ | 'VAPID_EMPTY'
31
+ | 'VAPID_INVALID_TYPE'
32
+ | 'VAPID_INVALID_BASE64'
33
+ | 'VAPID_INVALID_LENGTH'
34
+ | 'VAPID_INVALID_FORMAT';
35
+
36
+ /**
37
+ * Convert base64url VAPID public key to Uint8Array
38
+ *
39
+ * Validates and converts a VAPID public key from base64url format
40
+ * to Uint8Array for use with PushManager.subscribe().
41
+ *
42
+ * @param base64String - VAPID public key in base64url format
43
+ * @returns Uint8Array ready for pushManager.subscribe()
44
+ * @throws VapidKeyError if key is invalid
45
+ *
46
+ * @example
47
+ * ```typescript
48
+ * try {
49
+ * const key = urlBase64ToUint8Array(process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY);
50
+ * await registration.pushManager.subscribe({
51
+ * userVisibleOnly: true,
52
+ * applicationServerKey: key,
53
+ * });
54
+ * } catch (e) {
55
+ * if (e instanceof VapidKeyError) {
56
+ * console.error('Invalid VAPID key:', e.message, e.code);
57
+ * }
58
+ * }
59
+ * ```
60
+ */
61
+ export function urlBase64ToUint8Array(base64String: string): Uint8Array {
62
+ // 1. Validate input
63
+ if (!base64String) {
64
+ throw new VapidKeyError('VAPID public key is required', 'VAPID_EMPTY');
65
+ }
66
+
67
+ if (typeof base64String !== 'string') {
68
+ throw new VapidKeyError('VAPID public key must be a string', 'VAPID_INVALID_TYPE');
69
+ }
70
+
71
+ // 2. Convert base64url to base64
72
+ // base64url uses '-' and '_' instead of '+' and '/'
73
+ const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
74
+ const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
75
+
76
+ // 3. Decode base64
77
+ let rawData: string;
78
+ try {
79
+ rawData = window.atob(base64);
80
+ } catch (e) {
81
+ throw new VapidKeyError(
82
+ `Invalid base64url format: ${e instanceof Error ? e.message : String(e)}`,
83
+ 'VAPID_INVALID_BASE64'
84
+ );
85
+ }
86
+
87
+ // 4. Convert to Uint8Array
88
+ const outputArray = new Uint8Array(rawData.length);
89
+ for (let i = 0; i < rawData.length; i++) {
90
+ outputArray[i] = rawData.charCodeAt(i);
91
+ }
92
+
93
+ // 5. Validate length (P-256 uncompressed = 65 bytes)
94
+ // Format: 0x04 (1 byte) + X coordinate (32 bytes) + Y coordinate (32 bytes)
95
+ if (outputArray.length !== 65) {
96
+ throw new VapidKeyError(
97
+ `Invalid key length: expected 65 bytes (P-256 uncompressed), got ${outputArray.length} bytes`,
98
+ 'VAPID_INVALID_LENGTH'
99
+ );
100
+ }
101
+
102
+ // 6. Validate format (must start with 0x04 for uncompressed P-256 point)
103
+ if (outputArray[0] !== 0x04) {
104
+ throw new VapidKeyError(
105
+ `Invalid key format: must start with 0x04 (uncompressed P-256 point), got 0x${outputArray[0]
106
+ .toString(16)
107
+ .padStart(2, '0')}`,
108
+ 'VAPID_INVALID_FORMAT'
109
+ );
110
+ }
111
+
112
+ return outputArray;
113
+ }
114
+
115
+ /**
116
+ * Validate VAPID key without conversion
117
+ *
118
+ * Checks if a VAPID key is valid without converting it.
119
+ * Useful for validation before attempting subscription.
120
+ *
121
+ * @param base64String - VAPID public key to validate
122
+ * @returns true if key is valid
123
+ *
124
+ * @example
125
+ * ```typescript
126
+ * if (isValidVapidKey(vapidKey)) {
127
+ * // Proceed with subscription
128
+ * } else {
129
+ * console.error('Invalid VAPID key configuration');
130
+ * }
131
+ * ```
132
+ */
133
+ export function isValidVapidKey(base64String: string): boolean {
134
+ try {
135
+ urlBase64ToUint8Array(base64String);
136
+ return true;
137
+ } catch {
138
+ return false;
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Get VAPID key information for debugging
144
+ *
145
+ * Returns detailed information about a VAPID key for troubleshooting.
146
+ *
147
+ * @param base64String - VAPID public key to inspect
148
+ * @returns Key information object
149
+ *
150
+ * @example
151
+ * ```typescript
152
+ * const info = getVapidKeyInfo(process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY);
153
+ * console.log('VAPID Key Info:', info);
154
+ * // {
155
+ * // valid: true,
156
+ * // length: 65,
157
+ * // firstByte: '0x04',
158
+ * // format: 'P-256 uncompressed'
159
+ * // }
160
+ * ```
161
+ */
162
+ export function getVapidKeyInfo(base64String: string): {
163
+ valid: boolean;
164
+ length?: number;
165
+ firstByte?: string;
166
+ format?: string;
167
+ error?: string;
168
+ errorCode?: VapidKeyErrorCode;
169
+ } {
170
+ try {
171
+ const key = urlBase64ToUint8Array(base64String);
172
+ return {
173
+ valid: true,
174
+ length: key.length,
175
+ firstByte: `0x${key[0].toString(16).padStart(2, '0')}`,
176
+ format: key[0] === 0x04 ? 'P-256 uncompressed' : 'Unknown',
177
+ };
178
+ } catch (e) {
179
+ if (e instanceof VapidKeyError) {
180
+ return {
181
+ valid: false,
182
+ error: e.message,
183
+ errorCode: e.code,
184
+ };
185
+ }
186
+ return {
187
+ valid: false,
188
+ error: String(e),
189
+ };
190
+ }
191
+ }
192
+
193
+ /**
194
+ * Safe VAPID key conversion with error handling
195
+ *
196
+ * Attempts to convert a VAPID key with detailed error logging.
197
+ * Returns null on failure instead of throwing.
198
+ *
199
+ * @param base64String - VAPID public key
200
+ * @param onError - Optional error callback
201
+ * @returns Uint8Array or null if conversion fails
202
+ *
203
+ * @example
204
+ * ```typescript
205
+ * const key = safeUrlBase64ToUint8Array(vapidKey, (error) => {
206
+ * console.error('VAPID conversion failed:', error.message, error.code);
207
+ * });
208
+ *
209
+ * if (key) {
210
+ * // Use key for subscription
211
+ * }
212
+ * ```
213
+ */
214
+ export function safeUrlBase64ToUint8Array(
215
+ base64String: string,
216
+ onError?: (error: VapidKeyError) => void
217
+ ): Uint8Array | null {
218
+ try {
219
+ return urlBase64ToUint8Array(base64String);
220
+ } catch (e) {
221
+ if (e instanceof VapidKeyError) {
222
+ onError?.(e);
223
+ }
224
+ return null;
225
+ }
226
+ }
@@ -43,3 +43,33 @@ export type {
43
43
  UseMcpChatReturn,
44
44
  AskAIButtonProps,
45
45
  } from './McpChat';
46
+
47
+ // PWA (Progressive Web App)
48
+ export {
49
+ PwaProvider,
50
+ useInstall,
51
+ PushProvider,
52
+ usePush,
53
+ A2HSHint,
54
+ PushPrompt,
55
+ useIsPWA,
56
+ usePushNotifications,
57
+ DEFAULT_VAPID_PUBLIC_KEY,
58
+ clearAllPWAData,
59
+ } from './PWA';
60
+ export type {
61
+ PlatformInfo,
62
+ InstallPromptState,
63
+ BeforeInstallPromptEvent,
64
+ InstallOutcome,
65
+ IOSGuideState,
66
+ PushNotificationState,
67
+ PushNotificationOptions,
68
+ InstallContextType,
69
+ InstallManagerProps,
70
+ AndroidInstallButtonProps,
71
+ IOSGuideModalProps,
72
+ InstallStep,
73
+ PushMessage,
74
+ PushContextValue,
75
+ } from './PWA';