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

Skip to content

Conversation

@steven-tey
Copy link
Collaborator

@steven-tey steven-tey commented Dec 18, 2025

Summary by CodeRabbit

  • New Features

    • Added validation to ensure only configured email domains can be used as sender addresses in campaign previews.
    • Partner invitation emails now dynamically use configured program email domains as sender addresses.
  • Improvements

    • Enhanced email domain configuration handling throughout partner and campaign workflows.

✏️ Tip: You can customize this high-level summary in your review settings.

@vercel
Copy link
Contributor

vercel bot commented Dec 18, 2025

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

Project Deployment Review Updated (UTC)
dub Ready Ready Preview Dec 19, 2025 5:22am

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 18, 2025

Walkthrough

This PR refactors email domain handling across campaign and partner invite features. It generalizes the getProgramOrThrow utility to use a flexible include pattern, introduces a new program schema with invite email data, and adds runtime validation of campaign from emails against program email domain slugs.

Changes

Cohort / File(s) Summary
Core utility refactoring
apps/web/lib/api/programs/get-program-or-throw.ts
Replaced boolean includeCategories parameter with generic include of type Prisma.ProgramInclude, introducing ProgramWithInclude<T> type alias; adds post-fetch transformation for categories when present.
Schema extensions
apps/web/lib/zod/schemas/programs.ts
Added new exported schema ProgramSchemaWithInviteEmailData that extends ProgramSchema with inviteEmailData field.
API route handlers
apps/web/app/(ee)/api/programs/[programId]/route.ts, apps/web/app/(ee)/api/campaigns/[campaignId]/preview/route.ts
Updated program fetch calls to use new generic include pattern; added emailDomains inclusion; added runtime validation of from email address against program's email domain slugs before sending emails.
Business logic actions
apps/web/lib/actions/partners/invite-partner.ts
Updated program fetch to include emailDomains; conditionally set invite email from address using first email domain when available.
Client-side hooks
apps/web/lib/swr/use-email-domains.ts
Renamed SWR data field to emailDomains, added verifiedEmailDomain derived field computed via useMemo.
UI components
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/[campaignId]/campaign-editor.tsx, apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx
Updated to use verifiedEmailDomain from hook instead of local derivation; changed hardcoded sender email to dynamic partners@{verifiedEmailDomain.slug} in invite sheet.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

  • Generic type handling in getProgramOrThrow: Verify type safety of the generic include parameter and ProgramWithInclude<T> type construction, especially category transformation logic.
  • Email domain validation: Confirm the validation in preview route correctly matches email domain against program slugs and handles edge cases (missing domains, empty slug list).
  • Hook API changes: Ensure verifiedEmailDomain computation and loading state changes in useEmailDomains do not break existing consuming components.
  • Consistency across updates: Verify all call sites of getProgramOrThrow properly pass the new include parameter structure.

Possibly related PRs

  • dubinc/dub#3062: Modifies the campaign preview route to add preview field handling; overlaps with email-sending logic changes in this PR.
  • dubinc/dub#3093: Also modifies campaign preview API route email-sending logic and validation flow.
  • dubinc/dub#3066: Adds and uses inviteEmailData schema patterns for program invite email surface.

Suggested reviewers

  • TWilson023

Poem

🐰 Hop, hop, hooray! Domains now validated,
Generic includes have arrived, no longer belated,
From emails verified against the sluggy domain,
Invites sent with proper senders, what a gain!
Types are stronger, logic flows so clean,
Best refactor this rabbit has ever seen!

Pre-merge checks and finishing touches

❌ 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 'Use program emailDomains for invite email' accurately summarizes the main change: integrating program emailDomains throughout the invite email workflow.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch invite-email-domain

📜 Recent review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c2ee54d and c020b57.

📒 Files selected for processing (3)
  • apps/web/app/(ee)/api/programs/[programId]/route.ts (1 hunks)
  • apps/web/lib/api/programs/get-program-or-throw.ts (2 hunks)
  • apps/web/lib/zod/schemas/programs.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/lib/zod/schemas/programs.ts
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-10-28T19:17:44.390Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2985
File: apps/web/app/(ee)/api/network/programs/[programSlug]/route.ts:32-37
Timestamp: 2025-10-28T19:17:44.390Z
Learning: In Prisma queries, the `include` clause is only used for relationships (one-to-one, one-to-many, many-to-many). Regular scalar fields, JSON fields, and other non-relational columns are automatically included in the query result and do not need to be specified in the `include` object.

