diff --git a/apps/landing/playwright.config.ts b/apps/landing/playwright.config.ts new file mode 100644 index 000000000..0a35429a1 --- /dev/null +++ b/apps/landing/playwright.config.ts @@ -0,0 +1,34 @@ +import { devices, PlaywrightTestConfig } from "@playwright/test"; + +const ci = process.env.CI === "true"; + +// Use process.env.PORT by default and fallback to port 3000 +const PORT = process.env.PORT || 3000; + +// Set webServer.url and use.baseURL with the location of the WebServer respecting the correct set port +const baseURL = `http://localhost:${PORT}`; + +// Reference: https://playwright.dev/docs/test-configuration +const config: PlaywrightTestConfig = { + // Artifacts folder where screenshots, videos, and traces are stored. + outputDir: "test-results/", + projects: [{ name: "chromium", use: { ...devices["Desktop Chrome"] } }], + use: { + viewport: { width: 1280, height: 720 }, + baseURL, + permissions: ["clipboard-read"], + trace: "retain-on-failure", + }, + webServer: { + command: `NODE_ENV=test yarn dev --port ${PORT}`, + url: baseURL, + timeout: 120 * 1000, + reuseExistingServer: !ci, + }, + reporter: [ + [ci ? "github" : "list"], + ["html", { open: !ci ? "on-failure" : "never" }], + ], + workers: 1, +}; +export default config; diff --git a/packages/backend/trpc/routers/auth.ts b/packages/backend/trpc/routers/auth.ts index d2ccd2533..d563330a3 100644 --- a/packages/backend/trpc/routers/auth.ts +++ b/packages/backend/trpc/routers/auth.ts @@ -1,10 +1,10 @@ import { prisma } from "@rallly/database"; +import { sendEmail } from "@rallly/emails"; import { absoluteUrl } from "@rallly/utils"; import { TRPCError } from "@trpc/server"; import { z } from "zod"; import { createToken, decryptToken } from "../../session"; -import { emailClient } from "../../utils/email-client"; import { generateOtp } from "../../utils/nanoid"; import { publicProcedure, router } from "../trpc"; import { LoginTokenPayload, RegistrationTokenPayload } from "../types"; @@ -107,7 +107,7 @@ export const auth = router({ code, }); - await emailClient.sendTemplate("RegisterEmail", { + await sendEmail("RegisterEmail", { to: input.email, subject: `${input.name}, please verify your email address`, props: { @@ -193,7 +193,7 @@ export const auth = router({ code, }); - await emailClient.sendTemplate("LoginEmail", { + await sendEmail("LoginEmail", { to: input.email, subject: `${code} is your 6-digit code`, props: { diff --git a/packages/backend/trpc/routers/polls.ts b/packages/backend/trpc/routers/polls.ts index 2cd18e4b2..c25ef46c1 100644 --- a/packages/backend/trpc/routers/polls.ts +++ b/packages/backend/trpc/routers/polls.ts @@ -1,4 +1,5 @@ import { prisma } from "@rallly/database"; +import { sendEmail } from "@rallly/emails"; import { absoluteUrl, shortUrl } from "@rallly/utils"; import { TRPCError } from "@trpc/server"; import dayjs from "dayjs"; @@ -9,7 +10,6 @@ import * as ics from "ics"; import { z } from "zod"; import { getTimeZoneAbbreviation } from "../../utils/date"; -import { emailClient } from "../../utils/email-client"; import { nanoid } from "../../utils/nanoid"; import { possiblyPublicProcedure, @@ -133,7 +133,7 @@ export const polls = router({ }); if (user) { - await emailClient.sendTemplate("NewPollEmail", { + await sendEmail("NewPollEmail", { to: user.email, subject: `Let's find a date for ${poll.title}`, props: { @@ -678,7 +678,7 @@ export const polls = router({ }); } - const emailToHost = emailClient.sendTemplate("FinalizeHostEmail", { + const emailToHost = sendEmail("FinalizeHostEmail", { subject: `Date booked for ${poll.title}`, to: poll.user.email, props: { @@ -702,7 +702,7 @@ export const polls = router({ }); const emailsToParticipants = participantsToEmail.map((p) => { - return emailClient.sendTemplate("FinalizeParticipantEmail", { + return sendEmail("FinalizeParticipantEmail", { subject: `Date booked for ${poll.title}`, to: p.email, props: { diff --git a/packages/backend/trpc/routers/polls/comments.ts b/packages/backend/trpc/routers/polls/comments.ts index d876dd1ef..b54d69705 100644 --- a/packages/backend/trpc/routers/polls/comments.ts +++ b/packages/backend/trpc/routers/polls/comments.ts @@ -1,9 +1,9 @@ import { prisma } from "@rallly/database"; +import { sendEmail } from "@rallly/emails"; import { absoluteUrl } from "@rallly/utils"; import { z } from "zod"; import { createToken } from "../../../session"; -import { emailClient } from "../../../utils/email-client"; import { publicProcedure, router } from "../../trpc"; import { DisableNotificationsPayload } from "../../types"; @@ -82,7 +82,7 @@ export const comments = router({ { ttl: 0 }, ); emailsToSend.push( - emailClient.sendTemplate("NewCommentEmail", { + sendEmail("NewCommentEmail", { to: email, subject: `${authorName} has commented on ${poll.title}`, props: { diff --git a/packages/backend/trpc/routers/polls/participants.ts b/packages/backend/trpc/routers/polls/participants.ts index d2fa318d8..ae74f476d 100644 --- a/packages/backend/trpc/routers/polls/participants.ts +++ b/packages/backend/trpc/routers/polls/participants.ts @@ -1,10 +1,10 @@ import { prisma } from "@rallly/database"; +import { sendEmail } from "@rallly/emails"; import { absoluteUrl } from "@rallly/utils"; import { TRPCError } from "@trpc/server"; import { z } from "zod"; import { createToken } from "../../../session"; -import { emailClient } from "../../../utils/email-client"; import { publicProcedure, router } from "../../trpc"; import { DisableNotificationsPayload } from "../../types"; @@ -107,7 +107,7 @@ export const participants = router({ ); emailsToSend.push( - emailClient.sendTemplate("NewParticipantConfirmationEmail", { + sendEmail("NewParticipantConfirmationEmail", { to: email, subject: `Thanks for responding to ${poll.title}`, props: { @@ -144,7 +144,7 @@ export const participants = router({ { ttl: 0 }, ); emailsToSend.push( - emailClient.sendTemplate("NewParticipantEmail", { + sendEmail("NewParticipantEmail", { to: email, subject: `${participant.name} has responded to ${poll.title}`, props: { diff --git a/packages/backend/utils/email-client.ts b/packages/backend/utils/email-client.ts deleted file mode 100644 index 4cffbf1ae..000000000 --- a/packages/backend/utils/email-client.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { EmailClient, SupportedEmailProviders } from "@rallly/emails"; - -const env = process.env["NODE" + "_ENV"]; - -export const emailClient = new EmailClient({ - openPreviews: env === "developement", - useTestServer: env === "test", - provider: { - name: process.env.EMAIL_PROVIDER as SupportedEmailProviders, - }, - mail: { - from: { - name: "Rallly", - address: - (process.env.NOREPLY_EMAIL as string) || - (process.env.SUPPORT_EMAIL as string), - }, - }, -}); diff --git a/packages/emails/package.json b/packages/emails/package.json index 1da271767..d9e46c677 100644 --- a/packages/emails/package.json +++ b/packages/emails/package.json @@ -7,8 +7,8 @@ "lint": "eslint ./src", "lint:tsc": "tsc --noEmit" }, - "main": "./src/index.ts", - "types": "./src/index.ts", + "main": "./src/index.tsx", + "types": "./src/index.tsx", "dependencies": { "@aws-sdk/client-ses": "^3.292.0", "@aws-sdk/credential-provider-node": "^3.292.0", @@ -16,14 +16,12 @@ "@react-email/render": "0.0.6", "clsx": "^1.2.1", "nodemailer": "^6.9.1", - "preview-email": "^3.0.19", "react-email": "^1.9.1" }, "devDependencies": { "@rallly/tailwind-config": "*", "@rallly/tsconfig": "*", "@rallly/utils": "*", - "@types/nodemailer": "^6.4.7", - "@types/preview-email": "^3.0.1" + "@types/nodemailer": "^6.4.7" } } diff --git a/packages/emails/src/index.ts b/packages/emails/src/index.tsx similarity index 100% rename from packages/emails/src/index.ts rename to packages/emails/src/index.tsx diff --git a/packages/emails/src/send-email.tsx b/packages/emails/src/send-email.tsx index 60d9aad1f..7cffbc423 100644 --- a/packages/emails/src/send-email.tsx +++ b/packages/emails/src/send-email.tsx @@ -3,7 +3,6 @@ import { defaultProvider } from "@aws-sdk/credential-provider-node"; import { render } from "@react-email/render"; import { createTransport, Transporter } from "nodemailer"; import type Mail from "nodemailer/lib/mailer"; -import previewEmail from "preview-email"; import React from "react"; import * as templates from "./templates"; @@ -18,121 +17,30 @@ type TemplateProps = React.ComponentProps< type TemplateComponent = Templates[T]; -type SendEmailOptions = { - to: string; - subject: string; - props: TemplateProps; - attachments?: Mail.Options["attachments"]; -}; +const env = process.env["NODE" + "_ENV"] || "development"; -type EmailProviderConfig = - | { - name: "ses"; - // config defined through env vars - } - | { - name: "smtp"; - // config defined through env vars - }; +let transport: Transporter; -export type SupportedEmailProviders = EmailProviderConfig["name"]; - -type EmailClientConfig = { - /** - * Whether to open previews of each email in the browser - */ - openPreviews?: boolean; - /** - * Whether to send emails to the test server - */ - useTestServer: boolean; - /** - * Email provider config - */ - provider: EmailProviderConfig; - /** - * Mail config - */ - mail: { - from: { - name: string; - address: string; - }; - }; -}; - -export class EmailClient { - private config: EmailClientConfig; - private cachedTransport?: Transporter; - - constructor(config: EmailClientConfig) { - this.config = config; +const getTransport = () => { + if (transport) { + // Reuse the transport if it exists + return transport; } - async sendTemplate( - templateName: T, - options: SendEmailOptions, - ) { - if (!process.env.SUPPORT_EMAIL) { - console.info("SUPPORT_EMAIL not configured - skipping email send"); - return; - } - - const Template = templates[templateName] as TemplateComponent; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const html = render(