'use client'; import { useEffect, useState, useTransition } from 'react'; import { validateSubdomain } from '@/lib/validation'; type CheckState = | { kind: 'idle' } | { kind: 'checking' } | { kind: 'ok' } | { kind: 'taken' } | { kind: 'invalid'; message: string }; const ERROR_TEXT: Record = { invalid_user_code: 'That code is invalid. Double-check and try again.', expired: 'That code has expired. Generate a new one in your add-on.', 'subdomain taken': 'That subdomain is already taken.', subdomain_required: 'Please choose a subdomain for your tunnel.', aal2_required: 'Please finish 2FA, then try again.', unauthorized: 'Your session expired. Please sign in again.', }; function friendly(error: string | undefined, status: number): string { if (error && ERROR_TEXT[error]) return ERROR_TEXT[error]; return error ?? `Request failed (${status})`; } /** Normalize a user-typed code to XXXX-XXXX for display (client-side mirror). */ function normalizeCode(raw: string): string { const cleaned = raw.toUpperCase().replace(/[^A-Z0-9]/g, ''); if (cleaned.length !== 8) return raw.toUpperCase(); return `${cleaned.slice(0, 4)}-${cleaned.slice(4, 8)}`; } export function ActivateClient({ prefillCode, currentSubdomain, }: { prefillCode: string; currentSubdomain: string | null; }) { const [userCode, setUserCode] = useState( prefillCode ? normalizeCode(prefillCode) : '', ); // When the user has an existing subdomain, default to reusing it. const [useExisting, setUseExisting] = useState(!!currentSubdomain); const [subdomain, setSubdomain] = useState(''); const [check, setCheck] = useState({ kind: 'idle' }); const [error, setError] = useState(null); const [done, setDone] = useState(false); const [isPending, startTransition] = useTransition(); const needsSubdomain = !useExisting; useEffect(() => { if (!needsSubdomain) { setCheck({ kind: 'idle' }); return; } const v = validateSubdomain(subdomain); if (!subdomain) { setCheck({ kind: 'idle' }); return; } if (!v.ok) { setCheck({ kind: 'invalid', message: v.error }); return; } setCheck({ kind: 'checking' }); const ctrl = new AbortController(); const t = setTimeout(async () => { try { const res = await fetch( `/api/tunnel/check?subdomain=${encodeURIComponent(v.value)}`, { signal: ctrl.signal }, ); const body = (await res.json()) as { available?: boolean }; setCheck(body.available ? { kind: 'ok' } : { kind: 'taken' }); } catch (e) { if ((e as { name?: string }).name !== 'AbortError') { setCheck({ kind: 'idle' }); } } }, 300); return () => { ctrl.abort(); clearTimeout(t); }; }, [subdomain, needsSubdomain]); async function onSubmit(e: React.FormEvent) { e.preventDefault(); setError(null); const code = normalizeCode(userCode); if (code.replace(/[^A-Z0-9]/g, '').length !== 8) { setError(ERROR_TEXT.invalid_user_code); return; } const payload: { user_code: string; subdomain?: string } = { user_code: code, }; if (needsSubdomain) { const v = validateSubdomain(subdomain); if (!v.ok) { setError(v.error); return; } payload.subdomain = v.value; } startTransition(async () => { const res = await fetch('/api/device/approve', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); if (!res.ok) { const body = (await res.json().catch(() => ({}))) as { error?: string }; setError(friendly(body.error, res.status)); return; } setDone(true); }); } if (done) { return (

✓ Device approved — return to your Home Assistant add-on.

); } const submitDisabled = isPending || !userCode || (needsSubdomain && (check.kind === 'invalid' || check.kind === 'taken' || check.kind === 'checking' || !subdomain)); return (
setUserCode(e.target.value.toUpperCase())} onBlur={() => setUserCode((c) => normalizeCode(c))} required autoCapitalize="characters" autoCorrect="off" spellCheck={false} placeholder="WXYZ-2345" />
{currentSubdomain && (
)} {needsSubdomain && (
setSubdomain(e.target.value.toLowerCase())} required autoCapitalize="none" autoCorrect="off" spellCheck={false} placeholder="smith" /> .linumiq.net
{check.kind === 'checking' && ( Checking availability… )} {check.kind === 'ok' && Available} {check.kind === 'taken' && Taken} {check.kind === 'invalid' && ( {check.message} )}
)} {error &&

{error}

}
); }