-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Remove the program importers from program onboarding flow #2823
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughThis PR removes Rewardful/Tolt/PartnerStack importer UI and server actions, flattens the program reward model to { defaultRewardType, type, amount, maxDuration }, simplifies onboarding forms (rewards & partners), and stops normalizing programType/importSource during form save and program creation. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User
participant RewardsForm as Rewards Form (client)
participant Server as Server (create-program)
participant DB as Database
User->>RewardsForm: choose defaultRewardType, type, amount, maxDuration
RewardsForm->>Server: POST flattened payload + program data
Server->>DB: create program (uses generated programId), create group, persist reward
DB-->>Server: ids persisted
Server-->>RewardsForm: success
RewardsForm-->>User: navigate next step
sequenceDiagram
autonumber
actor User
participant PartnersForm as Partners Form (client)
participant Store as Onboarding Store (server)
User->>PartnersForm: add/remove partner emails
PartnersForm->>Store: submit partners (filters empty emails), set step=invite-partners
Store-->>PartnersForm: ack
PartnersForm-->>User: advance to invite step
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Pre-merge checks (2 passed, 1 warning)❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
Poem
✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 7
🧹 Nitpick comments (4)
apps/web/app/(ee)/app.dub.co/(new-program)/[slug]/program/new/partners/form.tsx (1)
70-79: A11y: associate label with inputUse a label with htmlFor and provide an id on the input for better accessibility.
- <span className="mb-1.5 block text-sm font-medium text-neutral-800"> - Email - </span> + <label + htmlFor={`partner-email-${index}`} + className="mb-1.5 block text-sm font-medium text-neutral-800" + > + Email + </label> ... - <Input + <Input + id={`partner-email-${index}`} {...register(`partners.${index}.email`)} type="email" placeholder="[email protected]"apps/web/lib/actions/partners/create-program.ts (1)
58-60: Pre-generating IDs inside the txn is fine, but consider moving ID gen outside.Generating
programIdearly enables deterministic logo paths—nice. However, you can move ID generation (and the logo upload) outside the Prisma transaction to avoid holding the DB transaction open during network I/O.Apply this diff to drop in-txn ID gen:
- const programId = createId({ prefix: "prog_" }); - const defaultGroupId = createId({ prefix: "grp_" });Then, above
prisma.$transaction(...), precompute IDs and (optionally) upload the logo:// before prisma.$transaction const programId = createId({ prefix: "prog_" }); const defaultGroupId = createId({ prefix: "grp_" }); const logoUrl = uploadedLogo ? (await storage .upload(`programs/${programId}/logo_${nanoid(7)}`, uploadedLogo) .then(({ url }) => url)) : null; // inside txn: use the precomputed programId/defaultGroupId/logoUrlIf you move the upload out, consider best-effort cleanup on txn failure (e.g., try/catch around the txn and delete the uploaded key on error).
apps/web/app/(ee)/app.dub.co/(new-program)/[slug]/program/new/rewards/form.tsx (2)
50-56: Make commission structure derived, not duplicated state.
commissionStructureis derived frommaxDurationand also manually set, creating two sources of truth. Derive it directly and update onlymaxDurationon radio change.Apply this diff:
- const [commissionStructure, setCommissionStructure] = useState< - "one-off" | "recurring" - >("recurring"); - - useEffect(() => { - setCommissionStructure(maxDuration === 0 ? "one-off" : "recurring"); - }, [maxDuration]); + const commissionStructure: "one-off" | "recurring" = + maxDuration === 0 ? "one-off" : "recurring";And here:
- onChange={(e) => { - if (value === "one-off") { - setCommissionStructure("one-off"); - setValue("maxDuration", 0, { shouldValidate: true }); - } - - if (value === "recurring") { - setCommissionStructure("recurring"); - setValue("maxDuration", 12, { - shouldValidate: true, - }); - } - }} + onChange={() => { + setValue( + "maxDuration", + value === "one-off" ? 0 : 12, + { shouldValidate: true }, + ); + }}If you adopt this, remove the unused
useEffectimport.Also applies to: 158-189
270-270: Use strict equality.Prefer
!==over!=to avoid coercion.- Amount {defaultRewardType != "sale" ? "per lead" : ""} + Amount {defaultRewardType !== "sale" ? "per lead" : ""}
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (11)
apps/web/app/(ee)/app.dub.co/(new-program)/[slug]/program/new/overview/page-client.tsx(2 hunks)apps/web/app/(ee)/app.dub.co/(new-program)/[slug]/program/new/partners/form.tsx(1 hunks)apps/web/app/(ee)/app.dub.co/(new-program)/[slug]/program/new/rewards/form.tsx(5 hunks)apps/web/app/(ee)/app.dub.co/(new-program)/[slug]/program/new/rewards/import-partnerstack-form.tsx(0 hunks)apps/web/app/(ee)/app.dub.co/(new-program)/[slug]/program/new/rewards/import-rewardful-form.tsx(0 hunks)apps/web/app/(ee)/app.dub.co/(new-program)/[slug]/program/new/rewards/import-tolt-form.tsx(0 hunks)apps/web/app/(ee)/app.dub.co/(new-program)/form-wrapper.tsx(0 hunks)apps/web/app/(ee)/app.dub.co/(new-program)/header.tsx(0 hunks)apps/web/lib/actions/partners/create-program.ts(1 hunks)apps/web/lib/actions/partners/set-partnerstack-token.ts(0 hunks)apps/web/lib/zod/schemas/program-onboarding.ts(1 hunks)
💤 Files with no reviewable changes (6)
- apps/web/app/(ee)/app.dub.co/(new-program)/form-wrapper.tsx
- apps/web/lib/actions/partners/set-partnerstack-token.ts
- apps/web/app/(ee)/app.dub.co/(new-program)/[slug]/program/new/rewards/import-rewardful-form.tsx
- apps/web/app/(ee)/app.dub.co/(new-program)/[slug]/program/new/rewards/import-tolt-form.tsx
- apps/web/app/(ee)/app.dub.co/(new-program)/[slug]/program/new/rewards/import-partnerstack-form.tsx
- apps/web/app/(ee)/app.dub.co/(new-program)/header.tsx
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-07-30T15:29:54.131Z
Learnt from: TWilson023
PR: dubinc/dub#2673
File: apps/web/ui/partners/rewards/rewards-logic.tsx:268-275
Timestamp: 2025-07-30T15:29:54.131Z
Learning: In apps/web/ui/partners/rewards/rewards-logic.tsx, when setting the entity field in a reward condition, dependent fields (attribute, operator, value) should be reset rather than preserved because different entities (customer vs sale) have different available attributes. Maintaining existing fields when the entity changes would create invalid state combinations and confusing UX.
Applied to files:
apps/web/app/(ee)/app.dub.co/(new-program)/[slug]/program/new/rewards/form.tsxapps/web/app/(ee)/app.dub.co/(new-program)/[slug]/program/new/overview/page-client.tsx
📚 Learning: 2025-07-30T15:25:13.936Z
Learnt from: TWilson023
PR: dubinc/dub#2673
File: apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx:56-66
Timestamp: 2025-07-30T15:25:13.936Z
Learning: In apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx, the form schema uses partial condition objects to allow users to add empty/unconfigured condition fields without type errors, while submission validation uses strict schemas to ensure data integrity. This two-stage validation pattern improves UX by allowing progressive completion of complex forms.
Applied to files:
apps/web/app/(ee)/app.dub.co/(new-program)/[slug]/program/new/rewards/form.tsxapps/web/app/(ee)/app.dub.co/(new-program)/[slug]/program/new/overview/page-client.tsxapps/web/lib/zod/schemas/program-onboarding.ts
🧬 Code graph analysis (3)
apps/web/app/(ee)/app.dub.co/(new-program)/[slug]/program/new/rewards/form.tsx (2)
apps/web/lib/zod/schemas/rewards.ts (1)
COMMISSION_TYPES(5-16)apps/web/lib/zod/schemas/misc.ts (1)
RECURRING_MAX_DURATIONS(6-6)
apps/web/lib/actions/partners/create-program.ts (1)
apps/web/lib/api/create-id.ts (1)
createId(60-69)
apps/web/lib/zod/schemas/program-onboarding.ts (2)
packages/prisma/client.ts (1)
RewardStructure(22-22)apps/web/lib/zod/schemas/misc.ts (1)
maxDurationSchema(56-61)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (1)
apps/web/app/(ee)/app.dub.co/(new-program)/[slug]/program/new/rewards/form.tsx (1)
121-124: Good reset of dependent fields when switching to “Lead”.Forcing
type="flat"andmaxDuration=0on “Lead” matches the learned UX pattern of resetting dependent fields when the controlling field changes.
apps/web/app/(ee)/app.dub.co/(new-program)/[slug]/program/new/overview/page-client.tsx
Show resolved
Hide resolved
apps/web/app/(ee)/app.dub.co/(new-program)/[slug]/program/new/overview/page-client.tsx
Show resolved
Hide resolved
apps/web/app/(ee)/app.dub.co/(new-program)/[slug]/program/new/partners/form.tsx
Show resolved
Hide resolved
apps/web/app/(ee)/app.dub.co/(new-program)/[slug]/program/new/partners/form.tsx
Show resolved
Hide resolved
apps/web/app/(ee)/app.dub.co/(new-program)/[slug]/program/new/rewards/form.tsx
Show resolved
Hide resolved
apps/web/app/(ee)/app.dub.co/(new-program)/[slug]/program/new/rewards/form.tsx
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (4)
apps/web/scripts/perplexity/backfill-leads.ts (4)
165-195: Critical: index misalignment corrupts customer↔lead↔click associationsFiltering out nulls in
clicksToCreate(Line 165) shifts indices; later you index intoclicksToCreate[idx]while iteratingleadsToBackfilland then filter customers independently, causing mismatches and potential wrongcustomer_idon events.Fix by preserving alignment and filtering only at the end:
- ).then((res) => res.filter((r) => r !== null)); + ); @@ - const customersToCreate: Prisma.CustomerCreateManyInput[] = - leadsToBackfill - .map((lead, idx) => { - const clickData = clicksToCreate[idx]; - if (!clickData) { - return null; - } - return { + const customersToCreate = + leadsToBackfill.map((lead, idx): Prisma.CustomerCreateManyInput | null => { + const clickData = clicksToCreate[idx]; + if (!clickData) return null; + return { id: createId({ prefix: "cus_" }), name: generateRandomName(), externalId: lead.customerExternalId, projectId: workspace.id, projectConnectId: workspace.stripeConnectId, clickId: clickData.click_id, linkId: clickData.link_id, country: clickData.country, clickedAt: new Date(lead.timestamp).toISOString(), createdAt: new Date(lead.timestamp).toISOString(), - }; - }) - .filter((c) => c !== null); + }; + }); @@ - const leadsToCreate = clicksToCreate.map((clickData, idx) => ({ - ...clickData, - event_id: nanoid(16), - event_name: "activated", - customer_id: customersToCreate[idx]!.id, - })); + const leadsToCreate = clicksToCreate + .map((clickData, idx) => { + const customer = customersToCreate[idx]; + if (!clickData || !customer) return null; + return { + ...clickData, + event_id: nanoid(16), + event_name: "activated", + customer_id: customer.id, + }; + }) + .filter((e): e is NonNullable<typeof e> => e !== null);
121-126: Ensure header value is always a string
COUNTRIES_TO_CONTINENTS[...]can be undefined, which is invalid in Headers.- "x-vercel-ip-continent": - COUNTRIES_TO_CONTINENTS[lead.country.toUpperCase()], + "x-vercel-ip-continent": + COUNTRIES_TO_CONTINENTS[lead.country.toUpperCase()] ?? "",
147-147: Guardcapitalizecall against undefined UA type
ua.device.typecan be undefined;capitalize(undefined)will throw.- device: capitalize(ua.device.type) || "Desktop", + device: ua.device.type ? capitalize(ua.device.type) : "Desktop",
10-14: Critical: Fix missing Edge/Next imports in Node script
Imports of@vercel/functionsandnext/serverin apps/web/scripts/perplexity/backfill-leads.ts (lines 10–14, 130–132) fail at runtime (Error: Cannot find module). Remove or replace these with Node-compatible APIs or install/bundle the proper packages so the script can run successfully.
🧹 Nitpick comments (3)
apps/web/scripts/perplexity/backfill-leads.ts (3)
1-2: Avoid blanket TypeScript suppression; prefer targeted typing fixes
// @ts-nocheckhides real bugs (see index misalignment comment below). Suggest removing it and typing the CSV parse callback instead.Apply minimally:
-// @ts-nocheck + +type CSVRow = { + CUSTOM_USER_ID: string; + CAMPAIGN_NAME: string; + COUNTRY: string; + ADJUSTED_TIMESTAMP: string; +}; @@ - Papa.parse(fs.createReadStream("perplexity_leads.csv", "utf-8"), { + Papa.parse<CSVRow>(fs.createReadStream("perplexity_leads.csv", "utf-8"), { @@ - step: (result: { - data: { - CUSTOM_USER_ID: string; - CAMPAIGN_NAME: string; - COUNTRY: string; - ADJUSTED_TIMESTAMP: string; - }; - }) => { - leadsToBackfill.push({ - customerExternalId: result.data.CUSTOM_USER_ID, - partnerLinkKey: result.data.CAMPAIGN_NAME, - country: result.data.COUNTRY, - timestamp: result.data.ADJUSTED_TIMESTAMP, - }); - }, + step: ({ data }) => { + leadsToBackfill.push({ + customerExternalId: data.CUSTOM_USER_ID, + partnerLinkKey: data.CAMPAIGN_NAME, + country: data.COUNTRY, + timestamp: data.ADJUSTED_TIMESTAMP, + }); + },
51-58: Avoid repeated O(n) searches overpartnerLinksYou do multiple
.find/.somescans by key/id; build a Map for O(1) lookups.+ const linksByLowerKey = new Map(partnerLinks.map(l => [l.key.toLowerCase(), l])); + const linksById = new Map(partnerLinks.map(l => [l.id, l])); @@ - .filter((lead) => - partnerLinks.some( - (link) => - link.key.toLowerCase() === lead.partnerLinkKey.toLowerCase(), - ), - ) + .filter((lead) => linksByLowerKey.has(lead.partnerLinkKey.toLowerCase())) @@ - const link = partnerLinks.find( - (link) => - link.key.toLowerCase() === lead.partnerLinkKey.toLowerCase(), - ); + const link = linksByLowerKey.get(lead.partnerLinkKey.toLowerCase()); @@ - const link = partnerLinks.find( - (link) => link.id === leadData.link_id, - ); + const link = linksById.get(leadData.link_id);Also applies to: 60-73, 112-116, 221-224
196-201: Externalize hard-coded IDs
project.idandreward.idliterals make this script environment-specific.- const workspace = await prisma.project.findUniqueOrThrow({ + const workspace = await prisma.project.findUniqueOrThrow({ where: { - id: "xxx", + id: process.env.PROJECT_ID ?? "xxx", }, }); @@ - const leadReward = await prisma.reward.findUniqueOrThrow({ + const leadReward = await prisma.reward.findUniqueOrThrow({ where: { - id: "rw_1K1KTWJPS14SEENXFHE0FYN7J", + id: process.env.LEAD_REWARD_ID ?? "rw_1K1KTWJPS14SEENXFHE0FYN7J", }, });
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
apps/web/scripts/perplexity/backfill-leads.ts(1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Vade Review
🔇 Additional comments (1)
apps/web/scripts/perplexity/backfill-leads.ts (1)
196-216: ConfirmdeterminePartnerReward+EventType.leadstill match the refactored reward schemaGiven the PR consolidates reward shapes, ensure the function’s input contract and enum cases are still valid to avoid silent
undefinedat runtime masked by@ts-nocheck.Run:
apps/web/app/(ee)/app.dub.co/(new-program)/[slug]/program/new/partners/form.tsx
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
♻️ Duplicate comments (3)
apps/web/lib/zod/schemas/program-onboarding.ts (1)
19-24: Coerce reward amount to number (form inputs send strings)Switch to
z.coerce.number()to accept numeric strings from forms. This was flagged earlier.export const programRewardSchema = z.object({ defaultRewardType: z.enum(["lead", "sale"]).default("lead"), type: z.nativeEnum(RewardStructure).nullish(), - amount: z.number().min(0).nullish(), + amount: z.coerce.number().min(0).nullish(), maxDuration: maxDurationSchema, -}); +});apps/web/app/(ee)/app.dub.co/(new-program)/[slug]/program/new/partners/form.tsx (2)
39-47: Unstick the form after failed submit + add fallback error toastReset
hasSubmittedon errors so users can retry; also guard against undefinedserverError. Previously noted.const { executeAsync, isPending } = useAction(onboardProgramAction, { onSuccess: () => { router.push(`/${workspaceSlug}/program/new/support`); mutate(); }, onError: ({ error }) => { - toast.error(error.serverError); + setHasSubmitted(false); + toast.error(error.serverError ?? "We couldn’t save your invites. Please try again."); }, });
93-106: Prevent accidental submit from "Add partner" and focus new inputSet
type="button"to avoid submitting the form; focus the appended field for faster entry. Previously noted.<div className="mb-4"> <Button text="Add partner" variant="secondary" icon={<Plus className="size-4" />} className="w-fit" + type="button" onClick={() => { if (fields.length < PROGRAM_ONBOARDING_PARTNERS_LIMIT) { - append({ email: "" }); + append({ email: "" }, { shouldFocus: true }); } }} disabled={fields.length >= PROGRAM_ONBOARDING_PARTNERS_LIMIT} /> </div>
🧹 Nitpick comments (3)
apps/web/lib/zod/schemas/program-onboarding.ts (2)
19-24: Add cross-field validation: require amount and type togetherPrevents partial configurations like amount without type (or vice versa).
-export const programRewardSchema = z.object({ +export const programRewardSchema = z.object({ defaultRewardType: z.enum(["lead", "sale"]).default("lead"), type: z.nativeEnum(RewardStructure).nullish(), - amount: z.number().min(0).nullish(), + amount: z.coerce.number().min(0).nullish(), maxDuration: maxDurationSchema, -}); +}) + .superRefine((val, ctx) => { + const hasAmount = val.amount != null; + const hasType = val.type != null; + if (hasAmount !== hasType) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: + "When specifying a reward amount, select a reward type (and vice versa).", + path: hasAmount ? ["type"] : ["amount"], + }); + } + });
27-33: Trim partner emails in-field to reduce false validation failuresLeading/trailing spaces cause
.email()to fail; trim before validation.- z.object({ - email: z.string().email("Please enter a valid email"), - }), + z.object({ + email: z.string().trim().email("Please enter a valid email"), + }),apps/web/app/(ee)/app.dub.co/(new-program)/[slug]/program/new/partners/form.tsx (1)
108-113: Optional: show remaining invites instead of only the capSmall UX nudge.
- {fields.length >= PROGRAM_ONBOARDING_PARTNERS_LIMIT && ( - <p className="text-sm text-neutral-600"> - You can only invite up to {PROGRAM_ONBOARDING_PARTNERS_LIMIT}{" "} - partners. - </p> - )} + <p className="text-sm text-neutral-600"> + {PROGRAM_ONBOARDING_PARTNERS_LIMIT - fields.length} of{" "} + {PROGRAM_ONBOARDING_PARTNERS_LIMIT} partner invites remaining. + </p>
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
apps/web/app/(ee)/app.dub.co/(new-program)/[slug]/program/new/overview/page-client.tsx(2 hunks)apps/web/app/(ee)/app.dub.co/(new-program)/[slug]/program/new/partners/form.tsx(2 hunks)apps/web/lib/partners/constants.ts(1 hunks)apps/web/lib/zod/schemas/program-onboarding.ts(3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- apps/web/app/(ee)/app.dub.co/(new-program)/[slug]/program/new/overview/page-client.tsx
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-07-30T15:25:13.936Z
Learnt from: TWilson023
PR: dubinc/dub#2673
File: apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx:56-66
Timestamp: 2025-07-30T15:25:13.936Z
Learning: In apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx, the form schema uses partial condition objects to allow users to add empty/unconfigured condition fields without type errors, while submission validation uses strict schemas to ensure data integrity. This two-stage validation pattern improves UX by allowing progressive completion of complex forms.
Applied to files:
apps/web/lib/zod/schemas/program-onboarding.ts
🧬 Code graph analysis (2)
apps/web/lib/zod/schemas/program-onboarding.ts (2)
apps/web/lib/zod/schemas/misc.ts (1)
maxDurationSchema(56-61)apps/web/lib/partners/constants.ts (1)
PROGRAM_ONBOARDING_PARTNERS_LIMIT(4-4)
apps/web/app/(ee)/app.dub.co/(new-program)/[slug]/program/new/partners/form.tsx (1)
apps/web/lib/partners/constants.ts (1)
PROGRAM_ONBOARDING_PARTNERS_LIMIT(4-4)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: Vade Review
- GitHub Check: build
🔇 Additional comments (4)
apps/web/lib/partners/constants.ts (2)
4-4: Centralize the invite-partners cap — LGTMSingle source of truth for the onboarding partners limit. Matches schema/UI usage in this PR.
69-88: PROGRAM_IMPORT_SOURCES is still in use
It’s imported inapps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/import-export-buttons.tsx(line 3) and mapped at line 40, so it cannot be removed.Likely an incorrect or invalid review comment.
apps/web/lib/zod/schemas/program-onboarding.ts (2)
1-1: Importing the limit from constants keeps UI/server aligned — LGTM
34-37: Schema/UI limit now sourced from constant — LGTMDynamic error message reflects the constant; avoids future drift.
Summary by CodeRabbit
New Features
Refactor