diff --git a/src/components/StashBackupDialogs.tsx b/src/components/StashBackupDialogs.tsx index ffd8605..bf4854a 100644 --- a/src/components/StashBackupDialogs.tsx +++ b/src/components/StashBackupDialogs.tsx @@ -10,17 +10,40 @@ import { cn } from '@/lib/utils'; type ExportMode = 'password' | 'nip44' | 'plaintext'; +/** Cheap password-strength heuristic (length + character classes), 0–4. */ +function passwordStrength(pw: string): number { + if (!pw) return 0; + let score = 0; + if (pw.length >= 8) score++; + if (pw.length >= 12) score++; + if (/[a-z]/.test(pw) && /[A-Z]/.test(pw)) score++; + if (/\d/.test(pw)) score++; + if (/[^A-Za-z0-9]/.test(pw)) score++; + return Math.min(4, score); +} + +const STRENGTH_STEPS = [ + { key: 'walletUi.stash.strengthWeak', bar: 'bg-red-500', text: 'text-red-400' }, + { key: 'walletUi.stash.strengthWeak', bar: 'bg-red-500', text: 'text-red-400' }, + { key: 'walletUi.stash.strengthFair', bar: 'bg-amber-500', text: 'text-amber-400' }, + { key: 'walletUi.stash.strengthGood', bar: 'bg-lime-500', text: 'text-lime-400' }, + { key: 'walletUi.stash.strengthStrong', bar: 'bg-green-500', text: 'text-green-400' }, +] as const; + /** Export the stash as a file, optionally password- or key-protected. */ export function StashBackupExportDialog({ open, onOpenChange, itemCount, + baseName, canUseKey, onExport, }: { open: boolean; onOpenChange: (open: boolean) => void; itemCount: number; + /** Filename without extension, e.g. `skatespots-stash-2026-10-03`. */ + baseName: string; canUseKey: boolean; onExport: (opts: { mode: ExportMode; password?: string }) => Promise; }) { @@ -32,6 +55,12 @@ export function StashBackupExportDialog({ const [busy, setBusy] = useState(false); const [error, setError] = useState(null); + const strength = passwordStrength(pw); + const step = STRENGTH_STEPS[strength]; + // The main button always produces an encrypted file; the plaintext link is a + // separate, explicit action. + const previewName = `${baseName}-encrypted.json`; + useEffect(() => { if (open) { setMode('password'); @@ -133,6 +162,19 @@ export function StashBackupExportDialog({ />

{t('walletUi.stash.passwordHint')}

+ {pw.length > 0 && ( +
+
+ {t('walletUi.stash.strength')} + {t(step.key)} +
+
+ {[0, 1, 2, 3].map((i) => ( + + ))} +
+
+ )} ) : (

{t('walletUi.stash.keyModeHint')}

@@ -143,6 +185,10 @@ export function StashBackupExportDialog({ {error}

)} + +

+ {t('walletUi.stash.filenameWillSave', { name: previewName })} +

diff --git a/src/i18n/en.ts b/src/i18n/en.ts index ba6882f..ac105eb 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -6324,6 +6324,12 @@ walletUi: { unlockFailed: 'Could not unlock the backup.', show: 'Show', hide: 'Hide', + strength: 'Password strength', + strengthWeak: 'Weak', + strengthFair: 'Fair', + strengthGood: 'Good', + strengthStrong: 'Strong', + filenameWillSave: 'Will save as {{name}}', export: 'Export', import: 'Import', exported: 'Stash exported', diff --git a/src/i18n/es.ts b/src/i18n/es.ts index 4bb4bf6..7e48c3b 100644 --- a/src/i18n/es.ts +++ b/src/i18n/es.ts @@ -6324,6 +6324,12 @@ walletUi: { unlockFailed: 'No se pudo desbloquear la copia.', show: 'Mostrar', hide: 'Ocultar', + strength: 'Seguridad de la contraseña', + strengthWeak: 'Débil', + strengthFair: 'Aceptable', + strengthGood: 'Buena', + strengthStrong: 'Fuerte', + filenameWillSave: 'Se guardará como {{name}}', export: 'Exportar', import: 'Importar', exported: 'Stash exportado', diff --git a/src/pages/StashPage.tsx b/src/pages/StashPage.tsx index d019865..76e2e19 100644 --- a/src/pages/StashPage.tsx +++ b/src/pages/StashPage.tsx @@ -662,6 +662,7 @@ export default function StashPage() { const [exportOpen, setExportOpen] = useState(false); const [unlockOpen, setUnlockOpen] = useState(false); const [importParsed, setImportParsed] = useState(null); + const exportBaseName = `skatespots-stash-${new Date().toISOString().slice(0, 10)}`; // ── Backup & transfer: your stash is encrypted on Nostr, but a local copy you // control means a dead relay (or a new account) can't lose your gear list. ── @@ -677,7 +678,7 @@ export default function StashPage() { const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; - a.download = `skatespots-stash-${new Date().toISOString().slice(0, 10)}.json`; + a.download = `${exportBaseName}${mode === 'plaintext' ? '' : '-encrypted'}.json`; document.body.appendChild(a); a.click(); a.remove(); @@ -1567,6 +1568,7 @@ export default function StashPage() { open={exportOpen} onOpenChange={setExportOpen} itemCount={items.length} + baseName={exportBaseName} canUseKey={!!user?.signer?.nip44} onExport={handleExport} />