fix(mobile): lance la vérification de mise à jour à chaque ouverture (#106)
All checks were successful
CI / lint (push) Successful in 3m9s
CI / build (push) Successful in 3m50s
CI / intent-service-test (push) Successful in 7m32s
CI / e2e (push) Successful in 10m13s
CI / test (push) Successful in 33m6s
CI / build-android (push) Has been skipped

`useAppUpdateCheck` tournait au montage de `<UpdateBanner />`, mais celui-ci
n'était monté que dans `AppLayout`, derrière `RequireAuth` : un utilisateur
non connecté sur l'app native voit `LandingPage`, `AppLayout` n'est jamais
monté, donc aucune vérification tant qu'on n'est pas connecté — alors que
le back suppose l'inverse (`mobile.routes.ts` : « runs on every app start,
session or not »).

- `<UpdateBanner />` monté dans `App.tsx`, hors `<Routes>`, donc rendu pour
  toute route quel que soit l'état d'auth. Garde `!isNativePlatform()` et
  throttle 6 h inchangés ; rend toujours `null` sur le web.
- `update-banner.scss` : passe en `position: fixed` (bandeau haut,
  `env(safe-area-inset-top)`, `z-index: 100`) pour ne pas entrer dans la
  mise en page / le calcul de hauteur d'une page routée (`.app-layout` est
  `height: 100vh; overflow: hidden`).

Pas de test e2e : `Capacitor.isNativePlatform()` n'a pas de couture pour
simuler le natif depuis un run web (posture déjà assumée pour
`useAppUpdateCheck` / `UpdateBanner`, voir `app-settings-page.cy.ts`). Suite
e2e existante (layout / smoke / app-settings) revérifiée : aucun régression.

Closes #106

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Nicolas 2026-09-08 01:40:35 +02:00
parent 99d7badddf
commit 3df24fe71c
3 changed files with 124 additions and 105 deletions

View file

