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

Skip to content

Conversation

@devkiran
Copy link
Collaborator

@devkiran devkiran commented Oct 16, 2025

Summary by CodeRabbit

  • New Features
    • Full email domain management: add/edit/delete domains, DNS records view, verify/refresh, domain cards, modals, plan-gated domains page, and hourly background verification
    • Campaign UI improvements: domain-aware From editor, consolidated scheduling/confirmation modals, schedule/send flows, and send-preview accepts From
  • Bug Fixes
    • Campaign creation now returns HTTP 201 on success
  • Other
    • Marketing campaigns notification preference and related UI additions

@vercel
Copy link
Contributor

vercel bot commented Oct 16, 2025

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

Project Deployment Preview Updated (UTC)
dub Ready Ready Preview Nov 1, 2025 5:27am

@socket-security
Copy link

socket-security bot commented Oct 16, 2025

No dependency changes detected. Learn more about Socket for GitHub.

👍 No dependency changes detected in pull request

@devkiran devkiran changed the base branch from main to email-campaigns October 16, 2025 13:11
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 16, 2025

Walkthrough

Adds email-domain support (CRUD, Resend integration, verification endpoint and cron), email-sender fields and campaign scheduling changes (new from, statuses, validation, QStash scheduling/broadcast), Prisma schema updates for EmailDomain and campaign fields, and multiple frontend UI components/hooks/modals to manage domains and campaign flows.

Changes

