feat: post-signup onboarding wizard (dietary/allergen prefs, notifications, skippable) (v0.77.0)
New accounts land on a 3-step wizard after signup — welcome, dietary preferences & allergens, notification opt-in — skippable at every step, shown once via a users.onboardingCompletedAt gate in the (app) layout. Existing accounts are backfilled as already-onboarded. Also gives the allergen-preferences step a real write path: user_allergens previously only had a GDPR-export read. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -497,6 +497,18 @@ export function generateOpenApiSpec(): object {
|
||||
caloriesKcal: z.number().int().min(0).optional(), proteinG: z.number().int().min(0).optional(),
|
||||
carbsG: z.number().int().min(0).optional(), fatG: z.number().int().min(0).optional(),
|
||||
}));
|
||||
const OnboardingStatusRef = registry.register("OnboardingStatus", z.object({
|
||||
completed: z.boolean(),
|
||||
dietaryTags: z.record(z.string(), z.boolean()),
|
||||
allergens: z.array(z.string()),
|
||||
}));
|
||||
const CompleteOnboardingRef = registry.register("CompleteOnboarding", z.object({
|
||||
dietaryTags: z.object({
|
||||
vegan: z.boolean().optional(), vegetarian: z.boolean().optional(), glutenFree: z.boolean().optional(),
|
||||
dairyFree: z.boolean().optional(), nutFree: z.boolean().optional(), halal: z.boolean().optional(), kosher: z.boolean().optional(),
|
||||
}).optional(),
|
||||
allergens: z.array(z.enum(["peanuts", "treeNuts", "dairy", "eggs", "shellfish", "fish", "soy", "gluten"])).optional(),
|
||||
}));
|
||||
const BillingDetailsRef = registry.register("BillingDetailsMe", z.object({
|
||||
id: z.string(), userId: z.string(),
|
||||
fullName: z.string().nullable(), addressLine1: z.string().nullable(), addressLine2: z.string().nullable(),
|
||||
@@ -558,6 +570,8 @@ export function generateOpenApiSpec(): object {
|
||||
registry.registerPath({ method: "put", path: "/api/v1/users/me/nutrition-goals", summary: "Set your daily nutrition goals", security, request: { body: { content: { "application/json": { schema: UpdateNutritionGoalsRef } }, required: true } }, responses: { 200: { description: "Saved", content: { "application/json": { schema: z.object({ ok: z.boolean() }) } } }, 400: { description: "Invalid request", content: { "application/json": { schema: ApiErrorRef } } }, 401: { description: "Unauthorized", content: { "application/json": { schema: ApiErrorRef } } } } });
|
||||
registry.registerPath({ method: "get", path: "/api/v1/users/me/billing-details", summary: "Get your billing/invoice details", description: "Full name, address, and phone used on invoices — separate from your display name.", security, responses: { 200: { description: "Billing details or null", content: { "application/json": { schema: z.object({ data: BillingDetailsRef }) } } }, 401: { description: "Unauthorized", content: { "application/json": { schema: ApiErrorRef } } } } });
|
||||
registry.registerPath({ method: "put", path: "/api/v1/users/me/billing-details", summary: "Set your billing/invoice details", description: "fullName is required; addressLine1/2, city, postalCode, country, and phone are all optional.", security, request: { body: { content: { "application/json": { schema: UpdateBillingDetailsRef } }, required: true } }, responses: { 200: { description: "Saved", content: { "application/json": { schema: z.object({ ok: z.boolean() }) } } }, 400: { description: "Invalid request", content: { "application/json": { schema: ApiErrorRef } } }, 401: { description: "Unauthorized", content: { "application/json": { schema: ApiErrorRef } } } } });
|
||||
registry.registerPath({ method: "get", path: "/api/v1/users/me/onboarding", summary: "Get your post-signup onboarding status", security, responses: { 200: { description: "Status", content: { "application/json": { schema: z.object({ data: OnboardingStatusRef }) } } }, 401: { description: "Unauthorized", content: { "application/json": { schema: ApiErrorRef } } } } });
|
||||
registry.registerPath({ method: "post", path: "/api/v1/users/me/onboarding", summary: "Complete (or skip) the post-signup onboarding wizard", description: "Marks onboarding done regardless of what's in the body — call with an empty body to skip entirely. dietaryTags/allergens are only written when provided.", security, request: { body: { content: { "application/json": { schema: CompleteOnboardingRef } } } }, responses: { 200: { description: "Completed", content: { "application/json": { schema: z.object({ ok: z.boolean() }) } } }, 400: { description: "Invalid request", content: { "application/json": { schema: ApiErrorRef } } }, 401: { description: "Unauthorized", content: { "application/json": { schema: ApiErrorRef } } } } });
|
||||
registry.registerPath({ method: "get", path: "/api/v1/users/search", summary: "Search users by name or username", description: "Excludes private accounts and any account you've blocked or that has blocked you. Requires at least 2 characters.", security, request: { query: z.object({ q: z.string().min(2).max(50) }) }, responses: { 200: { description: "Matches (up to 20)", content: { "application/json": { schema: z.object({ users: z.array(UserSearchResultRef) }) } } }, 401: { description: "Unauthorized", content: { "application/json": { schema: ApiErrorRef } } } } });
|
||||
registry.registerPath({ method: "get", path: "/api/v1/users/{username}", summary: "Get a public profile by username", security: [], request: { params: usernameParam }, responses: { 200: { description: "Profile", content: { "application/json": { schema: UserProfilePublicRef } } }, 404: { description: "Not found", content: { "application/json": { schema: ApiErrorRef } } } } });
|
||||
registry.registerPath({ method: "post", path: "/api/v1/users/{username}/follow", summary: "Follow a user", description: "Rate-limited: 30 req/min. Fails if either side has blocked the other.", security, request: { params: usernameParam }, responses: { 200: { description: "Following", content: { "application/json": { schema: z.object({ following: z.boolean() }) } } }, 400: { description: "Cannot follow yourself", content: { "application/json": { schema: ApiErrorRef } } }, 404: { description: "Not found", content: { "application/json": { schema: ApiErrorRef } } } } });
|
||||
|
||||
Reference in New Issue
Block a user