feat(web): toast "Nouvelle version" persistant + lien vers le changelog
Composant `<VersionToast/>` monté au root de la SPA qui s'affiche quand l'utilisateur ouvre l'app sur une version plus récente que la dernière qu'il a vue. Mécanique : - Source de vérité = `apps/web/src/version.ts` (constante semver, à bumper manuellement à chaque release, en même temps que l'ajout du .md correspondant dans `apps/landing/src/content/changelog/`). - Comparaison à `localStorage["rubis:last-seen-version"]` au mount. - 1re visite (clé absente) → on enregistre la version courante en silence, pas de toast (sinon spam d'onboarding). - Version identique à la dernière vue → rien. - Version différente → toast Sonner persistant (`duration: Infinity`) avec icône Sparkles rubis et action "Voir les nouveautés ↗" qui ouvre `rubis.pro/changelog#<version>` dans un nouvel onglet. Au moment de l'affichage on enregistre la version comme vue — donc même si l'user ferme sans cliquer, plus de toast pour cette version (il a été informé). - localStorage indisponible (private mode) → fail silent. Version initiale : `1.10.0` (remerciement automatique au client, dernière entrée du changelog). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
fc0d13e955
commit
847e7a3fc5
70
apps/web/src/components/version-toast.tsx
Normal file
70
apps/web/src/components/version-toast.tsx
Normal file
@ -0,0 +1,70 @@
|
||||
import { useEffect } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Sparkles } from "lucide-react";
|
||||
|
||||
import { APP_VERSION, CHANGELOG_URL } from "../version";
|
||||
|
||||
const STORAGE_KEY = "rubis:last-seen-version";
|
||||
|
||||
/**
|
||||
* Affiche un toast persistant quand l'utilisateur ouvre l'app sur une version
|
||||
* plus récente que la dernière qu'il a vue (lu/comparé via `localStorage`).
|
||||
*
|
||||
* Comportement :
|
||||
* - 1re visite (pas de clé en localStorage) → on enregistre la version
|
||||
* courante en silence, pas de toast (sinon spam d'onboarding).
|
||||
* - Visite suivante, version identique → rien.
|
||||
* - Visite suivante, version différente → toast persistant (duration: Infinity)
|
||||
* avec action "Voir les nouveautés" qui ouvre `rubis.pro/changelog#<version>`
|
||||
* dans un nouvel onglet. Au moment de l'affichage on enregistre la version
|
||||
* comme vue — donc même si l'user ferme le toast sans cliquer, il ne le
|
||||
* reverra pas (il a été informé).
|
||||
*
|
||||
* localStorage indisponible (private mode, restrictions) → fail silent, pas
|
||||
* de toast et pas de crash.
|
||||
*/
|
||||
export function VersionToast() {
|
||||
useEffect(() => {
|
||||
let seen: string | null = null;
|
||||
try {
|
||||
seen = localStorage.getItem(STORAGE_KEY);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
if (seen === null) {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, APP_VERSION);
|
||||
} catch {
|
||||
// Tant pis — on ne montrera pas de toast non plus.
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (seen === APP_VERSION) return;
|
||||
|
||||
toast.message(`Nouvelle version v${APP_VERSION}`, {
|
||||
description: "Découvrez ce qui change.",
|
||||
duration: Infinity,
|
||||
icon: <Sparkles className="size-4 text-rubis" aria-hidden />,
|
||||
action: {
|
||||
label: "Voir les nouveautés ↗",
|
||||
onClick: () => {
|
||||
window.open(
|
||||
`${CHANGELOG_URL}#${APP_VERSION}`,
|
||||
"_blank",
|
||||
"noopener,noreferrer",
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, APP_VERSION);
|
||||
} catch {
|
||||
// Pareil — on a quand même informé l'user via le toast, c'est l'essentiel.
|
||||
}
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
}
|
||||
@ -3,6 +3,8 @@ import type { QueryClient } from "@tanstack/react-query";
|
||||
import { Toaster } from "sonner";
|
||||
import { lazy, Suspense } from "react";
|
||||
|
||||
import { VersionToast } from "../components/version-toast";
|
||||
|
||||
/**
|
||||
* Devtools — chargés uniquement en dev pour ne pas alourdir le bundle prod.
|
||||
* Cf. /docs/tech/frontend.md §4.
|
||||
@ -47,6 +49,7 @@ function RootLayout() {
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<VersionToast />
|
||||
{import.meta.env.DEV && (
|
||||
<Suspense fallback={null}>
|
||||
<TanStackRouterDevtools position="bottom-left" />
|
||||
|
||||
17
apps/web/src/version.ts
Normal file
17
apps/web/src/version.ts
Normal file
@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Version courante de l'app SPA, source de vérité pour :
|
||||
* - le toast "Nouvelle version" (<VersionToast/>) qui compare cette valeur à
|
||||
* `localStorage["rubis:last-seen-version"]` au mount du root
|
||||
* - le release tag Sentry (à brancher si pas déjà fait)
|
||||
*
|
||||
* À bumper manuellement à chaque release, EN MÊME TEMPS qu'on ajoute le .md
|
||||
* correspondant dans `apps/landing/src/content/changelog/<version>.md`. La
|
||||
* même PR contient les deux — sinon le toast pointe sur une ancre absente.
|
||||
*
|
||||
* Convention : semver (major.minor.patch). Le toast affiche `v${APP_VERSION}`
|
||||
* et linke vers `https://rubis.pro/changelog#${APP_VERSION}`.
|
||||
*/
|
||||
export const APP_VERSION = "1.10.0";
|
||||
|
||||
/** URL absolue de la page changelog (utilisée par le toast). */
|
||||
export const CHANGELOG_URL = "https://rubis.pro/changelog";
|
||||
Loading…
x
Reference in New Issue
Block a user