@djangocfg/layouts 2.0.7 → 2.0.8

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/README.md CHANGED
@@ -71,15 +71,21 @@ import { AppLayout } from '@djangocfg/layouts';
71
71
 
72
72
  ## Auth
73
73
 
74
- Complete authentication system with context, hooks, and middleware.
74
+ Complete authentication system with OTP and OAuth support.
75
75
 
76
76
  ```tsx
77
77
  import { AuthProvider, useAuth } from '@djangocfg/layouts/auth';
78
- import { AuthDialog } from '@djangocfg/layouts/snippets';
79
-
80
- <AuthProvider>
81
- <AuthDialog trigger={<Button>Sign In</Button>} />
82
- </AuthProvider>
78
+ import { AuthLayout, OAuthCallback } from '@djangocfg/layouts';
79
+
80
+ // Basic auth page with OTP and GitHub OAuth
81
+ <AuthLayout
82
+ enablePhoneAuth={false}
83
+ enableGithubAuth={true}
84
+ termsUrl="/legal/terms"
85
+ privacyUrl="/legal/privacy"
86
+ >
87
+ <h1>Welcome Back</h1>
88
+ </AuthLayout>
83
89
  ```
84
90
 
85
91
  | Export | Description |
@@ -89,8 +95,59 @@ import { AuthDialog } from '@djangocfg/layouts/snippets';
89
95
  | `useAuthGuard` | Route protection hook |
90
96
  | `useAuthRedirect` | Redirect hook for auth flows |
91
97
  | `useAutoAuth` | Auto-authentication hook |
98
+ | `useGithubAuth` | GitHub OAuth hook |
92
99
  | `authMiddleware` | Next.js middleware |
93
100
 
101
+ ### GitHub OAuth
102
+
103
+ Complete GitHub OAuth flow with automatic token handling:
104
+
105
+ ```tsx
106
+ // app/auth/page.tsx
107
+ import { AuthLayout } from '@djangocfg/layouts';
108
+
109
+ export default function AuthPage() {
110
+ return (
111
+ <AuthLayout
112
+ enableGithubAuth={true}
113
+ redirectUrl="/dashboard"
114
+ onOAuthSuccess={(user, isNewUser, provider) => console.log('Success!', user)}
115
+ onError={(error) => console.error(error)}
116
+ >
117
+ <h1>Sign In</h1>
118
+ </AuthLayout>
119
+ );
120
+ }
121
+ ```
122
+
123
+ **OAuth Flow:**
124
+ 1. User clicks "Continue with GitHub"
125
+ 2. Redirects to GitHub authorization page
126
+ 3. GitHub redirects to `/auth?provider=github&code=XXX&state=YYY`
127
+ 4. `AuthLayout` automatically handles callback and exchanges code for JWT tokens
128
+ 5. User is logged in and redirected to `redirectUrl`
129
+
130
+ > **Note:** OAuth callback handling is built into `AuthLayout` when `enableGithubAuth={true}`. No need to add `OAuthCallback` separately!
131
+
132
+ **Using the hook directly:**
133
+
134
+ ```tsx
135
+ import { useGithubAuth } from '@djangocfg/layouts';
136
+
137
+ function CustomGithubButton() {
138
+ const { isLoading, startGithubAuth } = useGithubAuth({
139
+ onSuccess: (user) => console.log('Logged in!', user),
140
+ onError: (error) => console.error(error),
141
+ });
142
+
143
+ return (
144
+ <button onClick={startGithubAuth} disabled={isLoading}>
145
+ {isLoading ? 'Connecting...' : 'Login with GitHub'}
146
+ </button>
147
+ );
148
+ }
149
+ ```
150
+
94
151
  ### Auth Context
95
152
 
96
153
  ```tsx
@@ -152,6 +209,7 @@ Analytics.setUser('user-123');
152
209
  | Category | Events |
153
210
  |----------|--------|
154
211
  | **Auth** | `AUTH_OTP_REQUEST`, `AUTH_LOGIN_SUCCESS`, `AUTH_OTP_VERIFY_FAIL`, `AUTH_LOGOUT`, `AUTH_SESSION_EXPIRED`, `AUTH_TOKEN_REFRESH` |
212
+ | **OAuth** | `AUTH_OAUTH_START`, `AUTH_OAUTH_SUCCESS`, `AUTH_OAUTH_FAIL` |
155
213
  | **Error** | `ERROR_BOUNDARY`, `ERROR_API`, `ERROR_VALIDATION`, `ERROR_NETWORK` |
156
214
  | **Navigation** | `NAV_ADMIN_ENTER`, `NAV_DASHBOARD_ENTER`, `NAV_PAGE_VIEW` |
157
215
  | **Engagement** | `THEME_CHANGE`, `SIDEBAR_TOGGLE`, `MOBILE_MENU_OPEN` |
@@ -162,6 +220,7 @@ Built-in tracking for:
162
220
  - **Page views** - on every route change
163
221
  - **User ID** - automatically set when user is authenticated
164
222
  - **Auth events** - login, logout, OTP, session expiry
223
+ - **OAuth events** - GitHub OAuth start, success, failure
165
224
  - **Errors** - React ErrorBoundary errors
166
225
 
167
226
  ## Snippets
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/layouts",
3
- "version": "2.0.7",
3
+ "version": "2.0.8",
4
4
  "description": "Simple, straightforward layout components for Next.js - import and use with props",
5
5
  "keywords": [
6
6
  "layouts",
@@ -92,9 +92,9 @@
92
92
  "check": "tsc --noEmit"
93
93
  },
94
94
  "peerDependencies": {
95
- "@djangocfg/api": "^1.4.37",
96
- "@djangocfg/centrifugo": "^1.4.37",
97
- "@djangocfg/ui": "^1.4.37",
95
+ "@djangocfg/api": "^1.4.38",
96
+ "@djangocfg/centrifugo": "^1.4.38",
97
+ "@djangocfg/ui": "^1.4.38",
98
98
  "@hookform/resolvers": "^5.2.0",
99
99
  "consola": "^3.4.2",
100
100
  "lucide-react": "^0.545.0",
@@ -114,7 +114,7 @@
114
114
  "uuid": "^11.1.0"
115
115
  },
116
116
  "devDependencies": {
117
- "@djangocfg/typescript-config": "^1.4.37",
117
+ "@djangocfg/typescript-config": "^1.4.38",
118
118
  "@types/node": "^24.7.2",
119
119
  "@types/react": "19.2.2",
120
120
  "@types/react-dom": "19.2.1",
@@ -6,6 +6,7 @@ export { useSessionStorage } from './useSessionStorage';
6
6
  export { useLocalStorage } from './useLocalStorage';
7
7
  export { useAuthForm } from './useAuthForm';
8
8
  export { useAutoAuth } from './useAutoAuth';
9
+ export { useGithubAuth, type UseGithubAuthOptions, type UseGithubAuthReturn } from './useGithubAuth';
9
10
  export {
10
11
  getCachedProfile,
11
12
  setCachedProfile,
@@ -0,0 +1,183 @@
1
+ 'use client';
2
+
3
+ import { useCallback, useState } from 'react';
4
+ import { useRouter } from 'next/navigation';
5
+
6
+ import { api } from '@djangocfg/api';
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 = useRouter();
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
+ const finalRedirectUrl = redirectUrl || '/dashboard';
159
+ router.push(finalRedirectUrl);
160
+
161
+ } catch (err) {
162
+ const errorMessage = err instanceof Error ? err.message : 'GitHub authentication failed';
163
+ authLogger.error('GitHub OAuth callback error:', err);
164
+ setError(errorMessage);
165
+ onError?.(errorMessage);
166
+
167
+ // Track OAuth error
168
+ Analytics.event(AnalyticsEvent.AUTH_OAUTH_FAIL, {
169
+ category: AnalyticsCategory.AUTH,
170
+ label: 'github',
171
+ });
172
+ } finally {
173
+ setIsLoading(false);
174
+ }
175
+ }, [onSuccess, onError, redirectUrl, router]);
176
+
177
+ return {
178
+ isLoading,
179
+ error,
180
+ startGithubAuth,
181
+ handleGithubCallback,
182
+ };
183
+ };
@@ -15,6 +15,7 @@ export const AuthProvider: React.FC<AuthProps> = ({
15
15
  termsUrl,
16
16
  privacyUrl,
17
17
  enablePhoneAuth = false, // Default to false for backward compatibility
18
+ enableGithubAuth = false, // Default to false for backward compatibility
18
19
  onIdentifierSuccess,
19
20
  onOTPSuccess,
20
21
  onError,
@@ -39,6 +40,7 @@ export const AuthProvider: React.FC<AuthProps> = ({
39
40
  termsUrl,
40
41
  privacyUrl,
41
42
  enablePhoneAuth,
43
+ enableGithubAuth,
42
44
  };
43
45
 
44
46
  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
@@ -1,19 +1,22 @@
1
1
  /**
2
2
  * Auth Layout
3
- *
4
- * Layout for authentication pages with OTP authentication (email/phone)
3
+ *
4
+ * Layout for authentication pages with OTP (email/phone) and OAuth (GitHub) support.
5
5
  * Supports two-step authentication flow: identifier input → OTP verification
6
- *
6
+ * Also handles OAuth callbacks automatically when enableGithubAuth is true.
7
+ *
7
8
  * @example
8
9
  * ```tsx
