"use client"; import { useEffect, useState, useCallback } from "react"; import type { ReactElement } from "react"; import { useRouter, useSearchParams } from "next/navigation"; import { Loader2 } from "lucide-react"; import type { AuthConfigResponse, AuthProviderConfig } from "@mosaic/shared"; import { API_BASE_URL } from "@/lib/config"; import { signIn } from "@/lib/auth-client"; import { OAuthButton } from "@/components/auth/OAuthButton"; import { LoginForm } from "@/components/auth/LoginForm"; import { AuthDivider } from "@/components/auth/AuthDivider"; import { AuthErrorBanner } from "@/components/auth/AuthErrorBanner"; /** Fallback config when the backend is unreachable */ const EMAIL_ONLY_CONFIG: AuthConfigResponse = { providers: [{ id: "email", name: "Email", type: "credentials" }], }; /** Maps URL error codes to PDA-friendly messages (no alarming language). */ const ERROR_CODE_MESSAGES: Record = { access_denied: "Authentication paused. Please try again when ready.", invalid_credentials: "The email and password combination wasn't recognized.", server_error: "The service is taking a break. Please try again in a moment.", network_error: "Unable to connect. Check your network and try again.", rate_limited: "You've tried a few times. Take a moment and try again shortly.", session_expired: "Your session ended. Please sign in again when ready.", }; const DEFAULT_ERROR_MESSAGE = "Authentication didn't complete. Please try again when ready."; function mapErrorCodeToMessage(code: string): string { return ERROR_CODE_MESSAGES[code] ?? DEFAULT_ERROR_MESSAGE; } export default function LoginPage(): ReactElement { const router = useRouter(); const searchParams = useSearchParams(); const [config, setConfig] = useState(null); const [loadingConfig, setLoadingConfig] = useState(true); const [oauthLoading, setOauthLoading] = useState(null); const [credentialsLoading, setCredentialsLoading] = useState(false); const [error, setError] = useState(null); const [urlError, setUrlError] = useState(null); /* Read ?error= query param on mount and map to PDA-friendly message */ useEffect(() => { const errorCode = searchParams.get("error"); if (errorCode) { setUrlError(mapErrorCodeToMessage(errorCode)); // Clean up the URL by removing the error param without triggering navigation const nextParams = new URLSearchParams(searchParams.toString()); nextParams.delete("error"); const query = nextParams.toString(); router.replace(query ? `/login?${query}` : "/login"); } }, [searchParams, router]); useEffect(() => { let cancelled = false; async function fetchConfig(): Promise { try { const response = await fetch(`${API_BASE_URL}/auth/config`); if (!response.ok) { throw new Error("Failed to fetch auth config"); } const data = (await response.json()) as AuthConfigResponse; if (!cancelled) { setConfig(data); } } catch { if (!cancelled) { setConfig(EMAIL_ONLY_CONFIG); } } finally { if (!cancelled) { setLoadingConfig(false); } } } void fetchConfig(); return (): void => { cancelled = true; }; }, []); const oauthProviders: AuthProviderConfig[] = config?.providers.filter((p) => p.type === "oauth") ?? []; const credentialProviders: AuthProviderConfig[] = config?.providers.filter((p) => p.type === "credentials") ?? []; const hasOAuth = oauthProviders.length > 0; const hasCredentials = credentialProviders.length > 0; const handleOAuthLogin = useCallback((providerId: string): void => { setOauthLoading(providerId); setError(null); void signIn.oauth2({ providerId, callbackURL: "/" }); }, []); const handleCredentialsLogin = useCallback( async (email: string, password: string): Promise => { setCredentialsLoading(true); setError(null); try { const result = await signIn.email({ email, password }); if (result.error) { setError( typeof result.error.message === "string" ? result.error.message : "Unable to sign in. Please check your credentials and try again." ); } else { router.push("/tasks"); } } catch { setError("Something went wrong. Please try again in a moment."); } finally { setCredentialsLoading(false); } }, [router] ); return (

Welcome to Mosaic Stack

Your personal assistant platform. Organize tasks, events, and projects with a PDA-friendly approach.

{loadingConfig ? (
) : ( <> {urlError && ( { setUrlError(null); }} /> )} {error && !hasCredentials && ( { setError(null); }} /> )} {hasOAuth && oauthProviders.map((provider) => ( { handleOAuthLogin(provider.id); }} isLoading={oauthLoading === provider.id} disabled={oauthLoading !== null && oauthLoading !== provider.id} /> ))} {hasOAuth && hasCredentials && } {hasCredentials && ( )} )}
); }