From babaa9f1b174c3fc31ba82fcc392ea90706475b9 Mon Sep 17 00:00:00 2001 From: Henry Date: Mon, 28 Jul 2025 07:09:30 +0100 Subject: [PATCH] feat: setup plunk for transactional/marketing emails --- cloud/docker-compose.yml | 4 +++- packages/auth/src/auth.ts | 13 ++++++++++++- packages/email/package.json | 1 + packages/email/src/cloudMailerClient.tsx | 8 ++++++++ packages/email/src/index.tsx | 1 + packages/email/src/sendEmail.tsx | 10 ++++++++++ pnpm-lock.yaml | 10 ++++++++++ turbo.json | 4 +++- 8 files changed, 48 insertions(+), 3 deletions(-) create mode 100644 packages/email/src/cloudMailerClient.tsx diff --git a/cloud/docker-compose.yml b/cloud/docker-compose.yml index faa8e230..e30a9b0e 100644 --- a/cloud/docker-compose.yml +++ b/cloud/docker-compose.yml @@ -23,13 +23,15 @@ services: - STRIPE_SECRET_KEY=${STRIPE_SECRET_KEY} - STRIPE_WEBHOOK_SECRET=${STRIPE_WEBHOOK_SECRET} - # SMTP + # Email - SMTP_HOST=${SMTP_HOST} - SMTP_PORT=${SMTP_PORT} - SMTP_USER=${SMTP_USER} - SMTP_PASSWORD=${SMTP_PASSWORD} - SMTP_SECURE=${SMTP_SECURE} - EMAIL_FROM=${EMAIL_FROM} + - PLUNK_API_KEY=${PLUNK_API_KEY} + - PLUNK_API_URL=${PLUNK_API_URL} # S3 storage - S3_ACCESS_KEY_ID=${S3_ACCESS_KEY_ID} diff --git a/packages/auth/src/auth.ts b/packages/auth/src/auth.ts index 6d36864c..d6498b0d 100644 --- a/packages/auth/src/auth.ts +++ b/packages/auth/src/auth.ts @@ -11,7 +11,7 @@ import type { dbClient } from "@kan/db/client"; import * as memberRepo from "@kan/db/repository/member.repo"; import * as userRepo from "@kan/db/repository/user.repo"; import * as schema from "@kan/db/schema"; -import { sendEmail } from "@kan/email"; +import { cloudMailerClient, sendEmail } from "@kan/email"; import { createStripeClient } from "@kan/stripe"; export const configuredProviders = socialProviderList.reduce< @@ -183,6 +183,17 @@ export const initAuth = (db: dbClient) => { return Promise.resolve(true); }, async after(user) { + if (cloudMailerClient) { + await cloudMailerClient.events.track({ + event: "user-signup", + email: user.email, + data: { + name: user.name, + userId: user.id, + }, + }); + } + if ( user.image && !user.image.includes(process.env.NEXT_PUBLIC_STORAGE_DOMAIN!) diff --git a/packages/email/package.json b/packages/email/package.json index 7d6f0054..4ff80eda 100644 --- a/packages/email/package.json +++ b/packages/email/package.json @@ -23,6 +23,7 @@ "typecheck": "tsc --noEmit --emitDeclarationOnly false" }, "dependencies": { + "@plunk/node": "^3.0.3", "@react-email/components": "^0.0.25", "nodemailer": "^7.0.3", "react-email": "^3.0.1" diff --git a/packages/email/src/cloudMailerClient.tsx b/packages/email/src/cloudMailerClient.tsx new file mode 100644 index 00000000..e6471bcf --- /dev/null +++ b/packages/email/src/cloudMailerClient.tsx @@ -0,0 +1,8 @@ +import Plunk from "@plunk/node"; + +export const cloudMailerClient = + process.env.NEXT_PUBLIC_KAN_ENV === "cloud" + ? new Plunk(process.env.PLUNK_API_KEY ?? "", { + baseUrl: process.env.PLUNK_API_URL, + }) + : null; diff --git a/packages/email/src/index.tsx b/packages/email/src/index.tsx index 6be1c177..a5201f3c 100644 --- a/packages/email/src/index.tsx +++ b/packages/email/src/index.tsx @@ -1,3 +1,4 @@ export const name = "email"; export { sendEmail } from "./sendEmail"; +export { cloudMailerClient } from "./cloudMailerClient"; diff --git a/packages/email/src/sendEmail.tsx b/packages/email/src/sendEmail.tsx index 41ed1d23..752250b6 100644 --- a/packages/email/src/sendEmail.tsx +++ b/packages/email/src/sendEmail.tsx @@ -1,6 +1,7 @@ import { render } from "@react-email/render"; import nodemailer from "nodemailer"; +import { cloudMailerClient } from "./cloudMailerClient"; import JoinWorkspaceTemplate from "./templates/join-workspace"; import MagicLinkTemplate from "./templates/magic-link"; import ResetPasswordTemplate from "./templates/reset-password"; @@ -44,6 +45,15 @@ export const sendEmail = async ( html, }; + if (cloudMailerClient) { + const response = await cloudMailerClient.emails.send({ + ...options, + body: html, + }); + + return response; + } + const response = await transporter.sendMail(options); if (!response.accepted.length) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a78d64b6..d764094e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -409,6 +409,9 @@ importers: packages/email: dependencies: + '@plunk/node': + specifier: ^3.0.3 + version: 3.0.3 '@react-email/components': specifier: ^0.0.25 version: 0.0.25(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -2194,6 +2197,9 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} + '@plunk/node@3.0.3': + resolution: {integrity: sha512-f3NT418/EdLjebQ4arAwR64cnz7d5H7i1CeKh8+B0cVBrwzHvF+Q+QkhutF+FAp49yuI/BYc71qQH+RFc2fxxQ==} + '@popperjs/core@2.11.8': resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} @@ -8910,6 +8916,10 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true + '@plunk/node@3.0.3': + dependencies: + tslib: 2.8.1 + '@popperjs/core@2.11.8': {} '@react-aria/focus@3.19.0(react@18.3.1)': diff --git a/turbo.json b/turbo.json index 93e8d331..55da2f62 100644 --- a/turbo.json +++ b/turbo.json @@ -109,7 +109,9 @@ "S3_FORCE_PATH_STYLE", "PORT", "BETTER_AUTH_SECRET", - "BETTER_AUTH_TRUSTED_ORIGINS" + "BETTER_AUTH_TRUSTED_ORIGINS", + "PLUNK_API_KEY", + "PLUNK_API_URL" ], "globalPassThroughEnv": [ "NODE_ENV",