Skip to content

Commit d3887e4

Browse files
committed
chore: add use case step to onboarding
1 parent 9177cf6 commit d3887e4

12 files changed

Lines changed: 2580 additions & 7 deletions

File tree

apps/acme/next-env.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/// <reference types="next" />
22
/// <reference types="next/image-types/global" />
3+
import "./.next/dev/types/routes.d.ts";
34

45
// NOTE: This file should not be edited
56
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

apps/acme/tsconfig.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
"moduleResolution": "bundler",
1515
"resolveJsonModule": true,
1616
"isolatedModules": true,
17-
"jsx": "preserve",
17+
"jsx": "react-jsx",
1818
"incremental": true,
1919
"plugins": [
2020
{
@@ -32,7 +32,8 @@
3232
"next-env.d.ts",
3333
"**/*.ts",
3434
"**/*.tsx",
35-
".next/types/**/*.ts"
35+
".next/types/**/*.ts",
36+
".next/dev/types/**/*.ts"
3637
],
3738
"exclude": [
3839
"node_modules"

apps/webapp/src/app/(authenticated)/onboarding/page.tsx

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,15 @@ import {
1818
import { ProductInfoStep } from "@/components/onboarding/product-info-step";
1919
import { AppTypeStep } from "@/components/onboarding/app-type-step";
2020
import { PaymentProviderStep } from "@/components/onboarding/payment-provider-step";
21+
import { UseCaseStep } from "@/components/onboarding/use-case-step";
2122
import {
2223
OnboardingFormValues,
2324
useOnboardingForm,
2425
} from "@/lib/forms/onboarding-form";
2526
import { onboardingSchema } from "@/lib/validations/onboarding";
2627
import z from "zod";
2728
import { appTypes } from "@/lib/validations/onboarding";
29+
import { useCases } from "@/lib/validations/onboarding";
2830
import { paymentProviders } from "@/lib/validations/onboarding";
2931

3032
const steps = [
@@ -34,10 +36,14 @@ const steps = [
3436
},
3537
{
3638
id: 2,
37-
title: "App Type",
39+
title: "Use Case",
3840
},
3941
{
4042
id: 3,
43+
title: "App Type",
44+
},
45+
{
46+
id: 4,
4147
title: "Payment Provider",
4248
},
4349
];
@@ -63,6 +69,7 @@ export default function OnboardingPage() {
6369
productName: "",
6470
productUrl: "",
6571
appType: "saas",
72+
useCase: [],
6673
paymentProvider: "stripe",
6774
otherPaymentProvider: "",
6875
} as z.input<typeof onboardingSchema>,
@@ -80,6 +87,7 @@ export default function OnboardingPage() {
8087
name: value.productName,
8188
url,
8289
appType: value.appType,
90+
useCase: value.useCase,
8391
paymentProvider: value.paymentProvider,
8492
otherPaymentProvider: value.otherPaymentProvider,
8593
});
@@ -198,6 +206,18 @@ export default function OnboardingPage() {
198206
/>
199207
)}
200208
{currentStep === 2 && (
209+
<UseCaseStep
210+
form={form}
211+
fields={{ useCase: "useCase" }}
212+
onNext={handleNext}
213+
onPrevious={handlePrevious}
214+
isFirstStep={false}
215+
isLastStep={false}
216+
isSubmitting={createProduct.isPending}
217+
submitButtonRef={submitButtonRef}
218+
/>
219+
)}
220+
{currentStep === 3 && (
201221
<AppTypeStep
202222
form={form}
203223
fields={{ appType: "appType" }}
@@ -209,7 +229,7 @@ export default function OnboardingPage() {
209229
submitButtonRef={submitButtonRef}
210230
/>
211231
)}
212-
{currentStep === 3 && (
232+
{currentStep === 4 && (
213233
<PaymentProviderStep
214234
form={form}
215235
fields={{
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
"use client";
2+
3+
import React from "react";
4+
import { withFieldGroup } from "@/lib/forms/onboarding-form";
5+
import {
6+
useCaseLabels,
7+
useCaseDescriptions,
8+
useCases,
9+
useCaseSchema,
10+
} from "@/lib/validations/onboarding";
11+
import { Button } from "@refref/ui/components/button";
12+
import { Checkbox } from "@refref/ui/components/checkbox";
13+
import { Label } from "@refref/ui/components/label";
14+
import { ChevronLeft, ChevronRight } from "lucide-react";
15+
16+
export const UseCaseStep = withFieldGroup({
17+
defaultValues: {
18+
useCase: [] as (typeof useCases)[number][],
19+
},
20+
props: {
21+
onNext: () => {},
22+
onPrevious: () => {},
23+
isFirstStep: false,
24+
isLastStep: false,
25+
isSubmitting: false,
26+
submitButtonRef: {
27+
current: null,
28+
} as React.RefObject<HTMLButtonElement | null>,
29+
},
30+
render: function Render({
31+
group,
32+
onNext,
33+
onPrevious,
34+
isFirstStep,
35+
isLastStep,
36+
isSubmitting,
37+
submitButtonRef,
38+
}) {
39+
const onSubmit = async () => {
40+
group.form.setFieldMeta("*", (m) => ({ ...m, errors: [] }));
41+
const errors = await group.validateAllFields("change");
42+
if (errors.length > 0) {
43+
return;
44+
}
45+
onNext();
46+
};
47+
48+
const onBefore = () => {
49+
Object.values(group.fieldsMap).forEach((field) => {
50+
group.form.setFieldMeta(field, (m) => ({
51+
...m,
52+
errors: [],
53+
errorMap: {},
54+
}));
55+
});
56+
57+
onPrevious();
58+
};
59+
60+
return (
61+
<div className="space-y-6">
62+
<div className="space-y-2">
63+
<h2 className="text-2xl font-semibold">
64+
Which programs are you interested in?
65+
</h2>
66+
<p className="text-muted-foreground">
67+
Choose all that apply. You can change this later.
68+
</p>
69+
</div>
70+
71+
<div className="space-y-3">
72+
<group.AppField
73+
name="useCase"
74+
validators={{
75+
onChange: useCaseSchema.shape.useCase,
76+
}}
77+
>
78+
{(field) => {
79+
const value = field.state.value ?? [];
80+
81+
const toggleOption = (option: (typeof useCases)[number]) => {
82+
const next = value.includes(option)
83+
? value.filter((v) => v !== option)
84+
: [...value, option];
85+
field.handleChange(next);
86+
};
87+
88+
return (
89+
<>
90+
{useCases.map((option) => (
91+
<Label
92+
key={option}
93+
htmlFor={`usecase-${option}`}
94+
className="flex items-start space-x-3 rounded-lg border p-4 hover:bg-muted/50 cursor-pointer transition-colors"
95+
data-testid={`checkbox-option-${option}`}
96+
>
97+
<Checkbox
98+
id={`usecase-${option}`}
99+
checked={value.includes(option)}
100+
onCheckedChange={() => toggleOption(option)}
101+
className="mt-0.5"
102+
/>
103+
<div className="flex-1 space-y-1">
104+
<span className="font-medium">
105+
{useCaseLabels[option]}
106+
</span>
107+
<p className="text-sm text-muted-foreground">
108+
{useCaseDescriptions[option]}
109+
</p>
110+
</div>
111+
</Label>
112+
))}
113+
{field.state.meta.errors &&
114+
field.state.meta.errors.length > 0 && (
115+
<p className="text-sm text-destructive mt-2">
116+
{typeof field.state.meta.errors[0] === "string"
117+
? field.state.meta.errors[0]
118+
: field.state.meta.errors[0]?.message ||
119+
"Invalid value"}
120+
</p>
121+
)}
122+
</>
123+
);
124+
}}
125+
</group.AppField>
126+
</div>
127+
{/* Navigation Buttons */}
128+
<div className="flex justify-between pt-6 border-t">
129+
<Button
130+
type="button"
131+
variant="outline"
132+
onClick={onBefore}
133+
disabled={isFirstStep}
134+
data-testid="onboarding-previous-btn"
135+
>
136+
<ChevronLeft className="h-4 w-4 mr-2" />
137+
Previous
138+
</Button>
139+
140+
<Button
141+
ref={submitButtonRef}
142+
type="button"
143+
onClick={onSubmit}
144+
disabled={isSubmitting}
145+
data-testid="onboarding-next-btn"
146+
>
147+
{isLastStep
148+
? isSubmitting
149+
? "Creating..."
150+
: "Complete Setup"
151+
: "Next"}
152+
{!isLastStep && <ChevronRight className="h-4 w-4 ml-2" />}
153+
</Button>
154+
</div>
155+
</div>
156+
);
157+
},
158+
});

apps/webapp/src/lib/forms/onboarding-form.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,18 @@ import { createFormHookContexts, createFormHook } from "@tanstack/react-form";
44
import { Input } from "@refref/ui/components/input";
55
import { Label } from "@refref/ui/components/label";
66
import { RadioGroup, RadioGroupItem } from "@refref/ui/components/radio-group";
7-
import type { appTypes, paymentProviders } from "@/lib/validations/onboarding";
7+
import type {
8+
appTypes,
9+
useCases,
10+
paymentProviders,
11+
} from "@/lib/validations/onboarding";
812

913
// Define form values type
1014
export type OnboardingFormValues = {
1115
productName: string;
1216
productUrl: string;
1317
appType: (typeof appTypes)[number] | undefined;
18+
useCase: (typeof useCases)[number][];
1419
paymentProvider: (typeof paymentProviders)[number] | undefined;
1520
otherPaymentProvider: string | undefined;
1621
};

apps/webapp/src/lib/validations/onboarding.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,14 @@ export const appTypes = [
99
"other",
1010
] as const;
1111

12+
export const useCases = ["referrals", "affiliates"] as const;
13+
14+
export const useCaseDescriptions: Record<(typeof useCases)[number], string> = {
15+
referrals: "Let customers invite friends from inside your product.",
16+
affiliates:
17+
"Manage partner and creator referrals with links, attribution, and payouts.",
18+
};
19+
1220
export const paymentProviders = [
1321
"stripe",
1422
"chargebee",
@@ -37,6 +45,9 @@ export const onboardingSchema = z
3745
appType: z.enum(appTypes, {
3846
message: "Please select an app type",
3947
}),
48+
useCase: z
49+
.array(z.enum(useCases))
50+
.min(1, "Please select at least one option"),
4051
paymentProvider: z.enum(paymentProviders, {
4152
message: "Please select a payment provider",
4253
}),
@@ -72,6 +83,10 @@ export const appTypeSchema = onboardingSchema.pick({
7283
appType: true,
7384
});
7485

86+
export const useCaseSchema = onboardingSchema.pick({
87+
useCase: true,
88+
});
89+
7590
export const paymentProviderSchema = onboardingSchema
7691
.pick({ paymentProvider: true, otherPaymentProvider: true })
7792
.refine(
@@ -92,11 +107,13 @@ export const paymentProviderSchema = onboardingSchema
92107

93108
export type ProductInfoFormData = z.infer<typeof productInfoSchema>;
94109
export type AppTypeFormData = z.infer<typeof appTypeSchema>;
110+
export type UseCaseFormData = z.infer<typeof useCaseSchema>;
95111
export type PaymentProviderFormData = z.infer<typeof paymentProviderSchema>;
96112
export type OnboardingFormData = z.infer<typeof onboardingSchema>;
97113

98114
// Type helpers for optional fields
99115
export type AppType = (typeof appTypes)[number];
116+
export type UseCase = (typeof useCases)[number];
100117
export type PaymentProvider = (typeof paymentProviders)[number];
101118

102119
export const appTypeLabels: Record<(typeof appTypes)[number], string> = {
@@ -108,6 +125,11 @@ export const appTypeLabels: Record<(typeof appTypes)[number], string> = {
108125
other: "Other",
109126
};
110127

128+
export const useCaseLabels: Record<(typeof useCases)[number], string> = {
129+
referrals: "Customer Referrals",
130+
affiliates: "Affiliate Program",
131+
};
132+
111133
export const paymentProviderLabels: Record<
112134
(typeof paymentProviders)[number],
113135
string

apps/webapp/src/server/api/routers/product.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@ import { createId, init } from "@paralleldrive/cuid2";
1111
import { randomBytes } from "crypto";
1212
import { auth } from "@/lib/auth";
1313
import { headers } from "next/headers";
14-
import { appTypes, paymentProviders } from "@/lib/validations/onboarding";
14+
import {
15+
appTypes,
16+
useCases,
17+
paymentProviders,
18+
} from "@/lib/validations/onboarding";
1519
import { and, eq } from "drizzle-orm";
1620

1721
const slugGenerator = init({
@@ -30,6 +34,7 @@ export const createProductWithOnboardingSchema = z
3034
name: z.string().min(1, "Name is required").max(100, "Name is too long"),
3135
url: z.string().min(1, "URL is required"),
3236
appType: z.enum(appTypes),
37+
useCase: z.array(z.enum(useCases)).min(1),
3338
paymentProvider: z.enum(paymentProviders),
3439
otherPaymentProvider: z.string().optional(),
3540
})
@@ -130,12 +135,13 @@ export const productRouter = createTRPCRouter({
130135
slug: slugGenerator(),
131136
orgId: activeOrgId,
132137
appType: input.appType,
138+
useCase: input.useCase.join(","),
133139
paymentProvider:
134140
input.paymentProvider === "other"
135141
? input.otherPaymentProvider || "other"
136142
: input.paymentProvider,
137143
onboardingCompleted: true,
138-
onboardingStep: 4,
144+
onboardingStep: 5,
139145
})
140146
.returning();
141147

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ALTER TABLE "product" ADD COLUMN "use_case" text;

0 commit comments

Comments
 (0)