"use client"; import { useState, useEffect, useCallback } from "react"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import * as z from "zod"; import { Button } from "@/components/ui/button"; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { createApiClient, formatAxiosError } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { useRouter } from "next/navigation"; import { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot } from "@/components/ui/input-otp"; import { REGEXP_ONLY_DIGITS_AND_CHARS } from "input-otp"; import { AlertTriangle, Loader2 } from "lucide-react"; import { DeviceAuthConfirmation } from "@/components/DeviceAuthConfirmation"; import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext"; import BrandingLogo from "./BrandingLogo"; import { useTranslations } from "next-intl"; import UserProfileCard from "@/components/UserProfileCard"; const createFormSchema = (t: (key: string) => string) => z.object({ code: z.string().length(8, t("deviceCodeInvalidFormat")) }); type DeviceAuthMetadata = { ip: string | null; city: string | null; deviceName: string | null; applicationName: string; createdAt: number; }; type DeviceLoginFormProps = { userEmail: string; userName?: string; initialCode?: string; }; export default function DeviceLoginForm({ userEmail, userName, initialCode = "" }: DeviceLoginFormProps) { const router = useRouter(); const { env } = useEnvContext(); const api = createApiClient({ env }); const [error, setError] = useState(null); const [loading, setLoading] = useState(false); const [validatingInitialCode, setValidatingInitialCode] = useState(false); const [verifyingInitialCode, setVerifyingInitialCode] = useState(false); const [metadata, setMetadata] = useState(null); const [code, setCode] = useState(""); const { isUnlocked } = useLicenseStatusContext(); const t = useTranslations(); const formSchema = createFormSchema(t); const form = useForm>({ resolver: zodResolver(formSchema), defaultValues: { code: initialCode.replace(/-/g, "").toUpperCase() } }); const validateCode = useCallback( async (codeToValidate: string, skipConfirmation = false) => { setError(null); setLoading(true); try { // split code and add dash if missing let formattedCode = codeToValidate; if ( !formattedCode.includes("-") && formattedCode.length === 8 ) { formattedCode = formattedCode.slice(0, 4) + "-" + formattedCode.slice(4); } // First check - get metadata const res = await api.post( "/device-web-auth/verify?forceLogin=true", { code: formattedCode.toUpperCase(), verify: false } ); if (res.data.success && res.data.data.metadata) { setCode(formattedCode.toUpperCase()); // If skipping confirmation (initial code), go straight to verify if (skipConfirmation) { setVerifyingInitialCode(true); try { await api.post("/device-web-auth/verify", { code: formattedCode.toUpperCase(), verify: true }); router.push("/auth/login/device/success"); } catch (e: any) { const errorMessage = formatAxiosError(e); setError( errorMessage || t("deviceCodeVerifyFailed") ); setVerifyingInitialCode(false); return false; } return true; } else { setMetadata(res.data.data.metadata); return true; } } else { setError(t("deviceCodeInvalidOrExpired")); return false; } } catch (e: any) { const errorMessage = formatAxiosError(e); setError(errorMessage || t("deviceCodeInvalidOrExpired")); return false; } finally { setLoading(false); } }, [api, t, router] ); async function onSubmit(data: z.infer) { await validateCode(data.code); } // Auto-validate initial code if provided useEffect(() => { const cleanedInitialCode = initialCode.replace(/-/g, "").toUpperCase(); if (cleanedInitialCode && cleanedInitialCode.length === 8) { setValidatingInitialCode(true); validateCode(cleanedInitialCode, false).finally(() => { setValidatingInitialCode(false); }); } }, [initialCode, validateCode]); async function onConfirm() { if (!code || !metadata) return; setError(null); setLoading(true); try { // Final verify await api.post("/device-web-auth/verify", { code: code, verify: true }); // Redirect to success page router.push("/auth/login/device/success"); } catch (e: any) { const errorMessage = formatAxiosError(e); setError(errorMessage || t("deviceCodeVerifyFailed")); setMetadata(null); setCode(""); form.reset(); } finally { setLoading(false); } } const logoWidth = isUnlocked() ? env.branding.logo?.authPage?.width || 175 : 175; const logoHeight = isUnlocked() ? env.branding.logo?.authPage?.height || 58 : 58; function onCancel() { setMetadata(null); setCode(""); form.reset(); setError(null); } const profileLabel = (userName || userEmail || "").trim(); async function handleUseDifferentAccount() { try { await api.post("/auth/logout"); } catch (logoutError) { console.error( "Failed to logout before switching account", logoutError ); } finally { const currentSearch = typeof window !== "undefined" ? window.location.search : ""; const redirectTarget = `/auth/login/device${currentSearch || ""}`; router.push( `/auth/login?forceLogin=true&redirect=${encodeURIComponent(redirectTarget)}` ); router.refresh(); } } // Show loading state while validating/verifying initial code if (validatingInitialCode || verifyingInitialCode) { return (
{t("deviceActivation")} {validatingInitialCode ? t("deviceCodeValidating") : t("deviceCodeVerifying")}
{validatingInitialCode ? t("deviceCodeValidating") : t("deviceCodeVerifying")}
{error && ( {error} )}
); } if (metadata) { return ( ); } return (

{t("deviceActivation")}

{t("deviceCodeEnterPrompt")}

(
{ // Strip hyphens and convert to uppercase const cleanedValue = value .replace(/-/g, "") .toUpperCase(); field.onChange( cleanedValue ); }} >
)} /> {error && ( {error} )}
); }