@djangocfg/layouts 1.0.1

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 (138) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +77 -0
  3. package/package.json +86 -0
  4. package/src/auth/README.md +962 -0
  5. package/src/auth/context/AuthContext.tsx +458 -0
  6. package/src/auth/context/index.ts +2 -0
  7. package/src/auth/context/types.ts +63 -0
  8. package/src/auth/hooks/index.ts +6 -0
  9. package/src/auth/hooks/useAuthForm.ts +329 -0
  10. package/src/auth/hooks/useAuthGuard.ts +23 -0
  11. package/src/auth/hooks/useAuthRedirect.ts +51 -0
  12. package/src/auth/hooks/useAutoAuth.ts +42 -0
  13. package/src/auth/hooks/useLocalStorage.ts +211 -0
  14. package/src/auth/hooks/useSessionStorage.ts +186 -0
  15. package/src/auth/index.ts +10 -0
  16. package/src/auth/middlewares/index.ts +1 -0
  17. package/src/auth/middlewares/proxy.ts +24 -0
  18. package/src/auth/server.ts +6 -0
  19. package/src/auth/utils/errors.ts +34 -0
  20. package/src/auth/utils/index.ts +2 -0
  21. package/src/auth/utils/validation.ts +14 -0
  22. package/src/index.ts +15 -0
  23. package/src/layouts/AppLayout/AppLayout.tsx +123 -0
  24. package/src/layouts/AppLayout/README.md +204 -0
  25. package/src/layouts/AppLayout/SUMMARY.md +240 -0
  26. package/src/layouts/AppLayout/USAGE.md +312 -0
  27. package/src/layouts/AppLayout/components/PageProgress.tsx +104 -0
  28. package/src/layouts/AppLayout/components/Seo.tsx +87 -0
  29. package/src/layouts/AppLayout/components/index.ts +6 -0
  30. package/src/layouts/AppLayout/context/AppContext.tsx +146 -0
  31. package/src/layouts/AppLayout/context/index.ts +5 -0
  32. package/src/layouts/AppLayout/hooks/index.ts +6 -0
  33. package/src/layouts/AppLayout/hooks/useLayoutMode.ts +26 -0
  34. package/src/layouts/AppLayout/hooks/useNavigation.ts +49 -0
  35. package/src/layouts/AppLayout/index.ts +31 -0
  36. package/src/layouts/AppLayout/layouts/AuthLayout/AuthContext.tsx +51 -0
  37. package/src/layouts/AppLayout/layouts/AuthLayout/AuthHelp.tsx +111 -0
  38. package/src/layouts/AppLayout/layouts/AuthLayout/AuthLayout.tsx +40 -0
  39. package/src/layouts/AppLayout/layouts/AuthLayout/IdentifierForm.tsx +330 -0
  40. package/src/layouts/AppLayout/layouts/AuthLayout/OTPForm.tsx +158 -0
  41. package/src/layouts/AppLayout/layouts/AuthLayout/index.ts +13 -0
  42. package/src/layouts/AppLayout/layouts/AuthLayout/types.ts +61 -0
  43. package/src/layouts/AppLayout/layouts/PrivateLayout/PrivateLayout.tsx +92 -0
  44. package/src/layouts/AppLayout/layouts/PrivateLayout/components/DashboardContent.tsx +60 -0
  45. package/src/layouts/AppLayout/layouts/PrivateLayout/components/DashboardHeader.tsx +170 -0
  46. package/src/layouts/AppLayout/layouts/PrivateLayout/components/DashboardSidebar.tsx +164 -0
  47. package/src/layouts/AppLayout/layouts/PrivateLayout/components/index.ts +7 -0
  48. package/src/layouts/AppLayout/layouts/PrivateLayout/index.ts +5 -0
  49. package/src/layouts/AppLayout/layouts/PublicLayout/PublicLayout.tsx +44 -0
  50. package/src/layouts/AppLayout/layouts/PublicLayout/components/DesktopUserMenu.tsx +136 -0
  51. package/src/layouts/AppLayout/layouts/PublicLayout/components/Footer.tsx +262 -0
  52. package/src/layouts/AppLayout/layouts/PublicLayout/components/MobileMenu.tsx +289 -0
  53. package/src/layouts/AppLayout/layouts/PublicLayout/components/Navigation.tsx +159 -0
  54. package/src/layouts/AppLayout/layouts/PublicLayout/index.ts +5 -0
  55. package/src/layouts/AppLayout/layouts/index.ts +7 -0
  56. package/src/layouts/AppLayout/providers/CoreProviders.tsx +47 -0
  57. package/src/layouts/AppLayout/providers/index.ts +5 -0
  58. package/src/layouts/AppLayout/types/config.ts +40 -0
  59. package/src/layouts/AppLayout/types/index.ts +10 -0
  60. package/src/layouts/AppLayout/types/layout.ts +47 -0
  61. package/src/layouts/AppLayout/types/navigation.ts +41 -0
  62. package/src/layouts/AppLayout/types/routes.ts +45 -0
  63. package/src/layouts/AppLayout/utils/index.ts +5 -0
  64. package/src/layouts/AppLayout/utils/routeDetection.ts +31 -0
  65. package/src/layouts/PaymentsLayout/PaymentsLayout.tsx +125 -0
  66. package/src/layouts/PaymentsLayout/README.md +133 -0
  67. package/src/layouts/PaymentsLayout/components/CreateApiKeyDialog.tsx +172 -0
  68. package/src/layouts/PaymentsLayout/components/CreatePaymentDialog.tsx +203 -0
  69. package/src/layouts/PaymentsLayout/components/DeleteApiKeyDialog.tsx +100 -0
  70. package/src/layouts/PaymentsLayout/components/index.ts +4 -0
  71. package/src/layouts/PaymentsLayout/events.ts +106 -0
  72. package/src/layouts/PaymentsLayout/index.ts +20 -0
  73. package/src/layouts/PaymentsLayout/types.ts +19 -0
  74. package/src/layouts/PaymentsLayout/views/apikeys/components/ApiKeyMetrics.tsx +109 -0
  75. package/src/layouts/PaymentsLayout/views/apikeys/components/ApiKeysList.tsx +194 -0
  76. package/src/layouts/PaymentsLayout/views/apikeys/components/index.ts +3 -0
  77. package/src/layouts/PaymentsLayout/views/apikeys/index.tsx +19 -0
  78. package/src/layouts/PaymentsLayout/views/overview/components/BalanceCard.tsx +99 -0
  79. package/src/layouts/PaymentsLayout/views/overview/components/MetricsCards.tsx +103 -0
  80. package/src/layouts/PaymentsLayout/views/overview/components/RecentPayments.tsx +138 -0
  81. package/src/layouts/PaymentsLayout/views/overview/components/index.ts +4 -0
  82. package/src/layouts/PaymentsLayout/views/overview/index.tsx +23 -0
  83. package/src/layouts/PaymentsLayout/views/payments/components/PaymentsList.tsx +282 -0
  84. package/src/layouts/PaymentsLayout/views/payments/components/index.ts +2 -0
  85. package/src/layouts/PaymentsLayout/views/payments/index.tsx +18 -0
  86. package/src/layouts/PaymentsLayout/views/tariffs/index.tsx +29 -0
  87. package/src/layouts/PaymentsLayout/views/transactions/index.tsx +29 -0
  88. package/src/layouts/ProfileLayout/ProfileLayout.tsx +110 -0
  89. package/src/layouts/ProfileLayout/components/AvatarSection.tsx +146 -0
  90. package/src/layouts/ProfileLayout/components/ProfileForm.tsx +208 -0
  91. package/src/layouts/ProfileLayout/components/index.ts +3 -0
  92. package/src/layouts/ProfileLayout/index.ts +3 -0
  93. package/src/layouts/SupportLayout/README.md +91 -0
  94. package/src/layouts/SupportLayout/SupportLayout.tsx +178 -0
  95. package/src/layouts/SupportLayout/components/CreateTicketDialog.tsx +154 -0
  96. package/src/layouts/SupportLayout/components/MessageInput.tsx +92 -0
  97. package/src/layouts/SupportLayout/components/MessageList.tsx +312 -0
  98. package/src/layouts/SupportLayout/components/TicketCard.tsx +96 -0
  99. package/src/layouts/SupportLayout/components/TicketList.tsx +152 -0
  100. package/src/layouts/SupportLayout/components/index.ts +6 -0
  101. package/src/layouts/SupportLayout/context/SupportLayoutContext.tsx +260 -0
  102. package/src/layouts/SupportLayout/context/index.ts +2 -0
  103. package/src/layouts/SupportLayout/events.ts +31 -0
  104. package/src/layouts/SupportLayout/hooks/index.ts +2 -0
  105. package/src/layouts/SupportLayout/hooks/useInfiniteMessages.ts +118 -0
  106. package/src/layouts/SupportLayout/hooks/useInfiniteTickets.ts +91 -0
  107. package/src/layouts/SupportLayout/index.ts +6 -0
  108. package/src/layouts/SupportLayout/types.ts +23 -0
  109. package/src/layouts/index.ts +9 -0
  110. package/src/snippets/AuthDialog/AuthDialog.tsx +88 -0
  111. package/src/snippets/AuthDialog/events.ts +21 -0
  112. package/src/snippets/AuthDialog/index.ts +3 -0
  113. package/src/snippets/AuthDialog/useAuthDialog.ts +27 -0
  114. package/src/snippets/Breadcrumbs.tsx +80 -0
  115. package/src/snippets/Chat/ChatUIContext.tsx +110 -0
  116. package/src/snippets/Chat/ChatWidget.tsx +476 -0
  117. package/src/snippets/Chat/README.md +122 -0
  118. package/src/snippets/Chat/components/MessageInput.tsx +124 -0
  119. package/src/snippets/Chat/components/MessageList.tsx +168 -0
  120. package/src/snippets/Chat/components/SessionList.tsx +192 -0
  121. package/src/snippets/Chat/components/index.ts +9 -0
  122. package/src/snippets/Chat/hooks/index.ts +6 -0
  123. package/src/snippets/Chat/hooks/useInfiniteSessions.ts +83 -0
  124. package/src/snippets/Chat/index.tsx +44 -0
  125. package/src/snippets/Chat/types.ts +79 -0
  126. package/src/snippets/VideoPlayer/README.md +203 -0
  127. package/src/snippets/VideoPlayer/VideoControls.tsx +133 -0
  128. package/src/snippets/VideoPlayer/VideoPlayer.tsx +114 -0
  129. package/src/snippets/VideoPlayer/index.ts +8 -0
  130. package/src/snippets/VideoPlayer/types.ts +61 -0
  131. package/src/snippets/index.ts +10 -0
  132. package/src/styles/dashboard.css +41 -0
  133. package/src/styles/index.css +20 -0
  134. package/src/styles/sources.css +6 -0
  135. package/src/types/index.ts +1 -0
  136. package/src/types/pageConfig.ts +103 -0
  137. package/src/utils/index.ts +6 -0
  138. package/src/utils/logger.ts +57 -0
