"use client" import { useState, useEffect, useRef, useCallback } from "react" import { Loader2, Key } from "lucide-react" import { authClient } from "@/lib/auth-client" import { loginEmailSchema, loginSchema, } from "@/lib/auth/validation" import SocialButtons from "./social-buttons" import TurnstileWidget, { type TurnstileWidgetHandle } from "./turnstile-widget" import Link from "next/link" import { useRouter, useSearchParams } from "next/navigation" function isWebAuthnAbortError(err: unknown): boolean { if (err instanceof DOMException && err.name === "AbortError") return true const msg = err instanceof Error ? err.message : String(err ?? "") return msg.includes("abort signal") || msg.includes("AbortError") } // Suppress the console.error that @better-auth/passkey logs internally // when a WebAuthn ceremony is aborted (expected on navigation/remount). let suppressPasskeyErrors = false const originalConsoleError = console.error const passkeyErrorPattern = /\[Better Auth\] Error verifying passkey/ console.error = (...args: unknown[]) => { if (suppressPasskeyErrors) { const msg = typeof args[0] === 'string' ? args[0] : '' if (passkeyErrorPattern.test(msg) || args.some(a => isWebAuthnAbortError(a))) { return } } originalConsoleError(...args) } export default function LoginForm() { const router = useRouter() const searchParams = useSearchParams() const redirectTo = searchParams.get("redirect") || "/" const [showPassword, setShowPassword] = useState(false) const [email, setEmail] = useState("") const [password, setPassword] = useState("") const [error, setError] = useState("") const [isLoading, setIsLoading] = useState(false) // eslint-disable-next-line @typescript-eslint/no-unused-vars const [emailChecked, setEmailChecked] = useState(false) const [turnstileToken, setTurnstileToken] = useState("") const mountedRef = useRef(true) const passkeyInitiatedRef = useRef(false) const turnstileRef = useRef(null) // Redirect to the intended page after successful login const handleLoginSuccess = useCallback(() => { if (mountedRef.current) router.push(redirectTo) }, [router, redirectTo]) // Preload passkeys for conditional UI — must be called on mount when // both email + password fields are in the DOM. // Note: WebAuthn conditional mediation can throw NetworkError on pages // with self-signed certs (common in dev). We catch and suppress it. useEffect(() => { mountedRef.current = true if ("PublicKeyCredential" in window && !passkeyInitiatedRef.current) { passkeyInitiatedRef.current = true suppressPasskeyErrors = true const startPasskeyAutoFill = async () => { try { await authClient.signIn.passkey({ autoFill: true, fetchOptions: { onSuccess: handleLoginSuccess, }, }) } catch (err) { if (!isWebAuthnAbortError(err)) { console.warn("[passkey-conditional-ui]", err) } } finally { suppressPasskeyErrors = false } } startPasskeyAutoFill() } return () => { mountedRef.current = false } }, [handleLoginSuccess]) const handleEmailSubmit = async (e: React.FormEvent) => { e.preventDefault() setError("") const result = loginEmailSchema.safeParse({ email }) if (!result.success) { setError(result.error.issues[0].message) return } setIsLoading(true) try { const res = await fetch("/api/auth/check-email", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email }), }) if (!res.ok) { const data = await res.json() setError(data.error || "Something went wrong. Please try again.") return } const data = await res.json() if (!data.exists) { setError("No account found with this email.") return } // Email exists, show password field setEmailChecked(true) setShowPassword(true) } catch { setError("Something went wrong. Please try again.") } finally { setIsLoading(false) } } const handleLogin = async (e: React.FormEvent) => { e.preventDefault() setError("") const result = loginSchema.safeParse({ email, password }) if (!result.success) { setError(result.error.issues[0].message) return } setIsLoading(true) const { error } = await authClient.signIn.email({ email, password, fetchOptions: { headers: { "x-captcha-response": turnstileToken, }, }, }) setIsLoading(false) if (error) { setError(error.message || "Invalid credentials. Please try again.") turnstileRef.current?.reset() setTurnstileToken("") return } router.push(redirectTo) } const handleChangeEmail = () => { setShowPassword(false) setPassword("") setError("") setEmailChecked(false) turnstileRef.current?.reset() setTurnstileToken("") } const handlePasskeyError = useCallback((ctx: { error?: { message?: string } }) => { // Only show errors that aren't from conditional UI cancellation // (user dismissing the browser prompt is expected and not an error) const msg = ctx.error?.message || "" if ( !msg.includes("No available") && !msg.includes("cancelled") && !msg.includes("NotAllowed") && !msg.includes("aborted") ) { setError(msg || "Passkey sign-in failed. Please try again.") } }, []) const handlePasskeyLogin = async () => { setError("") setIsLoading(true) suppressPasskeyErrors = true try { const result = await authClient.signIn.passkey({ autoFill: false, fetchOptions: { onSuccess: handleLoginSuccess, onError: handlePasskeyError, }, }).catch((err) => { if (isWebAuthnAbortError(err)) return { data: null, error: null } return { data: null, error: { message: err?.message || "Passkey sign-in failed" } } }) if (result?.error) { handlePasskeyError({ error: result.error }) } } catch { setError("Passkey sign-in failed. Please try again.") } finally { suppressPasskeyErrors = false setIsLoading(false) } } return (

Welcome back

{showPassword ? "Signing in as " : "Sign in to DeckyVault"} {showPassword && ( <> {email} {" "} )}

or continue with email
{error && (
{error} {error.includes("No account found") && ( <> {" "} Create one → )}
)} {/* Single form always contains both email and password inputs so that WebAuthn conditional UI (autofill) works correctly. The password field is visually hidden until the email is verified. */}
setEmail(e.target.value)} placeholder="you@deckyvault.xyz" autoComplete="username webauthn" className="w-full px-4 py-3 rounded-lg border border-border bg-text/5 text-text text-sm placeholder:text-text/40 outline-none focus:border-primary focus:ring-2 focus:ring-primary/50 transition-colors" />
{/* Always render the password input for WebAuthn conditional UI, but visually hide it until the email is verified */}
setPassword(e.target.value)} placeholder="Enter your password" autoComplete="current-password webauthn" autoFocus={showPassword} tabIndex={showPassword ? 0 : -1} className="w-full px-4 py-3 rounded-lg border border-border bg-text/5 text-text text-sm placeholder:text-text/40 outline-none focus:border-primary focus:ring-2 focus:ring-primary/50 transition-colors" />
{showPassword && (
Forgot password?
)} {showPassword && ( { setTurnstileToken("") }} /> )} {showPassword && (

Your browser may offer to sign in with a passkey

)} {/* Explicit passkey login button */} {"PublicKeyCredential" in globalThis && !showPassword && ( )}

Don't have an account?{" "} Create one

) }