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.