Skip to content

Conversation

@marcusljf
Copy link
Collaborator

@marcusljf marcusljf commented Nov 6, 2025

Enables editing of subject, title, and body for partner invite emails in the dashboard. Updates the invite action and schema to accept custom email fields, and updates the email template to render user-provided markdown content. This allows program admins to personalize partner invitation emails.

CleanShot.2025-11-05.at.22.12.07.mp4

Summary by CodeRabbit

  • New Features
    • Customize partner invitation emails: editable Subject, Title, and Body (up to 3000 chars) with Edit/Save/Cancel workflow and per-field drafts.
    • Live Markdown authoring with preview and help link; rendered Markdown is included in sent emails.
    • Client- and server-side sanitization/validation of custom content before sending.
    • Email template now accepts custom subject/title/body and renders them in previews and outgoing messages.

Enables editing of subject, title, and body for partner invite emails in the dashboard. Updates the invite action and schema to accept custom email fields, and updates the email template to render user-provided markdown content. This allows program admins to personalize partner invitation emails.
@vercel
Copy link
Contributor

vercel bot commented Nov 6, 2025

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

Project Deployment Preview Updated (UTC)
dub Error Error Nov 6, 2025 9:02pm

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 6, 2025

Walkthrough

Adds editable Markdown-capable email subject/title/body to the partner-invite UI, validates and sanitizes these fields server-side, encapsulates the email send in a promise with logging and rollback on failure, and renders optional Markdown body in the ProgramInvite email template using ReactMarkdown/remark-gfm.

Changes

Cohort / File(s) Summary
UI — Invite sheet
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx
Adds editable email content (subject/title/body), draft vs saved state, edit/save/cancel handlers, Markdown hints, and a dual-mode preview/edit UI using ReactMarkdown/remark-gfm. Form submit includes email fields when present.
Server Action — Invite
apps/web/lib/actions/partners/invite-partner.ts
Parses emailSubject, emailTitle, emailBody; sanitizes body via sanitizeMarkdown; encapsulates email dispatch in a protected promise with try/catch and logging; sequences sending and audit logging via waitUntil; rolls back/deletes enrollment on send failure; includes optional email fields in ProgramInvite payload.
Validation — Zod schema
apps/web/lib/zod/schemas/partners.ts
Extends invitePartnerSchema with optional emailSubject, emailTitle, and emailBody (max length 3000).
Email template & deps
packages/email/src/templates/program-invite.tsx
packages/email/package.json
ProgramInvite accepts optional subject, title, body; derives defaults and conditionally renders provided Markdown body via ReactMarkdown + remark-gfm with rehype-sanitize and custom component mapping. Adds react-markdown, remark-gfm, rehype-sanitize dependencies.
Sanitizer util
apps/web/lib/partners/sanitize-markdown.ts
New export sanitizeMarkdown(markdown) that rejects null/empty input, removes null bytes, rejects overly long lines (>1000 chars), normalizes line endings, and returns sanitized string or null.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant U as User
    participant UI as Invite Sheet (client)
    participant F as Form Submit
    participant A as invitePartnerAction (server)
    participant S as Email Service
    participant T as ProgramInvite Template

    U->>UI: Click "Edit Email"
    UI->>UI: enter edit mode, update draft fields
    U->>UI: Edit subject/title/body
    UI->>UI: Click "Save" (persist to emailContent)
    U->>F: Submit invite form (includes emailSubject/title/body)
    F->>A: Invoke server action with parsed input
    A->>A: sanitizeMarkdown(emailBody)
    A->>S: sendEmail(payload with subject/title/body) in protected promise
    S->>T: Render ProgramInvite (ReactMarkdown if body present)
    T-->>S: Rendered HTML
    S-->>A: Send result (success/fail)
    A->>A: waitUntil(audit log + send promise) and finalize / rollback on failure
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Review focus:
    • apps/web/lib/actions/partners/invite-partner.ts — promise-based send, rollback/delete enrollment, and waitUntil sequencing.
    • packages/email/src/templates/program-invite.tsx — ReactMarkdown + rehype-sanitize usage and custom component mapping.
    • apps/web/lib/partners/sanitize-markdown.ts — edge-case checks (null bytes, long lines) and return semantics.
    • apps/web/app/.../invite-partner-sheet.tsx — state transitions (draft vs saved) and form inclusion of email fields.

