SkateSpots/src/components/BackupReminder.tsx
SkateSpots 90cb429dc3 fix(ui): backup-reminder timing + stray X, and event spot-link layout
- 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.
2026-10-03 21:18:40 +07:00

165 lines
6.1 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
);
}