@@ -0,0 +1,329 @@
1
+ import { useCallback, useEffect, useState } from 'react';
2
+
3
+ import { useAuth } from '../context';
4
+ import { useAutoAuth } from './useAutoAuth';
5
+ import { useLocalStorage } from './useLocalStorage';
6
+
7
+ export interface AuthFormState {
8
+ identifier: string; // Email or phone number
9
+ channel: 'email' | 'phone';
10
+ otp: string;
11
+ isLoading: boolean;
12
+ acceptedTerms: boolean;
13
+ step: 'identifier' | 'otp';
14
+ error: string;
15
+ }
16
+
17
+ export interface AuthFormHandlers {
18
+ setIdentifier: (identifier: string) => void;
19
+ setChannel: (channel: 'email' | 'phone') => void;
20
+ setOtp: (otp: string) => void;
21
+ setAcceptedTerms: (accepted: boolean) => void;
22
+ setError: (error: string) => void;
23
+ clearError: () => void;
24
+ handleIdentifierSubmit: (e: React.FormEvent) => Promise<void>;
25
+ handleOTPSubmit: (e: React.FormEvent) => Promise<void>;
26
+ handleResendOTP: () => Promise<void>;
27
+ handleBackToIdentifier: () => void;
28
+ forceOTPStep: () => void;
29
+ // Utility methods
30
+ detectChannelFromIdentifier: (identifier: string) => 'email' | 'phone' | null;
31
+ validateIdentifier: (identifier: string, channel?: 'email' | 'phone') => boolean;
32
+ }
33
+
34
+ export interface UseAuthFormOptions {
35
+ onIdentifierSuccess?: (identifier: string, channel: 'email' | 'phone') => void;
36
+ onOTPSuccess?: () => void;
37
+ onError?: (message: string) => void;
38
+ sourceUrl: string;
39
+ }
40
+
41
+ export const useAuthForm = (options: UseAuthFormOptions): AuthFormState & AuthFormHandlers => {
42
+ const { onIdentifierSuccess, onOTPSuccess, onError, sourceUrl } = options;
43
+
44
+ // Form state
45
+ const [identifier, setIdentifier] = useState('');
46
+ const [channel, setChannel] = useState<'email' | 'phone'>('email');
47
+ const [otp, setOtp] = useState('');
48
+ const [isLoading, setIsLoading] = useState(false);
49
+ const [acceptedTerms, setAcceptedTerms] = useState(false);
50
+ const [step, setStep] = useState<'identifier' | 'otp'>('identifier');
51
+ const [error, setError] = useState('');
52
+
53
+
54
+
55
+ // Auth hooks
56
+ const { requestOTP, verifyOTP, getSavedEmail, saveEmail, getSavedPhone, savePhone } = useAuth();
57
+ const [savedTermsAccepted, setSavedTermsAccepted] = useLocalStorage('auth_terms_accepted', false);
58
+ const [savedEmail, setSavedEmail] = useLocalStorage('auth_email', '');
59
+ const [savedPhone, setSavedPhone] = useLocalStorage('auth_phone', '');
60
+
61
+ // Utility functions
62
+ const detectChannelFromIdentifier = useCallback((identifier: string): 'email' | 'phone' | null => {
63
+ if (!identifier) return null;
64
+
65
+ // Email detection
66
+ if (identifier.includes('@')) {
67
+ return 'email';
68
+ }
69
+
70
+ // Phone detection (starts with + and contains digits)
71
+ if (identifier.startsWith('+') && /^\+[1-9]\d{6,14}$/.test(identifier)) {
72
+ return 'phone';
73
+ }
74
+
75
+ return null;
76
+ }, []);
77
+
78
+ const validateIdentifier = useCallback((identifier: string, channelType?: 'email' | 'phone'): boolean => {
79
+ if (!identifier) return false;
80
+
81
+ const detectedChannel = channelType || detectChannelFromIdentifier(identifier);
82
+
83
+ if (detectedChannel === 'email') {
84
+ // Basic email validation
85
+ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(identifier);
86
+ } else if (detectedChannel === 'phone') {
87
+ // E.164 phone validation
88
+ return /^\+[1-9]\d{6,14}$/.test(identifier);
89
+ }
90
+
91
+ return false;
92
+ }, [detectChannelFromIdentifier]);
93
+
94
+ // Load saved data on mount
95
+ useEffect(() => {
96
+ const authSavedEmail = getSavedEmail();
97
+ const authSavedPhone = getSavedPhone();
98
+
99
+ // Prioritize phone over email if both exist
100
+ if (authSavedPhone) {
101
+ setIdentifier(authSavedPhone);
102
+ setChannel('phone');
103
+ } else if (authSavedEmail) {
104
+ setIdentifier(authSavedEmail);
105
+ setChannel('email');
106
+ }
107
+
108
+ if (savedTermsAccepted) {
109
+ setAcceptedTerms(savedTermsAccepted);
110
+ }
111
+ }, [getSavedEmail, getSavedPhone, savedTermsAccepted]);
112
+
113
+ // Auto-detect channel when identifier changes
114
+ useEffect(() => {
115
+ if (identifier) {
116
+ const detectedChannel = detectChannelFromIdentifier(identifier);
117
+ if (detectedChannel && detectedChannel !== channel) {
118
+ setChannel(detectedChannel);
119
+ }
120
+ }
121
+ }, [identifier, channel, detectChannelFromIdentifier]);
122
+
123
+
124
+
125
+ const clearError = useCallback(() => setError(''), []);
126
+
127
+ const handleIdentifierSubmit = useCallback(async (e: React.FormEvent) => {
128
+ e.preventDefault();
129
+
130
+ if (!identifier) {
131
+ const message = channel === 'phone' ? 'Please enter your phone number' : 'Please enter your email address';
132
+ setError(message);
133
+ onError?.(message);
134
+ return;
135
+ }
136
+
137
+ // Validate identifier format
138
+ if (!validateIdentifier(identifier, channel)) {
139
+ const message = channel === 'phone'
140
+ ? 'Please enter a valid phone number (e.g., +1234567890)'
141
+ : 'Please enter a valid email address';
142
+ setError(message);
143
+ onError?.(message);
144
+ return;
145
+ }
146
+
147
+ if (!acceptedTerms) {
148
+ const message = 'Please accept the Terms of Service and Privacy Policy';
149
+ setError(message);
150
+ onError?.(message);
151
+ return;
152
+ }
153
+
154
+ setIsLoading(true);
155
+ clearError();
156
+
157
+ try {
158
+ const result = await requestOTP(identifier, channel, sourceUrl);
159
+
160
+ if (result.success) {
161
+ // Save identifier and terms acceptance on successful request, clear opposite channel
162
+ if (channel === 'email') {
163
+ saveEmail(identifier);
164
+ setSavedPhone(''); // Clear phone storage
165
+ } else if (channel === 'phone') {
166
+ savePhone(identifier);
167
+ setSavedEmail(''); // Clear email storage
168
+ }
169
+ setSavedTermsAccepted(true);
170
+ setStep('otp');
171
+ onIdentifierSuccess?.(identifier, channel);
172
+ } else {
173
+ setError(result.message);
174
+ onError?.(result.message);
175
+ }
176
+ } catch (error) {
177
+ const message = 'An unexpected error occurred';
178
+ setError(message);
179
+ onError?.(message);
180
+ } finally {
181
+ setIsLoading(false);
182
+ }
183
+ }, [identifier, channel, acceptedTerms, validateIdentifier, requestOTP, saveEmail, clearError, setSavedTermsAccepted, onIdentifierSuccess, onError, sourceUrl]);
184
+
185
+ const handleOTPSubmit = useCallback(async (e: React.FormEvent) => {
186
+ e.preventDefault();
187
+
188
+ if (!otp || otp.length < 6) {
189
+ const message = 'Please enter the 6-digit verification code';
190
+ setError(message);
191
+ onError?.(message);
192
+ return;
193
+ }
194
+
195
+ setIsLoading(true);
196
+ clearError();
197
+
198
+ try {
199
+ const result = await verifyOTP(identifier, otp, channel, sourceUrl);
200
+
201
+ if (result.success) {
202
+ // Save identifier on successful login, clear opposite channel
203
+ if (channel === 'email') {
204
+ setSavedEmail(identifier);
205
+ setSavedPhone(''); // Clear phone storage
206
+ } else if (channel === 'phone') {
207
+ setSavedPhone(identifier);
208
+ setSavedEmail(''); // Clear email storage
209
+ }
210
+ onOTPSuccess?.();
211
+ } else {
212
+ setError(result.message);
213
+ onError?.(result.message);
214
+ }
215
+ } catch (error) {
216
+ const message = 'An unexpected error occurred';
217
+ setError(message);
218
+ onError?.(message);
219
+ } finally {
220
+ setIsLoading(false);
221
+ }
222
+ }, [identifier, otp, channel, verifyOTP, clearError, setSavedEmail, onOTPSuccess, onError, sourceUrl]);
223
+
224
+ const handleResendOTP = useCallback(async () => {
225
+ setIsLoading(true);
226
+ clearError();
227
+
228
+ try {
229
+ const result = await requestOTP(identifier, channel, sourceUrl);
230
+
231
+ if (result.success) {
232
+ // Save identifier and clear OTP input, clear opposite channel
233
+ if (channel === 'email') {
234
+ saveEmail(identifier);
235
+ setSavedPhone(''); // Clear phone storage
236
+ } else if (channel === 'phone') {
237
+ savePhone(identifier);
238
+ setSavedEmail(''); // Clear email storage
239
+ }
240
+ setOtp('');
241
+ } else {
242
+ setError(result.message);
243
+ onError?.(result.message);
244
+ }
245
+ } catch (error) {
246
+ const message = 'Failed to resend verification code';
247
+ setError(message);
248
+ onError?.(message);
249
+ } finally {
250
+ setIsLoading(false);
251
+ }
252
+ }, [identifier, channel, requestOTP, saveEmail, clearError, setOtp, onError, sourceUrl]);
253
+
254
+ const handleBackToIdentifier = useCallback(() => {
255
+ setStep('identifier');
256
+ clearError();
257
+ }, [clearError]);
258
+
259
+ const forceOTPStep = useCallback(() => {
260
+ setStep('otp');
261
+ clearError();
262
+ }, [clearError]);
263
+
264
+ const handleAcceptedTermsChange = useCallback((checked: boolean) => {
265
+ setAcceptedTerms(checked);
266
+ setSavedTermsAccepted(checked);
267
+ }, [setSavedTermsAccepted]);
268
+
269
+ // Auto-detect OTP from URL query parameters
270
+ useAutoAuth({
271
+ onOTPDetected: (otp: string) => {
272
+ console.log('[useAuthForm] OTP detected, auto-submitting');
273
+
274
+ // Get saved identifier from auth context
275
+ const savedEmail = getSavedEmail();
276
+ const savedPhone = getSavedPhone();
277
+
278
+ // Prioritize phone over email if both exist
279
+ if (savedPhone) {
280
+ setIdentifier(savedPhone);
281
+ setChannel('phone');
282
+ } else if (savedEmail) {
283
+ setIdentifier(savedEmail);
284
+ setChannel('email');
285
+ }
286
+
287
+ // Set OTP and force OTP step
288
+ setOtp(otp);
289
+ setStep('otp');
290
+
291
+ // Auto-submit after a short delay to ensure state is updated
292
+ setTimeout(() => {
293
+ const fakeEvent = { preventDefault: () => {} } as React.FormEvent;
294
+ handleOTPSubmit(fakeEvent);
295
+ }, 200);
296
+ },
297
+ cleanupUrl: true,
298
+ });
299
+
300
+ return {
301
+ // Form state
302
+ identifier,
303
+ channel,
304
+ otp,
305
+ isLoading,
306
+ acceptedTerms,
307
+ step,
308
+ error,
309
+
310
+ // Form handlers
311
+ setIdentifier,
312
+ setChannel,
313
+ setOtp,
314
+ setAcceptedTerms: handleAcceptedTermsChange,
315
+ setError,
316
+ clearError,
317
+
318
+ // Auth handlers
319
+ handleIdentifierSubmit,
320
+ handleOTPSubmit,
321
+ handleResendOTP,
322
+ handleBackToIdentifier,
323
+ forceOTPStep,
324
+
325
+ // Utility methods
326
+ detectChannelFromIdentifier,
327
+ validateIdentifier,
328
+ };
329
+ };
@@ -0,0 +1,23 @@
1
+ import { useRouter } from 'next/router';
2
+ import { useEffect } from 'react';
3
+
4
+ import { useAuth } from '../context';
5
+
6
+ interface UseAuthGuardOptions {
7
+ redirectTo?: string;
8
+ requireAuth?: boolean;
9
+ }
10
+
11
+ export const useAuthGuard = (options: UseAuthGuardOptions = {}) => {
12
+ const { redirectTo = '/auth', requireAuth = true } = options;
13
+ const { isAuthenticated, isLoading } = useAuth();
14
+ const router = useRouter();
15
+
16
+ useEffect(() => {
17
+ if (!isLoading && requireAuth && !isAuthenticated) {
18
+ router.push(redirectTo);
19
+ }
20
+ }, [isAuthenticated, isLoading, router, redirectTo, requireAuth]);
21
+
22
+ return { isAuthenticated, isLoading };
23
+ };
@@ -0,0 +1,51 @@
1
+ import { useSessionStorage } from './useSessionStorage';
2
+
3
+ const AUTH_REDIRECT_KEY = 'auth_redirect_url';
4
+
5
+ export interface AuthRedirectOptions {
6
+ fallbackUrl?: string;
7
+ clearOnUse?: boolean;
8
+ }
9
+
10
+ export const useAuthRedirectManager = (options: AuthRedirectOptions = {}) => {
11
+ const { fallbackUrl = '/dashboard', clearOnUse = true } = options;
12
+ const [redirectUrl, setRedirectUrl, removeRedirectUrl] = useSessionStorage<string>(AUTH_REDIRECT_KEY, '');
13
+
14
+ const setRedirect = (url: string) => {
15
+ setRedirectUrl(url);
16
+ };
17
+
18
+ const getRedirect = () => {
19
+ return redirectUrl;
20
+ };
21
+
22
+ const clearRedirect = () => {
23
+ removeRedirectUrl();
24
+ };
25
+
26
+ const hasRedirect = () => {
27
+ return redirectUrl.length > 0;
28
+ };
29
+
30
+ const getFinalRedirectUrl = () => {
31
+ return redirectUrl || fallbackUrl;
32
+ };
33
+
34
+ const useAndClearRedirect = () => {
35
+ const finalUrl = getFinalRedirectUrl();
36
+ if (clearOnUse) {
37
+ clearRedirect();
38
+ }
39
+ return finalUrl;
40
+ };
41
+
42
+ return {
43
+ redirectUrl,
44
+ setRedirect,
45
+ getRedirect,
46
+ clearRedirect,
47
+ hasRedirect,
48
+ getFinalRedirectUrl,
49
+ useAndClearRedirect
50
+ };
51
+ };
@@ -0,0 +1,42 @@
1
+ import { useRouter } from 'next/router';
2
+ import { useEffect } from 'react';
3
+
4
+ export interface UseAutoAuthOptions {
5
+ onOTPDetected?: (otp: string) => void;
6
+ cleanupUrl?: boolean;
7
+ }
8
+
9
+ /**
10
+ * Hook for automatic authentication from URL query parameters
11
+ * Detects OTP from URL and triggers callback
12
+ */
13
+ export const useAutoAuth = (options: UseAutoAuthOptions = {}) => {
14
+ const { onOTPDetected, cleanupUrl = true } = options;
15
+ const router = useRouter();
16
+
17
+ useEffect(() => {
18
+ if (!router.isReady) return;
19
+
20
+ const queryOtp = router.query.otp as string;
21
+
22
+ // Handle OTP detection
23
+ if (queryOtp && typeof queryOtp === 'string' && queryOtp.length === 6) {
24
+ console.log('[useAutoAuth] OTP detected in URL:', queryOtp);
25
+ onOTPDetected?.(queryOtp);
26
+ }
27
+
28
+ // Clean up URL to remove sensitive params for security
29
+ if (cleanupUrl && queryOtp) {
30
+ const { otp: _, ...cleanQuery } = router.query;
31
+ router.replace({
32
+ pathname: router.pathname,
33
+ query: cleanQuery
34
+ }, undefined, { shallow: true });
35
+ }
36
+ }, [router.isReady, router.query, router.pathname, onOTPDetected, cleanupUrl]);
37
+
38
+ return {
39
+ isReady: router.isReady,
40
+ hasOTP: !!(router.query.otp),
41
+ };
42
+ };
@@ -0,0 +1,211 @@
1
+ import { useEffect, useState } from 'react';
2
+
3
+ /**
4
+ * Simple localStorage hook with better error handling
5
+ * @param key - Storage key
6
+ * @param initialValue - Default value if key doesn't exist
7
+ * @returns [value, setValue, removeValue] - Current value, setter function, and remove function
8
+ */
9
+ export function useLocalStorage<T>(key: string, initialValue: T) {
10
+ // Get initial value from localStorage or use provided initialValue
11
+ const [storedValue, setStoredValue] = useState<T>(() => {
12
+ if (typeof window === 'undefined') {
13
+ return initialValue;
14
+ }
15
+
16
+ try {
17
+ const item = window.localStorage.getItem(key);
18
+ if (item === null) {
19
+ return initialValue;
20
+ }
21
+
22
+ // Try to parse as JSON first, fallback to string
23
+ try {
24
+ return JSON.parse(item);
25
+ } catch {
26
+ // If JSON.parse fails, return as string
27
+ return item as T;
28
+ }
29
+ } catch (error) {
30
+ console.error(`Error reading localStorage key "${key}":`, error);
31
+ return initialValue;
32
+ }
33
+ });
34
+
35
+ // Check data size and limit
36
+ const checkDataSize = (data: any): boolean => {
37
+ try {
38
+ const jsonString = JSON.stringify(data);
39
+ const sizeInBytes = new Blob([jsonString]).size;
40
+ const sizeInKB = sizeInBytes / 1024;
41
+
42
+ // Limit to 1MB per item
43
+ if (sizeInKB > 1024) {
44
+ console.warn(`Data size (${sizeInKB.toFixed(2)}KB) exceeds 1MB limit for key "${key}"`);
45
+ return false;
46
+ }
47
+
48
+ return true;
49
+ } catch (error) {
50
+ console.error(`Error checking data size for key "${key}":`, error);
51
+ return false;
52
+ }
53
+ };
54
+
55
+ // Clear old data when localStorage is full
56
+ const clearOldData = () => {
57
+ try {
58
+ const keys = Object.keys(localStorage).filter(key => key && typeof key === 'string');
59
+ // Remove oldest items if we have more than 50 items
60
+ if (keys.length > 50) {
61
+ const itemsToRemove = Math.ceil(keys.length * 0.2);
62
+ for (let i = 0; i < itemsToRemove; i++) {
63
+ try {
64
+ const key = keys[i];
65
+ if (key) {
66
+ localStorage.removeItem(key);
67
+ localStorage.removeItem(`${key}_timestamp`);
68
+ }
69
+ } catch {
70
+ // Ignore errors when removing items
71
+ }
72
+ }
73
+ }
74
+ } catch (error) {
75
+ console.error('Error clearing old localStorage data:', error);
76
+ }
77
+ };
78
+
79
+ // Force clear all data if quota is exceeded
80
+ const forceClearAll = () => {
81
+ try {
82
+ const keys = Object.keys(localStorage);
83
+ for (const key of keys) {
84
+ try {
85
+ localStorage.removeItem(key);
86
+ } catch {
87
+ // Ignore errors when removing items
88
+ }
89
+ }
90
+ } catch (error) {
91
+ console.error('Error force clearing localStorage:', error);
92
+ }
93
+ };
94
+
95
+ // Update localStorage when value changes
96
+ const setValue = (value: T | ((val: T) => T)) => {
97
+ try {
98
+ const valueToStore = value instanceof Function ? value(storedValue) : value;
99
+
100
+ // Check data size before attempting to save
101
+ if (!checkDataSize(valueToStore)) {
102
+ console.warn(`Data size too large for key "${key}", removing key`);
103
+ // Remove the key if data is too large
104
+ try {
105
+ window.localStorage.removeItem(key);
106
+ window.localStorage.removeItem(`${key}_timestamp`);
107
+ } catch {
108
+ // Ignore errors when removing
109
+ }
110
+ // Still update the state
111
+ setStoredValue(valueToStore);
112
+ return;
113
+ }
114
+
115
+ setStoredValue(valueToStore);
116
+
117
+ if (typeof window !== 'undefined') {
118
+ // Try to set the value
119
+ try {
120
+ // For strings, store directly without JSON.stringify
121
+ if (typeof valueToStore === 'string') {
122
+ window.localStorage.setItem(key, valueToStore);
123
+ } else {
124
+ window.localStorage.setItem(key, JSON.stringify(valueToStore));
125
+ }
126
+ // Add timestamp for cleanup
127
+ window.localStorage.setItem(`${key}_timestamp`, Date.now().toString());
128
+ } catch (storageError: any) {
129
+ // If quota exceeded, clear old data and try again
130
+ if (storageError.name === 'QuotaExceededError' ||
131
+ storageError.code === 22 ||
132
+ storageError.message?.includes('quota')) {
133
+ console.warn('localStorage quota exceeded, clearing old data...');
134
+ clearOldData();
135
+
136
+ // Try again after clearing
137
+ try {
138
+ // For strings, store directly without JSON.stringify
139
+ if (typeof valueToStore === 'string') {
140
+ window.localStorage.setItem(key, valueToStore);
141
+ } else {
142
+ window.localStorage.setItem(key, JSON.stringify(valueToStore));
143
+ }
144
+ window.localStorage.setItem(`${key}_timestamp`, Date.now().toString());
145
+ } catch (retryError) {
146
+ console.error(`Failed to set localStorage key "${key}" after clearing old data:`, retryError);
147
+ // If still fails, force clear all and try one more time
148
+ try {
149
+ forceClearAll();
150
+ // For strings, store directly without JSON.stringify
151
+ if (typeof valueToStore === 'string') {
152
+ window.localStorage.setItem(key, valueToStore);
153
+ } else {
154
+ window.localStorage.setItem(key, JSON.stringify(valueToStore));
155
+ }
156
+ window.localStorage.setItem(`${key}_timestamp`, Date.now().toString());
157
+ } catch (finalError) {
158
+ console.error(`Failed to set localStorage key "${key}" after force clearing:`, finalError);
159
+ // If still fails, just update the state without localStorage
160
+ setStoredValue(valueToStore);
161
+ }
162
+ }
163
+ } else {
164
+ throw storageError;
165
+ }
166
+ }
167
+ }
168
+ } catch (error) {
169
+ console.error(`Error setting localStorage key "${key}":`, error);
170
+ // Still update the state even if localStorage fails
171
+ const valueToStore = value instanceof Function ? value(storedValue) : value;
172
+ setStoredValue(valueToStore);
173
+ }
174
+ };
175
+
176
+ // Remove value from localStorage
177
+ const removeValue = () => {
178
+ try {
179
+ setStoredValue(initialValue);
180
+ if (typeof window !== 'undefined') {
181
+ try {
182
+ window.localStorage.removeItem(key);
183
+ window.localStorage.removeItem(`${key}_timestamp`);
184
+ } catch (removeError: any) {
185
+ // If removal fails due to quota, try to clear some data first
186
+ if (removeError.name === 'QuotaExceededError' ||
187
+ removeError.code === 22 ||
188
+ removeError.message?.includes('quota')) {
189
+ console.warn('localStorage quota exceeded during removal, clearing old data...');
190
+ clearOldData();
191
+
192
+ try {
193
+ window.localStorage.removeItem(key);
194
+ window.localStorage.removeItem(`${key}_timestamp`);
195
+ } catch (retryError) {
196
+ console.error(`Failed to remove localStorage key "${key}" after clearing:`, retryError);
197
+ // If still fails, force clear all
198
+ forceClearAll();
199
+ }
200
+ } else {
201
+ throw removeError;
202
+ }
203
+ }
204
+ }
205
+ } catch (error) {
206
+ console.error(`Error removing localStorage key "${key}":`, error);
207
+ }
208
+ };
209
+
210
+ return [storedValue, setValue, removeValue] as const;
211
+ }