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

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
    • Partner invite emails can now be customized with editable subject, title, and body content
    • Email templates support rich text editing with Markdown formatting
    • Preview changes in real-time before sending partner invites
    • Saved email templates are persisted and reused for future invites

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 Ready Ready Preview Nov 15, 2025 6:41am

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 6, 2025

Walkthrough

This PR introduces email template customization for partner invitations. Users can edit invitation email subject, title, and body (with Markdown rendering) through an enhanced UI, save templates to the database, and apply saved content when sending invites. Includes input sanitization, Zod validation, and preview functionality.

Changes

Cohort / File(s) Summary
Email Editing UI
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx
Replaces simple invite flow with rich email template editing: adds editing state management, draft/saved content tracking, in-place editing controls for subject/title/body with RichText editor, keyboard shortcuts (Ctrl/Cmd+Enter to save), and preview/edit mode switching.
Server Actions
apps/web/lib/actions/partners/save-invite-email-data.ts, apps/web/lib/actions/partners/invite-partner.ts
save-invite-email-data: new action to persist email templates (subject, title, body) to Program record with sanitization validation. invite-partner: updated to fetch and apply saved email data as template overrides during invite send.
Schemas & Validation
apps/web/lib/zod/schemas/partners.ts, apps/web/lib/zod/schemas/program-invite-email.ts, apps/web/lib/api/programs/get-program-or-throw.ts
Adds optional emailSubject/emailTitle/emailBody fields to invitePartnerSchema; introduces new programInviteEmailDataSchema for email template data; extends ProgramSchema parsing with template validation.
Database
packages/prisma/schema/program.prisma
Adds optional JSON field inviteEmailData to Program model to store email template customizations.
Email Template & Rendering
packages/email/src/templates/program-invite.tsx, packages/email/package.json
Extends ProgramInvite component to accept subject/title/body props; renders custom body as Markdown (falls back to static content if not provided); updates preview/heading to use customized values; adds @react-email/markdown dependency.
Utilities & Types
apps/web/lib/partners/sanitize-markdown.ts, apps/web/lib/types.ts
New sanitizeMarkdown function validates and cleans Markdown input (rejects binary content, lines >1000 chars, empty strings); exports ProgramInviteEmailData type for template data typing.

Sequence Diagram

sequenceDiagram
    participant User
    participant UI as Edit UI
    participant Action as save-invite-email-data
    participant DB as Database
    participant Partner as Partner Invite Send

    rect rgb(200, 220, 255)
        note over User, DB: Email Template Customization
        User->>UI: Edit subject/title/body
        UI->>UI: Track draft changes
        User->>UI: Click Save (or Ctrl+Enter)
        UI->>Action: saveInviteEmailDataAction(subject, title, body)
        Action->>Action: Validate & sanitize body
        Action->>DB: Update Program.inviteEmailData
        DB-->>Action: Confirm
        Action-->>UI: Success
        UI->>UI: Update saved state, exit edit mode
        UI-->>User: Show confirmation
    end

    rect rgb(220, 255, 220)
        note over User, Partner: Send Partner Invites
        User->>UI: Click Send Invite
        UI->>Partner: Trigger invite send
        Partner->>DB: Fetch Program + inviteEmailData
        Partner->>Partner: Apply email template overrides
        Partner->>Partner: Send customized email
        Partner-->>UI: Invite sent
        UI-->>User: Show success
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Areas requiring attention:
    • Email editing UI state management complexity in invite-partner-sheet.tsx (form state, draft tracking, edit modes)
    • Sanitization logic in sanitize-markdown.ts (line length limits, null byte detection, DoS considerations)
    • Server action save-invite-email-data.ts validation chain and program resolution logic
    • Email template rendering logic in program-invite.tsx (Markdown fallback handling, prop integration)

Possibly related PRs

  • Partner invite email updatesΒ #2965: Modifies partner invite email sending to enrich payload with template data (subject, title, body) and pre-fetched rewards/bountiesβ€”directly related as this PR builds on that foundation.

Poem

