feat(settings): harden 2fa recovery code UX with warning copy and download
Some checks failed
CI / test (push) Has been cancelled

This commit is contained in:
2026-03-08 21:02:49 +03:00
parent c222c93628
commit eb27371f0d
2 changed files with 55 additions and 1 deletions

View File

@@ -240,6 +240,37 @@ export function SettingsPanel({ open, onClose }: Props) {
}
}
async function copyRecoveryCodes(codes: string[]) {
if (!codes.length) {
showToast("No recovery codes to copy");
return;
}
try {
await navigator.clipboard.writeText(codes.join("\n"));
showToast("Recovery codes copied");
} catch {
showToast("Failed to copy recovery codes");
}
}
function downloadRecoveryCodes(codes: string[]) {
if (!codes.length) {
showToast("No recovery codes to download");
return;
}
const content = `BenyaMessenger 2FA Recovery Codes\n\n${codes.join("\n")}\n`;
const blob = new Blob([content], { type: "text/plain;charset=utf-8" });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = "benya-recovery-codes.txt";
document.body.appendChild(link);
link.click();
link.remove();
URL.revokeObjectURL(url);
showToast("Recovery codes downloaded");
}
return createPortal(
<div className="fixed inset-0 z-[150] bg-slate-950/60" onClick={onClose}>
<aside
@@ -633,6 +664,11 @@ export function SettingsPanel({ open, onClose }: Props) {
{twofaError ? <p className="mt-2 text-xs text-red-400">{twofaError}</p> : null}
{me.twofa_enabled ? (
<div className="mt-3 rounded bg-slate-900/50 px-2 py-2">
{recoveryRemaining === 0 ? (
<p className="mb-2 rounded border border-amber-500/40 bg-amber-500/10 px-2 py-1.5 text-[11px] text-amber-200">
You have no recovery codes. Generate and save them to avoid lockout.
</p>
) : null}
<div className="mb-2 flex items-center justify-between gap-2">
<p className="text-xs text-slate-300">Recovery codes remaining: {recoveryRemaining}</p>
<button
@@ -657,6 +693,24 @@ export function SettingsPanel({ open, onClose }: Props) {
{recoveryCodes.length > 0 ? (
<div className="rounded border border-amber-500/40 bg-amber-500/10 p-2">
<p className="mb-1 text-[11px] text-amber-200">Save these codes now. They are shown only once.</p>
<div className="mb-2 flex items-center gap-2">
<button
className="rounded bg-slate-700 px-2 py-1 text-[11px]"
onClick={() => {
void copyRecoveryCodes(recoveryCodes);
}}
type="button"
>
Copy
</button>
<button
className="rounded bg-slate-700 px-2 py-1 text-[11px]"
onClick={() => downloadRecoveryCodes(recoveryCodes)}
type="button"
>
Download .txt
</button>
</div>
<div className="grid grid-cols-2 gap-1">
{recoveryCodes.map((item) => (
<code className="rounded bg-slate-900 px-1.5 py-1 text-[11px]" key={item}>