- BackupReminder: wait while the signup dialog is open (the key is minted mid-flow), so "Save your login" no longer pops before signup is finished. Uses a small non-reactive signup flag set by SignupDialog. - BackupReminder: removed the custom close button — the Sheet already renders its own X (the sheet's close now snoozes for the session). - CreateEventPage: the "Link to a Skate Spot" row no longer overlaps — the label/hint can shrink and the Select Spot button stays put.
165 lines
6.1 KiB
TypeScript
165 lines
6.1 KiB
TypeScript
import { useState, useEffect } from 'react';
|
||
import { useNavigate } from 'react-router-dom';
|
||
import { AlertTriangle, Download, CheckCircle } from 'lucide-react';
|
||
import { Button } from '@/components/ui/button';
|
||
import { useCurrentUser } from '@/hooks/useCurrentUser';
|
||
import { useLocalStorage } from '@/hooks/useLocalStorage';
|
||
import { backupConfirmedKey } from '@/lib/backup';
|
||
import { isSignupOpen } from '@/lib/uiFlags';
|
||
import { useTranslation } from '@/contexts/language';
|
||
import {
|
||
Sheet,
|
||
SheetContent,
|
||
SheetDescription,
|
||
SheetHeader,
|
||
SheetTitle,
|
||
} from '@/components/ui/sheet';
|
||
|
||
// 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 the per-account `backup-confirmed:<pubkey>` flag (set when they back up)
|
||
// stops it for good.
|
||
const BACKUP_SNOOZE_KEY = 'backup-reminder-snoozed-session';
|
||
|
||
/**
|
||
* 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 [confirmed, setConfirmed] = useLocalStorage(backupConfirmedKey(user?.pubkey), false);
|
||
const [showReminder, setShowReminder] = useState(false);
|
||
const { t } = useTranslation();
|
||
const navigate = useNavigate();
|
||
|
||
useEffect(() => {
|
||
if (!user) {
|
||
setShowReminder(false);
|
||
return;
|
||
}
|
||
|
||
// Wait out the signup dialog (the key is minted mid-flow), then show unless
|
||
// snoozed for this session or already confirmed.
|
||
let cancelled = false;
|
||
let timer: ReturnType<typeof setTimeout>;
|
||
const attempt = () => {
|
||
if (cancelled) return;
|
||
if (isSignupOpen()) {
|
||
timer = setTimeout(attempt, 2000);
|
||
return;
|
||
}
|
||
const snoozed = sessionStorage.getItem(BACKUP_SNOOZE_KEY) === 'true';
|
||
setShowReminder(!snoozed && !confirmed);
|
||
};
|
||
timer = setTimeout(attempt, 2000);
|
||
|
||
return () => {
|
||
cancelled = true;
|
||
clearTimeout(timer);
|
||
};
|
||
}, [user, confirmed]);
|
||
|
||
const handleDismiss = () => {
|
||
// 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);
|
||
};
|
||
|
||
const handleConfirmBackup = () => {
|
||
setConfirmed(true);
|
||
setShowReminder(false);
|
||
};
|
||
|
||
const handleBackupNow = () => {
|
||
// Client-side nav (HashRouter). window.location.href forced a full reload to
|
||
// a non-hash path the router couldn't match — it dropped the user on a blank
|
||
// page instead of the security settings. navigate() keeps it in-app.
|
||
setShowReminder(false);
|
||
// SettingsPage deep-links via ?section=… (NOT ?tab=…) — using the wrong
|
||
// param silently dropped the user on the settings root instead of Security.
|
||
navigate('/settings?section=security');
|
||
};
|
||
|
||
if (!showReminder) return null;
|
||
|
||
return (
|
||
<Sheet open={showReminder} onOpenChange={(open) => { if (!open) handleDismiss(); }}>
|
||
<SheetContent
|
||
side="bottom"
|
||
className="rounded-t-2xl border-t-4 border-amber-500 bg-gradient-to-b from-amber-950/90 to-black/95 backdrop-blur-lg"
|
||
onInteractOutside={(e) => e.preventDefault()}
|
||
>
|
||
<SheetHeader className="text-left">
|
||
<div className="flex items-start justify-between">
|
||
<div className="flex items-center gap-3">
|
||
<div className="flex-shrink-0 w-10 h-10 sm:w-12 sm:h-12 rounded-full bg-amber-500/20 flex items-center justify-center">
|
||
<AlertTriangle className="h-5 w-5 sm:h-6 sm:w-6 text-amber-500" />
|
||
</div>
|
||
<div className="flex-1">
|
||
<SheetTitle className="text-lg sm:text-xl font-bold text-amber-400">
|
||
{t('backup.title')}
|
||
</SheetTitle>
|
||
<SheetDescription className="text-xs sm:text-sm text-gray-400">
|
||
{t('backup.subtitle')}
|
||
</SheetDescription>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</SheetHeader>
|
||
|
||
<div className="mt-4 sm:mt-6 space-y-4">
|
||
{/* Warning message */}
|
||
<div className="p-3 sm:p-4 rounded-xl bg-amber-500/10 border border-amber-500/30">
|
||
<p className="text-sm sm:text-base text-gray-200 leading-relaxed">
|
||
{t('backup.message')}
|
||
</p>
|
||
<p className="text-sm sm:text-base text-gray-300 mt-2">
|
||
⚠️ {t('backup.warning')}
|
||
</p>
|
||
</div>
|
||
|
||
{/* Action buttons - stacked on mobile */}
|
||
<div className="flex flex-col sm:flex-row gap-3">
|
||
<Button
|
||
onClick={handleBackupNow}
|
||
className="flex-1 h-12 sm:h-11 text-sm sm:text-base font-semibold bg-amber-500 hover:bg-amber-600 text-black"
|
||
size="lg"
|
||
>
|
||
<Download className="mr-2 h-5 w-5" />
|
||
{t('backup.backupNow')}
|
||
</Button>
|
||
|
||
<Button
|
||
onClick={handleConfirmBackup}
|
||
variant="outline"
|
||
className="flex-1 h-12 sm:h-11 text-sm sm:text-base border-amber-500/50 text-amber-400 hover:bg-amber-500/20"
|
||
size="lg"
|
||
>
|
||
<CheckCircle className="mr-2 h-5 w-5" />
|
||
{t('backup.confirmed')}
|
||
</Button>
|
||
</div>
|
||
|
||
{/* Dismiss link */}
|
||
<div className="text-center pt-1">
|
||
<button
|
||
onClick={handleDismiss}
|
||
className="text-xs text-gray-500 hover:text-gray-400 underline"
|
||
>
|
||
{t('backup.remindLater')}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</SheetContent>
|
||
</Sheet>
|
||
);
|
||
}
|