🐰 A rabbit hops through templates bright,
Customizing invites left and right,
Subject and body now edible tales,
Markdown-styled without fail,
Partners invited with flair! ✨

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 describes the main change: adding customizable email content for partner invites, which is the core feature implemented 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

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.
@socket-security
Copy link

socket-security bot commented Nov 15, 2025

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Added@​react-email/​markdown@​0.0.17100100100100100

View full report

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 (4)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx (4)

266-273: Consider adding a tooltip to explain disabled submit button.

The submit button is correctly disabled during email editing (line 272), but users might not understand why. A tooltip would improve clarity.

 <Button
   type="submit"
   variant="primary"
   text="Send invite"
   className="w-fit"
   loading={isPending}
   disabled={isPending || !email || isEditingEmail}
+  {...(isEditingEmail && {
+    disabledTooltip: "Save or cancel email changes before sending invite"
+  })}
 />

476-486: Dynamic key causes unnecessary remounts on body changes.

Using the full email body content in the key (line 477) forces the RichTextProvider to remount every time the body changes, which may cause performance issues and lose editor state unnecessarily.

Consider using a stable key or incrementing counter:

+// At component level
+const [previewKey, setPreviewKey] = useState(0);

+// In handleSaveEmail, increment the key when content is saved
+setPreviewKey(prev => prev + 1);

 <RichTextProvider
-  key={`preview-${displayContent.body}`}
+  key={`preview-${previewKey}`}
   features={["bold", "italic", "links"]}
   style="condensed"
   markdown
   editable={false}
   initialValue={displayContent.body}

Alternatively, if remounting is intentional to reset editor state on content changes, add a comment explaining this choice.


348-397: Consider extracting a helper to reduce code duplication.

The pattern for updating draftEmailContent is repeated for subject (lines 362-366) and title (lines 387-391) fields.

// Helper function
const updateDraftField = (field: keyof EmailContent, value: string) => {
  setDraftEmailContent({
    ...draftEmailContent,
    [field]: value,
  });
};

// Then use it:
<input
  id="email-subject"
  type="text"
  value={draftEmailContent.subject}
  onChange={(e) => updateDraftField('subject', e.target.value)}
  // ...
/>

<input
  id="email-title"
  type="text"
  value={draftEmailContent.title}
  onChange={(e) => updateDraftField('title', e.target.value)}
  // ...
/>

280-499: Consider splitting the large EmailPreview component.

At 219 lines, the EmailPreview component handles multiple concerns: rendering editing UI, preview UI, and managing RichText editor state. This makes it harder to maintain and test.

Consider extracting sub-components:

  1. EmailEditForm - handles the editing UI (lines 348-449)
  2. EmailPreviewDisplay - handles the preview UI (lines 450-494)
  3. Keep EmailPreview as the coordinator

Example structure:

function EmailPreview({ ... }) {
  // State and effects remain here
  return (
    <div className="mt-6 rounded-lg border border-neutral-200 bg-neutral-50">
      <EmailPreviewHeader 
        isEditingEmail={isEditingEmail}
        onStartEditing={onStartEditing}
        onSave={onSave}
        onCancel={onCancel}
      />
      <div className="...">
        {isEditingEmail ? (
          <EmailEditForm 
            draftEmailContent={draftEmailContent}
            setDraftEmailContent={setDraftEmailContent}
            richTextRef={richTextRef}
            onSave={onSave}
          />
        ) : (
          <EmailPreviewDisplay 
            displayContent={displayContent}
            program={program}
          />
        )}
      </div>
    </div>
  );
}

This would improve readability and make each component easier to test in isolation.

πŸ“œ Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 310835a and 8cf68ec.

πŸ“’ Files selected for processing (1)
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx (5 hunks)
🧰 Additional context used
🧠 Learnings (6)
πŸ“š 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-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/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-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 (1)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx (5)
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/rich-text-area/rich-text-provider.tsx (1)
  • RichTextProvider (69-242)
packages/ui/src/rich-text-area/index.tsx (1)
  • RichTextArea (9-32)
packages/ui/src/rich-text-area/rich-text-toolbar.tsx (1)
  • RichTextToolbar (16-129)
⏰ 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)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx (4)

12-14: LGTM! Clean imports and type definition.

The imports are well-organized and the EmailContent type clearly defines the structure for customizable email fields.