9
10
  * import { AuthLayout } from '@djangocfg/layouts';
10
- *
11
+ *
11
12
  * <AuthLayout
12
13
  * sourceUrl="https://example.com"
13
14
  * supportUrl="https://example.com/support"
14
15
  * termsUrl="https://example.com/terms"
15
16
  * privacyUrl="https://example.com/privacy"
16
- * enablePhoneAuth={true}
17
+ * enablePhoneAuth={false}
18
+ * enableGithubAuth={true}
19
+ * redirectUrl="/dashboard"
17
20
  * >
18
21
  * {/* Optional custom content above forms *\/}
19
22
  * </AuthLayout>
@@ -27,6 +30,7 @@ import React from 'react';
27
30
  import { AuthProvider, useAuthContext } from './AuthContext';
28
31
  import { IdentifierForm } from './IdentifierForm';
29
32
  import { OTPForm } from './OTPForm';
33
+ import { OAuthCallback } from './OAuthCallback';
30
34
  import { Suspense } from '../../components';
31
35
 
32
36
  import type { AuthProps } from './types';
@@ -34,8 +38,21 @@ import type { AuthProps } from './types';
34
38
  export type AuthLayoutProps = AuthProps;
35
39
 
36
40
  export const AuthLayout: React.FC<AuthProps> = (props) => {
41
+ const { enableGithubAuth, redirectUrl = '/dashboard', onOAuthSuccess, onError } = props;
42
+
37
43
  return (
38
44
  <Suspense>
45
+ {/* Handle OAuth callback when GitHub auth is enabled */}
46
+ {enableGithubAuth && (
47
+ <Suspense fallback={null}>
48
+ <OAuthCallback
49
+ redirectUrl={redirectUrl}
50
+ onSuccess={onOAuthSuccess ? (user, isNewUser) => onOAuthSuccess(user, isNewUser, 'github') : undefined}
51
+ onError={onError}
52
+ />
53
+ </Suspense>
54
+ )}
55
+
39
56
  <AuthProvider {...props}>
40
57
  <div
41
58
  className={`min-h-screen flex flex-col items-center justify-center bg-background py-6 px-4 sm:py-12 sm:px-6 lg:px-8 ${props.className || ''}`}