Possibly related PRs

Suggested reviewers

  • TWilson023
  • steven-tey

Poem

🐰 I nibble text and stitch a line,
Subject, title, body — trimmed and fine.
Markdown hops with gentle cheer,
Edit, save, the invites appear.
Hop, send, partners draw near ✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.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 accurately and clearly describes the main feature added: enabling customizable email content for partner invites, which matches the primary purpose across all modified files.
✨ 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

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3551b92 and 310835a.

📒 Files selected for processing (1)
  • packages/email/src/templates/program-invite.tsx (4 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 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:

  • packages/email/src/templates/program-invite.tsx
📚 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:

  • packages/email/src/templates/program-invite.tsx
🧬 Code graph analysis (1)
packages/email/src/templates/program-invite.tsx (1)
packages/email/src/react-email.d.ts (8)
  • Text (15-15)
  • Link (14-14)
  • Html (4-4)
  • Head (5-5)
  • Preview (18-18)
  • Tailwind (19-19)
  • Heading (16-16)
  • Section (8-8)
⏰ 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 (4)
packages/email/src/templates/program-invite.tsx (4)

22-59: LGTM! Excellent email client compatibility approach.

The pixel-based preset directly addresses the email client compatibility concerns from previous reviews. Using pixels instead of rems ensures consistent rendering across Gmail, Outlook, and other email clients that strip or mishandle relative units.


61-191: Excellent inline style usage for email compatibility.

The markdownComponents implementation is solid. Using inline styles for critical elements (lists at lines 78-122, blockquotes at 131-146, code blocks at 148-187) ensures maximum compatibility across email clients. These are precisely the elements that commonly break with CSS classes alone.

The mix of inline styles for complex elements and Tailwind classes for simpler elements (p, a, strong, em, hr) is a pragmatic approach—React Email's rendering pipeline will inline the Tailwind classes, while your explicit inline styles guarantee the critical styling works everywhere.


221-240: Props and computed values look good.

The optional props are properly typed, and the computed emailTitle and emailSubject values have sensible fallbacks. The previous issue with unused emailSubject has been resolved—it's now correctly used in the <Preview> component at line 245.


261-290: Conditional rendering and security measures are well-implemented.

The conditional rendering logic is correct: custom markdown content when body is provided (lines 262-268), falling back to the default invitation text otherwise (lines 270-289). The ReactMarkdown integration with remarkGfm and rehypeSanitize provides both feature richness and XSS protection. Combined with the server-side sanitization mentioned in the PR summary, this gives you defense in depth.


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

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between aaba109 and 533cd47.

📒 Files selected for processing (4)
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx (5 hunks)
  • apps/web/lib/actions/partners/invite-partner.ts (2 hunks)
  • apps/web/lib/zod/schemas/partners.ts (1 hunks)
  • packages/email/src/templates/program-invite.tsx (4 hunks)
🧰 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/partners.ts
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx
📚 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:

  • packages/email/src/templates/program-invite.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/partners/invite-partner-sheet.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/partners/invite-partner-sheet.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/partners/invite-partner-sheet.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/partners/invite-partner-sheet.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/partners/invite-partner-sheet.tsx
📚 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-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
🧬 Code graph analysis (3)
packages/email/src/templates/program-invite.tsx (1)
packages/email/src/react-email.d.ts (3)
  • Heading (16-16)
  • Text (15-15)
  • Link (14-14)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx (3)
apps/web/lib/swr/use-program.ts (1)
  • useProgram (6-40)
apps/web/lib/swr/use-workspace.ts (1)
  • useWorkspace (6-46)
packages/ui/src/hooks/use-enter-submit.ts (1)
  • useEnterSubmit (3-25)
apps/web/lib/actions/partners/invite-partner.ts (3)
packages/email/src/index.ts (1)
  • sendEmail (6-29)
packages/email/src/templates/program-invite.tsx (1)
  • ProgramInvite (69-257)
apps/web/lib/api/partners/get-partner-invite-rewards-and-bounties.ts (1)
  • getPartnerInviteRewardsAndBounties (19-97)
⏰ 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

Replaced custom markdown parsing in the program invite email template with react-markdown and remark-gfm for improved markdown rendering. Updated dependencies to include react-markdown and remark-gfm.
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)
packages/email/src/templates/program-invite.tsx (1)

