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}
/>