@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,458 @@
1
+ import { useRouter } from 'next/router';
2
+ import React, {
3
+ createContext, ReactNode, useCallback, useContext, useEffect, useMemo, useRef, useState
4
+ } from 'react';
5
+
6
+ import { api, Enums } from '@djangocfg/api';
7
+ import { useAccountsContext, AccountsProvider } from '@djangocfg/api/cfg/contexts';
8
+ import { useLocalStorage } from '@djangocfg/ui/hooks';
9
+
10
+ import { authLogger } from '../../utils/logger';
11
+ import type { AuthConfig, AuthContextType, AuthProviderProps, UserProfile } from './types';
12
+
13
+ // Default routes
14
+ const defaultRoutes = {
15
+ auth: '/auth',
16
+ defaultCallback: '/dashboard',
17
+ defaultAuthCallback: '/auth',
18
+ };
19
+
20
+ const AuthContext = createContext<AuthContextType | undefined>(undefined);
21
+
22
+ // Constants
23
+ const EMAIL_STORAGE_KEY = 'auth_email';
24
+ const PHONE_STORAGE_KEY = 'auth_phone';
25
+ const AUTH_REDIRECT_KEY = 'auth_redirect_url';
26
+
27
+ const hasValidTokens = (): boolean => {
28
+ if (typeof window === 'undefined') return false;
29
+ return api.isAuthenticated();
30
+ };
31
+
32
+ // Internal provider that uses AccountsContext
33
+ const AuthProviderInternal: React.FC<AuthProviderProps> = ({ children, config }) => {
34
+ const accounts = useAccountsContext();
35
+ const [isLoading, setIsLoading] = useState(true);
36
+ const [initialized, setInitialized] = useState(false);
37
+ const router = useRouter();
38
+
39
+ // Use localStorage hooks for email, phone, and redirect
40
+ const [storedEmail, setStoredEmail, clearStoredEmail] = useLocalStorage<string | null>(EMAIL_STORAGE_KEY, null);
41
+ const [storedPhone, setStoredPhone, clearStoredPhone] = useLocalStorage<string | null>(PHONE_STORAGE_KEY, null);
42
+ const [redirectUrl, setRedirectUrl, clearRedirectUrl] = useLocalStorage<string | null>(AUTH_REDIRECT_KEY, null);
43
+
44
+ // Map AccountsContext profile to UserProfile
45
+ const user = accounts.profile as UserProfile | null;
46
+
47
+ // Use refs to avoid dependency issues
48
+ const userRef = useRef(user);
49
+ const configRef = useRef(config);
50
+
51
+ // Update refs when values change
52
+ useEffect(() => {
53
+ userRef.current = user;
54
+ }, [user]);
55
+
56
+ useEffect(() => {
57
+ configRef.current = config;
58
+ }, [config]);
59
+
60
+ // Note: API URL is configured in BaseClient, not at runtime
61
+
62
+ // Common function to clear auth state
63
+ const clearAuthState = useCallback((caller: string) => {
64
+ authLogger.info('clearAuthState >> caller', caller);
65
+ api.clearTokens();
66
+ // Note: user is now managed by AccountsContext, will auto-update
67
+ setInitialized(true);
68
+ setIsLoading(false);
69
+ }, []);
70
+
71
+ // Global error handler for auth-related errors
72
+ const handleGlobalAuthError = useCallback((error: any, context: string = 'API Request') => {
73
+ // Simple error check - if response has error flag, it's an error
74
+ if (error?.success === false) {
75
+ authLogger.warn(`Error detected in ${context}, clearing tokens`);
76
+ clearAuthState(`globalAuthError:${context}`);
77
+ return true;
78
+ }
79
+
80
+ return false;
81
+ }, [clearAuthState]);
82
+
83
+ // Simple profile loading without retry - now uses AccountsContext
84
+ const loadCurrentProfile = useCallback(async (): Promise<void> => {
85
+ try {
86
+ // Ensure API clients are properly initialized with current token
87
+ if (!api.isAuthenticated()) {
88
+ throw new Error('No valid authentication token');
89
+ }
90
+
91
+ // Refresh profile from AccountsContext
92
+ const refreshedProfile = await accounts.refreshProfile();
93
+
94
+ if (refreshedProfile) {
95
+ setInitialized(true);
96
+ authLogger.info('Profile loaded successfully:', refreshedProfile.id);
97
+ } else {
98
+ authLogger.warn('Profile refresh returned undefined');
99
+ clearAuthState('loadCurrentProfile:noProfile');
100
+ }
101
+ } catch (error) {
102
+ authLogger.error('Failed to load profile:', error);
103
+ // Use global error handler first, fallback to clearing state
104
+ if (!handleGlobalAuthError(error, 'loadCurrentProfile')) {
105
+ clearAuthState('loadCurrentProfile:error');
106
+ }
107
+ }
108
+ }, [clearAuthState, handleGlobalAuthError, accounts]);
109
+
110
+ // Initialize auth state once
111
+ useEffect(() => {
112
+ if (initialized) return;
113
+
114
+ const initializeAuth = async () => {
115
+ authLogger.info('Initializing auth...');
116
+ setIsLoading(true);
117
+
118
+ // Debug token state
119
+ const token = api.getToken();
120
+ const refreshToken = api.getRefreshToken();
121
+ authLogger.info('Token from API:', token ? `${token.substring(0, 20)}...` : 'null');
122
+ authLogger.info('Refresh token from API:', refreshToken ? `${refreshToken.substring(0, 20)}...` : 'null');
123
+ authLogger.info('localStorage keys:', Object.keys(localStorage).filter(k => k.includes('token') || k.includes('auth')));
124
+
125
+ const hasTokens = hasValidTokens();
126
+ authLogger.info('Has tokens:', hasTokens);
127
+
128
+ if (hasTokens) {
129
+ try {
130
+ await loadCurrentProfile();
131
+ } catch (error) {
132
+ authLogger.error('Failed to load profile during initialization:', error);
133
+ // If profile loading fails, clear auth state
134
+ clearAuthState('initializeAuth:loadProfileFailed');
135
+ }
136
+ } else {
137
+ setInitialized(true);
138
+ }
139
+
140
+ setIsLoading(false);
141
+ };
142
+
143
+ initializeAuth();
144
+ }, [initialized, loadCurrentProfile, clearAuthState]);
145
+
146
+ // Redirect logic - only for unauthenticated users on protected pages
147
+ useEffect(() => {
148
+ if (!initialized) return;
149
+
150
+ const isAuthenticated = !!userRef.current && api.isAuthenticated();
151
+ const authRoute = config?.routes?.auth || defaultRoutes.auth;
152
+ const isAuthPage = router.pathname === authRoute;
153
+
154
+ // Only redirect authenticated users away from auth page if they're not in a flow
155
+ // This prevents interference with OTP verification flow
156
+ if (isAuthenticated && isAuthPage && !router.query.flow) {
157
+ const callbackUrl = config?.routes?.defaultCallback || defaultRoutes.defaultCallback;
158
+ window.location.href = callbackUrl;
159
+ }
160
+ }, [initialized, router.pathname, config?.routes, router.query.flow]);
161
+
162
+ const pushToDefaultCallbackUrl = useCallback(() => {
163
+ const callbackUrl = config?.routes?.defaultCallback || defaultRoutes.defaultCallback;
164
+ window.location.href = callbackUrl;
165
+ }, [config?.routes]);
166
+
167
+ const pushToDefaultAuthCallbackUrl = useCallback(() => {
168
+ const authCallbackUrl = config?.routes?.defaultAuthCallback || defaultRoutes.defaultAuthCallback;
169
+ window.location.href = authCallbackUrl;
170
+ }, [config?.routes]);
171
+
172
+ // Memoized checkAuthAndRedirect function
173
+ const checkAuthAndRedirect = useCallback(async () => {
174
+ try {
175
+ setIsLoading(true);
176
+ const isAuthenticated = api.isAuthenticated();
177
+
178
+ if (isAuthenticated) {
179
+ await loadCurrentProfile();
180
+ if (userRef.current) {
181
+ pushToDefaultCallbackUrl();
182
+ }
183
+ } else {
184
+ pushToDefaultAuthCallbackUrl();
185
+ }
186
+ } catch (error) {
187
+ authLogger.error('Failed to check authentication:', error);
188
+ // Use global error handler first
189
+ if (!handleGlobalAuthError(error, 'checkAuthAndRedirect')) {
190
+ clearAuthState('checkAuthAndRedirect');
191
+ }
192
+ pushToDefaultAuthCallbackUrl();
193
+ } finally {
194
+ setIsLoading(false);
195
+ }
196
+ }, [loadCurrentProfile, clearAuthState, pushToDefaultCallbackUrl, pushToDefaultAuthCallbackUrl, handleGlobalAuthError]);
197
+
198
+ // OTP methods - supports both email and phone - now uses AccountsContext
199
+ const requestOTP = useCallback(
200
+ async (identifier: string, channel?: 'email' | 'phone', sourceUrl?: string): Promise<{ success: boolean; message: string }> => {
201
+ // Clear tokens before requesting OTP
202
+ api.clearTokens();
203
+
204
+ try {
205
+ const channelValue = channel === 'phone'
206
+ ? Enums.OTPRequestRequestChannel.PHONE
207
+ : Enums.OTPRequestRequestChannel.EMAIL;
208
+ const result = await accounts.requestOTP({
209
+ identifier,
210
+ channel: channelValue,
211
+ });
212
+
213
+ const channelName = channel === 'phone' ? 'phone number' : 'email address';
214
+ return {
215
+ success: true,
216
+ message: result.message || `OTP code sent to your ${channelName}`,
217
+ };
218
+ } catch (error) {
219
+ authLogger.error('Request OTP error:', error);
220
+ return {
221
+ success: false,
222
+ message: 'Failed to send OTP',
223
+ };
224
+ }
225
+ },
226
+ [accounts],
227
+ );
228
+
229
+ const verifyOTP = useCallback(
230
+ async (identifier: string, otpCode: string, channel?: 'email' | 'phone', sourceUrl?: string): Promise<{ success: boolean; message: string; user?: UserProfile }> => {
231
+ try {
232
+ const channelValue = channel === 'phone'
233
+ ? Enums.OTPVerifyRequestChannel.PHONE
234
+ : Enums.OTPVerifyRequestChannel.EMAIL;
235
+ // AccountsContext automatically saves tokens and refreshes profile
236
+ const result = await accounts.verifyOTP({
237
+ identifier,
238
+ otp: otpCode,
239
+ channel: channelValue,
240
+ });
241
+
242
+ // Verify that we got valid tokens
243
+ if (!result.access || !result.refresh) {
244
+ authLogger.error('Verify OTP returned invalid response:', result);
245
+ return {
246
+ success: false,
247
+ message: 'Invalid OTP verification response',
248
+ };
249
+ }
250
+
251
+ // Save identifier based on channel and clear opposite channel
252
+ if (channel === 'phone') {
253
+ setStoredPhone(identifier);
254
+ clearStoredEmail();
255
+ } else if (identifier.includes('@')) {
256
+ setStoredEmail(identifier);
257
+ clearStoredPhone();
258
+ }
259
+
260
+ // Small delay to ensure profile state is updated
261
+ await new Promise(resolve => setTimeout(resolve, 200));
262
+
263
+ // Handle redirect logic here
264
+ const defaultCallback = config?.routes?.defaultCallback || defaultRoutes.defaultCallback;
265
+
266
+ if (redirectUrl && redirectUrl !== defaultCallback) {
267
+ clearRedirectUrl();
268
+ window.location.href = redirectUrl;
269
+ } else {
270
+ window.location.href = defaultCallback;
271
+ }
272
+
273
+ return {
274
+ success: true,
275
+ message: 'Login successful',
276
+ user: result.user as UserProfile,
277
+ };
278
+ } catch (error) {
279
+ authLogger.error('Verify OTP error:', error);
280
+ return {
281
+ success: false,
282
+ message: 'Failed to verify OTP',
283
+ };
284
+ }
285
+ },
286
+ [setStoredEmail, setStoredPhone, clearStoredEmail, clearStoredPhone, redirectUrl, clearRedirectUrl, config?.routes?.defaultCallback, accounts],
287
+ );
288
+
289
+ const refreshToken = useCallback(async (): Promise<{ success: boolean; message: string }> => {
290
+ try {
291
+ const refreshTokenValue = api.getRefreshToken();
292
+ if (!refreshTokenValue) {
293
+ clearAuthState('refreshToken:noToken');
294
+ return {
295
+ success: false,
296
+ message: 'No refresh token available',
297
+ };
298
+ }
299
+
300
+ await accounts.refreshToken(refreshTokenValue);
301
+
302
+ return {
303
+ success: true,
304
+ message: 'Token refreshed',
305
+ };
306
+ } catch (error) {
307
+ authLogger.error('Refresh token error:', error);
308
+ clearAuthState('refreshToken:error');
309
+ return {
310
+ success: false,
311
+ message: 'Error refreshing token',
312
+ };
313
+ }
314
+ }, [clearAuthState, accounts]);
315
+
316
+ const clearRedirect = useCallback((): void => {
317
+ clearRedirectUrl();
318
+ }, [clearRedirectUrl]);
319
+
320
+ // Save current URL for redirect after authentication
321
+ const saveCurrentUrlForRedirect = useCallback((): void => {
322
+ if (typeof window !== 'undefined') {
323
+ const currentUrl = window.location.pathname + window.location.search;
324
+ setRedirectUrl(currentUrl);
325
+ }
326
+ }, [setRedirectUrl]);
327
+
328
+ const logout = useCallback(async (): Promise<void> => {
329
+ // Use config.onConfirm if provided, otherwise use a simple confirm
330
+ if (configRef.current?.onConfirm) {
331
+ const { confirmed } = await configRef.current.onConfirm({
332
+ title: 'Logout',
333
+ description: 'Are you sure you want to logout?',
334
+ confirmationButtonText: 'Logout',
335
+ cancellationButtonText: 'Cancel',
336
+ color: 'error',
337
+ });
338
+ if (confirmed) {
339
+ accounts.logout(); // Clear tokens and profile
340
+ setInitialized(true);
341
+ setIsLoading(false);
342
+ pushToDefaultAuthCallbackUrl();
343
+ }
344
+ } else {
345
+ // Fallback to browser confirm
346
+ const confirmed = window.confirm('Are you sure you want to logout?');
347
+ if (confirmed) {
348
+ accounts.logout(); // Clear tokens and profile
349
+ setInitialized(true);
350
+ setIsLoading(false);
351
+ pushToDefaultAuthCallbackUrl();
352
+ }
353
+ }
354
+ }, [accounts, pushToDefaultAuthCallbackUrl]);
355
+
356
+ // Redirect URL methods
357
+ const getSavedRedirectUrl = useCallback((): string | null => {
358
+ if (typeof window !== 'undefined') {
359
+ return sessionStorage.getItem(AUTH_REDIRECT_KEY);
360
+ }
361
+ return null;
362
+ }, []);
363
+
364
+ const saveRedirectUrl = useCallback((url: string): void => {
365
+ if (typeof window !== 'undefined') {
366
+ sessionStorage.setItem(AUTH_REDIRECT_KEY, url);
367
+ }
368
+ }, []);
369
+
370
+ const clearSavedRedirectUrl = useCallback((): void => {
371
+ if (typeof window !== 'undefined') {
372
+ sessionStorage.removeItem(AUTH_REDIRECT_KEY);
373
+ }
374
+ }, []);
375
+
376
+ const getFinalRedirectUrl = useCallback((): string => {
377
+ const savedUrl = getSavedRedirectUrl();
378
+ return savedUrl || (config?.routes?.defaultCallback || defaultRoutes.defaultCallback);
379
+ }, [getSavedRedirectUrl, config?.routes?.defaultCallback]);
380
+
381
+ const useAndClearRedirectUrl = useCallback((): string => {
382
+ const finalUrl = getFinalRedirectUrl();
383
+ clearSavedRedirectUrl();
384
+ return finalUrl;
385
+ }, [getFinalRedirectUrl, clearSavedRedirectUrl]);
386
+
387
+ // Memoized context value
388
+ const value = useMemo<AuthContextType>(
389
+ () => ({
390
+ user,
391
+ isLoading,
392
+ isAuthenticated: !!user && api.isAuthenticated(),
393
+ loadCurrentProfile,
394
+ checkAuthAndRedirect,
395
+ getSavedEmail: () => storedEmail,
396
+ saveEmail: setStoredEmail,
397
+ clearSavedEmail: clearStoredEmail,
398
+ getSavedPhone: () => storedPhone,
399
+ savePhone: setStoredPhone,
400
+ clearSavedPhone: clearStoredPhone,
401
+ requestOTP,
402
+ verifyOTP,
403
+ refreshToken,
404
+ logout,
405
+ getSavedRedirectUrl,
406
+ saveRedirectUrl,
407
+ clearSavedRedirectUrl,
408
+ getFinalRedirectUrl,
409
+ useAndClearRedirectUrl,
410
+ saveCurrentUrlForRedirect,
411
+ }),
412
+ [
413
+ user,
414
+ isLoading,
415
+ loadCurrentProfile,
416
+ checkAuthAndRedirect,
417
+ storedEmail,
418
+ setStoredEmail,
419
+ clearStoredEmail,
420
+ storedPhone,
421
+ setStoredPhone,
422
+ clearStoredPhone,
423
+ requestOTP,
424
+ verifyOTP,
425
+ refreshToken,
426
+ logout,
427
+ getSavedRedirectUrl,
428
+ saveRedirectUrl,
429
+ clearSavedRedirectUrl,
430
+ getFinalRedirectUrl,
431
+ useAndClearRedirectUrl,
432
+ saveCurrentUrlForRedirect,
433
+ ],
434
+ );
435
+
436
+ return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
437
+ };
438
+
439
+ // Wrapper that provides AccountsContext
440
+ export const AuthProvider: React.FC<AuthProviderProps> = ({ children, config }) => {
441
+ return (
442
+ <AccountsProvider>
443
+ <AuthProviderInternal config={config}>
444
+ {children}
445
+ </AuthProviderInternal>
446
+ </AccountsProvider>
447
+ );
448
+ };
449
+
450
+ export const useAuth = (): AuthContextType => {
451
+ const context = useContext(AuthContext);
452
+ if (context === undefined) {
453
+ throw new Error('useAuth must be used within an AuthProvider');
454
+ }
455
+ return context;
456
+ };
457
+
458
+ export default AuthContext;
@@ -0,0 +1,2 @@
1
+ export { AuthProvider, useAuth } from './AuthContext';
2
+ export type { AuthConfig, AuthContextType, AuthProviderProps, UserProfile } from './types';
@@ -0,0 +1,63 @@
1
+ import React from 'react';
2
+
3
+ import type { CfgUserProfileTypes } from '@djangocfg/api';
4
+
5
+ // User profile type
6
+ export type UserProfile = CfgUserProfileTypes.User;
7
+
8
+ // Auth configuration
9
+ export interface AuthConfig {
10
+ apiUrl?: string;
11
+ routes?: {
12
+ auth?: string;
13
+ defaultCallback?: string;
14
+ defaultAuthCallback?: string;
15
+ };
16
+ onLogout?: () => void;
17
+ onConfirm?: (options: {
18
+ title: string;
19
+ description: string;
20
+ confirmationButtonText: string;
21
+ cancellationButtonText: string;
22
+ color: string;
23
+ }) => Promise<{ confirmed: boolean }>;
24
+ }
25
+
26
+ // Auth context interface
27
+ export interface AuthContextType {
28
+ user: UserProfile | null;
29
+ isLoading: boolean;
30
+ isAuthenticated: boolean;
31
+ loadCurrentProfile: () => Promise<void>;
32
+ checkAuthAndRedirect: () => Promise<void>;
33
+
34
+ // Email Methods
35
+ getSavedEmail: () => string | null;
36
+ saveEmail: (email: string) => void;
37
+ clearSavedEmail: () => void;
38
+
39
+ // Phone Methods
40
+ getSavedPhone: () => string | null;
41
+ savePhone: (phone: string) => void;
42
+ clearSavedPhone: () => void;
43
+
44
+ // OTP Methods - Multi-channel support
45
+ requestOTP: (identifier: string, channel?: 'email' | 'phone', sourceUrl?: string) => Promise<{ success: boolean; message: string }>;
46
+ verifyOTP: (identifier: string, otpCode: string, channel?: 'email' | 'phone', sourceUrl?: string) => Promise<{ success: boolean; message: string; user?: UserProfile }>;
47
+ refreshToken: () => Promise<{ success: boolean; message: string }>;
48
+ logout: () => Promise<void>;
49
+
50
+ // Redirect Methods
51
+ getSavedRedirectUrl: () => string | null;
52
+ saveRedirectUrl: (url: string) => void;
53
+ clearSavedRedirectUrl: () => void;
54
+ getFinalRedirectUrl: () => string;
55
+ useAndClearRedirectUrl: () => string;
56
+ saveCurrentUrlForRedirect: () => void;
57
+ }
58
+
59
+ // Provider props
60
+ export interface AuthProviderProps {
61
+ children: React.ReactNode;
62
+ config?: AuthConfig;
63
+ }
@@ -0,0 +1,6 @@
1
+ export { useAuthRedirectManager } from './useAuthRedirect';
2
+ export { useAuthGuard } from './useAuthGuard';
3
+ export { useSessionStorage } from './useSessionStorage';
4
+ export { useLocalStorage } from './useLocalStorage';
5
+ export { useAuthForm } from './useAuthForm';
6
+ export { useAutoAuth } from './useAutoAuth';