@djangocfg/layouts 2.1.10 → 2.1.15

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 (107) hide show
  1. package/README.md +53 -161
  2. package/package.json +6 -6
  3. package/src/components/RedirectPage/RedirectPage.tsx +1 -1
  4. package/src/index.ts +0 -6
  5. package/src/layouts/AppLayout/AppLayout.tsx +1 -1
  6. package/src/layouts/AppLayout/BaseApp.tsx +1 -1
  7. package/src/layouts/AuthLayout/AuthContext.tsx +1 -1
  8. package/src/layouts/AuthLayout/OAuthCallback.tsx +1 -1
  9. package/src/layouts/AuthLayout/OAuthProviders.tsx +1 -1
  10. package/src/layouts/PrivateLayout/PrivateLayout.tsx +1 -1
  11. package/src/layouts/PrivateLayout/components/PrivateHeader.tsx +1 -1
  12. package/src/layouts/ProfileLayout/ProfileLayout.tsx +2 -2
  13. package/src/layouts/ProfileLayout/components/AvatarSection.tsx +2 -2
  14. package/src/layouts/ProfileLayout/components/ProfileForm.tsx +2 -2
  15. package/src/layouts/PublicLayout/components/PublicMobileDrawer.tsx +1 -1
  16. package/src/layouts/PublicLayout/components/PublicNavigation.tsx +1 -1
  17. package/src/layouts/_components/UserMenu.tsx +1 -1
  18. package/src/layouts/index.ts +0 -2
  19. package/src/snippets/Analytics/useAnalytics.ts +1 -1
  20. package/src/snippets/index.ts +0 -3
  21. package/src/auth/README.md +0 -962
  22. package/src/auth/context/AccountsContext.tsx +0 -240
  23. package/src/auth/context/AuthContext.tsx +0 -604
  24. package/src/auth/context/index.ts +0 -4
  25. package/src/auth/context/types.ts +0 -68
  26. package/src/auth/hooks/index.ts +0 -17
  27. package/src/auth/hooks/useAuthForm.ts +0 -332
  28. package/src/auth/hooks/useAuthGuard.ts +0 -25
  29. package/src/auth/hooks/useAuthRedirect.ts +0 -51
  30. package/src/auth/hooks/useAutoAuth.ts +0 -49
  31. package/src/auth/hooks/useGithubAuth.ts +0 -184
  32. package/src/auth/hooks/useLocalStorage.ts +0 -214
  33. package/src/auth/hooks/useProfileCache.ts +0 -146
  34. package/src/auth/hooks/useSessionStorage.ts +0 -189
  35. package/src/auth/index.ts +0 -10
  36. package/src/auth/middlewares/index.ts +0 -1
  37. package/src/auth/middlewares/proxy.ts +0 -32
  38. package/src/auth/server.ts +0 -6
  39. package/src/auth/utils/errors.ts +0 -34
  40. package/src/auth/utils/index.ts +0 -2
  41. package/src/auth/utils/validation.ts +0 -14
  42. package/src/contexts/LeadsContext.tsx +0 -156
  43. package/src/contexts/NewsletterContext.tsx +0 -263
  44. package/src/contexts/SupportContext.tsx +0 -256
  45. package/src/contexts/index.ts +0 -59
  46. package/src/contexts/knowbase/ChatContext.tsx +0 -174
  47. package/src/contexts/knowbase/DocumentsContext.tsx +0 -304
  48. package/src/contexts/knowbase/SessionsContext.tsx +0 -174
  49. package/src/contexts/knowbase/index.ts +0 -61
  50. package/src/contexts/payments/BalancesContext.tsx +0 -65
  51. package/src/contexts/payments/CurrenciesContext.tsx +0 -66
  52. package/src/contexts/payments/OverviewContext.tsx +0 -174
  53. package/src/contexts/payments/PaymentsContext.tsx +0 -132
  54. package/src/contexts/payments/README.md +0 -201
  55. package/src/contexts/payments/RootPaymentsContext.tsx +0 -68
  56. package/src/contexts/payments/index.ts +0 -50
  57. package/src/layouts/PaymentsLayout/PaymentsLayout.tsx +0 -92
  58. package/src/layouts/PaymentsLayout/components/CreatePaymentDialog.tsx +0 -291
  59. package/src/layouts/PaymentsLayout/components/PaymentDetailsDialog.tsx +0 -290
  60. package/src/layouts/PaymentsLayout/components/index.ts +0 -2
  61. package/src/layouts/PaymentsLayout/events.ts +0 -47
  62. package/src/layouts/PaymentsLayout/index.ts +0 -16
  63. package/src/layouts/PaymentsLayout/types.ts +0 -6
  64. package/src/layouts/PaymentsLayout/views/overview/components/BalanceCard.tsx +0 -128
  65. package/src/layouts/PaymentsLayout/views/overview/components/RecentPayments.tsx +0 -142
  66. package/src/layouts/PaymentsLayout/views/overview/components/index.ts +0 -2
  67. package/src/layouts/PaymentsLayout/views/overview/index.tsx +0 -20
  68. package/src/layouts/PaymentsLayout/views/payments/components/PaymentsList.tsx +0 -276
  69. package/src/layouts/PaymentsLayout/views/payments/components/index.ts +0 -1
  70. package/src/layouts/PaymentsLayout/views/payments/index.tsx +0 -17
  71. package/src/layouts/PaymentsLayout/views/transactions/components/TransactionsList.tsx +0 -273
  72. package/src/layouts/PaymentsLayout/views/transactions/components/index.ts +0 -1
  73. package/src/layouts/PaymentsLayout/views/transactions/index.tsx +0 -17
  74. package/src/layouts/SupportLayout/README.md +0 -91
  75. package/src/layouts/SupportLayout/SupportLayout.tsx +0 -179
  76. package/src/layouts/SupportLayout/components/CreateTicketDialog.tsx +0 -155
  77. package/src/layouts/SupportLayout/components/MessageInput.tsx +0 -92
  78. package/src/layouts/SupportLayout/components/MessageList.tsx +0 -314
  79. package/src/layouts/SupportLayout/components/TicketCard.tsx +0 -96
  80. package/src/layouts/SupportLayout/components/TicketList.tsx +0 -153
  81. package/src/layouts/SupportLayout/components/index.ts +0 -6
  82. package/src/layouts/SupportLayout/context/SupportLayoutContext.tsx +0 -263
  83. package/src/layouts/SupportLayout/context/index.ts +0 -2
  84. package/src/layouts/SupportLayout/events.ts +0 -33
  85. package/src/layouts/SupportLayout/hooks/index.ts +0 -2
  86. package/src/layouts/SupportLayout/hooks/useInfiniteMessages.ts +0 -119
  87. package/src/layouts/SupportLayout/hooks/useInfiniteTickets.ts +0 -92
  88. package/src/layouts/SupportLayout/index.ts +0 -8
  89. package/src/layouts/SupportLayout/types.ts +0 -21
  90. package/src/snippets/Chat/ChatUIContext.tsx +0 -110
  91. package/src/snippets/Chat/ChatWidget.tsx +0 -476
  92. package/src/snippets/Chat/README.md +0 -122
  93. package/src/snippets/Chat/components/MessageInput.tsx +0 -124
  94. package/src/snippets/Chat/components/MessageList.tsx +0 -169
  95. package/src/snippets/Chat/components/SessionList.tsx +0 -192
  96. package/src/snippets/Chat/components/index.ts +0 -9
  97. package/src/snippets/Chat/hooks/index.ts +0 -6
  98. package/src/snippets/Chat/hooks/useInfiniteSessions.ts +0 -82
  99. package/src/snippets/Chat/index.tsx +0 -45
  100. package/src/snippets/Chat/types.ts +0 -80
  101. package/src/snippets/ContactForm/ContactForm.tsx +0 -346
  102. package/src/snippets/ContactForm/ContactFormProvider.tsx +0 -153
  103. package/src/snippets/ContactForm/ContactInfo.tsx +0 -114
  104. package/src/snippets/ContactForm/ContactPage.tsx +0 -131
  105. package/src/snippets/ContactForm/dynamic.tsx +0 -55
  106. package/src/snippets/ContactForm/index.ts +0 -34
  107. package/src/snippets/ContactForm/types.ts +0 -110
