Fix/dismissible infinite toasts (#1319)

* fix: make PWA install and OpenCode update toasts dismissible

Both 'Install OpenChamber' and 'OpenCode update available' toasts use
duration: Infinity with no close affordance, so they persist on screen
until the user accepts (install/update) or reloads the tab. For users
who do not want to install the PWA or upgrade right now, this is
intrusive and there is no opt-out.

Add a Dismiss button (sonner cancel action) to both toasts. When the
user dismisses:

- PWA: persist a flag in localStorage so the prompt does not reappear
  on future sessions. Accepting Install still works as before.
- OpenCode update: persist the dismissed version in localStorage. The
  toast will appear again only when a newer version becomes available.

Adds new i18n keys pwa.installPrompt.dismiss and
opencodeUpdate.toast.actions.dismiss across all seven locales (en,
es, ko, pl, pt-BR, uk, zh-CN).

* test: extract toast dedup helpers and cover with 28 unit tests

Lift the dismissal-decision logic out of usePwaInstallPrompt and
OpenCodeUpdateToast into a React-free sibling module so it can be
unit-tested directly. The React surfaces remain sole owners of side
effects (storage writes, toast.info, event listeners); the new module
only answers 'should we show?'.

New module openCodeUpdateDedup.ts exposes four helpers:
- shouldShowPwaInstallToast(input) - three gates: dismissed,
  sessionShown, hasActiveToast.
- shouldShowOpenCodeUpdateToast(input) - empty version, seen set,
  dismissed===version gates; a different dismissed version lets the
  toast resurface for the new release.
- resolveOpenCodeUpdateVersion(detail) - parses CustomEvent payloads
  defensively (null/non-object/non-string -> '').
- resolveOpenCodeUpgradeStatusVersion(status) - parses upgrade status
  payloads (status falsy / available!==true / latestVersion non-string
  -> '').

Consumers now call the helpers and only run the side-effect when the
decision is true. Behaviour is unchanged.

Coverage: 28 tests via bun:test, 33 expects, all pass first try.
This commit is contained in:
Roberto Bertó
2026-05-19 16:57:23 +03:00
committed by GitHub
parent bb87111c49
commit 46bef9b0c7
11 changed files with 395 additions and 22 deletions
+17 -6
View File
@@ -3,7 +3,8 @@ import { toast } from '@/components/ui';
import { isWebRuntime } from '@/lib/desktop';
import { usePwaDetection } from '@/hooks/usePwaDetection';
import { useI18n } from '@/lib/i18n';
import { getSafeSessionStorage } from '@/stores/utils/safeStorage';
import { getSafeSessionStorage, getSafeStorage } from '@/stores/utils/safeStorage';
import { shouldShowPwaInstallToast } from '@/components/update/openCodeUpdateDedup';
type InstallPromptOutcome = 'accepted' | 'dismissed';
@@ -13,6 +14,7 @@ type BeforeInstallPromptEvent = Event & {
};
const INSTALL_TOAST_SESSION_KEY = 'pwa-install-toast-shown';
const INSTALL_TOAST_DISMISSED_KEY = 'pwa-install-toast-dismissed';
export const usePwaInstallPrompt = () => {
const { browserTab } = usePwaDetection();
@@ -64,12 +66,14 @@ export const usePwaInstallPrompt = () => {
installEvent.preventDefault();
deferredPrompt = installEvent;
const localStorage = getSafeStorage();
const sessionStorage = getSafeSessionStorage();
if (sessionStorage.getItem(INSTALL_TOAST_SESSION_KEY) === 'true') {
return;
}
if (installToastId !== null) {
const decision = shouldShowPwaInstallToast({
dismissed: localStorage.getItem(INSTALL_TOAST_DISMISSED_KEY),
sessionShown: sessionStorage.getItem(INSTALL_TOAST_SESSION_KEY),
hasActiveToast: installToastId !== null,
});
if (!decision) {
return;
}
@@ -83,6 +87,13 @@ export const usePwaInstallPrompt = () => {
void triggerInstall();
},
},
cancel: {
label: tRef.current('pwa.installPrompt.dismiss'),
onClick: () => {
getSafeStorage().setItem(INSTALL_TOAST_DISMISSED_KEY, 'true');
dismissInstallToast();
},
},
});
};