feat(onboarding): add resumable module-tour progress field + API

Add nullable onboarding_tour_progress_json to users (migration 0173),
a Zod-validated progress shape, and extend POST /api/onboarding/tour to
accept a mid-tour resume checkpoint and clear it on replay. Surface the
resume point on GET /api/auth/me and register the contract in OpenAPI so
the native client mirrors it.
This commit is contained in:
Marc-André Bombeck committed 2026-06-18 19:27:28 +02:00
1 parent b474ea1489
commit 59e7b33285
15 files changed
+442 -17

No files matched your search

+2
View File
@@ -42,6 +42,7 @@ import { measurementReminderPaths } from "./measurement-reminders";
import { medicationPaths, medicationResource } from "./medications";
import { metaPaths } from "./meta";
import { moodPaths } from "./mood";
import { onboardingPaths } from "./onboarding";
import { profilePaths } from "./profile";
import { settingsPaths } from "./settings";
import { syncPaths } from "./sync";
@@ -72,6 +73,7 @@ export const openApiPaths: NonNullable<ZodOpenApiObject["paths"]> = {
...labsPaths,
...biomarkerPaths,
...illnessPaths,
...onboardingPaths,
};
export const openApiComponents: NonNullable<ZodOpenApiObject["components"]> = {
+81
View File
@@ -0,0 +1,81 @@
/**
* OpenAPI route table — onboarding module tour.
*
* Part of the OpenAPI route table; aggregated in `./index.ts`.
* The request schema comes from `src/lib/onboarding/tour-progress.ts`
* so the wire contract stays single-source with the runtime parser.
*
* v1.18.6 — the resumable module-tour contract the iOS client mirrors:
* a fire-and-forget progress checkpoint plus the coarse completion
* flip. The resume point also rides `GET /api/auth/me` as
* `onboardingTourProgress`.
*/
import { z } from "zod/v4";
import type { ZodOpenApiObject } from "zod-openapi";
import { tourProgressSchema } from "@/lib/onboarding/tour-progress";
import { dataEnvelope, stdResponses } from "./shared";
const tourProgressResource = tourProgressSchema.meta({
id: "TourProgress",
description:
"Resumable module-tour progress point. `lastStopId` seeds the resume index; `status` is the running/terminal state.",
});
const tourUpdateRequest = z
.object({
completed: z
.boolean()
.optional()
.describe(
"Flip the coarse completion flag. `false` is a replay reset and clears the stored progress point.",
),
outcome: z
.enum(["completed", "skipped"])
.optional()
.describe("Informational — distinguishes reaching the end from a skip."),
progress: tourProgressResource
.optional()
.describe("Mid-tour resume checkpoint. May arrive alone or with `completed`."),
})
.meta({
id: "TourUpdateRequest",
description:
"Update the module-tour state. Provide `completed` and/or `progress`.",
});
const tourUpdateResponse = z
.object({
onboardingTourCompleted: z.boolean(),
progress: tourProgressResource.nullable(),
})
.meta({
id: "TourUpdateResponse",
description: "The persisted completion flag and resume point after the write.",
});
export const onboardingPaths: NonNullable<ZodOpenApiObject["paths"]> = {
"/api/onboarding/tour": {
post: {
tags: ["Onboarding"],
summary: "Update module-tour completion + resume point",
description:
"Persists the module-tour state. The client posts a fire-and-forget `progress` checkpoint on each step so a reload resumes at the right module, and a terminal `completed:true` with `outcome` when the tour ends. `completed:false` is a replay reset that also clears the resume point.",
requestBody: {
required: true,
content: { "application/json": { schema: tourUpdateRequest } },
},
responses: {
"200": {
description: "Tour state updated.",
content: {
"application/json": {
schema: dataEnvelope(tourUpdateResponse, "TourUpdateEnvelope"),
},
},
},
...stdResponses,
},
},
},
};