Skip to content

feat(frontend): streamline onboarding from 7 steps to 3 with skip option (#701)#746

Merged
ericsocrat merged 3 commits intomainfrom
feat/701-onboarding-streamline
Mar 8, 2026
Merged

feat(frontend): streamline onboarding from 7 steps to 3 with skip option (#701)#746
ericsocrat merged 3 commits intomainfrom
feat/701-onboarding-streamline

Conversation

@ericsocrat
Copy link
Owner

feat(frontend): streamline onboarding from 7 steps to 3 with skip option

Closes #701

Summary

Merges the 7-step onboarding flow into 3 visible steps with auto-complete, reducing friction and improving completion rates.

New 3-Step Flow

Step Components Merged Content
0 Welcome + Region 🍎 greeting, country selection, language picker
1 Diet + Allergens Diet preference, strict toggle, allergen pills, trace toggles
2 Goals + Categories Health goals, food category grid, Finish button

On "Finish" → auto-saves preferences → success toast → redirect to /app/categories.

Key Changes

  • 3 new merged step components: WelcomeRegionStep, DietAllergensStep, GoalsCategoriesStep
  • OnboardingWizard rewritten: 3-step flow, localStorage persistence (tryvit_onboarding_progress), auto-complete on final step
  • Progress bar on all steps: "Step X of 3" visible throughout
  • Skip available everywhere: Step 0 has skip button; steps 1-2 have skip link
  • localStorage persistence: Refresh-safe — partial progress restored on mount, cleared after save/skip
  • Redirect updated: /app/search/app/categories
  • i18n: Added "finish" key (en: "Finish", pl: "Zakończ", de: "Fertig")

Files Changed

12 files changed, +979 / -158 lines

  • 6 new files (3 components + 3 test files)
  • 6 modified files (wizard, types, i18n × 3, wizard tests)

Test Coverage

  • 160 onboarding tests across 17 test files (all passing)
  • 47 new tests in 4 files:
    • WelcomeRegionStep.test.tsx — 16 tests
    • DietAllergensStep.test.tsx — 14 tests
    • GoalsCategoriesStep.test.tsx — 12 tests
    • OnboardingWizard.test.tsx — 17 tests (rewritten for 3-step flow + localStorage)
  • Old step tests kept for backward compatibility

Verification

npx tsc --noEmit            → 0 errors
npx vitest run onboarding   → 160/160 tests pass (17 files)

…ion (#701)

- Merge Welcome+Region, Diet+Allergens, Goals+Categories into 3 visible steps
- Auto-complete on Finish (no separate Done screen)
- Add localStorage persistence (tryvit_onboarding_progress key)
- Redirect to /app/categories after completion or skip
- Add 'finish' i18n key (en/pl/de)
- 47 new tests across 4 test files (160 onboarding tests total)
Copilot AI review requested due to automatic review settings March 8, 2026 00:11
@vercel
Copy link

vercel bot commented Mar 8, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
tryvit Canceled Canceled Mar 8, 2026 9:49am

@chatgpt-codex-connector
Copy link

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@github-actions
Copy link

github-actions bot commented Mar 8, 2026

Bundle Size Report

Metric Value
Main baseline 3471 KB
This PR 3469 KB
Delta -2 KB (-0.1%)
JS chunks 135
Hard limit 4000 KB

✅ Bundle size is within acceptable limits.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR streamlines the frontend onboarding experience by collapsing the previous multi-step wizard into a 3-step flow with skip support and persisted progress, and updates copy/i18n to match the new UI.

Changes:

  • Replaces the 7-step onboarding wizard with 3 merged steps (Welcome+Region, Diet+Allergens, Goals+Categories) and updates navigation/progress display.
  • Adds localStorage persistence for onboarding progress and clears it on successful completion/skip.
  • Adds the new i18n key for the “Finish” CTA in en/pl/de.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
frontend/src/app/onboarding/types.ts Updates step count constant and introduces a shared localStorage key for onboarding persistence.
frontend/src/app/onboarding/steps/WelcomeRegionStep.tsx New merged step for welcome + region + language selection, with skip/next CTAs.
frontend/src/app/onboarding/steps/WelcomeRegionStep.test.tsx Unit tests covering rendering and interactions for the merged Welcome+Region step.
frontend/src/app/onboarding/steps/DietAllergensStep.tsx New merged step combining diet selection and allergen selection/toggles.
frontend/src/app/onboarding/steps/DietAllergensStep.test.tsx Unit tests for diet/allergen selection behaviors and navigation.
frontend/src/app/onboarding/steps/GoalsCategoriesStep.tsx New merged step combining health goals and category selection with a Finish CTA.
frontend/src/app/onboarding/steps/GoalsCategoriesStep.test.tsx Unit tests for goals/categories selection and finish/back navigation.
frontend/src/app/onboarding/OnboardingWizard.tsx Rewrites wizard flow for 3 steps, adds persistence, updates progress bar behavior, and redirects to /app/categories.
frontend/src/app/onboarding/OnboardingWizard.test.tsx Updates wizard tests for the new 3-step flow and adds localStorage persistence coverage.
frontend/messages/en.json Adds onboarding.finish translation.
frontend/messages/pl.json Adds onboarding.finish translation.
frontend/messages/de.json Adds onboarding.finish translation.

Comment on lines +28 to +38
function loadPersistedState(): { step: number; data: OnboardingData } | null {
try {
const raw = localStorage.getItem(ONBOARDING_STORAGE_KEY);
if (!raw) return null;
const parsed = JSON.parse(raw) as { step: number; data: OnboardingData };
if (typeof parsed.step !== "number" || !parsed.data) return null;
return parsed;
} catch {
return null;
}
}
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

loadPersistedState() returns whatever is in localStorage without clamping the step to a valid range or validating the data shape. A corrupted/stale value (e.g., step > 2 or missing arrays like healthGoals) can lead to an empty wizard render or runtime errors (e.g., calling .includes on undefined). Consider normalizing here: clamp step to [0, TOTAL_STEPS-1] (or discard invalid), and merge persisted data into INITIAL_ONBOARDING_DATA to guarantee required fields/arrays exist.

Copilot uses AI. Check for mistakes.
Comment on lines +46 to +49
const [step, setStep] = useState(() => loadPersistedState()?.step ?? 0);
const [data, setData] = useState<OnboardingData>(
() => loadPersistedState()?.data ?? INITIAL_ONBOARDING_DATA,
);
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The component reads and parses persisted onboarding state twice (once for step and once for data). This can produce inconsistent initial state (and does duplicate JSON.parse work). Consider loading persisted state once (e.g., in a single initializer) and deriving both step and data from that single result.

Copilot uses AI. Check for mistakes.
Comment on lines 68 to 157
@@ -74,81 +137,47 @@ export function OnboardingWizard() {
return;
}

track("onboarding_completed", { skipped: true });
router.push("/app/search");
router.refresh();
}

async function handleComplete() {
setLoading(true);
const result = await completeOnboarding(supabase, {
country: data.country,
language: data.language || undefined,
diet: data.diet,
allergens: data.allergens,
strict_allergen: data.strictAllergen,
strict_diet: data.strictDiet,
treat_may_contain_as_unsafe: data.treatMayContain,
health_goals: data.healthGoals,
favorite_categories: data.favoriteCategories,
});
setLoading(false);

if (!result.ok) {
showToast({
type: "error",
messageKey: "onboarding.onboardingFailed",
});
return;
// Clear persisted progress
try {
localStorage.removeItem(ONBOARDING_STORAGE_KEY);
} catch {
// noop
}

track("onboarding_completed", {
skipped: false,
diet: data.diet,
allergen_count: data.allergens.length,
health_goal_count: data.healthGoals.length,
category_count: data.favoriteCategories.length,
});
showToast({ type: "success", messageKey: "onboarding.preferencesSaved" });
router.push("/app/search");
track("onboarding_completed", { skipped: true });
router.push("/app/categories");
router.refresh();
}

const stepProps = {
data,
onChange: updateData,
onNext: goNext,
onNext: step === TOTAL_STEPS - 1 ? handleComplete : goNext,
onBack: goBack,
};
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

handleComplete/handleSkipAll set loading, but the primary action buttons (e.g., Finish in the last step, Skip button on step 0, Next buttons) are not consistently disabled/guarded when loading is true. This allows double-submits (multiple RPC calls + duplicate analytics) from rapid clicks. Consider guarding early in the handlers when loading is already true and/or passing loading down so all navigation/submit controls can be disabled while an RPC is in-flight.

Copilot uses AI. Check for mistakes.

// ─── Step 1: Welcome + Region ───────────────────────────────────────────────
// Combines greeting with country/language selection in a single step.
// Issue #701: streamline onboarding from 7 steps to 4.
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The step header comment says this flow is “7 steps to 4”, but this PR implements 3 visible steps. Please update the comment to match the current behavior to avoid future confusion when maintaining onboarding step logic.

Suggested change
// Issue #701: streamline onboarding from 7 steps to 4.
// Issue #701: streamline onboarding from a 7-step flow to 3 visible steps.

Copilot uses AI. Check for mistakes.

// ─── Step 2: Diet + Allergens ───────────────────────────────────────────────
// Combines diet preference selection with allergen checklist in a single step.
// Issue #701: streamline onboarding from 7 steps to 4.
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The step header comment says this flow is “7 steps to 4”, but the wizard now has 3 visible steps. Please update the comment to reflect the actual 3-step flow so the file doesn’t contradict TOTAL_STEPS = 3.

Suggested change
// Issue #701: streamline onboarding from 7 steps to 4.
// Issue #701: streamlined onboarding into a 3-step flow (TOTAL_STEPS = 3).

Copilot uses AI. Check for mistakes.

// ─── Step 3: Health Goals + Categories ──────────────────────────────────────
// Combines health goals selection with favorite category selection.
// Issue #701: streamline onboarding from 7 steps to 4.
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The step header comment says this flow is “7 steps to 4”, but the wizard now has 3 visible steps. Please update the comment to match the current 3-step onboarding to keep documentation consistent.

Suggested change
// Issue #701: streamline onboarding from 7 steps to 4.
// Issue #701: part of streamlining onboarding to a 3-step flow.

Copilot uses AI. Check for mistakes.
Comment on lines +98 to +104
<button
onClick={onNext}
className="btn-primary flex-1"
data-testid="onboarding-complete"
>
{t("onboarding.finish")}
</button>
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The “Finish” button can trigger an async completion RPC via onNext, but it isn’t disabled while the request is in-flight. This can lead to multiple completion submissions if the user clicks repeatedly. Consider threading a loading/submitting prop from OnboardingWizard and disabling the button (and optionally showing a saving label) while completion is running.

Copilot uses AI. Check for mistakes.
Comment on lines +102 to +109
<div className="mt-8 flex gap-3">
<button
onClick={onSkipAll}
className="btn-secondary flex-1"
data-testid="onboarding-skip-all"
>
{t("onboarding.skipAll")}
</button>
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The step 0 “Skip” button calls an async handler (onSkipAll) but isn’t disabled during the in-flight request. This makes it possible to fire multiple skip RPCs via rapid clicks. Consider passing a loading prop from the wizard and disabling this button while skipping is in progress (or adding a re-entrancy guard in the handler).

Copilot uses AI. Check for mistakes.
Copilot AI review requested due to automatic review settings March 8, 2026 09:04
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated 7 comments.

Comment on lines +26 to +49
const STEP_NAMES = ["welcome_region", "diet_allergens", "goals_categories"] as const;

function loadPersistedState(): { step: number; data: OnboardingData } | null {
try {
const raw = localStorage.getItem(ONBOARDING_STORAGE_KEY);
if (!raw) return null;
const parsed = JSON.parse(raw) as { step: number; data: OnboardingData };
if (typeof parsed.step !== "number" || !parsed.data) return null;
return parsed;
} catch {
return null;
}
}

export function OnboardingWizard() {
const router = useRouter();
const supabase = createClient();
const { track } = useAnalytics();
const { t } = useTranslation();
const [step, setStep] = useState(0); // 0-indexed
const [data, setData] = useState<OnboardingData>(INITIAL_ONBOARDING_DATA);

const [step, setStep] = useState(() => loadPersistedState()?.step ?? 0);
const [data, setData] = useState<OnboardingData>(
() => loadPersistedState()?.data ?? INITIAL_ONBOARDING_DATA,
);
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

loadPersistedState()only checksparsed.datatruthiness and doesn’t validate/normalize the shape (arrays/booleans/strings) or clampstepinto the valid range. A corrupted/partial localStorage value can lead to runtime crashes in steps (e.g.,data.healthGoals.includeswhenhealthGoalsis missing) or render no step ifstepis out of bounds. Consider (a) loading once into a singleinitialvalue sostep/datacan’t get out of sync and you don’t parse JSON twice, and (b) normalizing persisted data by merging withINITIAL_ONBOARDING_DATAplus validatingstepto be within[0, TOTAL_STEPS - 1]` before using it.

Copilot uses AI. Check for mistakes.
Comment on lines +68 to +81
async function handleComplete() {
setLoading(true);
const result = await completeOnboarding(supabase, {
country: data.country,
language: data.language || undefined,
diet: data.diet,
allergens: data.allergens,
strict_allergen: data.strictAllergen,
strict_diet: data.strictDiet,
treat_may_contain_as_unsafe: data.treatMayContain,
health_goals: data.healthGoals,
favorite_categories: data.favoriteCategories,
});
setLoading(false);
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

handleComplete()can be triggered multiple times (e.g., double-clicking “Finish”) because there’s no re-entrancy guard (and the step components don’t receiveloadingto disable their CTA buttons). This can cause duplicated submissions and duplicated analytics events. Consider short-circuiting early whenloadingis already true and/or passingloading` into steps to disable Next/Finish while a request is in-flight.

Copilot uses AI. Check for mistakes.
Comment on lines +50 to +60
{COUNTRIES.map((country) => (
<button
key={country.code}
onClick={() => handleCountrySelect(country.code)}
className={`flex w-full items-center gap-4 rounded-xl border-2 p-4 text-left transition-colors ${
data.country === country.code
? "border-brand bg-brand-subtle"
: "border bg-surface hover:border-strong"
}`}
data-testid={`country-${country.code}`}
>
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The country selection is a “single-select” control but is implemented as plain buttons without any selection semantics for assistive tech. Consider adding aria-pressed for a toggle-button pattern or switching to a radiogroup/radio pattern (with appropriate labels) so screen readers can announce the current selection state. The same applies to the language selector buttons.

Copilot uses AI. Check for mistakes.
Comment on lines +3 to +5
// ─── Step 1: Welcome + Region ───────────────────────────────────────────────
// Combines greeting with country/language selection in a single step.
// Issue #701: streamline onboarding from 7 steps to 4.
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The header comment is out of date/inconsistent with the PR’s 3-step flow: it says “Step 1” and “7 steps to 4”. Please update to reflect the actual step index naming used by the wizard (Step 0) and the correct “7 → 3” change to avoid future confusion.

Suggested change
// ─── Step 1: Welcome + Region ───────────────────────────────────────────────
// Combines greeting with country/language selection in a single step.
// Issue #701: streamline onboarding from 7 steps to 4.
// ─── Step 0: Welcome + Region ───────────────────────────────────────────────
// Combines greeting with country/language selection in a single step.
// Issue #701: streamline onboarding from 7 steps to 3.

Copilot uses AI. Check for mistakes.
Comment on lines +3 to +5
// ─── Step 2: Diet + Allergens ───────────────────────────────────────────────
// Combines diet preference selection with allergen checklist in a single step.
// Issue #701: streamline onboarding from 7 steps to 4.
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The header comment is inconsistent with the new 3-step flow: this is Step 1 in the wizard, and the issue note should say “7 → 3” (not “7 → 4”). Updating these comments will keep the code aligned with the current onboarding model.

Suggested change
// ─── Step 2: Diet + Allergens ───────────────────────────────────────────────
// Combines diet preference selection with allergen checklist in a single step.
// Issue #701: streamline onboarding from 7 steps to 4.
// ─── Step 1: Diet + Allergens ───────────────────────────────────────────────
// Combines diet preference selection with allergen checklist in a single step.
// Issue #701: streamline onboarding from 7 steps to 3.

Copilot uses AI. Check for mistakes.
Comment on lines +3 to +5
// ─── Step 3: Health Goals + Categories ──────────────────────────────────────
// Combines health goals selection with favorite category selection.
// Issue #701: streamline onboarding from 7 steps to 4.
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The header comment is inconsistent with the new 3-step flow: this is Step 2 (final step), and the issue note should say “7 → 3” (not “7 → 4”).

Suggested change
// ─── Step 3: Health Goals + Categories ──────────────────────────────────────
// Combines health goals selection with favorite category selection.
// Issue #701: streamline onboarding from 7 steps to 4.
// ─── Step 2: Health Goals + Categories ──────────────────────────────────────
// Combines health goals selection with favorite category selection.
// Issue #701: streamline onboarding from 7 steps to 3.

Copilot uses AI. Check for mistakes.

it("shows checkmark for selected country", () => {
renderStep({ country: "PL", language: "en" });
const checkSpan = document.querySelector(".text-brand svg");
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This checkmark assertion is brittle because it queries the entire document by a CSS class that could match unrelated elements. Prefer scoping the query to the selected country button (e.g., within(screen.getByTestId("country-PL"))) and/or adding a stable data-testid/accessible label for the checkmark to make the test less sensitive to styling changes.

Suggested change
const checkSpan = document.querySelector(".text-brand svg");
const selectedCountry = screen.getByTestId("country-PL");
const checkSpan = selectedCountry.querySelector("svg");

Copilot uses AI. Check for mistakes.
@ericsocrat ericsocrat merged commit 8bf1837 into main Mar 8, 2026
18 of 20 checks passed
@ericsocrat ericsocrat deleted the feat/701-onboarding-streamline branch March 8, 2026 09:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(frontend): streamline onboarding from 7 steps to 4 with skip option

2 participants