From f1cb2a32bafb24ccf5b158026d0a9a1c985087e4 Mon Sep 17 00:00:00 2001 From: "shakespeare.diy" Date: Thu, 27 Aug 2026 11:10:51 +0700 Subject: [PATCH] Add recovery-code retrieval to Settings > Security (closes the deferred-backup loop) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Since signup now defers the key backup, users who skip it MUST be able to get their recovery code later — otherwise we've just moved the trap. This adds a "Your Recovery Code" card at the top of Settings > Security that: - For nsec logins: reveals the recovery code on demand (hidden by default), with Copy, Download (.txt), and QR options, plus a Hide button and clear "never share this" warnings. - For bunker / browser-extension / Amber logins: explains that SkateSpots never held the key and points the user to back it up in their signer app. Also removed the old placeholder QR handler that only showed an error, and updated the "Backup Not Confirmed" copy to direct users to the new reveal card instead of falsely claiming lost keys are unrecoverable. Co-authored-by: shakespeare.diy --- src/components/SecuritySettings.tsx | 187 ++++++++++++++++++++++++---- 1 file changed, 166 insertions(+), 21 deletions(-) diff --git a/src/components/SecuritySettings.tsx b/src/components/SecuritySettings.tsx index 5547d8f..119a856 100644 --- a/src/components/SecuritySettings.tsx +++ b/src/components/SecuritySettings.tsx @@ -1,18 +1,67 @@ import { useState } from 'react'; -import { Download, Copy, Eye, EyeOff, Shield, AlertTriangle, CheckCircle, ExternalLink } from 'lucide-react'; +import { Download, Copy, Eye, EyeOff, Shield, AlertTriangle, CheckCircle, ExternalLink, KeyRound, QrCode } from 'lucide-react'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Alert, AlertDescription } from '@/components/ui/alert'; +import { QRCodeCanvas } from '@/components/ui/qrcode'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { useLocalStorage } from '@/hooks/useLocalStorage'; +import { useNostrLogin } from '@nostrify/react/login'; import { toast } from '@/hooks/useToast'; +const sanitizeFilename = (filename: string) => filename.replace(/[^a-z0-9_.-]/gi, '_'); export function SecuritySettings() { const { user } = useCurrentUser(); + const { logins } = useNostrLogin(); const [confirmed, setConfirmed] = useLocalStorage('backup-confirmed', false); const [showWarning, setShowWarning] = useState(!confirmed); + // Recovery reveal state + const [revealed, setRevealed] = useState(false); + const [showQr, setShowQr] = useState(false); + + // The active login. For nsec logins, the recovery code (nsec) is stored on + // the login object itself — this is how we can reveal it later for users who + // skipped backup at signup. For bunker/extension/Amber logins, the app never + // held the key, so there is nothing to reveal here. + const activeLogin = logins[0] as + | (typeof logins[number] & { nsec?: string; data?: { nsec?: string } }) + | undefined; + const loginType = activeLogin?.type; + // Nostrify stores the bech32 nsec directly on the login (older builds nested + // it under `data`), so check both to be safe. + const recoveryCode = loginType === 'nsec' + ? (activeLogin?.nsec ?? activeLogin?.data?.nsec) + : undefined; + + const handleReveal = () => setRevealed(true); + + const handleCopyRecovery = () => { + if (!recoveryCode) return; + navigator.clipboard.writeText(recoveryCode); + toast({ title: 'Copied!', description: 'Recovery code copied. Store it somewhere safe.' }); + }; + + const handleDownloadRecovery = () => { + if (!recoveryCode) return; + try { + const blob = new Blob([recoveryCode], { type: 'text/plain; charset=utf-8' }); + const url = globalThis.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = sanitizeFilename('skatespots-recovery-code.txt'); + a.style.display = 'none'; + document.body.appendChild(a); + a.click(); + globalThis.URL.revokeObjectURL(url); + document.body.removeChild(a); + toast({ title: 'Saved!', description: 'Recovery code downloaded. Keep it somewhere safe.' }); + } catch { + toast({ title: 'Download failed', description: 'Please copy it manually instead.', variant: 'destructive' }); + } + }; + if (!user) { return ( @@ -36,26 +85,121 @@ export function SecuritySettings() { }); }; - const handleGenerateQR = async () => { - try { - // Note: In a real implementation, you'd need access to the user's nsec/mnemonic - // This is a placeholder showing where QR generation would happen - toast({ - title: 'QR Code Generation', - description: 'This feature requires accessing your stored keys. Please use the backup methods provided during signup.', - variant: 'destructive', - }); - } catch { - toast({ - title: 'QR Code Failed', - description: 'Could not generate QR code.', - variant: 'destructive', - }); - } - }; - return (
+ {/* Recover / reveal recovery code — the real backup path for skaters who + skipped it at signup. Only possible for nsec logins. */} + + + + + Your Recovery Code + + + Didn't save it when you signed up? You can grab it here anytime. + + + + {recoveryCode ? ( + <> +

+ This code is the only way to log back in on a + new device or browser. Save it somewhere safe (a password manager, or written on paper). + Anyone who has it controls your account — never share it. +

+ + {!revealed ? ( + + ) : ( +
+
+ {recoveryCode} +
+ +
+ + + +
+ + {showQr && ( +
+
+ +
+

+ Scan with another Nostr app to log in. Keep this screen private. +

+
+ )} + +
+

⚠️ Never share this. Hide it when you're done.

+ +
+
+ )} + + ) : ( + + + + {loginType === 'x-amber' && ( + <>You're logged in with Amber. Your recovery code lives inside Amber — open the Amber app to back it up. SkateSpots never sees your key. + )} + {loginType === 'bunker' && ( + <>You're logged in with a remote signer (bunker). Your recovery code is held by your signer service, not SkateSpots. Back it up there. + )} + {loginType === 'extension' && ( + <>You're logged in with a browser extension. Your recovery code lives in the extension — use its own backup/export option. SkateSpots never sees your key. + )} + {!loginType && ( + <>Your recovery code is managed by your signer app, not SkateSpots. Back it up there. + )} + + + )} +
+
+ {/* Backup Warning */} {showWarning && ( @@ -97,8 +241,9 @@ export function SecuritySettings() { {!confirmed && (

- Did you save your recovery phrase or secret key during signup? - If not, you'll need to create a new account as we cannot recover lost keys. + Haven't saved your recovery code yet? No problem — grab it from the + Your Recovery Code section above, store it + somewhere safe, then confirm below.