-
-
Notifications
You must be signed in to change notification settings - Fork 810
chore(webapp): improvements to app emails #2482
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
# Conflicts: # pnpm-lock.yaml
|
WalkthroughThe PR migrates multiple email templates to Tailwind-based rendering via @react-email/tailwind: Footer switches inline styles to Tailwind classes; styles.ts removes the Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes ✨ Finishing Touches
🧪 Generate unit tests
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: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
internal-packages/emails/emails/invite.tsx (1)
26-31
: Follow repo guideline: avoid default exports.Prefer a named export to align with “No default exports” guidance.
Apply:
-export default function Email({ +export function InviteEmail({ orgName, inviterName, inviterEmail, inviteLink, }: z.infer<typeof InviteEmailSchema>) {Follow-up: update any import sites (e.g., index/routers) to
import { InviteEmail } ...
.Run to find consumers:
#!/bin/bash rg -nP --type=ts --type=tsx -C2 'from\s+["'\'']\./emails/invite["'\'']|require\(\s*["'\'']\./emails/invite["'\'']\s*\)'
🧹 Nitpick comments (4)
internal-packages/emails/emails/invite.tsx (2)
16-16
: Avoid mixing style objects with Tailwind; inline via className for consistency.You can drop the styles import and express paragraphLight as Tailwind classes.
Apply:
-import { anchor, container, h1, main, paragraphLight } from "./components/styles"; +// Tailwind used throughout; local style tokens not needed here.- <Text style={paragraphLight}> + <Text className="text-[14px] leading-6 text-[#878C99]"> {inviterName ?? inviterEmail} has invited you to join their organization on Trigger.dev. </Text>Also applies to: 47-50
62-66
: Add rel attribute when using target="_blank".Harmless in emails and good hygiene.
Apply:
- <Link + <Link href={inviteLink} - target="_blank" + target="_blank" + rel="noopener noreferrer" className="text-[#6366F1] text-[14px] no-underline" >internal-packages/emails/emails/magic-link.tsx (2)
46-53
: Prevent long-link overflow and keep link affordance.Long magic links can break layout in some clients; also underline improves recognizability.
- <Link - href={magicLink} - target="_blank" - className="text-[#6366F1] text-[14px] no-underline" - > + <Link + href={magicLink} + target="_blank" + rel="noopener noreferrer" + className="text-[#6366F1] text-[14px] underline break-all" + >
22-24
: Minor: drop redundant body margins.Email clients ignore/override body margins; Container already centers content.
- <Body className="bg-[#121317] my-auto mx-auto font-sans"> + <Body className="bg-[#121317] font-sans">
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
apps/webapp/public/emails/logo-triangle.png
is excluded by!**/*.png
📒 Files selected for processing (6)
internal-packages/emails/emails/components/Footer.tsx
(1 hunks)internal-packages/emails/emails/components/styles.ts
(0 hunks)internal-packages/emails/emails/invite.tsx
(2 hunks)internal-packages/emails/emails/magic-link.tsx
(1 hunks)internal-packages/emails/emails/welcome.tsx
(1 hunks)internal-packages/emails/package.json
(1 hunks)
💤 Files with no reviewable changes (1)
- internal-packages/emails/emails/components/styles.ts
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
**/*.{ts,tsx}
: Always prefer using isomorphic code like fetch, ReadableStream, etc. instead of Node.js specific code
For TypeScript, we usually use types over interfaces
Avoid enums
No default exports, use function declarations
Files:
internal-packages/emails/emails/magic-link.tsx
internal-packages/emails/emails/invite.tsx
internal-packages/emails/emails/welcome.tsx
internal-packages/emails/emails/components/Footer.tsx
🪛 GitHub Actions: 🤖 PR Checks
internal-packages/emails/package.json
[error] 1-1: Lockfile specifiers do not match package.json: lockfile lists @react-email/components 0.0.16, @react-email/render ^0.0.12, and react-email ^2.1.1 (among others) whereas package.json requires @react-email/components 0.4.0, @react-email/render ^1.1.4, and react-email ^4.2.7; also @react-email/tailwind is present in package.json but missing from the lockfile. Run 'pnpm install' to update.
🔇 Additional comments (5)
internal-packages/emails/emails/components/Footer.tsx (1)
8-13
: LGTM on the presentational refactor.Consistent text sizing/color and keeping the hr tokenized via styles looks good.
internal-packages/emails/emails/invite.tsx (1)
35-73
: Sanity-check Tailwind inlining with the new renderer.Given multiple arbitrary values (e.g., bg-[#121317]), render one sample locally to ensure styles inline as expected with @react-email/render ^1.1.4.
#!/bin/bash # Find any remaining incorrect Tailwind imports across the repo rg -nPU '(?s)import\s*{\s*[^}]*Tailwind[^}]*}\s*from\s*"@react-email/components"' # (manual step) locally run the email preview if you use react-email CLI # pnpm --filter emails devinternal-packages/emails/emails/welcome.tsx (1)
18-69
: LGTM on the content and Tailwind migration.Clear copy, sensible defaults, consistent tokens, and Footer integration look good.
internal-packages/emails/emails/magic-link.tsx (2)
26-31
: Verify logo URL resolution in production emails.Relative paths often break in email clients/CDNs. Confirm your Image component rewrites to an absolute URL (e.g., CDN/base URL).
#!/bin/bash # Inspect Image component(s) to confirm absolute URL handling fd -t f Image.tsx | while read -r f; do echo "== $f"; rg -nP -C2 'src|path|baseUrl|cdn|process\.env|absolute' "$f" done
55-60
: Confirm the 30-minute expiry matches backend policy.If the TTL differs, this is user-facing misinformation.
#!/bin/bash # Search for magic link TTL/expiry to validate the copy rg -nPI -C2 '(magic[_ -]?link|login).*expir|expiresIn|ttl|maxAge|token' \ -g '!**/node_modules/**' -g '!**/dist/**' --type=ts --type=tsx --type=tsv --type=js
import { Hr, Link, Text } from "@react-email/components"; | ||
import React from "react"; | ||
import { footer, footerAnchor, hr } from "./styles"; | ||
import { Hr, Link, Tailwind, Text } from "@react-email/components"; |
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.
Tailwind is imported from the wrong package and unused here.
Footer doesn’t use Tailwind; drop it from the import (and if needed elsewhere, import from @react-email/tailwind, not components).
Apply:
-import { Hr, Link, Tailwind, Text } from "@react-email/components";
+import { Hr, Link, Text } from "@react-email/components";
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
import { Hr, Link, Tailwind, Text } from "@react-email/components"; | |
import { Hr, Link, Text } from "@react-email/components"; |
🤖 Prompt for AI Agents
internal-packages/emails/emails/components/Footer.tsx lines 1-1: the import
currently brings in Tailwind from the wrong package and it is unused in this
file; remove Tailwind from the import list (keep Hr, Link, Text) so Footer only
imports what it uses, and if you need Tailwind elsewhere import it from
@react-email/tailwind rather than @react-email/components.
import { Footer } from "./components/Footer"; | ||
import { Image } from "./components/Image"; | ||
import { anchor, container, h1, main, paragraphLight } from "./components/styles"; | ||
import { hr } from "./components/styles"; | ||
|
||
export default function Email({ magicLink }: { magicLink: string }) { |
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.
🛠️ Refactor suggestion
Switch to a named export per repo guidelines; also use a clearer component name.
Avoid default exports. Rename to a named export to align with “No default exports, use function declarations”.
-export default function Email({ magicLink }: { magicLink: string }) {
+export function MagicLinkEmail({ magicLink }: { magicLink: string }) {
Run to find and update call sites that import the default:
#!/bin/bash
# Find default imports of magic-link and re-export patterns using default
rg -nP -C1 '\bimport\s+Email\s+from\s+["\'][^"\']*magic-link' --type=ts --type=tsx
rg -nP -C1 'export\s*{\s*default\s+as\s+[^}]+}\s*from\s*["\'][^"\']*magic-link' --type=ts --type=tsx
🤖 Prompt for AI Agents
In internal-packages/emails/emails/magic-link.tsx around line 17, the component
is exported as a default and named Email; change this to a named function export
with a clearer name (e.g., MagicLinkEmail) and export it as a named export
(export function MagicLinkEmail(...)). Update the component name internally if
needed but keep the same props signature, remove the default export, and ensure
the file exports the component by name. Run the provided ripgrep commands to
find and update all call sites to import { MagicLinkEmail } from
'.../magic-link' (or adjust to the new name) and replace any re-exports of
default with corresponding named re-exports. Finally run TypeScript build/tests
to validate no remaining default-import references.
import { Footer } from "./components/Footer"; | ||
import { anchor, bullets, footerItalic, main, paragraphLight } from "./components/styles"; | ||
|
||
export default function Email({ name }: { name?: string }) { |
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.
🛠️ Refactor suggestion
Use a named export per repo guideline (no default exports).
Switch to a named function export; consider a descriptive name.
Apply:
-export default function Email({ name }: { name?: string }) {
+export function WelcomeEmail({ name }: { name?: string }) {
Update import sites accordingly.
#!/bin/bash
rg -nP --type=ts --type=tsx -C2 'export\s+default\s+function\s+Email\(|from\s+["'\'']\./emails/welcome["'\'']'
🤖 Prompt for AI Agents
internal-packages/emails/emails/welcome.tsx around line 13: this file currently
uses a default export for the Email component; change it to a named export
(e.g., export function WelcomeEmail({ name }: { name?: string }) { ... }) and
update all import sites to use the new named import (import { WelcomeEmail }
from './emails/welcome'). Run the provided ripgrep command to find and update
usages and ensure TypeScript/ESLint passes after renaming.
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: 0
♻️ Duplicate comments (1)
internal-packages/emails/emails/invite.tsx (1)
1-11
: Correct Tailwind import path (resolved prior warning).Tailwind is now imported from @react-email/tailwind, not from @react-email/components. Looks good.
🧹 Nitpick comments (5)
internal-packages/emails/emails/invite.tsx (5)
17-23
: Tighten schema: validate inviterEmail and restrict inviteLink host.Prevents malformed emails and accidental/phishing domains if data ever becomes user-influenced.
Apply:
export const InviteEmailSchema = z.object({ email: z.literal("invite"), orgName: z.string(), inviterName: z.string().optional(), - inviterEmail: z.string(), - inviteLink: z.string().url(), + inviterEmail: z.string().email(), + inviteLink: z + .string() + .url() + .refine((u) => { + try { + return new URL(u).hostname.endsWith("trigger.dev"); + } catch { + return false; + } + }, { message: "Invite link must be a trigger.dev URL" }), });Would you like me to update the other email schemas similarly?
35-37
: Minor: drop auto margins on Body.Email clients often ignore body auto-centering; the Container already centers.
- <Body className="bg-[#121317] my-auto mx-auto font-sans"> + <Body className="bg-[#121317] font-sans"> <Container className="my-[40px] mx-auto p-[20px] max-w-[600px]">
61-67
: Harden external link and improve long-URL wrapping.Add rel to mitigate opener leaks; wrap long URLs to prevent layout overflow.
- <Link - href={inviteLink} - target="_blank" - className="text-[#6366F1] text-[14px] no-underline" - > + <Link + href={inviteLink} + target="_blank" + rel="noopener noreferrer" + className="text-[#6366F1] text-[14px] no-underline break-all" + > {inviteLink} </Link>
25-31
: Avoid default export; use a named export per repo guidelines.Keeps imports consistent and avoids refactor hazards.
-export default function Email({ +export function InviteEmail({ orgName, inviterName, inviterEmail, inviteLink, }: z.infer<typeof InviteEmailSchema>) {After this change, update call sites importing the default. To find them:
#!/bin/bash rg -nP -C2 'import\s+Email\s+from\s+["\'][^"\']*invite["\']|import\s+[^}]*\*\s+as\s+Email\s+from\s+["\'][^"\']*invite'
38-45
: Image props: use numeric dimensions and confirm absolute URL resolution.@react-email Image-like components typically expect numeric width/height; also ensure path resolves to an absolute URL in prod emails.
- <Image - path="/emails/logo.png" - width="180px" - height="32px" + <Image + path="/emails/logo.png" + width={180} + height={32} alt="Trigger.dev" className="mt-0 mb-12" />To verify the Image component API and URL handling:
#!/bin/bash fd -a '^Image\.(tsx|jsx)$' internal-packages | xargs -I{} sh -c 'echo "== {} =="; sed -n "1,160p" "{}"'
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
internal-packages/emails/emails/invite.tsx
(2 hunks)internal-packages/emails/emails/magic-link.tsx
(1 hunks)internal-packages/emails/emails/welcome.tsx
(1 hunks)internal-packages/emails/package.json
(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
- internal-packages/emails/emails/welcome.tsx
- internal-packages/emails/emails/magic-link.tsx
- internal-packages/emails/package.json
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
**/*.{ts,tsx}
: Always prefer using isomorphic code like fetch, ReadableStream, etc. instead of Node.js specific code
For TypeScript, we usually use types over interfaces
Avoid enums
No default exports, use function declarations
Files:
internal-packages/emails/emails/invite.tsx
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: 0
🧹 Nitpick comments (4)
internal-packages/emails/src/transports/smtp.ts (1)
25-33
: Add plaintext fallback using toPlainText
Render your HTML once, then pass atext
field generated viatoPlainText
(the{ plainText: true }
option is deprecated). For example:+ import { toPlainText } from '@react-email/render'; // … - const html = await render(react); + const html = await render(react); await this.#client.sendMail({ from, to, replyTo, subject, - html: html, + html: html, + text: toPlainText(html), });internal-packages/emails/src/transports/aws-ses.ts (3)
14-21
: Allow SES region/creds injection for env parity and testingLet consumers pass a prebuilt
SESClient
or config (region, credentials). This avoids relying on ambient env and eases testing.Apply this diff:
export type AwsSesMailTransportOptions = { - type: "aws-ses"; + type: "aws-ses"; + client?: awsSes.SESClient; + clientConfig?: awsSes.SESClientConfig; }; export class AwsSesMailTransport implements MailTransport { #client: nodemailer.Transporter; constructor(options: AwsSesMailTransportOptions) { - const ses = new awsSes.SESClient(); + const ses = options.client ?? new awsSes.SESClient(options.clientConfig); this.#client = nodemailer.createTransport({ SES: { aws: awsSes, - ses, - }, - }); + ses, + }, + }); }
33-43
: Log unknown errors too (nit)If a non-
Error
is thrown, we skip logging. Add a minimal fallback log for easier incident triage.- } catch (error) { + } catch (error) { if (error instanceof Error) { console.error( `Failed to send email to ${to}, ${subject}. Error ${error.name}: ${error.message}` ); throw new EmailError(error); } else { - throw error; + console.error(`Failed to send email to ${to}, ${subject}. Unknown error:`, error); + throw error; } }
24-33
: Include plain text alternative in AWS SES transport
Add atext
field usingtoPlainText
from@react-email/render
alongsidehtml
for better inbox placement. Example diff:--- a/internal-packages/emails/src/transports/aws-ses.ts +++ b/internal-packages/emails/src/transports/aws-ses.ts @@ -1,5 +1,6 @@ -import { render } from "@react-email/render"; +import { render, toPlainText } from "@react-email/render"; import { EmailError, MailMessage, MailTransport, PlainTextMailMessage } from "./index"; import nodemailer from "nodemailer"; import * as awsSes from "@aws-sdk/client-ses"; @@ -28,7 +29,10 @@ export class AwsSesMailTransport implements MailTransport { async send({ to, from, replyTo, subject, react }: MailMessage): Promise<void> { + const html = await render(react); + const text = toPlainText(html); try { await this.#client.sendMail({ from, to, replyTo, subject, - html: await render(react), + html, + text, }); } catch (error) { // ...Use
toPlainText(html)
since theplainText
option onrender
is deprecated (react.email)
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
internal-packages/emails/src/transports/aws-ses.ts
(3 hunks)internal-packages/emails/src/transports/smtp.ts
(1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
**/*.{ts,tsx}
: Always prefer using isomorphic code like fetch, ReadableStream, etc. instead of Node.js specific code
For TypeScript, we usually use types over interfaces
Avoid enums
No default exports, use function declarations
Files:
internal-packages/emails/src/transports/smtp.ts
internal-packages/emails/src/transports/aws-ses.ts
🧬 Code graph analysis (1)
internal-packages/emails/src/transports/aws-ses.ts (1)
internal-packages/emails/src/transports/index.ts (3)
MailTransport
(23-26)MailMessage
(7-13)PlainTextMailMessage
(15-21)
⏰ 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: typecheck / typecheck
- GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (4)
internal-packages/emails/src/transports/smtp.ts (1)
32-32
: Async render usage looks correctAwaiting
render(react)
aligns with newer react-email versions and is safe even ifrender
is sync.internal-packages/emails/src/transports/aws-ses.ts (3)
3-4
: Imports LGTMSwitch to explicit
* as awsSes
and keepingnodemailer
import is fine for SES v3 transport.
7-8
: Literal type formatting change is harmlessThe string-literal type
"aws-ses"
is consistent. No behavior change.
45-54
: Plaintext path unchanged — LGTM
sendPlainText
remains straightforward and consistent with the transport interface.
Updates these emails sent from the app:
With these improvements:
New layouts (without images and dynamic content loaded):
Welcome email

Login email

Invite email
