@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.
- package/package.json +5 -5
- package/src/layouts/AppLayout/BaseApp.tsx +31 -25
- package/src/layouts/shared/types.ts +36 -0
- package/src/snippets/McpChat/context/ChatContext.tsx +9 -0
- package/src/snippets/PWA/@docs/research.md +576 -0
- package/src/snippets/PWA/@refactoring/ARCHITECTURE_ANALYSIS.md +1179 -0
- package/src/snippets/PWA/@refactoring/EXECUTIVE_SUMMARY.md +271 -0
- package/src/snippets/PWA/@refactoring/README.md +204 -0
- package/src/snippets/PWA/@refactoring/REFACTORING_PROPOSALS.md +1109 -0
- package/src/snippets/PWA/@refactoring2/COMPARISON-WITH-NEXTJS.md +718 -0
- package/src/snippets/PWA/@refactoring2/P1-FIXES-COMPLETED.md +188 -0
- package/src/snippets/PWA/@refactoring2/POST-P0-ANALYSIS.md +362 -0
- package/src/snippets/PWA/@refactoring2/README.md +85 -0
- package/src/snippets/PWA/@refactoring2/RECOMMENDATIONS.md +1321 -0
- package/src/snippets/PWA/@refactoring2/REMAINING-ISSUES.md +557 -0
- package/src/snippets/PWA/README.md +387 -0
- package/src/snippets/PWA/components/A2HSHint.tsx +226 -0
- package/src/snippets/PWA/components/IOSGuide.tsx +29 -0
- package/src/snippets/PWA/components/IOSGuideDrawer.tsx +101 -0
- package/src/snippets/PWA/components/IOSGuideModal.tsx +101 -0
- package/src/snippets/PWA/components/PushPrompt.tsx +165 -0
- package/src/snippets/PWA/config.ts +20 -0
- package/src/snippets/PWA/context/DjangoPushContext.tsx +105 -0
- package/src/snippets/PWA/context/InstallContext.tsx +118 -0
- package/src/snippets/PWA/context/PushContext.tsx +156 -0
- package/src/snippets/PWA/hooks/useDjangoPush.ts +277 -0
- package/src/snippets/PWA/hooks/useInstallPrompt.ts +164 -0
- package/src/snippets/PWA/hooks/useIsPWA.ts +115 -0
- package/src/snippets/PWA/hooks/usePushNotifications.ts +205 -0
- package/src/snippets/PWA/index.ts +95 -0
- package/src/snippets/PWA/types/components.ts +101 -0
- package/src/snippets/PWA/types/index.ts +26 -0
- package/src/snippets/PWA/types/install.ts +38 -0
- package/src/snippets/PWA/types/platform.ts +29 -0
- package/src/snippets/PWA/types/push.ts +21 -0
- package/src/snippets/PWA/utils/localStorage.ts +203 -0
- package/src/snippets/PWA/utils/logger.ts +149 -0
- package/src/snippets/PWA/utils/platform.ts +151 -0
- package/src/snippets/PWA/utils/vapid.ts +226 -0
- 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
|
+
}
|
package/src/snippets/index.ts
CHANGED
|
@@ -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';
|