Files
Cibello-app/packages/validation/src/profile.ts
T
Sven (AAMOS AI) 2f3cdf8016 FAS 1b: Progressive onboarding (3-layer flow)
- Add PROGRESSIVE_ONBOARDING feature flag to KNOWN_FLAGS
- Add onboarding_step column to users table (a/b/c)
- New backend endpoints:
  - GET /v1/onboarding/status – check current step + feature flag
  - POST /v1/onboarding/quick-start – complete Step A (goal + precision)
  - POST /v1/onboarding/complete-b – complete Step B (diet, allergens, household)
  - POST /v1/onboarding/complete-c – complete Step C (health profile)
  - POST /v1/onboarding/skip – GDPR-friendly skip
- Refactor mobile onboarding screen into 3 progressive layers
- Update auth store with onboardingStep state
- Update tab layout to only block on Step A
- Update registration to set onboardingStep='a'
- Add i18n keys for Step B/C titles across all 12 locales
- Add Zod validation schemas (quickStartInputSchema, onboardingStatusSchema)
- Add tests for validation and feature flag
- Preserve existing /v1/me/onboarding for backward compatibility
- Migration: 0004_progressive_onboarding.sql
2026-08-06 00:40:59 +07:00

90 lines
3.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { z } from "zod";
import {
ACTIVITY_LEVELS,
ALLERGENS,
CONSENT_KINDS,
CUISINES,
DIET_PATTERNS,
EQUIPMENT,
GOAL_TYPES,
PRECISION_MODES,
RELIGIOUS_RULES,
SEXES,
} from "@app/shared-types";
/** Hälsoprofil hanteras separat från hushållsdata (spec §7, §56). */
export const updateHealthProfileInputSchema = z.object({
birthYear: z.number().int().min(1900).max(2030).optional(),
sex: z.enum(SEXES).optional(),
heightCm: z.number().min(80).max(250).optional(),
weightKg: z.number().min(20).max(400).optional(),
targetWeightKg: z.number().min(20).max(400).optional(),
activityLevel: z.enum(ACTIVITY_LEVELS).optional(),
trainingSessionsPerWeek: z.number().int().min(0).max(21).optional(),
trainingTypes: z.array(z.string().max(50)).max(10).optional(),
});
export type UpdateHealthProfileInput = z.infer<typeof updateHealthProfileInputSchema>;
export const updatePreferencesInputSchema = z.object({
primaryGoal: z.enum(GOAL_TYPES).optional(),
goals: z.array(z.enum(GOAL_TYPES)).max(11).optional(),
dietPattern: z.enum(DIET_PATTERNS).optional(),
religiousRule: z.enum(RELIGIOUS_RULES).optional(),
allergens: z.array(z.enum(ALLERGENS)).optional(),
intolerances: z.array(z.string().max(60)).max(30).optional(),
avoidIngredientIds: z.array(z.string().max(80)).max(100).optional(),
favoriteCuisines: z.array(z.enum(CUISINES)).max(19).optional(),
dislikedDishes: z.array(z.string().max(80)).max(50).optional(),
spiceLevelMax: z.number().int().min(0).max(5).optional(),
weeklyBudgetMinor: z.number().int().min(0).max(10_000_000).nullable().optional(),
maxCookingMinutesWeekday: z.number().int().min(5).max(360).nullable().optional(),
equipment: z.array(z.enum(EQUIPMENT)).optional(),
defaultPortions: z.number().int().min(1).max(20).optional(),
});
export type UpdatePreferencesInput = z.infer<typeof updatePreferencesInputSchema>;
export const updateMeInputSchema = z.object({
displayName: z.string().min(1).max(80).trim().optional(),
locale: z.string().min(2).max(10).optional(),
precisionMode: z.enum(PRECISION_MODES).optional(),
});
export type UpdateMeInput = z.infer<typeof updateMeInputSchema>;
export const consentInputSchema = z.object({
kind: z.enum(CONSENT_KINDS),
granted: z.boolean(),
});
export type ConsentInput = z.infer<typeof consentInputSchema>;
/** Onboarding i ett svep (spec §6): allt är valfritt utom visningsnamnet som redan finns. */
export const onboardingInputSchema = z.object({
healthProfile: updateHealthProfileInputSchema.optional(),
preferences: updatePreferencesInputSchema.optional(),
precisionMode: z.enum(PRECISION_MODES).default("simple"),
householdChoice: z
.discriminatedUnion("kind", [
z.object({ kind: z.literal("create"), name: z.string().min(1).max(80) }),
z.object({ kind: z.literal("join"), inviteCode: z.string().min(4).max(20) }),
z.object({ kind: z.literal("skip") }),
])
.default({ kind: "skip" }),
});
export type OnboardingInput = z.infer<typeof onboardingInputSchema>;
/** Progressive onboarding Step A minimal info for immediate value (FAS 1b). */
export const quickStartInputSchema = z.object({
primaryGoal: z.enum(GOAL_TYPES).optional(),
precisionMode: z.enum(PRECISION_MODES).default("simple"),
});
export type QuickStartInput = z.infer<typeof quickStartInputSchema>;
/** Progressive onboarding status response (FAS 1b). */
export const onboardingStatusSchema = z.object({
step: z.enum(["a", "b", "c"]),
onboardingCompleted: z.boolean(),
hasHousehold: z.boolean(),
hasHealthProfile: z.boolean(),
hasPreferences: z.boolean(),
});
export type OnboardingStatus = z.infer<typeof onboardingStatusSchema>;