Also applies to: 18-18, 20-27, 38-42


49-63: Well-structured state management for email editing.

The use of separate emailContent (saved) and draftEmailContent (working draft) states enables a clean cancel flow. The memoized default content is computed efficiently.


88-102: Form submission correctly includes optional email fields.

The conditional spread pattern ensures email customization fields are only sent when the user has edited them, keeping the payload clean for default invites.


415-421: No changes required β€” the type cast is the correct way to use the official @tiptap/markdown API.

The getMarkdown() method is a documented method added by the @tiptap/markdown extension and works correctly across the codebase. The type cast to any is necessary because @tiptap adds this method at runtime through its extension system, so it's not exposed in the Editor type definitions. This pattern is used consistently throughout the codebase (message-input.tsx, add-edit-integration-form.tsx, add-edit-app-form.tsx, etc.) and is the standard way to access @tiptap extension methods in TypeScript.

Likely an incorrect or invalid review comment.

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

πŸ“œ Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 8cf68ec and 8acdedd.

πŸ“’ 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 (7)
  • Html (4-4)
  • Head (5-5)
  • Preview (18-18)
  • Heading (16-16)
  • Text (15-15)
  • Link (14-14)
  • 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 (1)
packages/email/src/templates/program-invite.tsx (1)

148-151: Dynamic subject/title wiring looks good

Using emailTitle/emailSubject for the main heading and <Preview> aligns the template with the customizable subject/title behavior and removes the previous unused-variable issue. The smaller text-xs on the CTA also matches the more compact layout.

No further changes needed here from my side.

Also applies to: 155-155, 168-168, 200-200

@steven-tey
Copy link
Collaborator

@coderabbitai full review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 15, 2025

βœ… 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/partners/invite-partner-sheet.tsx (1)

331-365: Document the intentional useEffect dependency suppression in EmailPreview

The useEffect that syncs the editor when entering edit mode:

const prevIsEditingEmail = useRef(isEditingEmail);
useEffect(() => {
  if (isEditingEmail && !prevIsEditingEmail.current && richTextRef.current) {
    richTextRef.current.setContent(draftEmailContent.body);
  }
  prevIsEditingEmail.current = isEditingEmail;
  // eslint-disable-next-line react-hooks/exhaustive-deps
}, [isEditingEmail]);

intentionally ignores draftEmailContent.body so the content is only injected once when toggling into edit mode (subsequent changes are driven by the editor itself). To make this clearer for future maintainers and justify the ESLint override, consider adding an inline comment above the disable explaining that draftEmailContent.body is deliberately excluded and that initialValue/editor updates handle ongoing changes.

The preview-side RichTextProvider with editable={false} and the markdown configuration looks good and closely matches what the email template sends.

Also applies to: 462-500, 531-541

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

72-132: Invite email customization wiring looks correct; consider DRY-ing subject handling

The new flow correctly:

  • Pulls inviteEmailData from program,
  • Uses the custom subject when present (with a sensible default),
  • Passes optional subject/title/body into ProgramInvite,
  • Sends the email in a background task while logging failures, without rolling back enrollment.

To avoid future drift between the sendEmail subject and the template’s default, consider computing the subject once and reusing it:

