From 872f562c75ecf821a6a7fbcfcc5d795e36717b4a Mon Sep 17 00:00:00 2001 From: Tim Wilson Date: Tue, 4 Nov 2025 16:45:23 -0500 Subject: [PATCH 01/13] Email preview fixes --- apps/web/app/(ee)/api/cron/campaigns/broadcast/route.ts | 2 +- .../lib/actions/campaigns/send-campaign-preview-email.ts | 6 ++++-- .../web/lib/api/workflows/execute-send-campaign-workflow.ts | 2 +- packages/email/src/templates/campaign-email.tsx | 6 +++--- packages/prisma/schema/campaign.prisma | 1 + 5 files changed, 10 insertions(+), 7 deletions(-) diff --git a/apps/web/app/(ee)/api/cron/campaigns/broadcast/route.ts b/apps/web/app/(ee)/api/cron/campaigns/broadcast/route.ts index fe5db59303d..3dcfea3f56b 100644 --- a/apps/web/app/(ee)/api/cron/campaigns/broadcast/route.ts +++ b/apps/web/app/(ee)/api/cron/campaigns/broadcast/route.ts @@ -226,7 +226,7 @@ export async function POST(req: Request) { }, campaign: { type: campaign.type, - subject: campaign.subject, + preview: campaign.preview, body: renderCampaignEmailHTML({ content: campaign.bodyJson as unknown as TiptapNode, variables: { diff --git a/apps/web/lib/actions/campaigns/send-campaign-preview-email.ts b/apps/web/lib/actions/campaigns/send-campaign-preview-email.ts index 0d2533764d1..0a3bb481cc1 100644 --- a/apps/web/lib/actions/campaigns/send-campaign-preview-email.ts +++ b/apps/web/lib/actions/campaigns/send-campaign-preview-email.ts @@ -16,6 +16,7 @@ const sendPreviewEmailSchema = z campaignId: z.string(), workspaceId: z.string(), subject: z.string().min(1, "Email subject is required."), + preview: z.string().nullish(), from: z.string().email().optional(), emailAddresses: z .array(z.string().email()) @@ -32,7 +33,8 @@ export const sendCampaignPreviewEmail = authActionClient .schema(sendPreviewEmailSchema) .action(async ({ parsedInput, ctx }) => { const { workspace } = ctx; - const { campaignId, subject, from, bodyJson, emailAddresses } = parsedInput; + const { campaignId, subject, preview, from, bodyJson, emailAddresses } = + parsedInput; const programId = getDefaultProgramIdOrThrow(workspace); @@ -66,7 +68,7 @@ export const sendCampaignPreviewEmail = authActionClient }, campaign: { type: campaign.type, - subject, + preview, body: renderCampaignEmailHTML({ content: bodyJson as unknown as TiptapNode, variables: { diff --git a/apps/web/lib/api/workflows/execute-send-campaign-workflow.ts b/apps/web/lib/api/workflows/execute-send-campaign-workflow.ts index eae4816c604..01e8c9ac0d8 100644 --- a/apps/web/lib/api/workflows/execute-send-campaign-workflow.ts +++ b/apps/web/lib/api/workflows/execute-send-campaign-workflow.ts @@ -192,7 +192,7 @@ export const executeSendCampaignWorkflow = async ({ }, campaign: { type: campaign.type, - subject: campaign.subject, + preview: campaign.preview, body: renderCampaignEmailHTML({ content: campaign.bodyJson as unknown as TiptapNode, variables: { diff --git a/packages/email/src/templates/campaign-email.tsx b/packages/email/src/templates/campaign-email.tsx index 0f669aa53e2..fad8a229701 100644 --- a/packages/email/src/templates/campaign-email.tsx +++ b/packages/email/src/templates/campaign-email.tsx @@ -24,7 +24,7 @@ export default function CampaignEmail({ }, campaign = { type: "marketing", - subject: "Test Subject", + preview: "Test Preview", body: `

Hi {{PartnerName}},

Thrilled to have you officially join the Acme Ambassador Program!

As a Acme Ambassador, you're joining the front line of change. You're freeing people from broken healthcare and giving them back control of their health.

Your 3 quick steps to get started:

  1. Activate your membership with your 50% off code: ACME50OFF

  2. Open your dashboard and copy your referral link

  3. Share Acme with your loved ones! Use our Ambassador Hub for all the information you need like message templates, images, and information about Acme.

And a bonus: make your first referral within 7 days and you'll also receive a limited-edition Acme hoodie.

We're here with you every step of the way.

To your health,

The Acme team

`, }, }: { @@ -36,7 +36,7 @@ export default function CampaignEmail({ }; campaign?: { type: "transactional" | "marketing"; - subject: string; + preview?: string | null; body: string; }; }) { @@ -57,7 +57,7 @@ export default function CampaignEmail({ return ( - {campaign.subject} + {campaign.preview && {campaign.preview}} diff --git a/packages/prisma/schema/campaign.prisma b/packages/prisma/schema/campaign.prisma index d2d9933748f..a67441e0e52 100644 --- a/packages/prisma/schema/campaign.prisma +++ b/packages/prisma/schema/campaign.prisma @@ -27,6 +27,7 @@ model Campaign { status CampaignStatus @default(draft) name String subject String + preview String? from String? bodyJson Json @db.Json scheduledAt DateTime? From 6ca6250296c2e17a2012258ac75047160ad5755a Mon Sep 17 00:00:00 2001 From: Tim Wilson Date: Tue, 4 Nov 2025 16:50:06 -0500 Subject: [PATCH 02/13] Update campaign-email.tsx --- packages/email/src/templates/campaign-email.tsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/packages/email/src/templates/campaign-email.tsx b/packages/email/src/templates/campaign-email.tsx index fad8a229701..fb52d139699 100644 --- a/packages/email/src/templates/campaign-email.tsx +++ b/packages/email/src/templates/campaign-email.tsx @@ -1,5 +1,3 @@ -import { DUB_WORDMARK } from "@dub/utils"; - import { Body, Container, @@ -61,10 +59,6 @@ export default function CampaignEmail({ -
- Dub -
-
Date: Tue, 4 Nov 2025 17:04:31 -0500 Subject: [PATCH 03/13] Add inline image styles --- apps/web/lib/api/workflows/render-campaign-email-html.ts | 6 +++++- packages/email/src/templates/campaign-email.tsx | 8 -------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/apps/web/lib/api/workflows/render-campaign-email-html.ts b/apps/web/lib/api/workflows/render-campaign-email-html.ts index 335c905e824..f8c884de961 100644 --- a/apps/web/lib/api/workflows/render-campaign-email-html.ts +++ b/apps/web/lib/api/workflows/render-campaign-email-html.ts @@ -20,7 +20,11 @@ export function renderCampaignEmailHTML({ levels: [1, 2], }, }), - Image, + Image.configure({ + HTMLAttributes: { + style: "max-width: 100%; height: auto; margin: 12px auto;", + }, + }), Mention.extend({ renderHTML({ node }: { node: any }) { return [ diff --git a/packages/email/src/templates/campaign-email.tsx b/packages/email/src/templates/campaign-email.tsx index fb52d139699..9ec9eaaea00 100644 --- a/packages/email/src/templates/campaign-email.tsx +++ b/packages/email/src/templates/campaign-email.tsx @@ -38,16 +38,8 @@ export default function CampaignEmail({ body: string; }; }) { - // Wrap the HTML with styles to prevent overflow from large images const styledHtml = `
- ${campaign.body}
`; From 086b3f5c8d91b9f691395b0d14f829b7405adffa Mon Sep 17 00:00:00 2001 From: Tim Wilson Date: Tue, 4 Nov 2025 17:17:07 -0500 Subject: [PATCH 04/13] Temporarily remove new column --- apps/web/app/(ee)/api/cron/campaigns/broadcast/route.ts | 2 +- apps/web/lib/api/workflows/execute-send-campaign-workflow.ts | 2 +- packages/prisma/schema/campaign.prisma | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/web/app/(ee)/api/cron/campaigns/broadcast/route.ts b/apps/web/app/(ee)/api/cron/campaigns/broadcast/route.ts index 3dcfea3f56b..c2f8b457b0a 100644 --- a/apps/web/app/(ee)/api/cron/campaigns/broadcast/route.ts +++ b/apps/web/app/(ee)/api/cron/campaigns/broadcast/route.ts @@ -226,7 +226,7 @@ export async function POST(req: Request) { }, campaign: { type: campaign.type, - preview: campaign.preview, + // preview: campaign.preview, body: renderCampaignEmailHTML({ content: campaign.bodyJson as unknown as TiptapNode, variables: { diff --git a/apps/web/lib/api/workflows/execute-send-campaign-workflow.ts b/apps/web/lib/api/workflows/execute-send-campaign-workflow.ts index 01e8c9ac0d8..673eb613e22 100644 --- a/apps/web/lib/api/workflows/execute-send-campaign-workflow.ts +++ b/apps/web/lib/api/workflows/execute-send-campaign-workflow.ts @@ -192,7 +192,7 @@ export const executeSendCampaignWorkflow = async ({ }, campaign: { type: campaign.type, - preview: campaign.preview, + //preview: campaign.preview, body: renderCampaignEmailHTML({ content: campaign.bodyJson as unknown as TiptapNode, variables: { diff --git a/packages/prisma/schema/campaign.prisma b/packages/prisma/schema/campaign.prisma index a67441e0e52..c184cc4ae11 100644 --- a/packages/prisma/schema/campaign.prisma +++ b/packages/prisma/schema/campaign.prisma @@ -27,7 +27,7 @@ model Campaign { status CampaignStatus @default(draft) name String subject String - preview String? + // preview String? from String? bodyJson Json @db.Json scheduledAt DateTime? From 2db989826e67b3fdae9c18936ae026596238db12 Mon Sep 17 00:00:00 2001 From: Tim Wilson Date: Tue, 4 Nov 2025 17:31:11 -0500 Subject: [PATCH 05/13] Update render-campaign-email-html.ts --- apps/web/lib/api/workflows/render-campaign-email-html.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/lib/api/workflows/render-campaign-email-html.ts b/apps/web/lib/api/workflows/render-campaign-email-html.ts index f8c884de961..cee5ebbe9f8 100644 --- a/apps/web/lib/api/workflows/render-campaign-email-html.ts +++ b/apps/web/lib/api/workflows/render-campaign-email-html.ts @@ -88,7 +88,7 @@ const sanitizeHtmlBody = (body: string) => { ], allowedAttributes: { a: ["href", "name", "target", "rel"], - img: ["src", "alt", "title"], + img: ["src", "alt", "title", "style"], ul: ["style"], ol: ["style"], li: ["style"], From 903eef4813d1ea5a6b5d35627509764cddde0f89 Mon Sep 17 00:00:00 2001 From: Tim Wilson Date: Wed, 5 Nov 2025 10:01:16 -0500 Subject: [PATCH 06/13] Revert "Temporarily remove new column" This reverts commit 086b3f5c8d91b9f691395b0d14f829b7405adffa. --- apps/web/app/(ee)/api/cron/campaigns/broadcast/route.ts | 2 +- apps/web/lib/api/workflows/execute-send-campaign-workflow.ts | 2 +- packages/prisma/schema/campaign.prisma | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/web/app/(ee)/api/cron/campaigns/broadcast/route.ts b/apps/web/app/(ee)/api/cron/campaigns/broadcast/route.ts index c2f8b457b0a..3dcfea3f56b 100644 --- a/apps/web/app/(ee)/api/cron/campaigns/broadcast/route.ts +++ b/apps/web/app/(ee)/api/cron/campaigns/broadcast/route.ts @@ -226,7 +226,7 @@ export async function POST(req: Request) { }, campaign: { type: campaign.type, - // preview: campaign.preview, + preview: campaign.preview, body: renderCampaignEmailHTML({ content: campaign.bodyJson as unknown as TiptapNode, variables: { diff --git a/apps/web/lib/api/workflows/execute-send-campaign-workflow.ts b/apps/web/lib/api/workflows/execute-send-campaign-workflow.ts index 673eb613e22..01e8c9ac0d8 100644 --- a/apps/web/lib/api/workflows/execute-send-campaign-workflow.ts +++ b/apps/web/lib/api/workflows/execute-send-campaign-workflow.ts @@ -192,7 +192,7 @@ export const executeSendCampaignWorkflow = async ({ }, campaign: { type: campaign.type, - //preview: campaign.preview, + preview: campaign.preview, body: renderCampaignEmailHTML({ content: campaign.bodyJson as unknown as TiptapNode, variables: { diff --git a/packages/prisma/schema/campaign.prisma b/packages/prisma/schema/campaign.prisma index c184cc4ae11..a67441e0e52 100644 --- a/packages/prisma/schema/campaign.prisma +++ b/packages/prisma/schema/campaign.prisma @@ -27,7 +27,7 @@ model Campaign { status CampaignStatus @default(draft) name String subject String - // preview String? + preview String? from String? bodyJson Json @db.Json scheduledAt DateTime? From ccdb34788ebea3b62558a731114fc3e798280cb1 Mon Sep 17 00:00:00 2001 From: Tim Wilson Date: Wed, 5 Nov 2025 16:04:42 -0500 Subject: [PATCH 07/13] Add email preview customization --- .../(ee)/api/campaigns/[campaignId]/route.ts | 2 + .../[campaignId]/campaign-editor.tsx | 102 ++++++++++++++++-- .../[campaignId]/send-email-preview-modal.tsx | 5 +- .../lib/api/campaigns/validate-campaign.ts | 1 + apps/web/lib/zod/schemas/campaigns.ts | 2 + 5 files changed, 103 insertions(+), 9 deletions(-) diff --git a/apps/web/app/(ee)/api/campaigns/[campaignId]/route.ts b/apps/web/app/(ee)/api/campaigns/[campaignId]/route.ts index a375ecf5031..862f02a0223 100644 --- a/apps/web/app/(ee)/api/campaigns/[campaignId]/route.ts +++ b/apps/web/app/(ee)/api/campaigns/[campaignId]/route.ts @@ -64,6 +64,7 @@ export const PATCH = withWorkspace( const { name, subject, + preview, from, status, bodyJson, @@ -121,6 +122,7 @@ export const PATCH = withWorkspace( data: { ...(name && { name }), ...(subject && { subject }), + ...(preview && { preview }), ...(from && { from }), ...(status && { status }), ...(bodyJson && { bodyJson }), diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/[campaignId]/campaign-editor.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/[campaignId]/campaign-editor.tsx index abdcefc317c..1832215f2ec 100644 --- a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/[campaignId]/campaign-editor.tsx +++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/[campaignId]/campaign-editor.tsx @@ -21,10 +21,17 @@ import { TooltipContent, useKeyboardShortcut, } from "@dub/ui"; -import { capitalize } from "@dub/utils"; +import { capitalize, cn } from "@dub/utils"; +import { motion } from "motion/react"; import { useAction } from "next-safe-action/hooks"; import Link from "next/link"; -import { useCallback, useEffect, useRef } from "react"; +import { + PropsWithChildren, + useCallback, + useEffect, + useRef, + useState, +} from "react"; import { Controller, FormProvider, useForm } from "react-hook-form"; import { toast } from "sonner"; import { useDebouncedCallback } from "use-debounce"; @@ -38,7 +45,7 @@ import { TransactionalCampaignLogic } from "./transactional-campaign-logic"; import { isValidTriggerCondition } from "./utils"; const inputClassName = - "hover:border-border-subtle h-8 w-full rounded-md transition-colors duration-150 focus:border-black/75 border focus:ring-black/75 border-transparent px-1.5 py-0 text-sm text-content-default placeholder:text-content-muted hover:bg-neutral-100 hover:cursor-pointer"; + "hover:border-border-subtle h-8 w-full rounded-md transition-colors duration-150 focus:border-black/75 border focus:ring-black/75 border-transparent px-1.5 py-0 sm:text-sm text-content-default placeholder:text-content-muted hover:bg-neutral-100 hover:cursor-pointer"; const labelClassName = "text-sm font-medium text-content-subtle"; @@ -112,6 +119,25 @@ export function CampaignEditor({ campaign }: { campaign: Campaign }) { formState: { dirtyFields }, } = form; + const previewInputRef = useRef(null); + + const [showPreviewText, setShowPreviewText] = useState( + Boolean(campaign.preview), + ); + + // Show preview text when preview is set + useEffect(() => { + const { unsubscribe } = watch(({ preview }) => { + if (preview) setShowPreviewText(true); + }); + return () => unsubscribe(); + }, [watch]); + + // Focus preview input when opened + useEffect(() => { + if (showPreviewText) previewInputRef.current?.focus(); + }, [showPreviewText]); + const saveCampaign = useCallback( async ({ isDraft = false }: { isDraft?: boolean }) => { const allFormData = getValues(); @@ -215,6 +241,11 @@ export function CampaignEditor({ campaign }: { campaign: Campaign }) { const statusBadge = CAMPAIGN_STATUS_BADGES[campaign.status]; const editorRef = useRef<{ setContent: (content: any) => void }>(null); + const previewInputProps = register("preview", { + onBlur: (e) => { + if (!e.target.value) setShowPreviewText(false); + }, + }); return ( @@ -326,7 +357,7 @@ export function CampaignEditor({ campaign }: { campaign: Campaign }) { { @@ -402,14 +433,54 @@ export function CampaignEditor({ campaign }: { campaign: Campaign }) { disabled={isReadOnly} hideIcon={true} > +
+ + {!isReadOnly && ( +
+ +
+ )} +
+ + + + + Preview + + + { + previewInputProps.ref(e); + previewInputRef.current = e; + }} /> - + {campaign.type === "transactional" && ( <> @@ -515,3 +586,20 @@ export function CampaignEditor({ campaign }: { campaign: Campaign }) {
); } + +const ConditionalColumn = ({ + show, + children, +}: PropsWithChildren<{ show: boolean }>) => { + return ( + + {children} + + ); +}; diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/[campaignId]/send-email-preview-modal.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/[campaignId]/send-email-preview-modal.tsx index e406e85f5e3..474b17898b5 100644 --- a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/[campaignId]/send-email-preview-modal.tsx +++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/[campaignId]/send-email-preview-modal.tsx @@ -27,9 +27,9 @@ function SendEmailPreviewModal({ const { control } = useCampaignFormContext(); const [emailAddresses, setEmailAddresses] = useState(user?.email ?? ""); - const [subject, bodyJson, from] = useWatch({ + const [subject, preview, bodyJson, from] = useWatch({ control, - name: ["subject", "bodyJson", "from"], + name: ["subject", "preview", "bodyJson", "from"], }); const { executeAsync: sendEmailPreview, isPending } = useAction( @@ -81,6 +81,7 @@ function SendEmailPreviewModal({ workspaceId, campaignId, subject, + preview, bodyJson, from, emailAddresses: emails, diff --git a/apps/web/lib/api/campaigns/validate-campaign.ts b/apps/web/lib/api/campaigns/validate-campaign.ts index 532b2f3878d..b256757938c 100644 --- a/apps/web/lib/api/campaigns/validate-campaign.ts +++ b/apps/web/lib/api/campaigns/validate-campaign.ts @@ -34,6 +34,7 @@ export async function validateCampaign({ if ( input.name || input.subject || + input.preview || input.bodyJson || input.groupIds || input.triggerCondition || diff --git a/apps/web/lib/zod/schemas/campaigns.ts b/apps/web/lib/zod/schemas/campaigns.ts index ac780bf5cda..c21c705d5bf 100644 --- a/apps/web/lib/zod/schemas/campaigns.ts +++ b/apps/web/lib/zod/schemas/campaigns.ts @@ -50,6 +50,7 @@ export const CampaignSchema = z.object({ id: z.string(), name: z.string(), subject: z.string(), + preview: z.string().nullable().default(null), from: z.string().nullable(), bodyJson: z.record(z.string(), z.any()), type: z.nativeEnum(CampaignType), @@ -84,6 +85,7 @@ export const updateCampaignSchema = z .string() .trim() .max(100, "Subject must be less than 100 characters."), + preview: z.string().nullable().default(null), from: z.string().email().trim().toLowerCase(), bodyJson: z.record(z.string(), z.any()), triggerCondition: workflowConditionSchema.nullish(), From f87fa0390560c3cbbcb576b250b3daae92bfdfc2 Mon Sep 17 00:00:00 2001 From: Tim Wilson Date: Wed, 5 Nov 2025 16:31:27 -0500 Subject: [PATCH 08/13] Fixes --- .../app/(ee)/api/campaigns/[campaignId]/route.ts | 2 +- .../campaigns/[campaignId]/campaign-editor.tsx | 13 +++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/apps/web/app/(ee)/api/campaigns/[campaignId]/route.ts b/apps/web/app/(ee)/api/campaigns/[campaignId]/route.ts index 862f02a0223..0ed5676d514 100644 --- a/apps/web/app/(ee)/api/campaigns/[campaignId]/route.ts +++ b/apps/web/app/(ee)/api/campaigns/[campaignId]/route.ts @@ -122,7 +122,7 @@ export const PATCH = withWorkspace( data: { ...(name && { name }), ...(subject && { subject }), - ...(preview && { preview }), + ...(preview !== undefined && { preview }), ...(from && { from }), ...(status && { status }), ...(bodyJson && { bodyJson }), diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/[campaignId]/campaign-editor.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/[campaignId]/campaign-editor.tsx index 1832215f2ec..75db9c2424d 100644 --- a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/[campaignId]/campaign-editor.tsx +++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/campaigns/[campaignId]/campaign-editor.tsx @@ -102,6 +102,7 @@ export function CampaignEditor({ campaign }: { campaign: Campaign }) { defaultValues: { name: campaign.name, subject: campaign.subject, + preview: campaign.preview, from: campaign.from ?? undefined, bodyJson: campaign.bodyJson, groupIds: campaign.groups.map(({ id }) => id), @@ -135,8 +136,9 @@ export function CampaignEditor({ campaign }: { campaign: Campaign }) { // Focus preview input when opened useEffect(() => { - if (showPreviewText) previewInputRef.current?.focus(); - }, [showPreviewText]); + if (showPreviewText && !getValues("preview")) + previewInputRef.current?.focus(); + }, [showPreviewText, getValues]); const saveCampaign = useCallback( async ({ isDraft = false }: { isDraft?: boolean }) => { @@ -306,7 +308,7 @@ export function CampaignEditor({ campaign }: { campaign: Campaign }) { contentWrapperClassName="flex flex-col" > -
+
Name {children} From ba363b073c2ab5e5962a218db8eb12b95ddda0a0 Mon Sep 17 00:00:00 2001 From: Tim Wilson Date: Wed, 5 Nov 2025 16:35:44 -0500 Subject: [PATCH 09/13] Update campaign-email.tsx --- packages/email/src/templates/campaign-email.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/email/src/templates/campaign-email.tsx b/packages/email/src/templates/campaign-email.tsx index 9ec9eaaea00..5fb1e5e42e5 100644 --- a/packages/email/src/templates/campaign-email.tsx +++ b/packages/email/src/templates/campaign-email.tsx @@ -23,7 +23,7 @@ export default function CampaignEmail({ campaign = { type: "marketing", preview: "Test Preview", - body: `

Hi {{PartnerName}},

Thrilled to have you officially join the Acme Ambassador Program!

As a Acme Ambassador, you're joining the front line of change. You're freeing people from broken healthcare and giving them back control of their health.

Your 3 quick steps to get started:

  1. Activate your membership with your 50% off code: ACME50OFF

  2. Open your dashboard and copy your referral link

  3. Share Acme with your loved ones! Use our Ambassador Hub for all the information you need like message templates, images, and information about Acme.

And a bonus: make your first referral within 7 days and you'll also receive a limited-edition Acme hoodie.

We're here with you every step of the way.

To your health,

The Acme team

`, + body: `

Hi {{PartnerName}},

Thrilled to have you officially join the Acme Ambassador Program!

As a Acme Ambassador, you're joining the front line of change. You're freeing people from broken healthcare and giving them back control of their health.

Your 3 quick steps to get started:

  1. Activate your membership with your 50% off code: ACME50OFF

  2. Open your dashboard and copy your referral link

  3. Share Acme with your loved ones! Use our Ambassador Hub for all the information you need like message templates, images, and information about Acme.

And a bonus: make your first referral within 7 days and you'll also receive a limited-edition Acme hoodie.

We're here with you every step of the way.

To your health,

The Acme team

`, }, }: { program?: { From dafcc715a529a95d04213ad9101b76379525f17f Mon Sep 17 00:00:00 2001 From: Tim Wilson Date: Wed, 5 Nov 2025 16:36:16 -0500 Subject: [PATCH 10/13] Update campaign-email.tsx --- packages/email/src/templates/campaign-email.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/email/src/templates/campaign-email.tsx b/packages/email/src/templates/campaign-email.tsx index 5fb1e5e42e5..5aa632b745e 100644 --- a/packages/email/src/templates/campaign-email.tsx +++ b/packages/email/src/templates/campaign-email.tsx @@ -75,10 +75,7 @@ export default function CampaignEmail({
-
+
{program?.messagingEnabledAt && From d94c418b8a0e163a2e5c8e54dedc51adcb82ceed Mon Sep 17 00:00:00 2001 From: Tim Wilson Date: Wed, 5 Nov 2025 16:45:40 -0500 Subject: [PATCH 11/13] Update campaigns.ts --- apps/web/lib/zod/schemas/campaigns.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/lib/zod/schemas/campaigns.ts b/apps/web/lib/zod/schemas/campaigns.ts index c21c705d5bf..b4bc8c694f5 100644 --- a/apps/web/lib/zod/schemas/campaigns.ts +++ b/apps/web/lib/zod/schemas/campaigns.ts @@ -50,7 +50,7 @@ export const CampaignSchema = z.object({ id: z.string(), name: z.string(), subject: z.string(), - preview: z.string().nullable().default(null), + preview: z.string().nullish(), from: z.string().nullable(), bodyJson: z.record(z.string(), z.any()), type: z.nativeEnum(CampaignType), From 7e66ad140c1f76dccae5f005ec64b3eb266573ca Mon Sep 17 00:00:00 2001 From: Tim Wilson Date: Wed, 5 Nov 2025 16:46:04 -0500 Subject: [PATCH 12/13] Update campaigns.ts --- apps/web/lib/zod/schemas/campaigns.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/lib/zod/schemas/campaigns.ts b/apps/web/lib/zod/schemas/campaigns.ts index b4bc8c694f5..d2c0f86835a 100644 --- a/apps/web/lib/zod/schemas/campaigns.ts +++ b/apps/web/lib/zod/schemas/campaigns.ts @@ -50,7 +50,7 @@ export const CampaignSchema = z.object({ id: z.string(), name: z.string(), subject: z.string(), - preview: z.string().nullish(), + preview: z.string().nullable().default(null), from: z.string().nullable(), bodyJson: z.record(z.string(), z.any()), type: z.nativeEnum(CampaignType), @@ -85,7 +85,7 @@ export const updateCampaignSchema = z .string() .trim() .max(100, "Subject must be less than 100 characters."), - preview: z.string().nullable().default(null), + preview: z.string().nullish(), from: z.string().email().trim().toLowerCase(), bodyJson: z.record(z.string(), z.any()), triggerCondition: workflowConditionSchema.nullish(), From 938af45d20a714add9aea20d4e9cfdfad116ec45 Mon Sep 17 00:00:00 2001 From: Steven Tey Date: Wed, 5 Nov 2025 14:22:11 -0800 Subject: [PATCH 13/13] update schema to db.Text, fix test --- apps/web/scripts/perplexity/ban-partners.ts | 9 +++++++-- apps/web/tests/campaigns/index.test.ts | 1 + packages/prisma/schema/campaign.prisma | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/apps/web/scripts/perplexity/ban-partners.ts b/apps/web/scripts/perplexity/ban-partners.ts index e18d6e69848..0a7672a9ace 100644 --- a/apps/web/scripts/perplexity/ban-partners.ts +++ b/apps/web/scripts/perplexity/ban-partners.ts @@ -1,4 +1,3 @@ -import { syncTotalCommissions } from "@/lib/api/partners/sync-total-commissions"; import { BAN_PARTNER_REASONS } from "@/lib/zod/schemas/partners"; import PartnerBanned from "@dub/email/templates/partner-banned"; import { prisma } from "@dub/prisma"; @@ -6,6 +5,7 @@ import "dotenv-flow/config"; import * as fs from "fs"; import * as Papa from "papaparse"; import { linkCache } from "../../lib/api/links/cache"; +import { syncTotalCommissions } from "../../lib/api/partners/sync-total-commissions"; import { queueBatchEmail } from "../../lib/email/queue-batch-email"; let partnersToBan: string[] = []; @@ -38,12 +38,15 @@ async function main() { partnerId: { in: partnersToBan, }, + status: { + not: "banned", + }, }, include: { links: true, partner: true, }, - take: 100, + take: 200, }); if (programEnrollments.length === 0) { @@ -127,6 +130,8 @@ async function main() { ), ); + console.log("commissionsRes", commissionsRes); + const qstashRes = await queueBatchEmail( programEnrollments .filter((p) => p.partner.email) diff --git a/apps/web/tests/campaigns/index.test.ts b/apps/web/tests/campaigns/index.test.ts index 226f0685586..1a668d668cc 100644 --- a/apps/web/tests/campaigns/index.test.ts +++ b/apps/web/tests/campaigns/index.test.ts @@ -33,6 +33,7 @@ const expectedCampaign: Partial = { ...campaign, type: "transactional", status: expect.any(String), + preview: null, from: null, scheduledAt: null, groups: [{ id: E2E_PARTNER_GROUP.id }], diff --git a/packages/prisma/schema/campaign.prisma b/packages/prisma/schema/campaign.prisma index a67441e0e52..32e5ad7988d 100644 --- a/packages/prisma/schema/campaign.prisma +++ b/packages/prisma/schema/campaign.prisma @@ -27,7 +27,7 @@ model Campaign { status CampaignStatus @default(draft) name String subject String - preview String? + preview String? @db.Text from String? bodyJson Json @db.Json scheduledAt DateTime?