({
+ adminUrlId: input.adminUrlId,
+ });
+
+ await sendEmail("EnableNotificationsEmail", {
+ to: poll.user.email,
+ subject: "Please verify your email address",
+ props: {
+ name: poll.user.name,
+ title: poll.title,
+ adminLink: absoluteUrl(`/admin/${input.adminUrlId}`),
+ verificationLink: absoluteUrl(
+ `/auth/enable-notifications?token=${token}`,
+ ),
+ },
+ });
+ }),
getByAdminUrlId: publicProcedure
.input(
z.object({
diff --git a/apps/web/src/server/routers/polls/verification.ts b/apps/web/src/server/routers/polls/verification.ts
index 9a7be9cfd..a4d3351e4 100644
--- a/apps/web/src/server/routers/polls/verification.ts
+++ b/apps/web/src/server/routers/polls/verification.ts
@@ -1,14 +1,8 @@
import { prisma } from "@rallly/database";
-import { sendEmail } from "@rallly/emails";
import { TRPCError } from "@trpc/server";
import { z } from "zod";
-import { absoluteUrl } from "../../../utils/absolute-url";
-import {
- createToken,
- decryptToken,
- mergeGuestsIntoUser,
-} from "../../../utils/auth";
+import { decryptToken, mergeGuestsIntoUser } from "../../../utils/auth";
import { publicProcedure, router } from "../../trpc";
export const verification = router({
@@ -20,10 +14,19 @@ export const verification = router({
}),
)
.mutation(async ({ ctx, input }) => {
- const { pollId } = await decryptToken<{
+ const payload = await decryptToken<{
pollId: string;
}>(input.code);
+ if (!payload) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Invalid token",
+ });
+ }
+
+ const { pollId } = payload;
+
if (pollId !== input.pollId) {
throw new TRPCError({
code: "BAD_REQUEST",
@@ -54,45 +57,4 @@ export const verification = router({
};
await ctx.session.save();
}),
- request: publicProcedure
- .input(
- z.object({
- pollId: z.string(),
- adminUrlId: z.string(),
- }),
- )
- .mutation(async ({ input: { pollId, adminUrlId } }) => {
- const poll = await prisma.poll.findUnique({
- where: {
- id: pollId,
- },
- include: {
- user: true,
- },
- });
-
- if (!poll) {
- throw new TRPCError({
- code: "NOT_FOUND",
- message: `Poll with id ${pollId} not found`,
- });
- }
-
- const pollUrl = absoluteUrl(`/admin/${adminUrlId}`);
- const token = await createToken({
- pollId,
- });
- const verifyEmailUrl = `${pollUrl}?code=${token}`;
-
- await sendEmail("GuestVerifyEmail", {
- to: poll.user.email,
- subject: "Please verify your email address",
- props: {
- title: poll.title,
- name: poll.user.name,
- adminLink: pollUrl,
- verificationLink: verifyEmailUrl,
- },
- });
- }),
});
diff --git a/apps/web/src/utils/auth.ts b/apps/web/src/utils/auth.ts
index 8c5593639..67c2f9048 100644
--- a/apps/web/src/utils/auth.ts
+++ b/apps/web/src/utils/auth.ts
@@ -35,6 +35,10 @@ export type LoginTokenPayload = {
code: string;
};
+export type EnableNotificationsTokenPayload = {
+ adminUrlId: string;
+};
+
export type RegisteredUserSession = {
isGuest: false;
id: string;
@@ -75,7 +79,9 @@ export function withSessionRoute(handler: NextApiHandler) {
}, sessionOptions);
}
-const compose = (...fns: GetServerSideProps[]): GetServerSideProps => {
+export const composeGetServerSideProps = (
+ ...fns: GetServerSideProps[]
+): GetServerSideProps => {
return async (ctx) => {
const res = { props: {} };
for (const getServerSideProps of fns) {
@@ -87,7 +93,7 @@ const compose = (...fns: GetServerSideProps[]): GetServerSideProps => {
...fnRes.props,
};
} else {
- return { notFound: true };
+ return fnRes;
}
}
@@ -105,7 +111,7 @@ export function withSessionSsr(
},
): GetServerSideProps {
const composedHandler = Array.isArray(handler)
- ? compose(...handler)
+ ? composeGetServerSideProps(...handler)
: handler;
return withIronSessionSsr(async (ctx) => {
@@ -137,8 +143,15 @@ export function withSessionSsr(
export const decryptToken = async >(
token: string,
-): Promise
=> {
- return await unsealData(token, { password: sessionOptions.password });
+): Promise
=> {
+ const payload = await unsealData(token, {
+ password: sessionOptions.password,
+ });
+ if (Object.keys(payload).length === 0) {
+ return null;
+ }
+
+ return payload as P;
};
export const createToken = async >(
diff --git a/apps/web/tests/create-delete-poll.spec.ts b/apps/web/tests/create-delete-poll.spec.ts
index fe3875a61..0b2bb10bd 100644
--- a/apps/web/tests/create-delete-poll.spec.ts
+++ b/apps/web/tests/create-delete-poll.spec.ts
@@ -51,15 +51,24 @@ test.describe.serial(() => {
await expect(title).toHaveText("Monthly Meetup");
- pollUrl = page.url();
- });
-
- test("verify poll", async ({ page, baseURL }) => {
const { email } = await mailServer.captureOne("john.doe@email.com", {
wait: 5000,
});
- expect(email.headers.subject).toBe("Monthly Meetup has been created");
+ expect(email.headers.subject).toBe("Let's find a date for Monthly Meetup");
+
+ pollUrl = page.url();
+ });
+
+ test("enable notifications", async ({ page, baseURL }) => {
+ await page.goto(pollUrl);
+ await page.getByTestId("notifications-toggle").click();
+
+ const { email } = await mailServer.captureOne("john.doe@email.com", {
+ wait: 5000,
+ });
+
+ expect(email.headers.subject).toBe("Please verify your email address");
const $ = load(email.html);
const verifyLink = $("#verifyEmailUrl").attr("href");
@@ -72,6 +81,12 @@ test.describe.serial(() => {
await page.goto(verifyLink);
+ await expect(
+ page.getByText("Notifications have been enabled for Monthly Meetup"),
+ ).toBeVisible();
+
+ page.getByText("Click here").click();
+
await expect(page.getByTestId("poll-title")).toHaveText("Monthly Meetup");
});
diff --git a/apps/web/tests/edit-options.spec.ts b/apps/web/tests/edit-options.spec.ts
index 343a638cb..2c481f1f2 100644
--- a/apps/web/tests/edit-options.spec.ts
+++ b/apps/web/tests/edit-options.spec.ts
@@ -1,6 +1,6 @@
import { expect, test } from "@playwright/test";
-test.describe("Edit options", () => {
+test.describe("edit options", () => {
test("should show warning when deleting options with votes in them", async ({
page,
}) => {
diff --git a/apps/web/tests/house-keeping.spec.ts b/apps/web/tests/house-keeping.spec.ts
index 886a262d0..36f6cb4d7 100644
--- a/apps/web/tests/house-keeping.spec.ts
+++ b/apps/web/tests/house-keeping.spec.ts
@@ -1,4 +1,4 @@
-import { expect, test } from "@playwright/test";
+import { APIRequestContext, expect, test } from "@playwright/test";
import { Prisma, prisma } from "@rallly/database";
import dayjs from "dayjs";
@@ -8,7 +8,168 @@ import dayjs from "dayjs";
* * Polls are soft deleted after 30 days of inactivity
* * Soft deleted polls are hard deleted after 7 days of being soft deleted
*/
-test.beforeAll(async ({ request, baseURL }) => {
+test.describe("house keeping", () => {
+ const callHouseKeeping = async (
+ request: APIRequestContext,
+ baseURL?: string,
+ ) => {
+ const res = await request.post(`${baseURL}/api/house-keeping`, {
+ headers: {
+ Authorization: `Bearer ${process.env.API_SECRET}`,
+ },
+ });
+ return res;
+ };
+ test.beforeAll(async ({ request, baseURL }) => {
+ // call the endpoint to delete any existing data that needs to be removed
+ await callHouseKeeping(request, baseURL);
+ await seedData();
+ const res = await callHouseKeeping(request, baseURL);
+ expect(await res.json()).toMatchObject({
+ softDeleted: 1,
+ deleted: 2,
+ });
+ });
+
+ test("should keep active polls", async () => {
+ const poll = await prisma.poll.findUnique({
+ where: {
+ id: "active-poll",
+ },
+ });
+
+ // expect active poll to not be deleted
+ expect(poll).not.toBeNull();
+ expect(poll?.deleted).toBeFalsy();
+ });
+
+ test("should keep polls that have been soft deleted for less than 7 days", async () => {
+ const deletedPoll6d = await prisma.poll.findFirst({
+ where: {
+ id: "deleted-poll-6d",
+ deleted: true,
+ },
+ });
+
+ // expect a poll that has been deleted for 6 days to
+ expect(deletedPoll6d).not.toBeNull();
+ });
+
+ test("should hard delete polls that have been soft deleted for 7 days", async () => {
+ const deletedPoll7d = await prisma.poll.findFirst({
+ where: {
+ id: "deleted-poll-7d",
+ deleted: true,
+ },
+ });
+
+ expect(deletedPoll7d).toBeNull();
+
+ const participants = await prisma.participant.findMany({
+ where: {
+ pollId: "deleted-poll-7d",
+ },
+ });
+
+ expect(participants.length).toBe(0);
+
+ const votes = await prisma.vote.findMany({
+ where: {
+ pollId: "deleted-poll-7d",
+ },
+ });
+
+ expect(votes.length).toBe(0);
+
+ const options = await prisma.option.findMany({
+ where: {
+ pollId: "deleted-poll-7d",
+ },
+ });
+
+ expect(options.length).toBe(0);
+ });
+
+ test("should keep polls that are still active", async () => {
+ const stillActivePoll = await prisma.poll.findUnique({
+ where: {
+ id: "still-active-poll",
+ },
+ });
+
+ expect(stillActivePoll).not.toBeNull();
+ expect(stillActivePoll?.deleted).toBeFalsy();
+ });
+
+ test("should soft delete polls that are inactive", async () => {
+ const inactivePoll = await prisma.poll.findFirst({
+ where: {
+ id: "inactive-poll",
+ deleted: true,
+ },
+ });
+
+ expect(inactivePoll).not.toBeNull();
+ expect(inactivePoll?.deleted).toBeTruthy();
+ expect(inactivePoll?.deletedAt).toBeTruthy();
+ });
+
+ test("should keep new demo poll", async () => {
+ const demoPoll = await prisma.poll.findFirst({
+ where: {
+ id: "demo-poll-new",
+ },
+ });
+
+ expect(demoPoll).not.toBeNull();
+ });
+
+ test("should delete old demo poll", async () => {
+ const oldDemoPoll = await prisma.poll.findFirst({
+ where: {
+ id: "demo-poll-old",
+ },
+ });
+
+ expect(oldDemoPoll).toBeNull();
+ });
+
+ test("should not delete poll that has options in the future", async () => {
+ const futureOptionPoll = await prisma.poll.findFirst({
+ where: {
+ id: "inactive-poll-future-option",
+ },
+ });
+
+ expect(futureOptionPoll).not.toBeNull();
+ });
+
+ // Teardown
+ test.afterAll(async () => {
+ await prisma.$executeRaw`DELETE FROM polls WHERE id IN (${Prisma.join([
+ "active-poll",
+ "deleted-poll-6d",
+ "deleted-poll-7d",
+ "still-active-poll",
+ "inactive-poll",
+ "inactive-poll-future-option",
+ "demo-poll-new",
+ "demo-poll-old",
+ ])})`;
+ await prisma.$executeRaw`DELETE FROM options WHERE id IN (${Prisma.join([
+ "option-1",
+ "option-2",
+ "option-3",
+ "option-4",
+ "option-5",
+ ])})`;
+ await prisma.$executeRaw`DELETE FROM participants WHERE id IN (${Prisma.join(
+ ["participant-1"],
+ )})`;
+ });
+});
+
+const seedData = async () => {
await prisma.poll.createMany({
data: [
// Active Poll
@@ -156,151 +317,4 @@ test.beforeAll(async ({ request, baseURL }) => {
},
],
});
-
- // call house-keeping endpoint
- const res = await request.post(`${baseURL}/api/house-keeping`, {
- headers: {
- Authorization: `Bearer ${process.env.API_SECRET}`,
- },
- });
-
- expect(await res.json()).toMatchObject({
- softDeleted: 1,
- deleted: 2,
- });
-});
-
-test("should keep active polls", async () => {
- const poll = await prisma.poll.findUnique({
- where: {
- id: "active-poll",
- },
- });
-
- // expect active poll to not be deleted
- expect(poll).not.toBeNull();
- expect(poll?.deleted).toBeFalsy();
-});
-
-test("should keep polls that have been soft deleted for less than 7 days", async () => {
- const deletedPoll6d = await prisma.poll.findFirst({
- where: {
- id: "deleted-poll-6d",
- deleted: true,
- },
- });
-
- // expect a poll that has been deleted for 6 days to
- expect(deletedPoll6d).not.toBeNull();
-});
-
-test("should hard delete polls that have been soft deleted for 7 days", async () => {
- const deletedPoll7d = await prisma.poll.findFirst({
- where: {
- id: "deleted-poll-7d",
- deleted: true,
- },
- });
-
- expect(deletedPoll7d).toBeNull();
-
- const participants = await prisma.participant.findMany({
- where: {
- pollId: "deleted-poll-7d",
- },
- });
-
- expect(participants.length).toBe(0);
-
- const votes = await prisma.vote.findMany({
- where: {
- pollId: "deleted-poll-7d",
- },
- });
-
- expect(votes.length).toBe(0);
-
- const options = await prisma.option.findMany({
- where: {
- pollId: "deleted-poll-7d",
- },
- });
-
- expect(options.length).toBe(0);
-});
-
-test("should keep polls that are still active", async () => {
- const stillActivePoll = await prisma.poll.findUnique({
- where: {
- id: "still-active-poll",
- },
- });
-
- expect(stillActivePoll).not.toBeNull();
- expect(stillActivePoll?.deleted).toBeFalsy();
-});
-
-test("should soft delete polls that are inactive", async () => {
- const inactivePoll = await prisma.poll.findFirst({
- where: {
- id: "inactive-poll",
- deleted: true,
- },
- });
-
- expect(inactivePoll).not.toBeNull();
- expect(inactivePoll?.deleted).toBeTruthy();
- expect(inactivePoll?.deletedAt).toBeTruthy();
-});
-
-test("should keep new demo poll", async () => {
- const demoPoll = await prisma.poll.findFirst({
- where: {
- id: "demo-poll-new",
- },
- });
-
- expect(demoPoll).not.toBeNull();
-});
-
-test("should delete old demo poll", async () => {
- const oldDemoPoll = await prisma.poll.findFirst({
- where: {
- id: "demo-poll-old",
- },
- });
-
- expect(oldDemoPoll).toBeNull();
-});
-
-test("should not delete poll that has options in the future", async () => {
- const futureOptionPoll = await prisma.poll.findFirst({
- where: {
- id: "inactive-poll-future-option",
- },
- });
-
- expect(futureOptionPoll).not.toBeNull();
-});
-
-// Teardown
-test.afterAll(async () => {
- await prisma.$executeRaw`DELETE FROM polls WHERE id IN (${Prisma.join([
- "active-poll",
- "deleted-poll-6d",
- "deleted-poll-7d",
- "still-active-poll",
- "inactive-poll",
- "demo-poll-new",
- "demo-poll-old",
- ])})`;
- await prisma.$executeRaw`DELETE FROM options WHERE id IN (${Prisma.join([
- "active-poll",
- "deleted-poll-6d",
- "deleted-poll-7d",
- "still-active-poll",
- "inactive-poll",
- "demo-poll-new",
- "demo-poll-old",
- ])})`;
-});
+};
diff --git a/packages/emails/src/send-email.tsx b/packages/emails/src/send-email.tsx
index a927680d4..74b0603d2 100644
--- a/packages/emails/src/send-email.tsx
+++ b/packages/emails/src/send-email.tsx
@@ -46,12 +46,19 @@ export const sendEmail = async (
templateName: T,
options: SendEmailOptions,
) => {
+ if (!process.env.SUPPORT_EMAIL) {
+ console.info("SUPPORT_EMAIL not configured - skipping email send");
+ return;
+ }
const transport = getTransport();
const Template = templates[templateName] as TemplateComponent;
try {
return await transport.sendMail({
- from: process.env.SUPPORT_EMAIL,
+ from: {
+ name: "Rallly",
+ address: process.env.SUPPORT_EMAIL,
+ },
to: options.to,
subject: options.subject,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
diff --git a/packages/emails/src/templates.ts b/packages/emails/src/templates.ts
index 563e97bcc..114a31aaa 100644
--- a/packages/emails/src/templates.ts
+++ b/packages/emails/src/templates.ts
@@ -1,7 +1,7 @@
-export * from "./templates/guest-verify-email";
+export * from "./templates/login";
export * from "./templates/new-comment";
export * from "./templates/new-participant";
export * from "./templates/new-participant-confirmation";
export * from "./templates/new-poll";
-export * from "./templates/new-poll-verification";
-export * from "./templates/verification-code";
+export * from "./templates/register";
+export * from "./templates/turn-on-notifications";
diff --git a/packages/emails/src/templates/components/email-layout.tsx b/packages/emails/src/templates/components/email-layout.tsx
index 7c76540a2..a76252558 100644
--- a/packages/emails/src/templates/components/email-layout.tsx
+++ b/packages/emails/src/templates/components/email-layout.tsx
@@ -4,6 +4,7 @@ import {
Body,
Container,
Head,
+ Hr,
Html,
Img,
Link,
@@ -12,14 +13,24 @@ import {
} from "@react-email/components";
import { Tailwind } from "@react-email/tailwind";
-export const EmailLayout = (props: {
- children: React.ReactNode;
+import { SmallText, Text } from "./styled-components";
+
+interface EmailLayoutProps {
preview: string;
-}) => {
+ recipientName: string;
+ footNote?: React.ReactNode;
+}
+
+export const EmailLayout = ({
+ preview,
+ recipientName = "Guest",
+ children,
+ footNote,
+}: React.PropsWithChildren) => {
return (
- {props.preview}
+ {preview}
-
-
+
+
-
+
+ Hi {recipientName},
+ {children}
+ {footNote ? (
+ <>
+
+ {footNote}
+ >
+ ) : null}
+
Home
diff --git a/packages/emails/src/templates/components/new-poll-base.tsx b/packages/emails/src/templates/components/new-poll-base.tsx
index 69bd8fb08..6e3db565d 100644
--- a/packages/emails/src/templates/components/new-poll-base.tsx
+++ b/packages/emails/src/templates/components/new-poll-base.tsx
@@ -1,6 +1,9 @@
+import { absoluteUrl } from "@rallly/utils";
+import { Hr } from "@react-email/components";
import { Container } from "@react-email/container";
-import { Link, Text } from "./styled-components";
+import { Link, SmallText, Text } from "./styled-components";
+import { removeProtocalFromUrl } from "./utils";
export interface NewPollBaseEmailProps {
title: string;
@@ -29,6 +32,12 @@ export const NewPollBaseEmail = ({
{children}
+
+
+ You are receiving this email because a new poll was created with this
+ email address on{" "}
+ {removeProtocalFromUrl(absoluteUrl())}
+
);
};
diff --git a/packages/emails/src/templates/components/notification-email.tsx b/packages/emails/src/templates/components/notification-email.tsx
new file mode 100644
index 000000000..4ed01b698
--- /dev/null
+++ b/packages/emails/src/templates/components/notification-email.tsx
@@ -0,0 +1,47 @@
+import { EmailLayout } from "./email-layout";
+import { Link, Section } from "./styled-components";
+
+export interface NotificationBaseProps {
+ name: string;
+ title: string;
+ pollUrl: string;
+ unsubscribeUrl: string;
+}
+
+export interface NotificationEmailProps extends NotificationBaseProps {
+ preview: string;
+}
+
+export const NotificationEmail = ({
+ name,
+ title,
+ pollUrl,
+ unsubscribeUrl,
+ preview,
+ children,
+}: React.PropsWithChildren) => {
+ return (
+
+ You're receiving this email because notifications are enabled for{" "}
+ {title}. If you want to stop receiving emails about
+ this event you can{" "}
+
+ turn notifications off
+
+ .
+ >
+ }
+ preview={preview}
+ >
+ {children}
+
+
+ );
+};
+
+export default NotificationEmail;
diff --git a/packages/emails/src/templates/components/styled-components.tsx b/packages/emails/src/templates/components/styled-components.tsx
index 24016580a..613d4efaf 100644
--- a/packages/emails/src/templates/components/styled-components.tsx
+++ b/packages/emails/src/templates/components/styled-components.tsx
@@ -39,7 +39,7 @@ export const Link = (props: LinkProps) => {
return (
);
};
@@ -61,3 +61,12 @@ export const Section = (props: SectionProps) => {
);
};
+
+export const SmallText = (props: TextProps) => {
+ return (
+
+ );
+};
diff --git a/packages/emails/src/templates/components/utils.ts b/packages/emails/src/templates/components/utils.ts
new file mode 100644
index 000000000..0198f1275
--- /dev/null
+++ b/packages/emails/src/templates/components/utils.ts
@@ -0,0 +1,3 @@
+export const removeProtocalFromUrl = (url: string) => {
+ return url.replace(/(^\w+:|^)\/\//, "");
+};
diff --git a/packages/emails/src/templates/guest-verify-email.tsx b/packages/emails/src/templates/guest-verify-email.tsx
deleted file mode 100644
index 164488e91..000000000
--- a/packages/emails/src/templates/guest-verify-email.tsx
+++ /dev/null
@@ -1,41 +0,0 @@
-import { Button, Container } from "@react-email/components";
-
-import { EmailLayout } from "./components/email-layout";
-import { Section, Text } from "./components/styled-components";
-
-type GuestVerifyEmailProps = {
- title: string;
- name: string;
- verificationLink: string;
- adminLink: string;
-};
-
-export const GuestVerifyEmail = ({
- title = "Untitled Poll",
- name = "Guest",
- verificationLink = "https://rallly.co",
-}: GuestVerifyEmailProps) => {
- return (
-
-
- Hi {name},
-
- To receive notifications for "{title}" you
- will need to verify your email address.
-
- To verify your email please click the button below.
-
-
-
-
-
- );
-};
-
-export default GuestVerifyEmail;
diff --git a/packages/emails/src/templates/login.tsx b/packages/emails/src/templates/login.tsx
new file mode 100644
index 000000000..09a855fbf
--- /dev/null
+++ b/packages/emails/src/templates/login.tsx
@@ -0,0 +1,55 @@
+import { absoluteUrl } from "@rallly/utils";
+
+import { EmailLayout } from "./components/email-layout";
+import { Heading, Link, Text } from "./components/styled-components";
+import { removeProtocalFromUrl } from "./components/utils";
+
+interface LoginEmailProps {
+ name: string;
+ code: string;
+ // magicLink: string;
+}
+
+export const LoginEmail = ({
+ name = "Guest",
+ code = "123456",
+}: // magicLink = "https://rallly.co",
+LoginEmailProps) => {
+ return (
+
+ You're receiving this email because a request was made to login
+ to{" "}
+
+ {removeProtocalFromUrl(absoluteUrl())}
+
+ . If this wasn't you, let us know by replying to this email.
+ >
+ }
+ recipientName={name}
+ preview={`Your 6-digit code: ${code}`}
+ >
+ Your 6-digit code is:
+
+ {code}
+
+
+ Use this code to complete the verification process on{" "}
+ {removeProtocalFromUrl(absoluteUrl())}
+
+
+
+ This code is valid for 15 minutes
+
+
+ {/* Magic link
+
+ Alternatively, you can login by using this{" "}
+ magic link ✨
+ */}
+
+ );
+};
+
+export default LoginEmail;
diff --git a/packages/emails/src/templates/new-comment.tsx b/packages/emails/src/templates/new-comment.tsx
index 4824b7f58..f6110e22b 100644
--- a/packages/emails/src/templates/new-comment.tsx
+++ b/packages/emails/src/templates/new-comment.tsx
@@ -1,12 +1,10 @@
-import { EmailLayout } from "./components/email-layout";
-import { Button, Link, Section, Text } from "./components/styled-components";
+import NotificationEmail, {
+ NotificationBaseProps,
+} from "./components/notification-email";
+import { Text } from "./components/styled-components";
-export interface NewCommentEmailProps {
- name: string;
- title: string;
+export interface NewCommentEmailProps extends NotificationBaseProps {
authorName: string;
- pollUrl: string;
- unsubscribeUrl: string;
}
export const NewCommentEmail = ({
@@ -17,20 +15,17 @@ export const NewCommentEmail = ({
unsubscribeUrl = "https://rallly.co",
}: NewCommentEmailProps) => {
return (
-
- Hi {name},
+
{authorName} has commented on {title}.
-
-
-
- Stop receiving notifications for this poll.
-
-
-
+
);
};
diff --git a/packages/emails/src/templates/new-participant-confirmation.tsx b/packages/emails/src/templates/new-participant-confirmation.tsx
index 43ec9b1a6..0d4cc22a3 100644
--- a/packages/emails/src/templates/new-participant-confirmation.tsx
+++ b/packages/emails/src/templates/new-participant-confirmation.tsx
@@ -12,8 +12,13 @@ export const NewParticipantConfirmationEmail = ({
editSubmissionUrl = "https://rallly.co",
}: NewParticipantConfirmationEmailProps) => {
return (
-
- Hi {name},
+ You are receiving this email because a response was submitting >
+ }
+ recipientName={name}
+ preview="To edit your response use the link below"
+ >
Thank you for submitting your availability for {title}.
diff --git a/packages/emails/src/templates/new-participant.tsx b/packages/emails/src/templates/new-participant.tsx
index 41b84de06..7fb734521 100644
--- a/packages/emails/src/templates/new-participant.tsx
+++ b/packages/emails/src/templates/new-participant.tsx
@@ -1,12 +1,10 @@
-import { EmailLayout } from "./components/email-layout";
-import { Button, Link, Section, Text } from "./components/styled-components";
+import NotificationEmail, {
+ NotificationBaseProps,
+} from "./components/notification-email";
+import { Text } from "./components/styled-components";
-export interface NewParticipantEmailProps {
- name: string;
- title: string;
+export interface NewParticipantEmailProps extends NotificationBaseProps {
participantName: string;
- pollUrl: string;
- unsubscribeUrl: string;
}
export const NewParticipantEmail = ({
@@ -17,21 +15,18 @@ export const NewParticipantEmail = ({
unsubscribeUrl = "https://rallly.co",
}: NewParticipantEmailProps) => {
return (
-
- Hi {name},
+
- {participantName} has shared their availability for{" "}
+ {participantName} has responded to{" "}
{title}.
-
-
-
- Stop receiving notifications for this poll.
-
-
-
+
);
};
diff --git a/packages/emails/src/templates/new-poll-verification.tsx b/packages/emails/src/templates/new-poll-verification.tsx
deleted file mode 100644
index 6d71ff38d..000000000
--- a/packages/emails/src/templates/new-poll-verification.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-import { EmailLayout } from "./components/email-layout";
-import {
- NewPollBaseEmail,
- NewPollBaseEmailProps,
-} from "./components/new-poll-base";
-import { Button, Heading, Section, Text } from "./components/styled-components";
-
-export interface NewPollVerificationEmailProps extends NewPollBaseEmailProps {
- verificationLink: string;
-}
-
-export const NewPollVerificationEmail = ({
- title = "Untitled Poll",
- name = "Guest",
- verificationLink = "https://rallly.co",
- adminLink = "https://rallly.co/admin/abcdefg123",
-}: NewPollVerificationEmailProps) => {
- return (
-
-
-
-
- Want to get notified when participants vote?
-
- Verify your email address to turn on notifications.
-
-
-
-
-
-
- );
-};
-
-export default NewPollVerificationEmail;
diff --git a/packages/emails/src/templates/new-poll.tsx b/packages/emails/src/templates/new-poll.tsx
index ce81fa8bc..2939d31dd 100644
--- a/packages/emails/src/templates/new-poll.tsx
+++ b/packages/emails/src/templates/new-poll.tsx
@@ -1,17 +1,93 @@
+import { absoluteUrl } from "@rallly/utils";
+
import { EmailLayout } from "./components/email-layout";
-import {
- NewPollBaseEmail,
- NewPollBaseEmailProps,
-} from "./components/new-poll-base";
+import { Heading, Link, Section, Text } from "./components/styled-components";
+import { removeProtocalFromUrl } from "./components/utils";
+
+export interface NewPollEmailProps {
+ title: string;
+ name: string;
+ adminLink: string;
+ participantLink: string;
+}
+
+const ShareLink = ({
+ title,
+ participantLink,
+ name,
+ children,
+}: React.PropsWithChildren<{
+ name: string;
+ title: string;
+ participantLink: string;
+}>) => {
+ return (
+
+ {children}
+
+ );
+};
+
+const LinkContainer = (props: { link: string }) => {
+ return (
+
+ );
+};
export const NewPollEmail = ({
title = "Untitled Poll",
name = "Guest",
adminLink = "https://rallly.co/admin/abcdefg123",
-}: NewPollBaseEmailProps) => {
+ participantLink = "https://rallly.co/p/wxyz9876",
+}: NewPollEmailProps) => {
return (
-
-
+
+ You are receiving this email because a new poll was created with this
+ email address on{" "}
+
+ {removeProtocalFromUrl(absoluteUrl())}
+
+ . If this wasn't you, please ignore this email.
+ >
+ }
+ recipientName={name}
+ preview="Share your participant link to start collecting responses."
+ >
+
+ Your new poll is ready! Now lets find a date for{" "}
+ {title}.
+
+
+ Copy this link and share it with your participants to start collecting
+ responses.
+
+
+
+
+ Share via email →
+
+
+ Your secret link
+
+ Use this link to access the admin page where you can view and edit your
+ poll.
+
+
+
+ Go to admin page →
+
);
};
diff --git a/packages/emails/src/templates/register.tsx b/packages/emails/src/templates/register.tsx
new file mode 100644
index 000000000..4edf63411
--- /dev/null
+++ b/packages/emails/src/templates/register.tsx
@@ -0,0 +1,47 @@
+import { absoluteUrl } from "@rallly/utils";
+import { Heading } from "@react-email/heading";
+
+import { EmailLayout } from "./components/email-layout";
+import { Link, Text } from "./components/styled-components";
+import { removeProtocalFromUrl } from "./components/utils";
+
+interface RegisterEmailProps {
+ name: string;
+ code: string;
+}
+
+export const RegisterEmail = ({
+ name = "Guest",
+ code = "123456",
+}: RegisterEmailProps) => {
+ return (
+
+ You're receiving this email because a request was made to
+ register an account on{" "}
+
+ {removeProtocalFromUrl(absoluteUrl())}
+
+ .
+ >
+ }
+ recipientName={name}
+ preview={`Your 6-digit code is: ${code}`}
+ >
+ Your 6-digit code is:
+
+ {code}
+
+
+ Use this code to complete the verification process on{" "}
+ {removeProtocalFromUrl(absoluteUrl())}
+
+
+ This code is valid for 15 minutes
+
+
+ );
+};
+
+export default RegisterEmail;
diff --git a/packages/emails/src/templates/turn-on-notifications.tsx b/packages/emails/src/templates/turn-on-notifications.tsx
new file mode 100644
index 000000000..4a2d1f419
--- /dev/null
+++ b/packages/emails/src/templates/turn-on-notifications.tsx
@@ -0,0 +1,51 @@
+import { EmailLayout } from "./components/email-layout";
+import {
+ Button,
+ Link,
+ Section,
+ SmallText,
+ Text,
+} from "./components/styled-components";
+
+type EnableNotificationsEmailProps = {
+ title: string;
+ name: string;
+ verificationLink: string;
+ adminLink: string;
+};
+
+export const EnableNotificationsEmail = ({
+ title = "Untitled Poll",
+ name = "Guest",
+ verificationLink = "https://rallly.co",
+ adminLink = "https://rallly.co",
+}: EnableNotificationsEmailProps) => {
+ return (
+
+ You are receiving this email because a request was made to enable
+ notifications for {title}.
+ >
+ }
+ >
+
+ Before we can send you notifications we need to verify your email.
+
+
+ Click the button below to complete the email verification and enable
+ notifications for {title}.
+
+
+
+
+ The link will expire in 15 minutes.
+
+ );
+};
+
+export default EnableNotificationsEmail;
diff --git a/packages/emails/src/templates/verification-code.tsx b/packages/emails/src/templates/verification-code.tsx
deleted file mode 100644
index 28d8ae50d..000000000
--- a/packages/emails/src/templates/verification-code.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-import { Heading } from "@react-email/heading";
-
-import { EmailLayout } from "./components/email-layout";
-import { Section, Text } from "./components/styled-components";
-
-interface VerificationCodeEmailProps {
- name: string;
- code: string;
-}
-
-export const VerificationCodeEmail = ({
- name = "Guest",
- code = "123456",
-}: VerificationCodeEmailProps) => {
- return (
-
- Hi {name},
- Please use the code below to verify your email address.
-
- Your 6-digit code is:
-
- {code}
-
-
-
- This code is valid for 15 minutes
-
-
-
-
- );
-};
-
-export default VerificationCodeEmail;