@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.
- package/LICENSE +21 -0
- package/README.md +77 -0
- package/package.json +86 -0
- package/src/auth/README.md +962 -0
- package/src/auth/context/AuthContext.tsx +458 -0
- package/src/auth/context/index.ts +2 -0
- package/src/auth/context/types.ts +63 -0
- package/src/auth/hooks/index.ts +6 -0
- package/src/auth/hooks/useAuthForm.ts +329 -0
- package/src/auth/hooks/useAuthGuard.ts +23 -0
- package/src/auth/hooks/useAuthRedirect.ts +51 -0
- package/src/auth/hooks/useAutoAuth.ts +42 -0
- package/src/auth/hooks/useLocalStorage.ts +211 -0
- package/src/auth/hooks/useSessionStorage.ts +186 -0
- package/src/auth/index.ts +10 -0
- package/src/auth/middlewares/index.ts +1 -0
- package/src/auth/middlewares/proxy.ts +24 -0
- package/src/auth/server.ts +6 -0
- package/src/auth/utils/errors.ts +34 -0
- package/src/auth/utils/index.ts +2 -0
- package/src/auth/utils/validation.ts +14 -0
- package/src/index.ts +15 -0
- package/src/layouts/AppLayout/AppLayout.tsx +123 -0
- package/src/layouts/AppLayout/README.md +204 -0
- package/src/layouts/AppLayout/SUMMARY.md +240 -0
- package/src/layouts/AppLayout/USAGE.md +312 -0
- package/src/layouts/AppLayout/components/PageProgress.tsx +104 -0
- package/src/layouts/AppLayout/components/Seo.tsx +87 -0
- package/src/layouts/AppLayout/components/index.ts +6 -0
- package/src/layouts/AppLayout/context/AppContext.tsx +146 -0
- package/src/layouts/AppLayout/context/index.ts +5 -0
- package/src/layouts/AppLayout/hooks/index.ts +6 -0
- package/src/layouts/AppLayout/hooks/useLayoutMode.ts +26 -0
- package/src/layouts/AppLayout/hooks/useNavigation.ts +49 -0
- package/src/layouts/AppLayout/index.ts +31 -0
- package/src/layouts/AppLayout/layouts/AuthLayout/AuthContext.tsx +51 -0
- package/src/layouts/AppLayout/layouts/AuthLayout/AuthHelp.tsx +111 -0
- package/src/layouts/AppLayout/layouts/AuthLayout/AuthLayout.tsx +40 -0
- package/src/layouts/AppLayout/layouts/AuthLayout/IdentifierForm.tsx +330 -0
- package/src/layouts/AppLayout/layouts/AuthLayout/OTPForm.tsx +158 -0
- package/src/layouts/AppLayout/layouts/AuthLayout/index.ts +13 -0
- package/src/layouts/AppLayout/layouts/AuthLayout/types.ts +61 -0
- package/src/layouts/AppLayout/layouts/PrivateLayout/PrivateLayout.tsx +92 -0
- package/src/layouts/AppLayout/layouts/PrivateLayout/components/DashboardContent.tsx +60 -0
- package/src/layouts/AppLayout/layouts/PrivateLayout/components/DashboardHeader.tsx +170 -0
- package/src/layouts/AppLayout/layouts/PrivateLayout/components/DashboardSidebar.tsx +164 -0
- package/src/layouts/AppLayout/layouts/PrivateLayout/components/index.ts +7 -0
- package/src/layouts/AppLayout/layouts/PrivateLayout/index.ts +5 -0
- package/src/layouts/AppLayout/layouts/PublicLayout/PublicLayout.tsx +44 -0
- package/src/layouts/AppLayout/layouts/PublicLayout/components/DesktopUserMenu.tsx +136 -0
- package/src/layouts/AppLayout/layouts/PublicLayout/components/Footer.tsx +262 -0
- package/src/layouts/AppLayout/layouts/PublicLayout/components/MobileMenu.tsx +289 -0
- package/src/layouts/AppLayout/layouts/PublicLayout/components/Navigation.tsx +159 -0
- package/src/layouts/AppLayout/layouts/PublicLayout/index.ts +5 -0
- package/src/layouts/AppLayout/layouts/index.ts +7 -0
- package/src/layouts/AppLayout/providers/CoreProviders.tsx +47 -0
- package/src/layouts/AppLayout/providers/index.ts +5 -0
- package/src/layouts/AppLayout/types/config.ts +40 -0
- package/src/layouts/AppLayout/types/index.ts +10 -0
- package/src/layouts/AppLayout/types/layout.ts +47 -0
- package/src/layouts/AppLayout/types/navigation.ts +41 -0
- package/src/layouts/AppLayout/types/routes.ts +45 -0
- package/src/layouts/AppLayout/utils/index.ts +5 -0
- package/src/layouts/AppLayout/utils/routeDetection.ts +31 -0
- package/src/layouts/PaymentsLayout/PaymentsLayout.tsx +125 -0
- package/src/layouts/PaymentsLayout/README.md +133 -0
- package/src/layouts/PaymentsLayout/components/CreateApiKeyDialog.tsx +172 -0
- package/src/layouts/PaymentsLayout/components/CreatePaymentDialog.tsx +203 -0
- package/src/layouts/PaymentsLayout/components/DeleteApiKeyDialog.tsx +100 -0
- package/src/layouts/PaymentsLayout/components/index.ts +4 -0
- package/src/layouts/PaymentsLayout/events.ts +106 -0
- package/src/layouts/PaymentsLayout/index.ts +20 -0
- package/src/layouts/PaymentsLayout/types.ts +19 -0
- package/src/layouts/PaymentsLayout/views/apikeys/components/ApiKeyMetrics.tsx +109 -0
- package/src/layouts/PaymentsLayout/views/apikeys/components/ApiKeysList.tsx +194 -0
- package/src/layouts/PaymentsLayout/views/apikeys/components/index.ts +3 -0
- package/src/layouts/PaymentsLayout/views/apikeys/index.tsx +19 -0
- package/src/layouts/PaymentsLayout/views/overview/components/BalanceCard.tsx +99 -0
- package/src/layouts/PaymentsLayout/views/overview/components/MetricsCards.tsx +103 -0
- package/src/layouts/PaymentsLayout/views/overview/components/RecentPayments.tsx +138 -0
- package/src/layouts/PaymentsLayout/views/overview/components/index.ts +4 -0
- package/src/layouts/PaymentsLayout/views/overview/index.tsx +23 -0
- package/src/layouts/PaymentsLayout/views/payments/components/PaymentsList.tsx +282 -0
- package/src/layouts/PaymentsLayout/views/payments/components/index.ts +2 -0
- package/src/layouts/PaymentsLayout/views/payments/index.tsx +18 -0
- package/src/layouts/PaymentsLayout/views/tariffs/index.tsx +29 -0
- package/src/layouts/PaymentsLayout/views/transactions/index.tsx +29 -0
- package/src/layouts/ProfileLayout/ProfileLayout.tsx +110 -0
- package/src/layouts/ProfileLayout/components/AvatarSection.tsx +146 -0
- package/src/layouts/ProfileLayout/components/ProfileForm.tsx +208 -0
- package/src/layouts/ProfileLayout/components/index.ts +3 -0
- package/src/layouts/ProfileLayout/index.ts +3 -0
- package/src/layouts/SupportLayout/README.md +91 -0
- package/src/layouts/SupportLayout/SupportLayout.tsx +178 -0
- package/src/layouts/SupportLayout/components/CreateTicketDialog.tsx +154 -0
- package/src/layouts/SupportLayout/components/MessageInput.tsx +92 -0
- package/src/layouts/SupportLayout/components/MessageList.tsx +312 -0
- package/src/layouts/SupportLayout/components/TicketCard.tsx +96 -0
- package/src/layouts/SupportLayout/components/TicketList.tsx +152 -0
- package/src/layouts/SupportLayout/components/index.ts +6 -0
- package/src/layouts/SupportLayout/context/SupportLayoutContext.tsx +260 -0
- package/src/layouts/SupportLayout/context/index.ts +2 -0
- package/src/layouts/SupportLayout/events.ts +31 -0
- package/src/layouts/SupportLayout/hooks/index.ts +2 -0
- package/src/layouts/SupportLayout/hooks/useInfiniteMessages.ts +118 -0
- package/src/layouts/SupportLayout/hooks/useInfiniteTickets.ts +91 -0
- package/src/layouts/SupportLayout/index.ts +6 -0
- package/src/layouts/SupportLayout/types.ts +23 -0
- package/src/layouts/index.ts +9 -0
- package/src/snippets/AuthDialog/AuthDialog.tsx +88 -0
- package/src/snippets/AuthDialog/events.ts +21 -0
- package/src/snippets/AuthDialog/index.ts +3 -0
- package/src/snippets/AuthDialog/useAuthDialog.ts +27 -0
- package/src/snippets/Breadcrumbs.tsx +80 -0
- package/src/snippets/Chat/ChatUIContext.tsx +110 -0
- package/src/snippets/Chat/ChatWidget.tsx +476 -0
- package/src/snippets/Chat/README.md +122 -0
- package/src/snippets/Chat/components/MessageInput.tsx +124 -0
- package/src/snippets/Chat/components/MessageList.tsx +168 -0
- package/src/snippets/Chat/components/SessionList.tsx +192 -0
- package/src/snippets/Chat/components/index.ts +9 -0
- package/src/snippets/Chat/hooks/index.ts +6 -0
- package/src/snippets/Chat/hooks/useInfiniteSessions.ts +83 -0
- package/src/snippets/Chat/index.tsx +44 -0
- package/src/snippets/Chat/types.ts +79 -0
- package/src/snippets/VideoPlayer/README.md +203 -0
- package/src/snippets/VideoPlayer/VideoControls.tsx +133 -0
- package/src/snippets/VideoPlayer/VideoPlayer.tsx +114 -0
- package/src/snippets/VideoPlayer/index.ts +8 -0
- package/src/snippets/VideoPlayer/types.ts +61 -0
- package/src/snippets/index.ts +10 -0
- package/src/styles/dashboard.css +41 -0
- package/src/styles/index.css +20 -0
- package/src/styles/sources.css +6 -0
- package/src/types/index.ts +1 -0
- package/src/types/pageConfig.ts +103 -0
- package/src/utils/index.ts +6 -0
- 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
|
+
}
|