Cohort / File(s) Summary
Email Domain API
apps/web/app/(ee)/api/email-domains/route.ts, apps/web/app/(ee)/api/email-domains/[domain]/route.ts, apps/web/app/(ee)/api/email-domains/[domain]/verify/route.ts
Added list/POST and per-domain PATCH/DELETE plus verify GET handlers with workspace gating, Resend integration, async verification (waitUntil), and error mapping.
Email Domain Cron & Vercel
apps/web/app/(ee)/api/cron/email-domains/verify/route.ts, apps/web/vercel.json
New cron route to batch-check Resend domains (limit 10) and added hourly cron schedule entry.
Email Domain UI & Hooks
apps/web/app/app.dub.co/(dashboard)/[slug]/settings/domains/*, apps/web/lib/swr/use-email-domains.ts, apps/web/app/app.dub.co/(dashboard)/[slug]/settings/domains/header.tsx
New EmailDomainCard, EmailDomainDnsRecords, client page wiring, header tab gating, and useEmailDomains SWR hook.
Email Domain Modals
apps/web/ui/modals/add-edit-email-domain-modal.tsx, apps/web/ui/modals/delete-email-domain-modal.tsx
Add/edit and delete modals/hooks with validation, API POST/PATCH/DELETE, typed delete confirmation, toasts, and cache revalidation.
Schemas, Types & Helpers
apps/web/lib/zod/schemas/email-domains.ts, apps/web/lib/types.ts, apps/web/lib/api/domains/get-email-domain-or-throw.ts
EmailDomainSchema and body schemas, EmailDomainProps type, and helper to load/authorize email domain by program.
Prisma: EmailDomain & Client Export
packages/prisma/schema/domain.prisma, packages/prisma/client.ts, packages/prisma/schema/program.prisma
Added EmailDomain model and EmailDomainStatus enum, Program.emailDomains relation, and exported EmailDomainStatus.
Campaign DB & Schema
packages/prisma/schema/campaign.prisma, packages/prisma/schema/notification.prisma
Campaign adds from, qstashMessageId, userId; CampaignStatus adds sending,sent,canceled; PartnerNotificationPreferences adds marketingCampaign.
Campaign Validation & Scheduling
apps/web/lib/api/campaigns/constants.ts, apps/web/lib/api/campaigns/validate-campaign.ts, apps/web/lib/api/campaigns/schedule-campaigns.ts, apps/web/app/(ee)/api/campaigns/[campaignId]/route.ts, apps/web/app/(ee)/api/campaigns/route.ts, apps/web/app/(ee)/api/campaigns/[campaignId]/duplicate/route.ts
New status transitions and editable/read-only sets, campaign validation (including from-address/domain checks), scheduling helpers for marketing/transactional campaigns (manage QStash), PATCH uses validation, POST returns 201, duplicate preserves from.
Campaign Broadcast Job
apps/web/app/(ee)/api/cron/campaigns/broadcast/route.ts
New QStash-invoked broadcast endpoint for batched marketing sends with pagination, safety checks, state updates, and re-enqueueing.
Campaign UI & Controls
apps/web/app/app.dub.co/.../program/campaigns/*
Editor and controls accept from and scheduledAt, read-only handling, centralized confirmation modals, create flow type picker, preview includes from, and assorted UX updates.
Campaign Data & Actions
apps/web/lib/api/campaigns/get-campaigns.ts, apps/web/lib/actions/campaigns/send-campaign-preview-email.ts
Removed legacy getCampaigns helper; preview send accepts optional from and derives email variant.
Plan Capabilities & Program Lookup
apps/web/lib/plan-capabilities.ts, apps/web/lib/api/programs/get-program-or-throw.ts
Added canManageEmailDomains capability and tightened program lookup enforcing workspace association.
Resend Types & Webhooks
packages/email/src/resend/types.ts, apps/web/app/api/resend/webhook/email-bounced.ts, apps/web/app/api/resend/webhook/email-delivered.ts
Re-exported Resend type GetDomainResponseSuccess; removed unused subject param from two webhook handlers.
Partners / Notifications
apps/web/lib/zod/schemas/partner-profile.ts, apps/web/app/(ee)/partners.dub.co/.../page-client.tsx
Added marketingCampaign notification type and partner UI option.
Partners Count Hook & Bounty UI
apps/web/lib/swr/use-partners-count-by-groupids.ts, multiple bounty UI files
Renamed hook to usePartnersCountByGroupIds({ groupIds }), updated call sites and variable names to totalPartners.
Misc UI, Icons, Editor
apps/web/ui/modals/*, packages/ui/src/icons/nucleo/*, packages/ui/src/rich-text-area/index.tsx
Minor semantic/styling fixes, SVG prop camelCase updates, and added editable?: boolean prop to RichTextArea.
Tests
apps/web/tests/campaigns/index.test.ts
Updated tests: POST /campaigns expects 201; test objects include from: null and scheduledAt: null.
Small edits / Cleanups
apps/web/app/(ee)/api/cron/bounties/notify-partners/route.ts, apps/web/app/(ee)/api/cron/program-application-reminder/route.ts, apps/web/app/(ee)/api/cron/cleanup/e2e-tests/route.ts, packages/ui/src/menu-item.tsx, packages/ui/src/icons/nucleo/*
Minor refactors: const usage, wording, cleanup window broadened, hover style changes, SVG prop names adjusted.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant UI
    participant API
    participant DB
    participant Resend
    Note right of API #bfe8d6: Create email domain
    User->>UI: Submit new domain (slug, fromAddress)
    UI->>API: POST /api/email-domains
    API->>Resend: create domain
    Resend-->>API: resendDomainId
    API->>DB: create EmailDomain (resendDomainId, status: pending)
    API-->>UI: 201 Created
Loading
sequenceDiagram
    actor Cron
    participant API
    participant DB
    participant Resend
    Note right of API #fff3bf: Cron verification
    Cron->>API: GET /api/cron/email-domains/verify
    API->>DB: fetch domains needing check (limit 10)
    alt domains exist
        API->>Resend: fetch domain statuses (parallel)
        Resend-->>API: domain status
        API->>DB: update EmailDomain.status, lastChecked
    else none
        API-->>Cron: no domains to check
    end
Loading
sequenceDiagram
    actor User
    participant UI
    participant API
    participant DB
    participant QStash
    Note right of API #e6f0ff: Campaign schedule & broadcast
    User->>UI: Create/Update campaign (type, from, scheduledAt)
    UI->>API: POST/PATCH /api/campaigns
    API->>API: validateCampaign (status transitions, from-domain)
    API->>DB: persist campaign (qstashMessageId if scheduled)
    API->>QStash: schedule broadcast (qstash)
    QStash->>API: invoke /api/cron/campaigns/broadcast
    API->>DB: mark sending, process batches, mark sent
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Areas needing extra attention:

  • Scheduling & QStash interactions: apps/web/lib/api/campaigns/schedule-campaigns.ts, apps/web/app/(ee)/api/cron/campaigns/broadcast/route.ts
  • Resend integration and verification flows: apps/web/app/(ee)/api/email-domains/* and apps/web/app/(ee)/api/cron/email-domains/verify/route.ts
  • Campaign validation and from-address checks: apps/web/lib/api/campaigns/validate-campaign.ts and usage sites (workflows execution)
  • Prisma schema additions and migration impact: packages/prisma/schema/*.prisma and packages/prisma/client.ts
  • UI integration for domain-based From editing and new modals: apps/web/app/.../settings/domains/* and apps/web/ui/modals/*

Possibly related PRs

Poem

🐇
I found a slug beneath a log,
I hopped and wired Resend to the bog.
QStash queues hum, domains check in line,
Campaigns await their scheduled shine.
A tiny hop — deployment's fine!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.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 pull request title "Email domains & Marketing campaigns" directly addresses the two major features introduced in this changeset: a comprehensive email domain management system (including API routes, UI components, and Prisma models) and support for a new marketing campaign type alongside transactional campaigns (including scheduling, validation, and campaign controls). The title is specific and descriptive without being overly broad or generic, clearly communicating the primary changes to a teammate reviewing the repository history.
✨ 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 program-email-domains

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.

Base automatically changed from email-campaigns to main October 17, 2025 01:32
@panda-sandeep
Copy link

/bug0 run

@devkiran
Copy link
Collaborator Author

@coderabbitai final review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 31, 2025

@devkiran: I'll perform a final comprehensive review of all changes in this PR.

✅ Actions performed

Full review triggered.

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 (1)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/campaigns-table.tsx (1)

79-86: LGTM! Clean refactor with dedicated type icon component.

The email column now displays the campaign type icon and name in a clear, accessible layout. The CampaignTypeIcon abstraction improves maintainability, and text truncation prevents overflow issues.

🧹 Nitpick comments (2)
apps/web/app/(ee)/api/cron/cleanup/e2e-tests/route.ts (1)

27-96: Consider applying consistent pagination across all resource queries.

Only the links query includes a take: 100 limit (line 42), while domains, tags, partners, and users queries have no pagination limits. With the expanded 1-day retention window, this inconsistency becomes more significant.

If E2E tests generate large volumes of non-link resources over a 24-hour period, the unbounded queries could impact memory usage and query performance.

Consider one of the following approaches:

  1. Apply consistent limits across all queries (if resource volumes could be high):
// Add take limit to each query
domains: prisma.domain.findMany({
  // ... existing where clause
  take: 100,
}),
tags: prisma.tag.findMany({
  // ... existing where clause
  take: 100,
}),
// ... etc
  1. Verify and document if the current approach is intentional because links are expected to be the only high-volume resource.
apps/web/app/(ee)/api/campaigns/route.ts (1)

27-44: Consider case-insensitive search for better UX.

The search functionality uses contains without specifying a mode, which defaults to case-sensitive matching in PostgreSQL (and case-insensitive in MySQL). For a better user experience, consider adding mode: 'insensitive' to the search conditions:

       ...(search && {
         OR: [
-          { name: { contains: search } },
-          { subject: { contains: search } },
+          { name: { contains: search, mode: "insensitive" } },
+          { subject: { contains: search, mode: "insensitive" } },
         ],
       }),

Note: If using a database that doesn't support case-insensitive operations (like older SQLite versions), you may need to handle this differently or keep the current behavior.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 418c8e8 and 0d3a935.

📒 Files selected for processing (5)
  • apps/web/app/(ee)/api/campaigns/route.ts (2 hunks)
  • apps/web/app/(ee)/api/cron/cleanup/e2e-tests/route.ts (5 hunks)
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/campaigns-table.tsx (8 hunks)
  • apps/web/lib/api/campaigns/get-campaigns.ts (0 hunks)
  • apps/web/lib/zod/schemas/campaigns.ts (4 hunks)
💤 Files with no reviewable changes (1)
  • apps/web/lib/api/campaigns/get-campaigns.ts
🧰 Additional context used
🧠 Learnings (9)
📚 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/campaigns.ts
📚 Learning: 2025-06-06T07:59:03.120Z
Learnt from: devkiran
Repo: dubinc/dub PR: 2177
File: apps/web/lib/api/links/bulk-create-links.ts:66-84
Timestamp: 2025-06-06T07:59:03.120Z
Learning: In apps/web/lib/api/links/bulk-create-links.ts, the team accepts the risk of potential undefined results from links.find() operations when building invalidLinks arrays, because existing links are fetched from the database based on the input links, so matches are expected to always exist.

Applied to files:

  • apps/web/app/(ee)/api/cron/cleanup/e2e-tests/route.ts
📚 Learning: 2025-05-29T04:45:18.504Z
Learnt from: devkiran
Repo: dubinc/dub PR: 2448
File: packages/email/src/templates/partner-program-summary.tsx:0-0
Timestamp: 2025-05-29T04:45:18.504Z
Learning: In the PartnerProgramSummary email template (packages/email/src/templates/partner-program-summary.tsx), the stat titles are hardcoded constants ("Clicks", "Leads", "Sales", "Earnings") that will always match the ICONS object keys after toLowerCase() conversion, so icon lookup failures are not possible.

Applied to files:

  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/campaigns-table.tsx
📚 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/campaigns/campaigns-table.tsx
📚 Learning: 2025-09-24T16:10:37.349Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/ui/partners/partner-about.tsx:11-11
Timestamp: 2025-09-24T16:10:37.349Z
Learning: In the Dub codebase, the team prefers to import Icon as a runtime value from "dub/ui" and uses Icon as both a type and variable name in component props, even when this creates shadowing. This is their established pattern and should not be suggested for refactoring.

Applied to files:

  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/campaigns-table.tsx
📚 Learning: 2025-09-24T15:47:40.293Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/about-you-form.tsx:152-157
Timestamp: 2025-09-24T15:47:40.293Z
Learning: The Button component from dub/ui automatically adds type="button" when an onClick prop is passed, preventing accidental form submissions without requiring explicit type specification. The implementation uses: type={onClick ? "button" : type}

Applied to files:

  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/campaigns-table.tsx
📚 Learning: 2025-09-24T15:47:40.293Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/about-you-form.tsx:152-157
Timestamp: 2025-09-24T15:47:40.293Z
Learning: The Button component from dub/ui automatically adds type="button" when an onClick prop is passed, preventing accidental form submissions without requiring explicit type specification.

Applied to files:

  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/campaigns-table.tsx
📚 Learning: 2025-09-24T15:47:40.293Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/about-you-form.tsx:152-157
Timestamp: 2025-09-24T15:47:40.293Z
Learning: The Button component from dub/ui automatically sets type="button" when an onClick prop is passed and defaults to type="submit" otherwise, using the logic: type={props.onClick ? "button" : "submit"}. This prevents accidental form submissions when buttons are used for modal triggers or other non-form actions.

Applied to files:

  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/campaigns-table.tsx
📚 Learning: 2025-08-26T15:05:55.081Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2736
File: apps/web/lib/swr/use-bounty.ts:11-16
Timestamp: 2025-08-26T15:05:55.081Z
Learning: In the Dub codebase, workspace authentication and route structures prevent endless loading states when workspaceId or similar route parameters are missing, so gating SWR loading states on parameter availability is often unnecessary.

Applied to files:

  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/campaigns-table.tsx
🧬 Code graph analysis (4)
apps/web/lib/zod/schemas/campaigns.ts (3)
apps/web/lib/zod/schemas/workflows.ts (1)
  • workflowConditionSchema (66-70)
apps/web/lib/zod/schemas/groups.ts (1)
  • GroupSchema (51-64)
apps/web/lib/zod/schemas/utils.ts (1)
  • parseDateSchema (37-40)
apps/web/app/(ee)/api/cron/cleanup/e2e-tests/route.ts (2)
packages/prisma/index.ts (1)
  • prisma (3-9)
apps/web/tests/utils/resource.ts (2)
  • E2E_USER_ID (1-1)
  • E2E_WORKSPACE_ID (2-2)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/campaigns-table.tsx (2)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/campaign-type-icon.tsx (1)
  • CampaignTypeIcon (4-24)
packages/ui/src/timestamp-tooltip.tsx (1)
  • TimestampTooltip (28-50)
apps/web/app/(ee)/api/campaigns/route.ts (2)
apps/web/lib/zod/schemas/campaigns.ts (1)
  • getCampaignsQuerySchema (106-112)
packages/prisma/index.ts (1)
  • prisma (3-9)
⏰ 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 (11)
apps/web/app/(ee)/api/cron/cleanup/e2e-tests/route.ts (1)

24-24: Verify the 24x increase in retention window is intentional.

The retention window has been expanded from 1 hour to 1 day (24 hours), meaning E2E test data will remain in the system 24 times longer before cleanup. Given that this cron job runs every 6 hours, multiple cleanup cycles will now process the same data window.

Please confirm:

  • Is this change intentional to accommodate longer-running E2E test scenarios (possibly related to email campaigns/scheduling features introduced in this PR)?
  • Have you considered the potential accumulation of test data and its impact on database size and query performance?

If the change is intentional, consider updating the comment on line 16 to reflect the new retention policy for clarity.

apps/web/lib/zod/schemas/campaigns.ts (4)

53-53: LGTM! Appropriate nullable fields for optional campaign properties.

The from and scheduledAt fields are correctly defined as nullable in the read schema, allowing campaigns to exist without these values set initially.

Also applies to: 59-59


91-91: Verify if from should support explicit null for clearing.

The from field uses .email().trim().toLowerCase() without .nullish(), which means it can be omitted (via .partial() at line 104) but cannot be explicitly set to null. In contrast, scheduledAt at line 95 uses parseDateSchema.nullish() to support both null and undefined.

This creates an asymmetry: users can clear scheduledAt by sending null, but cannot clear from in the same way. If the intent is to allow clearing the from field after it's been set, consider applying .nullish() or .nullable() to the from field as well.

Apply this diff if from should support explicit clearing:

-    from: z.string().email().trim().toLowerCase(),
+    from: z.string().email().trim().toLowerCase().nullish(),

96-102: LGTM! New campaign statuses support scheduling functionality.

The addition of scheduled and canceled statuses aligns with the PR's campaign scheduling feature and appropriately extends the allowed status transitions.


74-74: LGTM! Appropriate addition to list schema.

Including scheduledAt in the list schema enables displaying when campaigns are scheduled to run, which is useful for the campaigns table view.

apps/web/app/(ee)/api/campaigns/route.ts (1)

110-115: LGTM! Correct HTTP semantics for resource creation.

Returning 201 Created for successful campaign creation is semantically correct and aligns with REST best practices. The response body appropriately contains the new resource identifier.

apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/campaigns-table.tsx (5)

104-116: LGTM! Excellent UX with timestamp formatting and tooltip.

The Created column provides a user-friendly relative timestamp display with a detailed tooltip on hover. The configuration (side="right", delayDuration=150) is appropriate for table context.


120-122: Verify menu column width is sufficient.

The menu column width was reduced from 43 to 20 pixels. Given that the RowMenuButton at line 303 uses px-2 padding plus an icon, a width of 20 may be too narrow and could cause visual cramping or button clipping.

If the button appears cramped, consider reverting to the previous width or adjusting to a value that accommodates the button's actual rendered size (typically 32-40px minimum for an icon button with padding).

       {
         id: "menu",
-        minSize: 20,
-        size: 20,
-        maxSize: 20,
+        minSize: 40,
+        size: 40,
+        maxSize: 40,
         cell: ({ row }) => <RowMenuButton row={row} />,
       },

141-141: LGTM! Column pinning improves table usability.

Pinning the createdAt and menu columns to the right ensures they remain visible during horizontal scrolling, which is a standard and effective UX pattern for action-oriented tables.


275-286: LGTM! Correct conditional logic for Pause/Resume.

The Pause/Resume action is appropriately restricted to non-draft transactional campaigns. This aligns with the business logic where only automated campaigns can be paused/resumed, while one-time broadcast campaigns and drafts cannot.


314-320: LGTM! Improved precision for percentage calculations.

Returning percentages with two decimal places (e.g., 23.45% instead of 23%) provides more accurate metrics representation. The Number() conversion ensures the return type remains numeric after toFixed().

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

🧹 Nitpick comments (1)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/delete-campaign-modal.tsx (1)

55-57: Consider consolidating validation logic within react-hook-form.

The current approach uses both react-hook-form's required validation and a separate isDisabled memo based on exact text matching. The required: true rule is redundant since the button is disabled via isDisabled, not form validation state.

You could simplify by using custom validation within react-hook-form:

- const {
-   register,
-   handleSubmit,
-   watch,
-   formState: { errors },
- } = useForm<{ confirm: string }>({
-   defaultValues: {
-     confirm: "",
-   },
- });
-
- const confirm = watch("confirm");
-
- const isDisabled = useMemo(() => {
-   return confirm !== "confirm delete campaign";
- }, [confirm]);
+ const {
+   register,
+   handleSubmit,
+   formState: { errors, isValid },
+ } = useForm<{ confirm: string }>({
+   mode: "onChange",
+   defaultValues: {
+     confirm: "",
+   },
+ });

And update the validation rule:

  <input
    className={cn(
      "block w-full rounded-md border-neutral-300 text-neutral-900 placeholder-neutral-400 focus:border-neutral-500 focus:outline-none focus:ring-neutral-500 sm:text-sm",
      errors.confirm && "border-red-600",
    )}
    placeholder="confirm delete campaign"
    type="text"
    autoComplete="off"
-   {...register("confirm", {
-     required: true,
-   })}
+   {...register("confirm", {
+     validate: (value) => value === "confirm delete campaign" || "Please type the exact phrase",
+   })}
  />

Then use isValid for the button:

  <Button
    type="submit"
    variant="danger"
    className="h-8 w-fit px-3"
    text="Delete campaign"
-   disabled={isDisabled}
+   disabled={!isValid}
    loading={isSubmitting}
  />

Also applies to: 91-102

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0d3a935 and 705ef88.

📒 Files selected for processing (2)
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/delete-campaign-modal.tsx (3 hunks)
  • packages/ui/src/menu-item.tsx (1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-10-15T01:52:37.048Z
Learnt from: steven-tey
Repo: dubinc/dub PR: 2958
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/members/page-client.tsx:270-303
Timestamp: 2025-10-15T01:52:37.048Z
Learning: In React components with dropdowns or form controls that show modals for confirmation (e.g., role selection, delete confirmation), local state should be reverted to match the prop value when the modal is cancelled. This prevents the UI from showing an unconfirmed change. The solution is to either: (1) pass an onClose callback to the modal that resets the local state, or (2) observe modal visibility state and reset on close. Example context: RoleCell component in apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/members/page-client.tsx where role dropdown should revert to user.role when UpdateUserModal is cancelled.

Applied to files:

  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/delete-campaign-modal.tsx
📚 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/campaigns/delete-campaign-modal.tsx
  • packages/ui/src/menu-item.tsx
🔇 Additional comments (3)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/delete-campaign-modal.tsx (1)

65-124: LGTM! Well-structured form and deletion flow.

The form structure is well-implemented with proper:

  • Form submission handling via handleSubmit
  • Submit button type and disabled/loading states
  • Campaign summary banner for context
  • Success flow with cache invalidation, toast, and navigation
packages/ui/src/menu-item.tsx (2)

23-23: No changes needed — the explicit color approach aligns with established codebase patterns.

The search confirms that packages/ui/src consistently uses explicit Tailwind color values (e.g., bg-red-500, bg-red-600, hover:ring-red-100) for danger/error states across button.tsx, status-badge.tsx, progress-bar.tsx, and dub-status-badge.tsx. No design token alternatives like bg-bg-error exist in the codebase. The menu-item.tsx change follows this established pattern perfectly and adds proper dark mode support (dark:hover:bg-red-950/20), which the default variant lacks.


16-16: Hover state consolidation is correct—no issues found.

The refactoring safely consolidates hover styles to the variant level. The disabled variant's hover:bg-bg-default (line 26) properly overrides the variant hover states, and browsers inherently suppress hover on disabled elements anyway. No usages with disabled states were found in the codebase search, but the structure ensures correct behavior when disabledTooltip is set. The styling changes are sound.

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

♻️ Duplicate comments (1)
apps/web/app/(ee)/api/campaigns/route.ts (1)

74-121: Verify email domain handling for campaign creation.

The past review flagged a concern about non-deterministic email domain selection during campaign creation. Since the email domain query logic is not visible in the current code snippet, please verify that:

  1. If email domain selection happens during creation, it includes explicit orderBy to ensure deterministic results
  2. Validation exists to handle cases where no verified email domain is available
  3. The from field is properly set when a verified domain exists

This verification ensures campaigns aren't created with from: null when a verified domain should be assigned.

#!/bin/bash
# Description: Verify email domain handling in campaign creation flow

# Search for email domain queries in campaign creation
rg -nP -C5 'emailDomain.*findFirst|findFirst.*emailDomain' -g '*campaign*' -g '!*.test.ts'

# Search for verified domain validation
rg -nP -C5 'status.*verified|verified.*domain' -g '*campaign*' -g '!*.test.ts'

# Check if from field is set during creation
ast-grep --pattern 'await tx.campaign.create({
  $$$
  from: $VALUE
  $$$
})'
🧹 Nitpick comments (1)
apps/web/tests/campaigns/index.test.ts (1)

35-35: Tighten status validation in test expectations.

Using expect.any(String) for the status field is overly permissive. It would be better to validate against specific expected status values to catch regressions.

Consider updating to:

 const expectedCampaign: Partial<Campaign> = {
   ...campaign,
   type: "transactional",
-  status: expect.any(String),
+  // Status varies by test but should be a valid CampaignStatus enum value
   from: null,
   scheduledAt: null,
   groups: [{ id: E2E_PARTNER_GROUP.id }],

Then in each test, explicitly assert the expected status (as already done in most tests like lines 78, 91, 107, 123, 139).

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 705ef88 and 87afda8.

📒 Files selected for processing (3)
  • apps/web/app/(ee)/api/campaigns/route.ts (3 hunks)
  • apps/web/lib/zod/schemas/campaigns.ts (4 hunks)
  • apps/web/tests/campaigns/index.test.ts (3 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 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/campaigns.ts
🧬 Code graph analysis (2)
apps/web/lib/zod/schemas/campaigns.ts (3)
apps/web/lib/zod/schemas/workflows.ts (1)
  • workflowConditionSchema (66-70)
apps/web/lib/zod/schemas/groups.ts (1)
  • GroupSchema (51-64)
apps/web/lib/zod/schemas/utils.ts (1)
  • parseDateSchema (37-40)
apps/web/app/(ee)/api/campaigns/route.ts (2)
apps/web/lib/zod/schemas/campaigns.ts (2)
  • getCampaignsQuerySchema (102-108)
  • CampaignSchema (49-62)
packages/prisma/index.ts (1)
  • prisma (3-9)
⏰ 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 (6)
apps/web/lib/zod/schemas/campaigns.ts (2)

10-10: LGTM!

The parseDateSchema import is correctly added to support the new scheduledAt field validation in updateCampaignSchema.


53-53: LGTM!

The new from and scheduledAt fields are appropriately added as nullable to support email sender configuration and campaign scheduling.

Also applies to: 59-59

apps/web/app/(ee)/api/campaigns/route.ts (2)

25-59: LGTM! Well-structured query refactor.

The GET endpoint refactor is solid:

  • Direct Prisma query with proper filtering, ordering, and pagination
  • Includes necessary relations (groups, workflow)
  • Results validated through CampaignSchema
  • Proper optional chaining for nullable relations

One minor note: Line 56 assumes the first trigger condition is the primary one (campaign.workflow?.triggerConditions?.[0]). This is safe due to optional chaining but assumes a single-condition workflow design.


123-128: LGTM! More RESTful status code.

Changing the POST response to 201 Created is the correct HTTP semantics for resource creation, improving API consistency.

apps/web/tests/campaigns/index.test.ts (2)

57-57: LGTM!

Test correctly updated to expect 201 Created status, matching the API change.


178-181: LGTM! Cleaner assertion pattern.

Using the spread operator to reuse expectedCampaign improves maintainability and reduces duplication.

.string()
.trim()
.max(100, "Subject must be less than 100 characters."),
from: z.string().email().trim().toLowerCase(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

LGTM! Validation flow looks appropriate.

The new fields and statuses are well-structured:

  • from has proper email validation with normalization (trim, lowercase)
  • scheduledAt uses parseDateSchema.nullish() for flexible date input
  • New scheduled and canceled statuses support the expanded campaign lifecycle

Since the schema is .partial(), these fields are optional during updates, which is correct for PATCH semantics. The email validation ensures that when from is provided, it's valid.

Verify that business logic elsewhere (e.g., before scheduling) enforces that from must be set and valid:

Also applies to: 91-91, 96-97


🏁 Script executed:

#!/bin/bash
# Description: Check for validation logic that enforces 'from' is set before scheduling campaigns

# Search for campaign validation logic
ast-grep --pattern 'function $FUNC($$$) {
  $$$
  scheduledAt
  $$$
  from
  $$$
}'

# Search for scheduling-related validation
rg -nP -C3 --type=ts 'scheduledAt.*from|from.*scheduledAt' -g '!*.test.ts' -g '!node_modules'

# Search for validation before campaign scheduling
rg -nP -C5 'status.*scheduled|schedule.*campaign' -g 'validate*.ts' -g '*validation*.ts'

Length of output: 0



---

🏁 Script executed:

```shell
#!/bin/bash
# Search for updateCampaignSchema usage in the codebase
echo "=== Searching for updateCampaignSchema usage ==="
rg -n 'updateCampaignSchema' --type=ts --type=tsx -C2

echo -e "\n=== Searching for campaign API routes/handlers ==="
fd -e ts -e tsx | grep -i 'campaign' | head -20

echo -e "\n=== Searching for campaign update handlers ==="
rg -n 'PUT.*campaign|PATCH.*campaign' --type=ts -C3

echo -e "\n=== Searching for scheduling validation ==="
rg -n 'status.*scheduled|schedule' --type=ts -C2 | grep -i 'campaign\|validate\|schema' | head -30

Length of output: 6392



---

🏁 Script executed:

```shell
#!/bin/bash
# Examine validate-campaign.ts to understand validation logic
echo "=== Contents of validate-campaign.ts ==="
cat apps/web/lib/api/campaigns/validate-campaign.ts

echo -e "\n=== PATCH handler in route.ts ==="
cat apps/web/app/(ee)/api/campaigns/[campaignId]/route.ts

echo -e "\n=== Schedule campaigns logic ==="
cat apps/web/lib/api/campaigns/schedule-campaigns.ts

Length of output: 3430



---

🏁 Script executed:

```shell
#!/bin/bash
# Use fd to find and read the PATCH route handler
echo "=== PATCH handler route.ts ==="
fd 'campaigns.*campaignId.*route' -e ts | head -1 | xargs cat

echo -e "\n=== Schedule campaigns logic ==="
cat apps/web/lib/api/campaigns/schedule-campaigns.ts

echo -e "\n=== Campaign status transitions constants ==="
cat apps/web/lib/api/campaigns/constants.ts

Length of output: 4070



---

🏁 Script executed:

```shell
#!/bin/bash
# Read the PATCH handler using different approach
echo "=== PATCH handler route.ts ==="
find apps/web -path "*campaigns*campaignId*route.ts" -type f | head -1 | xargs cat 2>/dev/null || rg -n "export const PATCH" --type=ts -A 50 -g "*campaigns*" | head -80

echo -e "\n=== Check if validateCampaign enforces from for scheduled status ==="
rg -n "validateCampaign|validateCampaignFromAddress" apps/web/app -A 5 -B 2 --type=ts | head -60

Length of output: 5851


Add validation to enforce from field is set before scheduling campaigns.

The schema and validation logic have a gap: a user can transition a campaign to "scheduled" status without providing a from field. The validateCampaign function only validates from if it's included in the update input (if (input.from)), but doesn't check if an existing campaign already has a from address before allowing a transition to scheduled status.

This is confirmed by a TODO comment in the broadcast handler: "We should make the from address required. There are existing campaign without from address"—indicating the team is aware of this gap.

Fix: In apps/web/lib/api/campaigns/validate-campaign.ts, add a check in the validateCampaign function to enforce that campaign.from exists before allowing a transition to the scheduled status:

if (input.status === CampaignStatus.scheduled) {
  if (!campaign.from) {
    throw new DubApiError({
      code: "bad_request",
      message: "Campaign must have a valid 'from' address before scheduling.",
    });
  }
}
🤖 Prompt for AI Agents
In apps/web/lib/api/campaigns/validate-campaign.ts around where validateCampaign
handles status updates (near transition to scheduled), ensure that when
input.status === CampaignStatus.scheduled you verify the existing campaign has a
from address; if not, throw a DubApiError with code "bad_request" and message
"Campaign must have a valid 'from' address before scheduling." so campaigns
cannot be moved to scheduled without a campaign.from value.

@steven-tey steven-tey merged commit 7bf5e73 into main Nov 1, 2025
8 checks passed
@steven-tey steven-tey deleted the program-email-domains branch November 1, 2025 19:30
@coderabbitai coderabbitai bot mentioned this pull request Dec 10, 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.

4 participants