@ -1,6 +1,7 @@
import { Navigate, Outlet, Route, Routes } from "react-router-dom";
import { AdminAuthProvider } from "./features/admin/AdminAuthContext";
import { RequireAdmin } from "./features/admin/RequireAdmin";
import { UpdateBanner } from "./features/app-update/UpdateBanner";
import { RedirectIfAuthenticated } from "./features/auth/RedirectIfAuthenticated";
import { RequireAuth } from "./features/auth/RequireAuth";
import { AdminLayout } from "./layouts/AdminLayout";
@ -71,111 +72,120 @@ import { ShoppingListPage } from "./pages/shopping-list/ShoppingListPage";
* never nested under `RequireAuth`. `/admin/login` is the only
* unauthenticated admin route; everything else sits under `RequireAdmin` +
* `AdminLayout`.
*
* {@link UpdateBanner} is mounted here, outside `<Routes>`, rather than in
* `AppLayout` — the native app-update check (issue #66) must run on every
* app start, session or not, and `AppLayout` only mounts once a visitor is
* past `RequireAuth`. It's a `position: fixed` strip and renders nothing on
* the web, so mounting it app-wide costs the routed pages no layout.
*/
export function App() {
return (
<Routes>
<Route
element={
<RequireAuth>
<AppLayout />
</RequireAuth>
}
>
<Route path="/" element={<PlanningPage />} />
{/* Same component for both — a master-detail layout, not a
<>
<UpdateBanner />
<Routes>
<Route
element={
<RequireAuth>
<AppLayout />
</RequireAuth>
}
>
<Route path="/" element={<PlanningPage />} />
{/* Same component for both — a master-detail layout, not a
navigation to a separate page: the tab bar + table stay
mounted, only RecipesPage's detail panel changes with `:id`
(see RecipesPage.tsx). */}
<Route path="/recettes" element={<RecipesPage />} />
<Route path="/recettes/nouvelle" element={<RecipeFormPage />} />
<Route path="/recettes/importer/:sourceKey/:externalId" element={<ImportRecipePage />} />
<Route path="/recettes/sources/:sourceKey/:externalId" element={<RecipesPage />} />
<Route path="/recettes/:id" element={<RecipesPage />} />
<Route path="/recettes/:id/modifier" element={<RecipeFormPage />} />
<Route path="/liste-de-courses" element={<ShoppingListPage />} />
<Route path="/cuisiner" element={<CookingSessionPage />} />
<Route path="/parametres/compte" element={<AccountSettingsPage />} />
<Route path="/parametres/preferences" element={<PreferencesPage />} />
<Route path="/parametres/foyer" element={<HouseholdSettingsPage />} />
<Route path="/parametres/preferences-utilisateur" element={<UserPreferencesPage />} />
<Route path="/parametres/application" element={<AppSettingsPage />} />
<Route path="/parametres/credits" element={<CreditsPage />} />
<Route path="/foyer" element={<Navigate to="/parametres/foyer" replace />} />
</Route>
<Route
path="/onboarding/regime"
element={
<RequireAuth>
<OnboardingDietPage />
</RequireAuth>
}
/>
<Route
path="/onboarding/foyer"
element={
<RequireAuth>
<OnboardingHouseholdPage />
</RequireAuth>
}
/>
<Route
path="/onboarding/sources"
element={
<RequireAuth>
<OnboardingSourcesPage />
</RequireAuth>
}
/>
<Route
path="/onboarding/allergenes"
element={
<RequireAuth>
<OnboardingAllergensPage />
</RequireAuth>
}
/>
<Route
path="/login"
element={
<RedirectIfAuthenticated>
<LoginPage />
</RedirectIfAuthenticated>
}
/>
<Route
path="/signup"
element={
<RedirectIfAuthenticated>
<SignupPage />
</RedirectIfAuthenticated>
}
/>
<Route path="/telecharger" element={<DownloadPage />} />
<Route
element={
<AdminAuthProvider>
<Outlet />
</AdminAuthProvider>
}
>
<Route path="/admin/login" element={<AdminLoginPage />} />
<Route path="/recettes" element={<RecipesPage />} />
<Route path="/recettes/nouvelle" element={<RecipeFormPage />} />
<Route path="/recettes/importer/:sourceKey/:externalId" element={<ImportRecipePage />} />
<Route path="/recettes/sources/:sourceKey/:externalId" element={<RecipesPage />} />
<Route path="/recettes/:id" element={<RecipesPage />} />
<Route path="/recettes/:id/modifier" element={<RecipeFormPage />} />
<Route path="/liste-de-courses" element={<ShoppingListPage />} />
<Route path="/cuisiner" element={<CookingSessionPage />} />
<Route path="/parametres/compte" element={<AccountSettingsPage />} />
<Route path="/parametres/preferences" element={<PreferencesPage />} />
<Route path="/parametres/foyer" element={<HouseholdSettingsPage />} />
<Route path="/parametres/preferences-utilisateur" element={<UserPreferencesPage />} />
<Route path="/parametres/application" element={<AppSettingsPage />} />
<Route path="/parametres/credits" element={<CreditsPage />} />
<Route path="/foyer" element={<Navigate to="/parametres/foyer" replace />} />
</Route>
<Route
path="/admin"
path="/onboarding/regime"
element={
<RequireAdmin>
<AdminLayout />
</RequireAdmin>
<RequireAuth>
<OnboardingDietPage />
</RequireAuth>
}
/>
<Route
path="/onboarding/foyer"
element={
<RequireAuth>
<OnboardingHouseholdPage />
</RequireAuth>
}
/>
<Route
path="/onboarding/sources"
element={
<RequireAuth>
<OnboardingSourcesPage />
</RequireAuth>
}
/>
<Route
path="/onboarding/allergenes"
element={
<RequireAuth>
<OnboardingAllergensPage />
</RequireAuth>
}
/>
<Route
path="/login"
element={
<RedirectIfAuthenticated>
<LoginPage />
</RedirectIfAuthenticated>
}
/>
<Route
path="/signup"
element={
<RedirectIfAuthenticated>
<SignupPage />
</RedirectIfAuthenticated>
}
/>
<Route path="/telecharger" element={<DownloadPage />} />
<Route
element={
<AdminAuthProvider>
<Outlet />
</AdminAuthProvider>
}
>
<Route index element={<AdminDashboardPage />} />
<Route path="monitoring" element={<AdminMonitoringPage />} />
<Route path="corrections" element={<AdminCorrectionsPage />} />
<Route path="catalogue" element={<AdminCatalogPage />} />
<Route path="didacticiels" element={<AdminToursPage />} />
<Route path="/admin/login" element={<AdminLoginPage />} />
<Route
path="/admin"
element={
<RequireAdmin>
<AdminLayout />
</RequireAdmin>
}
>
<Route index element={<AdminDashboardPage />} />
<Route path="monitoring" element={<AdminMonitoringPage />} />
<Route path="corrections" element={<AdminCorrectionsPage />} />
<Route path="catalogue" element={<AdminCatalogPage />} />
<Route path="didacticiels" element={<AdminToursPage />} />
</Route>
</Route>
</Route>
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</>
);
}

View file

@ -1,18 +1,28 @@
// Styles for UpdateBanner — a discreet, dismissible strip above the routed
// page content (see AppLayout.tsx). Uses --color-warning the same way
// source-item-preview__hint does elsewhere in the app: "worth noticing",
// Styles for UpdateBanner — a discreet, dismissible strip pinned to the top
// of the screen. Mounted app-wide in App.tsx (outside `<Routes>`) so the
// native update check runs session or not, hence `position: fixed` rather
// than the old in-flow placement inside `.app-content`: this way it never
// enters any routed page's layout / height math (`.app-layout` is
// `height: 100vh; overflow: hidden`), it just overlays. Native-only —
// renders nothing on the web (see UpdateBanner.tsx). Uses --color-warning
// the same way source-item-preview__hint does elsewhere: "worth noticing",
// not an error.
.update-banner {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: var(--space-sm);
margin-bottom: var(--space-md);
padding: var(--space-sm) var(--space-md);
// Clears the Android status bar when the WebView draws under it.
padding-top: calc(var(--space-sm) + env(safe-area-inset-top, 0px));
background: color-mix(in srgb, var(--color-warning) 12%, var(--color-surface));
border: 1px solid var(--color-warning);
border-radius: var(--radius-base);
border-bottom: 1px solid var(--color-warning);
font-size: var(--font-size-sm);
&__text {

View file

@ -2,7 +2,6 @@ import { useState } from "react";
import { useTranslation } from "react-i18next";
import { NavLink, Outlet, useLocation, useNavigate } from "react-router-dom";
import { BottomSheet } from "../components/ui/BottomSheet";
import { UpdateBanner } from "../features/app-update/UpdateBanner";
import { useAuth } from "../features/auth/AuthContext";
import { useIsMobileViewport } from "../lib/useIsMobileViewport";
import "./AppLayout.scss";
@ -148,9 +147,9 @@ export function AppLayout() {
)}
<main className={isMobile ? "app-content app-content--with-bottom-nav" : "app-content"}>
{/* Renders nothing on the web build (native-only check) or once no
newer version is known — see UpdateBanner's own doc comment. */}
<UpdateBanner />
{/* `UpdateBanner` used to sit here, but the native update check
(issue #66) must run session or not — it's mounted app-wide in
`App.tsx` now, as a `position: fixed` strip. */}
<Outlet />
</main>