@@ -22,6 +22,7 @@ import {
22
22
 
23
23
  import { useAuthContext } from './AuthContext';
24
24
  import { AuthHelp } from './AuthHelp';
25
+ import { OAuthProviders } from './OAuthProviders';
25
26
 
26
27
  export const IdentifierForm: React.FC = () => {
27
28
  const {
@@ -328,6 +329,9 @@ export const IdentifierForm: React.FC = () => {
328
329
  </form>
329
330
  )}
330
331
 
332
+ {/* OAuth Providers (GitHub, etc.) */}
333
+ <OAuthProviders />
334
+
331
335
  {/* Help Section */}
332
336
  <AuthHelp />
333
337
  </CardContent>
@@ -0,0 +1,172 @@
1
+ 'use client';
2
+
3
+ import React, { useEffect, useState } from 'react';
4
+ import { useSearchParams } from 'next/navigation';
5
+ import { Loader2, AlertCircle, CheckCircle } from 'lucide-react';
6
+
7
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@djangocfg/ui/components';
8
+
9
+ import { useGithubAuth } from '../../auth/hooks';
10
+
11
+ export interface OAuthCallbackProps {
12
+ onSuccess?: (user: any, isNewUser: boolean, provider: string) => void;
13
+ onError?: (error: string) => void;
14
+ redirectUrl?: string;
15
+ }
16
+
17
+ type CallbackStatus = 'processing' | 'success' | 'error';
18
+
19
+ /**
20
+ * OAuth Callback Handler Component
21
+ *
22
+ * Processes OAuth callback from providers (GitHub, etc.).
23
+ * Reads code, state, and provider from URL params and exchanges for tokens.
24
+ *
25
+ * Usage:
26
+ * Place this component on your /auth page to handle OAuth callbacks.
27
+ *
28
+ * @example
29
+ * ```tsx
30
+ * // app/auth/page.tsx
31
+ * import { OAuthCallback, AuthLayout } from '@djangocfg/layouts';
32
+ *
33
+ * export default function AuthPage() {
34
+ * return (
35
+ * <>
36
+ * <OAuthCallback
37
+ * onSuccess={(user) => console.log('OAuth success:', user)}
38
+ * onError={(error) => console.error('OAuth error:', error)}
39
+ * />
40
+ * <AuthLayout enableGithubAuth>
41
+ * {/* Your auth content *\/}
42
+ * </AuthLayout>
43
+ * </>
44
+ * );
45
+ * }
46
+ * ```
47
+ */
48
+ export const OAuthCallback: React.FC<OAuthCallbackProps> = ({
49
+ onSuccess,
50
+ onError,
51
+ redirectUrl,
52
+ }) => {
53
+ const searchParams = useSearchParams();
54
+ const [status, setStatus] = useState<CallbackStatus | null>(null);
55
+ const [errorMessage, setErrorMessage] = useState<string | null>(null);
56
+
57
+ const provider = searchParams.get('provider');
58
+ const code = searchParams.get('code');
59
+ const state = searchParams.get('state');
60
+ const error = searchParams.get('error');
61
+ const errorDescription = searchParams.get('error_description');
62
+
63
+ const {
64
+ handleGithubCallback,
65
+ isLoading,
66
+ error: githubError,
67
+ } = useGithubAuth({
68
+ onSuccess: (user, isNewUser) => {
69
+ setStatus('success');
70
+ onSuccess?.(user, isNewUser, 'github');
71
+ },
72
+ onError: (err) => {
73
+ setStatus('error');
74
+ setErrorMessage(err);
75
+ onError?.(err);
76
+ },
77
+ redirectUrl,
78
+ });
79
+
80
+ // Process OAuth callback on mount
81
+ useEffect(() => {
82
+ // Check if this is an OAuth callback
83
+ if (!provider || !code || !state) {
84
+ // Not an OAuth callback, don't show anything
85
+ return;
86
+ }
87
+
88
+ // Check for error from provider
89
+ if (error) {
90
+ setStatus('error');
91
+ setErrorMessage(errorDescription || error);
92
+ onError?.(errorDescription || error);
93
+ return;
94
+ }
95
+
96
+ // Process based on provider
97
+ const processCallback = async () => {
98
+ setStatus('processing');
99
+
100
+ if (provider === 'github') {
101
+ await handleGithubCallback(code, state);
102
+ } else {
103
+ setStatus('error');
104
+ setErrorMessage(`Unsupported OAuth provider: ${provider}`);
105
+ onError?.(`Unsupported OAuth provider: ${provider}`);
106
+ }
107
+ };
108
+
109
+ processCallback();
110
+ // eslint-disable-next-line react-hooks/exhaustive-deps
111
+ }, [provider, code, state, error]);
112
+
113
+ // Don't render if not an OAuth callback
114
+ if (!provider || (!code && !error)) {
115
+ return null;
116
+ }
117
+
118
+ return (
119
+ <div className="fixed inset-0 bg-background/80 backdrop-blur-sm z-50 flex items-center justify-center">
120
+ <Card className="w-full max-w-md mx-4 shadow-lg">
121
+ <CardHeader className="text-center">
122
+ {status === 'processing' && (
123
+ <>
124
+ <div className="mx-auto w-12 h-12 bg-primary/10 rounded-full flex items-center justify-center mb-4">
125
+ <Loader2 className="w-6 h-6 text-primary animate-spin" />
126
+ </div>
127
+ <CardTitle>Signing you in...</CardTitle>
128
+ <CardDescription>
129
+ Please wait while we complete your {provider} authentication.
130
+ </CardDescription>
131
+ </>
132
+ )}
133
+
134
+ {status === 'success' && (
135
+ <>
136
+ <div className="mx-auto w-12 h-12 bg-green-100 dark:bg-green-900/20 rounded-full flex items-center justify-center mb-4">
137
+ <CheckCircle className="w-6 h-6 text-green-600 dark:text-green-400" />
138
+ </div>
139
+ <CardTitle>Success!</CardTitle>
140
+ <CardDescription>
141
+ You have been signed in successfully. Redirecting...
142
+ </CardDescription>
143
+ </>
144
+ )}
145
+
146
+ {status === 'error' && (
147
+ <>
148
+ <div className="mx-auto w-12 h-12 bg-destructive/10 rounded-full flex items-center justify-center mb-4">
149
+ <AlertCircle className="w-6 h-6 text-destructive" />
150
+ </div>
151
+ <CardTitle>Authentication Failed</CardTitle>
152
+ <CardDescription className="text-destructive">
153
+ {errorMessage || githubError || 'An error occurred during authentication.'}
154
+ </CardDescription>
155
+ </>
156
+ )}
157
+ </CardHeader>
158
+
159
+ {status === 'error' && (
160
+ <CardContent className="text-center">
161
+ <a
162
+ href="/auth"
163
+ className="text-primary hover:underline text-sm"
164
+ >
165
+ Try again
166
+ </a>
167
+ </CardContent>
168
+ )}
169
+ </Card>
170
+ </div>
171
+ );
172
+ };
@@ -0,0 +1,85 @@
1
+ 'use client';
2
+
3
+ import React from 'react';
4
+ import { Github, Loader2 } from 'lucide-react';
5
+
6
+ import { Button } from '@djangocfg/ui/components';
7
+
8
+ import { useGithubAuth } from '../../auth/hooks';
9
+ import { useAuthContext } from './AuthContext';
10
+
11
+ /**
12
+ * OAuth Providers Component
13
+ *
14
+ * Shows OAuth login buttons (GitHub, etc.) when enabled.
15
+ * Handles the OAuth flow initiation.
16
+ */
17
+ export const OAuthProviders: React.FC = () => {
18
+ const { enableGithubAuth, sourceUrl, error: contextError, setError } = useAuthContext();
19
+
20
+ const {
21
+ isLoading: isGithubLoading,
22
+ error: githubError,
23
+ startGithubAuth,
24
+ } = useGithubAuth({
25
+ sourceUrl,
26
+ onError: (error) => {
27
+ setError(error);
28
+ },
29
+ });
30
+
31
+ // Don't render if no OAuth providers are enabled
32
+ if (!enableGithubAuth) {
33
+ return null;
34
+ }
35
+
36
+ const error = githubError || contextError;
37
+
38
+ return (
39
+ <div className="space-y-4">
40
+ {/* Divider */}
41
+ <div className="relative">
42
+ <div className="absolute inset-0 flex items-center">
43
+ <div className="w-full border-t border-border" />
44
+ </div>
45
+ <div className="relative flex justify-center text-xs uppercase">
46
+ <span className="bg-card px-2 text-muted-foreground">
47
+ Or continue with
48
+ </span>
49
+ </div>
50
+ </div>
51
+
52
+ {/* OAuth Buttons */}
53
+ <div className="grid gap-3">
54
+ {enableGithubAuth && (
55
+ <Button
56
+ type="button"
57
+ variant="outline"
58
+ className="w-full h-11 text-base font-medium"
59
+ onClick={startGithubAuth}
60
+ disabled={isGithubLoading}
61
+ >
62
+ {isGithubLoading ? (
63
+ <div className="flex items-center gap-2">
64
+ <Loader2 className="w-5 h-5 animate-spin" />
65
+ Connecting...
66
+ </div>
67
+ ) : (
68
+ <div className="flex items-center gap-2">
69
+ <Github className="w-5 h-5" />
70
+ Continue with GitHub
71
+ </div>
72
+ )}
73
+ </Button>
74
+ )}
75
+ </div>
76
+
77
+ {/* Error Message */}
78
+ {error && (
79
+ <div className="text-sm text-destructive bg-destructive/10 p-3 rounded-md border border-destructive/20">
80
+ {error}
81
+ </div>
82
+ )}
83
+ </div>
84
+ );
85
+ };
@@ -12,6 +12,10 @@ export { AuthProvider as AuthLayoutProvider, useAuthContext } from './AuthContex
12
12
  export { IdentifierForm } from './IdentifierForm';
13
13
  export { OTPForm } from './OTPForm';
14
14
 
15
+ // OAuth
16
+ export { OAuthProviders } from './OAuthProviders';
17
+ export { OAuthCallback, type OAuthCallbackProps } from './OAuthCallback';
18
+
15
19
  // Help component
16
20
  export { AuthHelp } from './AuthHelp';
17
21
 
@@ -19,6 +19,7 @@ export interface AuthContextType {
19
19
  privacyUrl?: string;
20
20
  sourceUrl: string;
21
21
  enablePhoneAuth?: boolean;
22
+ enableGithubAuth?: boolean;
22
23
 
23
24
  // Form handlers
24
25
  setIdentifier: (identifier: string) => void;
@@ -49,8 +50,11 @@ export interface AuthProps {
49
50
  privacyUrl?: string;
50
51
  className?: string;
51
52
  enablePhoneAuth?: boolean; // Controls whether phone authentication is available
53
+ enableGithubAuth?: boolean; // Controls whether GitHub OAuth is available
54
+ redirectUrl?: string; // URL to redirect after successful auth (default: /dashboard)
52
55
  onIdentifierSuccess?: (identifier: string, channel: 'email' | 'phone') => void;
53
56
  onOTPSuccess?: () => void;
57
+ onOAuthSuccess?: (user: any, isNewUser: boolean, provider: string) => void;
54
58
  onError?: (message: string) => void;
55
59
  }
56
60
 
@@ -30,6 +30,11 @@ export const AnalyticsEvent = {
30
30
  AUTH_TOKEN_REFRESH: 'auth_token_refresh',
31
31
  AUTH_TOKEN_REFRESH_FAIL: 'auth_token_refresh_fail',
32
32
 
33
+ // OAuth Events
34
+ AUTH_OAUTH_START: 'auth_oauth_start',
35
+ AUTH_OAUTH_SUCCESS: 'auth_oauth_success',
36
+ AUTH_OAUTH_FAIL: 'auth_oauth_fail',
37
+
33
38
  // Error Events
34
39
  ERROR_BOUNDARY: 'error_boundary',
35
40
  ERROR_API: 'error_api',