From 4dc2b0129ecaaa2389972f0e6ef411f5e1b0f2db Mon Sep 17 00:00:00 2001 From: "shakespeare.diy" Date: Wed, 9 Sep 2026 11:23:44 +0700 Subject: [PATCH] Make backup gently unavoidable, not skippable-forever (1.3.13) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/components/BackupReminder.tsx | 34 +++++++++++++++++++--------- src/components/SecuritySettings.tsx | 8 +++++++ src/components/auth/SignupDialog.tsx | 8 ++++++- src/i18n/en.ts | 14 ++++++------ src/i18n/es.ts | 14 ++++++------ 5 files changed, 52 insertions(+), 26 deletions(-) diff --git a/src/components/BackupReminder.tsx b/src/components/BackupReminder.tsx index 9985605..98f651b 100644 --- a/src/components/BackupReminder.tsx +++ b/src/components/BackupReminder.tsx @@ -13,17 +13,23 @@ import { SheetTitle, } from '@/components/ui/sheet'; -const BACKUP_REMINDER_KEY = 'backup-reminder-dismissed'; const BACKUP_CONFIRMED_KEY = 'backup-confirmed'; +// Session-scoped snooze key. "Remind me later" used to set a PERMANENT +// localStorage flag, which silently turned into "never remind again" — a real +// safety hole. We now snooze only for the current browser session, so the +// reminder returns on the next visit until the user actually saves their login. +// Only `backup-confirmed` (set when they back up) stops it for good. +const BACKUP_SNOOZE_KEY = 'backup-reminder-snoozed-session'; /** - * Persistent backup reminder that shows after signup - * Encourages users to backup their recovery phrase or nsec - * Uses a bottom sheet for better mobile experience + * Gently-persistent backup reminder shown after signup. + * Encourages users to save their recovery code (nsec) so they never lose access. + * Non-blocking: one tap to back up, one tap to say "already done", or snooze for + * this session. It keeps coming back each session until backup is confirmed. + * Uses a bottom sheet for a good experience on both phone and desktop. */ export function BackupReminder() { const { user } = useCurrentUser(); - const [dismissed, setDismissed] = useLocalStorage(BACKUP_REMINDER_KEY, false); const [confirmed, setConfirmed] = useLocalStorage(BACKUP_CONFIRMED_KEY, false); const [showReminder, setShowReminder] = useState(false); const { t } = useTranslation(); @@ -37,19 +43,25 @@ export function BackupReminder() { // Show reminder if: // 1. User is logged in - // 2. Not dismissed - // 3. Not confirmed they backed up - // Add a small delay so it doesn't immediately pop up on login + // 2. Not snoozed for THIS session (resets when they come back) + // 3. Not confirmed they backed up (the only permanent stop) + // Add a small delay so it doesn't immediately pop up on login. const timer = setTimeout(() => { - const shouldShow = !dismissed && !confirmed; + const snoozed = sessionStorage.getItem(BACKUP_SNOOZE_KEY) === 'true'; + const shouldShow = !snoozed && !confirmed; setShowReminder(shouldShow); }, 2000); return () => clearTimeout(timer); - }, [user, dismissed, confirmed]); + }, [user, confirmed]); const handleDismiss = () => { - setDismissed(true); + // Snooze for this session only — it returns next visit until backup is done. + try { + sessionStorage.setItem(BACKUP_SNOOZE_KEY, 'true'); + } catch { + // sessionStorage can throw in strict privacy modes — safe to ignore. + } setShowReminder(false); }; diff --git a/src/components/SecuritySettings.tsx b/src/components/SecuritySettings.tsx index 47eb549..2eac4f9 100644 --- a/src/components/SecuritySettings.tsx +++ b/src/components/SecuritySettings.tsx @@ -40,6 +40,10 @@ export function SecuritySettings() { 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.' }); }; @@ -56,6 +60,10 @@ export function SecuritySettings() { 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' }); diff --git a/src/components/auth/SignupDialog.tsx b/src/components/auth/SignupDialog.tsx index 65fb6b2..d8b2d2f 100644 --- a/src/components/auth/SignupDialog.tsx +++ b/src/components/auth/SignupDialog.tsx @@ -13,6 +13,7 @@ import { toast } from '@/hooks/useToast'; import { useLoginActions } from '@/hooks/useLoginActions'; import { useNostrPublish } from '@/hooks/useNostrPublish'; import { useUploadFile } from '@/hooks/useUploadFile'; +import { useLocalStorage } from '@/hooks/useLocalStorage'; import { generateSecretKey, nip19 } from 'nostr-tools'; import { cn } from '@/lib/utils'; @@ -44,6 +45,9 @@ const SignupDialog: React.FC = ({ isOpen, onClose, onComplete const { mutateAsync: publishEvent, isPending: isPublishing } = useNostrPublish(); const { mutateAsync: uploadFile, isPending: isUploading } = useUploadFile(); const avatarFileInputRef = useRef(null); + // Shared source of truth: once the recovery code is saved here, mark backup + // confirmed so the gentle post-signup reminder never nags a safe user. + const [, setBackupConfirmed] = useLocalStorage('backup-confirmed', false); // Silently generate a key and log the user in immediately. // No forced download, no crypto vocabulary — the account "just works". @@ -86,8 +90,9 @@ const SignupDialog: React.FC = ({ isOpen, onClose, onComplete globalThis.URL.revokeObjectURL(url); document.body.removeChild(a); - // Mark as secured + // Mark as secured — and record a confirmed backup so the reminder stops. setKeySecured('downloaded'); + setBackupConfirmed(true); toast({ title: 'Secret Key Saved!', @@ -561,6 +566,7 @@ const SignupDialog: React.FC = ({ isOpen, onClose, onComplete className='border-amber-500/40 text-amber-300 hover:bg-amber-500/10' onClick={() => { navigator.clipboard.writeText(nsec); + setBackupConfirmed(true); toast({ title: 'Copied!', description: 'Recovery code copied. Keep it somewhere safe.' }); }} > diff --git a/src/i18n/en.ts b/src/i18n/en.ts index 3ecf332..8f80d40 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -92,13 +92,13 @@ const en = { // Backup Reminder backup: { - title: 'Backup Your Account!', - subtitle: 'Important security reminder', - message: 'Your recovery phrase or secret key (nsec) is the ONLY way to access your account from other devices.', - warning: 'If you lose it, you lose your account forever!', - backupNow: 'Backup Now', - confirmed: "I've Backed It Up", - remindLater: 'Remind me later', + title: 'Save your login', + subtitle: 'Takes 10 seconds — do it once', + message: 'Your recovery code is the only way back into your account. Save it and you can log in on any device.', + warning: "Lose it and there's no reset — no email, no password recovery.", + backupNow: 'Save it now', + confirmed: "I've saved it", + remindLater: 'Later', }, // PWA Install diff --git a/src/i18n/es.ts b/src/i18n/es.ts index d1a0cc9..cda63b1 100644 --- a/src/i18n/es.ts +++ b/src/i18n/es.ts @@ -93,13 +93,13 @@ const es = { // Backup Reminder backup: { - title: '¡Respalda Tu Cuenta!', - subtitle: 'Recordatorio de seguridad importante', - message: 'Tu frase de recuperación o clave secreta (nsec) es la ÚNICA forma de acceder a tu cuenta desde otros dispositivos.', - warning: '¡Si la pierdes, pierdes tu cuenta para siempre!', - backupNow: 'Respaldar Ahora', - confirmed: 'Ya la Respaldé', - remindLater: 'Recordarme después', + title: 'Guarda tu acceso', + subtitle: 'Toma 10 segundos — hazlo una vez', + message: 'Tu código de recuperación es la única forma de volver a tu cuenta. Guárdalo y podrás iniciar sesión en cualquier dispositivo.', + warning: 'Si lo pierdes no hay reinicio — sin correo ni recuperación de contraseña.', + backupNow: 'Guardar ahora', + confirmed: 'Ya lo guardé', + remindLater: 'Después', }, // PWA Install