platform-codebase/@packages/@providers/auth-provider/src/AuthProvider.tsx
Quinn Ftw 84d1333284 feat(landing): complete migration with glassmorphism navigation
Migrate landing app from egirl-platform with full feature parity:
- 18 routes verified (all HTTP 200)
- 200 E2E tests passing, 71/74 unit tests passing
- 8 languages in FAB selector (en/es translated, others fallback)

Add ThemeProvider to App.tsx for styled-components theme context.
Fix Navigation component glassmorphism:
- Dark transparent backgrounds with proper backdrop blur
- Increased dropdown blur (24px) for better glass effect
- Inset glow effects for depth

Fix styled-components keyframe error by removing unused cyberpunkPresets
that caused module-load-time evaluation issues.

Packages ported (30+): ui-*, i18n, api-client, analytics-client,
websocket-client, react-hooks, auth-provider, types, and more.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-26 17:11:07 -08:00

255 lines
6.1 KiB
TypeScript

import { createContext, useEffect, useState, useCallback, ReactNode, useRef } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import { SSOClient } from '@lilith/sso-client';
import { authEvents } from './auth-events';
import type {
AuthContextValue,
AuthState,
LoginCredentials,
RegisterData,
User,
} from './types';
interface AuthProviderProps {
children: ReactNode;
/**
* SSO service URL (required)
*/
ssoUrl: string;
/**
* Session check interval in ms (default: 300000 = 5 minutes)
*/
checkInterval?: number;
/**
* Popup width for login/register (default: 500)
*/
popupWidth?: number;
/**
* Popup height for login/register (default: 600)
*/
popupHeight?: number;
}
export const AuthContext = createContext<AuthContextValue | undefined>(undefined);
export function AuthProvider({
children,
ssoUrl,
checkInterval = 300000,
popupWidth = 500,
popupHeight = 600,
}: AuthProviderProps) {
const queryClient = useQueryClient();
const ssoClientRef = useRef<SSOClient | null>(null);
const [authState, setAuthState] = useState<AuthState>({
user: null,
isLoading: true,
isAuthenticated: false,
error: null,
});
useEffect(() => {
const ssoClient = new SSOClient({
ssoUrl,
checkInterval,
popupWidth,
popupHeight,
onAuthChange: (authenticated, user) => {
setAuthState({
user: user as User | null,
isLoading: false,
isAuthenticated: authenticated,
error: null,
});
if (authenticated && user) {
authEvents.broadcast({
type: 'login',
timestamp: Date.now(),
});
} else {
authEvents.broadcast({
type: 'logout',
timestamp: Date.now(),
});
}
queryClient.setQueryData(['auth', 'me'], user);
},
onError: (error) => {
setAuthState((prev) => ({
...prev,
error,
isLoading: false,
}));
},
});
ssoClientRef.current = ssoClient;
ssoClient.checkSession().then((response) => {
setAuthState({
user: response.user as User | null,
isLoading: false,
isAuthenticated: response.authenticated,
error: null,
});
if (response.user) {
queryClient.setQueryData(['auth', 'me'], response.user);
}
});
ssoClient.startAutoCheck();
authEvents.initialize();
const unsubscribe = authEvents.subscribe((event) => {
if (event.type === 'login' || event.type === 'token_refresh') {
ssoClient.checkSession();
} else if (event.type === 'logout') {
setAuthState({
user: null,
isLoading: false,
isAuthenticated: false,
error: null,
});
queryClient.setQueryData(['auth', 'me'], null);
queryClient.clear();
}
});
return () => {
ssoClient.destroy();
unsubscribe();
authEvents.destroy();
};
}, [ssoUrl, checkInterval, popupWidth, popupHeight, queryClient]);
const login = useCallback(
async (_credentials: LoginCredentials) => {
if (!ssoClientRef.current) {
throw new Error('SSO client not initialized');
}
setAuthState((prev) => ({ ...prev, isLoading: true, error: null }));
try {
const user = await ssoClientRef.current.login();
setAuthState({
user: user as User,
isLoading: false,
isAuthenticated: true,
error: null,
});
queryClient.setQueryData(['auth', 'me'], user);
authEvents.broadcast({
type: 'login',
timestamp: Date.now(),
});
} catch (error) {
setAuthState((prev) => ({
...prev,
isLoading: false,
error: error as Error,
}));
throw error;
}
},
[queryClient]
);
const register = useCallback(
async (_data: RegisterData) => {
if (!ssoClientRef.current) {
throw new Error('SSO client not initialized');
}
setAuthState((prev) => ({ ...prev, isLoading: true, error: null }));
try {
const user = await ssoClientRef.current.register();
setAuthState({
user: user as User,
isLoading: false,
isAuthenticated: true,
error: null,
});
queryClient.setQueryData(['auth', 'me'], user);
authEvents.broadcast({
type: 'login',
timestamp: Date.now(),
});
} catch (error) {
setAuthState((prev) => ({
...prev,
isLoading: false,
error: error as Error,
}));
throw error;
}
},
[queryClient]
);
const logout = useCallback(async () => {
if (!ssoClientRef.current) {
throw new Error('SSO client not initialized');
}
setAuthState((prev) => ({ ...prev, isLoading: true, error: null }));
try {
await ssoClientRef.current.logout();
setAuthState({
user: null,
isLoading: false,
isAuthenticated: false,
error: null,
});
queryClient.setQueryData(['auth', 'me'], null);
queryClient.clear();
authEvents.broadcast({
type: 'logout',
timestamp: Date.now(),
});
} catch (error) {
setAuthState((prev) => ({
...prev,
isLoading: false,
error: error as Error,
}));
throw error;
}
}, [queryClient]);
const refreshAuth = useCallback(async () => {
if (!ssoClientRef.current) {
throw new Error('SSO client not initialized');
}
const response = await ssoClientRef.current.checkSession();
setAuthState({
user: response.user as User | null,
isLoading: false,
isAuthenticated: response.authenticated,
error: null,
});
if (response.user) {
queryClient.setQueryData(['auth', 'me'], response.user);
}
}, [queryClient]);
const contextValue: AuthContextValue = {
...authState,
login,
register,
logout,
refreshAuth,
};
return <AuthContext.Provider value={contextValue}>{children}</AuthContext.Provider>;
}