Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@devkiran
Copy link
Collaborator

@devkiran devkiran commented Sep 10, 2025

Summary by CodeRabbit

  • New Features

    • Streamlined Rewards setup: single guided form for reward type (sale/lead), payout (flat/%), amount, and duration; Continue enabled only when required fields are provided.
    • Redesigned Partners step: add/remove partner emails with improved accessibility, capacity messaging, and a 5‑partner limit.
  • Refactor

    • Removed third‑party import flows and their UI paths (Rewardful, Tolt, PartnerStack).
    • Simplified onboarding validation, reward schema, and save behavior for more consistent payloads.

@vercel
Copy link
Contributor

vercel bot commented Sep 10, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Updated (UTC)
dub Ready Ready Preview Sep 10, 2025 5:21am

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 10, 2025

Walkthrough

This 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

Cohort / File(s) Summary
Flattened reward model & overview
apps/web/lib/zod/schemas/program-onboarding.ts, apps/web/app/(ee)/app.dub.co/(new-program)/[slug]/program/new/overview/page-client.tsx, apps/web/app/(ee)/app.dub.co/(new-program)/[slug]/program/new/rewards/form.tsx
Replace multi-source reward schema with a flat shape (defaultRewardType, type, amount, maxDuration); remove programType/importSource logic; event/reward construction now uses defaultRewardType and simplified amount handling (flat -> cents).
Importer component removals
apps/web/app/(ee)/app.dub.co/(new-program)/[slug]/program/new/rewards/import-rewardful-form.tsx, .../import-tolt-form.tsx, .../import-partnerstack-form.tsx
Delete Rewardful, Tolt, and PartnerStack client import forms and their exports.
PartnerStack server action removed
apps/web/lib/actions/partners/set-partnerstack-token.ts
Remove setPartnerStackTokenAction server action that validated/persisted PartnerStack credentials.
Create program flow cleanup
apps/web/lib/actions/partners/create-program.ts
Remove importer queuing and importer-related fields from onboarding parsing; move programId/defaultGroupId generation earlier; retain program/group creation, rewards linking, invites, emails, and audit logging.
Partners onboarding UI
apps/web/app/(ee)/app.dub.co/(new-program)/[slug]/program/new/partners/form.tsx
Replace affiliate-import UI with a dynamic email list using useFieldArray; add/remove rows, enforce PROGRAM_ONBOARDING_PARTNERS_LIMIT, submit filters empty emails and advances step.
Form wrapper & header changes
apps/web/app/(ee)/app.dub.co/(new-program)/form-wrapper.tsx, apps/web/app/(ee)/app.dub.co/(new-program)/header.tsx
Remove programType default/merge from form initialization; stop programType-based payload normalization/filtering on save (only url normalization retained).
Constants & scripts
apps/web/lib/partners/constants.ts, apps/web/scripts/perplexity/backfill-leads.ts
Add PROGRAM_ONBOARDING_PARTNERS_LIMIT = 5; add // @ts-nocheck to backfill-leads.ts.

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
Loading
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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • devkiran

Pre-merge checks (2 passed, 1 warning)

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title succinctly and accurately reflects the primary change set—removing all program importer integrations from the onboarding flow—providing a clear, focused summary that matches the bulk of the code modifications.

Poem

I nibble old imports, hop to the new,
Flatten rewards and tidy the queue.
Partners in rows, five per the rule,
No extra steps—clean, calm, and cool. 🐇✨

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch remove-imports-program-onboarding

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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 input

Use 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 programId early 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/logoUrl

If 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.

commissionStructure is derived from maxDuration and also manually set, creating two sources of truth. Derive it directly and update only maxDuration on 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 useEffect import.

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

📥 Commits

Reviewing files that changed from the base of the PR and between fdba367 and 67ba6c8.

📒 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.tsx
  • apps/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.tsx
  • apps/web/app/(ee)/app.dub.co/(new-program)/[slug]/program/new/overview/page-client.tsx
  • apps/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" and maxDuration=0 on “Lead” matches the learned UX pattern of resetting dependent fields when the controlling field changes.

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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 associations

Filtering out nulls in clicksToCreate (Line 165) shifts indices; later you index into clicksToCreate[idx] while iterating leadsToBackfill and then filter customers independently, causing mismatches and potential wrong customer_id on 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: Guard capitalize call against undefined UA type

ua.device.type can 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/functions and next/server in 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-nocheck hides 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 over partnerLinks

You do multiple .find/.some scans 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.id and reward.id literals 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

📥 Commits

Reviewing files that changed from the base of the PR and between 67ba6c8 and 669f11d.

📒 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: Confirm determinePartnerReward + EventType.lead still match the refactored reward schema

Given the PR consolidates reward shapes, ensure the function’s input contract and enum cases are still valid to avoid silent undefined at runtime masked by @ts-nocheck.

Run:

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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 toast

Reset hasSubmitted on errors so users can retry; also guard against undefined serverError. 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 input

Set 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 together

Prevents 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 failures

Leading/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 cap

Small 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

📥 Commits

Reviewing files that changed from the base of the PR and between 669f11d and 5e9afd5.

📒 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 — LGTM

Single 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 in apps/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 — LGTM

Dynamic error message reflects the constant; avoids future drift.

@steven-tey steven-tey merged commit af8e477 into main Sep 10, 2025
9 checks passed
@steven-tey steven-tey deleted the remove-imports-program-onboarding branch September 10, 2025 05:27
@coderabbitai coderabbitai bot mentioned this pull request Nov 20, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants