batchCooking/apps/web/src/App.tsx
Nicolas d6cf4aa63d
Some checks failed
CI / lint (push) Successful in 3m35s
CI / build (push) Successful in 4m33s
CI / intent-service-test (push) Successful in 4m43s
CI / e2e (push) Successful in 15m5s
CI / test (push) Failing after 15m28s
CI / build-android (push) Has been skipped
feat(mobile): redirection vers l'appli + page de telechargement
Ticket #84 du chantier UI mobile & tablette. Aucune infra de deep-link
n'existait (verifie en amont : pas de scheme personnalise, pas d'App
Links, pas d'assetlinks.json, pas de listener appUrlOpen).

- lib/mobile-app-redirect.ts : attemptOpenNativeApp() (pattern "smart
  app banner" classique - tente le scheme batchcooking://, bascule sur
  onFallback si la page est toujours visible passe un court delai) et
  isAndroidPhoneBrowser() (UA Android+Mobile, deliberement distinct de
  useIsMobileViewport() - une question de plateforme, pas de largeur).
  Respecte une preference "rester sur le web" (localStorage, cle
  exportee pour que l'onglet Parametres d'application - ticket #86 -
  la reutilise) sans jamais forcer la redirection.
- AndroidManifest.xml : nouvel intent-filter additif (scheme
  batchcooking, sans toucher au filtre MAIN/LAUNCHER existant).
- Nouvelle page /telecharger (publique, hors RequireAuth - un visiteur
  deja connecte sur le web mais sans l'appli sur cet appareil a une
  raison legitime d'y atterrir) : lien direct vers le proxy existant
  ${API_BASE_URL}/mobile/download (rien a changer cote API), bouton
  "reessayer d'ouvrir l'appli".

Deliberement pas de changement sur la landing page ici (lien discret
actuel laisse tel quel) - c'est le ticket #85, separe pour rester
petit/revuable.

Verifie en local : tsc -b --force, biome check, build de prod, suite
Cypress e2e complete (141/141 sur cette branche - #91 n'est pas encore
merge, layout.cy.ts y est donc encore dans son etat pre-#91) et
composants (69/69, incluant le nouveau mobile-app-redirect.cy.tsx).
Note technique : le premier essai de test du composant tentait un vrai
window.location.href vers le scheme personnalise depuis Cypress/
Electron, ce qui bloquait le runner indefiniment (protocole inconnu
mal gere par Electron) - corrige en rendant ce seul appel injectable
(openAppUrl, reserve aux tests) sans toucher au comportement reel.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-07 12:20:16 +02:00

178 lines
8 KiB
TypeScript

import { Navigate, Outlet, Route, Routes } from "react-router-dom";
import { AdminAuthProvider } from "./features/admin/AdminAuthContext";
import { RequireAdmin } from "./features/admin/RequireAdmin";
import { RedirectIfAuthenticated } from "./features/auth/RedirectIfAuthenticated";
import { RequireAuth } from "./features/auth/RequireAuth";
import { AdminLayout } from "./layouts/AdminLayout";
import { AppLayout } from "./layouts/AppLayout";
import { CatalogPage as AdminCatalogPage } from "./pages/admin/catalog/CatalogPage";
import { CorrectionsPage as AdminCorrectionsPage } from "./pages/admin/corrections/CorrectionsPage";
import { DashboardPage as AdminDashboardPage } from "./pages/admin/dashboard/DashboardPage";
import { AdminLoginPage } from "./pages/admin/login/AdminLoginPage";
import { MonitoringPage as AdminMonitoringPage } from "./pages/admin/monitoring/MonitoringPage";
import { ToursPage as AdminToursPage } from "./pages/admin/tours/ToursPage";
import { LoginPage } from "./pages/auth/LoginPage";
import { SignupPage } from "./pages/auth/SignupPage";
import { CookingSessionPage } from "./pages/cooking-session/CookingSessionPage";
import { DownloadPage } from "./pages/download/DownloadPage";
import { OnboardingAllergensPage } from "./pages/onboarding/OnboardingAllergensPage";
import { OnboardingDietPage } from "./pages/onboarding/OnboardingDietPage";
import { OnboardingHouseholdPage } from "./pages/onboarding/OnboardingHouseholdPage";
import { OnboardingSourcesPage } from "./pages/onboarding/OnboardingSourcesPage";
import { PlanningPage } from "./pages/planning/PlanningPage";
import { ImportRecipePage } from "./pages/recipes/ImportRecipePage";
import { RecipeFormPage } from "./pages/recipes/RecipeFormPage";
import { RecipesPage } from "./pages/recipes/RecipesPage";
import { AccountSettingsPage } from "./pages/settings/AccountSettingsPage";
import { CreditsPage } from "./pages/settings/CreditsPage";
import { HouseholdSettingsPage } from "./pages/settings/HouseholdSettingsPage";
import { PreferencesPage } from "./pages/settings/PreferencesPage";
import { UserPreferencesPage } from "./pages/settings/UserPreferencesPage";
import { ShoppingListPage } from "./pages/shopping-list/ShoppingListPage";
/**
* Top-level route table. Every authenticated section is nested under one
* `RequireAuth` + `AppLayout` parent route (sidebar chrome + `/auth`
* guard applied once, not per-page — see {@link AppLayout}); `/login` and
* `/signup` redirect an already-logged-in visitor to `/` instead (see
* {@link RedirectIfAuthenticated}). Anything else falls back to `/`, which
* itself redirects to `/login` if needed.
*
* `/parametres/*` (compte/préférences alimentaires/foyer/préférences
* utilisateur/crédits) are the settings pages, reachable from the sidebar's
* bottom "Paramètres" menu and the account menu (see `AppLayout`) — nested under
* `AppLayout` like every other authenticated section. `/foyer` is the old,
* pre-split combined page's path; it now just redirects to
* `/parametres/foyer` so an existing bookmark/link keeps working.
*
* `/onboarding/*` (regime/foyer/sources/allergens, in that order) is also
* `RequireAuth`-gated — reached right after signup, once a session already
* exists — but deliberately its own top-level route group, *not* nested
* under `AppLayout`: a focused, distraction-free wizard with no sidebar,
* same full-page-card language as `/login`/`/signup` (see `onboarding.scss`).
* `/onboarding/sources` is conditional — only reached when the `foyer` step
* created/joined a household (see `OnboardingHouseholdPage`'s `goToNextStep`);
* skipped otherwise, straight to `/onboarding/allergenes`.
*
* `/telecharger` is the mobile-app-redirect chantier's fallback landing spot
* (see `lib/mobile-app-redirect.ts`'s own doc comment) — public like
* `/login`/`/signup`, but deliberately *not* wrapped in
* `RedirectIfAuthenticated`: a signed-in visitor still has a legitimate
* reason to land here (logged in on the web, app just not installed on
* this particular device yet).
*
* `/admin/*` is the internal admin surface (metrics, monitoring, correction
* triage, off-catalog ingredient review). It's its own top-level group,
* wrapped in {@link AdminAuthProvider} so the `GET /admin/api/auth/me` session
* probe only runs under `/admin` — the admin session (cookie `admin_session`,
* `ADMIN_JWT_SECRET`) is entirely separate from the end-user one, so this is
* never nested under `RequireAuth`. `/admin/login` is the only
* unauthenticated admin route; everything else sits under `RequireAdmin` +
* `AdminLayout`.
*/
export function App() {
return (
<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/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="/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 path="*" element={<Navigate to="/" replace />} />
</Routes>
);
}