Removes the last react-refresh disables by splitting exports instead: - contexts/language.ts: Language/Translations types + LanguageContext + useTranslation/useT (LanguageProvider stays a component-only file); 12 importers repointed. - lib/sortOptions.ts: calculatePopularity + calculateDistance (out of SortDropdown) - lib/trustFilter.ts: TrustFilterLevel + filterByTrustLevel (out of TrustFilter) - EventCountdown: deleted the unused useEventStatus hook - NewsletterPage: dropped the NEWSLETTER_ISSUES re-export shim - AGENTS.md updated (lint stays clean with no blanket disables) Gate: 101 tests, 0 errors/warnings, boot smoke green.
164 lines
6.1 KiB
TypeScript
164 lines
6.1 KiB
TypeScript
import { useState, useEffect } from 'react';
|
||
import { useNavigate } from 'react-router-dom';
|
||
import { AlertTriangle, X, Download, CheckCircle } from 'lucide-react';
|
||
import { Button } from '@/components/ui/button';
|
||
import { useCurrentUser } from '@/hooks/useCurrentUser';
|
||
import { useLocalStorage } from '@/hooks/useLocalStorage';
|
||
import { useTranslation } from '@/contexts/language';
|
||
import {
|
||
Sheet,
|
||
SheetContent,
|
||
SheetDescription,
|
||
SheetHeader,
|
||
SheetTitle,
|
||
} from '@/components/ui/sheet';
|
||
|
||
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';
|
||
|
||
/**
|
||
* 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(BACKUP_CONFIRMED_KEY, false);
|
||
const [showReminder, setShowReminder] = useState(false);
|
||
const { t } = useTranslation();
|
||
const navigate = useNavigate();
|
||
|
||
useEffect(() => {
|
||
if (!user) {
|
||
setShowReminder(false);
|
||
return;
|
||
}
|
||
|
||
// Show reminder if:
|
||
// 1. User is logged in
|
||
// 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 snoozed = sessionStorage.getItem(BACKUP_SNOOZE_KEY) === 'true';
|
||
const shouldShow = !snoozed && !confirmed;
|
||
setShowReminder(shouldShow);
|
||
}, 2000);
|
||
|
||
return () => 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={setShowReminder}>
|
||
<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>
|
||
<Button
|
||
variant="ghost"
|
||
size="icon"
|
||
onClick={handleDismiss}
|
||
className="ml-2 -mt-1 text-gray-500 hover:text-gray-300"
|
||
>
|
||
<X className="h-5 w-5" />
|
||
</Button>
|
||
</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>
|
||
);
|
||
}
|