feat(onboarding): add welcome wizard scaffold + Step 1 (Your shop)
Lays the groundwork for the post-signup welcome wizard (Phase 2, Task 38). Authed users hitting /welcome are routed to the next incomplete step based on users.onboarding_step_completed + users.onboarding_dismissed; refresh resumes correctly because every navigation persists state server-side first. Backend: - Expose onboarding_step_completed (Optional[int]) and onboarding_dismissed (bool) on UserResponse so /auth/me drives client-side routing without a separate fetch. Frontend: - WelcomeRouter handles the /welcome decision table (dismissed → /, completed >=3 → /, else next step). - WelcomeStep1 renders the "Your shop" form (company name pre-filled from accounts.name, team size 1-2/3-5/6-10/11-25/26+, role Owner/Lead Tech/Tech/Other). Continue PATCHes /users/me/onboarding-step with action=complete; Skip-this-step PATCHes action=skip; Skip-the-rest POSTs /users/me/onboarding-dismiss-rest. Each action refreshes the auth store before navigating so the router resumes correctly on the next visit. - onboardingApi.updateStep + dismissRest (typed against backend OnboardingStepRequest/Response schemas). - Routes mounted inside AppLayout so EmailVerificationBanner persists above each step per spec. - 11 vitest cases covering the routing decision table + Continue / Skip / Skip-the-rest / persist-failure paths. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -19,3 +19,51 @@ export async function getOnboardingStatus(): Promise<OnboardingStatus> {
|
||||
export async function dismissOnboarding(): Promise<void> {
|
||||
await apiClient.post('/users/onboarding-status/dismiss')
|
||||
}
|
||||
|
||||
// --- Welcome wizard (Phase 2) ---------------------------------------------
|
||||
|
||||
export type WizardStep = 1 | 2 | 3
|
||||
export type WizardAction = 'complete' | 'skip'
|
||||
export type TeamSizeBucket = '1-2' | '3-5' | '6-10' | '11-25' | '26+'
|
||||
export type RoleAtSignup = 'owner' | 'lead_tech' | 'tech' | 'other'
|
||||
export type PrimaryPsa = 'connectwise' | 'autotask' | 'halopsa' | 'none'
|
||||
|
||||
export interface OnboardingStepData {
|
||||
// Step 1
|
||||
company_name?: string
|
||||
team_size_bucket?: TeamSizeBucket
|
||||
role_at_signup?: RoleAtSignup
|
||||
// Step 2
|
||||
primary_psa?: PrimaryPsa
|
||||
}
|
||||
|
||||
export interface OnboardingStepRequest {
|
||||
step: WizardStep
|
||||
action: WizardAction
|
||||
data?: OnboardingStepData
|
||||
}
|
||||
|
||||
export interface OnboardingStepResponse {
|
||||
onboarding_step_completed: number | null
|
||||
onboarding_dismissed: boolean
|
||||
}
|
||||
|
||||
export const onboardingApi = {
|
||||
getStatus: getOnboardingStatus,
|
||||
dismiss: dismissOnboarding,
|
||||
/** Persist welcome-wizard progress for the current user. */
|
||||
async updateStep(payload: OnboardingStepRequest): Promise<OnboardingStepResponse> {
|
||||
const response = await apiClient.patch<OnboardingStepResponse>(
|
||||
'/users/me/onboarding-step',
|
||||
payload,
|
||||
)
|
||||
return response.data
|
||||
},
|
||||
/** Skip the rest of the welcome wizard — sets users.onboarding_dismissed=TRUE. */
|
||||
async dismissRest(): Promise<OnboardingStepResponse> {
|
||||
const response = await apiClient.post<OnboardingStepResponse>(
|
||||
'/users/me/onboarding-dismiss-rest',
|
||||
)
|
||||
return response.data
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user