-
Notifications
You must be signed in to change notification settings - Fork 2.7k
Add customizable email content for partner invites #3066
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
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.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughAdds 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
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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes
Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 📒 Files selected for processing (1)
🧰 Additional context used🧠 Learnings (2)📚 Learning: 2025-10-06T15:48:14.205ZApplied to files:
📚 Learning: 2025-05-29T04:45:18.504ZApplied to files:
🧬 Code graph analysis (1)packages/email/src/templates/program-invite.tsx (1)
⏰ 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)
🔇 Additional comments (4)
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. Comment |
There was a problem hiding this 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
📒 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.tsapps/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.
There was a problem hiding this 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-autoon code blocks) often doesn't work in email- Border positioning (
border-l-2on blockquotes) can be inconsistent- Advanced layout properties may degrade in Outlook, Gmail, etc.
You previously discussed using
@react-email/markdownwhich 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
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis 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.
There was a problem hiding this 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
sendEmailfails, 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:
- Transactional approach: Wrap the enrollment and email send in a transaction-like pattern where enrollment is marked as "pending" until email succeeds, or
- Idempotent retry: Store the email send as a queued task that can be retried, or
- 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
sanitizeMarkdownsuggests 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 viarehype-sanitizein the email template.Consider renaming to
validateMarkdownornormalizeMarkdownfor 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:
- Real-time character counter: Show remaining characters as they type (e.g., "2,500 / 3,000 characters")
- Warning threshold: Show a warning toast when they approach the limit (e.g., at 2,800 characters)
- Prevent input beyond limit: The
maxLength={3000}on the textarea (line 399) already prevents typing beyond the limit, but copy-paste could bypass itSince 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
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis 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.tsxapps/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
markdownComponentsimplementation 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) anddraftEmailContent(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
isEditingEmailis 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.
There was a problem hiding this 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
📒 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.tspackages/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
listStyleTypeandlistStylePositionavoids the::markerpseudo-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
rehypeSanitizefor XSS protection and provides a sensible fallback. Note that an empty stringbodywill trigger the fallback, which appears intentional given the upstream sanitization returnsnullfor 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.allSettledcorrectly ensures both tasks complete.The use of
Promise.allSettledensures 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.
There was a problem hiding this 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
📒 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.
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