Applied to files:

  • apps/web/lib/api/programs/get-program-or-throw.ts
📚 Learning: 2025-10-06T15:48:45.956Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2935
File: packages/prisma/schema/workspace.prisma:21-36
Timestamp: 2025-10-06T15:48:45.956Z
Learning: In the Dub repository (dubinc/dub), Prisma schema changes are not managed with separate migration files. Do not flag missing Prisma migration files when schema changes are made to files like `packages/prisma/schema/workspace.prisma` or other schema files.

Applied to files:

  • apps/web/lib/api/programs/get-program-or-throw.ts
🧬 Code graph analysis (2)
apps/web/lib/api/programs/get-program-or-throw.ts (3)
packages/prisma/client.ts (1)
  • Prisma (30-30)
apps/web/lib/zod/schemas/programs.ts (1)
  • ProgramSchemaWithInviteEmailData (55-57)
packages/prisma/index.ts (1)
  • prisma (3-9)
apps/web/app/(ee)/api/programs/[programId]/route.ts (2)
apps/web/lib/api/programs/get-program-or-throw.ts (1)
  • getProgramOrThrow (12-46)
apps/web/lib/zod/schemas/programs.ts (1)
  • ProgramSchemaWithInviteEmailData (55-57)
⏰ 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 (5)
apps/web/app/(ee)/api/programs/[programId]/route.ts (2)

11-13: LGTM! Clean migration to the generic include pattern.

The refactoring from includeCategories: true to include: { categories: true } aligns well with the updated getProgramOrThrow signature and improves flexibility.


16-16: The Zod schema requires inviteEmailData but the database field is optional - parse will fail if null.

The inviteEmailData field is defined in the Prisma schema as optional (Json?), but ProgramSchemaWithInviteEmailData expects it to be a valid object with subject, title, and body fields. If the field is null in the database, the Zod parse on line 16 will fail at runtime.

Either make the schema field optional or ensure the database always populates inviteEmailData for programs that call this endpoint.

apps/web/lib/api/programs/get-program-or-throw.ts (3)

7-10: Type definition cleverly combines Zod schema with Prisma payload types.

The ProgramWithInclude<T> type intersects the inferred schema type with Prisma's dynamic payload type, providing good type safety for callers who specify different include options.


12-20: LGTM! Generic signature improves flexibility.

The refactoring from a boolean includeCategories parameter to a generic include parameter is a significant improvement. This allows callers to specify any relations they need while maintaining full type safety through the generic constraint.


35-45: Type assertion might be unsafe without runtime validation.

The type assertion as ProgramWithInclude<T> on line 45 assumes the Prisma-returned program object matches ProgramSchemaWithInviteEmailData, but there's no runtime validation here. This could lead to type mismatches if:

  1. The database schema differs from the Zod schema
  2. Fields expected by ProgramSchemaWithInviteEmailData (like inviteEmailData) are missing

While the categories transformation is correct, the overall return type safety depends on callers performing their own validation (as done in the route file with .parse()). Consider documenting this expectation or adding a lightweight runtime check.

Based on learnings, Prisma's include clause is only for relationships, so non-relational fields like inviteEmailData (if it exists) should be automatically selected. However, if inviteEmailData is not in the database model or requires computation, the type assertion will be misleading.

💡 Consider adding runtime validation or documentation

Option 1: Add runtime validation in the utility

  // Transform categories if included
  const transformedProgram =
    include?.categories && "categories" in program
      ? {
          ...program,
          // @ts-ignore conditionally transforming categories
          categories: program.categories?.map(({ category }) => category) ?? [],
        }
      : program;

- return transformedProgram as ProgramWithInclude<T>;
+ // Validate against schema to ensure type safety
+ return ProgramSchemaWithInviteEmailData.passthrough().parse(
+   transformedProgram
+ ) as ProgramWithInclude<T>;

Option 2: Document the expectation

