'use client'; import { useId, useRef, useState } from 'react'; import { Copy, Eye, EyeOff, KeyRound } from 'lucide-react'; import type { GameView } from '@/game/engine'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Textarea } from '@/components/ui/textarea'; import { isRoomClosed, isRoomRemoved, requestJson, requestMayHaveCompleted } from '@/lib/client-request'; import { clearHandoverOwner } from '@/lib/seat-handover'; import { createRecoveryAttempt, createRecoveryKit, parseRecoveryKit, serializeRecoveryKit, type SeatRecoveryAttempt, type SeatRecoveryKit, } from '@/lib/seat-recovery'; type Restored = (view: GameView) => void; type SetupResponse = { view: GameView; recoveryConfigured: true }; type ClaimResponse = { view: GameView; recovered: true; replayed: boolean }; /** Mount with key={`${game.code}:${game.me}`} to separate private owner contexts. */ export function SeatRecoverySetup({ game, onRestored, onPending, disabled = false, }: { game: GameView; onRestored: Restored; onPending?: (pending: boolean) => void; disabled?: boolean; }) { const id = useId(); const pending = useRef(false); const [draft, setDraft] = useState(null); const [saved, setSaved] = useState(false); const [confirmed, setConfirmed] = useState(false); const [busy, setBusy] = useState(false); const [uncertain, setUncertain] = useState(false); const [message, setMessage] = useState(''); const closed = !!game.roomControl?.closed; const own = game.players.find((player) => player.id === game.me); const kit = draft?.roomCode === game.code && draft.playerId === game.me ? draft : null; if (!own) return null; async function copyKit() { if (!kit) return; try { await navigator.clipboard.writeText(serializeRecoveryKit(kit)); setMessage( 'Private kit copied. Save it somewhere secure before continuing.', ); } catch { setMessage( 'Copy was unavailable. Select the complete kit below and copy it manually.', ); } } async function saveKey() { if (!kit || !saved || confirmed || disabled || closed || pending.current) return; let confirmedView: GameView | undefined; pending.current = true; setBusy(true); onPending?.(true); setMessage('Saving your recovery key…'); try { const result = await requestJson( `/api/rooms/${kit.roomCode}/control`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ type: 'setRecoveryKey', version: game.version, recoverySecret: kit.recoverySecret, }), }, ); if ( result.recoveryConfigured !== true || result.view.code !== kit.roomCode || result.view.me !== kit.playerId ) throw new Error('The server did not confirm this seat’s recovery key.'); setConfirmed(true); setUncertain(false); setMessage( 'Recovery key saved. Keep your private kit; it is needed if this browser loses its seat.', ); confirmedView = result.view; } catch (error) { const mayHaveSaved = requestMayHaveCompleted(error); setUncertain(mayHaveSaved); setMessage( isRoomClosed(error) ? `${(error as Error).message} Keep this kit and request; retry after reopening.` : isRoomRemoved(error) ? `${(error as Error).message} Keep this same recovery kit and this page open. Retry saving after the room is restored.` : mayHaveSaved ? 'Saving was not confirmed and may have completed. Keep this same kit. Retry saving to confirm it; no retry has been sent automatically.' : `${(error as Error).message} Your kit remains below. Review the current table and retry when ready.`, ); } finally { pending.current = false; setBusy(false); onPending?.(false); } if (confirmedView) onRestored(confirmedView); } return (
Protect your saved seat

Create or replace a private recovery key for {own.name}. Anyone with the kit can recover this seat, revealing its private information and revoking its old browser sessions. Keep it separate from the public invitation.

This restores your own seat after cookie loss. A replacement key invalidates the previous kit. The kit stays in memory here; save it before refreshing or closing this page.

{closed &&

The room is closed. Keep any existing kit; new recovery keys can be enabled after reopening.

} {!kit ? ( ) : (