Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
78 commits
Select commit Hold shift + click to select a range
729d252
add EmailDomain table
devkiran Oct 16, 2025
5f0a250
add API routes to manage the email domains
devkiran Oct 16, 2025
2a56c0c
add email domain modal
devkiran Oct 16, 2025
29d35e5
Refactor AddEditEmailDomainModal to improve form handling
devkiran Oct 16, 2025
84cd91f
Refactor email domain retrieval logic
devkiran Oct 16, 2025
860265a
Enhance email domain API to return multiple domains
devkiran Oct 16, 2025
7e244de
wip
devkiran Oct 16, 2025
84c9b68
add validation to the domain API
devkiran Oct 17, 2025
3f0788e
Merge branch 'main' into program-email-domains
devkiran Oct 18, 2025
84a16c0
Merge branch 'main' into program-email-domains
devkiran Oct 21, 2025
8bb0d40
Merge branch 'program-email-domains' of https://github.com/dubinc/dub…
devkiran Oct 21, 2025
2f50297
add domain
devkiran Oct 21, 2025
6a01990
add domain verification api
devkiran Oct 21, 2025
35a131e
display DNS records
devkiran Oct 21, 2025
5fb4d2a
refactor the email domain status
devkiran Oct 21, 2025
8d93749
refactor email domain components to use updated domain structure and …
devkiran Oct 21, 2025
88ab5d5
Update email-domain-dns-records.tsx
devkiran Oct 21, 2025
3c56c36
Update email-domain-dns-records.tsx
devkiran Oct 21, 2025
2f4701a
Update email-domain-dns-records.tsx
devkiran Oct 21, 2025
2c76234
delete email domain
devkiran Oct 21, 2025
5995787
update existing domain
devkiran Oct 21, 2025
3890fe8
add permissions
devkiran Oct 21, 2025
18e9d44
Update email-domain-dns-records.tsx
devkiran Oct 21, 2025
1c80c67
Merge branch 'main' into program-email-domains
devkiran Oct 21, 2025
6d9f1f7
Add cron job for verifying email domains and implement verification l…
devkiran Oct 21, 2025
19e42dc
Merge branch 'program-email-domains' of https://github.com/dubinc/dub…
devkiran Oct 21, 2025
d290204
Add cron job for verifying email domains and implement verification l…
devkiran Oct 21, 2025
73e908d
wip
devkiran Oct 21, 2025
e45f21f
wip schedule the campaign
devkiran Oct 21, 2025
3aef6cf
Add campaign type icon component and update UI
devkiran Oct 22, 2025
013d7a0
display CampaignStats filter
devkiran Oct 22, 2025
5d81806
use CampaignTypeIcon in stats
devkiran Oct 22, 2025
de59ab8
Add 'from' field to campaign schema and editor; integrate email domai…
devkiran Oct 22, 2025
8e45025
schedule the campaign
devkiran Oct 22, 2025
49c1c04
add useCampaignConfirmationModals
devkiran Oct 22, 2025
14cc2ff
fix the date picker
devkiran Oct 22, 2025
fdab3fd
Add new fields to campaign editor and skeleton
devkiran Oct 22, 2025
9fae54b
Update use-campaign-confirmation-modals.tsx
devkiran Oct 22, 2025
e98a424
Update campaign-controls.tsx
devkiran Oct 22, 2025
386f7c5
Add campaign broadcast API and notification preferences
devkiran Oct 22, 2025
50bb16b
Update route.ts
devkiran Oct 22, 2025
807e44c
Update use-campaigns-filters.tsx
devkiran Oct 22, 2025
29b63af
Refactor campaign status transitions and scheduling
devkiran Oct 22, 2025
68ccfad
Restrict editing for non-editable campaign statuses
devkiran Oct 22, 2025
7aad498
Add notification email creation for campaign broadcasts and improve c…
devkiran Oct 22, 2025
ede6dae
Restrict campaign emails to verified domains
devkiran Oct 22, 2025
ff335bf
Refactor campaign scheduling logic
devkiran Oct 22, 2025
f41db89
Merge branch 'main' into marketing-campaigns
devkiran Oct 23, 2025
ed58e0d
Refactor email domain selector and modal imports
devkiran Oct 23, 2025
0f2d0fd
Refactor campaign scheduling and broadcasting logic; update campaign …
devkiran Oct 23, 2025
97342bb
Update schedule-campaigns.ts
devkiran Oct 23, 2025
37757e3
Update route.ts
devkiran Oct 23, 2025
8b55035
Update route.ts
devkiran Oct 23, 2025
c51ad77
Update execute-send-campaign-workflow.ts
devkiran Oct 23, 2025
649af9f
Update route.ts
devkiran Oct 23, 2025
048864f
Add unsubscribe section for marketing emails
devkiran Oct 23, 2025
8e4789e
Add read-only campaign editor states and preview 'from' field
devkiran Oct 23, 2025
ab3f3b0
Merge branch 'main' into program-email-domains
devkiran Oct 23, 2025
e2c8180
Merge branch 'marketing-campaigns' into program-email-domains
devkiran Oct 23, 2025
bd660b3
coderabbit feedback
devkiran Oct 23, 2025
f8ac8ed
Update email-domain-selector.tsx
devkiran Oct 23, 2025
f911e91
qstashId -> qstashMessageId
devkiran Oct 23, 2025
d318ba7
Improve email domain API error handling and validation
devkiran Oct 23, 2025
e55cc21
Update route.ts
devkiran Oct 23, 2025
9bc3fdf
Update email-bounced.ts
devkiran Oct 23, 2025
ebb9e8e
Update email-delivered.ts
devkiran Oct 23, 2025
c668743
Merge branch 'main' into program-email-domains
steven-tey Oct 23, 2025
5c9d19b
Merge branch 'program-email-domains' of https://github.com/dubinc/dub…
devkiran Oct 24, 2025
635ec77
Merge branch 'main' into program-email-domains
steven-tey Oct 24, 2025
4dd431e
Merge branch 'main' into program-email-domains
devkiran Oct 26, 2025
d39404d
Update route.ts
devkiran Oct 26, 2025
dcc5513
Update route.ts
devkiran Oct 26, 2025
656234b
Update index.test.ts
devkiran Oct 26, 2025
e00b75c
remove fromAddress from email domain
devkiran Oct 26, 2025
5a003af
fix campaign routes
devkiran Oct 26, 2025
67f33a3
add from address verification
devkiran Oct 26, 2025
5319087
Update validate-campaign.ts
devkiran Oct 26, 2025
05968a6
fix campaign from address selection
devkiran Oct 26, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export const POST = withWorkspace(
workflowId,
userId: session.user.id,
status: CampaignStatus.draft,
from: campaign.from,
name: `${campaign.name} (copy)`,
subject: campaign.subject,
bodyJson: campaign.bodyJson ?? DEFAULT_CAMPAIGN_BODY,
Expand Down
88 changes: 40 additions & 48 deletions apps/web/app/(ee)/api/campaigns/[campaignId]/route.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
import { getCampaignOrThrow } from "@/lib/api/campaigns/get-campaign-or-throw";
import {
scheduleMarketingCampaign,
scheduleTransactionalCampaign,
} from "@/lib/api/campaigns/schedule-campaigns";
import { validateCampaign } from "@/lib/api/campaigns/validate-campaign";
import { throwIfInvalidGroupIds } from "@/lib/api/groups/throw-if-invalid-group-ids";
import { getDefaultProgramIdOrThrow } from "@/lib/api/programs/get-default-program-id-or-throw";
import { parseRequestBody } from "@/lib/api/utils";
import { parseWorkflowConfig } from "@/lib/api/workflows/parse-workflow-config";
import { isScheduledWorkflow } from "@/lib/api/workflows/utils";
import { withWorkspace } from "@/lib/auth";
import { qstash } from "@/lib/cron";
import {
CampaignSchema,
updateCampaignSchema,
} from "@/lib/zod/schemas/campaigns";
import {
WORKFLOW_ATTRIBUTE_TRIGGER,
WORKFLOW_SCHEDULES,
} from "@/lib/zod/schemas/workflows";
import { WORKFLOW_ATTRIBUTE_TRIGGER } from "@/lib/zod/schemas/workflows";
import { prisma } from "@dub/prisma";
import { APP_DOMAIN_WITH_NGROK, arrayEqual } from "@dub/utils";
import { arrayEqual } from "@dub/utils";
import { PartnerGroup } from "@prisma/client";
import { waitUntil } from "@vercel/functions";
import { NextResponse } from "next/server";
Expand Down Expand Up @@ -53,16 +54,27 @@ export const PATCH = withWorkspace(
const { campaignId } = params;
const programId = getDefaultProgramIdOrThrow(workspace);

const { name, subject, status, bodyJson, groupIds, triggerCondition } =
updateCampaignSchema.parse(await parseRequestBody(req));

const campaign = await getCampaignOrThrow({
programId,
campaignId,
includeWorkflow: true,
includeGroups: true,
});

const {
name,
subject,
from,
status,
bodyJson,
groupIds,
triggerCondition,
scheduledAt,
} = await validateCampaign({
input: updateCampaignSchema.parse(await parseRequestBody(req)),
campaign,
});

// if groupIds is provided and is different from the current groupIds, update the groups
let updatedPartnerGroups: PartnerGroup[] | undefined = undefined;
let shouldUpdateGroups = false;
Expand Down Expand Up @@ -109,8 +121,10 @@ export const PATCH = withWorkspace(
data: {
...(name && { name }),
...(subject && { subject }),
...(from && { from }),
...(status && { status }),
...(bodyJson && { bodyJson }),
...(scheduledAt !== undefined && { scheduledAt }),
...(shouldUpdateGroups && {
groups: {
deleteMany: {},
Expand All @@ -132,38 +146,16 @@ export const PATCH = withWorkspace(

waitUntil(
(async () => {
if (
!updatedCampaign.workflow ||
!isScheduledWorkflow(updatedCampaign.workflow)
) {
return;
}

// Decide whether to schedule the workflow or delete the schedule
const shouldSchedule =
(campaign.status === "draft" || campaign.status === "paused") &&
updatedCampaign.status === "active";

const shouldDeleteSchedule =
campaign.status === "active" && updatedCampaign.status === "paused";

const cronSchedule =
WORKFLOW_SCHEDULES[updatedCampaign.workflow.trigger];

if (!cronSchedule) {
throw new Error(
`Cron schedule not found for trigger ${updatedCampaign.workflow.trigger}`,
);
}

if (shouldSchedule) {
await qstash.schedules.create({
destination: `${APP_DOMAIN_WITH_NGROK}/api/cron/workflows/${updatedCampaign.workflow.id}`,
cron: cronSchedule,
scheduleId: updatedCampaign.workflow.id,
if (updatedCampaign.type === "marketing") {
await scheduleMarketingCampaign({
campaign,
updatedCampaign,
});
} else if (updatedCampaign.type === "transactional") {
await scheduleTransactionalCampaign({
campaign,
updatedCampaign,
});
} else if (shouldDeleteSchedule) {
await qstash.schedules.delete(updatedCampaign.workflow.id);
}
})(),
);
Expand Down Expand Up @@ -212,17 +204,17 @@ export const DELETE = withWorkspace(

waitUntil(
(async () => {
if (!campaign.workflow) {
return;
}
if (campaign.type === "marketing" && campaign.qstashMessageId) {
await qstash.messages.delete(campaign.qstashMessageId);
} else if (campaign.type === "transactional" && campaign.workflow) {
const { condition } = parseWorkflowConfig(campaign.workflow);

const { condition } = parseWorkflowConfig(campaign.workflow);
if (condition.attribute === "partnerJoined") {
return;
}

if (condition.attribute === "partnerJoined") {
return;
await qstash.schedules.delete(campaign.workflow.id);
}

await qstash.schedules.delete(campaign.workflow.id);
})(),
);

Expand Down
9 changes: 6 additions & 3 deletions apps/web/app/(ee)/api/campaigns/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,12 @@ export const POST = withWorkspace(
return campaign;
});

return NextResponse.json({
id: campaign.id,
});
return NextResponse.json(
{
id: campaign.id,
},
{ status: 201 },
);
},
{
requiredPlan: ["advanced", "enterprise"],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export async function POST(req: Request) {
});
}

let diffMinutes = differenceInMinutes(bounty.startsAt, new Date());
const diffMinutes = differenceInMinutes(bounty.startsAt, new Date());

if (diffMinutes >= 10) {
return logAndRespond(
Expand Down
Loading
Loading