@@ -1,332 +0,0 @@
1
- "use client"
2
-
3
- import { useCallback, useEffect, useState } from 'react';
4
-
5
- import { useAuth } from '../context';
6
- import { useAutoAuth } from './useAutoAuth';
7
- import { useLocalStorage } from './useLocalStorage';
8
- import { authLogger } from '../../utils/logger';
9
-
10
- export interface AuthFormState {
11
- identifier: string; // Email or phone number
12
- channel: 'email' | 'phone';
13
- otp: string;
14
- isLoading: boolean;
15
- acceptedTerms: boolean;
16
- step: 'identifier' | 'otp';
17
- error: string;
18
- }
19
-
20
- export interface AuthFormHandlers {
21
- setIdentifier: (identifier: string) => void;
22
- setChannel: (channel: 'email' | 'phone') => void;
23
- setOtp: (otp: string) => void;
24
- setAcceptedTerms: (accepted: boolean) => void;
25
- setError: (error: string) => void;
26
- clearError: () => void;
27
- handleIdentifierSubmit: (e: React.FormEvent) => Promise<void>;
28
- handleOTPSubmit: (e: React.FormEvent) => Promise<void>;
29
- handleResendOTP: () => Promise<void>;
30
- handleBackToIdentifier: () => void;
31
- forceOTPStep: () => void;
32
- // Utility methods
33
- detectChannelFromIdentifier: (identifier: string) => 'email' | 'phone' | null;
34
- validateIdentifier: (identifier: string, channel?: 'email' | 'phone') => boolean;
35
- }
36
-
37
- export interface UseAuthFormOptions {
38
- onIdentifierSuccess?: (identifier: string, channel: 'email' | 'phone') => void;
39
- onOTPSuccess?: () => void;
40
- onError?: (message: string) => void;
41
- sourceUrl: string;
42
- }
43
-
44
- export const useAuthForm = (options: UseAuthFormOptions): AuthFormState & AuthFormHandlers => {
45
- const { onIdentifierSuccess, onOTPSuccess, onError, sourceUrl } = options;
46
-
47
- // Form state
48
- const [identifier, setIdentifier] = useState('');
49
- const [channel, setChannel] = useState<'email' | 'phone'>('email');
50
- const [otp, setOtp] = useState('');
51
- const [isLoading, setIsLoading] = useState(false);
52
- const [acceptedTerms, setAcceptedTerms] = useState(false);
53
- const [step, setStep] = useState<'identifier' | 'otp'>('identifier');
54
- const [error, setError] = useState('');
55
-
56
-
57
-
58
- // Auth hooks
59
- const { requestOTP, verifyOTP, getSavedEmail, saveEmail, getSavedPhone, savePhone } = useAuth();
60
- const [savedTermsAccepted, setSavedTermsAccepted] = useLocalStorage('auth_terms_accepted', false);
61
- const [savedEmail, setSavedEmail] = useLocalStorage('auth_email', '');
62
- const [savedPhone, setSavedPhone] = useLocalStorage('auth_phone', '');
63
-
64
- // Utility functions
65
- const detectChannelFromIdentifier = useCallback((identifier: string): 'email' | 'phone' | null => {
66
- if (!identifier) return null;
67
-
68
- // Email detection
69
- if (identifier.includes('@')) {
70
- return 'email';
71
- }
72
-
73
- // Phone detection (starts with + and contains digits)
74
- if (identifier.startsWith('+') && /^\+[1-9]\d{6,14}$/.test(identifier)) {
75
- return 'phone';
76
- }
77
-
78
- return null;
79
- }, []);
80
-
81
- const validateIdentifier = useCallback((identifier: string, channelType?: 'email' | 'phone'): boolean => {
82
- if (!identifier) return false;
83
-
84
- const detectedChannel = channelType || detectChannelFromIdentifier(identifier);
85
-
86
- if (detectedChannel === 'email') {
87
- // Basic email validation
88
- return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(identifier);
89
- } else if (detectedChannel === 'phone') {
90
- // E.164 phone validation
91
- return /^\+[1-9]\d{6,14}$/.test(identifier);
92
- }
93
-
94
- return false;
95
- }, [detectChannelFromIdentifier]);
96
-
97
- // Load saved data on mount
98
- useEffect(() => {
99
- const authSavedEmail = getSavedEmail();
100
- const authSavedPhone = getSavedPhone();
101
-
102
- // Prioritize phone over email if both exist
103
- if (authSavedPhone) {
104
- setIdentifier(authSavedPhone);
105
- setChannel('phone');
106
- } else if (authSavedEmail) {
107
- setIdentifier(authSavedEmail);
108
- setChannel('email');
109
- }
110
-
111
- if (savedTermsAccepted) {
112
- setAcceptedTerms(savedTermsAccepted);
113
- }
114
- }, [getSavedEmail, getSavedPhone, savedTermsAccepted]);
115
-
116
- // Auto-detect channel when identifier changes
117
- useEffect(() => {
118
- if (identifier) {
119
- const detectedChannel = detectChannelFromIdentifier(identifier);
120
- if (detectedChannel && detectedChannel !== channel) {
121
- setChannel(detectedChannel);
122
- }
123
- }
124
- }, [identifier, channel, detectChannelFromIdentifier]);
125
-
126
-
127
-
128
- const clearError = useCallback(() => setError(''), []);
129
-
130
- const handleIdentifierSubmit = useCallback(async (e: React.FormEvent) => {
131
- e.preventDefault();
132
-
133
- if (!identifier) {
134
- const message = channel === 'phone' ? 'Please enter your phone number' : 'Please enter your email address';
135
- setError(message);
136
- onError?.(message);
137
- return;
138
- }
139
-
140
- // Validate identifier format
141
- if (!validateIdentifier(identifier, channel)) {
142
- const message = channel === 'phone'
143
- ? 'Please enter a valid phone number (e.g., +1234567890)'
144
- : 'Please enter a valid email address';
145
- setError(message);
146
- onError?.(message);
147
- return;
148
- }
149
-
150
- if (!acceptedTerms) {
151
- const message = 'Please accept the Terms of Service and Privacy Policy';
152
- setError(message);
153
- onError?.(message);
154
- return;
155
- }
156
-
157
- setIsLoading(true);
158
- clearError();
159
-
160
- try {
161
- const result = await requestOTP(identifier, channel, sourceUrl);
162
-
163
- if (result.success) {
164
- // Save identifier and terms acceptance on successful request, clear opposite channel
165
- if (channel === 'email') {
166
- saveEmail(identifier);
167
- setSavedPhone(''); // Clear phone storage
168
- } else if (channel === 'phone') {
169
- savePhone(identifier);
170
- setSavedEmail(''); // Clear email storage
171
- }
172
- setSavedTermsAccepted(true);
173
- setStep('otp');
174
- onIdentifierSuccess?.(identifier, channel);
175
- } else {
176
- setError(result.message);
177
- onError?.(result.message);
178
- }
179
- } catch (error) {
180
- const message = 'An unexpected error occurred';
181
- setError(message);
182
- onError?.(message);
183
- } finally {
184
- setIsLoading(false);
185
- }
186
- }, [identifier, channel, acceptedTerms, validateIdentifier, requestOTP, saveEmail, clearError, setSavedTermsAccepted, onIdentifierSuccess, onError, sourceUrl]);
187
-
188
- const handleOTPSubmit = useCallback(async (e: React.FormEvent) => {
189
- e.preventDefault();
190
-
191
- if (!otp || otp.length < 6) {
192
- const message = 'Please enter the 6-digit verification code';
193
- setError(message);
194
- onError?.(message);
195
- return;
196
- }
197
-
198
- setIsLoading(true);
199
- clearError();
200
-
201
- try {
202
- const result = await verifyOTP(identifier, otp, channel, sourceUrl);
203
-
204
- if (result.success) {
205
- // Save identifier on successful login, clear opposite channel
206
- if (channel === 'email') {
207
- setSavedEmail(identifier);
208
- setSavedPhone(''); // Clear phone storage
209
- } else if (channel === 'phone') {
210
- setSavedPhone(identifier);
211
- setSavedEmail(''); // Clear email storage
212
- }
213
- onOTPSuccess?.();
214
- } else {
215
- setError(result.message);
216
- onError?.(result.message);
217
- }
218
- } catch (error) {
219
- const message = 'An unexpected error occurred';
220
- setError(message);
221
- onError?.(message);
222
- } finally {
223
- setIsLoading(false);
224
- }
225
- }, [identifier, otp, channel, verifyOTP, clearError, setSavedEmail, onOTPSuccess, onError, sourceUrl]);
226
-
227
- const handleResendOTP = useCallback(async () => {
228
- setIsLoading(true);
229
- clearError();
230
-
231
- try {
232
- const result = await requestOTP(identifier, channel, sourceUrl);
233
-
234
- if (result.success) {
235
- // Save identifier and clear OTP input, clear opposite channel
236
- if (channel === 'email') {
237
- saveEmail(identifier);
238
- setSavedPhone(''); // Clear phone storage
239
- } else if (channel === 'phone') {
240
- savePhone(identifier);
241
- setSavedEmail(''); // Clear email storage
242
- }
243
- setOtp('');
244
- } else {
245
- setError(result.message);
246
- onError?.(result.message);
247
- }
248
- } catch (error) {
249
- const message = 'Failed to resend verification code';
250
- setError(message);
251
- onError?.(message);
252
- } finally {
253
- setIsLoading(false);
254
- }
255
- }, [identifier, channel, requestOTP, saveEmail, clearError, setOtp, onError, sourceUrl]);
256
-
257
- const handleBackToIdentifier = useCallback(() => {
258
- setStep('identifier');
259
- clearError();
260
- }, [clearError]);
261
-
262
- const forceOTPStep = useCallback(() => {
263
- setStep('otp');
264
- clearError();
265
- }, [clearError]);
266
-
267
- const handleAcceptedTermsChange = useCallback((checked: boolean) => {
268
- setAcceptedTerms(checked);
269
- setSavedTermsAccepted(checked);
270
- }, [setSavedTermsAccepted]);
271
-
272
- // Auto-detect OTP from URL query parameters
273
- useAutoAuth({
274
- onOTPDetected: (otp: string) => {
275
- authLogger.info('OTP detected, auto-submitting');
276
-
277
- // Get saved identifier from auth context
278
- const savedEmail = getSavedEmail();
279
- const savedPhone = getSavedPhone();
280
-
281
- // Prioritize phone over email if both exist
282
- if (savedPhone) {
283
- setIdentifier(savedPhone);
284
- setChannel('phone');
285
- } else if (savedEmail) {
286
- setIdentifier(savedEmail);
287
- setChannel('email');
288
- }
289
-
290
- // Set OTP and force OTP step
291
- setOtp(otp);
292
- setStep('otp');
293
-
294
- // Auto-submit after a short delay to ensure state is updated
295
- setTimeout(() => {
296
- const fakeEvent = { preventDefault: () => {} } as React.FormEvent;
297
- handleOTPSubmit(fakeEvent);
298
- }, 200);
299
- },
300
- cleanupUrl: true,
301
- });
302
-
303
- return {
304
- // Form state
305
- identifier,
306
- channel,
307
- otp,
308
- isLoading,
309
- acceptedTerms,
310
- step,
311
- error,
312
-
313
- // Form handlers
314
- setIdentifier,
315
- setChannel,
316
- setOtp,
317
- setAcceptedTerms: handleAcceptedTermsChange,
318
- setError,
319
- clearError,
320
-
321
- // Auth handlers
322
- handleIdentifierSubmit,
323
- handleOTPSubmit,
324
- handleResendOTP,
325
- handleBackToIdentifier,
326
- forceOTPStep,
327
-
328
- // Utility methods
329
- detectChannelFromIdentifier,
330
- validateIdentifier,
331
- };
332
- };
@@ -1,25 +0,0 @@
1
- "use client"
2
-
3
- import { useEffect } from 'react';
4
-
5
- import { useAuth } from '../context';
6
- import { useCfgRouter } from '@djangocfg/ui-nextjs/hooks';
7
-
8
- interface UseAuthGuardOptions {
9
- redirectTo?: string;
10
- requireAuth?: boolean;
11
- }
12
-
13
- export const useAuthGuard = (options: UseAuthGuardOptions = {}) => {
14
- const { redirectTo = '/auth', requireAuth = true } = options;
15
- const { isAuthenticated, isLoading } = useAuth();
16
- const router = useCfgRouter();
17
-
18
- useEffect(() => {
19
- if (!isLoading && requireAuth && !isAuthenticated) {
20
- router.push(redirectTo);
21
- }
22
- }, [isAuthenticated, isLoading, router, redirectTo, requireAuth]);
23
-
24
- return { isAuthenticated, isLoading };
25
- };
@@ -1,51 +0,0 @@
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
- };
@@ -1,49 +0,0 @@
1
- 'use client';
2
-
3
- import { useEffect } from 'react';
4
- import { usePathname } from 'next/navigation';
5
- import { useQueryParams, useCfgRouter } from '@djangocfg/ui-nextjs/hooks';
6
- import { authLogger } from '../../utils/logger';
7
-
8
- export interface UseAutoAuthOptions {
9
- onOTPDetected?: (otp: string) => void;
10
- cleanupUrl?: boolean;
11
- }
12
-
13
- /**
14
- * Hook for automatic authentication from URL query parameters
15
- * Detects OTP from URL and triggers callback
16
- */
17
- export const useAutoAuth = (options: UseAutoAuthOptions = {}) => {
18
- const { onOTPDetected, cleanupUrl = true } = options;
19
- const queryParams = useQueryParams();
20
- const pathname = usePathname();
21
- const router = useCfgRouter();
22
-
23
- const isReady = !!pathname && !!queryParams.get('otp');
24
- const hasOTP = !!(queryParams.get('otp'));
25
-
26
- useEffect(() => {
27
- if (!isReady) return;
28
-
29
- const queryOtp = queryParams.get('otp') as string;
30
-
31
- // Handle OTP detection
32
- if (queryOtp && typeof queryOtp === 'string' && queryOtp.length === 6) {
33
- authLogger.info('OTP detected in URL:', queryOtp);
34
- onOTPDetected?.(queryOtp);
35
- }
36
-
37
- // Clean up URL to remove sensitive params for security
38
- if (cleanupUrl && queryOtp) {
39
- const cleanQuery = Object.fromEntries(queryParams.entries());
40
- delete cleanQuery.otp;
41
- router.push(`${pathname}?${new URLSearchParams(cleanQuery).toString()}`);
42
- }
43
- }, [pathname, queryParams, onOTPDetected, cleanupUrl, router]);
44
-
45
- return {
46
- isReady,
47
- hasOTP,
48
- };
49
- };
@@ -1,184 +0,0 @@
1
- 'use client';
2
-
3
- import { useCallback, useState } from 'react';
4
-
5
- import { api } from '@djangocfg/api';
6
- import { useCfgRouter } from '@djangocfg/ui-nextjs/hooks';
7
- import { authLogger } from '../../utils/logger';
8
- import { Analytics, AnalyticsEvent, AnalyticsCategory } from '../../snippets/Analytics';
9
-
10
- export interface UseGithubAuthOptions {
11
- sourceUrl?: string;
12
- onSuccess?: (user: any, isNewUser: boolean) => void;
13
- onError?: (error: string) => void;
14
- redirectUrl?: string;
15
- }
16
-
17
- export interface UseGithubAuthReturn {
18
- isLoading: boolean;
19
- error: string | null;
20
- startGithubAuth: () => Promise<void>;
21
- handleGithubCallback: (code: string, state: string) => Promise<void>;
22
- }
23
-
24
- /**
25
- * Hook for GitHub OAuth authentication flow.
26
- *
27
- * Usage:
28
- * 1. Call startGithubAuth() to redirect user to GitHub
29
- * 2. After GitHub redirects back, call handleGithubCallback(code, state)
30
- *
31
- * @example
32
- * ```tsx
33
- * const { isLoading, error, startGithubAuth } = useGithubAuth({
34
- * onSuccess: (user) => router.push('/dashboard'),
35
- * onError: (error) => console.error(error),
36
- * });
37
- *
38
- * <Button onClick={startGithubAuth} disabled={isLoading}>
39
- * Continue with GitHub
40
- * </Button>
41
- * ```
42
- */
43
- export const useGithubAuth = (options: UseGithubAuthOptions = {}): UseGithubAuthReturn => {
44
- const { sourceUrl, onSuccess, onError, redirectUrl } = options;
45
- const router = useCfgRouter();
46
-
47
- const [isLoading, setIsLoading] = useState(false);
48
- const [error, setError] = useState<string | null>(null);
49
-
50
- /**
51
- * Start GitHub OAuth flow - redirects user to GitHub authorization page.
52
- */
53
- const startGithubAuth = useCallback(async () => {
54
- setIsLoading(true);
55
- setError(null);
56
-
57
- try {
58
- authLogger.info('Starting GitHub OAuth flow...');
59
-
60
- // Track OAuth start
61
- Analytics.event(AnalyticsEvent.AUTH_OAUTH_START, {
62
- category: AnalyticsCategory.AUTH,
63
- label: 'github',
64
- });
65
-
66
- // Call API to get authorization URL
67
- // The API will auto-generate redirect_uri from config if not provided
68
- const response = await api.cfg_oauth.accountsOauthGithubAuthorizeCreate({
69
- source_url: sourceUrl || (typeof window !== 'undefined' ? window.location.href : ''),
70
- });
71
-
72
- if (!response.authorization_url) {
73
- throw new Error('Failed to get authorization URL');
74
- }
75
-
76
- authLogger.info('Redirecting to GitHub...', response.authorization_url);
77
-
78
- // Store state in sessionStorage for verification on callback
79
- if (typeof window !== 'undefined') {
80
- sessionStorage.setItem('oauth_state', response.state);
81
- sessionStorage.setItem('oauth_provider', 'github');
82
- }
83
-
84
- // Redirect to GitHub
85
- window.location.href = response.authorization_url;
86
-
87
- } catch (err) {
88
- const errorMessage = err instanceof Error ? err.message : 'Failed to start GitHub authentication';
89
- authLogger.error('GitHub OAuth start error:', err);
90
- setError(errorMessage);
91
- onError?.(errorMessage);
92
-
93
- // Track OAuth error
94
- Analytics.event(AnalyticsEvent.AUTH_OAUTH_FAIL, {
95
- category: AnalyticsCategory.AUTH,
96
- label: 'github',
97
- });
98
- } finally {
99
- setIsLoading(false);
100
- }
101
- }, [sourceUrl, onError]);
102
-
103
- /**
104
- * Handle GitHub OAuth callback - exchanges code for JWT tokens.
105
- *
106
- * @param code - Authorization code from GitHub callback
107
- * @param state - State token for CSRF verification
108
- */
109
- const handleGithubCallback = useCallback(async (code: string, state: string) => {
110
- setIsLoading(true);
111
- setError(null);
112
-
113
- try {
114
- authLogger.info('Processing GitHub OAuth callback...');
115
-
116
- // Verify state matches what we stored
117
- if (typeof window !== 'undefined') {
118
- const storedState = sessionStorage.getItem('oauth_state');
119
- if (storedState && storedState !== state) {
120
- throw new Error('Invalid OAuth state - possible CSRF attack');
121
- }
122
- // Clear stored state
123
- sessionStorage.removeItem('oauth_state');
124
- sessionStorage.removeItem('oauth_provider');
125
- }
126
-
127
- // Exchange code for tokens
128
- // The API will auto-generate redirect_uri from config if not provided
129
- const response = await api.cfg_oauth.accountsOauthGithubCallbackCreate({
130
- code,
131
- state,
132
- });
133
-
134
- if (!response.access || !response.refresh) {
135
- throw new Error('Invalid response from OAuth callback');
136
- }
137
-
138
- authLogger.info('GitHub OAuth successful, user:', response.user);
139
-
140
- // Save tokens using API client
141
- api.setToken(response.access, response.refresh);
142
-
143
- // Track successful OAuth
144
- Analytics.event(AnalyticsEvent.AUTH_LOGIN_SUCCESS, {
145
- category: AnalyticsCategory.AUTH,
146
- label: 'github',
147
- });
148
-
149
- // Set user ID for future tracking
150
- if (response.user?.id) {
151
- Analytics.setUser(String(response.user.id));
152
- }
153
-
154
- // Call success callback
155
- onSuccess?.(response.user, response.is_new_user || false);
156
-
157
- // Redirect to dashboard or specified URL
158
- // Use hardPush for full page reload - ensures all React contexts reinitialize
159
- const finalRedirectUrl = redirectUrl || '/dashboard';
160
- router.hardPush(finalRedirectUrl);
161
-
162
- } catch (err) {
163
- const errorMessage = err instanceof Error ? err.message : 'GitHub authentication failed';
164
- authLogger.error('GitHub OAuth callback error:', err);
165
- setError(errorMessage);
166
- onError?.(errorMessage);
167
-
168
- // Track OAuth error
169
- Analytics.event(AnalyticsEvent.AUTH_OAUTH_FAIL, {
170
- category: AnalyticsCategory.AUTH,
171
- label: 'github',
172
- });
173
- } finally {
174
- setIsLoading(false);
175
- }
176
- }, [onSuccess, onError, redirectUrl, router]);
177
-
178
- return {
179
- isLoading,
180
- error,
181
- startGithubAuth,
182
- handleGithubCallback,
183
- };
184
- };