← vibesdk
src/components/vault/RecoveryCodesDisplay.tsx
4.4 KB
Download file
import { useState } from 'react';
import { cn } from '@cloudflare/kumo';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Copy, Check, Download, AlertTriangle } from 'lucide-react';
interface Props {
codes: string[];
onConfirm?: () => void;
showWarning?: boolean;
variant?: 'card' | 'inline';
}
export function RecoveryCodesDisplay({
codes,
onConfirm,
showWarning = true,
variant = 'card',
}: Props) {
const [copiedIndex, setCopiedIndex] = useState<number | null>(null);
const [allCopied, setAllCopied] = useState(false);
const [confirmed, setConfirmed] = useState(false);
const handleCopyCode = async (code: string, index: number) => {
await navigator.clipboard.writeText(code);
setCopiedIndex(index);
setTimeout(() => setCopiedIndex(null), 2000);
};
const handleCopyAll = async () => {
await navigator.clipboard.writeText(codes.join('\n'));
setAllCopied(true);
setTimeout(() => setAllCopied(false), 2000);
};
const handleDownload = () => {
const content = [
'Vault Recovery Codes',
'====================',
'',
'Keep these codes safe. Each code can only be used once.',
'',
...codes.map((code, i) => `${i + 1}. ${code}`),
'',
`Generated: ${new Date().toISOString()}`,
].join('\n');
const blob = new Blob([content], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'vault-recovery-codes.txt';
a.click();
URL.revokeObjectURL(url);
};
const title = (
<div className="flex items-center gap-2">
{showWarning && <AlertTriangle className="h-5 w-5 text-amber-500" />}
<span>Recovery Codes</span>
</div>
);
const description = (
<p className="text-sm text-muted-foreground">
These are one-time use codes to recover your vault if you lose access. Store them securely.
</p>
);
const body = (
<div className="space-y-4">
<div className="grid grid-cols-2 gap-2">
{codes.map((code, index) => (
<button
key={index}
onClick={() => handleCopyCode(code, index)}
className={cn(
'p-3 rounded-md border font-mono text-sm',
'bg-muted/50 hover:bg-muted transition-colors',
'flex items-center justify-between gap-2',
'focus:outline-none focus:ring-2 focus:ring-primary'
)}
>
<span className="text-muted-foreground text-xs">{index + 1}.</span>
<span className="flex-1 text-left">{code}</span>
{copiedIndex === index ? (
<Check className="h-4 w-4 text-green-500 shrink-0" />
) : (
<Copy className="h-4 w-4 text-muted-foreground shrink-0" />
)}
</button>
))}
</div>
<div className="flex gap-2">
<Button variant="outline" onClick={handleCopyAll} className="flex-1">
{allCopied ? (
<>
<Check className="h-4 w-4 mr-2" />
Copied
</>
) : (
<>
<Copy className="h-4 w-4 mr-2" />
Copy All
</>
)}
</Button>
<Button variant="outline" onClick={handleDownload} className="flex-1">
<Download className="h-4 w-4 mr-2" />
Download
</Button>
</div>
{onConfirm && (
<>
<div className="border-t pt-4">
<label className="flex items-start gap-3 cursor-pointer">
<input
type="checkbox"
checked={confirmed}
onChange={(e) => setConfirmed(e.target.checked)}
className="mt-1 rounded"
/>
<span className="text-sm">
I understand that these codes are the only way to recover my vault if I forget my
password or lose my passkey. I have saved them securely.
</span>
</label>
</div>
<Button onClick={onConfirm} disabled={!confirmed} className="w-full">
I've Saved My Codes
</Button>
</>
)}
</div>
);
if (variant === 'inline') {
return (
<div className="space-y-4">
<div className="space-y-1">
<div className="flex items-center gap-2 text-base font-semibold">
{showWarning && <AlertTriangle className="h-5 w-5 text-amber-500" />}
Recovery Codes
</div>
{description}
</div>
{body}
</div>
);
}
return (
<Card>
<CardHeader>
<CardTitle>{title}</CardTitle>
<CardDescription>{description}</CardDescription>
</CardHeader>
<CardContent>{body}</CardContent>
</Card>
);
}