"use client"; import { useEffect, useMemo, useState } from "react"; import { useFormState } from "react-dom"; import { loginAction, type LoginState } from "./actions"; const initialState: LoginState = { error: null }; const apiBaseUrl = typeof window === "undefined" ? process.env.NEXT_PUBLIC_API_BASE_URL ?? "" : ""; const base58Pattern = /^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]+$/; const viewKeyPattern = /^[0-9a-fA-F]{64}$/; const validationDelayMs = 450; // Wownero primary addresses are 97 base58 chars starting with "Wo". // This guard was inherited from the xmrcheckout fork — XMR addresses // are 95 chars starting with "4". The xmrcheckout-shape check // silently rejected every Wownero address, leaving the Sign in // button greyed out and the validate API never called. Suchshop // avoided this because that tenant was provisioned via direct SQL, // not via this form. const isLikelyPrimaryAddress = (value: string) => { const trimmed = value.trim(); if (trimmed.length !== 97) { return false; } if (!trimmed.startsWith("Wo")) { return false; } return base58Pattern.test(trimmed); }; const isLikelySecretViewKey = (value: string) => viewKeyPattern.test(value.trim()); export default function LoginForm() { const [state, formAction] = useFormState(loginAction, initialState); const [paymentAddress, setPaymentAddress] = useState(""); const [viewKey, setViewKey] = useState(""); const [validationStatus, setValidationStatus] = useState< "idle" | "checking" | "valid" | "invalid" | "error" >("idle"); const [validationMessage, setValidationMessage] = useState(null); const canSubmit = useMemo(() => { if (!isLikelyPrimaryAddress(paymentAddress) || !isLikelySecretViewKey(viewKey)) { return false; } return validationStatus !== "checking" && validationStatus !== "invalid"; }, [paymentAddress, validationStatus, viewKey]); useEffect(() => { if (!isLikelyPrimaryAddress(paymentAddress) || !isLikelySecretViewKey(viewKey)) { setValidationStatus("idle"); setValidationMessage(null); return; } const controller = new AbortController(); const timeoutId = window.setTimeout(async () => { setValidationStatus("checking"); setValidationMessage(null); try { const response = await fetch(`${apiBaseUrl}/api/core/auth/validate`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ payment_address: paymentAddress.trim(), view_key: viewKey.trim(), }), cache: "no-store", signal: controller.signal, }); if (!response.ok) { const detail = await response.json().catch(() => null); const message = detail?.detail ?? "Address and view key did not pass validation."; setValidationStatus("invalid"); setValidationMessage(message); return; } setValidationStatus("valid"); } catch (error) { if (controller.signal.aborted) { return; } setValidationStatus("error"); setValidationMessage( "Unable to validate the address and view key yet. You can still sign in to confirm." ); } }, validationDelayMs); return () => { controller.abort(); window.clearTimeout(timeoutId); }; }, [paymentAddress, viewKey]); return (
{isLikelyPrimaryAddress(paymentAddress) ? ( Valid ) : null}