21-86: Consider email client compatibility for raw HTML elements.

The custom markdown components use raw HTML elements (<ul>, <ol>, <li>, <blockquote>, <pre>, <hr>) with Tailwind classes. While React Email's Tailwind wrapper converts classes to inline styles, several features may not render consistently across email clients:

  • List styling (list-disc, list-decimal, marker:) has limited support
  • Overflow handling (overflow-x-auto on code blocks) often doesn't work in email
  • Border positioning (border-l-2 on blockquotes) can be inconsistent
  • Advanced layout properties may degrade in Outlook, Gmail, etc.

You previously discussed using @react-email/markdown which is purpose-built for email rendering and handles these edge cases. If you encounter rendering issues during testing, that component remains a more robust option.

Test the rendered emails across major clients (Gmail, Outlook, Apple Mail, Yahoo) to ensure markdown elements display as expected. Pay particular attention to:

  • Code blocks with long lines
  • Nested lists
  • Blockquotes
  • List markers
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 533cd47 and 466a5d7.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (2)
  • packages/email/package.json (1 hunks)
  • packages/email/src/templates/program-invite.tsx (4 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 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:

  • packages/email/src/templates/program-invite.tsx
🧬 Code graph analysis (1)
packages/email/src/templates/program-invite.tsx (1)
packages/email/src/react-email.d.ts (3)
  • Text (15-15)
  • Link (14-14)
  • Heading (16-16)
⏰ 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: Socket Security: Pull Request Alerts
  • GitHub Check: build
🔇 Additional comments (5)
packages/email/src/templates/program-invite.tsx (4)

17-18: LGTM!

The imports are appropriate for adding markdown rendering support to the email template.


116-118: LGTM!

The new optional props for customizable email content are correctly typed and align with the PR objectives.

Also applies to: 129-131


133-135: LGTM!

The derived email metadata with fallback values is well-structured and provides sensible defaults.


152-153: LGTM!

The heading correctly uses the customizable email title.

packages/email/package.json (1)

16-17: No action required—both packages are secure and appropriately versioned.

react-markdown v9.0.3 is secure with no known vulnerabilities, and the breaking change in v10 (className prop removal) does not affect this codebase, which uses custom component mapping and remark plugins instead. remark-gfm v4.0.1 includes only type and documentation improvements, making the current v4.0.0 safe and stable. Both packages are production-ready.

Added a sanitizeMarkdown utility to clean and validate markdown content for partner invite emails. Enforced a 3000 character limit for the email body in both the UI and schema, truncating and notifying users if exceeded. Updated the email template to use rehype-sanitize for additional HTML sanitization, and added the rehype-sanitize dependency.
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: 3

Caution

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

⚠️ Outside diff range comments (1)
apps/web/lib/actions/partners/invite-partner.ts (1)

60-103: Email failure leaves orphaned partner enrollment.

The partner is created and enrolled (lines 60-71) before the email is sent (lines 76-99). If sendEmail fails, the partner enrollment persists but no invitation email is delivered. This creates an inconsistent state where the partner appears "invited" in the database but never received the invitation.

Consider either:

  1. Transactional approach: Wrap the enrollment and email send in a transaction-like pattern where enrollment is marked as "pending" until email succeeds, or
  2. Idempotent retry: Store the email send as a queued task that can be retried, or
  3. Rollback on failure: Delete the enrollment if email sending fails:
  const enrolledPartner = await createAndEnrollPartner({
    workspace,
    program,
    partner: {
      email,
      username,
      ...(groupId && { groupId }),
    },
    userId: user.id,
    skipEnrollmentCheck: true,
    status: "invited",
  });

  const sanitizedEmailBody = emailBody ? sanitizeMarkdown(emailBody) : null;

  try {
    await sendEmail({
      subject:
        emailSubject || `${program.name} invited you to join Dub Partners`,
      variant: "notifications",
      to: email,
      replyTo: program.supportEmail || "noreply",
      react: ProgramInvite({
        email,
        name: enrolledPartner.name,
        program: {
          name: program.name,
          slug: program.slug,
          logo: program.logo,
        },
        ...(emailSubject && { subject: emailSubject }),
        ...(emailTitle && { title: emailTitle }),
        ...(sanitizedEmailBody && { body: sanitizedEmailBody }),
        ...(await getPartnerInviteRewardsAndBounties({
          programId,
          groupId: enrolledPartner.groupId || program.defaultGroupId,
        })),
      }),
    });
  } catch (error) {
    console.error("Failed to send partner invite email", error);
+   // Rollback the enrollment since email failed
+   await prisma.programEnrollment.delete({
+     where: { id: enrolledPartner.id },
+   });
    throw error;
  }
🧹 Nitpick comments (2)
apps/web/lib/partners/sanitize-markdown.ts (1)

15-49: Consider renaming to reflect validation/normalization behavior.

The function name sanitizeMarkdown suggests content sanitization (removing dangerous HTML/XSS), but the implementation primarily validates and normalizes (trims, checks for binary content, normalizes line endings). The actual HTML sanitization happens downstream via rehype-sanitize in the email template.

Consider renaming to validateMarkdown or normalizeMarkdown for clarity, or add a comment explaining that XSS/HTML sanitization occurs separately.

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

110-113: Consider warning users before they exceed the character limit.

Currently, truncation only shows an error toast after the user clicks "Save" (line 112). This could frustrate users who spend time crafting content beyond 3000 characters only to have it silently truncated.

Consider one of these UX improvements:

  1. Real-time character counter: Show remaining characters as they type (e.g., "2,500 / 3,000 characters")
  2. Warning threshold: Show a warning toast when they approach the limit (e.g., at 2,800 characters)
  3. Prevent input beyond limit: The maxLength={3000} on the textarea (line 399) already prevents typing beyond the limit, but copy-paste could bypass it

Since the textarea already has maxLength={3000}, the truncation logic may be redundant unless there's a path where users can bypass it.

Verify if the truncation logic (lines 110-113) is reachable given the maxLength={3000} attribute on the textarea (line 399).

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 466a5d7 and 15db264.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (6)
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx (5 hunks)
  • apps/web/lib/actions/partners/invite-partner.ts (3 hunks)
  • apps/web/lib/partners/sanitize-markdown.ts (1 hunks)
  • apps/web/lib/zod/schemas/partners.ts (1 hunks)
  • packages/email/package.json (1 hunks)
  • packages/email/src/templates/program-invite.tsx (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/lib/zod/schemas/partners.ts
🧰 Additional context used
🧠 Learnings (7)
📚 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:

  • packages/email/src/templates/program-invite.tsx
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.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/partners/invite-partner-sheet.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/partners/invite-partner-sheet.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/partners/invite-partner-sheet.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/partners/invite-partner-sheet.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/partners/invite-partner-sheet.tsx
📚 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/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx
🧬 Code graph analysis (3)
packages/email/src/templates/program-invite.tsx (1)
packages/email/src/react-email.d.ts (3)
  • Text (15-15)
  • Link (14-14)
  • Heading (16-16)
apps/web/lib/actions/partners/invite-partner.ts (4)
apps/web/lib/partners/sanitize-markdown.ts (1)
  • sanitizeMarkdown (15-49)
packages/email/src/index.ts (1)
  • sendEmail (6-29)
packages/email/src/templates/program-invite.tsx (1)
  • ProgramInvite (89-254)
apps/web/lib/api/partners/get-partner-invite-rewards-and-bounties.ts (1)
  • getPartnerInviteRewardsAndBounties (19-97)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx (4)
apps/web/lib/swr/use-program.ts (1)
  • useProgram (6-40)
apps/web/lib/swr/use-workspace.ts (1)
  • useWorkspace (6-46)
packages/ui/src/hooks/use-enter-submit.ts (1)
  • useEnterSubmit (3-25)
packages/email/src/templates/program-invite.tsx (6)
  • a (28-38)
  • p (23-27)
  • strong (56-58)
  • ul (39-45)
  • ol (46-52)
  • li (53-55)
⏰ 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: Socket Security: Pull Request Alerts
  • GitHub Check: build
🔇 Additional comments (5)
packages/email/package.json (1)

16-18: LGTM! Dependency additions support the Markdown rendering pipeline.

The three new dependencies correctly enable Markdown rendering with GitHub Flavored Markdown support and HTML sanitization for email templates.

packages/email/src/templates/program-invite.tsx (1)

22-87: Comprehensive Markdown component mapping with consistent styling.

The markdownComponents implementation correctly maps Markdown elements to styled React Email components, handling:

  • Paragraphs, links with proper targets
  • Lists (ul/ol) with custom markers
  • Inline/block code with appropriate styling
  • Typography (strong, em, blockquote)
  • Horizontal rules

The styling is consistent with the existing email design and uses email-safe Tailwind classes.

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

44-129: Well-structured email editing workflow with proper state management.

The implementation correctly handles:

  • Memoized default content derived from program name
  • Separate emailContent (saved) and draftEmailContent (editing) state
  • Edit/save/cancel workflow with proper state transitions
  • Fallback to defaults for empty fields
  • 3000-character limit enforcement with user feedback

The state management ensures that canceling edits properly reverts to the last saved state, and empty fields fall back to sensible defaults.


451-502: Excellent alignment between preview and email template rendering.

The ReactMarkdown implementation in the preview (lines 451-502) uses the same plugins (remarkGfm) and similar component styling as the email template (packages/email/src/templates/program-invite.tsx), ensuring that what users see in the preview matches what recipients receive. This addresses the past review concern about Markdown parsing alignment.

The component overrides correctly:

  • Style links, paragraphs, lists consistently
  • Use cn() for class composition
  • Maintain responsive styling

271-271: Good UX: disabling submit during email editing.

Disabling the "Send invite" button while isEditingEmail is true (line 271) prevents users from accidentally submitting with unsaved draft changes. They must either save or cancel their edits before submitting the form.

Refactors the partner invite action to rollback partner enrollment if sending the invite email fails. Updates markdown sanitization to reject content with excessively long lines instead of truncating. Refactors program invite email template to use inline styles for better compatibility and introduces a pixel-based Tailwind config for consistent styling.
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/partners/sanitize-markdown.ts (1)

15-43: Consider adding a total length limit.

While the per-line length check (1000 chars) prevents individual malformed lines, there's no limit on the total content size. A user could submit tens of thousands of 999-character lines, potentially causing memory or processing issues.

Consider adding a check before line splitting:

  // Return null if empty after trimming
  if (!sanitized) {
    return null;
  }
+
+  // Limit total content size to prevent DoS
+  const maxTotalLength = 10000; // Adjust based on requirements
+  if (sanitized.length > maxTotalLength) {
+    return null;
+  }

  // Check for binary content - markdown should be valid UTF-8 text
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 15db264 and 66d0e83.

📒 Files selected for processing (3)
  • apps/web/lib/actions/partners/invite-partner.ts (3 hunks)
  • apps/web/lib/partners/sanitize-markdown.ts (1 hunks)
  • packages/email/src/templates/program-invite.tsx (4 hunks)
🧰 Additional context used
🧠 Learnings (3)
📚 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
  • packages/email/src/templates/program-invite.tsx
📚 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-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:

  • packages/email/src/templates/program-invite.tsx
🧬 Code graph analysis (2)
apps/web/lib/actions/partners/invite-partner.ts (5)
apps/web/lib/partners/sanitize-markdown.ts (1)
  • sanitizeMarkdown (15-49)
apps/web/lib/api/partners/get-partner-invite-rewards-and-bounties.ts (1)
  • getPartnerInviteRewardsAndBounties (19-97)
packages/email/src/index.ts (1)
  • sendEmail (6-29)
packages/email/src/templates/program-invite.tsx (1)
  • ProgramInvite (191-356)
packages/prisma/index.ts (1)
  • prisma (3-9)
packages/email/src/templates/program-invite.tsx (1)
packages/email/src/react-email.d.ts (8)
  • Text (15-15)
  • Link (14-14)
  • Html (4-4)
  • Head (5-5)
  • Preview (18-18)
  • Tailwind (19-19)
  • Heading (16-16)
  • Section (8-8)
⏰ 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)
packages/email/src/templates/program-invite.tsx (4)

22-59: LGTM! Email-safe pixel-based preset.

The pixel-based preset correctly avoids rem units and other CSS features that email clients strip. This addresses the email client compatibility concerns from previous reviews.


61-189: LGTM! Email-safe Markdown components.

The custom components correctly use inline styles for cross-client compatibility. The list styling with explicit listStyleType and listStylePosition avoids the ::marker pseudo-element issues flagged in previous reviews, and arbitrary Tailwind values have been eliminated.


219-238: LGTM! Clear default values.

The optional email customization props with sensible defaults provide the right balance between flexibility and fallback behavior.


259-288: LGTM! Secure Markdown rendering with appropriate fallback.

The conditional rendering correctly uses rehypeSanitize for XSS protection and provides a sensible fallback. Note that an empty string body will trigger the fallback, which appears intentional given the upstream sanitization returns null for empty content.

apps/web/lib/partners/sanitize-markdown.ts (1)

45-49: LGTM! Standard line ending normalization.

The line ending normalization is correct and follows standard practices.

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

123-141: Promise.allSettled correctly ensures both tasks complete.

The use of Promise.allSettled ensures the audit log is recorded even if email sending fails. However, note the critical issue above regarding response timing—the action returns success before knowing whether the email succeeded.

Adds a check to throw an error if the sanitized email body is invalid, providing a clear message for unsupported content. Simplifies error handling and logging when sending partner invite emails by removing the rollback logic and including more context in the log output.
Improved code formatting in sanitizeMarkdown function and JSX strong element for better readability and consistency. No functional changes were made.
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

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fe8ceb1 and 3551b92.

📒 Files selected for processing (2)
  • apps/web/lib/partners/sanitize-markdown.ts (1 hunks)
  • packages/email/src/templates/program-invite.tsx (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/lib/partners/sanitize-markdown.ts
🧰 Additional context used
🧠 Learnings (2)
📚 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:

  • packages/email/src/templates/program-invite.tsx
📚 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:

  • packages/email/src/templates/program-invite.tsx
🧬 Code graph analysis (1)
packages/email/src/templates/program-invite.tsx (1)
packages/email/src/react-email.d.ts (8)
  • Text (15-15)
  • Link (14-14)
  • Html (4-4)
  • Head (5-5)
  • Preview (18-18)
  • Tailwind (19-19)
  • Heading (16-16)
  • Section (8-8)
⏰ 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

Replaces the hardcoded preview text with the dynamic emailSubject variable in the program invite email template for improved customization.
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.

2 participants