import { useState } from 'react'; import { Loader2, LockOpen } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { useLockActions } from '../useWalletData'; // Unlock, and nothing else. The wallet spends most of its life locked and that is the intended state — // this dialog exists for the moment before a signature, not as a gate on the app. // // The unlock window is capped by the deployment's unlockTtlSec; a caller may ask for less but never more, // so the shorter options here are real and the longest is simply "whatever the sidecar allows". type UnlockDialogProps = { open: boolean; onOpenChange: (open: boolean) => void; walletId: number; walletName: string; maxTtlSec: number; }; /** Offered windows, filtered to those the deployment actually permits. */ const TTL_CHOICES = [ { sec: 60, label: '1 minute' }, { sec: 300, label: '5 minutes' }, { sec: 900, label: '15 minutes' }, { sec: 3600, label: '1 hour' }, ]; export const UnlockDialog = ({ open, onOpenChange, walletId, walletName, maxTtlSec }: UnlockDialogProps) => { const { unlock } = useLockActions(walletId); // The passphrase lives HERE and nowhere else: no localStorage, no sessionStorage, no React Query // cache, no URL. It goes straight into the POST body and this state is cleared the moment the request // settles, on success and on failure alike. const [passphrase, setPassphrase] = useState(''); const [ttlSec, setTtlSec] = useState(() => Math.min(300, maxTtlSec)); const choices = TTL_CHOICES.filter((c) => c.sec <= maxTtlSec); const close = () => { setPassphrase(''); onOpenChange(false); }; const submit = async (ev: React.FormEvent) => { ev.preventDefault(); if (!passphrase) return; try { await unlock.mutateAsync({ passphrase, ttlSec }); close(); } finally { // Cleared even when the unlock failed — a wrong passphrase left sitting in an input is still a // passphrase sitting in memory attached to a mounted component. setPassphrase(''); } }; return ( (next ? onOpenChange(true) : close())}>
Unlock {walletName} The seed is decrypted in the wallet sidecar for the window you choose, then wiped. Balances and history do not need this — only signing does.
{/* Controlled input → request body → cleared. Never written to any store. */} setPassphrase(ev.target.value)} />
{choices.length > 0 && (
{choices.map(({ sec, label }) => ( ))}
)}
); };