-    const inviteEmailData = program.inviteEmailData;
+    const inviteEmailData = program.inviteEmailData;
+    const emailSubject =
+      inviteEmailData?.subject ||
+      `${program.name} invited you to join Dub Partners`;
@@
-        await sendEmail({
-          subject:
-            inviteEmailData?.subject ||
-            `${program.name} invited you to join Dub Partners`,
+        await sendEmail({
+          subject: emailSubject,
@@
-          react: ProgramInvite({
+          react: ProgramInvite({
             email,
             name: enrolledPartner.name,
             program: {
               name: program.name,
               slug: program.slug,
               logo: program.logo,
             },
-            ...(inviteEmailData?.subject && {
-              subject: inviteEmailData.subject,
-            }),
+            subject: emailSubject,
             ...(inviteEmailData?.title && { title: inviteEmailData.title }),
             ...(inviteEmailData?.body && { body: inviteEmailData.body }),
             ...rewardsAndBounties,
           }),

This keeps the subject line consistent between the actual email header and the rendered template, and centralizes the default logic.

πŸ“œ Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 8acdedd and 3052dc0.

πŸ“’ Files selected for processing (7)
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx (6 hunks)
  • apps/web/lib/actions/partners/invite-partner.ts (1 hunks)
  • apps/web/lib/actions/partners/save-invite-email-data.ts (1 hunks)
  • apps/web/lib/zod/schemas/program-invite-email.ts (1 hunks)
  • apps/web/lib/zod/schemas/programs.ts (2 hunks)
  • packages/email/src/templates/program-invite.tsx (4 hunks)
  • packages/prisma/schema/program.prisma (1 hunks)
🧰 Additional context used
🧠 Learnings (10)
πŸ“š 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
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx
  • apps/web/lib/actions/partners/save-invite-email-data.ts
πŸ“š Learning: 2025-11-12T22:23:10.390Z
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.390Z
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
  • apps/web/lib/actions/partners/save-invite-email-data.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
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx
  • apps/web/lib/actions/partners/save-invite-email-data.ts
πŸ“š Learning: 2025-10-17T08:18:19.278Z
Learnt from: devkiran
Repo: dubinc/dub PR: 0
File: :0-0
Timestamp: 2025-10-17T08:18:19.278Z
Learning: In the apps/web codebase, `@/lib/zod` should only be used for places that need OpenAPI extended zod schema. All other places should import from the standard `zod` package directly using `import { z } from "zod"`.

Applied to files:

  • apps/web/lib/zod/schemas/program-invite-email.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
  • 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/lib/zod/schemas/programs.ts
  • 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
🧬 Code graph analysis (5)
apps/web/lib/actions/partners/invite-partner.ts (3)
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 (103-264)
packages/email/src/templates/program-invite.tsx (1)
packages/email/src/react-email.d.ts (7)
  • Html (4-4)
  • Head (5-5)
  • Preview (18-18)
  • Heading (16-16)
  • Text (15-15)
  • Link (14-14)
  • Section (8-8)
apps/web/lib/zod/schemas/programs.ts (1)
apps/web/lib/zod/schemas/program-invite-email.ts (1)
  • programInviteEmailDataSchema (3-9)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx (7)
apps/web/lib/swr/use-program.ts (1)
  • useProgram (6-40)
apps/web/lib/swr/use-workspace.ts (1)
  • useWorkspace (6-46)
apps/web/lib/actions/partners/invite-partner.ts (1)
  • invitePartnerAction (15-133)
apps/web/lib/actions/partners/save-invite-email-data.ts (1)
  • saveInviteEmailDataAction (16-45)
packages/ui/src/rich-text-area/rich-text-provider.tsx (1)
  • RichTextProvider (69-242)
packages/ui/src/rich-text-area/index.tsx (1)
  • RichTextArea (9-32)
packages/ui/src/rich-text-area/rich-text-toolbar.tsx (1)
  • RichTextToolbar (16-129)
apps/web/lib/actions/partners/save-invite-email-data.ts (3)
apps/web/lib/actions/safe-action.ts (1)
  • authActionClient (39-88)
apps/web/lib/partners/sanitize-markdown.ts (1)
  • sanitizeMarkdown (15-51)
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 (7)
apps/web/lib/zod/schemas/programs.ts (1)

21-54: ProgramSchema wiring for inviteEmailData looks consistent

inviteEmailData: programInviteEmailDataSchema cleanly exposes the new JSON field on Program while keeping it optional/nullish and aligned with the Prisma Json? column and the dedicated schema file. No issues from a typing or serialization standpoint.

packages/prisma/schema/program.prisma (1)

42-44: New inviteEmailData column is well-placed and typed

Adding inviteEmailData Json? @db.Json to Program matches how other structured config blobs are stored (embedData, resources) and lines up with the Zod schema’s nullable shape. Migration-wise this is a safe additive change.

apps/web/lib/zod/schemas/program-invite-email.ts (1)

1-9: Invite email data schema matches persisted shape

programInviteEmailDataSchema (subject/title/body as strings, nullish) matches what the server action writes into Program.inviteEmailData and what the email template expects. Using the plain zod import here is also consistent with the apps/web schema conventions.

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

131-196: Markdown-powered body, subject/title props, and Preview wiring look solid

The new optional subject, title, and body props, together with emailTitle/emailSubject and the conditional {body ? <Markdown> : fallback copy}, cleanly support customized content while preserving the existing default copy. Using the same emailSubject for <Preview> ensures inbox preview text matches the actual subject logic.


21-101: I'll verify the claim about the @react-email/markdown library and the correct key for styling links.

Based on my web search of @react-email/markdown documentation which states that markdownCustomStyles provides custom styles for the corresponding html element (p, h1, h2, etc.), I was unable to find definitive documentation confirming whether the key for styling anchors should be link or a in the markdownCustomStyles object.

The official React Email documentation and examples consistently show keys like h1, h2, and codeInline, but notably no examples demonstrate anchor/link styling. While the review comment's reasoning that anchors render as <a> elements (and thus the key should be a) is logically sound, I cannot confirm from available documentation whether @react-email/markdown indeed expects the key a rather than link for anchor styling.

Verify the correct key for styling links in @react-email/markdown

Please confirm whether the markdownCustomStyles object should use link or a as the key for styling anchors by:

  • Testing the current implementation with the link key to see if anchor styles apply
  • Checking the @react-email/markdown source code or TypeScript type definitions to see what keys are supported
  • Consulting the library's repository issues or discussions for examples of link styling
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx (1)

51-81: Email editing state and save flow are coherent and aligned with server validation

The combination of:

  • defaultEmailContent derived from program?.name,
  • savedEmailContent built from program.inviteEmailData,
  • local emailContent vs draftEmailContent state, and
  • saveInviteEmailDataAction with trimming and a 3000‑char limit

gives a clear UX: admins can edit a draft, save to persist (with truncation surfaced via toast), and immediately see the updated preview. The main β€œSend invite” button being disabled while editing or while a save is in flight avoids sending with partially edited or unsaved content. This all matches the server schema and sanitization behavior.

Also applies to: 107-183, 292-323

apps/web/lib/actions/partners/save-invite-email-data.ts (1)

9-45: Validation and sanitization for invite email data are robust

saveInviteEmailDataAction:

  • Requires workspaceId, non-empty subject/title, and a trimmed body capped at 3000 characters, and
  • Runs sanitizeMarkdown(body) and throws if the content is invalid (binary/null bytes, excessively long lines, etc.),

before updating Program.inviteEmailData with trimmed subject/title and the sanitized body. This eliminates the earlier silent-failure risk around markdown sanitization and ensures only safe, bounded content is persisted.

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

♻️ Duplicate comments (3)
packages/email/src/templates/program-invite.tsx (1)

28-34: Fix the markdownCustomStyles key for links.

The link key won't match anchor elements rendered by @react-email/markdown. The component expects the key a to style anchor tags.

Apply this diff:

-  link: {
+  a: {
     fontWeight: "600",
     color: "#000000",
     textDecoration: "underline",
     textDecorationStyle: "dotted",
     textUnderlineOffset: "2px",
   },
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx (2)

358-364: Exhaustive-deps concern persists from previous review.

The useEffect intentionally excludes draftEmailContent.body from dependencies, which could cause stale editor content if the body changes while not in edit mode.

Refer to the previous review comment for suggested solutions: either add an inline comment explaining the intentional exclusion, or include draftEmailContent.body in the dependency array with appropriate guards.


152-156: Truncation UX concern persists from previous review.

As noted in the prior review, automatically truncating and saving content without explicit user confirmation could lead to unintended data loss, especially if the toast notification is missed.

Refer to the previous review comment for suggested alternatives (prevent saving until within limit, or show confirmation dialog).

🧹 Nitpick comments (3)
apps/web/lib/partners/sanitize-markdown.ts (1)

38-45: Consider more granular validation for excessively long lines.

The current implementation rejects the entire input if any single line exceeds 1000 characters. While this prevents broken Markdown from truncation, it may be too strict for legitimate use casesβ€”a single long URL or table row would cause the entire email body to be rejected.

Consider these alternatives:

  1. Line-by-line rejection: Return an error message indicating which line(s) are too long so users can fix specific issues.
  2. Smart truncation: Detect Markdown constructs (links, code blocks) and truncate only at safe boundaries.
  3. Document the limit: If 1000 chars/line is the hard limit, ensure the UI clearly communicates this constraint.

Example of providing more context:

- const hasExcessivelyLongLine = sanitized
-   .split("\n")
-   .some((line) => line.length > maxLineLength);
-
- if (hasExcessivelyLongLine) {
-   return null;
- }
+ const lines = sanitized.split("\n");
+ const longLineIndex = lines.findIndex((line) => line.length > maxLineLength);
+ 
+ if (longLineIndex !== -1) {
+   throw new Error(
+     `Line ${longLineIndex + 1} exceeds maximum length of ${maxLineLength} characters (${lines[longLineIndex].length} chars)`
+   );
+ }

This provides actionable feedback to users while maintaining security.

apps/web/lib/actions/partners/save-invite-email-data.ts (1)

37-43: Remove redundant trimming.

The schema already trims subject and title via z.string().trim(), so the additional .trim() calls at lines 39-40 are redundant.

Apply this diff:

     data: {
       inviteEmailData: {
-        subject: subject.trim(),
-        title: title.trim(),
+        subject,
+        title,
         body: sanitizedBody,
       },
     },
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx (1)

143-177: Simplify the validation logic.

The function creates two sets of variables with redundant fallback logic:

  • Lines 158-162: updatedContent with fallbacks
  • Lines 165-168: final* variables with the same fallback pattern

Since the server schema already validates min(1) after trim(), the client can rely on server-side validation and simplify this code.

Apply this diff to streamline the logic:

 const handleSaveEmail = async () => {
   if (!workspaceId) {
     return;
   }

-  const sanitizedSubject = draftEmailContent.subject.trim();
-  const sanitizedTitle = draftEmailContent.title.trim();
-  let sanitizedBody = draftEmailContent.body.trim();
+  let body = draftEmailContent.body.trim();

   // Enforce max length validation (matches schema)
-  if (sanitizedBody.length > 3000) {
-    sanitizedBody = sanitizedBody.substring(0, 3000);
+  if (body.length > 3000) {
+    body = body.substring(0, 3000);
     toast.error("Email body was truncated to 3000 characters");
   }

-  const updatedContent: EmailContent = {
-    subject: sanitizedSubject || defaultEmailContent.subject,
-    title: sanitizedTitle || defaultEmailContent.title,
-    body: sanitizedBody || defaultEmailContent.body,
-  };
-
-  // Ensure all values are non-empty (schema requirement)
-  const finalSubject =
-    updatedContent.subject.trim() || defaultEmailContent.subject;
-  const finalTitle = updatedContent.title.trim() || defaultEmailContent.title;
-  const finalBody = updatedContent.body.trim() || defaultEmailContent.body;
-
   // Save to server (state updates happen in onSuccess callback)
   await saveEmailDataAsync({
     workspaceId,
-    subject: finalSubject,
-    title: finalTitle,
-    body: finalBody,
+    subject: draftEmailContent.subject.trim() || defaultEmailContent.subject,
+    title: draftEmailContent.title.trim() || defaultEmailContent.title,
+    body: body || defaultEmailContent.body,
   });
 };
πŸ“œ Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 573207a and 3052dc0.

β›” Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
πŸ“’ Files selected for processing (10)
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx (6 hunks)
  • apps/web/lib/actions/partners/invite-partner.ts (1 hunks)
  • apps/web/lib/actions/partners/save-invite-email-data.ts (1 hunks)
  • apps/web/lib/partners/sanitize-markdown.ts (1 hunks)
  • apps/web/lib/zod/schemas/partners.ts (1 hunks)
  • apps/web/lib/zod/schemas/program-invite-email.ts (1 hunks)
  • apps/web/lib/zod/schemas/programs.ts (2 hunks)
  • packages/email/package.json (1 hunks)
  • packages/email/src/templates/program-invite.tsx (4 hunks)
  • packages/prisma/schema/program.prisma (1 hunks)
🧰 Additional context used
🧠 Learnings (10)
πŸ“š Learning: 2025-10-17T08:18:19.278Z
Learnt from: devkiran
Repo: dubinc/dub PR: 0
File: :0-0
Timestamp: 2025-10-17T08:18:19.278Z
Learning: In the apps/web codebase, `@/lib/zod` should only be used for places that need OpenAPI extended zod schema. All other places should import from the standard `zod` package directly using `import { z } from "zod"`.

Applied to files:

  • apps/web/lib/zod/schemas/programs.ts
πŸ“š 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
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.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/save-invite-email-data.ts
  • 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/save-invite-email-data.ts
  • packages/email/src/templates/program-invite.tsx
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx
  • 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
  • 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-11-12T22:23:10.390Z
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.390Z
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
πŸ”‡ Additional comments (8)
packages/prisma/schema/program.prisma (1)

42-42: LGTM!

The optional JSON field for storing invite email data is well-placed and appropriately typed for flexible email customization.

apps/web/lib/zod/schemas/programs.ts (1)

21-21: LGTM!

The import and integration of programInviteEmailDataSchema into ProgramSchema is clean and follows the existing pattern.

Also applies to: 50-50

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

103-196: Good integration of Markdown rendering with appropriate fallback.

The conditional rendering logic cleanly separates custom Markdown content from the default template, and the markdownCustomStyles provide consistent styling for all common Markdown elements. The use of emailTitle and emailSubject derived from props ensures proper defaults.

packages/email/package.json (1)

13-13: Version @react-email/markdown@^0.0.17 is current and actively maintained.

The package is actively maintained with recent npm publish and active React Email repo and docs. The specified version is the latest stable release. While pre-1.0 versioning historically indicates potential for breaking changes, this package is actively developed within the Resend ecosystem and suitable for the dependency.

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

38-80: LGTM! Clean state management pattern.

The separation of concerns between emailContent (persisted state), draftEmailContent (editing state), and defaultEmailContent (fallback) is well-structured and easy to follow.


320-323: LGTM! Comprehensive disabled state logic.

The submit button correctly prevents submission during email editing (isEditingEmail) and while saving email data (isSavingEmailData), ensuring data integrity.


477-488: Nice UX enhancement with keyboard shortcut!

The Ctrl/Cmd+Enter shortcut to save during editing is a thoughtful addition that improves the editing experience.

apps/web/lib/actions/partners/save-invite-email-data.ts (1)

24-31: The review comment's XSS concern is incorrect. Subject and title are rendered safely in React components which auto-escape user input.

The verification shows that subject and title are rendered through React Email components (<Heading> and <Preview>) and in React state assignments, both of which automatically escape potentially dangerous content. React's JSX syntax prevents HTML injection by default. The sanitizeMarkdown function applied to body is not an XSS sanitizerβ€”it validates content format (checks for binary data, excessive line length, and normalizes line endings).

There is a minor asymmetry worth noting: subject and title lack max() length constraints (unlike body with max(3000)), but this is unrelated to the XSS concern and poses no security risk given React's built-in escaping.

Likely an incorrect or invalid review comment.

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/partners/invite-partner-sheet.tsx (1)

361-367: Add explanatory comment for intentional dependency exclusion.

The useEffect intentionally excludes draftEmailContent.body from the dependency array, but there's no comment explaining why. This was previously flagged in review.

Consider adding a comment above the eslint-disable line:

// Note: draftEmailContent.body intentionally excluded - we only sync
// editor content when first entering edit mode, not on subsequent updates
// eslint-disable-next-line react-hooks/exhaustive-deps
🧹 Nitpick comments (1)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx (1)

167-171: Remove redundant validation logic.

The updatedContent values (lines 161-165) are already trimmed and defaulted. Creating final* variables that trim and default again is redundant.

Apply this diff to simplify:

-    const updatedContent: EmailContent = {
-      subject: sanitizedSubject || defaultEmailContent.subject,
-      title: sanitizedTitle || defaultEmailContent.title,
-      body: sanitizedBody || defaultEmailContent.body,
-    };
-
-    // Ensure all values are non-empty (schema requirement)
-    const finalSubject =
-      updatedContent.subject.trim() || defaultEmailContent.subject;
-    const finalTitle = updatedContent.title.trim() || defaultEmailContent.title;
-    const finalBody = updatedContent.body.trim() || defaultEmailContent.body;
+    // Ensure all values are non-empty (schema requirement)
+    const finalSubject = sanitizedSubject || defaultEmailContent.subject;
+    const finalTitle = sanitizedTitle || defaultEmailContent.title;
+    const finalBody = sanitizedBody || defaultEmailContent.body;
 
     // Save to server (state updates happen in onSuccess callback)
     await saveEmailDataAsync({
       workspaceId,
       subject: finalSubject,
       title: finalTitle,
       body: finalBody,
     });
πŸ“œ Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 3052dc0 and d0fc3ee.

πŸ“’ Files selected for processing (3)
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx (5 hunks)
  • apps/web/lib/api/programs/get-program-or-throw.ts (2 hunks)
  • apps/web/lib/types.ts (2 hunks)
🧰 Additional context used
🧠 Learnings (4)
πŸ“š Learning: 2025-10-17T08:18:19.278Z
Learnt from: devkiran
Repo: dubinc/dub PR: 0
File: :0-0
Timestamp: 2025-10-17T08:18:19.278Z
Learning: In the apps/web codebase, `@/lib/zod` should only be used for places that need OpenAPI extended zod schema. All other places should import from the standard `zod` package directly using `import { z } from "zod"`.

Applied to files:

  • apps/web/lib/types.ts
πŸ“š 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/types.ts
πŸ“š 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-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/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx
🧬 Code graph analysis (3)
apps/web/lib/types.ts (1)
apps/web/lib/zod/schemas/program-invite-email.ts (1)
  • programInviteEmailDataSchema (3-9)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/invite-partner-sheet.tsx (7)
apps/web/lib/swr/use-program.ts (1)
  • useProgram (6-40)
apps/web/lib/types.ts (2)
  • ProgramProps (467-467)
  • ProgramInviteEmailData (469-471)
apps/web/lib/swr/use-workspace.ts (1)
  • useWorkspace (6-46)
apps/web/lib/actions/partners/invite-partner.ts (1)
  • invitePartnerAction (15-133)
apps/web/lib/actions/partners/save-invite-email-data.ts (1)
  • saveInviteEmailDataAction (16-45)
packages/ui/src/rich-text-area/rich-text-provider.tsx (1)
  • RichTextProvider (69-242)
packages/ui/src/rich-text-area/rich-text-toolbar.tsx (1)
  • RichTextToolbar (16-129)
apps/web/lib/api/programs/get-program-or-throw.ts (2)
apps/web/lib/zod/schemas/programs.ts (1)
  • ProgramSchema (25-52)
apps/web/lib/zod/schemas/program-invite-email.ts (1)
  • programInviteEmailDataSchema (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 (10)
apps/web/lib/types.ts (2)

100-100: LGTM!

The import follows the correct pattern for local schema files.


469-471: LGTM!

The type definition is clean and follows the established patterns in this file.

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

1-1: LGTM!

The import is appropriately placed and required for the schema extension below.


26-28: LGTM!

The schema extension correctly adds the inviteEmailData field with runtime validation.

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

2-2: LGTM!

The new imports are appropriate for the email template editing functionality.

Also applies to: 13-15, 19-19, 21-28


46-83: LGTM!

The email content state management is well-structured with appropriate use of useMemo for derived values and clear separation between draft and committed content.


110-128: LGTM!

The save action handler properly synchronizes local state with the server response and manages the editing flow.


370-403: LGTM!

The conditional rendering of edit controls provides a clear and intuitive editing experience.


465-503: LGTM!

The RichText editor integration is well-implemented with appropriate features and excellent keyboard shortcut support (Cmd/Ctrl+Enter to save).


508-553: LGTM!

The preview mode provides a realistic representation of the email with proper markdown rendering.

@steven-tey steven-tey merged commit c904241 into main Nov 15, 2025
7 of 8 checks passed
@steven-tey steven-tey deleted the invite-email branch November 15, 2025 06:44
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