SkateSpots/src/components/SecuritySettings.tsx
SkateSpots 0b796440f1 feat(i18n): Spanish batch 2 — nav/chrome, events/shops/tours, crews/DMs, settings/auth, wallet/stash/newsletter/stats
~3,470 new keys (5,770 total, EN=ES), ~115 more files wired.
- nav + chromeUi: header, More menu, burger menu, footer, map toolbar widgets
  (sort/trust/weather), spot-type label helper.
- placesUi: events, shops, tours, food. socialUi: crews, DMs, notifications,
  collectives, crew chat/coordinator. settingsUi: settings, auth/signup,
  onboarding, profile edit, legal. walletUi: wallet/Cashu, stash, mints,
  app shop, newsletter, community stats, leaderboards, profile.
- Fixed 738 usages where an agent dropped the socialUi. prefix (caught by check:i18n).
2026-10-02 10:11:06 +07:00

481 lines
20 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';
import { useTranslation } from '@/contexts/language';
const sanitizeFilename = (filename: string) => filename.replace(/[^a-z0-9_.-]/gi, '_');
export function SecuritySettings() {
const { user } = useCurrentUser();
const { logins } = useNostrLogin();
const { t } = useTranslation();
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: t('common.copied'), description: t('settingsUi.security.recoveryCopied') });
};
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: t('settingsUi.security.savedTitle'), description: t('settingsUi.security.recoveryDownloaded') });
} catch {
toast({ title: t('settingsUi.security.downloadFailedTitle'), description: t('settingsUi.security.downloadFailedDesc'), 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">{t('settingsUi.security.loginRequired')}</h3>
<p className="text-muted-foreground">
{t('settingsUi.security.loginRequiredDesc')}
</p>
</CardContent>
</Card>
);
}
const handleConfirmBackup = () => {
setConfirmed(true);
setShowWarning(false);
toast({
title: t('settingsUi.security.backupConfirmedTitle'),
description: t('settingsUi.security.backupConfirmedDesc'),
});
};
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" />
{t('settingsUi.security.yourRecoveryCode')}
</CardTitle>
<CardDescription>
{t('settingsUi.security.recoveryCodeDesc')}
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{recoveryCode ? (
<>
<p className="text-sm text-gray-300">
{t('settingsUi.security.recoveryCodeBefore')}{' '}
<strong className="text-white">{t('settingsUi.security.only')}</strong>{' '}
{t('settingsUi.security.recoveryCodeAfter')}
</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" />
{t('settingsUi.security.revealRecovery')}
</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" />
{t('common.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" />
{t('common.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">
{t('settingsUi.security.qrHint')}
</p>
</div>
)}
<div className="flex items-center justify-between">
<p className="text-xs text-amber-300/80">{t('settingsUi.security.neverShareHide')}</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" />
{t('settingsUi.security.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' && (
<>{t('settingsUi.security.amberNoteBefore')} <strong className="text-white">Amber</strong>{t('settingsUi.security.amberNoteAfter')}</>
)}
{loginType === 'bunker' && (
<>{t('settingsUi.security.bunkerNoteBefore')} <strong className="text-white">{t('settingsUi.security.remoteSigner')}</strong>{t('settingsUi.security.bunkerNoteAfter')}</>
)}
{(loginType as string) === 'extension' && (
<>{t('settingsUi.security.extensionNoteBefore')} <strong className="text-white">{t('settingsUi.security.browserExtension')}</strong>{t('settingsUi.security.extensionNoteAfter')}</>
)}
{!loginType && (
<>{t('settingsUi.security.otherNote')}</>
)}
</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">
{t('settingsUi.security.backupWarningTitle')}
</p>
<p className="text-sm text-gray-300">
{t('settingsUi.security.backupWarningBody')}
</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" />
{t('settingsUi.security.iveBackedUp')}
</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 ? t('settingsUi.security.backupConfirmed') : t('settingsUi.security.backupNotConfirmed')}
</CardTitle>
<CardDescription>
{confirmed
? t('settingsUi.security.backupConfirmedStatusDesc')
: t('settingsUi.security.backupNotConfirmedDesc')}
</CardDescription>
</CardHeader>
{!confirmed && (
<CardContent>
<p className="text-sm text-gray-400 mb-4">
{t('settingsUi.security.notSavedYetBefore')}
<strong className="text-amber-400"> {t('settingsUi.security.yourRecoveryCode')}</strong> {t('settingsUi.security.notSavedYetAfter')}
</p>
<Button
onClick={handleConfirmBackup}
variant="outline"
className="border-green-500/50 text-green-400 hover:bg-green-500/20"
>
{t('settingsUi.security.yesBackedUp')}
</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" />
{t('settingsUi.security.bestPracticesTitle')}
</CardTitle>
<CardDescription>
{t('settingsUi.security.bestPracticesDesc')}
</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">{t('settingsUi.security.bp1Title')}</p>
<p className="text-gray-400">{t('settingsUi.security.bp1Desc')}</p>
</div>
</div>
<div className="flex items-start gap-3">
<div className="text-2xl">💾</div>
<div>
<p className="font-bold text-white">{t('settingsUi.security.bp2Title')}</p>
<p className="text-gray-400">{t('settingsUi.security.bp2Desc')}</p>
</div>
</div>
<div className="flex items-start gap-3">
<div className="text-2xl">🔒</div>
<div>
<p className="font-bold text-white">{t('settingsUi.security.bp3Title')}</p>
<p className="text-gray-400">{t('settingsUi.security.bp3Desc')}</p>
</div>
</div>
<div className="flex items-start gap-3">
<div className="text-2xl">🚫</div>
<div>
<p className="font-bold text-white">{t('settingsUi.security.bp4Title')}</p>
<p className="text-gray-400">{t('settingsUi.security.bp4Desc')}</p>
</div>
</div>
<div className="flex items-start gap-3">
<div className="text-2xl">🔐</div>
<div>
<p className="font-bold text-white">{t('settingsUi.security.bp5Title')}</p>
<p className="text-gray-400">{t('settingsUi.security.bp5Desc')}</p>
</div>
</div>
</div>
<div className="pt-4 border-t border-[#ccff00]/20">
<h4 className="font-bold text-[#ff0040] mb-2 uppercase text-sm">
{t('settingsUi.security.importantReminders')}
</h4>
<ul className="text-xs text-gray-400 space-y-1">
<li>• {t('settingsUi.security.rem1')}</li>
<li>• {t('settingsUi.security.rem2')}</li>
<li>• {t('settingsUi.security.rem3')}</li>
<li>• {t('settingsUi.security.rem4')}</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">{t('settingsUi.security.recommendedSigners')}</CardTitle>
<CardDescription>
{t('settingsUi.security.recommendedSignersDesc')}
</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">
{t('settingsUi.security.amberDesc')}
</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">{t('settingsUi.security.recommended')}</span>
</div>
<p className="text-xs text-gray-400 mt-1">
{t('settingsUi.security.albyDesc')}
</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">
{t('settingsUi.security.flamingoDesc')}
</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">
{t('settingsUi.security.nos2xDesc')}
</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">{t('settingsUi.security.nostrConnectTitle')}</CardTitle>
<CardDescription>
{t('settingsUi.security.nostrConnectDesc')}
</CardDescription>
</CardHeader>
<CardContent>
<p className="text-sm text-gray-400 mb-4">
{t('settingsUi.security.nostrConnectBody')}
</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">{t('common.popular')}</span>
</div>
<p className="text-xs text-gray-400 mt-1">
{t('settingsUi.security.nsecAppDesc')}
</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">
{t('settingsUi.security.nostrumDesc')}
</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">{t('settingsUi.security.howToUse')}</strong> {t('settingsUi.security.howToUseDesc')}</p>
</div>
</CardContent>
</Card>
</div>
);
}