diff --git a/README.md b/README.md
index fb11ee2cc8..28c0ec2c10 100644
--- a/README.md
+++ b/README.md
@@ -68,13 +68,12 @@ We are working on the documentation to get started with Midday for local develop
### Services
- Trigger.dev (background jobs)
-- Resend (email)
+- Resend (Transactional & Marketing)
- Novu (notifications)
- Github Actions (CI/CD)
- GoCardLess (Bank connection EU)
- Plaid (Bank connection in Canada and US)
- Teller (Bank connection in the US)
-- Loops (Marketing email)
- OpenPanel (Events and Analytics)
- Dub (Short URLs)
- Polar (Payment processing)
diff --git a/apps/dashboard/.env-example b/apps/dashboard/.env-example
index 7a1126e62f..d2af2b7779 100644
--- a/apps/dashboard/.env-example
+++ b/apps/dashboard/.env-example
@@ -6,10 +6,7 @@ SUPABASE_SERVICE_KEY=
# Resend
RESEND_API_KEY=
-
-# Loops
-LOOPS_ENDPOINT=
-LOOPS_API_KEY=
+RESEND_AUDIENCE_ID=
# GoCardLess
GOCARDLESS_SECRET_ID=
@@ -60,10 +57,6 @@ MISTRAL_API_KEY=
NEXT_PUBLIC_OPENPANEL_CLIENT_ID=
OPENPANEL_SECRET_KEY=
-# Engine
-ENGINE_API_ENDPOINT=http://localhost:3002
-ENGINE_API_SECRET=secret
-
# Webhook
WEBHOOK_SECRET_KEY=6c369443-1a88-444e-b459-7e662c1fff9e
@@ -75,7 +68,7 @@ SENTRY_PROJECT=
# Engine
MIDDAY_ENGINE_API_KEY=secret
-MIDDAY_BASE_URL=http://localhost:3002
+NEXT_PUBLIC_ENGINE_API_URL=http://localhost:3002
# Azure
AZURE_DOCUMENT_INTELLIGENCE_ENDPOINT=
diff --git a/apps/dashboard/README.md b/apps/dashboard/README.md
index 75b67d4b5f..3a38f9b3eb 100644
--- a/apps/dashboard/README.md
+++ b/apps/dashboard/README.md
@@ -1 +1 @@
-## Dashboard
+## Dashboard
\ No newline at end of file
diff --git a/apps/dashboard/package.json b/apps/dashboard/package.json
index a6dceb47c2..81090e2605 100644
--- a/apps/dashboard/package.json
+++ b/apps/dashboard/package.json
@@ -50,7 +50,6 @@
"framer-motion": "^11.12.0",
"geist": "^1.3.1",
"little-date": "^1.0.0",
- "loops": "1.0.1",
"lottie-react": "^2.4.0",
"ms": "^2.1.3",
"next": "14.2.1",
@@ -70,7 +69,7 @@
"react-plaid-link": "^3.6.0",
"react-use-draggable-scroll": "^0.4.7",
"recharts": "^2.12.7",
- "resend": "^3.5.0",
+ "resend": "^4.0.1",
"sharp": "^0.33.5",
"tus-js-client": "4.1.0",
"use-long-press": "^3.2.0",
diff --git a/apps/dashboard/src/actions/delete-user-action.ts b/apps/dashboard/src/actions/delete-user-action.ts
index 30254eba3b..2615957769 100644
--- a/apps/dashboard/src/actions/delete-user-action.ts
+++ b/apps/dashboard/src/actions/delete-user-action.ts
@@ -1,15 +1,13 @@
"use server";
+import { resend } from "@/utils/resend";
import { LogEvents } from "@midday/events/events";
import { setupAnalytics } from "@midday/events/server";
import { getUser } from "@midday/supabase/cached-queries";
import { deleteUser } from "@midday/supabase/mutations";
import { createClient } from "@midday/supabase/server";
-import { LoopsClient } from "loops";
import { redirect } from "next/navigation";
-const loops = new LoopsClient(process.env.LOOPS_API_KEY!);
-
export const deleteUserAction = async () => {
const supabase = createClient();
const user = await getUser();
@@ -30,7 +28,10 @@ export const deleteUserAction = async () => {
const userId = await deleteUser(supabase);
- await loops.deleteContact({ userId });
+ await resend.contacts.remove({
+ email: user.data?.email!,
+ audienceId: process.env.RESEND_AUDIENCE_ID!,
+ });
const analytics = await setupAnalytics({
userId,
diff --git a/apps/dashboard/src/actions/schema.ts b/apps/dashboard/src/actions/schema.ts
index afe2d85740..a607397066 100644
--- a/apps/dashboard/src/actions/schema.ts
+++ b/apps/dashboard/src/actions/schema.ts
@@ -70,7 +70,6 @@ export type UpdateTeamFormValues = z.infer;
export const subscribeSchema = z.object({
email: z.string().email(),
- userGroup: z.string(),
});
export const deleteBankAccountSchema = z.object({
diff --git a/apps/dashboard/src/actions/subscribe-action.ts b/apps/dashboard/src/actions/subscribe-action.ts
index 51256cbae3..eff42df905 100644
--- a/apps/dashboard/src/actions/subscribe-action.ts
+++ b/apps/dashboard/src/actions/subscribe-action.ts
@@ -1,5 +1,6 @@
"use server";
+import { resend } from "@/utils/resend";
import { authActionClient } from "./safe-action";
import { subscribeSchema } from "./schema";
@@ -8,20 +9,9 @@ export const subscribeAction = authActionClient
.metadata({
name: "subscribe",
})
- .action(async ({ parsedInput: { email, userGroup } }) => {
- const res = await fetch(
- "https://app.loops.so/api/newsletter-form/clna1p09j00d3l60og56gj3u1",
- {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify({
- email,
- userGroup,
- }),
- },
- );
-
- return res.json();
+ .action(async ({ parsedInput: { email } }) => {
+ return resend.contacts.create({
+ email,
+ audienceId: process.env.RESEND_AUDIENCE_ID!,
+ });
});
diff --git a/apps/dashboard/src/app/api/webhook/registered/route.ts b/apps/dashboard/src/app/api/webhook/registered/route.ts
index 5bb70633e6..86206b0f52 100644
--- a/apps/dashboard/src/app/api/webhook/registered/route.ts
+++ b/apps/dashboard/src/app/api/webhook/registered/route.ts
@@ -1,21 +1,17 @@
import * as crypto from "node:crypto";
import { env } from "@/env.mjs";
import { logger } from "@/utils/logger";
+import { resend } from "@/utils/resend";
import WelcomeEmail from "@midday/email/emails/welcome";
import { LogEvents } from "@midday/events/events";
import { setupAnalytics } from "@midday/events/server";
import { render } from "@react-email/render";
-import { LoopsClient } from "loops";
import { nanoid } from "nanoid";
import { headers } from "next/headers";
import { NextResponse } from "next/server";
-import { Resend } from "resend";
export const dynamic = "force-dynamic";
-const loops = new LoopsClient(env.LOOPS_API_KEY);
-const resend = new Resend(env.RESEND_API_KEY);
-
// NOTE: This is trigger from supabase database webhook
export async function POST(req: Request) {
const text = await req.clone().text();
@@ -74,34 +70,17 @@ export async function POST(req: Request) {
}
try {
- const found = await loops.findContact(email);
const [firstName, lastName] = fullName?.split(" ") ?? [];
- if (found.length > 0) {
- const userId = found?.at(0)?.id;
-
- if (!userId) {
- return null;
- }
-
- await loops.updateContact(email, {
- userId,
- userGroup: "registered",
- firstName,
- lastName,
- });
- } else {
- await loops.createContact(email, {
- userId: body.record.id,
- userGroup: "registered",
- firstName,
- lastName,
- });
- }
+ await resend.contacts.create({
+ email,
+ firstName,
+ lastName,
+ unsubscribed: false,
+ audienceId: env.RESEND_AUDIENCE_ID,
+ });
} catch (error) {
- const message = error instanceof Error ? error.message : "Unknown error";
-
- logger(message);
+ logger(error as string);
}
return NextResponse.json({ success: true });
diff --git a/apps/dashboard/src/env.mjs b/apps/dashboard/src/env.mjs
index c84727beeb..d033e4335d 100644
--- a/apps/dashboard/src/env.mjs
+++ b/apps/dashboard/src/env.mjs
@@ -19,12 +19,11 @@ export const env = createEnv({
SUPABASE_SERVICE_KEY: z.string(),
UPSTASH_REDIS_REST_TOKEN: z.string(),
UPSTASH_REDIS_REST_URL: z.string(),
- LOOPS_ENDPOINT: z.string(),
- LOOPS_API_KEY: z.string(),
GOCARDLESS_SECRET_ID: z.string(),
GOCARDLESS_SECRET_KEY: z.string(),
NOVU_API_KEY: z.string(),
RESEND_API_KEY: z.string(),
+ RESEND_AUDIENCE_ID: z.string(),
OPENPANEL_SECRET_KEY: z.string(),
MIDDAY_ENGINE_API_KEY: z.string(),
MIDDAY_CACHE_API_SECRET: z.string(),
@@ -61,9 +60,8 @@ export const env = createEnv({
NEXT_PUBLIC_TELLER_ENVIRONMENT: process.env.NEXT_PUBLIC_TELLER_ENVIRONMENT,
NEXT_PUBLIC_PLAID_ENVIRONMENT: process.env.NEXT_PUBLIC_PLAID_ENVIRONMENT,
RESEND_API_KEY: process.env.RESEND_API_KEY,
+ RESEND_AUDIENCE_ID: process.env.RESEND_AUDIENCE_ID,
PORT: process.env.PORT,
- LOOPS_ENDPOINT: process.env.LOOPS_ENDPOINT,
- LOOPS_API_KEY: process.env.LOOPS_API_KEY,
GOCARDLESS_SECRET_ID: process.env.GOCARDLESS_SECRET_ID,
GOCARDLESS_SECRET_KEY: process.env.GOCARDLESS_SECRET_KEY,
NOVU_API_KEY: process.env.NOVU_API_KEY,
diff --git a/apps/dashboard/src/utils/engine.ts b/apps/dashboard/src/utils/engine.ts
deleted file mode 100644
index af70d5b089..0000000000
--- a/apps/dashboard/src/utils/engine.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import Midday from "@midday-ai/engine";
-
-export const engine = new Midday({
- environment: process.env.MIDDAY_ENGINE_ENVIRONMENT as
- | "production"
- | "staging"
- | "development"
- | undefined,
-});
diff --git a/apps/website/.env-template b/apps/website/.env-template
index a971fefdf8..a54e4c1856 100644
--- a/apps/website/.env-template
+++ b/apps/website/.env-template
@@ -1,3 +1,5 @@
UPSTASH_REDIS_REST_TOKEN=
UPSTASH_REDIS_REST_URL=
-NEXT_PUBLIC_OPENPANEL_CLIENT_ID=
\ No newline at end of file
+NEXT_PUBLIC_OPENPANEL_CLIENT_ID=
+RESEND_API_KEY=
+RESEND_AUDIENCE_ID=
\ No newline at end of file
diff --git a/apps/website/package.json b/apps/website/package.json
index 725318b279..7e72e74545 100644
--- a/apps/website/package.json
+++ b/apps/website/package.json
@@ -28,6 +28,7 @@
"react-dom": "19.0.0-rc-66855b96-20241106",
"react-hls-player": "^3.0.7",
"react-use-draggable-scroll": "^0.4.7",
+ "resend": "^4.0.1",
"server-only": "^0.0.1",
"sharp": "^0.33.5",
"sugar-high": "^0.7.5"
diff --git a/apps/website/src/actions/subscribe-action.ts b/apps/website/src/actions/subscribe-action.ts
index 999376bff0..952fef4473 100644
--- a/apps/website/src/actions/subscribe-action.ts
+++ b/apps/website/src/actions/subscribe-action.ts
@@ -1,27 +1,12 @@
"use server";
-import { getCountryCode } from "@midday/location";
+import { resend } from "@/utils/resend";
-export async function subscribeAction(formData: FormData, userGroup: string) {
+export async function subscribeAction(formData: FormData) {
const email = formData.get("email") as string;
- const country = await getCountryCode();
- const res = await fetch(
- "https://app.loops.so/api/newsletter-form/clna1p09j00d3l60og56gj3u1",
- {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify({
- email,
- userGroup,
- country,
- }),
- },
- );
-
- const json = await res.json();
-
- return json;
+ return resend.contacts.create({
+ email,
+ audienceId: process.env.RESEND_AUDIENCE_ID!,
+ });
}
diff --git a/apps/website/src/app/engine/page.tsx b/apps/website/src/app/engine/page.tsx
index aaf84dd7cb..489102044f 100644
--- a/apps/website/src/app/engine/page.tsx
+++ b/apps/website/src/app/engine/page.tsx
@@ -31,7 +31,7 @@ export default function Page() {
-
+
diff --git a/apps/website/src/components/footer.tsx b/apps/website/src/components/footer.tsx
index 326f8f1d00..2903fc4d81 100644
--- a/apps/website/src/components/footer.tsx
+++ b/apps/website/src/components/footer.tsx
@@ -103,7 +103,7 @@ export function Footer() {
-
+
diff --git a/apps/website/src/components/subscribe-input.tsx b/apps/website/src/components/subscribe-input.tsx
index 6f3a730724..6ce7c76fcb 100644
--- a/apps/website/src/components/subscribe-input.tsx
+++ b/apps/website/src/components/subscribe-input.tsx
@@ -26,11 +26,7 @@ function SubmitButton() {
);
}
-type Props = {
- group: string;
-};
-
-export function SubscribeInput({ group }: Props) {
+export function SubscribeInput() {
const [isSubmitted, setSubmitted] = useState(false);
return (
@@ -57,7 +53,7 @@ export function SubscribeInput({ group }: Props) {