feat(web): toast "Nouvelle version" persistant + lien vers le changelog
All checks were successful
Build & Deploy Web / build-and-deploy (push) Successful in 35s
Build & Deploy Landing / build-and-deploy (push) Successful in 1m6s

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:
ordinarthur 2026-05-11 00:06:06 +02:00
parent fc0d13e955
commit 847e7a3fc5
3 changed files with 90 additions and 0 deletions

View 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 é 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;
}

View File

@ -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
View 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";