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>
This commit is contained in:
parent
e3fb84d539
commit
4dc2b0129e
5 changed files with 52 additions and 26 deletions
|
|
@ -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);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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' });
|
||||
|
|
|
|||
|
|
@ -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<SignupDialogProps> = ({ isOpen, onClose, onComplete
|
|||
const { mutateAsync: publishEvent, isPending: isPublishing } = useNostrPublish();
|
||||
const { mutateAsync: uploadFile, isPending: isUploading } = useUploadFile();
|
||||
const avatarFileInputRef = useRef<HTMLInputElement>(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<SignupDialogProps> = ({ 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<SignupDialogProps> = ({ 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.' });
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue