feat(onboarding): réordonne le wizard en compte → préférences → foyer → sources (#129) #139

Merged
kyuno merged 1 commit from feat/onboarding-wizard-order into main 2026-09-09 16:59:50 +02:00
10 changed files with 205 additions and 129 deletions
Showing only changes of commit 2a3fb0434a - Show all commits

View file

@ -1,41 +1,54 @@
Feature: Onboarding wizard
As a newly signed-up user
I want to set my diet, household, and allergens
I want to set my preferences (diet + allergens), then my household
So that my profile is ready before I start planning meals
# Order since #129: compte -> régime -> allergènes -> foyer -> sources,
# and the sources step only when the foyer step *created* a household
# (joining one or skipping ends the wizard at the home page).
Background:
Given the planning request returns nothing
Scenario: Walks through the wizard, creating a household on the way (which surfaces the sources step), and lands on the home page
Scenario: Walks through every step, creating a household (which surfaces the sources step), and lands on the home page
Given the diets reference list has options
And selecting the diet will succeed
And the household request returns no household
And creating a household will succeed
And the sources reference list is empty
And the allergies reference list has options
And updating allergies will succeed
And creating a household will succeed
And the sources reference list has options
And the household's enabled sources are empty
And saving the source selection will succeed
And I have signed up
Then the URL should include "/onboarding/regime"
And I should see "Étape 1 sur 3"
When I select "Végétarien" from the "diet" field
And I click the button "Continuer"
Then the diet update request should have been made with diet id 2
And the URL should include "/onboarding/foyer"
And I should see "Étape 2 sur 3"
When I fill in the "houseName" field with "Chez Alice"
And I click the button "Créer"
Then the household creation request should have been made with name "Chez Alice"
And the URL should include "/onboarding/allergenes"
And I should see "Étape 4 sur 4"
And I should see "Étape 2 sur 3"
And I should see the section "Allergies"
And I should see the section "Intolérances"
When I check the checkbox "Arachides"
And I click the button "Terminer"
And I click the button "Continuer"
Then the allergies update request should have been made with allergy id 1
And the URL should include "/onboarding/foyer"
And I should see "Étape 3 sur 3"
When I fill in the "houseName" field with "Chez Alice"
And I click the button "Créer"
Then the household creation request should have been made with name "Chez Alice"
And the URL should include "/onboarding/sources"
And I should see "Étape 4 sur 4"
And I should see the section "Sources disponibles"
And I should see "TheMealDB"
When I check the checkbox "TheMealDB"
And I click the button "Terminer"
Then the source selection update request should have been made with source id 1
And the URL should be the home page
And I should see the heading "Planning de la semaine"
Scenario: Lets the regime and allergens steps be skipped without changing anything
Scenario: Lets the preferences steps be skipped without changing anything, then skips the household step too
Given the diets reference list is empty
And selecting the diet will succeed
And the household request returns no household
@ -45,39 +58,32 @@ Feature: Onboarding wizard
Then the URL should include "/onboarding/regime"
When I click the button "Continuer"
Then the diet update request should have been made with no diet id
And the URL should include "/onboarding/allergenes"
When I click the button "Continuer"
Then the allergies update request should have been made with no allergy ids
And the URL should include "/onboarding/foyer"
When I click the button "Passer cette étape"
Then the URL should include "/onboarding/allergenes"
When I click the button "Terminer"
Then the allergies update request should have been made with no allergy ids
And the URL should be the home page
Then the URL should be the home page
Scenario: Shows and saves the sources step instead of skipping it, when sources are available
Scenario: Creating a household with no sources implemented ends the wizard through the silently-skipped sources step
Given the diets reference list is empty
And selecting the diet will succeed
And the household request returns no household
And creating a household will succeed
And the sources reference list has options
And the household's enabled sources are empty
And saving the source selection will succeed
And the sources reference list is empty
And the allergies reference list is empty
And updating allergies will succeed
And I have signed up
When I click the button "Continuer"
Then the URL should include "/onboarding/allergenes"
When I click the button "Continuer"
Then the URL should include "/onboarding/foyer"
When I fill in the "houseName" field with "Chez Alice"
And I click the button "Créer"
Then the household creation request should have been made with name "Chez Alice"
And the URL should include "/onboarding/sources"
And I should see "Étape 3 sur 4"
And I should see the section "Sources disponibles"
And I should see "TheMealDB"
When I check the checkbox "TheMealDB"
And I click the button "Continuer"
Then the source selection update request should have been made with source id 1
And the URL should include "/onboarding/allergenes"
And the URL should be the home page
Scenario: Lets the household step be completed by joining an existing household instead of creating one
Scenario: Joining an existing household skips the sources step entirely and ends the wizard
Given the diets reference list is empty
And selecting the diet will succeed
And the household request returns no household
@ -87,8 +93,10 @@ Feature: Onboarding wizard
And updating allergies will succeed
And I have signed up
When I click the button "Continuer"
Then the URL should include "/onboarding/allergenes"
When I click the button "Continuer"
Then the URL should include "/onboarding/foyer"
When I fill in the "inviteCode" field with "abcd2345"
And I click the button "Rejoindre"
Then the household join request should have been made with invite code "ABCD2345"
And the URL should include "/onboarding/allergenes"
And the URL should be the home page

View file

@ -32,10 +32,10 @@ Given("the allergies reference list is empty", () => {
cy.intercept("GET", "**/reference/allergies", { statusCode: 200, body: [] });
});
// Every onboarding scenario that reaches the household step also reaches
// `/onboarding/sources` right after (when a household got created/joined —
// see `OnboardingHouseholdPage`'s `goToNextStep`), which reads this before
// self-skipping to `/onboarding/allergenes` if it comes back empty.
// An onboarding scenario whose household step *creates* a household then
// reaches `/onboarding/sources` (see `OnboardingHouseholdPage`'s
// `goToNextStep`), which reads this before self-skipping straight to the
// home page if it comes back empty.
Given("the sources reference list is empty", () => {
cy.intercept("GET", "**/reference/sources", { statusCode: 200, body: [] });
});

View file

@ -51,14 +51,16 @@ import { ShoppingListPage } from "./pages/shopping-list/ShoppingListPage";
* just redirects to `/parametres/foyer` so an existing bookmark/link keeps
* working.
*
* `/onboarding/*` (regime/foyer/sources/allergens, in that order) is also
* `/onboarding/*` (regime/allergenes/foyer/sources, 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`.
* `regime` + `allergenes` are the two "préférences" screens; `/onboarding/sources`
* is conditional — only reached when the `foyer` step *created* a household
* (see `OnboardingHouseholdPage`'s `goToNextStep`), every other path ending
* the wizard straight to `/`. Step order and the "sur 3" / "4 sur 4"
* indicators live in `pages/onboarding/onboarding-steps.ts`.
*
* `/telecharger` is the mobile-app-redirect chantier's fallback landing spot
* (see `lib/mobile-app-redirect.ts`'s own doc comment) — public like
@ -138,6 +140,14 @@ export function App() {
</RequireAuth>
}
/>
<Route
path="/onboarding/allergenes"
element={
<RequireAuth>
<OnboardingAllergensPage />
</RequireAuth>
}
/>
<Route
path="/onboarding/foyer"
element={
@ -154,14 +164,6 @@ export function App() {
</RequireAuth>
}
/>
<Route
path="/onboarding/allergenes"
element={
<RequireAuth>
<OnboardingAllergensPage />
</RequireAuth>
}
/>
<Route
path="/login"
element={

View file

@ -37,7 +37,7 @@ export function SignupPage() {
const [formError, setFormError] = useState<string | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
/** Validates, then submits the form; on success, starts the regime/household/allergens onboarding wizard rather than going straight to the home. */
/** Validates, then submits the form; on success, starts the régime → allergènes → foyer onboarding wizard rather than going straight to the home. */
async function handleSubmit(e: FormEvent) {
e.preventDefault();
setFormError(null);

View file

@ -6,22 +6,23 @@ import { useNavigate } from "react-router-dom";
import { ApiError, apiClient } from "../../api/client";
import { AllergySelect } from "../../features/profile/AllergySelect";
import { errorMessageService } from "../../services/error-message.service";
import { onboardingStepIndicator } from "./onboarding-steps";
import "./onboarding.scss";
/**
* Last step of the post-signup onboarding wizard, routed at
* `/onboarding/allergenes`. Starts from an empty selection — a freshly
* signed-up profile has none yet, so there's no need for the extra
* Second step of the post-signup onboarding wizard, routed at
* `/onboarding/allergenes` — the allergens half of "préférences" (régime
* is the first half), now ahead of the household step rather than the
* wizard's final screen (see #129). Starts from an empty selection — a
* freshly signed-up profile has none yet, so there's no need for the extra
* `GET /profile/allergies` round trip a "resume where I left off" flow
* would require (the `/parametres/preferences` settings page is the
* always-fetch source of truth for editing an existing selection later).
*
* Also fetches the current household on mount, only to know whether the
* `/onboarding/sources` step ran before this one (household step created
* one and it had a source to configure) — the step count shown reflects
* whichever path the visitor actually took, "3 sur 3" or "4 sur 4"
* (there's no shared wizard state to read this from — see
* `OnboardingHouseholdPage`'s doc comment on why each step is independent).
* No household fetch anymore: this step no longer needs to retroactively
* work out whether a sources step ran, since it now comes before the
* household step entirely — the "sur 3" vs "4 sur 4" split lives in
* {@link onboardingStepIndicator}.
*/
export function OnboardingAllergensPage() {
const { t } = useTranslation();
@ -29,27 +30,22 @@ export function OnboardingAllergensPage() {
const [allergies, setAllergies] = useState<AllergyView[]>([]);
const [allergyIds, setAllergyIds] = useState<number[]>([]);
const [totalSteps, setTotalSteps] = useState(3);
const [isLoading, setIsLoading] = useState(true);
const [formError, setFormError] = useState<string | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
useEffect(() => {
let cancelled = false;
Promise.all([apiClient.getAllergies(), apiClient.getCurrentHouse()])
.then(([allergiesResult, house]) => {
if (cancelled) return;
setAllergies(allergiesResult);
setTotalSteps(house !== null ? 4 : 3);
})
.catch(() => {
// Neither piece is essential for this last step to render — the
// allergy list just stays empty (nothing to pick from) and the
// step count keeps its 3-of-3 default, same "don't strand the
// visitor over a transient failure" reasoning as
// OnboardingSourcesPage's own catch, just with nothing to
// navigate away to since this already is the last step.
apiClient
.getAllergies()
.then((allergiesResult) => {
if (!cancelled) setAllergies(allergiesResult);
})
// Not essential for this step to render — the allergy list just
// stays empty (nothing to pick from) rather than stranding the
// visitor over a transient failure, same reasoning as
// OnboardingSourcesPage's own catch.
.catch(() => {})
.finally(() => {
if (!cancelled) setIsLoading(false);
});
@ -58,14 +54,14 @@ export function OnboardingAllergensPage() {
};
}, []);
/** Finishes the wizard — the profile journey is complete, back to the app itself. */
/** Moves on to the household step — preferences (régime + allergènes) are done. */
async function handleSubmit(e: FormEvent) {
e.preventDefault();
setFormError(null);
setIsSubmitting(true);
try {
await apiClient.updateAllergyIds(allergyIds);
void navigate("/");
void navigate("/onboarding/foyer");
} catch (err) {
const code = err instanceof ApiError ? err.code : ErrorCode.INTERNAL_ERROR;
setFormError(errorMessageService.getLabel(code));
@ -78,7 +74,7 @@ export function OnboardingAllergensPage() {
<main className="onboarding-page">
<form className="onboarding-card" onSubmit={handleSubmit} noValidate>
<p className="onboarding-step">
{t("onboarding.step", { current: totalSteps, total: totalSteps })}
{t("onboarding.step", onboardingStepIndicator("allergens"))}
</p>
<h1>{t("onboarding.allergens.title")}</h1>
@ -104,7 +100,7 @@ export function OnboardingAllergensPage() {
{formError && <p className="form-error">{formError}</p>}
<button type="submit" disabled={isSubmitting || isLoading}>
{t("onboarding.finish")}
{t("onboarding.continue")}
</button>
</form>
</main>

View file

@ -7,6 +7,7 @@ import { ApiError, apiClient } from "../../api/client";
import { useAuth } from "../../features/auth/AuthContext";
import { DietSelect } from "../../features/profile/DietSelect";
import { errorMessageService } from "../../services/error-message.service";
import { onboardingStepIndicator } from "./onboarding-steps";
import "./onboarding.scss";
/**
@ -15,6 +16,9 @@ import "./onboarding.scss";
* already-loaded profile (`user.dietId`) — freshly signed-up, this is
* `null` — no extra fetch needed just to know the starting value, unlike
* the household step which has no such shortcut for the current name.
*
* Followed by the allergens step (both make up "préférences" — see #129);
* the household step comes after those, not between them.
*/
export function OnboardingDietPage() {
const { t } = useTranslation();
@ -53,7 +57,7 @@ export function OnboardingDietPage() {
setIsSubmitting(true);
try {
await apiClient.updateDiet(dietId);
void navigate("/onboarding/foyer");
void navigate("/onboarding/allergenes");
} catch (err) {
const code = err instanceof ApiError ? err.code : ErrorCode.INTERNAL_ERROR;
setFormError(errorMessageService.getLabel(code));
@ -65,7 +69,7 @@ export function OnboardingDietPage() {
return (
<main className="onboarding-page">
<form className="onboarding-card" onSubmit={handleSubmit} noValidate>
<p className="onboarding-step">{t("onboarding.step", { current: 1, total: 3 })}</p>
<p className="onboarding-step">{t("onboarding.step", onboardingStepIndicator("regime"))}</p>
<h1>{t("onboarding.diet.title")}</h1>
{isLoading ? (

View file

@ -6,16 +6,24 @@ import { ApiError, apiClient } from "../../api/client";
import { HouseNameField } from "../../features/profile/HouseNameField";
import { fieldErrorsFrom } from "../../lib/zod-errors";
import { errorMessageService } from "../../services/error-message.service";
import { onboardingStepIndicator } from "./onboarding-steps";
import "./onboarding.scss";
/**
* Second step of the post-signup onboarding wizard, routed at
* `/onboarding/foyer` — unlike the regime/allergens steps, this one is
* genuinely optional rather than just "skippable via an unedited default":
* no household is created at signup anymore (see the API's
* `auth.service.ts`), so there's nothing to prefill or implicitly keep by
* pressing "Continuer" — the visitor explicitly creates one, joins one by
* invite code, or skips the step outright via {@link SkipButton}.
* Third step of the post-signup onboarding wizard, routed at
* `/onboarding/foyer` — after the régime + allergènes "préférences" steps
* (see #129). Unlike those, this one is genuinely optional rather than
* just "skippable via an unedited default": no household is created at
* signup anymore (see the API's `auth.service.ts`), so there's nothing to
* prefill or implicitly keep by pressing "Continuer" — the visitor
* explicitly creates one, joins one by invite code, or skips the step
* outright via {@link SkipButton}.
*
* This step's outcome decides whether the wizard has a 4th step: only
* *creating* a household leads on to `/onboarding/sources` (a per-house
* preference to seed while the visitor is here); joining one or skipping
* ends the wizard straight to `/` (a joined household's sources were set
* by whoever created it; a skipped step has no household to attach any to).
*
* Still checks `getCurrentHouse()` on mount and skips straight to the
* "already have one" state if it finds one — defensive against revisiting
@ -50,19 +58,22 @@ export function OnboardingHouseholdPage() {
}, []);
/**
* A household is what the next step (`/onboarding/sources`) needs to
* attach a preference to — created/joined/already-there goes on to it,
* skipped goes straight to `/onboarding/allergenes` instead (nothing to
* configure sources for without a household).
* `/onboarding/sources` only makes sense for a household the visitor just
* *created* — that's the sole caller passing `true`. Joining one, skipping
* the step, or finding one already there (browser back — can't tell
* created from joined, and the sources step self-skips when there's
* nothing to configure anyway) all end the wizard at `/`.
*/
function goToNextStep(hasHousehold: boolean) {
void navigate(hasHousehold ? "/onboarding/sources" : "/onboarding/allergenes");
function goToNextStep(createdHousehold: boolean) {
void navigate(createdHousehold ? "/onboarding/sources" : "/");
}
return (
<main className="onboarding-page">
<div className="onboarding-card">
<p className="onboarding-step">{t("onboarding.step", { current: 2, total: 3 })}</p>
<p className="onboarding-step">
{t("onboarding.step", onboardingStepIndicator("household"))}
</p>
<h1>{t("onboarding.household.title")}</h1>
{isLoading ? (
@ -70,15 +81,15 @@ export function OnboardingHouseholdPage() {
) : house !== null ? (
<>
<p>{t("onboarding.household.alreadyHasHouse", { name: house.name })}</p>
<button type="button" onClick={() => goToNextStep(true)}>
{t("onboarding.continue")}
<button type="button" onClick={() => goToNextStep(false)}>
{t("onboarding.finish")}
</button>
</>
) : (
<>
<p className="onboarding-subtitle">{t("onboarding.household.subtitle")}</p>
<CreateHouseholdForm onDone={() => goToNextStep(true)} />
<JoinHouseholdForm onDone={() => goToNextStep(true)} />
<JoinHouseholdForm onDone={() => goToNextStep(false)} />
<SkipButton onSkip={() => goToNextStep(false)} />
</>
)}

View file

@ -6,22 +6,23 @@ import { useNavigate } from "react-router-dom";
import { ApiError, apiClient } from "../../api/client";
import { SourceSelect } from "../../features/house/SourceSelect";
import { errorMessageService } from "../../services/error-message.service";
import { onboardingStepIndicator } from "./onboarding-steps";
import "./onboarding.scss";
/**
* Third step of the post-signup onboarding wizard, routed at
* `/onboarding/sources` — only reached when a household now exists (see
* `OnboardingHouseholdPage`'s `goToStep`, which skips straight to
* `/onboarding/allergenes` when the previous step was skipped): enabled
* sources are a per-household preference (`HouseSource`, schema.prisma),
* so there's nothing to attach a selection to without one.
* Final step of the post-signup onboarding wizard, routed at
* `/onboarding/sources` — reached only when the previous step *created* a
* household (see `OnboardingHouseholdPage`'s `goToNextStep`, which ends the
* wizard at `/` on every other path): enabled sources are a per-household
* preference (`HouseSource`, schema.prisma), so there's nothing to attach
* a selection to otherwise.
*
* Starts from an empty selection — a newly created/joined household has
* nothing enabled yet by design (opt-in, see `house.service.ts`'s
* Starts from an empty selection — a newly created household has nothing
* enabled yet by design (opt-in, see `house.service.ts`'s
* `getHouseSourceIds`). If no source is implemented yet (`getSources()`
* comes back empty — currently always the case), there's nothing to pick
* from either, so this step silently steps itself over rather than showing
* an empty screen.
* from either, so this step silently finishes the wizard (straight to `/`)
* rather than showing an empty screen.
*/
export function OnboardingSourcesPage() {
const { t } = useTranslation();
@ -40,7 +41,7 @@ export function OnboardingSourcesPage() {
.then((result) => {
if (cancelled) return;
if (result.length === 0) {
void navigate("/onboarding/allergenes", { replace: true });
void navigate("/", { replace: true });
return;
}
setSources(result);
@ -50,7 +51,7 @@ export function OnboardingSourcesPage() {
// Nothing to configure sources for if we can't even list them — the
// wizard shouldn't strand the visitor here over a transient failure
// fetching an optional step's own data.
if (!cancelled) void navigate("/onboarding/allergenes", { replace: true });
if (!cancelled) void navigate("/", { replace: true });
});
return () => {
cancelled = true;
@ -63,7 +64,7 @@ export function OnboardingSourcesPage() {
setIsSubmitting(true);
try {
await apiClient.updateHouseSourceIds(sourceIds);
void navigate("/onboarding/allergenes");
void navigate("/");
} catch (err) {
const code = err instanceof ApiError ? err.code : ErrorCode.INTERNAL_ERROR;
setFormError(errorMessageService.getLabel(code));
@ -75,7 +76,9 @@ export function OnboardingSourcesPage() {
return (
<main className="onboarding-page">
<form className="onboarding-card" onSubmit={handleSubmit} noValidate>
<p className="onboarding-step">{t("onboarding.step", { current: 3, total: 4 })}</p>
<p className="onboarding-step">
{t("onboarding.step", onboardingStepIndicator("sources", { hasSources: true }))}
</p>
<h1>{t("onboarding.sources.title")}</h1>
<p className="onboarding-subtitle">{t("onboarding.sources.subtitle")}</p>
@ -93,7 +96,7 @@ export function OnboardingSourcesPage() {
{formError && <p className="form-error">{formError}</p>}
<button type="submit" disabled={isSubmitting || isLoading}>
{t("onboarding.continue")}
{t("onboarding.finish")}
</button>
</form>
</main>

View file

@ -0,0 +1,45 @@
/**
* The post-signup onboarding wizard's step order, in one place so the
* per-page "Étape N sur M" indicators can't drift out of sync when the
* sequence changes.
*
* Order: régime → allergènes → foyer → [sources]. `"sources"` is a
* conditional 4th step, reached only when the visitor *creates* a
* household on the foyer step — joining one or skipping the step ends the
* wizard at foyer (a joined household's sources were already configured by
* whoever created it, and there's nothing to attach a source selection to
* without a household at all).
*
* Each page renders its own indicator with no shared runtime wizard state
* to read from (see {@link OnboardingHouseholdPage}'s doc on why the steps
* are independent), so régime/allergènes/foyer — all upstream of the
* "create vs join/skip" decision — can't yet know whether a 4th step is
* coming and show "sur 3"; only the `"sources"` step, which by definition
* is only ever reached on the 4-step path, shows "4 sur 4". This mirrors
* the pre-#129 flow, whose last step likewise only learned the real total
* after the fact.
*/
export type OnboardingStep = "regime" | "allergens" | "household" | "sources";
/** The wizard's two possible step sequences, keyed by whether the sources step runs. */
export const ONBOARDING_STEP_SEQUENCE = {
/** régime → allergènes → foyer: household joined or skipped (or still upstream of that choice). */
withoutSources: ["regime", "allergens", "household"],
/** régime → allergènes → foyer → sources: the visitor created a household. */
withSources: ["regime", "allergens", "household", "sources"],
} as const satisfies Record<string, readonly OnboardingStep[]>;
/**
* `{ current, total }` to feed `t("onboarding.step", …)` for a given step.
* Defaults to the 3-step sequence; the `"sources"` step passes
* `hasSources: true` for its "4 sur 4".
*/
export function onboardingStepIndicator(
step: OnboardingStep,
{ hasSources = false }: { hasSources?: boolean } = {},
): { current: number; total: number } {
const sequence: readonly OnboardingStep[] = hasSources
? ONBOARDING_STEP_SEQUENCE.withSources
: ONBOARDING_STEP_SEQUENCE.withoutSources;
return { current: sequence.indexOf(step) + 1, total: sequence.length };
}

View file

@ -71,7 +71,7 @@ apps/web/src/
│ │ ├── AccountSettingsPage.tsx / HouseholdSettingsPage.tsx / PreferencesPage.tsx
│ │ ├── UserPreferencesPage.tsx / CreditsPage.tsx
│ │ └── settings-pages.scss
│ └── onboarding/ # wizard d'inscription (régime → foyer → [sources] → allergènes), voir plus bas
│ └── onboarding/ # wizard d'inscription (régime → allergènes → foyer → [sources]), voir plus bas
│ ├── OnboardingDietPage.tsx / OnboardingHouseholdPage.tsx / OnboardingSourcesPage.tsx / OnboardingAllergensPage.tsx
│ └── onboarding.scss
├── styles/
@ -156,9 +156,11 @@ flowchart TB
l'ancienne page combinée) redirige vers `/parametres/foyer` pour ne pas casser
un lien existant.
- **`/onboarding/*`** reste son propre groupe de routes top-level (pas nichées
sous `AppLayout` — wizard plein écran sans sidebar) : `regime` → `foyer` →
`sources` (conditionnelle — seulement si l'étape `foyer` a créé/rejoint un
foyer, sinon on saute directement à l'étape suivante) → `allergenes`.
sous `AppLayout` — wizard plein écran sans sidebar) : `regime` → `allergenes`
(les deux « préférences ») → `foyer` → `sources` (conditionnelle — seulement
si l'étape `foyer` a **créé** un foyer ; rejoindre/passer termine le wizard
vers `/`). L'ordre et les compteurs « sur 3 » / « 4 sur 4 » vivent dans
`pages/onboarding/onboarding-steps.ts`.
---
@ -226,11 +228,11 @@ Deux surfaces partagent les mêmes composants de champ (`features/profile/` +
```mermaid
flowchart LR
SIGNUP["SignupPage<br/>(POST /auth/signup)"] --> OB1["/onboarding/regime"]
OB1 --> OB2["/onboarding/foyer"]
OB2 -->|"foyer créé/rejoint"| OB3["/onboarding/sources"]
OB2 -->|"pas de foyer"| OB4["/onboarding/allergenes"]
OB3 --> OB4
OB4 --> HOME["/ (PlanningPage)"]
OB1 --> OB2["/onboarding/allergenes"]
OB2 --> OB3["/onboarding/foyer"]
OB3 -->|"foyer créé"| OB4["/onboarding/sources"]
OB3 -->|"rejoint / passé"| HOME["/ (PlanningPage)"]
OB4 --> HOME
SETTINGSMENU["Sidebar : Paramètres"] --> S1["/parametres/compte"]
SETTINGSMENU --> S2["/parametres/preferences"]
@ -240,18 +242,23 @@ flowchart LR
SETTINGSMENU --> S6["/parametres/credits"]
```
- **`pages/onboarding/`** — wizard de **4 écrans** (régime → foyer → sources →
allergènes), lancé une seule fois juste après l'inscription. Routes
top-level `RequireAuth`, **pas** nichées sous `AppLayout` : wizard plein
écran sans sidebar (`onboarding.scss`, même langage visuel que
`/login`/`/signup`). Chaque étape a un unique bouton "Continuer" qui envoie
la valeur courante — pas de bouton "Passer" séparé, une valeur
"aucune"/vide *est* le skip. `/onboarding/sources` est **conditionnelle** :
seulement atteinte si l'étape `foyer` vient de créer/rejoindre un foyer
(`OnboardingHouseholdPage`'s `goToNextStep`) — sautée sinon, directement vers
`/onboarding/allergenes`. `OnboardingSourcesPage` elle-même redirige en
silence vers l'étape suivante si `GET /reference/sources` revient vide ou en
erreur, plutôt que d'afficher une étape sans rien à choisir.
- **`pages/onboarding/`** — wizard de **3 écrans** (régime → allergènes →
foyer), + un **4ᵉ conditionnel** (sources), lancé une seule fois juste après
l'inscription. Routes top-level `RequireAuth`, **pas** nichées sous
`AppLayout` : wizard plein écran sans sidebar (`onboarding.scss`, même
langage visuel que `/login`/`/signup`). Régime et allergènes forment les
deux « préférences » ; chacun a un unique bouton "Continuer" qui envoie la
valeur courante — une valeur "aucune"/vide *est* le skip. L'étape `foyer`,
elle, est explicitement optionnelle (créer / rejoindre / "Passer cette
étape"). `/onboarding/sources` est **conditionnelle** : seulement atteinte
si l'étape `foyer` vient de **créer** un foyer
(`OnboardingHouseholdPage`'s `goToNextStep`) — rejoindre ou passer termine
le wizard vers `/`. `OnboardingSourcesPage` elle-même redirige en silence
vers `/` si `GET /reference/sources` revient vide ou en erreur, plutôt que
d'afficher une étape sans rien à choisir. L'ordre des étapes et les
compteurs affichés (« Étape 1 sur 3 » … « 4 sur 4 » pour le chemin avec
sources) sont centralisés dans `pages/onboarding/onboarding-steps.ts` —
chaque page les lisait auparavant en dur, ce qu'un réordonnancement cassait.
- **Pages `/parametres/*`** (`pages/settings/`, dans `AppLayout`) — l'ancienne
`HouseholdPage` combinée a été **éclatée en 5 pages** dédiées (voir la
section suivante pour le détail de chacune), toutes en **hot saving** (pas