+/**
+ * Fetches a program by ID or throws a not_found error.
+ * 
+ * Note: Callers should validate the returned program using 
+ * ProgramSchemaWithInviteEmailData.parse() if strict runtime type checking is needed.
+ */
 export async function getProgramOrThrow<T extends Prisma.ProgramInclude = {}>({
⛔ Skipped due to learnings
Learnt from: TWilson023
Repo: dubinc/dub PR: 2985
File: apps/web/app/(ee)/api/network/programs/[programSlug]/route.ts:20-27
Timestamp: 2025-10-28T19:17:28.085Z
Learning: In this codebase, Prisma's extendedWhereUnique feature is enabled (or Prisma 5.x is used), allowing findUniqueOrThrow to accept additional non-unique filters alongside unique fields like slug. This pattern is valid and acceptable.
Learnt from: TWilson023
Repo: dubinc/dub PR: 2985
File: apps/web/app/(ee)/api/network/programs/[programSlug]/route.ts:32-37
Timestamp: 2025-10-28T19:17:44.390Z
Learning: In Prisma queries, the `include` clause is only used for relationships (one-to-one, one-to-many, many-to-many). Regular scalar fields, JSON fields, and other non-relational columns are automatically included in the query result and do not need to be specified in the `include` object.

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: 2

🧹 Nitpick comments (1)
apps/web/lib/api/programs/get-program-or-throw.ts (1)

29-34: The finalInclude logic incorrectly forces categories: false when not requested.

When a caller requests include: { emailDomains: true }, the spread preserves emailDomains, but the explicit categories: include.categories ? true : false always sets categories to false. This is harmless but redundant. More importantly, if include is undefined, finalInclude becomes undefined, which is correct—but if include is {} (empty object), the conditional still triggers and produces { categories: false }.

Consider simplifying:

-  const finalInclude = include
-    ? ({
-        ...include,
-        categories: include.categories ? true : false,
-      } as T)
-    : undefined;
+  const finalInclude = include && Object.keys(include).length > 0
+    ? include
+    : undefined;

The category boolean normalization (true vs truthy) appears unnecessary since Prisma accepts truthy values for includes.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ecfd63e and 45e0caf.

📒 Files selected for processing (4)
  • apps/web/app/(ee)/api/campaigns/[campaignId]/preview/route.ts (2 hunks)
  • apps/web/app/(ee)/api/programs/[programId]/route.ts (1 hunks)
  • apps/web/lib/actions/partners/invite-partner.ts (2 hunks)
  • apps/web/lib/api/programs/get-program-or-throw.ts (2 hunks)
🧰 Additional context used
🧠 Learnings (6)
📚 Learning: 2025-11-12T22:23:10.414Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 3098
File: apps/web/lib/actions/partners/message-program.ts:49-58
Timestamp: 2025-11-12T22:23:10.414Z
Learning: In apps/web/lib/actions/partners/message-program.ts, when checking if a partner can continue messaging after messaging is disabled, the code intentionally requires `senderPartnerId: null` (program-initiated messages) to prevent partners from continuing to send junk messages. Only conversations started by the program should continue after messaging is disabled, as a spam prevention mechanism.

Applied to files:

  • apps/web/lib/actions/partners/invite-partner.ts
📚 Learning: 2025-09-17T17:44:03.965Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2857
File: apps/web/lib/actions/partners/update-program.ts:96-0
Timestamp: 2025-09-17T17:44:03.965Z
Learning: In apps/web/lib/actions/partners/update-program.ts, the team prefers to keep the messagingEnabledAt update logic simple by allowing client-provided timestamps rather than implementing server-controlled timestamp logic to avoid added complexity.

Applied to files:

  • apps/web/lib/actions/partners/invite-partner.ts
📚 Learning: 2025-10-06T15:48:14.205Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2935
File: apps/web/lib/actions/partners/invite-partner-from-network.ts:21-28
Timestamp: 2025-10-06T15:48:14.205Z
Learning: For the network invites limit check in apps/web/lib/actions/partners/invite-partner-from-network.ts, the team accepts that concurrent invites may bypass the limit due to race conditions. Perfect atomicity is not required for this feature.

Applied to files:

  • apps/web/lib/actions/partners/invite-partner.ts
📚 Learning: 2025-08-25T17:42:13.600Z
Learnt from: devkiran
Repo: dubinc/dub PR: 2736
File: apps/web/lib/api/get-workspace-users.ts:76-83
Timestamp: 2025-08-25T17:42:13.600Z
Learning: Business rule confirmed: Each workspace has exactly one program. The code should always return workspace.programs[0] since there's only one program per workspace.

Applied to files:

  • apps/web/lib/actions/partners/invite-partner.ts
📚 Learning: 2025-11-17T05:19:11.972Z
Learnt from: devkiran
Repo: dubinc/dub PR: 3113
File: apps/web/app/(ee)/api/cron/payouts/charge-succeeded/send-paypal-payouts.ts:65-75
Timestamp: 2025-11-17T05:19:11.972Z
Learning: In the Dub codebase, `sendBatchEmail` (implemented in packages/email/src/send-via-resend.ts) handles filtering of emails with invalid `to` addresses internally. Call sites can safely use non-null assertions on email addresses because the email sending layer will filter out any entries with null/undefined `to` values before sending. This centralized validation pattern is intentional and removes the need for filtering at individual call sites.

Applied to files:

  • apps/web/app/(ee)/api/campaigns/[campaignId]/preview/route.ts
📚 Learning: 2025-10-28T19:17:28.085Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2985
File: apps/web/app/(ee)/api/network/programs/[programSlug]/route.ts:20-27
Timestamp: 2025-10-28T19:17:28.085Z
Learning: In this codebase, Prisma's extendedWhereUnique feature is enabled (or Prisma 5.x is used), allowing findUniqueOrThrow to accept additional non-unique filters alongside unique fields like slug. This pattern is valid and acceptable.

Applied to files:

  • apps/web/lib/api/programs/get-program-or-throw.ts
⏰ 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 (5)
apps/web/lib/api/programs/get-program-or-throw.ts (1)

8-17: LGTM on schema and type definitions.

The ProgramWithInviteEmailSchema extension and ProgramWithInclude<T> type provide good type safety for combining Zod validation with Prisma's dynamic includes. The type intersection approach is appropriate for this use case.

apps/web/app/(ee)/api/programs/[programId]/route.ts (1)

10-12: LGTM!

The migration from includeCategories: true to include: { categories: true } correctly adopts the new generic include API pattern.

apps/web/lib/actions/partners/invite-partner.ts (2)

27-29: Verify that emailDomains is available after Zod parsing.

This include depends on getProgramOrThrow properly returning the emailDomains relation. As noted in the review of get-program-or-throw.ts, the current Zod parse() call may strip this field. Ensure that fix is applied for this code to function correctly.


91-95: LGTM on the conditional from address logic.

The defensive check program.emailDomains.length > 0 correctly handles the case where no email domains are configured, falling back to undefined which uses the default sender.

apps/web/app/(ee)/api/campaigns/[campaignId]/preview/route.ts (1)

41-43: Same verification note applies regarding emailDomains availability.

As with the invite flow, this include depends on getProgramOrThrow properly returning the emailDomains relation after Zod parsing.

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

🧹 Nitpick comments (1)
apps/web/lib/api/programs/get-program-or-throw.ts (1)

35-45: Consider extracting the category transformation to improve type safety.

The @ts-ignore comment works but could be avoided by narrowing the type more explicitly. If this pattern becomes common, consider a small helper.

🔎 Optional: Type-safe alternative
   // Transform categories if included
-  const transformedProgram =
-    include?.categories && "categories" in program
-      ? {
-          ...program,
-          // @ts-ignore conditionally transforming categories
-          categories: program.categories?.map(({ category }) => category) ?? [],
-        }
-      : program;
+  const transformedProgram = {
+    ...program,
+    ...(include?.categories &&
+      "categories" in program && {
+        categories:
+          (program.categories as { category: string }[] | undefined)?.map(
+            ({ category }) => category,
+          ) ?? [],
+      }),
+  };
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 45e0caf and 4626ebe.

📒 Files selected for processing (3)
  • apps/web/app/(ee)/api/programs/[programId]/route.ts (1 hunks)
  • apps/web/lib/api/programs/get-program-or-throw.ts (2 hunks)
  • apps/web/lib/zod/schemas/programs.ts (2 hunks)
🧰 Additional context used
🧠 Learnings (4)
📓 Common learnings
Learnt from: devkiran
Repo: dubinc/dub PR: 3113
File: apps/web/app/(ee)/api/cron/payouts/charge-succeeded/send-paypal-payouts.ts:65-75
Timestamp: 2025-11-17T05:19:11.972Z
Learning: In the Dub codebase, `sendBatchEmail` (implemented in packages/email/src/send-via-resend.ts) handles filtering of emails with invalid `to` addresses internally. Call sites can safely use non-null assertions on email addresses because the email sending layer will filter out any entries with null/undefined `to` values before sending. This centralized validation pattern is intentional and removes the need for filtering at individual call sites.
📚 Learning: 2025-07-30T15:25:13.936Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 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/programs.ts
📚 Learning: 2025-10-28T19:17:44.390Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2985
File: apps/web/app/(ee)/api/network/programs/[programSlug]/route.ts:32-37
Timestamp: 2025-10-28T19:17:44.390Z
Learning: In Prisma queries, the `include` clause is only used for relationships (one-to-one, one-to-many, many-to-many). Regular scalar fields, JSON fields, and other non-relational columns are automatically included in the query result and do not need to be specified in the `include` object.

Applied to files:

  • apps/web/lib/api/programs/get-program-or-throw.ts
📚 Learning: 2025-10-06T15:48:45.956Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2935
File: packages/prisma/schema/workspace.prisma:21-36
Timestamp: 2025-10-06T15:48:45.956Z
Learning: In the Dub repository (dubinc/dub), Prisma schema changes are not managed with separate migration files. Do not flag missing Prisma migration files when schema changes are made to files like `packages/prisma/schema/workspace.prisma` or other schema files.

Applied to files:

  • apps/web/lib/api/programs/get-program-or-throw.ts
🧬 Code graph analysis (2)
apps/web/lib/zod/schemas/programs.ts (1)
apps/web/lib/zod/schemas/program-invite-email.ts (1)
  • programInviteEmailDataSchema (3-9)
apps/web/app/(ee)/api/programs/[programId]/route.ts (2)
apps/web/lib/api/programs/get-program-or-throw.ts (1)
  • getProgramOrThrow (12-46)
apps/web/lib/zod/schemas/programs.ts (1)
  • ProgramSchema (27-53)
⏰ 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 (3)
apps/web/app/(ee)/api/programs/[programId]/route.ts (1)

3-16: LGTM!

The refactored approach is well-structured:

  • Using the generic include parameter aligns with the updated getProgramOrThrow signature
  • Applying ProgramSchema.parse() at the API boundary ensures runtime validation of the response before it's sent to clients
  • The categories transformation in getProgramOrThrow correctly maps to the enum array format expected by ProgramSchema
apps/web/lib/zod/schemas/programs.ts (1)

22-22: LGTM!

The inviteEmailData field is correctly added using the .nullish() schema, making it appropriately optional. The TODO comment provides good visibility for planned future refactoring.

Also applies to: 44-44

apps/web/lib/api/programs/get-program-or-throw.ts (1)

7-10: Type definition looks correct for the use case.

The intersection type combines the base ProgramSchema shape with the Prisma include payload, allowing callers to have type safety for both the base program fields and any included relations. Since validation happens at the API boundary (via ProgramSchema.parse in route handlers), this approach keeps the utility function flexible while maintaining type information.

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: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/web/app/(ee)/api/campaigns/[campaignId]/preview/route.ts (1)

93-93: Remove debug logging before merging.

The console.log statement appears to be debug output that should be removed or replaced with structured logging for production.

🔎 Suggested fix:
-    console.log("Resend response:", data);
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4626ebe and dc0b544.

📒 Files selected for processing (1)
  • apps/web/app/(ee)/api/campaigns/[campaignId]/preview/route.ts (2 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-11-17T05:19:11.972Z
Learnt from: devkiran
Repo: dubinc/dub PR: 3113
File: apps/web/app/(ee)/api/cron/payouts/charge-succeeded/send-paypal-payouts.ts:65-75
Timestamp: 2025-11-17T05:19:11.972Z
Learning: In the Dub codebase, `sendBatchEmail` (implemented in packages/email/src/send-via-resend.ts) handles filtering of emails with invalid `to` addresses internally. Call sites can safely use non-null assertions on email addresses because the email sending layer will filter out any entries with null/undefined `to` values before sending. This centralized validation pattern is intentional and removes the need for filtering at individual call sites.

Applied to files:

  • apps/web/app/(ee)/api/campaigns/[campaignId]/preview/route.ts
🧬 Code graph analysis (1)
apps/web/app/(ee)/api/campaigns/[campaignId]/preview/route.ts (1)
apps/web/lib/api/errors.ts (1)
  • DubApiError (58-75)
⏰ 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)/api/campaigns/[campaignId]/preview/route.ts (1)

41-44: LGTM!

The inclusion of emailDomains is correctly added to support the new validation logic.

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

🧹 Nitpick comments (1)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx (1)

517-519: Consider adding a visual indicator when using fallback email domain.

The silent fallback to [email protected] works correctly, but users may not realize they haven't configured a custom email domain. The campaign editor (lines 341-348 in campaign-editor.tsx) provides a helpful tooltip explaining what to do when no verified domain exists—consider a similar approach here for consistency.

Potential enhancement

Add an info tooltip or badge when the fallback domain is being used:

 <p className="text-xs text-neutral-500">
   <strong className="font-medium text-neutral-900">From: </strong>
   {verifiedEmailDomain
     ? `partners@${verifiedEmailDomain.slug}`
     : "[email protected]"}
+  {!verifiedEmailDomain && (
+    <InfoTooltip content="Configure a custom email domain in workspace settings to use your own domain." />
+  )}
 </p>
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dc0b544 and 5b1b5f3.

📒 Files selected for processing (3)
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/[campaignId]/campaign-editor.tsx (3 hunks)
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx (3 hunks)
  • apps/web/lib/swr/use-email-domains.ts (2 hunks)
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-10-15T01:05:43.266Z
Learnt from: steven-tey
Repo: dubinc/dub PR: 2958
File: apps/web/app/app.dub.co/(dashboard)/[slug]/settings/members/page-client.tsx:432-457
Timestamp: 2025-10-15T01:05:43.266Z
Learning: In apps/web/app/app.dub.co/(dashboard)/[slug]/settings/members/page-client.tsx, defer refactoring the custom MenuItem component (lines 432-457) to use the shared dub/ui MenuItem component to a future PR, as requested by steven-tey.

Applied to files:

  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/[campaignId]/campaign-editor.tsx
📚 Learning: 2025-11-17T05:19:11.972Z
Learnt from: devkiran
Repo: dubinc/dub PR: 3113
File: apps/web/app/(ee)/api/cron/payouts/charge-succeeded/send-paypal-payouts.ts:65-75
Timestamp: 2025-11-17T05:19:11.972Z
Learning: In the Dub codebase, `sendBatchEmail` (implemented in packages/email/src/send-via-resend.ts) handles filtering of emails with invalid `to` addresses internally. Call sites can safely use non-null assertions on email addresses because the email sending layer will filter out any entries with null/undefined `to` values before sending. This centralized validation pattern is intentional and removes the need for filtering at individual call sites.

Applied to files:

  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/[campaignId]/campaign-editor.tsx
📚 Learning: 2025-07-17T06:41:45.620Z
Learnt from: devkiran
Repo: dubinc/dub PR: 2637
File: apps/web/app/(ee)/api/singular/webhook/route.ts:0-0
Timestamp: 2025-07-17T06:41:45.620Z
Learning: In the Singular integration (apps/web/app/(ee)/api/singular/webhook/route.ts), the event names in the singularToDubEvent object have intentionally different casing: "Copy GAID" and "copy IDFA". This casing difference is valid and should not be changed, as these are the correct event names expected from Singular.

Applied to files:

  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/[campaignId]/campaign-editor.tsx
🧬 Code graph analysis (3)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx (1)
apps/web/lib/swr/use-email-domains.ts (1)
  • useEmailDomains (7-35)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/[campaignId]/campaign-editor.tsx (1)
apps/web/lib/swr/use-email-domains.ts (1)
  • useEmailDomains (7-35)
apps/web/lib/swr/use-email-domains.ts (2)
apps/web/lib/swr/use-workspace.ts (1)
  • useWorkspace (7-48)
apps/web/lib/types.ts (1)
  • EmailDomainProps (692-692)
⏰ 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 (2)
apps/web/lib/swr/use-email-domains.ts (1)

24-26: LGTM! Clean memoization of verified domain.

The memoized verifiedEmailDomain computation correctly finds the first verified domain. The useMemo dependency array is accurate, and consumers properly handle the undefined case with optional chaining.

apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/[campaignId]/campaign-editor.tsx (1)

331-367: Excellent integration with proper disabled states and helpful UX.

The From field implementation correctly:

  • Derives the domain suffix from verifiedEmailDomain.slug
  • Disables the field when no verified domain exists
  • Provides an actionable tooltip directing users to configure an email domain
  • Safely constructs the email address with optional chaining

The logic properly handles loading states, readonly campaigns, and missing domains.

@steven-tey steven-tey merged commit 47d12e6 into main Dec 19, 2025
7 of 8 checks passed
@steven-tey steven-tey deleted the invite-email-domain branch December 19, 2025 06:30
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