feat(settings): harden 2fa recovery code UX with warning copy and download
Some checks failed
CI / test (push) Has been cancelled
Some checks failed
CI / test (push) Has been cancelled
This commit is contained in:
@@ -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}>
|
||||
|
||||
Reference in New Issue
Block a user