@insforge/react 0.4.8 → 0.4.10

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.
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/provider/InsforgeProvider.tsx","../src/hooks/useAuth.ts","../src/hooks/useUser.ts","../src/hooks/usePublicAuthConfig.ts"],"names":["createContext","useContext","useState","useEffect"],"mappings":";;;;;;AAmEA,IAAM,eAAA,GAAkBA,oBAAgD,MAAS,CAAA;AAyf1E,SAAS,WAAA,GAAoC;AAClD,EAAA,MAAM,OAAA,GAAUC,iBAAW,eAAe,CAAA;AAC1C,EAAA,IAAI,CAAC,OAAA,EAAS;AACZ,IAAA,MAAM,IAAI,MAAM,kDAAkD,CAAA;AAAA,EACpE;AACA,EAAA,OAAO,OAAA;AACT;;;AC9hBO,SAAS,OAAA,GAAU;AACxB,EAAA,MAAM,EAAE,MAAA,EAAQ,MAAA,EAAQ,SAAS,QAAA,EAAU,UAAA,KAAe,WAAA,EAAY;AACtE,EAAA,OAAO,EAAE,MAAA,EAAQ,MAAA,EAAQ,OAAA,EAAS,UAAU,UAAA,EAAW;AACzD;;;ACNO,SAAS,OAAA,GAAU;AACxB,EAAA,MAAM,EAAE,IAAA,EAAM,QAAA,EAAU,UAAA,EAAY,OAAA,KAAY,WAAA,EAAY;AAC5D,EAAA,OAAO,EAAE,IAAA,EAAM,QAAA,EAAU,UAAA,EAAY,OAAA,EAAQ;AAC/C;ACCO,SAAS,mBAAA,GAGd;AACA,EAAA,MAAM,EAAE,mBAAA,EAAoB,GAAI,WAAA,EAAY;AAC5C,EAAA,MAAM,CAAC,UAAA,EAAY,aAAa,CAAA,GAAIC,eAA6C,IAAI,CAAA;AACrF,EAAA,MAAM,CAAC,QAAA,EAAU,WAAW,CAAA,GAAIA,eAAS,KAAK,CAAA;AAE9C,EAAAC,gBAAU,MAAM;AACd,IAAA,eAAe,WAAA,GAAc;AAC3B,MAAA,MAAM,MAAA,GAAS,MAAM,mBAAA,EAAoB;AACzC,MAAA,IAAI,MAAA,EAAQ;AACV,QAAA,aAAA,CAAc,MAAM,CAAA;AAAA,MACtB,CAAA,MAAO;AACL,QAAA,OAAA,CAAQ,MAAM,wDAAwD,CAAA;AACtE,QAAA,aAAA,CAAc,IAAI,CAAA;AAAA,MACpB;AACA,MAAA,WAAA,CAAY,IAAI,CAAA;AAAA,IAClB;AAEA,IAAA,WAAA,EAAY;AAAA,EACd,CAAA,EAAG,CAAC,mBAAmB,CAAC,CAAA;AAExB,EAAA,OAAO,EAAE,YAAY,QAAA,EAAS;AAChC","file":"hooks.cjs","sourcesContent":["'use client';\n\nimport {\n createContext,\n useContext,\n useEffect,\n useState,\n useCallback,\n useRef,\n type ReactNode,\n} from 'react';\nimport { UserSchema, createClient } from '@insforge/sdk';\nimport type { InsforgeUser, OAuthProvider } from '../types';\nimport {\n CreateSessionResponse,\n CreateUserResponse,\n GetPublicAuthConfigResponse,\n ResetPasswordResponse,\n} from '@insforge/shared-schemas';\n\ninterface InsforgeContextValue {\n // Auth state\n user: InsforgeUser | null;\n isLoaded: boolean;\n isSignedIn: boolean;\n\n // Auth methods\n setUser: (user: InsforgeUser | null) => void;\n signIn: (\n email: string,\n password: string\n ) => Promise<CreateSessionResponse | { error: string; statusCode?: number; errorCode?: string }>;\n signUp: (\n email: string,\n password: string\n ) => Promise<CreateUserResponse | { error: string; statusCode?: number; errorCode?: string }>;\n signOut: () => Promise<void>;\n updateUser: (data: Partial<InsforgeUser>) => Promise<void>;\n reloadAuth: () => Promise<{ success: boolean; error?: string }>;\n\n // Email verification methods\n sendVerificationEmail: (email: string) => Promise<{ success: boolean; message: string } | null>;\n sendResetPasswordEmail: (email: string) => Promise<{ success: boolean; message: string } | null>;\n resetPassword: (token: string, newPassword: string) => Promise<ResetPasswordResponse | null>;\n verifyEmail: (\n otp: string,\n email?: string\n ) => Promise<{\n accessToken: string;\n user?: UserSchema;\n redirectTo?: string;\n error?: { message: string };\n } | null>;\n exchangeResetPasswordToken: (\n email: string,\n code: string\n ) => Promise<{ token: string; expiresAt?: string } | { error: { message: string } }>;\n loginWithOAuth: (\n provider: OAuthProvider,\n redirectTo: string\n ) => Promise<{ url?: string | undefined; provider?: string | undefined }>;\n // Public auth config\n getPublicAuthConfig: () => Promise<GetPublicAuthConfigResponse | null>;\n // Base config\n baseUrl: string;\n}\n\nconst InsforgeContext = createContext<InsforgeContextValue | undefined>(undefined);\n\nexport interface InsforgeProviderProps {\n children: ReactNode;\n baseUrl: string;\n /**\n * URL to redirect to after successful sign in (when token is detected in URL)\n * @default '/'\n */\n afterSignInUrl?: string;\n onAuthChange?: (user: InsforgeUser | null) => void;\n onSignIn?: (authToken: string) => Promise<void>;\n onSignOut?: () => Promise<void>;\n}\n\n/**\n * Unified Insforge Provider - manages authentication state and configuration\n *\n * Manages user authentication state and provides all necessary context to child components.\n * Works with any React framework (Next.js, Vite, Remix, etc.).\n *\n * @example\n * ```tsx\n * // Basic usage (React/Vite)\n * import { InsforgeProvider } from '@insforge/react';\n *\n * export default function App() {\n * return (\n * <InsforgeProvider\n * baseUrl={process.env.VITE_INSFORGE_BASE_URL}\n * afterSignInUrl=\"/dashboard\"\n * >\n * {children}\n * </InsforgeProvider>\n * );\n * }\n * ```\n *\n * @example\n * ```tsx\n * // With cookie sync (Next.js optimization)\n * <InsforgeProvider\n * baseUrl={baseUrl}\n * onSignIn={async (authToken) => {\n * await signIn(authToken);\n * }}\n * onSignOut={async () => {\n * await signOut();\n * }}\n * >\n * {children}\n * </InsforgeProvider>\n * ```\n */\nexport function InsforgeProvider({\n children,\n baseUrl,\n afterSignInUrl = '/',\n onAuthChange,\n onSignIn,\n onSignOut,\n}: InsforgeProviderProps) {\n // Auth state\n const [user, setUser] = useState<InsforgeUser | null>(null);\n const [isLoaded, setIsLoaded] = useState(false);\n\n const refreshIntervalRef = useRef<NodeJS.Timeout | null>(null);\n const hasProcessedCallbackRef = useRef(false);\n\n // Initialize SDK client with lazy initialization - only runs once\n const [insforge] = useState(() => createClient({ baseUrl }));\n\n // Load auth state - returns explicit success/error status\n const loadAuthState = useCallback(async (): Promise<{\n success: boolean;\n error?: string;\n }> => {\n try {\n // Use SDK's getCurrentSession() to check for existing session\n const sessionResult = insforge.auth.getCurrentSession();\n const session = sessionResult.data?.session;\n const token = session?.accessToken || null;\n\n if (!token) {\n // No token, user is not authenticated\n setUser(null);\n if (onAuthChange) {\n onAuthChange(null);\n }\n setIsLoaded(true);\n return { success: false, error: 'no_session' };\n }\n\n const userResult = await insforge.auth.getCurrentUser();\n\n if (userResult.data) {\n // Token is valid, update user state with fresh data\n const profile = userResult.data.profile;\n const userData: InsforgeUser = {\n id: userResult.data.user.id,\n email: userResult.data.user.email,\n name: (profile?.nickname as string | undefined) || '',\n avatarUrl: (profile?.avatarUrl as string | undefined) || '',\n };\n\n setUser(userData);\n\n if (onAuthChange) {\n onAuthChange(userData);\n }\n setIsLoaded(true);\n return { success: true };\n } else {\n // Token invalid or expired\n await insforge.auth.signOut();\n\n // Clear cookie if function provided\n if (onSignOut) {\n try {\n await onSignOut();\n } catch (error: unknown) {\n if (error instanceof Error) {\n console.error('[InsforgeProvider] Error clearing cookie:', error.message);\n }\n }\n }\n\n setUser(null);\n if (onAuthChange) {\n onAuthChange(null);\n }\n setIsLoaded(true);\n return { success: false, error: 'invalid_token' };\n }\n } catch (error) {\n // Token validation failed\n console.error('[InsforgeProvider] Token validation failed:', error);\n\n await insforge.auth.signOut();\n\n // Clear cookie if function provided\n if (onSignOut) {\n try {\n await onSignOut();\n } catch (error: unknown) {\n if (error instanceof Error) {\n console.error('[InsforgeProvider] Error clearing cookie:', error.message);\n }\n }\n }\n\n setUser(null);\n if (onAuthChange) {\n onAuthChange(null);\n }\n setIsLoaded(true);\n\n return {\n success: false,\n error: error instanceof Error ? error.message : 'Authentication failed',\n };\n }\n }, [insforge, onAuthChange, onSignOut]);\n\n useEffect(() => {\n // Run loadAuthState only once on mount\n loadAuthState();\n\n // Capture the ref value when effect runs\n const intervalId = refreshIntervalRef.current;\n\n return () => {\n // Use the captured value in cleanup\n if (intervalId) {\n clearInterval(intervalId);\n }\n };\n }, [loadAuthState]);\n\n // Handle authentication callback from URL\n useEffect(() => {\n // Only run once and only after auth is loaded\n if (!isLoaded || hasProcessedCallbackRef.current) {\n return;\n }\n\n const searchParams = new URLSearchParams(window.location.search);\n const accessToken = searchParams.get('access_token');\n\n if (accessToken && !!user) {\n // Mark as processed\n hasProcessedCallbackRef.current = true;\n\n // Clean URL by removing query parameters\n const url = new URL(window.location.href);\n url.search = '';\n window.history.replaceState({}, '', url.toString());\n\n // Redirect to afterSignInUrl\n setTimeout(() => {\n window.location.href = afterSignInUrl;\n }, 100);\n }\n }, [isLoaded, user, afterSignInUrl]);\n\n const getPublicAuthConfig = useCallback(async () => {\n try {\n const result = await insforge.auth.getPublicAuthConfig();\n if (result.data) {\n return result.data;\n } else {\n console.error('[InsforgeProvider] Failed to get public auth config:', result.error);\n return null;\n }\n } catch (error) {\n console.error('[InsforgeProvider] Failed to get public auth config:', error);\n return null;\n }\n }, [insforge]);\n\n /**\n * Helper function to handle successful authentication\n */\n const handleAuthSuccess = useCallback(\n async (authToken: string, fallbackUser?: { id?: string; email?: string; name?: string }) => {\n const userResult = await insforge.auth.getCurrentUser();\n\n if (userResult.data) {\n const profile = userResult.data.profile;\n const userData: InsforgeUser = {\n id: userResult.data.user.id,\n email: userResult.data.user.email,\n name: (profile?.nickname as string | undefined) || '',\n avatarUrl: (profile?.avatarUrl as string | undefined) || '',\n };\n\n setUser(userData);\n\n if (onAuthChange) {\n onAuthChange(userData);\n }\n\n // Try to sync token to cookie if function provided\n if (onSignIn) {\n try {\n await onSignIn(authToken);\n } catch (error: unknown) {\n if (error instanceof Error) {\n console.error('[InsforgeProvider] Error syncing token to cookie:', error.message);\n }\n }\n }\n } else if (fallbackUser) {\n // Fallback to basic user data if getCurrentUser fails\n const userData: InsforgeUser = {\n id: fallbackUser.id || '',\n email: fallbackUser.email || '',\n name: fallbackUser.name || '',\n avatarUrl: '',\n };\n\n setUser(userData);\n\n if (onAuthChange) {\n onAuthChange(userData);\n }\n }\n },\n [insforge, onAuthChange, onSignIn]\n );\n\n const signIn = useCallback(\n async (email: string, password: string) => {\n const sdkResult = await insforge.auth.signInWithPassword({\n email,\n password,\n });\n\n if (sdkResult.data) {\n await handleAuthSuccess(\n sdkResult.data.accessToken || '',\n sdkResult.data.user\n ? {\n id: sdkResult.data.user.id,\n email: sdkResult.data.user.email,\n name: sdkResult.data.user.name,\n }\n : undefined\n );\n return sdkResult.data;\n } else {\n // Return the full error object to preserve statusCode and other properties\n return {\n error: sdkResult.error?.message || 'Invalid email or password',\n statusCode: sdkResult.error?.statusCode,\n errorCode: sdkResult.error?.error,\n };\n }\n },\n [insforge, handleAuthSuccess]\n );\n\n const signUp = useCallback(\n async (email: string, password: string) => {\n const sdkResult = await insforge.auth.signUp({ email, password });\n\n if (sdkResult.data) {\n // Only set auth state if we got a token back (email verification might be required)\n if (sdkResult.data.accessToken) {\n await handleAuthSuccess(\n sdkResult.data.accessToken,\n sdkResult.data.user\n ? {\n id: sdkResult.data.user.id,\n email: sdkResult.data.user.email,\n name: sdkResult.data.user.name,\n }\n : undefined\n );\n }\n return sdkResult.data;\n } else {\n // Return the full error object to preserve statusCode and other properties\n return {\n error: sdkResult.error?.message || 'Sign up failed',\n statusCode: sdkResult.error?.statusCode,\n errorCode: sdkResult.error?.error,\n };\n }\n },\n [insforge, handleAuthSuccess]\n );\n\n const signOut = useCallback(async () => {\n await insforge.auth.signOut();\n\n // Clear cookie if function provided\n if (onSignOut) {\n try {\n await onSignOut();\n } catch (error: unknown) {\n if (error instanceof Error) {\n console.error('[InsforgeProvider] Error clearing cookie:', error.message);\n }\n }\n }\n\n // Clear refresh interval if exists\n if (refreshIntervalRef.current) {\n clearInterval(refreshIntervalRef.current);\n }\n\n setUser(null);\n if (onAuthChange) {\n onAuthChange(null);\n }\n }, [insforge, onAuthChange, onSignOut]);\n\n const updateUser = useCallback(\n async (data: Partial<InsforgeUser>) => {\n if (!user) {\n throw new Error('No user signed in');\n }\n\n const profileUpdate: Record<string, string | undefined> = {\n nickname: data.name,\n avatarUrl: data.avatarUrl,\n };\n\n const result = await insforge.auth.setProfile(profileUpdate);\n\n if (result.data) {\n const userResult = await insforge.auth.getCurrentUser();\n if (userResult.data) {\n const profile = userResult.data.profile;\n const updatedUser: InsforgeUser = {\n id: userResult.data.user.id,\n email: userResult.data.user.email,\n name: profile?.nickname || '',\n avatarUrl: profile?.avatarUrl || '',\n };\n setUser(updatedUser);\n if (onAuthChange) {\n onAuthChange(updatedUser);\n }\n }\n }\n },\n [user, onAuthChange, insforge]\n );\n\n const sendVerificationEmail = useCallback(\n async (email: string) => {\n const sdkResult = await insforge.auth.sendVerificationEmail({ email });\n return sdkResult.data;\n },\n [insforge]\n );\n\n const sendResetPasswordEmail = useCallback(\n async (email: string) => {\n const sdkResult = await insforge.auth.sendResetPasswordEmail({ email });\n return sdkResult.data;\n },\n [insforge]\n );\n\n const resetPassword = useCallback(\n async (token: string, newPassword: string) => {\n const sdkResult = await insforge.auth.resetPassword({\n newPassword,\n otp: token,\n });\n return sdkResult.data;\n },\n [insforge]\n );\n\n const verifyEmail = useCallback(\n async (otp: string, email?: string) => {\n const sdkResult = await insforge.auth.verifyEmail({ otp, email: email || undefined });\n if (sdkResult.data) {\n return sdkResult.data;\n } else {\n return {\n accessToken: '',\n error: {\n message: sdkResult.error?.message || 'Email verification failed',\n },\n };\n }\n },\n [insforge]\n );\n\n const exchangeResetPasswordToken = useCallback(\n async (email: string, code: string) => {\n const sdkResult = await insforge.auth.exchangeResetPasswordToken({ email, code });\n if (sdkResult.data) {\n return sdkResult.data;\n } else {\n return {\n error: {\n message: sdkResult.error?.message || 'Failed to exchange reset password token',\n },\n };\n }\n },\n [insforge]\n );\n\n const loginWithOAuth = useCallback(\n async (provider: OAuthProvider, redirectTo: string) => {\n const sdkResult = await insforge.auth.signInWithOAuth({ provider, redirectTo });\n return sdkResult.data;\n },\n [insforge]\n );\n\n return (\n <InsforgeContext.Provider\n value={{\n user,\n isLoaded,\n isSignedIn: !!user,\n setUser,\n signIn,\n signUp,\n signOut,\n updateUser,\n reloadAuth: loadAuthState,\n baseUrl,\n sendVerificationEmail,\n sendResetPasswordEmail,\n resetPassword,\n verifyEmail,\n exchangeResetPasswordToken,\n getPublicAuthConfig,\n loginWithOAuth,\n }}\n >\n {children}\n </InsforgeContext.Provider>\n );\n}\n\n/**\n * Hook to access Insforge context\n *\n * @example\n * ```tsx\n * function MyComponent() {\n * const { user, isSignedIn, signOut } = useInsforge();\n *\n * if (!isSignedIn) return <SignIn />;\n *\n * return (\n * <div>\n * <p>Welcome {user.email}</p>\n * <button onClick={signOut}>Sign Out</button>\n * </div>\n * );\n * }\n * ```\n */\nexport function useInsforge(): InsforgeContextValue {\n const context = useContext(InsforgeContext);\n if (!context) {\n throw new Error('useInsforge must be used within InsforgeProvider');\n }\n return context;\n}\n","import { useInsforge } from '../provider/InsforgeProvider';\n\n/**\n * Hook to access authentication methods\n *\n * @returns Object containing:\n * - `signIn`: Function to sign in with email and password\n * - `signUp`: Function to sign up with email and password\n * - `signOut`: Function to sign out the current user\n * - `isLoaded`: Boolean indicating if auth state has been loaded\n * - `isSignedIn`: Boolean indicating if user is currently signed in\n *\n * @example\n * ```tsx\n * function LoginForm() {\n * const { signIn, signUp, signOut, isLoaded, isSignedIn } = useAuth();\n *\n * async function handleLogin(email: string, password: string) {\n * try {\n * await signIn(email, password);\n * // User is now signed in\n * } catch (error) {\n * console.error('Sign in failed:', error);\n * }\n * }\n *\n * if (!isLoaded) return <div>Loading...</div>;\n *\n * return (\n * <form onSubmit={(e) => { e.preventDefault(); handleLogin(email, password); }}>\n * {/* form fields *\\/}\n * </form>\n * );\n * }\n * ```\n */\nexport function useAuth() {\n const { signIn, signUp, signOut, isLoaded, isSignedIn } = useInsforge();\n return { signIn, signUp, signOut, isLoaded, isSignedIn };\n}\n","import { useInsforge } from '../provider/InsforgeProvider';\n\n/**\n * Hook to access current user data\n *\n * @returns Object containing:\n * - `user`: Current user object (InsforgeUser | null)\n * - `isLoaded`: Boolean indicating if auth state has been loaded\n * - `updateUser`: Function to update user profile data\n * - `setUser`: Internal function to manually set user state\n *\n * @example\n * ```tsx\n * function UserProfile() {\n * const { user, isLoaded, updateUser } = useUser();\n *\n * if (!isLoaded) return <div>Loading...</div>;\n * if (!user) return <div>Not signed in</div>;\n *\n * async function handleUpdate(name: string) {\n * await updateUser({ name });\n * }\n *\n * return (\n * <div>\n * <p>Email: {user.email}</p>\n * {user.name && <p>Name: {user.name}</p>}\n * {user.avatarUrl && <img src={user.avatarUrl} alt=\"Avatar\" />}\n * </div>\n * );\n * }\n * ```\n */\nexport function useUser() {\n const { user, isLoaded, updateUser, setUser } = useInsforge();\n return { user, isLoaded, updateUser, setUser };\n}\n","import { useState, useEffect } from 'react';\nimport type { GetPublicAuthConfigResponse } from '@insforge/shared-schemas';\nimport { useInsforge } from '../provider/InsforgeProvider';\n\n/**\n * Hook to get all public authentication configuration (OAuth + Email) from Insforge backend\n *\n * **IMPORTANT: This hook should ONLY be used in SignIn and SignUp components.**\n *\n * This hook lazily fetches all public authentication configuration from the backend\n * only when the component mounts. Using it in other components will cause unnecessary\n * API calls on every page load.\n *\n * @returns Object containing OAuth providers, email auth config, and loading state\n * - `oauthProviders`: Array of enabled OAuth provider names (e.g., ['google', 'github'])\n * - `authConfig`: Email authentication configuration object with password rules\n * - `isLoaded`: Boolean indicating if the config has been fetched\n *\n * @example\n * ```tsx\n * // ✅ Correct usage - only in SignIn/SignUp components\n * function SignUp() {\n * const { oauthProviders, authConfig, isLoaded } = usePublicAuthConfig();\n *\n * if (!isLoaded) return <div>Loading...</div>;\n *\n * return (\n * <div>\n * <p>OAuth providers: {oauthProviders.length}</p>\n * <p>Password min length: {authConfig?.passwordMinLength}</p>\n * </div>\n * );\n * }\n * ```\n *\n * @requires Must be used within InsforgeProvider\n */\nexport function usePublicAuthConfig(): {\n authConfig: GetPublicAuthConfigResponse | null;\n isLoaded: boolean;\n} {\n const { getPublicAuthConfig } = useInsforge();\n const [authConfig, setAuthConfig] = useState<GetPublicAuthConfigResponse | null>(null);\n const [isLoaded, setIsLoaded] = useState(false);\n\n useEffect(() => {\n async function fetchConfig() {\n const result = await getPublicAuthConfig();\n if (result) {\n setAuthConfig(result);\n } else {\n console.error('[usePublicAuthConfig] Failed to get public auth config');\n setAuthConfig(null);\n }\n setIsLoaded(true);\n }\n\n fetchConfig();\n }, [getPublicAuthConfig]);\n\n return { authConfig, isLoaded };\n}\n"]}
1
+ {"version":3,"sources":["../src/provider/InsforgeProvider.tsx","../src/hooks/useAuth.ts","../src/hooks/useUser.ts","../src/hooks/usePublicAuthConfig.ts"],"names":["createContext","useContext","useState","useEffect"],"mappings":";;;;;;AAgEA,IAAM,eAAA,GAAkBA,oBAAgD,MAAS,CAAA;AA8f1E,SAAS,WAAA,GAAoC;AAClD,EAAA,MAAM,OAAA,GAAUC,iBAAW,eAAe,CAAA;AAC1C,EAAA,IAAI,CAAC,OAAA,EAAS;AACZ,IAAA,MAAM,IAAI,MAAM,kDAAkD,CAAA;AAAA,EACpE;AACA,EAAA,OAAO,OAAA;AACT;;;AChiBO,SAAS,OAAA,GAAU;AACxB,EAAA,MAAM,EAAE,MAAA,EAAQ,MAAA,EAAQ,SAAS,QAAA,EAAU,UAAA,KAAe,WAAA,EAAY;AACtE,EAAA,OAAO,EAAE,MAAA,EAAQ,MAAA,EAAQ,OAAA,EAAS,UAAU,UAAA,EAAW;AACzD;;;ACNO,SAAS,OAAA,GAAU;AACxB,EAAA,MAAM,EAAE,IAAA,EAAM,QAAA,EAAU,UAAA,EAAY,OAAA,KAAY,WAAA,EAAY;AAC5D,EAAA,OAAO,EAAE,IAAA,EAAM,QAAA,EAAU,UAAA,EAAY,OAAA,EAAQ;AAC/C;ACCO,SAAS,mBAAA,GAGd;AACA,EAAA,MAAM,EAAE,mBAAA,EAAoB,GAAI,WAAA,EAAY;AAC5C,EAAA,MAAM,CAAC,UAAA,EAAY,aAAa,CAAA,GAAIC,eAA6C,IAAI,CAAA;AACrF,EAAA,MAAM,CAAC,QAAA,EAAU,WAAW,CAAA,GAAIA,eAAS,KAAK,CAAA;AAE9C,EAAAC,gBAAU,MAAM;AACd,IAAA,eAAe,WAAA,GAAc;AAC3B,MAAA,MAAM,MAAA,GAAS,MAAM,mBAAA,EAAoB;AACzC,MAAA,IAAI,MAAA,EAAQ;AACV,QAAA,aAAA,CAAc,MAAM,CAAA;AAAA,MACtB,CAAA,MAAO;AACL,QAAA,OAAA,CAAQ,MAAM,wDAAwD,CAAA;AACtE,QAAA,aAAA,CAAc,IAAI,CAAA;AAAA,MACpB;AACA,MAAA,WAAA,CAAY,IAAI,CAAA;AAAA,IAClB;AAEA,IAAA,WAAA,EAAY;AAAA,EACd,CAAA,EAAG,CAAC,mBAAmB,CAAC,CAAA;AAExB,EAAA,OAAO,EAAE,YAAY,QAAA,EAAS;AAChC","file":"hooks.cjs","sourcesContent":["'use client';\n\nimport {\n createContext,\n useContext,\n useEffect,\n useState,\n useCallback,\n useRef,\n type ReactNode,\n} from 'react';\nimport { UserSchema, createClient } from '@insforge/sdk';\nimport type { InsforgeUser, OAuthProvider } from '../types';\nimport {\n CreateSessionResponse,\n CreateUserResponse,\n GetPublicAuthConfigResponse,\n ResetPasswordResponse,\n} from '@insforge/shared-schemas';\n\ninterface InsforgeContextValue {\n // Auth state\n user: InsforgeUser | null;\n isLoaded: boolean;\n isSignedIn: boolean;\n\n // Auth methods\n setUser: (user: InsforgeUser | null) => void;\n signIn: (\n email: string,\n password: string\n ) => Promise<CreateSessionResponse | { error: string; statusCode?: number; errorCode?: string }>;\n signUp: (\n email: string,\n password: string\n ) => Promise<CreateUserResponse | { error: string; statusCode?: number; errorCode?: string }>;\n signOut: () => Promise<void>;\n updateUser: (data: Partial<InsforgeUser>) => Promise<void>;\n reloadAuth: () => Promise<{ success: boolean; error?: string }>;\n\n // Email verification methods\n sendVerificationEmail: (email: string) => Promise<{ success: boolean; message: string } | null>;\n sendResetPasswordEmail: (email: string) => Promise<{ success: boolean; message: string } | null>;\n resetPassword: (token: string, newPassword: string) => Promise<ResetPasswordResponse | null>;\n verifyEmail: (\n otp: string,\n email?: string\n ) => Promise<{\n accessToken: string;\n user?: UserSchema;\n redirectTo?: string;\n error?: { message: string };\n } | null>;\n exchangeResetPasswordToken: (\n email: string,\n code: string\n ) => Promise<{ token: string; expiresAt?: string } | { error: { message: string } }>;\n loginWithOAuth: (provider: OAuthProvider, redirectTo: string) => Promise<void>;\n // Public auth config\n getPublicAuthConfig: () => Promise<GetPublicAuthConfigResponse | null>;\n // Base config\n baseUrl: string;\n}\n\nconst InsforgeContext = createContext<InsforgeContextValue | undefined>(undefined);\n\nexport interface InsforgeProviderProps {\n children: ReactNode;\n baseUrl: string;\n /**\n * URL to redirect to after successful sign in (when token is detected in URL)\n * @default '/'\n */\n afterSignInUrl?: string;\n onAuthChange?: (user: InsforgeUser | null) => void;\n onSignIn?: (authToken: string) => Promise<void>;\n onSignOut?: () => Promise<void>;\n}\n\n/**\n * Unified Insforge Provider - manages authentication state and configuration\n *\n * Manages user authentication state and provides all necessary context to child components.\n * Works with any React framework (Next.js, Vite, Remix, etc.).\n *\n * @example\n * ```tsx\n * // Basic usage (React/Vite)\n * import { InsforgeProvider } from '@insforge/react';\n *\n * export default function App() {\n * return (\n * <InsforgeProvider\n * baseUrl={process.env.VITE_INSFORGE_BASE_URL}\n * afterSignInUrl=\"/dashboard\"\n * >\n * {children}\n * </InsforgeProvider>\n * );\n * }\n * ```\n *\n * @example\n * ```tsx\n * // With cookie sync (Next.js optimization)\n * <InsforgeProvider\n * baseUrl={baseUrl}\n * onSignIn={async (authToken) => {\n * await signIn(authToken);\n * }}\n * onSignOut={async () => {\n * await signOut();\n * }}\n * >\n * {children}\n * </InsforgeProvider>\n * ```\n */\nexport function InsforgeProvider({\n children,\n baseUrl,\n afterSignInUrl = '/',\n onAuthChange,\n onSignIn,\n onSignOut,\n}: InsforgeProviderProps) {\n // Auth state\n const [user, setUser] = useState<InsforgeUser | null>(null);\n const [isLoaded, setIsLoaded] = useState(false);\n\n const refreshIntervalRef = useRef<NodeJS.Timeout | null>(null);\n const hasProcessedCallbackRef = useRef(false);\n\n // Initialize SDK client with lazy initialization - only runs once\n const [insforge] = useState(() => createClient({ baseUrl }));\n\n // Load auth state - returns explicit success/error status\n const loadAuthState = useCallback(async (): Promise<{\n success: boolean;\n error?: string;\n }> => {\n try {\n // Use SDK's getCurrentSession() to check for existing session\n const sessionResult = insforge.auth.getCurrentSession();\n const session = sessionResult.data?.session;\n const token = session?.accessToken || null;\n\n if (!token) {\n // No token, user is not authenticated\n setUser(null);\n if (onAuthChange) {\n onAuthChange(null);\n }\n setIsLoaded(true);\n return { success: false, error: 'no_session' };\n }\n\n const userResult = await insforge.auth.getCurrentUser();\n\n if (userResult.data) {\n // Token is valid, update user state with fresh data\n const profile = userResult.data.profile;\n const userData: InsforgeUser = {\n id: userResult.data.user.id,\n email: userResult.data.user.email,\n name: (profile?.nickname as string | undefined) || '',\n avatarUrl: (profile?.avatarUrl as string | undefined) || '',\n };\n\n setUser(userData);\n\n if (onAuthChange) {\n onAuthChange(userData);\n }\n setIsLoaded(true);\n return { success: true };\n } else {\n // Token invalid or expired\n await insforge.auth.signOut();\n\n // Clear cookie if function provided\n if (onSignOut) {\n try {\n await onSignOut();\n } catch (error: unknown) {\n if (error instanceof Error) {\n console.error('[InsforgeProvider] Error clearing cookie:', error.message);\n }\n }\n }\n\n setUser(null);\n if (onAuthChange) {\n onAuthChange(null);\n }\n setIsLoaded(true);\n return { success: false, error: 'invalid_token' };\n }\n } catch (error) {\n // Token validation failed\n console.error('[InsforgeProvider] Token validation failed:', error);\n\n await insforge.auth.signOut();\n\n // Clear cookie if function provided\n if (onSignOut) {\n try {\n await onSignOut();\n } catch (error: unknown) {\n if (error instanceof Error) {\n console.error('[InsforgeProvider] Error clearing cookie:', error.message);\n }\n }\n }\n\n setUser(null);\n if (onAuthChange) {\n onAuthChange(null);\n }\n setIsLoaded(true);\n\n return {\n success: false,\n error: error instanceof Error ? error.message : 'Authentication failed',\n };\n }\n }, [insforge, onAuthChange, onSignOut]);\n\n useEffect(() => {\n // Run loadAuthState only once on mount\n loadAuthState();\n\n // Capture the ref value when effect runs\n const intervalId = refreshIntervalRef.current;\n\n return () => {\n // Use the captured value in cleanup\n if (intervalId) {\n clearInterval(intervalId);\n }\n };\n }, [loadAuthState]);\n\n // Handle authentication callback from URL\n useEffect(() => {\n // Only run once and only after auth is loaded\n if (!isLoaded || hasProcessedCallbackRef.current) {\n return;\n }\n\n const searchParams = new URLSearchParams(window.location.search);\n const accessToken = searchParams.get('access_token');\n\n if (accessToken && !!user) {\n // Mark as processed\n hasProcessedCallbackRef.current = true;\n\n // Clean URL by removing query parameters\n const url = new URL(window.location.href);\n url.search = '';\n window.history.replaceState({}, '', url.toString());\n\n // Redirect to afterSignInUrl\n setTimeout(() => {\n window.location.href = afterSignInUrl;\n }, 100);\n }\n }, [isLoaded, user, afterSignInUrl]);\n\n const getPublicAuthConfig = useCallback(async () => {\n try {\n const result = await insforge.auth.getPublicAuthConfig();\n if (result.data) {\n return result.data;\n } else {\n console.error('[InsforgeProvider] Failed to get public auth config:', result.error);\n return null;\n }\n } catch (error) {\n console.error('[InsforgeProvider] Failed to get public auth config:', error);\n return null;\n }\n }, [insforge]);\n\n /**\n * Helper function to handle successful authentication\n */\n const handleAuthSuccess = useCallback(\n async (authToken: string, fallbackUser?: { id?: string; email?: string; name?: string }) => {\n const userResult = await insforge.auth.getCurrentUser();\n\n if (userResult.data) {\n const profile = userResult.data.profile;\n const userData: InsforgeUser = {\n id: userResult.data.user.id,\n email: userResult.data.user.email,\n name: (profile?.nickname as string | undefined) || '',\n avatarUrl: (profile?.avatarUrl as string | undefined) || '',\n };\n\n setUser(userData);\n\n if (onAuthChange) {\n onAuthChange(userData);\n }\n\n // Try to sync token to cookie if function provided\n if (onSignIn) {\n try {\n await onSignIn(authToken);\n } catch (error: unknown) {\n if (error instanceof Error) {\n console.error('[InsforgeProvider] Error syncing token to cookie:', error.message);\n }\n }\n }\n } else if (fallbackUser) {\n // Fallback to basic user data if getCurrentUser fails\n const userData: InsforgeUser = {\n id: fallbackUser.id || '',\n email: fallbackUser.email || '',\n name: fallbackUser.name || '',\n avatarUrl: '',\n };\n\n setUser(userData);\n\n if (onAuthChange) {\n onAuthChange(userData);\n }\n }\n },\n [insforge, onAuthChange, onSignIn]\n );\n\n const signIn = useCallback(\n async (email: string, password: string) => {\n const sdkResult = await insforge.auth.signInWithPassword({\n email,\n password,\n });\n\n if (sdkResult.data) {\n await handleAuthSuccess(\n sdkResult.data.accessToken || '',\n sdkResult.data.user\n ? {\n id: sdkResult.data.user.id,\n email: sdkResult.data.user.email,\n name: sdkResult.data.user.name,\n }\n : undefined\n );\n return sdkResult.data;\n } else {\n // Return the full error object to preserve statusCode and other properties\n return {\n error: sdkResult.error?.message || 'Invalid email or password',\n statusCode: sdkResult.error?.statusCode,\n errorCode: sdkResult.error?.error,\n };\n }\n },\n [insforge, handleAuthSuccess]\n );\n\n const signUp = useCallback(\n async (email: string, password: string) => {\n const sdkResult = await insforge.auth.signUp({ email, password });\n\n if (sdkResult.data) {\n // Only set auth state if we got a token back (email verification might be required)\n if (sdkResult.data.accessToken) {\n await handleAuthSuccess(\n sdkResult.data.accessToken,\n sdkResult.data.user\n ? {\n id: sdkResult.data.user.id,\n email: sdkResult.data.user.email,\n name: sdkResult.data.user.name,\n }\n : undefined\n );\n }\n return sdkResult.data;\n } else {\n // Return the full error object to preserve statusCode and other properties\n return {\n error: sdkResult.error?.message || 'Sign up failed',\n statusCode: sdkResult.error?.statusCode,\n errorCode: sdkResult.error?.error,\n };\n }\n },\n [insforge, handleAuthSuccess]\n );\n\n const signOut = useCallback(async () => {\n await insforge.auth.signOut();\n\n // Clear cookie if function provided\n if (onSignOut) {\n try {\n await onSignOut();\n } catch (error: unknown) {\n if (error instanceof Error) {\n console.error('[InsforgeProvider] Error clearing cookie:', error.message);\n }\n }\n }\n\n // Clear refresh interval if exists\n if (refreshIntervalRef.current) {\n clearInterval(refreshIntervalRef.current);\n }\n\n setUser(null);\n if (onAuthChange) {\n onAuthChange(null);\n }\n }, [insforge, onAuthChange, onSignOut]);\n\n const updateUser = useCallback(\n async (data: Partial<InsforgeUser>) => {\n if (!user) {\n throw new Error('No user signed in');\n }\n\n const profileUpdate: Record<string, string | undefined> = {\n nickname: data.name,\n avatarUrl: data.avatarUrl,\n };\n\n const result = await insforge.auth.setProfile(profileUpdate);\n\n if (result.data) {\n const userResult = await insforge.auth.getCurrentUser();\n if (userResult.data) {\n const profile = userResult.data.profile;\n const updatedUser: InsforgeUser = {\n id: userResult.data.user.id,\n email: userResult.data.user.email,\n name: profile?.nickname || '',\n avatarUrl: profile?.avatarUrl || '',\n };\n setUser(updatedUser);\n if (onAuthChange) {\n onAuthChange(updatedUser);\n }\n }\n }\n },\n [user, onAuthChange, insforge]\n );\n\n const sendVerificationEmail = useCallback(\n async (email: string) => {\n const sdkResult = await insforge.auth.sendVerificationEmail({ email });\n return sdkResult.data;\n },\n [insforge]\n );\n\n const sendResetPasswordEmail = useCallback(\n async (email: string) => {\n const sdkResult = await insforge.auth.sendResetPasswordEmail({ email });\n return sdkResult.data;\n },\n [insforge]\n );\n\n const resetPassword = useCallback(\n async (token: string, newPassword: string) => {\n const sdkResult = await insforge.auth.resetPassword({\n newPassword,\n otp: token,\n });\n return sdkResult.data;\n },\n [insforge]\n );\n\n const verifyEmail = useCallback(\n async (otp: string, email?: string) => {\n const sdkResult = await insforge.auth.verifyEmail({ otp, email: email || undefined });\n if (sdkResult.data) {\n return sdkResult.data;\n } else {\n return {\n accessToken: '',\n error: {\n message: sdkResult.error?.message || 'Email verification failed',\n },\n };\n }\n },\n [insforge]\n );\n\n const exchangeResetPasswordToken = useCallback(\n async (email: string, code: string) => {\n const sdkResult = await insforge.auth.exchangeResetPasswordToken({ email, code });\n if (sdkResult.data) {\n return sdkResult.data;\n } else {\n return {\n error: {\n message: sdkResult.error?.message || 'Failed to exchange reset password token',\n },\n };\n }\n },\n [insforge]\n );\n\n const loginWithOAuth = useCallback(\n async (provider: OAuthProvider, redirectTo: string) => {\n const sdkResult = await insforge.auth.signInWithOAuth({\n provider,\n redirectTo: redirectTo || afterSignInUrl,\n });\n if (sdkResult.error) {\n throw new Error(sdkResult.error.message);\n }\n },\n [insforge, afterSignInUrl]\n );\n\n return (\n <InsforgeContext.Provider\n value={{\n user,\n isLoaded,\n isSignedIn: !!user,\n setUser,\n signIn,\n signUp,\n signOut,\n updateUser,\n reloadAuth: loadAuthState,\n baseUrl,\n sendVerificationEmail,\n sendResetPasswordEmail,\n resetPassword,\n verifyEmail,\n exchangeResetPasswordToken,\n getPublicAuthConfig,\n loginWithOAuth,\n }}\n >\n {children}\n </InsforgeContext.Provider>\n );\n}\n\n/**\n * Hook to access Insforge context\n *\n * @example\n * ```tsx\n * function MyComponent() {\n * const { user, isSignedIn, signOut } = useInsforge();\n *\n * if (!isSignedIn) return <SignIn />;\n *\n * return (\n * <div>\n * <p>Welcome {user.email}</p>\n * <button onClick={signOut}>Sign Out</button>\n * </div>\n * );\n * }\n * ```\n */\nexport function useInsforge(): InsforgeContextValue {\n const context = useContext(InsforgeContext);\n if (!context) {\n throw new Error('useInsforge must be used within InsforgeProvider');\n }\n return context;\n}\n","import { useInsforge } from '../provider/InsforgeProvider';\n\n/**\n * Hook to access authentication methods\n *\n * @returns Object containing:\n * - `signIn`: Function to sign in with email and password\n * - `signUp`: Function to sign up with email and password\n * - `signOut`: Function to sign out the current user\n * - `isLoaded`: Boolean indicating if auth state has been loaded\n * - `isSignedIn`: Boolean indicating if user is currently signed in\n *\n * @example\n * ```tsx\n * function LoginForm() {\n * const { signIn, signUp, signOut, isLoaded, isSignedIn } = useAuth();\n *\n * async function handleLogin(email: string, password: string) {\n * try {\n * await signIn(email, password);\n * // User is now signed in\n * } catch (error) {\n * console.error('Sign in failed:', error);\n * }\n * }\n *\n * if (!isLoaded) return <div>Loading...</div>;\n *\n * return (\n * <form onSubmit={(e) => { e.preventDefault(); handleLogin(email, password); }}>\n * {/* form fields *\\/}\n * </form>\n * );\n * }\n * ```\n */\nexport function useAuth() {\n const { signIn, signUp, signOut, isLoaded, isSignedIn } = useInsforge();\n return { signIn, signUp, signOut, isLoaded, isSignedIn };\n}\n","import { useInsforge } from '../provider/InsforgeProvider';\n\n/**\n * Hook to access current user data\n *\n * @returns Object containing:\n * - `user`: Current user object (InsforgeUser | null)\n * - `isLoaded`: Boolean indicating if auth state has been loaded\n * - `updateUser`: Function to update user profile data\n * - `setUser`: Internal function to manually set user state\n *\n * @example\n * ```tsx\n * function UserProfile() {\n * const { user, isLoaded, updateUser } = useUser();\n *\n * if (!isLoaded) return <div>Loading...</div>;\n * if (!user) return <div>Not signed in</div>;\n *\n * async function handleUpdate(name: string) {\n * await updateUser({ name });\n * }\n *\n * return (\n * <div>\n * <p>Email: {user.email}</p>\n * {user.name && <p>Name: {user.name}</p>}\n * {user.avatarUrl && <img src={user.avatarUrl} alt=\"Avatar\" />}\n * </div>\n * );\n * }\n * ```\n */\nexport function useUser() {\n const { user, isLoaded, updateUser, setUser } = useInsforge();\n return { user, isLoaded, updateUser, setUser };\n}\n","import { useState, useEffect } from 'react';\nimport type { GetPublicAuthConfigResponse } from '@insforge/shared-schemas';\nimport { useInsforge } from '../provider/InsforgeProvider';\n\n/**\n * Hook to get all public authentication configuration (OAuth + Email) from Insforge backend\n *\n * **IMPORTANT: This hook should ONLY be used in SignIn and SignUp components.**\n *\n * This hook lazily fetches all public authentication configuration from the backend\n * only when the component mounts. Using it in other components will cause unnecessary\n * API calls on every page load.\n *\n * @returns Object containing OAuth providers, email auth config, and loading state\n * - `oauthProviders`: Array of enabled OAuth provider names (e.g., ['google', 'github'])\n * - `authConfig`: Email authentication configuration object with password rules\n * - `isLoaded`: Boolean indicating if the config has been fetched\n *\n * @example\n * ```tsx\n * // ✅ Correct usage - only in SignIn/SignUp components\n * function SignUp() {\n * const { oauthProviders, authConfig, isLoaded } = usePublicAuthConfig();\n *\n * if (!isLoaded) return <div>Loading...</div>;\n *\n * return (\n * <div>\n * <p>OAuth providers: {oauthProviders.length}</p>\n * <p>Password min length: {authConfig?.passwordMinLength}</p>\n * </div>\n * );\n * }\n * ```\n *\n * @requires Must be used within InsforgeProvider\n */\nexport function usePublicAuthConfig(): {\n authConfig: GetPublicAuthConfigResponse | null;\n isLoaded: boolean;\n} {\n const { getPublicAuthConfig } = useInsforge();\n const [authConfig, setAuthConfig] = useState<GetPublicAuthConfigResponse | null>(null);\n const [isLoaded, setIsLoaded] = useState(false);\n\n useEffect(() => {\n async function fetchConfig() {\n const result = await getPublicAuthConfig();\n if (result) {\n setAuthConfig(result);\n } else {\n console.error('[usePublicAuthConfig] Failed to get public auth config');\n setAuthConfig(null);\n }\n setIsLoaded(true);\n }\n\n fetchConfig();\n }, [getPublicAuthConfig]);\n\n return { authConfig, isLoaded };\n}\n"]}
package/dist/hooks.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/provider/InsforgeProvider.tsx","../src/hooks/useAuth.ts","../src/hooks/useUser.ts","../src/hooks/usePublicAuthConfig.ts"],"names":["useState","useEffect"],"mappings":";;;;AAmEA,IAAM,eAAA,GAAkB,cAAgD,MAAS,CAAA;AAyf1E,SAAS,WAAA,GAAoC;AAClD,EAAA,MAAM,OAAA,GAAU,WAAW,eAAe,CAAA;AAC1C,EAAA,IAAI,CAAC,OAAA,EAAS;AACZ,IAAA,MAAM,IAAI,MAAM,kDAAkD,CAAA;AAAA,EACpE;AACA,EAAA,OAAO,OAAA;AACT;;;AC9hBO,SAAS,OAAA,GAAU;AACxB,EAAA,MAAM,EAAE,MAAA,EAAQ,MAAA,EAAQ,SAAS,QAAA,EAAU,UAAA,KAAe,WAAA,EAAY;AACtE,EAAA,OAAO,EAAE,MAAA,EAAQ,MAAA,EAAQ,OAAA,EAAS,UAAU,UAAA,EAAW;AACzD;;;ACNO,SAAS,OAAA,GAAU;AACxB,EAAA,MAAM,EAAE,IAAA,EAAM,QAAA,EAAU,UAAA,EAAY,OAAA,KAAY,WAAA,EAAY;AAC5D,EAAA,OAAO,EAAE,IAAA,EAAM,QAAA,EAAU,UAAA,EAAY,OAAA,EAAQ;AAC/C;ACCO,SAAS,mBAAA,GAGd;AACA,EAAA,MAAM,EAAE,mBAAA,EAAoB,GAAI,WAAA,EAAY;AAC5C,EAAA,MAAM,CAAC,UAAA,EAAY,aAAa,CAAA,GAAIA,SAA6C,IAAI,CAAA;AACrF,EAAA,MAAM,CAAC,QAAA,EAAU,WAAW,CAAA,GAAIA,SAAS,KAAK,CAAA;AAE9C,EAAAC,UAAU,MAAM;AACd,IAAA,eAAe,WAAA,GAAc;AAC3B,MAAA,MAAM,MAAA,GAAS,MAAM,mBAAA,EAAoB;AACzC,MAAA,IAAI,MAAA,EAAQ;AACV,QAAA,aAAA,CAAc,MAAM,CAAA;AAAA,MACtB,CAAA,MAAO;AACL,QAAA,OAAA,CAAQ,MAAM,wDAAwD,CAAA;AACtE,QAAA,aAAA,CAAc,IAAI,CAAA;AAAA,MACpB;AACA,MAAA,WAAA,CAAY,IAAI,CAAA;AAAA,IAClB;AAEA,IAAA,WAAA,EAAY;AAAA,EACd,CAAA,EAAG,CAAC,mBAAmB,CAAC,CAAA;AAExB,EAAA,OAAO,EAAE,YAAY,QAAA,EAAS;AAChC","file":"hooks.js","sourcesContent":["'use client';\n\nimport {\n createContext,\n useContext,\n useEffect,\n useState,\n useCallback,\n useRef,\n type ReactNode,\n} from 'react';\nimport { UserSchema, createClient } from '@insforge/sdk';\nimport type { InsforgeUser, OAuthProvider } from '../types';\nimport {\n CreateSessionResponse,\n CreateUserResponse,\n GetPublicAuthConfigResponse,\n ResetPasswordResponse,\n} from '@insforge/shared-schemas';\n\ninterface InsforgeContextValue {\n // Auth state\n user: InsforgeUser | null;\n isLoaded: boolean;\n isSignedIn: boolean;\n\n // Auth methods\n setUser: (user: InsforgeUser | null) => void;\n signIn: (\n email: string,\n password: string\n ) => Promise<CreateSessionResponse | { error: string; statusCode?: number; errorCode?: string }>;\n signUp: (\n email: string,\n password: string\n ) => Promise<CreateUserResponse | { error: string; statusCode?: number; errorCode?: string }>;\n signOut: () => Promise<void>;\n updateUser: (data: Partial<InsforgeUser>) => Promise<void>;\n reloadAuth: () => Promise<{ success: boolean; error?: string }>;\n\n // Email verification methods\n sendVerificationEmail: (email: string) => Promise<{ success: boolean; message: string } | null>;\n sendResetPasswordEmail: (email: string) => Promise<{ success: boolean; message: string } | null>;\n resetPassword: (token: string, newPassword: string) => Promise<ResetPasswordResponse | null>;\n verifyEmail: (\n otp: string,\n email?: string\n ) => Promise<{\n accessToken: string;\n user?: UserSchema;\n redirectTo?: string;\n error?: { message: string };\n } | null>;\n exchangeResetPasswordToken: (\n email: string,\n code: string\n ) => Promise<{ token: string; expiresAt?: string } | { error: { message: string } }>;\n loginWithOAuth: (\n provider: OAuthProvider,\n redirectTo: string\n ) => Promise<{ url?: string | undefined; provider?: string | undefined }>;\n // Public auth config\n getPublicAuthConfig: () => Promise<GetPublicAuthConfigResponse | null>;\n // Base config\n baseUrl: string;\n}\n\nconst InsforgeContext = createContext<InsforgeContextValue | undefined>(undefined);\n\nexport interface InsforgeProviderProps {\n children: ReactNode;\n baseUrl: string;\n /**\n * URL to redirect to after successful sign in (when token is detected in URL)\n * @default '/'\n */\n afterSignInUrl?: string;\n onAuthChange?: (user: InsforgeUser | null) => void;\n onSignIn?: (authToken: string) => Promise<void>;\n onSignOut?: () => Promise<void>;\n}\n\n/**\n * Unified Insforge Provider - manages authentication state and configuration\n *\n * Manages user authentication state and provides all necessary context to child components.\n * Works with any React framework (Next.js, Vite, Remix, etc.).\n *\n * @example\n * ```tsx\n * // Basic usage (React/Vite)\n * import { InsforgeProvider } from '@insforge/react';\n *\n * export default function App() {\n * return (\n * <InsforgeProvider\n * baseUrl={process.env.VITE_INSFORGE_BASE_URL}\n * afterSignInUrl=\"/dashboard\"\n * >\n * {children}\n * </InsforgeProvider>\n * );\n * }\n * ```\n *\n * @example\n * ```tsx\n * // With cookie sync (Next.js optimization)\n * <InsforgeProvider\n * baseUrl={baseUrl}\n * onSignIn={async (authToken) => {\n * await signIn(authToken);\n * }}\n * onSignOut={async () => {\n * await signOut();\n * }}\n * >\n * {children}\n * </InsforgeProvider>\n * ```\n */\nexport function InsforgeProvider({\n children,\n baseUrl,\n afterSignInUrl = '/',\n onAuthChange,\n onSignIn,\n onSignOut,\n}: InsforgeProviderProps) {\n // Auth state\n const [user, setUser] = useState<InsforgeUser | null>(null);\n const [isLoaded, setIsLoaded] = useState(false);\n\n const refreshIntervalRef = useRef<NodeJS.Timeout | null>(null);\n const hasProcessedCallbackRef = useRef(false);\n\n // Initialize SDK client with lazy initialization - only runs once\n const [insforge] = useState(() => createClient({ baseUrl }));\n\n // Load auth state - returns explicit success/error status\n const loadAuthState = useCallback(async (): Promise<{\n success: boolean;\n error?: string;\n }> => {\n try {\n // Use SDK's getCurrentSession() to check for existing session\n const sessionResult = insforge.auth.getCurrentSession();\n const session = sessionResult.data?.session;\n const token = session?.accessToken || null;\n\n if (!token) {\n // No token, user is not authenticated\n setUser(null);\n if (onAuthChange) {\n onAuthChange(null);\n }\n setIsLoaded(true);\n return { success: false, error: 'no_session' };\n }\n\n const userResult = await insforge.auth.getCurrentUser();\n\n if (userResult.data) {\n // Token is valid, update user state with fresh data\n const profile = userResult.data.profile;\n const userData: InsforgeUser = {\n id: userResult.data.user.id,\n email: userResult.data.user.email,\n name: (profile?.nickname as string | undefined) || '',\n avatarUrl: (profile?.avatarUrl as string | undefined) || '',\n };\n\n setUser(userData);\n\n if (onAuthChange) {\n onAuthChange(userData);\n }\n setIsLoaded(true);\n return { success: true };\n } else {\n // Token invalid or expired\n await insforge.auth.signOut();\n\n // Clear cookie if function provided\n if (onSignOut) {\n try {\n await onSignOut();\n } catch (error: unknown) {\n if (error instanceof Error) {\n console.error('[InsforgeProvider] Error clearing cookie:', error.message);\n }\n }\n }\n\n setUser(null);\n if (onAuthChange) {\n onAuthChange(null);\n }\n setIsLoaded(true);\n return { success: false, error: 'invalid_token' };\n }\n } catch (error) {\n // Token validation failed\n console.error('[InsforgeProvider] Token validation failed:', error);\n\n await insforge.auth.signOut();\n\n // Clear cookie if function provided\n if (onSignOut) {\n try {\n await onSignOut();\n } catch (error: unknown) {\n if (error instanceof Error) {\n console.error('[InsforgeProvider] Error clearing cookie:', error.message);\n }\n }\n }\n\n setUser(null);\n if (onAuthChange) {\n onAuthChange(null);\n }\n setIsLoaded(true);\n\n return {\n success: false,\n error: error instanceof Error ? error.message : 'Authentication failed',\n };\n }\n }, [insforge, onAuthChange, onSignOut]);\n\n useEffect(() => {\n // Run loadAuthState only once on mount\n loadAuthState();\n\n // Capture the ref value when effect runs\n const intervalId = refreshIntervalRef.current;\n\n return () => {\n // Use the captured value in cleanup\n if (intervalId) {\n clearInterval(intervalId);\n }\n };\n }, [loadAuthState]);\n\n // Handle authentication callback from URL\n useEffect(() => {\n // Only run once and only after auth is loaded\n if (!isLoaded || hasProcessedCallbackRef.current) {\n return;\n }\n\n const searchParams = new URLSearchParams(window.location.search);\n const accessToken = searchParams.get('access_token');\n\n if (accessToken && !!user) {\n // Mark as processed\n hasProcessedCallbackRef.current = true;\n\n // Clean URL by removing query parameters\n const url = new URL(window.location.href);\n url.search = '';\n window.history.replaceState({}, '', url.toString());\n\n // Redirect to afterSignInUrl\n setTimeout(() => {\n window.location.href = afterSignInUrl;\n }, 100);\n }\n }, [isLoaded, user, afterSignInUrl]);\n\n const getPublicAuthConfig = useCallback(async () => {\n try {\n const result = await insforge.auth.getPublicAuthConfig();\n if (result.data) {\n return result.data;\n } else {\n console.error('[InsforgeProvider] Failed to get public auth config:', result.error);\n return null;\n }\n } catch (error) {\n console.error('[InsforgeProvider] Failed to get public auth config:', error);\n return null;\n }\n }, [insforge]);\n\n /**\n * Helper function to handle successful authentication\n */\n const handleAuthSuccess = useCallback(\n async (authToken: string, fallbackUser?: { id?: string; email?: string; name?: string }) => {\n const userResult = await insforge.auth.getCurrentUser();\n\n if (userResult.data) {\n const profile = userResult.data.profile;\n const userData: InsforgeUser = {\n id: userResult.data.user.id,\n email: userResult.data.user.email,\n name: (profile?.nickname as string | undefined) || '',\n avatarUrl: (profile?.avatarUrl as string | undefined) || '',\n };\n\n setUser(userData);\n\n if (onAuthChange) {\n onAuthChange(userData);\n }\n\n // Try to sync token to cookie if function provided\n if (onSignIn) {\n try {\n await onSignIn(authToken);\n } catch (error: unknown) {\n if (error instanceof Error) {\n console.error('[InsforgeProvider] Error syncing token to cookie:', error.message);\n }\n }\n }\n } else if (fallbackUser) {\n // Fallback to basic user data if getCurrentUser fails\n const userData: InsforgeUser = {\n id: fallbackUser.id || '',\n email: fallbackUser.email || '',\n name: fallbackUser.name || '',\n avatarUrl: '',\n };\n\n setUser(userData);\n\n if (onAuthChange) {\n onAuthChange(userData);\n }\n }\n },\n [insforge, onAuthChange, onSignIn]\n );\n\n const signIn = useCallback(\n async (email: string, password: string) => {\n const sdkResult = await insforge.auth.signInWithPassword({\n email,\n password,\n });\n\n if (sdkResult.data) {\n await handleAuthSuccess(\n sdkResult.data.accessToken || '',\n sdkResult.data.user\n ? {\n id: sdkResult.data.user.id,\n email: sdkResult.data.user.email,\n name: sdkResult.data.user.name,\n }\n : undefined\n );\n return sdkResult.data;\n } else {\n // Return the full error object to preserve statusCode and other properties\n return {\n error: sdkResult.error?.message || 'Invalid email or password',\n statusCode: sdkResult.error?.statusCode,\n errorCode: sdkResult.error?.error,\n };\n }\n },\n [insforge, handleAuthSuccess]\n );\n\n const signUp = useCallback(\n async (email: string, password: string) => {\n const sdkResult = await insforge.auth.signUp({ email, password });\n\n if (sdkResult.data) {\n // Only set auth state if we got a token back (email verification might be required)\n if (sdkResult.data.accessToken) {\n await handleAuthSuccess(\n sdkResult.data.accessToken,\n sdkResult.data.user\n ? {\n id: sdkResult.data.user.id,\n email: sdkResult.data.user.email,\n name: sdkResult.data.user.name,\n }\n : undefined\n );\n }\n return sdkResult.data;\n } else {\n // Return the full error object to preserve statusCode and other properties\n return {\n error: sdkResult.error?.message || 'Sign up failed',\n statusCode: sdkResult.error?.statusCode,\n errorCode: sdkResult.error?.error,\n };\n }\n },\n [insforge, handleAuthSuccess]\n );\n\n const signOut = useCallback(async () => {\n await insforge.auth.signOut();\n\n // Clear cookie if function provided\n if (onSignOut) {\n try {\n await onSignOut();\n } catch (error: unknown) {\n if (error instanceof Error) {\n console.error('[InsforgeProvider] Error clearing cookie:', error.message);\n }\n }\n }\n\n // Clear refresh interval if exists\n if (refreshIntervalRef.current) {\n clearInterval(refreshIntervalRef.current);\n }\n\n setUser(null);\n if (onAuthChange) {\n onAuthChange(null);\n }\n }, [insforge, onAuthChange, onSignOut]);\n\n const updateUser = useCallback(\n async (data: Partial<InsforgeUser>) => {\n if (!user) {\n throw new Error('No user signed in');\n }\n\n const profileUpdate: Record<string, string | undefined> = {\n nickname: data.name,\n avatarUrl: data.avatarUrl,\n };\n\n const result = await insforge.auth.setProfile(profileUpdate);\n\n if (result.data) {\n const userResult = await insforge.auth.getCurrentUser();\n if (userResult.data) {\n const profile = userResult.data.profile;\n const updatedUser: InsforgeUser = {\n id: userResult.data.user.id,\n email: userResult.data.user.email,\n name: profile?.nickname || '',\n avatarUrl: profile?.avatarUrl || '',\n };\n setUser(updatedUser);\n if (onAuthChange) {\n onAuthChange(updatedUser);\n }\n }\n }\n },\n [user, onAuthChange, insforge]\n );\n\n const sendVerificationEmail = useCallback(\n async (email: string) => {\n const sdkResult = await insforge.auth.sendVerificationEmail({ email });\n return sdkResult.data;\n },\n [insforge]\n );\n\n const sendResetPasswordEmail = useCallback(\n async (email: string) => {\n const sdkResult = await insforge.auth.sendResetPasswordEmail({ email });\n return sdkResult.data;\n },\n [insforge]\n );\n\n const resetPassword = useCallback(\n async (token: string, newPassword: string) => {\n const sdkResult = await insforge.auth.resetPassword({\n newPassword,\n otp: token,\n });\n return sdkResult.data;\n },\n [insforge]\n );\n\n const verifyEmail = useCallback(\n async (otp: string, email?: string) => {\n const sdkResult = await insforge.auth.verifyEmail({ otp, email: email || undefined });\n if (sdkResult.data) {\n return sdkResult.data;\n } else {\n return {\n accessToken: '',\n error: {\n message: sdkResult.error?.message || 'Email verification failed',\n },\n };\n }\n },\n [insforge]\n );\n\n const exchangeResetPasswordToken = useCallback(\n async (email: string, code: string) => {\n const sdkResult = await insforge.auth.exchangeResetPasswordToken({ email, code });\n if (sdkResult.data) {\n return sdkResult.data;\n } else {\n return {\n error: {\n message: sdkResult.error?.message || 'Failed to exchange reset password token',\n },\n };\n }\n },\n [insforge]\n );\n\n const loginWithOAuth = useCallback(\n async (provider: OAuthProvider, redirectTo: string) => {\n const sdkResult = await insforge.auth.signInWithOAuth({ provider, redirectTo });\n return sdkResult.data;\n },\n [insforge]\n );\n\n return (\n <InsforgeContext.Provider\n value={{\n user,\n isLoaded,\n isSignedIn: !!user,\n setUser,\n signIn,\n signUp,\n signOut,\n updateUser,\n reloadAuth: loadAuthState,\n baseUrl,\n sendVerificationEmail,\n sendResetPasswordEmail,\n resetPassword,\n verifyEmail,\n exchangeResetPasswordToken,\n getPublicAuthConfig,\n loginWithOAuth,\n }}\n >\n {children}\n </InsforgeContext.Provider>\n );\n}\n\n/**\n * Hook to access Insforge context\n *\n * @example\n * ```tsx\n * function MyComponent() {\n * const { user, isSignedIn, signOut } = useInsforge();\n *\n * if (!isSignedIn) return <SignIn />;\n *\n * return (\n * <div>\n * <p>Welcome {user.email}</p>\n * <button onClick={signOut}>Sign Out</button>\n * </div>\n * );\n * }\n * ```\n */\nexport function useInsforge(): InsforgeContextValue {\n const context = useContext(InsforgeContext);\n if (!context) {\n throw new Error('useInsforge must be used within InsforgeProvider');\n }\n return context;\n}\n","import { useInsforge } from '../provider/InsforgeProvider';\n\n/**\n * Hook to access authentication methods\n *\n * @returns Object containing:\n * - `signIn`: Function to sign in with email and password\n * - `signUp`: Function to sign up with email and password\n * - `signOut`: Function to sign out the current user\n * - `isLoaded`: Boolean indicating if auth state has been loaded\n * - `isSignedIn`: Boolean indicating if user is currently signed in\n *\n * @example\n * ```tsx\n * function LoginForm() {\n * const { signIn, signUp, signOut, isLoaded, isSignedIn } = useAuth();\n *\n * async function handleLogin(email: string, password: string) {\n * try {\n * await signIn(email, password);\n * // User is now signed in\n * } catch (error) {\n * console.error('Sign in failed:', error);\n * }\n * }\n *\n * if (!isLoaded) return <div>Loading...</div>;\n *\n * return (\n * <form onSubmit={(e) => { e.preventDefault(); handleLogin(email, password); }}>\n * {/* form fields *\\/}\n * </form>\n * );\n * }\n * ```\n */\nexport function useAuth() {\n const { signIn, signUp, signOut, isLoaded, isSignedIn } = useInsforge();\n return { signIn, signUp, signOut, isLoaded, isSignedIn };\n}\n","import { useInsforge } from '../provider/InsforgeProvider';\n\n/**\n * Hook to access current user data\n *\n * @returns Object containing:\n * - `user`: Current user object (InsforgeUser | null)\n * - `isLoaded`: Boolean indicating if auth state has been loaded\n * - `updateUser`: Function to update user profile data\n * - `setUser`: Internal function to manually set user state\n *\n * @example\n * ```tsx\n * function UserProfile() {\n * const { user, isLoaded, updateUser } = useUser();\n *\n * if (!isLoaded) return <div>Loading...</div>;\n * if (!user) return <div>Not signed in</div>;\n *\n * async function handleUpdate(name: string) {\n * await updateUser({ name });\n * }\n *\n * return (\n * <div>\n * <p>Email: {user.email}</p>\n * {user.name && <p>Name: {user.name}</p>}\n * {user.avatarUrl && <img src={user.avatarUrl} alt=\"Avatar\" />}\n * </div>\n * );\n * }\n * ```\n */\nexport function useUser() {\n const { user, isLoaded, updateUser, setUser } = useInsforge();\n return { user, isLoaded, updateUser, setUser };\n}\n","import { useState, useEffect } from 'react';\nimport type { GetPublicAuthConfigResponse } from '@insforge/shared-schemas';\nimport { useInsforge } from '../provider/InsforgeProvider';\n\n/**\n * Hook to get all public authentication configuration (OAuth + Email) from Insforge backend\n *\n * **IMPORTANT: This hook should ONLY be used in SignIn and SignUp components.**\n *\n * This hook lazily fetches all public authentication configuration from the backend\n * only when the component mounts. Using it in other components will cause unnecessary\n * API calls on every page load.\n *\n * @returns Object containing OAuth providers, email auth config, and loading state\n * - `oauthProviders`: Array of enabled OAuth provider names (e.g., ['google', 'github'])\n * - `authConfig`: Email authentication configuration object with password rules\n * - `isLoaded`: Boolean indicating if the config has been fetched\n *\n * @example\n * ```tsx\n * // ✅ Correct usage - only in SignIn/SignUp components\n * function SignUp() {\n * const { oauthProviders, authConfig, isLoaded } = usePublicAuthConfig();\n *\n * if (!isLoaded) return <div>Loading...</div>;\n *\n * return (\n * <div>\n * <p>OAuth providers: {oauthProviders.length}</p>\n * <p>Password min length: {authConfig?.passwordMinLength}</p>\n * </div>\n * );\n * }\n * ```\n *\n * @requires Must be used within InsforgeProvider\n */\nexport function usePublicAuthConfig(): {\n authConfig: GetPublicAuthConfigResponse | null;\n isLoaded: boolean;\n} {\n const { getPublicAuthConfig } = useInsforge();\n const [authConfig, setAuthConfig] = useState<GetPublicAuthConfigResponse | null>(null);\n const [isLoaded, setIsLoaded] = useState(false);\n\n useEffect(() => {\n async function fetchConfig() {\n const result = await getPublicAuthConfig();\n if (result) {\n setAuthConfig(result);\n } else {\n console.error('[usePublicAuthConfig] Failed to get public auth config');\n setAuthConfig(null);\n }\n setIsLoaded(true);\n }\n\n fetchConfig();\n }, [getPublicAuthConfig]);\n\n return { authConfig, isLoaded };\n}\n"]}
1
+ {"version":3,"sources":["../src/provider/InsforgeProvider.tsx","../src/hooks/useAuth.ts","../src/hooks/useUser.ts","../src/hooks/usePublicAuthConfig.ts"],"names":["useState","useEffect"],"mappings":";;;;AAgEA,IAAM,eAAA,GAAkB,cAAgD,MAAS,CAAA;AA8f1E,SAAS,WAAA,GAAoC;AAClD,EAAA,MAAM,OAAA,GAAU,WAAW,eAAe,CAAA;AAC1C,EAAA,IAAI,CAAC,OAAA,EAAS;AACZ,IAAA,MAAM,IAAI,MAAM,kDAAkD,CAAA;AAAA,EACpE;AACA,EAAA,OAAO,OAAA;AACT;;;AChiBO,SAAS,OAAA,GAAU;AACxB,EAAA,MAAM,EAAE,MAAA,EAAQ,MAAA,EAAQ,SAAS,QAAA,EAAU,UAAA,KAAe,WAAA,EAAY;AACtE,EAAA,OAAO,EAAE,MAAA,EAAQ,MAAA,EAAQ,OAAA,EAAS,UAAU,UAAA,EAAW;AACzD;;;ACNO,SAAS,OAAA,GAAU;AACxB,EAAA,MAAM,EAAE,IAAA,EAAM,QAAA,EAAU,UAAA,EAAY,OAAA,KAAY,WAAA,EAAY;AAC5D,EAAA,OAAO,EAAE,IAAA,EAAM,QAAA,EAAU,UAAA,EAAY,OAAA,EAAQ;AAC/C;ACCO,SAAS,mBAAA,GAGd;AACA,EAAA,MAAM,EAAE,mBAAA,EAAoB,GAAI,WAAA,EAAY;AAC5C,EAAA,MAAM,CAAC,UAAA,EAAY,aAAa,CAAA,GAAIA,SAA6C,IAAI,CAAA;AACrF,EAAA,MAAM,CAAC,QAAA,EAAU,WAAW,CAAA,GAAIA,SAAS,KAAK,CAAA;AAE9C,EAAAC,UAAU,MAAM;AACd,IAAA,eAAe,WAAA,GAAc;AAC3B,MAAA,MAAM,MAAA,GAAS,MAAM,mBAAA,EAAoB;AACzC,MAAA,IAAI,MAAA,EAAQ;AACV,QAAA,aAAA,CAAc,MAAM,CAAA;AAAA,MACtB,CAAA,MAAO;AACL,QAAA,OAAA,CAAQ,MAAM,wDAAwD,CAAA;AACtE,QAAA,aAAA,CAAc,IAAI,CAAA;AAAA,MACpB;AACA,MAAA,WAAA,CAAY,IAAI,CAAA;AAAA,IAClB;AAEA,IAAA,WAAA,EAAY;AAAA,EACd,CAAA,EAAG,CAAC,mBAAmB,CAAC,CAAA;AAExB,EAAA,OAAO,EAAE,YAAY,QAAA,EAAS;AAChC","file":"hooks.js","sourcesContent":["'use client';\n\nimport {\n createContext,\n useContext,\n useEffect,\n useState,\n useCallback,\n useRef,\n type ReactNode,\n} from 'react';\nimport { UserSchema, createClient } from '@insforge/sdk';\nimport type { InsforgeUser, OAuthProvider } from '../types';\nimport {\n CreateSessionResponse,\n CreateUserResponse,\n GetPublicAuthConfigResponse,\n ResetPasswordResponse,\n} from '@insforge/shared-schemas';\n\ninterface InsforgeContextValue {\n // Auth state\n user: InsforgeUser | null;\n isLoaded: boolean;\n isSignedIn: boolean;\n\n // Auth methods\n setUser: (user: InsforgeUser | null) => void;\n signIn: (\n email: string,\n password: string\n ) => Promise<CreateSessionResponse | { error: string; statusCode?: number; errorCode?: string }>;\n signUp: (\n email: string,\n password: string\n ) => Promise<CreateUserResponse | { error: string; statusCode?: number; errorCode?: string }>;\n signOut: () => Promise<void>;\n updateUser: (data: Partial<InsforgeUser>) => Promise<void>;\n reloadAuth: () => Promise<{ success: boolean; error?: string }>;\n\n // Email verification methods\n sendVerificationEmail: (email: string) => Promise<{ success: boolean; message: string } | null>;\n sendResetPasswordEmail: (email: string) => Promise<{ success: boolean; message: string } | null>;\n resetPassword: (token: string, newPassword: string) => Promise<ResetPasswordResponse | null>;\n verifyEmail: (\n otp: string,\n email?: string\n ) => Promise<{\n accessToken: string;\n user?: UserSchema;\n redirectTo?: string;\n error?: { message: string };\n } | null>;\n exchangeResetPasswordToken: (\n email: string,\n code: string\n ) => Promise<{ token: string; expiresAt?: string } | { error: { message: string } }>;\n loginWithOAuth: (provider: OAuthProvider, redirectTo: string) => Promise<void>;\n // Public auth config\n getPublicAuthConfig: () => Promise<GetPublicAuthConfigResponse | null>;\n // Base config\n baseUrl: string;\n}\n\nconst InsforgeContext = createContext<InsforgeContextValue | undefined>(undefined);\n\nexport interface InsforgeProviderProps {\n children: ReactNode;\n baseUrl: string;\n /**\n * URL to redirect to after successful sign in (when token is detected in URL)\n * @default '/'\n */\n afterSignInUrl?: string;\n onAuthChange?: (user: InsforgeUser | null) => void;\n onSignIn?: (authToken: string) => Promise<void>;\n onSignOut?: () => Promise<void>;\n}\n\n/**\n * Unified Insforge Provider - manages authentication state and configuration\n *\n * Manages user authentication state and provides all necessary context to child components.\n * Works with any React framework (Next.js, Vite, Remix, etc.).\n *\n * @example\n * ```tsx\n * // Basic usage (React/Vite)\n * import { InsforgeProvider } from '@insforge/react';\n *\n * export default function App() {\n * return (\n * <InsforgeProvider\n * baseUrl={process.env.VITE_INSFORGE_BASE_URL}\n * afterSignInUrl=\"/dashboard\"\n * >\n * {children}\n * </InsforgeProvider>\n * );\n * }\n * ```\n *\n * @example\n * ```tsx\n * // With cookie sync (Next.js optimization)\n * <InsforgeProvider\n * baseUrl={baseUrl}\n * onSignIn={async (authToken) => {\n * await signIn(authToken);\n * }}\n * onSignOut={async () => {\n * await signOut();\n * }}\n * >\n * {children}\n * </InsforgeProvider>\n * ```\n */\nexport function InsforgeProvider({\n children,\n baseUrl,\n afterSignInUrl = '/',\n onAuthChange,\n onSignIn,\n onSignOut,\n}: InsforgeProviderProps) {\n // Auth state\n const [user, setUser] = useState<InsforgeUser | null>(null);\n const [isLoaded, setIsLoaded] = useState(false);\n\n const refreshIntervalRef = useRef<NodeJS.Timeout | null>(null);\n const hasProcessedCallbackRef = useRef(false);\n\n // Initialize SDK client with lazy initialization - only runs once\n const [insforge] = useState(() => createClient({ baseUrl }));\n\n // Load auth state - returns explicit success/error status\n const loadAuthState = useCallback(async (): Promise<{\n success: boolean;\n error?: string;\n }> => {\n try {\n // Use SDK's getCurrentSession() to check for existing session\n const sessionResult = insforge.auth.getCurrentSession();\n const session = sessionResult.data?.session;\n const token = session?.accessToken || null;\n\n if (!token) {\n // No token, user is not authenticated\n setUser(null);\n if (onAuthChange) {\n onAuthChange(null);\n }\n setIsLoaded(true);\n return { success: false, error: 'no_session' };\n }\n\n const userResult = await insforge.auth.getCurrentUser();\n\n if (userResult.data) {\n // Token is valid, update user state with fresh data\n const profile = userResult.data.profile;\n const userData: InsforgeUser = {\n id: userResult.data.user.id,\n email: userResult.data.user.email,\n name: (profile?.nickname as string | undefined) || '',\n avatarUrl: (profile?.avatarUrl as string | undefined) || '',\n };\n\n setUser(userData);\n\n if (onAuthChange) {\n onAuthChange(userData);\n }\n setIsLoaded(true);\n return { success: true };\n } else {\n // Token invalid or expired\n await insforge.auth.signOut();\n\n // Clear cookie if function provided\n if (onSignOut) {\n try {\n await onSignOut();\n } catch (error: unknown) {\n if (error instanceof Error) {\n console.error('[InsforgeProvider] Error clearing cookie:', error.message);\n }\n }\n }\n\n setUser(null);\n if (onAuthChange) {\n onAuthChange(null);\n }\n setIsLoaded(true);\n return { success: false, error: 'invalid_token' };\n }\n } catch (error) {\n // Token validation failed\n console.error('[InsforgeProvider] Token validation failed:', error);\n\n await insforge.auth.signOut();\n\n // Clear cookie if function provided\n if (onSignOut) {\n try {\n await onSignOut();\n } catch (error: unknown) {\n if (error instanceof Error) {\n console.error('[InsforgeProvider] Error clearing cookie:', error.message);\n }\n }\n }\n\n setUser(null);\n if (onAuthChange) {\n onAuthChange(null);\n }\n setIsLoaded(true);\n\n return {\n success: false,\n error: error instanceof Error ? error.message : 'Authentication failed',\n };\n }\n }, [insforge, onAuthChange, onSignOut]);\n\n useEffect(() => {\n // Run loadAuthState only once on mount\n loadAuthState();\n\n // Capture the ref value when effect runs\n const intervalId = refreshIntervalRef.current;\n\n return () => {\n // Use the captured value in cleanup\n if (intervalId) {\n clearInterval(intervalId);\n }\n };\n }, [loadAuthState]);\n\n // Handle authentication callback from URL\n useEffect(() => {\n // Only run once and only after auth is loaded\n if (!isLoaded || hasProcessedCallbackRef.current) {\n return;\n }\n\n const searchParams = new URLSearchParams(window.location.search);\n const accessToken = searchParams.get('access_token');\n\n if (accessToken && !!user) {\n // Mark as processed\n hasProcessedCallbackRef.current = true;\n\n // Clean URL by removing query parameters\n const url = new URL(window.location.href);\n url.search = '';\n window.history.replaceState({}, '', url.toString());\n\n // Redirect to afterSignInUrl\n setTimeout(() => {\n window.location.href = afterSignInUrl;\n }, 100);\n }\n }, [isLoaded, user, afterSignInUrl]);\n\n const getPublicAuthConfig = useCallback(async () => {\n try {\n const result = await insforge.auth.getPublicAuthConfig();\n if (result.data) {\n return result.data;\n } else {\n console.error('[InsforgeProvider] Failed to get public auth config:', result.error);\n return null;\n }\n } catch (error) {\n console.error('[InsforgeProvider] Failed to get public auth config:', error);\n return null;\n }\n }, [insforge]);\n\n /**\n * Helper function to handle successful authentication\n */\n const handleAuthSuccess = useCallback(\n async (authToken: string, fallbackUser?: { id?: string; email?: string; name?: string }) => {\n const userResult = await insforge.auth.getCurrentUser();\n\n if (userResult.data) {\n const profile = userResult.data.profile;\n const userData: InsforgeUser = {\n id: userResult.data.user.id,\n email: userResult.data.user.email,\n name: (profile?.nickname as string | undefined) || '',\n avatarUrl: (profile?.avatarUrl as string | undefined) || '',\n };\n\n setUser(userData);\n\n if (onAuthChange) {\n onAuthChange(userData);\n }\n\n // Try to sync token to cookie if function provided\n if (onSignIn) {\n try {\n await onSignIn(authToken);\n } catch (error: unknown) {\n if (error instanceof Error) {\n console.error('[InsforgeProvider] Error syncing token to cookie:', error.message);\n }\n }\n }\n } else if (fallbackUser) {\n // Fallback to basic user data if getCurrentUser fails\n const userData: InsforgeUser = {\n id: fallbackUser.id || '',\n email: fallbackUser.email || '',\n name: fallbackUser.name || '',\n avatarUrl: '',\n };\n\n setUser(userData);\n\n if (onAuthChange) {\n onAuthChange(userData);\n }\n }\n },\n [insforge, onAuthChange, onSignIn]\n );\n\n const signIn = useCallback(\n async (email: string, password: string) => {\n const sdkResult = await insforge.auth.signInWithPassword({\n email,\n password,\n });\n\n if (sdkResult.data) {\n await handleAuthSuccess(\n sdkResult.data.accessToken || '',\n sdkResult.data.user\n ? {\n id: sdkResult.data.user.id,\n email: sdkResult.data.user.email,\n name: sdkResult.data.user.name,\n }\n : undefined\n );\n return sdkResult.data;\n } else {\n // Return the full error object to preserve statusCode and other properties\n return {\n error: sdkResult.error?.message || 'Invalid email or password',\n statusCode: sdkResult.error?.statusCode,\n errorCode: sdkResult.error?.error,\n };\n }\n },\n [insforge, handleAuthSuccess]\n );\n\n const signUp = useCallback(\n async (email: string, password: string) => {\n const sdkResult = await insforge.auth.signUp({ email, password });\n\n if (sdkResult.data) {\n // Only set auth state if we got a token back (email verification might be required)\n if (sdkResult.data.accessToken) {\n await handleAuthSuccess(\n sdkResult.data.accessToken,\n sdkResult.data.user\n ? {\n id: sdkResult.data.user.id,\n email: sdkResult.data.user.email,\n name: sdkResult.data.user.name,\n }\n : undefined\n );\n }\n return sdkResult.data;\n } else {\n // Return the full error object to preserve statusCode and other properties\n return {\n error: sdkResult.error?.message || 'Sign up failed',\n statusCode: sdkResult.error?.statusCode,\n errorCode: sdkResult.error?.error,\n };\n }\n },\n [insforge, handleAuthSuccess]\n );\n\n const signOut = useCallback(async () => {\n await insforge.auth.signOut();\n\n // Clear cookie if function provided\n if (onSignOut) {\n try {\n await onSignOut();\n } catch (error: unknown) {\n if (error instanceof Error) {\n console.error('[InsforgeProvider] Error clearing cookie:', error.message);\n }\n }\n }\n\n // Clear refresh interval if exists\n if (refreshIntervalRef.current) {\n clearInterval(refreshIntervalRef.current);\n }\n\n setUser(null);\n if (onAuthChange) {\n onAuthChange(null);\n }\n }, [insforge, onAuthChange, onSignOut]);\n\n const updateUser = useCallback(\n async (data: Partial<InsforgeUser>) => {\n if (!user) {\n throw new Error('No user signed in');\n }\n\n const profileUpdate: Record<string, string | undefined> = {\n nickname: data.name,\n avatarUrl: data.avatarUrl,\n };\n\n const result = await insforge.auth.setProfile(profileUpdate);\n\n if (result.data) {\n const userResult = await insforge.auth.getCurrentUser();\n if (userResult.data) {\n const profile = userResult.data.profile;\n const updatedUser: InsforgeUser = {\n id: userResult.data.user.id,\n email: userResult.data.user.email,\n name: profile?.nickname || '',\n avatarUrl: profile?.avatarUrl || '',\n };\n setUser(updatedUser);\n if (onAuthChange) {\n onAuthChange(updatedUser);\n }\n }\n }\n },\n [user, onAuthChange, insforge]\n );\n\n const sendVerificationEmail = useCallback(\n async (email: string) => {\n const sdkResult = await insforge.auth.sendVerificationEmail({ email });\n return sdkResult.data;\n },\n [insforge]\n );\n\n const sendResetPasswordEmail = useCallback(\n async (email: string) => {\n const sdkResult = await insforge.auth.sendResetPasswordEmail({ email });\n return sdkResult.data;\n },\n [insforge]\n );\n\n const resetPassword = useCallback(\n async (token: string, newPassword: string) => {\n const sdkResult = await insforge.auth.resetPassword({\n newPassword,\n otp: token,\n });\n return sdkResult.data;\n },\n [insforge]\n );\n\n const verifyEmail = useCallback(\n async (otp: string, email?: string) => {\n const sdkResult = await insforge.auth.verifyEmail({ otp, email: email || undefined });\n if (sdkResult.data) {\n return sdkResult.data;\n } else {\n return {\n accessToken: '',\n error: {\n message: sdkResult.error?.message || 'Email verification failed',\n },\n };\n }\n },\n [insforge]\n );\n\n const exchangeResetPasswordToken = useCallback(\n async (email: string, code: string) => {\n const sdkResult = await insforge.auth.exchangeResetPasswordToken({ email, code });\n if (sdkResult.data) {\n return sdkResult.data;\n } else {\n return {\n error: {\n message: sdkResult.error?.message || 'Failed to exchange reset password token',\n },\n };\n }\n },\n [insforge]\n );\n\n const loginWithOAuth = useCallback(\n async (provider: OAuthProvider, redirectTo: string) => {\n const sdkResult = await insforge.auth.signInWithOAuth({\n provider,\n redirectTo: redirectTo || afterSignInUrl,\n });\n if (sdkResult.error) {\n throw new Error(sdkResult.error.message);\n }\n },\n [insforge, afterSignInUrl]\n );\n\n return (\n <InsforgeContext.Provider\n value={{\n user,\n isLoaded,\n isSignedIn: !!user,\n setUser,\n signIn,\n signUp,\n signOut,\n updateUser,\n reloadAuth: loadAuthState,\n baseUrl,\n sendVerificationEmail,\n sendResetPasswordEmail,\n resetPassword,\n verifyEmail,\n exchangeResetPasswordToken,\n getPublicAuthConfig,\n loginWithOAuth,\n }}\n >\n {children}\n </InsforgeContext.Provider>\n );\n}\n\n/**\n * Hook to access Insforge context\n *\n * @example\n * ```tsx\n * function MyComponent() {\n * const { user, isSignedIn, signOut } = useInsforge();\n *\n * if (!isSignedIn) return <SignIn />;\n *\n * return (\n * <div>\n * <p>Welcome {user.email}</p>\n * <button onClick={signOut}>Sign Out</button>\n * </div>\n * );\n * }\n * ```\n */\nexport function useInsforge(): InsforgeContextValue {\n const context = useContext(InsforgeContext);\n if (!context) {\n throw new Error('useInsforge must be used within InsforgeProvider');\n }\n return context;\n}\n","import { useInsforge } from '../provider/InsforgeProvider';\n\n/**\n * Hook to access authentication methods\n *\n * @returns Object containing:\n * - `signIn`: Function to sign in with email and password\n * - `signUp`: Function to sign up with email and password\n * - `signOut`: Function to sign out the current user\n * - `isLoaded`: Boolean indicating if auth state has been loaded\n * - `isSignedIn`: Boolean indicating if user is currently signed in\n *\n * @example\n * ```tsx\n * function LoginForm() {\n * const { signIn, signUp, signOut, isLoaded, isSignedIn } = useAuth();\n *\n * async function handleLogin(email: string, password: string) {\n * try {\n * await signIn(email, password);\n * // User is now signed in\n * } catch (error) {\n * console.error('Sign in failed:', error);\n * }\n * }\n *\n * if (!isLoaded) return <div>Loading...</div>;\n *\n * return (\n * <form onSubmit={(e) => { e.preventDefault(); handleLogin(email, password); }}>\n * {/* form fields *\\/}\n * </form>\n * );\n * }\n * ```\n */\nexport function useAuth() {\n const { signIn, signUp, signOut, isLoaded, isSignedIn } = useInsforge();\n return { signIn, signUp, signOut, isLoaded, isSignedIn };\n}\n","import { useInsforge } from '../provider/InsforgeProvider';\n\n/**\n * Hook to access current user data\n *\n * @returns Object containing:\n * - `user`: Current user object (InsforgeUser | null)\n * - `isLoaded`: Boolean indicating if auth state has been loaded\n * - `updateUser`: Function to update user profile data\n * - `setUser`: Internal function to manually set user state\n *\n * @example\n * ```tsx\n * function UserProfile() {\n * const { user, isLoaded, updateUser } = useUser();\n *\n * if (!isLoaded) return <div>Loading...</div>;\n * if (!user) return <div>Not signed in</div>;\n *\n * async function handleUpdate(name: string) {\n * await updateUser({ name });\n * }\n *\n * return (\n * <div>\n * <p>Email: {user.email}</p>\n * {user.name && <p>Name: {user.name}</p>}\n * {user.avatarUrl && <img src={user.avatarUrl} alt=\"Avatar\" />}\n * </div>\n * );\n * }\n * ```\n */\nexport function useUser() {\n const { user, isLoaded, updateUser, setUser } = useInsforge();\n return { user, isLoaded, updateUser, setUser };\n}\n","import { useState, useEffect } from 'react';\nimport type { GetPublicAuthConfigResponse } from '@insforge/shared-schemas';\nimport { useInsforge } from '../provider/InsforgeProvider';\n\n/**\n * Hook to get all public authentication configuration (OAuth + Email) from Insforge backend\n *\n * **IMPORTANT: This hook should ONLY be used in SignIn and SignUp components.**\n *\n * This hook lazily fetches all public authentication configuration from the backend\n * only when the component mounts. Using it in other components will cause unnecessary\n * API calls on every page load.\n *\n * @returns Object containing OAuth providers, email auth config, and loading state\n * - `oauthProviders`: Array of enabled OAuth provider names (e.g., ['google', 'github'])\n * - `authConfig`: Email authentication configuration object with password rules\n * - `isLoaded`: Boolean indicating if the config has been fetched\n *\n * @example\n * ```tsx\n * // ✅ Correct usage - only in SignIn/SignUp components\n * function SignUp() {\n * const { oauthProviders, authConfig, isLoaded } = usePublicAuthConfig();\n *\n * if (!isLoaded) return <div>Loading...</div>;\n *\n * return (\n * <div>\n * <p>OAuth providers: {oauthProviders.length}</p>\n * <p>Password min length: {authConfig?.passwordMinLength}</p>\n * </div>\n * );\n * }\n * ```\n *\n * @requires Must be used within InsforgeProvider\n */\nexport function usePublicAuthConfig(): {\n authConfig: GetPublicAuthConfigResponse | null;\n isLoaded: boolean;\n} {\n const { getPublicAuthConfig } = useInsforge();\n const [authConfig, setAuthConfig] = useState<GetPublicAuthConfigResponse | null>(null);\n const [isLoaded, setIsLoaded] = useState(false);\n\n useEffect(() => {\n async function fetchConfig() {\n const result = await getPublicAuthConfig();\n if (result) {\n setAuthConfig(result);\n } else {\n console.error('[usePublicAuthConfig] Failed to get public auth config');\n setAuthConfig(null);\n }\n setIsLoaded(true);\n }\n\n fetchConfig();\n }, [getPublicAuthConfig]);\n\n return { authConfig, isLoaded };\n}\n"]}
package/dist/index.cjs CHANGED
@@ -5,7 +5,7 @@ if (typeof document !== 'undefined' && typeof window !== 'undefined') {
5
5
  if (!document.getElementById(styleId)) {
6
6
  const style = document.createElement('style');
7
7
  style.id = styleId;
8
- style.textContent = "/**\n * InsForge React Component Library Styles\n * Traditional CSS with scoped class names (no Tailwind)\n */\n\n/* ============================================\n FONTS\n ============================================ */\n@import url('https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap');\n@import url('https://fonts.googleapis.com/css2?family=Manrope:wght@200..800&display=swap');\n\n/* ============================================\n CSS VARIABLES\n ============================================ */\n:root {\n /* Colors */\n --if-color-primary: #000000;\n --if-color-primary-hover: #1f1f1f;\n --if-color-text: #000000;\n --if-color-text-secondary: #828282;\n --if-color-text-muted: #a3a3a3;\n --if-color-border: #d4d4d4;\n --if-color-border-focus: #000000;\n --if-color-bg-white: #ffffff;\n --if-color-bg-light: #fafafa;\n --if-color-bg-hover: #f9fafb;\n --if-color-error: #dc2626;\n --if-color-error-bg: #fee2e2;\n --if-color-success: #16a34a;\n\n /* Spacing */\n --if-space-1: 0.25rem; /* 4px */\n --if-space-2: 0.5rem; /* 8px */\n --if-space-3: 0.75rem; /* 12px */\n --if-space-4: 1rem; /* 16px */\n --if-space-6: 1.5rem; /* 24px */\n --if-space-8: 2rem; /* 32px */\n\n /* Border Radius */\n --if-radius-xs: 0.125rem; /* 2px */\n --if-radius-sm: 0.25rem; /* 4px */\n --if-radius-md: 0.375rem; /* 6px */\n --if-radius-lg: 0.5rem; /* 8px */\n --if-radius-xl: 0.75rem; /* 12px */\n\n /* Typography */\n --if-font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;\n --if-font-family-manrope:\n 'Manrope', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;\n --if-font-size-xs: 0.75rem; /* 12px */\n --if-font-size-sm: 0.875rem; /* 14px */\n --if-font-size-base: 1rem; /* 16px */\n --if-font-size-lg: 1.125rem; /* 18px */\n --if-font-size-xl: 1.25rem; /* 20px */\n --if-font-size-2xl: 1.5rem; /* 24px */\n\n /* Shadows */\n --if-shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);\n --if-shadow-md: 0 1px 2px 0 rgba(0, 0, 0, 0.1);\n --if-shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);\n\n /* Transitions */\n --if-transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1);\n --if-transition-base: 200ms cubic-bezier(0.4, 0, 0.2, 1);\n}\n\n/* ============================================\n AUTH CONTAINER\n ============================================ */\n.if-authContainer {\n width: 100%;\n max-width: 400px;\n border-radius: var(--if-radius-xl);\n overflow: hidden;\n box-shadow: var(--if-shadow-lg);\n}\n\n.if-authCard {\n background-color: var(--if-color-bg-white);\n padding: var(--if-space-6);\n display: flex;\n flex-direction: column;\n justify-content: center;\n align-items: stretch;\n gap: var(--if-space-6);\n}\n\n/* ============================================\n AUTH HEADER\n ============================================ */\n.if-authHeader {\n display: flex;\n flex-direction: column;\n justify-content: flex-start;\n align-items: flex-start;\n gap: var(--if-space-2);\n}\n\n.if-authHeader-title {\n font-size: var(--if-font-size-2xl);\n font-weight: 600;\n color: var(--if-color-text);\n line-height: 2rem;\n margin: 0;\n font-family: var(--if-font-family);\n}\n\n.if-authHeader-subtitle {\n font-size: var(--if-font-size-sm);\n font-weight: 400;\n color: var(--if-color-text-secondary);\n line-height: 1.5rem;\n margin: 0;\n font-family: var(--if-font-family);\n}\n\n/* ============================================\n FORM FIELD\n ============================================ */\n.if-formField {\n display: flex;\n flex-direction: column;\n justify-content: center;\n align-items: stretch;\n gap: var(--if-space-1);\n}\n\n.if-formField-label {\n font-size: var(--if-font-size-sm);\n font-weight: 400;\n color: var(--if-color-text);\n line-height: 1.5rem;\n font-family: var(--if-font-family);\n}\n\n.if-formField-input {\n width: 100%;\n display: flex;\n align-items: center;\n gap: var(--if-space-2);\n align-self: stretch;\n padding: var(--if-space-2) var(--if-space-3);\n border-radius: var(--if-radius-sm);\n border: 1px solid var(--if-color-border);\n background-color: var(--if-color-bg-white);\n font-size: var(--if-font-size-sm);\n font-weight: 400;\n line-height: 1.25rem;\n color: var(--if-color-text);\n font-family: var(--if-font-family);\n transition: border-color var(--if-transition-base);\n}\n\n.if-formField-input::placeholder {\n color: var(--if-color-text-muted);\n font-size: var(--if-font-size-sm);\n font-weight: 400;\n}\n\n.if-formField-input:focus {\n outline: none;\n border-color: var(--if-color-border-focus);\n}\n\n/* ============================================\n PASSWORD FIELD\n ============================================ */\n.if-passwordField {\n display: flex;\n flex-direction: column;\n justify-content: center;\n align-items: stretch;\n gap: var(--if-space-1);\n}\n\n.if-passwordField-labelRow {\n display: flex;\n justify-content: space-between;\n align-items: center;\n}\n\n.if-passwordField-label {\n font-size: var(--if-font-size-sm);\n font-weight: 400;\n color: var(--if-color-text);\n line-height: 1.5rem;\n font-family: var(--if-font-family);\n}\n\n.if-passwordField-forgotLink {\n font-size: var(--if-font-size-sm);\n font-weight: 400;\n color: var(--if-color-text-secondary);\n text-decoration: none;\n transition: color var(--if-transition-fast);\n font-family: var(--if-font-family);\n}\n\n.if-passwordField-inputWrapper {\n position: relative;\n width: 100%;\n}\n\n.if-passwordField-input {\n width: 100%;\n display: flex;\n align-items: center;\n align-self: stretch;\n padding: var(--if-space-2) var(--if-space-3);\n padding-right: 2.5rem; /* Space for toggle button */\n border-radius: var(--if-radius-sm);\n border: 1px solid var(--if-color-border);\n background-color: var(--if-color-bg-white);\n font-size: var(--if-font-size-sm);\n font-weight: 400;\n line-height: 1.25rem;\n color: var(--if-color-text);\n font-family: var(--if-font-family);\n transition: border-color var(--if-transition-base);\n}\n\n.if-passwordField-input::placeholder {\n color: var(--if-color-text-muted);\n}\n\n.if-passwordField-input:focus {\n outline: none;\n border-color: var(--if-color-border-focus);\n}\n\n.if-passwordField-toggleButton {\n position: absolute;\n right: var(--if-space-1);\n top: 50%;\n transform: translateY(-50%);\n background: none;\n border: none;\n cursor: pointer;\n padding: var(--if-space-1);\n display: flex;\n align-items: center;\n justify-content: center;\n color: var(--if-color-text-secondary);\n transition: color var(--if-transition-fast);\n}\n\n/* ============================================\n SUBMIT BUTTON\n ============================================ */\n.if-submitButton {\n border-radius: var(--if-radius-sm);\n background-color: var(--if-color-primary);\n height: 2.5rem;\n width: 100%;\n display: flex;\n margin-top: var(--if-space-4);\n padding: var(--if-space-2) var(--if-space-4);\n justify-content: center;\n align-items: center;\n gap: 0.625rem;\n align-self: stretch;\n color: var(--if-color-bg-white);\n font-weight: 600;\n font-family: var(--if-font-family-manrope);\n font-size: var(--if-font-size-base);\n line-height: normal;\n border: none;\n cursor: pointer;\n transition: background-color var(--if-transition-base);\n}\n\n.if-submitButton:hover:not(:disabled) {\n background-color: var(--if-color-primary-hover);\n}\n\n.if-submitButton:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n}\n\n.if-submitButton-icon {\n width: 1.25rem;\n height: 1.25rem;\n}\n\n/* Spinner animation */\n@keyframes if-spin {\n from {\n transform: rotate(0deg);\n }\n to {\n transform: rotate(360deg);\n }\n}\n\n.if-submitButton-spinner {\n animation: if-spin 1s linear infinite;\n}\n\n/* ============================================\n OAUTH BUTTON\n ============================================ */\n.if-oauthButton {\n display: flex;\n width: 100%;\n height: 2.25rem;\n padding: var(--if-space-2) var(--if-space-3);\n flex-direction: row;\n justify-content: center;\n align-items: center;\n gap: var(--if-space-3);\n border-radius: var(--if-radius-md);\n border: 1px solid #e4e4e7;\n background-color: var(--if-color-bg-white);\n box-shadow: var(--if-shadow-md);\n color: #09090b;\n text-align: center;\n font-size: var(--if-font-size-sm);\n font-weight: 500;\n line-height: 1.25rem;\n cursor: pointer;\n transition: all var(--if-transition-base);\n font-family: var(--if-font-family);\n}\n\n.if-oauthButton:hover:not(:disabled) {\n background-color: var(--if-color-bg-hover);\n border-color: #9ca3af;\n}\n\n.if-oauthButton:disabled {\n opacity: 0.6;\n cursor: not-allowed;\n}\n\n.if-oauthButton-icon {\n display: flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n width: 1.125rem;\n height: 1.125rem;\n}\n\n.if-oauthButton-full {\n justify-content: center;\n}\n\n.if-oauthButton-short {\n justify-content: center;\n padding: var(--if-space-2);\n gap: var(--if-space-2);\n}\n\n.if-oauthButton-icon-only {\n justify-content: center;\n gap: 0;\n}\n\n/* ============================================\n OAUTH PROVIDERS CONTAINER\n ============================================ */\n.if-oauthProviders {\n display: flex;\n flex-direction: column;\n gap: var(--if-space-3);\n width: 100%;\n}\n\n/* ============================================\n AUTH LINK\n ============================================ */\n.if-authLink {\n display: flex;\n justify-content: center;\n align-items: center;\n gap: var(--if-space-1);\n font-size: var(--if-font-size-sm);\n color: var(--if-color-text-secondary);\n font-family: var(--if-font-family);\n}\n\n.if-authLink-text {\n font-weight: 400;\n}\n\n.if-authLink-link {\n font-weight: 600;\n color: var(--if-color-text);\n text-decoration: none;\n transition: opacity var(--if-transition-fast);\n}\n\n/* ============================================\n AUTH DIVIDER\n ============================================ */\n.if-authDivider {\n display: flex;\n align-items: center;\n text-align: center;\n width: 100%;\n gap: var(--if-space-3);\n}\n\n.if-authDivider-line {\n flex: 1;\n border-top: 1px solid var(--if-color-border);\n}\n\n.if-authDivider-text {\n font-size: var(--if-font-size-sm);\n color: var(--if-color-text-secondary);\n font-weight: 400;\n font-family: var(--if-font-family-manrope);\n}\n\n/* ============================================\n ERROR BANNER\n ============================================ */\n.if-errorBanner {\n padding: var(--if-space-3);\n background-color: var(--if-color-error-bg);\n border-radius: var(--if-radius-md);\n border: 1px solid var(--if-color-error);\n}\n\n.if-errorBanner-content {\n display: flex;\n align-items: center;\n gap: var(--if-space-2);\n}\n\n.if-errorBanner-icon {\n width: 1.5rem;\n height: 1.5rem;\n flex-shrink: 0;\n color: var(--if-color-error);\n}\n\n.if-errorBanner-text {\n font-size: var(--if-font-size-sm);\n color: var(--if-color-error);\n font-weight: 400;\n font-family: var(--if-font-family);\n margin: 0;\n}\n\n/* ============================================\n AUTH BRANDING\n ============================================ */\n.if-authBranding {\n background-color: var(--if-color-bg-light);\n padding: var(--if-space-4) var(--if-space-2);\n display: flex;\n flex-direction: row;\n justify-content: center;\n align-items: center;\n gap: var(--if-space-1);\n}\n\n.if-authBranding-text {\n font-size: var(--if-font-size-xs);\n font-weight: 500;\n color: var(--if-color-text);\n font-family: var(--if-font-family-manrope);\n margin: 0;\n}\n\n/* ============================================\n VERIFICATION CODE INPUT\n ============================================ */\n.if-verificationCode-inputContainer {\n display: flex;\n gap: var(--if-space-3);\n justify-content: center;\n}\n\n.if-verificationCode-input {\n width: 3rem;\n height: 3rem;\n text-align: center;\n font-size: var(--if-font-size-base);\n font-weight: 600;\n border: 1px solid var(--if-color-border);\n border-radius: var(--if-radius-sm);\n transition: border-color var(--if-transition-base);\n font-family: var(--if-font-family-manrope);\n}\n\n.if-verificationCode-input:focus {\n outline: none;\n border-color: var(--if-color-border-focus);\n}\n\n/* ============================================\n VERIFICATION STEP\n ============================================ */\n.if-verificationStep {\n display: flex;\n flex-direction: column;\n gap: var(--if-space-6);\n align-items: stretch;\n}\n\n.if-verificationStep-descriptionContainer {\n width: 100%;\n background-color: #f5f5f5;\n border-radius: var(--if-radius-lg);\n padding: var(--if-space-3) var(--if-space-3) var(--if-space-6) var(--if-space-3);\n display: flex;\n flex-direction: column;\n gap: var(--if-space-3);\n}\n\n.if-verificationStep-descriptionTitle {\n color: var(--black, #000);\n font-family: var(--if-font-family);\n font-size: var(--if-font-size-base);\n font-style: normal;\n font-weight: 600;\n line-height: 24px;\n}\n\n.if-verificationStep-description,\n.if-verificationStep-codeDescription {\n font-size: var(--if-font-size-sm);\n color: #525252;\n text-align: left;\n font-family: var(--if-font-family);\n margin: 0;\n}\n\n.if-verificationLink-email,\n.if-verificationCode-email {\n font-weight: 500;\n color: var(--if-color-text);\n}\n\n.if-verificationStep-codeContainer {\n width: 100%;\n display: flex;\n flex-direction: column;\n gap: 40px;\n}\n\n.if-verificationStep-codeInputWrapper {\n display: flex;\n flex-direction: column;\n gap: var(--if-space-6);\n}\n\n.if-verificationStep-verifyingText {\n font-size: var(--if-font-size-sm);\n color: var(--if-color-text-secondary);\n text-align: center;\n font-family: var(--if-font-family);\n}\n\n.if-verificationStep-resendContainer {\n width: 100%;\n font-size: var(--if-font-size-sm);\n text-align: center;\n color: var(--if-color-text-secondary);\n font-family: var(--if-font-family);\n}\n\n.if-verificationStep-resendButton {\n color: var(--if-color-text);\n font-weight: 500;\n transition: all var(--if-transition-base);\n background: none;\n border: none;\n padding: 0;\n font-family: var(--if-font-family);\n font-size: var(--if-font-size-sm);\n}\n\n.if-verificationStep-resendButton:not(:disabled) {\n cursor: pointer;\n}\n\n.if-verificationStep-resendButton:disabled {\n cursor: not-allowed;\n opacity: 0.5;\n}\n\n/* ============================================\n PASSWORD STRENGTH INDICATOR\n ============================================ */\n.if-passwordStrength {\n margin-top: var(--if-space-2);\n}\n\n.if-passwordStrength-fill {\n height: 100%;\n transition:\n width var(--if-transition-base),\n background-color var(--if-transition-base);\n}\n\n.if-passwordStrength-text {\n font-size: var(--if-font-size-xs);\n color: var(--if-color-text-secondary);\n font-family: var(--if-font-family);\n}\n\n.if-passwordStrength-requirements {\n display: flex;\n flex-direction: column;\n gap: var(--if-space-2);\n font-size: var(--if-font-size-sm);\n color: #525252;\n font-family: var(--if-font-family);\n}\n\n.if-passwordStrength-requirement {\n display: flex;\n align-items: center;\n gap: var(--if-space-2);\n}\n\n.if-passwordStrength-requirement.met {\n color: var(--if-color-success);\n}\n\n.if-passwordStrength-requirement.unmet {\n color: var(--if-color-text-muted);\n}\n\n/* ============================================\n FORM CONTAINER\n ============================================ */\n.if-form {\n display: flex;\n flex-direction: column;\n align-items: stretch;\n justify-content: center;\n gap: var(--if-space-6);\n}\n\n/* ============================================\n USER BUTTON\n ============================================ */\n.if-userButton-container {\n position: relative;\n display: inline-block;\n}\n\n.if-userButton {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: var(--if-space-2);\n padding: var(--if-space-2);\n border-radius: 9999px;\n background-color: transparent;\n border: none;\n cursor: pointer;\n transition: all var(--if-transition-base);\n}\n\n.if-userButton:hover {\n opacity: 0.8;\n}\n\n.if-userButton-detailed {\n background-color: var(--if-color-bg-white);\n border: 1px solid var(--if-color-border);\n border-radius: var(--if-radius-sm);\n padding: var(--if-space-2);\n}\n\n.if-userButton-detailed:hover {\n background-color: var(--if-color-bg-light);\n opacity: 1;\n}\n\n.if-userButton-avatar {\n width: 2rem;\n height: 2rem;\n border-radius: 9999px;\n background-color: var(--if-color-primary);\n color: var(--if-color-bg-white);\n display: flex;\n align-items: center;\n justify-content: center;\n font-weight: 600;\n font-size: var(--if-font-size-sm);\n font-family: var(--if-font-family);\n overflow: hidden;\n}\n\n.if-userButton-avatarImage {\n border-radius: 9999px;\n object-fit: cover;\n width: 100%;\n height: 100%;\n}\n\n.if-userButton-avatarInitials {\n color: var(--if-color-bg-white);\n font-weight: 600;\n font-size: var(--if-font-size-sm);\n}\n\n.if-userButton-info {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 0.125rem;\n}\n\n.if-userButton-name {\n font-size: var(--if-font-size-sm);\n font-weight: 600;\n color: var(--if-color-text);\n line-height: 1.25rem;\n text-align: left;\n font-family: var(--if-font-family);\n}\n\n.if-userButton-email {\n font-size: var(--if-font-size-xs);\n color: var(--if-color-text-secondary);\n line-height: 1rem;\n text-align: left;\n font-family: var(--if-font-family);\n}\n\n.if-userButton-menu {\n position: absolute;\n margin-top: var(--if-space-2);\n background-color: var(--if-color-bg-white);\n border: 1px solid var(--if-color-border);\n border-radius: var(--if-radius-md);\n box-shadow: var(--if-shadow-lg);\n padding: var(--if-space-2);\n min-width: 200px;\n z-index: 50;\n}\n\n.if-userButton-menuItem {\n display: flex;\n align-items: center;\n gap: var(--if-space-2);\n padding: var(--if-space-2);\n border-radius: var(--if-radius-sm);\n cursor: pointer;\n transition: background-color var(--if-transition-fast);\n font-size: var(--if-font-size-sm);\n color: var(--if-color-text);\n font-family: var(--if-font-family);\n background: none;\n border: none;\n width: 100%;\n text-align: left;\n}\n\n.if-userButton-menuItem:hover {\n background-color: var(--if-color-bg-light);\n}\n\n.if-userButton-menuItem-signout {\n color: var(--if-color-error);\n}\n\n.if-userButton-menuItem-icon {\n width: 1.25rem;\n height: 1.25rem;\n}\n\n/* ============================================\n EMAIL VERIFICATION STATUS\n ============================================ */\n.if-verifyStatus-container {\n width: 100%;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: var(--if-space-6);\n}\n\n.if-verifyStatus-container-stretch {\n width: 100%;\n display: flex;\n flex-direction: column;\n align-items: stretch;\n justify-content: center;\n gap: var(--if-space-6);\n}\n\n.if-verifyStatus-spinner {\n border-radius: 9999px;\n height: 3rem;\n width: 3rem;\n border-bottom: 2px solid var(--if-color-primary);\n}\n\n.if-verifyStatus-successContent {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: var(--if-space-4);\n}\n\n.if-verifyStatus-successIcon {\n width: 4rem;\n height: 4rem;\n border-radius: 9999px;\n background-color: #d1fae5;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.if-verifyStatus-successIconSvg {\n width: 2rem;\n height: 2rem;\n color: #059669;\n}\n\n.if-verifyStatus-textCenter {\n text-align: center;\n}\n\n/* ============================================\n UTILITY CLASSES\n ============================================ */\n.if-hidden {\n display: none;\n}\n\n.if-visuallyHidden {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border-width: 0;\n}\n";
8
+ style.textContent = "/**\n * InsForge React Component Library Styles\n * Traditional CSS with scoped class names (no Tailwind)\n */\n\n/* ============================================\n FONTS\n ============================================ */\n@import url('https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap');\n@import url('https://fonts.googleapis.com/css2?family=Manrope:wght@200..800&display=swap');\n\n/* ============================================\n CSS VARIABLES\n ============================================ */\n:root {\n /* Colors */\n --if-color-primary: #000000;\n --if-color-primary-hover: #1f1f1f;\n --if-color-text: #000000;\n --if-color-text-secondary: #828282;\n --if-color-text-muted: #a3a3a3;\n --if-color-border: #d4d4d4;\n --if-color-border-focus: #000000;\n --if-color-bg-white: #ffffff;\n --if-color-bg-light: #fafafa;\n --if-color-bg-hover: #f9fafb;\n --if-color-error: #dc2626;\n --if-color-error-bg: #fee2e2;\n --if-color-success: #16a34a;\n\n /* Spacing */\n --if-space-1: 0.25rem; /* 4px */\n --if-space-2: 0.5rem; /* 8px */\n --if-space-3: 0.75rem; /* 12px */\n --if-space-4: 1rem; /* 16px */\n --if-space-6: 1.5rem; /* 24px */\n --if-space-8: 2rem; /* 32px */\n\n /* Border Radius */\n --if-radius-xs: 0.125rem; /* 2px */\n --if-radius-sm: 0.25rem; /* 4px */\n --if-radius-md: 0.375rem; /* 6px */\n --if-radius-lg: 0.5rem; /* 8px */\n --if-radius-xl: 0.75rem; /* 12px */\n\n /* Typography */\n --if-font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;\n --if-font-family-manrope:\n 'Manrope', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;\n --if-font-size-xs: 0.75rem; /* 12px */\n --if-font-size-sm: 0.875rem; /* 14px */\n --if-font-size-base: 1rem; /* 16px */\n --if-font-size-lg: 1.125rem; /* 18px */\n --if-font-size-xl: 1.25rem; /* 20px */\n --if-font-size-2xl: 1.5rem; /* 24px */\n\n /* Shadows */\n --if-shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);\n --if-shadow-md: 0 1px 2px 0 rgba(0, 0, 0, 0.1);\n --if-shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);\n\n /* Transitions */\n --if-transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1);\n --if-transition-base: 200ms cubic-bezier(0.4, 0, 0.2, 1);\n}\n\n/* ============================================\n AUTH CONTAINER\n ============================================ */\n.if-authContainer {\n width: 100%;\n max-width: 400px;\n border-radius: var(--if-radius-xl);\n overflow: hidden;\n box-shadow: var(--if-shadow-lg);\n}\n\n.if-authCard {\n background-color: var(--if-color-bg-white);\n padding: var(--if-space-6);\n display: flex;\n flex-direction: column;\n justify-content: center;\n align-items: stretch;\n gap: var(--if-space-6);\n}\n\n/* ============================================\n AUTH HEADER\n ============================================ */\n.if-authHeader {\n display: flex;\n flex-direction: column;\n justify-content: flex-start;\n align-items: flex-start;\n gap: var(--if-space-2);\n}\n\n.if-authHeader-title {\n font-size: var(--if-font-size-2xl);\n font-weight: 600;\n color: var(--if-color-text);\n line-height: 2rem;\n margin: 0;\n font-family: var(--if-font-family);\n}\n\n.if-authHeader-subtitle {\n font-size: var(--if-font-size-sm);\n font-weight: 400;\n color: var(--if-color-text-secondary);\n line-height: 1.5rem;\n margin: 0;\n font-family: var(--if-font-family);\n}\n\n/* ============================================\n FORM FIELD\n ============================================ */\n.if-formField {\n display: flex;\n flex-direction: column;\n justify-content: center;\n align-items: stretch;\n gap: var(--if-space-1);\n}\n\n.if-formField-label {\n font-size: var(--if-font-size-sm);\n font-weight: 400;\n color: var(--if-color-text);\n line-height: 1.5rem;\n font-family: var(--if-font-family);\n}\n\n.if-formField-input {\n width: 100%;\n display: flex;\n align-items: center;\n gap: var(--if-space-2);\n align-self: stretch;\n padding: var(--if-space-2) var(--if-space-3);\n border-radius: var(--if-radius-sm);\n border: 1px solid var(--if-color-border);\n background-color: var(--if-color-bg-white);\n font-size: var(--if-font-size-sm);\n font-weight: 400;\n line-height: 1.25rem;\n color: var(--if-color-text);\n font-family: var(--if-font-family);\n transition: border-color var(--if-transition-base);\n}\n\n.if-formField-input::placeholder {\n color: var(--if-color-text-muted);\n font-size: var(--if-font-size-sm);\n font-weight: 400;\n}\n\n.if-formField-input:focus {\n outline: none;\n border-color: var(--if-color-border-focus);\n}\n\n/* ============================================\n PASSWORD FIELD\n ============================================ */\n.if-passwordField {\n display: flex;\n flex-direction: column;\n justify-content: center;\n align-items: stretch;\n gap: var(--if-space-1);\n}\n\n.if-passwordField-labelRow {\n display: flex;\n justify-content: space-between;\n align-items: center;\n}\n\n.if-passwordField-label {\n font-size: var(--if-font-size-sm);\n font-weight: 400;\n color: var(--if-color-text);\n line-height: 1.5rem;\n font-family: var(--if-font-family);\n}\n\n.if-passwordField-forgotLink {\n font-size: var(--if-font-size-sm);\n font-weight: 400;\n color: var(--if-color-text-secondary);\n text-decoration: none;\n transition: color var(--if-transition-fast);\n font-family: var(--if-font-family);\n}\n\n.if-passwordField-inputWrapper {\n position: relative;\n width: 100%;\n}\n\n.if-passwordField-input {\n width: 100%;\n display: flex;\n align-items: center;\n align-self: stretch;\n padding: var(--if-space-2) var(--if-space-3);\n padding-right: 2.5rem; /* Space for toggle button */\n border-radius: var(--if-radius-sm);\n border: 1px solid var(--if-color-border);\n background-color: var(--if-color-bg-white);\n font-size: var(--if-font-size-sm);\n font-weight: 400;\n line-height: 1.25rem;\n color: var(--if-color-text);\n font-family: var(--if-font-family);\n transition: border-color var(--if-transition-base);\n}\n\n.if-passwordField-input::placeholder {\n color: var(--if-color-text-muted);\n}\n\n.if-passwordField-input:focus {\n outline: none;\n border-color: var(--if-color-border-focus);\n}\n\n.if-passwordField-toggleButton {\n position: absolute;\n right: var(--if-space-1);\n top: 50%;\n transform: translateY(-50%);\n background: none;\n border: none;\n cursor: pointer;\n padding: var(--if-space-1);\n display: flex;\n align-items: center;\n justify-content: center;\n color: var(--if-color-text-secondary);\n transition: color var(--if-transition-fast);\n}\n\n/* ============================================\n SUBMIT BUTTON\n ============================================ */\n.if-submitButton {\n border-radius: var(--if-radius-sm);\n background-color: var(--if-color-primary);\n height: 2.5rem;\n width: 100%;\n display: flex;\n margin-top: var(--if-space-4);\n padding: var(--if-space-2) var(--if-space-4);\n justify-content: center;\n align-items: center;\n gap: 0.625rem;\n align-self: stretch;\n color: var(--if-color-bg-white);\n font-weight: 600;\n font-family: var(--if-font-family-manrope);\n font-size: var(--if-font-size-base);\n line-height: normal;\n border: none;\n cursor: pointer;\n transition: background-color var(--if-transition-base);\n}\n\n.if-submitButton:hover:not(:disabled) {\n background-color: var(--if-color-primary-hover);\n}\n\n.if-submitButton:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n}\n\n.if-submitButton-icon {\n width: 1.25rem;\n height: 1.25rem;\n}\n\n/* Spinner animation */\n@keyframes if-spin {\n from {\n transform: rotate(0deg);\n }\n to {\n transform: rotate(360deg);\n }\n}\n\n.if-submitButton-spinner {\n animation: if-spin 1s linear infinite;\n}\n\n/* ============================================\n OAUTH BUTTON\n ============================================ */\n.if-oauthButton {\n display: flex;\n width: 100%;\n height: 2.25rem;\n padding: var(--if-space-2) var(--if-space-3);\n flex-direction: row;\n justify-content: center;\n align-items: center;\n gap: var(--if-space-3);\n border-radius: var(--if-radius-md);\n border: 1px solid #e4e4e7;\n background-color: var(--if-color-bg-white);\n box-shadow: var(--if-shadow-md);\n color: #09090b;\n text-align: center;\n font-size: var(--if-font-size-sm);\n font-weight: 500;\n line-height: 1.25rem;\n cursor: pointer;\n transition: all var(--if-transition-base);\n font-family: var(--if-font-family);\n}\n\n.if-oauthButton:hover:not(:disabled) {\n background-color: var(--if-color-bg-hover);\n border-color: #9ca3af;\n}\n\n.if-oauthButton:disabled {\n opacity: 0.6;\n cursor: not-allowed;\n}\n\n.if-oauthButton-icon {\n display: flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n width: 1.125rem;\n height: 1.125rem;\n}\n\n.if-oauthButton-full {\n justify-content: center;\n}\n\n.if-oauthButton-short {\n justify-content: center;\n padding: var(--if-space-2);\n gap: var(--if-space-2);\n}\n\n.if-oauthButton-icon-only {\n justify-content: center;\n gap: 0;\n}\n\n/* ============================================\n OAUTH PROVIDERS CONTAINER\n ============================================ */\n.if-oauthProviders {\n display: flex;\n flex-direction: column;\n gap: var(--if-space-3);\n width: 100%;\n}\n\n/* ============================================\n AUTH LINK\n ============================================ */\n.if-authLink {\n display: flex;\n justify-content: center;\n align-items: center;\n gap: var(--if-space-1);\n font-size: var(--if-font-size-sm);\n color: var(--if-color-text-secondary);\n font-family: var(--if-font-family);\n}\n\n.if-authLink-text {\n font-weight: 400;\n}\n\n.if-authLink-link {\n font-weight: 600;\n color: var(--if-color-text);\n text-decoration: none;\n transition: opacity var(--if-transition-fast);\n}\n\n/* ============================================\n AUTH DIVIDER\n ============================================ */\n.if-authDivider {\n display: flex;\n align-items: center;\n text-align: center;\n width: 100%;\n gap: var(--if-space-3);\n}\n\n.if-authDivider-line {\n flex: 1;\n border-top: 1px solid var(--if-color-border);\n}\n\n.if-authDivider-text {\n font-size: var(--if-font-size-sm);\n color: var(--if-color-text-secondary);\n font-weight: 400;\n font-family: var(--if-font-family-manrope);\n}\n\n/* ============================================\n ERROR BANNER\n ============================================ */\n.if-errorBanner {\n padding: var(--if-space-3);\n background-color: var(--if-color-error-bg);\n border-radius: var(--if-radius-md);\n border: 1px solid var(--if-color-error);\n}\n\n.if-errorBanner-content {\n display: flex;\n align-items: center;\n gap: var(--if-space-2);\n}\n\n.if-errorBanner-icon {\n width: 1.5rem;\n height: 1.5rem;\n flex-shrink: 0;\n color: var(--if-color-error);\n}\n\n.if-errorBanner-text {\n font-size: var(--if-font-size-sm);\n color: var(--if-color-error);\n font-weight: 400;\n font-family: var(--if-font-family);\n margin: 0;\n}\n\n/* ============================================\n AUTH BRANDING\n ============================================ */\n.if-authBranding {\n background-color: var(--if-color-bg-light);\n padding: var(--if-space-4) var(--if-space-2);\n display: flex;\n flex-direction: row;\n justify-content: center;\n align-items: center;\n gap: var(--if-space-1);\n}\n\n.if-authBranding-text {\n font-size: var(--if-font-size-xs);\n font-weight: 500;\n color: var(--if-color-text);\n font-family: var(--if-font-family-manrope);\n margin: 0;\n}\n\n/* ============================================\n VERIFICATION CODE INPUT\n ============================================ */\n.if-verificationCode-inputContainer {\n display: flex;\n gap: var(--if-space-3);\n justify-content: center;\n}\n\n.if-verificationCode-input {\n width: 3rem;\n height: 3rem;\n text-align: center;\n font-size: var(--if-font-size-base);\n font-weight: 600;\n border: 1px solid var(--if-color-border);\n border-radius: var(--if-radius-sm);\n transition: border-color var(--if-transition-base);\n font-family: var(--if-font-family-manrope);\n}\n\n.if-verificationCode-input:focus {\n outline: none;\n border-color: var(--if-color-border-focus);\n}\n\n/* ============================================\n VERIFICATION STEP\n ============================================ */\n.if-verificationStep {\n display: flex;\n flex-direction: column;\n gap: var(--if-space-6);\n align-items: stretch;\n}\n\n.if-verificationStep-descriptionContainer {\n width: 100%;\n background-color: #f5f5f5;\n border-radius: var(--if-radius-lg);\n padding: var(--if-space-3) var(--if-space-3) var(--if-space-6) var(--if-space-3);\n display: flex;\n flex-direction: column;\n gap: var(--if-space-3);\n}\n\n.if-verificationStep-descriptionTitle {\n color: var(--black, #000);\n font-family: var(--if-font-family);\n font-size: var(--if-font-size-base);\n font-style: normal;\n font-weight: 600;\n line-height: 24px;\n}\n\n.if-verificationStep-description,\n.if-verificationStep-codeDescription {\n font-size: var(--if-font-size-sm);\n color: #525252;\n text-align: left;\n font-family: var(--if-font-family);\n margin: 0;\n}\n\n.if-verificationLink-email,\n.if-verificationCode-email {\n font-weight: 500;\n color: var(--if-color-text);\n}\n\n.if-verificationStep-codeContainer {\n width: 100%;\n display: flex;\n flex-direction: column;\n gap: 40px;\n}\n\n.if-verificationStep-codeInputWrapper {\n display: flex;\n flex-direction: column;\n gap: var(--if-space-6);\n}\n\n.if-verificationStep-verifyingText {\n font-size: var(--if-font-size-sm);\n color: var(--if-color-text-secondary);\n text-align: center;\n font-family: var(--if-font-family);\n}\n\n.if-verificationStep-resendContainer {\n width: 100%;\n font-size: var(--if-font-size-sm);\n text-align: center;\n color: var(--if-color-text-secondary);\n font-family: var(--if-font-family);\n}\n\n.if-verificationStep-resendButton {\n color: var(--if-color-text);\n font-weight: 500;\n transition: all var(--if-transition-base);\n background: none;\n border: none;\n padding: 0;\n font-family: var(--if-font-family);\n font-size: var(--if-font-size-sm);\n}\n\n.if-verificationStep-resendButton:not(:disabled) {\n cursor: pointer;\n}\n\n.if-verificationStep-resendButton:disabled {\n cursor: not-allowed;\n opacity: 0.5;\n}\n\n/* ============================================\n PASSWORD STRENGTH INDICATOR\n ============================================ */\n.if-passwordStrength {\n margin-top: var(--if-space-2);\n}\n\n.if-passwordStrength-fill {\n height: 100%;\n transition:\n width var(--if-transition-base),\n background-color var(--if-transition-base);\n}\n\n.if-passwordStrength-text {\n font-size: var(--if-font-size-xs);\n color: var(--if-color-text-secondary);\n font-family: var(--if-font-family);\n}\n\n.if-passwordStrength-requirements {\n display: flex;\n flex-direction: column;\n gap: var(--if-space-2);\n font-size: var(--if-font-size-sm);\n color: #525252;\n font-family: var(--if-font-family);\n}\n\n.if-passwordStrength-requirement {\n display: flex;\n align-items: center;\n gap: var(--if-space-2);\n}\n\n/* ============================================\n FORM CONTAINER\n ============================================ */\n.if-form {\n display: flex;\n flex-direction: column;\n align-items: stretch;\n justify-content: center;\n gap: var(--if-space-6);\n}\n\n/* ============================================\n USER BUTTON\n ============================================ */\n.if-userButton-container {\n position: relative;\n display: inline-block;\n}\n\n.if-userButton {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: var(--if-space-2);\n padding: var(--if-space-2);\n border-radius: 9999px;\n background-color: transparent;\n border: none;\n cursor: pointer;\n transition: all var(--if-transition-base);\n}\n\n.if-userButton:hover {\n opacity: 0.8;\n}\n\n.if-userButton-detailed {\n background-color: var(--if-color-bg-white);\n border: 1px solid var(--if-color-border);\n border-radius: var(--if-radius-sm);\n padding: var(--if-space-2);\n}\n\n.if-userButton-detailed:hover {\n background-color: var(--if-color-bg-light);\n opacity: 1;\n}\n\n.if-userButton-avatar {\n width: 2rem;\n height: 2rem;\n border-radius: 9999px;\n background-color: var(--if-color-primary);\n color: var(--if-color-bg-white);\n display: flex;\n align-items: center;\n justify-content: center;\n font-weight: 600;\n font-size: var(--if-font-size-sm);\n font-family: var(--if-font-family);\n overflow: hidden;\n}\n\n.if-userButton-avatarImage {\n border-radius: 9999px;\n object-fit: cover;\n width: 100%;\n height: 100%;\n}\n\n.if-userButton-avatarInitials {\n color: var(--if-color-bg-white);\n font-weight: 600;\n font-size: var(--if-font-size-sm);\n}\n\n.if-userButton-info {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 0.125rem;\n}\n\n.if-userButton-name {\n font-size: var(--if-font-size-sm);\n font-weight: 600;\n color: var(--if-color-text);\n line-height: 1.25rem;\n text-align: left;\n font-family: var(--if-font-family);\n}\n\n.if-userButton-email {\n font-size: var(--if-font-size-xs);\n color: var(--if-color-text-secondary);\n line-height: 1rem;\n text-align: left;\n font-family: var(--if-font-family);\n}\n\n.if-userButton-menu {\n position: absolute;\n margin-top: var(--if-space-2);\n background-color: var(--if-color-bg-white);\n border: 1px solid var(--if-color-border);\n border-radius: var(--if-radius-md);\n box-shadow: var(--if-shadow-lg);\n padding: var(--if-space-2);\n min-width: 200px;\n z-index: 50;\n}\n\n.if-userButton-menuItem {\n display: flex;\n align-items: center;\n gap: var(--if-space-2);\n padding: var(--if-space-2);\n border-radius: var(--if-radius-sm);\n cursor: pointer;\n transition: background-color var(--if-transition-fast);\n font-size: var(--if-font-size-sm);\n color: var(--if-color-text);\n font-family: var(--if-font-family);\n background: none;\n border: none;\n width: 100%;\n text-align: left;\n}\n\n.if-userButton-menuItem:hover {\n background-color: var(--if-color-bg-light);\n}\n\n.if-userButton-menuItem-signout {\n color: var(--if-color-error);\n}\n\n.if-userButton-menuItem-icon {\n width: 1.25rem;\n height: 1.25rem;\n}\n\n/* ============================================\n EMAIL VERIFICATION STATUS\n ============================================ */\n.if-verifyStatus-container {\n width: 100%;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: var(--if-space-6);\n}\n\n.if-verifyStatus-container-stretch {\n width: 100%;\n display: flex;\n flex-direction: column;\n align-items: stretch;\n justify-content: center;\n gap: var(--if-space-6);\n}\n\n.if-verifyStatus-spinner {\n border-radius: 9999px;\n height: 3rem;\n width: 3rem;\n border-bottom: 2px solid var(--if-color-primary);\n}\n\n.if-verifyStatus-successContent {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: var(--if-space-4);\n}\n\n.if-verifyStatus-successIcon {\n width: 4rem;\n height: 4rem;\n border-radius: 9999px;\n background-color: #d1fae5;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.if-verifyStatus-successIconSvg {\n width: 2rem;\n height: 2rem;\n color: #059669;\n}\n\n.if-verifyStatus-textCenter {\n text-align: center;\n}\n\n/* ============================================\n UTILITY CLASSES\n ============================================ */\n.if-hidden {\n display: none;\n}\n\n.if-visuallyHidden {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border-width: 0;\n}\n";
9
9
  if (document.head) {
10
10
  document.head.appendChild(style);
11
11
  }
@@ -338,10 +338,15 @@ function InsforgeProvider({
338
338
  );
339
339
  const loginWithOAuth = react.useCallback(
340
340
  async (provider, redirectTo) => {
341
- const sdkResult = await insforge.auth.signInWithOAuth({ provider, redirectTo });
342
- return sdkResult.data;
341
+ const sdkResult = await insforge.auth.signInWithOAuth({
342
+ provider,
343
+ redirectTo: redirectTo || afterSignInUrl
344
+ });
345
+ if (sdkResult.error) {
346
+ throw new Error(sdkResult.error.message);
347
+ }
343
348
  },
344
- [insforge]
349
+ [insforge, afterSignInUrl]
345
350
  );
346
351
  return /* @__PURE__ */ jsxRuntime.jsx(
347
352
  InsforgeContext.Provider,
@@ -497,34 +502,27 @@ function AuthPasswordStrengthIndicator({
497
502
  config
498
503
  }) {
499
504
  const requirements = createRequirements(config);
500
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "if-passwordStrength if-internal-ps6w3k", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "if-passwordStrength-requirements", children: requirements.map((req) => /* @__PURE__ */ jsxRuntime.jsxs(
501
- "div",
502
- {
503
- className: `if-passwordStrength-requirement ${req.test(password) ? "met" : "unmet"}`,
504
- children: [
505
- /* @__PURE__ */ jsxRuntime.jsx(
506
- "div",
507
- {
508
- style: {
509
- display: "flex",
510
- alignItems: "center",
511
- justifyContent: "center",
512
- width: "1rem",
513
- height: "1rem",
514
- borderRadius: "50%",
515
- border: "2px solid",
516
- borderColor: req.test(password) ? "transparent" : "#9ca3af",
517
- backgroundColor: req.test(password) ? "#059669" : "white",
518
- transition: "all 0.2s"
519
- },
520
- children: req.test(password) && /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Check, { style: { width: "0.75rem", height: "0.75rem", color: "white" } })
521
- }
522
- ),
523
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: req.label })
524
- ]
525
- },
526
- req.label
527
- )) }) });
505
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "if-passwordStrength if-internal-ps6w3k", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "if-passwordStrength-requirements", children: requirements.map((req) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "if-passwordStrength-requirement", children: [
506
+ /* @__PURE__ */ jsxRuntime.jsx(
507
+ "div",
508
+ {
509
+ style: {
510
+ display: "flex",
511
+ alignItems: "center",
512
+ justifyContent: "center",
513
+ width: "1rem",
514
+ height: "1rem",
515
+ borderRadius: "50%",
516
+ border: "2px solid",
517
+ borderColor: req.test(password) ? "transparent" : "#9ca3af",
518
+ backgroundColor: req.test(password) ? "#059669" : "white",
519
+ transition: "all 0.2s"
520
+ },
521
+ children: req.test(password) && /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Check, { style: { width: "0.75rem", height: "0.75rem", color: "white" } })
522
+ }
523
+ ),
524
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: req.label })
525
+ ] }, req.label)) }) });
528
526
  }
