/** * Code validation component for PodTUI * 8-character alphanumeric code input for sync authentication */ import { createSignal } from "solid-js"; import { useAuthStore } from "@/stores/auth"; import { AUTH_CONFIG } from "@/config/auth"; import { useTheme } from "@/context/ThemeContext"; interface CodeValidationProps { focused?: boolean; onBack?: () => void; } type FocusField = "code" | "submit" | "back"; export function CodeValidation(props: CodeValidationProps) { const auth = useAuthStore(); const { theme } = useTheme(); const [code, setCode] = createSignal(""); const [focusField, setFocusField] = createSignal("code"); const [codeError, setCodeError] = createSignal(null); const fields: FocusField[] = ["code", "submit", "back"]; /** Format code as user types (uppercase, alphanumeric only) */ const handleCodeInput = (value: string) => { const formatted = value.toUpperCase().replace(/[^A-Z0-9]/g, ""); // Limit to max length const limited = formatted.slice(0, AUTH_CONFIG.codeValidation.codeLength); setCode(limited); // Clear error when typing if (codeError()) { setCodeError(null); } }; const validateCode = (value: string): boolean => { if (!value) { setCodeError("Code is required"); return false; } if (value.length !== AUTH_CONFIG.codeValidation.codeLength) { setCodeError( `Code must be ${AUTH_CONFIG.codeValidation.codeLength} characters`, ); return false; } if (!AUTH_CONFIG.codeValidation.allowedChars.test(value)) { setCodeError("Code must contain only letters and numbers"); return false; } setCodeError(null); return true; }; const handleSubmit = async () => { if (!validateCode(code())) { return; } const success = await auth.validateCode(code()); if (!success && auth.error) { setCodeError(auth.error.message); } }; const handleKeyPress = (key: { name: string; shift?: boolean }) => { if (key.name === "tab") { const currentIndex = fields.indexOf(focusField()); const nextIndex = key.shift ? (currentIndex - 1 + fields.length) % fields.length : (currentIndex + 1) % fields.length; setFocusField(fields[nextIndex]); } else if (key.name === "return" || key.name === "tab") { if (focusField() === "submit") { handleSubmit(); } else if (focusField() === "back" && props.onBack) { props.onBack(); } } else if (key.name === "escape" && props.onBack) { props.onBack(); } }; const codeProgress = () => { const len = code().length; const max = AUTH_CONFIG.codeValidation.codeLength; return `${len}/${max}`; }; const codeDisplay = () => { const current = code(); const max = AUTH_CONFIG.codeValidation.codeLength; const filled = current.split(""); const empty = Array(max - filled.length).fill("_"); return [...filled, ...empty].join(" "); }; return ( Enter Sync Code Enter your 8-character sync code to link your account. You can get this code from the web portal. {/* Code display */} Code ({codeProgress()}): {codeDisplay()} {/* Hidden input for actual typing */} {codeError() && {codeError()}} {/* Action buttons */} {auth.isLoading ? "Validating..." : "[Enter] Validate Code"} [Esc] Back to Login {/* Auth error message */} {auth.error && {auth.error.message}} Tab to navigate, Enter to select, Esc to go back ); }