SkateSpots/src/components/SecuritySettings.tsx
shakespeare.diy 4dc2b0129e Make backup gently unavoidable, not skippable-forever (1.3.13)
Closes the real safety hole: "Remind me later" used to set a PERMANENT
localStorage flag, silently becoming "never remind again" — so users who
never backed up were never reminded again.

- BackupReminder: "Later" now snoozes for the current session only
  (sessionStorage), so it returns each visit until backup is actually
  done. Only `backup-confirmed` stops it for good. Never blocks the user.
- Downloading OR copying the recovery code (in SignupDialog's done step
  and in SecuritySettings) now marks backup-confirmed — genuinely safe
  users stop being nagged automatically.
- Plain-language copy pass (EN + ES): "Save your login" / "recovery code"
  instead of "nsec / recovery phrase" jargon.
- Non-blocking and identical UX on both phone and desktop (bottom sheet).

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-09 11:23:44 +07:00

483 lines
21 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState } from '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);
// Copying the recovery code counts as a real backup — mark confirmed so the
// gentle reminder stops nagging a user who has genuinely saved their login.
setConfirmed(true);
setShowWarning(false);
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);
// A downloaded recovery file is a genuine backup — mark confirmed so the
// reminder stops for good.
setConfirmed(true);
setShowWarning(false);
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 (
<Card className="border-dashed">
<CardContent className="py-12 text-center">
<Shield className="h-12 w-12 mx-auto mb-4 text-muted-foreground/50" />
<h3 className="text-lg font-medium mb-2">Login Required</h3>
<p className="text-muted-foreground">
Log in to access security settings.
</p>
</CardContent>
</Card>
);
}
const handleConfirmBackup = () => {
setConfirmed(true);
setShowWarning(false);
toast({
title: 'Backup Confirmed! ✅',
description: 'Great! Your keys are safe.',
});
};
return (
<div className="space-y-6">
{/* Recover / reveal recovery code — the real backup path for skaters who
skipped it at signup. Only possible for nsec logins. */}
<Card className="bg-black/90 border-2 border-amber-500/40">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-amber-400">
<KeyRound className="h-5 w-5" />
Your Recovery Code
</CardTitle>
<CardDescription>
Didn't save it when you signed up? You can grab it here anytime.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{recoveryCode ? (
<>
<p className="text-sm text-gray-300">
This code is the <strong className="text-white">only</strong> 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.
</p>
{!revealed ? (
<Button
onClick={handleReveal}
className="w-full bg-amber-500 hover:bg-amber-600 text-black font-bold"
>
<Eye className="h-4 w-4 mr-2" />
Reveal my recovery code
</Button>
) : (
<div className="space-y-3">
<div className="p-3 bg-black rounded-lg border border-amber-500/30 font-mono text-xs break-all text-white select-all">
{recoveryCode}
</div>
<div className="grid grid-cols-3 gap-2">
<Button
variant="outline"
size="sm"
className="border-amber-500/40 text-amber-300 hover:bg-amber-500/10"
onClick={handleCopyRecovery}
>
<Copy className="h-3.5 w-3.5 mr-1.5" />
Copy
</Button>
<Button
variant="outline"
size="sm"
className="border-amber-500/40 text-amber-300 hover:bg-amber-500/10"
onClick={handleDownloadRecovery}
>
<Download className="h-3.5 w-3.5 mr-1.5" />
Save
</Button>
<Button
variant="outline"
size="sm"
className="border-amber-500/40 text-amber-300 hover:bg-amber-500/10"
onClick={() => setShowQr((v) => !v)}
>
<QrCode className="h-3.5 w-3.5 mr-1.5" />
QR
</Button>
</div>
{showQr && (
<div className="flex flex-col items-center gap-2 pt-2">
<div className="bg-white p-3 rounded-xl">
<QRCodeCanvas value={recoveryCode} size={200} level="M" />
</div>
<p className="text-xs text-gray-500 text-center max-w-xs">
Scan with another Nostr app to log in. Keep this screen private.
</p>
</div>
)}
<div className="flex items-center justify-between">
<p className="text-xs text-amber-300/80">⚠️ Never share this. Hide it when you're done.</p>
<Button
variant="ghost"
size="sm"
className="text-gray-500 hover:text-gray-300 h-auto py-1"
onClick={() => { setRevealed(false); setShowQr(false); }}
>
<EyeOff className="h-3.5 w-3.5 mr-1.5" />
Hide
</Button>
</div>
</div>
)}
</>
) : (
<Alert className="bg-blue-950/30 border border-blue-500/30">
<Shield className="h-4 w-4 text-blue-400" />
<AlertDescription className="text-sm text-gray-300">
{loginType === 'x-amber' && (
<>You're logged in with <strong className="text-white">Amber</strong>. 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 <strong className="text-white">remote signer (bunker)</strong>. Your recovery code is held by your signer service, not SkateSpots. Back it up there.</>
)}
{(loginType as string) === 'extension' && (
<>You're logged in with a <strong className="text-white">browser extension</strong>. 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.</>
)}
</AlertDescription>
</Alert>
)}
</CardContent>
</Card>
{/* Backup Warning */}
{showWarning && (
<Alert className="bg-gradient-to-r from-amber-500/10 to-red-500/10 border-2 border-amber-500/50">
<AlertTriangle className="h-5 w-5 text-amber-500" />
<AlertDescription>
<div className="space-y-3">
<p className="font-bold text-amber-400">
⚠️ Have you backed up your recovery phrase or secret key?
</p>
<p className="text-sm text-gray-300">
Your keys are the ONLY way to access your account. If you lose them, you lose everything -
there's no password reset!
</p>
<Button
onClick={handleConfirmBackup}
size="sm"
className="bg-amber-500 hover:bg-amber-600 text-black font-bold"
>
<CheckCircle className="h-4 w-4 mr-2" />
I've Backed Up My Keys
</Button>
</div>
</AlertDescription>
</Alert>
)}
{/* Backup Status */}
<Card className={confirmed ? "bg-green-950/20 border-2 border-green-500/30" : "bg-red-950/20 border-2 border-red-500/30"}>
<CardHeader>
<CardTitle className={confirmed ? "text-green-400" : "text-red-400"}>
{confirmed ? '✅ Backup Confirmed' : '⚠️ Backup Not Confirmed'}
</CardTitle>
<CardDescription>
{confirmed
? 'You\'ve confirmed that your keys are safely backed up.'
: 'Make sure to backup your recovery phrase or secret key!'}
</CardDescription>
</CardHeader>
{!confirmed && (
<CardContent>
<p className="text-sm text-gray-400 mb-4">
Haven't saved your recovery code yet? No problem — grab it from the
<strong className="text-amber-400"> Your Recovery Code</strong> section above, store it
somewhere safe, then confirm below.
</p>
<Button
onClick={handleConfirmBackup}
variant="outline"
className="border-green-500/50 text-green-400 hover:bg-green-500/20"
>
Yes, I've Backed Up My Keys
</Button>
</CardContent>
)}
</Card>
{/* Security Information */}
<Card className="bg-black/90 border-2 border-[#ccff00]/30">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-[#ccff00]">
<Shield className="h-5 w-5" />
Key Security Best Practices
</CardTitle>
<CardDescription>
How to keep your Nostr account safe
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-3 text-sm">
<div className="flex items-start gap-3">
<div className="text-2xl">✍️</div>
<div>
<p className="font-bold text-white">Write it down on paper</p>
<p className="text-gray-400">Physical backup is the safest. Store in a secure location like a safe.</p>
</div>
</div>
<div className="flex items-start gap-3">
<div className="text-2xl">💾</div>
<div>
<p className="font-bold text-white">Multiple backups</p>
<p className="text-gray-400">Keep copies in different secure locations (home safe, bank deposit box, etc.)</p>
</div>
</div>
<div className="flex items-start gap-3">
<div className="text-2xl">🔒</div>
<div>
<p className="font-bold text-white">Never share your keys</p>
<p className="text-gray-400">Real Nostr apps NEVER ask for your nsec or recovery phrase after initial login</p>
</div>
</div>
<div className="flex items-start gap-3">
<div className="text-2xl">🚫</div>
<div>
<p className="font-bold text-white">Don't store in cloud unencrypted</p>
<p className="text-gray-400">Avoid Google Docs, email drafts, or plain text cloud storage</p>
</div>
</div>
<div className="flex items-start gap-3">
<div className="text-2xl">🔐</div>
<div>
<p className="font-bold text-white">Consider using a browser extension</p>
<p className="text-gray-400">Alby, nos2x, or Flamingo keep your keys secure and never expose them to websites</p>
</div>
</div>
</div>
<div className="pt-4 border-t border-[#ccff00]/20">
<h4 className="font-bold text-[#ff0040] mb-2 uppercase text-sm">
⚠️ Important Reminders
</h4>
<ul className="text-xs text-gray-400 space-y-1">
<li>• If you lose your keys, you lose your account permanently</li>
<li>• No company or person can reset your password - not even us</li>
<li>• Anyone with your keys has full control of your account</li>
<li>• Your keys control all your Nostr data across ALL apps</li>
</ul>
</div>
</CardContent>
</Card>
{/* Browser Extension & Mobile App Recommendations */}
<Card className="bg-black/90 border-2 border-purple-500/30">
<CardHeader>
<CardTitle className="text-purple-400">🔌 Recommended Nostr Signers</CardTitle>
<CardDescription>
Use a Nostr signer for better security and convenience
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-3">
{/* Amber - Android */}
<a
href="https://github.com/greenart7c3/Amber"
target="_blank"
rel="noopener noreferrer"
className="block p-4 rounded-lg bg-gradient-to-r from-[#ccff00]/10 to-green-500/10 border border-[#ccff00]/30 hover:border-[#ccff00]/60 transition-all"
>
<div className="flex items-start justify-between gap-3">
<div className="flex-1">
<div className="font-bold text-white flex items-center gap-2">
Amber
<span className="text-xs px-2 py-0.5 bg-[#ccff00]/20 text-[#ccff00] rounded-full">Android</span>
</div>
<p className="text-xs text-gray-400 mt-1">
Secure Nostr signer for Android. Keeps your keys safe on your phone and signs events without exposing them.
</p>
</div>
<ExternalLink className="h-4 w-4 text-gray-500" />
</div>
</a>
<a
href="https://getalby.com"
target="_blank"
rel="noopener noreferrer"
className="block p-4 rounded-lg bg-gradient-to-r from-amber-500/10 to-yellow-500/10 border border-amber-500/30 hover:border-amber-500/60 transition-all"
>
<div className="flex items-start justify-between gap-3">
<div className="flex-1">
<div className="font-bold text-white flex items-center gap-2">
Alby
<span className="text-xs px-2 py-0.5 bg-green-500/20 text-green-400 rounded-full">Recommended</span>
</div>
<p className="text-xs text-gray-400 mt-1">
Full-featured extension with Lightning wallet, WebLN, and NWC support.
</p>
</div>
<ExternalLink className="h-4 w-4 text-gray-500" />
</div>
</a>
<a
href="https://www.getflamingo.org/"
target="_blank"
rel="noopener noreferrer"
className="block p-4 rounded-lg bg-black/50 border border-purple-500/30 hover:border-purple-500/60 transition-all"
>
<div className="flex items-start justify-between gap-3">
<div className="flex-1">
<div className="font-bold text-white">Flamingo</div>
<p className="text-xs text-gray-400 mt-1">
Simple, clean interface. Focuses on Nostr signing without wallet features.
</p>
</div>
<ExternalLink className="h-4 w-4 text-gray-500" />
</div>
</a>
<a
href="https://github.com/fiatjaf/nos2x"
target="_blank"
rel="noopener noreferrer"
className="block p-4 rounded-lg bg-black/50 border border-purple-500/30 hover:border-purple-500/60 transition-all"
>
<div className="flex items-start justify-between gap-3">
<div className="flex-1">
<div className="font-bold text-white">nos2x</div>
<p className="text-xs text-gray-400 mt-1">
Minimal, lightweight extension for advanced users.
</p>
</div>
<ExternalLink className="h-4 w-4 text-gray-500" />
</div>
</a>
</div>
</CardContent>
</Card>
{/* Nostr Connect (Bunkers) */}
<Card className="bg-black/90 border-2 border-blue-500/30">
<CardHeader>
<CardTitle className="text-blue-400">☁️ Nostr Connect (Remote Signers)</CardTitle>
<CardDescription>
Use a remote signer to access your account from any device
</CardDescription>
</CardHeader>
<CardContent>
<p className="text-sm text-gray-400 mb-4">
Nostr Connect (also called "bunkers") lets you store your keys on a secure server and sign in from any device.
Your keys never leave the server, but you can use your account anywhere!
</p>
<div className="space-y-3">
<a
href="https://nsec.app"
target="_blank"
rel="noopener noreferrer"
className="block p-4 rounded-lg bg-gradient-to-r from-blue-500/10 to-cyan-500/10 border border-blue-500/30 hover:border-blue-500/60 transition-all"
>
<div className="flex items-start justify-between gap-3">
<div className="flex-1">
<div className="font-bold text-white flex items-center gap-2">
nsec.app
<span className="text-xs px-2 py-0.5 bg-blue-500/20 text-blue-400 rounded-full">Popular</span>
</div>
<p className="text-xs text-gray-400 mt-1">
Free, secure remote signer. Create bunker:// URI and paste when logging in.
</p>
</div>
<ExternalLink className="h-4 w-4 text-gray-500" />
</div>
</a>
<a
href="https://nostrum.social"
target="_blank"
rel="noopener noreferrer"
className="block p-4 rounded-lg bg-black/50 border border-blue-500/30 hover:border-blue-500/60 transition-all"
>
<div className="flex items-start justify-between gap-3">
<div className="flex-1">
<div className="font-bold text-white">Nostrum</div>
<p className="text-xs text-gray-400 mt-1">
Another remote signer option with mobile support.
</p>
</div>
<ExternalLink className="h-4 w-4 text-gray-500" />
</div>
</a>
</div>
<div className="mt-4 p-3 rounded-lg bg-blue-950/30 border border-blue-500/30 text-sm text-gray-300">
<p><strong className="text-blue-400">💡 How to use:</strong> Create an account on one of these services,
get your bunker:// connection string, then paste it in the "Nostr Connect" tab when logging in.</p>
</div>
</CardContent>
</Card>
</div>
);
}