529
527
  function createRequirements(config) {
530
528
  const requirements = [];
@@ -592,11 +590,12 @@ function AuthPasswordField({
592
590
  onFocus,
593
591
  ...props
594
592
  }) {
593
+ const [searchParams] = reactRouterDom.useSearchParams();
595
594
  const [showPassword, setShowPassword] = react.useState(false);
596
595
  const [showStrength, setShowStrength] = react.useState(false);
597
596
  const resolvedForgotPasswordHref = react.useMemo(
598
- () => forgotPasswordLink ? resolveAuthPath(forgotPasswordLink.href) : void 0,
599
- [forgotPasswordLink]
597
+ () => forgotPasswordLink ? resolveAuthUrl(forgotPasswordLink.href, searchParams) : void 0,
598
+ [forgotPasswordLink, searchParams]
600
599
  );
601
600
  const handleFocus = (e) => {
602
601
  if (showStrengthIndicator) {
@@ -1006,29 +1005,20 @@ function AuthVerificationCodeInput({
1006
1005
  }
1007
1006
  function AuthEmailVerificationStep({
1008
1007
  email,
1009
- method = "code",
1010
- onVerifyCode
1008
+ method,
1009
+ onVerifyCode,
1010
+ emailSent = false
1011
1011
  }) {
1012
1012
  const { sendVerificationEmail } = useInsforge();
1013
1013
  const [resendDisabled, setResendDisabled] = react.useState(true);
1014
- const [resendCountdown, setResendCountdown] = react.useState(60);
1014
+ const [resendCountdown, setResendCountdown] = react.useState(emailSent ? 60 : 0);
1015
1015
  const [isSending, setIsSending] = react.useState(false);
1016
1016
  const [verificationCode, setVerificationCode] = react.useState("");
1017
1017
  const [isVerifying, setIsVerifying] = react.useState(false);
1018
1018
  const isLinkMethod = method === "link";
1019
- const displayDescription = isLinkMethod ? "We have sent an email to {email}. Please check your email to confirm your account before signing in. The confirmation link expires in 10 minutes." : "We've sent a verification code to your inbox at {email}. Enter it below to proceed.";
1019
+ const displayDescription = isLinkMethod ? "We've sent an email to {email}. Please check your email to confirm your account before signing in. The confirmation link expires in 10 minutes." : "We've sent a verification code to your inbox at {email}. Enter it below to proceed.";
1020
1020
  react.useEffect(() => {
1021
- const sendInitialEmail = async () => {
1022
- try {
1023
- await sendVerificationEmail(email);
1024
- } catch (error) {
1025
- console.error("Failed to send verification email:", error);
1026
- }
1027
- };
1028
- void sendInitialEmail();
1029
- }, [email, sendVerificationEmail]);
1030
- react.useEffect(() => {
1031
- if (resendCountdown > 0) {
1021
+ if (emailSent && resendCountdown > 0) {
1032
1022
  const timer = setInterval(() => {
1033
1023
  setResendCountdown((prev) => {
1034
1024
  if (prev <= 1) {
@@ -1040,7 +1030,7 @@ function AuthEmailVerificationStep({
1040
1030
  }, 1e3);
1041
1031
  return () => clearInterval(timer);
1042
1032
  }
1043
- }, [resendCountdown]);
1033
+ }, [emailSent, resendCountdown]);
1044
1034
  const handleResend = async () => {
1045
1035
  setResendDisabled(true);
1046
1036
  setResendCountdown(60);
@@ -1227,7 +1217,6 @@ function SignInForm({
1227
1217
  error,
1228
1218
  loading = false,
1229
1219
  oauthLoading = null,
1230
- availableProviders = [],
1231
1220
  onOAuthClick,
1232
1221
  authConfig,
1233
1222
  title = "Welcome Back",
@@ -1250,7 +1239,15 @@ function SignInForm({
1250
1239
  return /* @__PURE__ */ jsxRuntime.jsxs(AuthContainer, { children: [
1251
1240
  /* @__PURE__ */ jsxRuntime.jsx(AuthHeader, { title, subtitle }),
1252
1241
  /* @__PURE__ */ jsxRuntime.jsx(AuthErrorBanner, { error: error || "" }),
1253
- showVerificationStep ? /* @__PURE__ */ jsxRuntime.jsx(AuthEmailVerificationStep, { email, onVerifyCode }) : /* @__PURE__ */ jsxRuntime.jsxs("form", { onSubmit, noValidate: true, className: "if-form if-internal-fm9k2p", children: [
1242
+ showVerificationStep ? /* @__PURE__ */ jsxRuntime.jsx(
1243
+ AuthEmailVerificationStep,
1244
+ {
1245
+ email,
1246
+ method: authConfig.verifyEmailMethod,
1247
+ onVerifyCode,
1248
+ emailSent: false
1249
+ }
1250
+ ) : /* @__PURE__ */ jsxRuntime.jsxs("form", { onSubmit, noValidate: true, className: "if-form if-internal-fm9k2p", children: [
1254
1251
  /* @__PURE__ */ jsxRuntime.jsx(
1255
1252
  AuthFormField,
1256
1253
  {
@@ -1285,12 +1282,12 @@ function SignInForm({
1285
1282
  ] }),
1286
1283
  !showVerificationStep && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1287
1284
  /* @__PURE__ */ jsxRuntime.jsx(AuthLink, { text: signUpText, linkText: signUpLinkText, href: signUpUrl }),
1288
- availableProviders.length > 0 && onOAuthClick && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1285
+ authConfig.oAuthProviders.length > 0 && onOAuthClick && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1289
1286
  /* @__PURE__ */ jsxRuntime.jsx(AuthDivider, { text: dividerText }),
1290
1287
  /* @__PURE__ */ jsxRuntime.jsx(
1291
1288
  AuthOAuthProviders,
1292
1289
  {
1293
- providers: availableProviders,
1290
+ providers: authConfig.oAuthProviders,
1294
1291
  onClick: onOAuthClick,
1295
1292
  disabled: loading || oauthLoading !== null,
1296
1293
  loading: oauthLoading
@@ -1365,7 +1362,15 @@ function SignIn({ onError, ...uiProps }) {
1365
1362
  }
1366
1363
  }
1367
1364
  function handleOAuth(provider) {
1368
- loginWithOAuth(provider, redirectUrl || "");
1365
+ try {
1366
+ loginWithOAuth(provider, redirectUrl || "");
1367
+ } catch (err) {
1368
+ const errorMessage = err instanceof Error ? err.message : "OAuth login failed";
1369
+ setError(errorMessage);
1370
+ if (onError) {
1371
+ onError(new Error(errorMessage));
1372
+ }
1373
+ }
1369
1374
  }
1370
1375
  if (!authConfig) {
1371
1376
  return null;
@@ -1381,7 +1386,6 @@ function SignIn({ onError, ...uiProps }) {
1381
1386
  error,
1382
1387
  loading,
1383
1388
  oauthLoading,
1384
- availableProviders: authConfig?.oAuthProviders || [],
1385
1389
  onOAuthClick: handleOAuth,
1386
1390
  authConfig,
1387
1391
  showVerificationStep: step === "awaiting-verification",
@@ -1399,7 +1403,6 @@ function SignUpForm({
1399
1403
  error,
1400
1404
  loading = false,
1401
1405
  oauthLoading = null,
1402
- availableProviders = [],
1403
1406
  onOAuthClick,
1404
1407
  authConfig,
1405
1408
  title = "Get Started",
@@ -1420,7 +1423,15 @@ function SignUpForm({
1420
1423
  return /* @__PURE__ */ jsxRuntime.jsxs(AuthContainer, { children: [
1421
1424
  /* @__PURE__ */ jsxRuntime.jsx(AuthHeader, { title, subtitle }),
1422
1425
  /* @__PURE__ */ jsxRuntime.jsx(AuthErrorBanner, { error: error || "" }),
1423
- showVerificationStep ? /* @__PURE__ */ jsxRuntime.jsx(AuthEmailVerificationStep, { email, onVerifyCode }) : /* @__PURE__ */ jsxRuntime.jsxs("form", { onSubmit, noValidate: true, className: "if-form if-internal-fm9k2p", children: [
1426
+ showVerificationStep ? /* @__PURE__ */ jsxRuntime.jsx(
1427
+ AuthEmailVerificationStep,
1428
+ {
1429
+ email,
1430
+ method: authConfig.verifyEmailMethod,
1431
+ onVerifyCode,
1432
+ emailSent: true
1433
+ }
1434
+ ) : /* @__PURE__ */ jsxRuntime.jsxs("form", { onSubmit, noValidate: true, className: "if-form if-internal-fm9k2p", children: [
1424
1435
  /* @__PURE__ */ jsxRuntime.jsx(
1425
1436
  AuthFormField,
1426
1437
  {
@@ -1443,7 +1454,6 @@ function SignUpForm({
1443
1454
  value: password,
1444
1455
  onChange: (e) => onPasswordChange(e.target.value),
1445
1456
  required: true,
1446
- minLength: authConfig.passwordMinLength,
1447
1457
  autoComplete: "new-password",
1448
1458
  showStrengthIndicator: true,
1449
1459
  authConfig
@@ -1453,12 +1463,12 @@ function SignUpForm({
1453
1463
  ] }),
1454
1464
  !showVerificationStep && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1455
1465
  /* @__PURE__ */ jsxRuntime.jsx(AuthLink, { text: signInText, linkText: signInLinkText, href: signInUrl }),
1456
- availableProviders.length > 0 && onOAuthClick && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1466
+ authConfig.oAuthProviders.length > 0 && onOAuthClick && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1457
1467
  /* @__PURE__ */ jsxRuntime.jsx(AuthDivider, { text: dividerText }),
1458
1468
  /* @__PURE__ */ jsxRuntime.jsx(
1459
1469
  AuthOAuthProviders,
1460
1470
  {
1461
- providers: availableProviders,
1471
+ providers: authConfig.oAuthProviders,
1462
1472
  onClick: onOAuthClick,
1463
1473
  disabled: loading || oauthLoading !== null,
1464
1474
  loading: oauthLoading
@@ -1629,7 +1639,15 @@ function SignUp({ onError, ...uiProps }) {
1629
1639
  }
1630
1640
  }
1631
1641
  function handleOAuth(provider) {
1632
- loginWithOAuth(provider, redirectUrl || "");
1642
+ try {
1643
+ loginWithOAuth(provider, redirectUrl || "");
1644
+ } catch (err) {
1645
+ const errorMessage = err instanceof Error ? err.message : "OAuth login failed";
1646
+ setError(errorMessage);
1647
+ if (onError) {
1648
+ onError(new Error(errorMessage));
1649
+ }
1650
+ }
1633
1651
  }
1634
1652
  if (!authConfig) {
1635
1653
  return null;
@@ -1645,7 +1663,6 @@ function SignUp({ onError, ...uiProps }) {
1645
1663
  error,
1646
1664
  loading,
1647
1665
  oauthLoading,
1648
- availableProviders: authConfig?.oAuthProviders || [],
1649
1666
  onOAuthClick: handleOAuth,
1650
1667
  authConfig,
1651
1668
  showVerificationStep: step === "awaiting-verification",