Skip to content

Conversation

samejr
Copy link
Member

@samejr samejr commented Sep 7, 2025

Updates these emails sent from the app:

  • Welcome email
  • Invite email
  • Magic link login email

With these improvements:

  • Updates them all to use Tailwind
  • Updates react-email to the latest
  • Improved styling and layout
  • Fixes some very outdated links that were 404s

New layouts (without images and dynamic content loaded):

Welcome email
CleanShot 2025-09-07 at 15 11 46@2x

Login email
CleanShot 2025-09-07 at 15 11 31@2x

Invite email
CleanShot 2025-09-07 at 15 11 16@2x

Copy link

changeset-bot bot commented Sep 7, 2025

⚠️ No Changeset found

Latest commit: 6677844

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

Copy link
Contributor

coderabbitai bot commented Sep 7, 2025

Walkthrough

The PR migrates multiple email templates to Tailwind-based rendering via @react-email/tailwind: Footer switches inline styles to Tailwind classes; styles.ts removes the hr margin. invite.tsx, magic-link.tsx, and welcome.tsx are restructured to use Tailwind components (Section, Button, Tailwind), update logo assets, and introduce fallback-link sections. invite.tsx updates InviteEmailSchema (adds orgName, inviterName optional, inviterEmail, inviteLink) and types the Email props from that schema. package.json updates @react-email packages and adds @react-email/tailwind. Transports (aws-ses.ts, smtp.ts) now await render(react) for HTML; AwsSesMailTransportOptions.type string-literal formatting changed.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch nicer-app-emails

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.

@samejr samejr changed the title chore(webapp)Improvements to app emails chore(webapp):Improvements to app emails Sep 7, 2025
@samejr samejr changed the title chore(webapp):Improvements to app emails chore(webapp): improvements to app emails Sep 7, 2025
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: 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

📥 Commits

Reviewing files that changed from the base of the PR and between 89b1d8b and a42c799.

⛔ 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 dev
internal-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";
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

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.

Suggested change
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 }) {
Copy link
Contributor

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 }) {
Copy link
Contributor

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.

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)
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

📥 Commits

Reviewing files that changed from the base of the PR and between a42c799 and d3a7fcc.

📒 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

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

🧹 Nitpick comments (4)
internal-packages/emails/src/transports/smtp.ts (1)

25-33: Add plaintext fallback using toPlainText
Render your HTML once, then pass a text field generated via toPlainText (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 testing

Let 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 a text field using toPlainText from @react-email/render alongside html 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 the plainText option on render is deprecated (react.email)

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d3a7fcc and 6677844.

📒 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 correct

Awaiting render(react) aligns with newer react-email versions and is safe even if render is sync.

internal-packages/emails/src/transports/aws-ses.ts (3)

3-4: Imports LGTM

Switch to explicit * as awsSes and keeping nodemailer import is fine for SES v3 transport.


7-8: Literal type formatting change is harmless

The 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.

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.

1 participant