feat: dismissable banner with session-storage memory (#81)
All checks were successful
PR Check / rust (push) Successful in 22m28s
PR Check / frontend (push) Successful in 2m17s
PR Check / rust (pull_request) Successful in 22m30s
PR Check / frontend (pull_request) Successful in 2m18s

Adds a close button and session-scoped dismissal flag so the banner
can be acknowledged for the current run but reappears on the next
app launch if the fallback is still active — matches the #81
acceptance criterion.

- sessionStorage key survives page navigation within the run, is
  cleared on app restart.
- Graceful on storage quota errors.
- New `common.close` i18n key (FR: "Fermer", EN: "Close") used as
  the aria-label of the close button.
This commit is contained in:
le king fu 2026-04-14 08:20:20 -04:00
parent 3b1c41c48e
commit 9a9d3c89b9
3 changed files with 37 additions and 5 deletions

View file

@ -1,11 +1,23 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { ShieldAlert } from "lucide-react"; import { ShieldAlert, X } from "lucide-react";
import { getTokenStoreMode, TokenStoreMode } from "../../services/authService"; import { getTokenStoreMode, TokenStoreMode } from "../../services/authService";
// Per-session dismissal flag. Kept in sessionStorage so the banner
// returns on the next app launch if the fallback condition still
// holds — this matches the acceptance criteria from issue #81.
const DISMISS_KEY = "tokenStoreFallbackBannerDismissed";
export default function TokenStoreFallbackBanner() { export default function TokenStoreFallbackBanner() {
const { t } = useTranslation(); const { t } = useTranslation();
const [mode, setMode] = useState<TokenStoreMode | null>(null); const [mode, setMode] = useState<TokenStoreMode | null>(null);
const [dismissed, setDismissed] = useState<boolean>(() => {
try {
return sessionStorage.getItem(DISMISS_KEY) === "1";
} catch {
return false;
}
});
useEffect(() => { useEffect(() => {
let cancelled = false; let cancelled = false;
@ -21,12 +33,22 @@ export default function TokenStoreFallbackBanner() {
}; };
}, []); }, []);
if (mode !== "file") return null; if (mode !== "file" || dismissed) return null;
const dismiss = () => {
try {
sessionStorage.setItem(DISMISS_KEY, "1");
} catch {
// Ignore storage errors — the banner will simply hide for the
// remainder of this render cycle via state.
}
setDismissed(true);
};
return ( return (
<div className="flex items-start gap-3 rounded-xl border border-amber-500/40 bg-amber-500/10 p-4"> <div className="flex items-start gap-3 rounded-xl border border-amber-500/40 bg-amber-500/10 p-4">
<ShieldAlert size={20} className="mt-0.5 shrink-0 text-amber-500" /> <ShieldAlert size={20} className="mt-0.5 shrink-0 text-amber-500" />
<div className="space-y-1 text-sm"> <div className="flex-1 space-y-1 text-sm">
<p className="font-semibold text-[var(--foreground)]"> <p className="font-semibold text-[var(--foreground)]">
{t("account.tokenStore.fallback.title")} {t("account.tokenStore.fallback.title")}
</p> </p>
@ -34,6 +56,14 @@ export default function TokenStoreFallbackBanner() {
{t("account.tokenStore.fallback.description")} {t("account.tokenStore.fallback.description")}
</p> </p>
</div> </div>
<button
type="button"
onClick={dismiss}
aria-label={t("common.close")}
className="shrink-0 text-[var(--muted-foreground)] hover:text-[var(--foreground)] transition-colors"
>
<X size={16} />
</button>
</div> </div>
); );
} }

View file

@ -847,7 +847,8 @@
"language": "Language", "language": "Language",
"total": "Total", "total": "Total",
"darkMode": "Dark mode", "darkMode": "Dark mode",
"lightMode": "Light mode" "lightMode": "Light mode",
"close": "Close"
}, },
"license": { "license": {
"title": "License", "title": "License",

View file

@ -847,7 +847,8 @@
"language": "Langue", "language": "Langue",
"total": "Total", "total": "Total",
"darkMode": "Mode sombre", "darkMode": "Mode sombre",
"lightMode": "Mode clair" "lightMode": "Mode clair",
"close": "Fermer"
}, },
"license": { "license": {
"title": "Licence", "title": "Licence",