From 2a1e6dd875b2c73438bd7d8ec742ce67abe58a39 Mon Sep 17 00:00:00 2001 From: Steven Tey Date: Thu, 16 Apr 2026 18:22:43 -0700 Subject: [PATCH 1/3] remove sortableColumns in AnalyticsPartnersTable, run prisma format --- .../(ee)/program/analytics/analytics-partners-table.tsx | 1 - packages/prisma/schema/fraud.prisma | 1 + packages/prisma/schema/network.prisma | 4 ++-- packages/prisma/schema/workspace.prisma | 6 +++--- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/analytics/analytics-partners-table.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/analytics/analytics-partners-table.tsx index c749b236261..16874164ad4 100644 --- a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/analytics/analytics-partners-table.tsx +++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/analytics/analytics-partners-table.tsx @@ -172,7 +172,6 @@ export function AnalyticsPartnersTable() { ], pagination, onPaginationChange: setPagination, - sortableColumns: ["clicks", "leads", "saleAmount"], sortBy: selectedTab === "sales" ? "saleAmount" : selectedTab, thClassName: "border-l-0", tdClassName: "border-l-0", diff --git a/packages/prisma/schema/fraud.prisma b/packages/prisma/schema/fraud.prisma index 822406fec33..59181593171 100644 --- a/packages/prisma/schema/fraud.prisma +++ b/packages/prisma/schema/fraud.prisma @@ -85,6 +85,7 @@ enum FraudAlertStatus { confirmed dismissed } + model FraudAlert { id String @id @default(cuid()) partnerId String diff --git a/packages/prisma/schema/network.prisma b/packages/prisma/schema/network.prisma index a8ac554f9d1..a2c988e5360 100644 --- a/packages/prisma/schema/network.prisma +++ b/packages/prisma/schema/network.prisma @@ -52,8 +52,8 @@ model DiscoveredPartner { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - program Program @relation(fields: [programId], references: [id], onDelete: Cascade) - partner Partner @relation(fields: [partnerId], references: [id], onDelete: Cascade) + program Program @relation(fields: [programId], references: [id], onDelete: Cascade) + partner Partner @relation(fields: [partnerId], references: [id], onDelete: Cascade) programEnrollment ProgramEnrollment? @relation(fields: [programId, partnerId], references: [programId, partnerId]) @@unique([programId, partnerId]) diff --git a/packages/prisma/schema/workspace.prisma b/packages/prisma/schema/workspace.prisma index 5bf84809a31..f5ab0f57423 100644 --- a/packages/prisma/schema/workspace.prisma +++ b/packages/prisma/schema/workspace.prisma @@ -42,10 +42,10 @@ model Project { referralLinkId String? @unique referredSignups Int @default(0) - store Json? // General key-value store for things like persisting toggles, dismissing popups, etc. + store Json? // General key-value store for things like persisting toggles, dismissing popups, etc. siteVisitTrackingSettings Json? // Site visit tracking: sitemaps, short-link domain, Site Links folder id - allowedHostnames Json? - publishableKey String? @unique // for the client-side publishable key + allowedHostnames Json? + publishableKey String? @unique // for the client-side publishable key conversionEnabled Boolean @default(false) // Whether to enable conversion tracking for links by default webhookEnabled Boolean @default(false) From 9dd9fb3bff7aa47a43bb9bd289d2d93f654cecb8 Mon Sep 17 00:00:00 2001 From: Steven Tey Date: Thu, 16 Apr 2026 20:04:41 -0700 Subject: [PATCH 2/3] Add `reactivateProgram` (#3764) --- .../reactivate/route.ts | 4 +- .../webhook/checkout-session-completed.ts | 17 +++++-- .../webhook/utils/update-workspace-plan.ts | 16 ++----- .../lib/api/programs/reactivate-program.ts | 47 +++++++++++++++++++ 4 files changed, 66 insertions(+), 18 deletions(-) rename apps/web/app/(ee)/api/cron/{partners => programs}/reactivate/route.ts (95%) create mode 100644 apps/web/lib/api/programs/reactivate-program.ts diff --git a/apps/web/app/(ee)/api/cron/partners/reactivate/route.ts b/apps/web/app/(ee)/api/cron/programs/reactivate/route.ts similarity index 95% rename from apps/web/app/(ee)/api/cron/partners/reactivate/route.ts rename to apps/web/app/(ee)/api/cron/programs/reactivate/route.ts index 4c577045cc2..a6c50f9e0f4 100644 --- a/apps/web/app/(ee)/api/cron/partners/reactivate/route.ts +++ b/apps/web/app/(ee)/api/cron/programs/reactivate/route.ts @@ -11,7 +11,7 @@ const inputSchema = z.object({ programId: z.string(), }); -// POST /api/cron/partners/reactivate - reactivate partners in a program +// POST /api/cron/programs/reactivate - reactivate all partners in a program export const POST = withCron(async ({ rawBody }) => { const { programId } = inputSchema.parse(JSON.parse(rawBody)); @@ -69,7 +69,7 @@ export const POST = withCron(async ({ rawBody }) => { // Self-queue the next batch if there are more partners to process if (programEnrollments.length === CRON_BATCH_SIZE) { const response = await qstash.publishJSON({ - url: `${APP_DOMAIN_WITH_NGROK}/api/cron/partners/reactivate`, + url: `${APP_DOMAIN_WITH_NGROK}/api/cron/programs/reactivate`, body: { programId, }, diff --git a/apps/web/app/(ee)/api/stripe/webhook/checkout-session-completed.ts b/apps/web/app/(ee)/api/stripe/webhook/checkout-session-completed.ts index 54dcdd0f185..b6b565be0f9 100644 --- a/apps/web/app/(ee)/api/stripe/webhook/checkout-session-completed.ts +++ b/apps/web/app/(ee)/api/stripe/webhook/checkout-session-completed.ts @@ -1,7 +1,9 @@ import { createProgram } from "@/lib/actions/partners/create-program"; import { claimDotLinkDomain } from "@/lib/api/domains/claim-dot-link-domain"; +import { reactivateProgram } from "@/lib/api/programs/reactivate-program"; import { onboardingStepCache } from "@/lib/api/workspaces/onboarding-step-cache"; import { tokenCache } from "@/lib/auth/token-cache"; +import { wouldGainPartnerAccess } from "@/lib/plans/has-partner-access"; import { stripe } from "@/lib/stripe"; import { WorkspaceProps } from "@/lib/types"; import { redis } from "@/lib/upstash"; @@ -54,7 +56,7 @@ export async function checkoutSessionCompleted( // in the database for easy identification in future webhook events // also update the billingCycleStart to today's date - const workspace = await prisma.project.update({ + const updatedWorkspace = await prisma.project.update({ where: { id: workspaceId, }, @@ -102,7 +104,7 @@ export async function checkoutSessionCompleted( }, }); - const users = workspace.users.map(({ user }) => ({ + const users = updatedWorkspace.users.map(({ user }) => ({ id: user.id, name: user.name, email: user.email, @@ -110,6 +112,13 @@ export async function checkoutSessionCompleted( await Promise.allSettled([ completeOnboarding({ users, workspaceId }), + // if workspace had a program from before and is upgrading to an eligible plan, reactivate it + updatedWorkspace.defaultProgramId && + wouldGainPartnerAccess({ + currentPlan: "free", + newPlan: updatedWorkspace.plan, + }) && + reactivateProgram(updatedWorkspace.defaultProgramId), sendBatchEmail( users.map((user) => ({ to: user.email as string, @@ -135,7 +144,9 @@ export async function checkoutSessionCompleted( }), // expire tokens cache tokenCache.expireMany({ - hashedKeys: workspace.restrictedTokens.map(({ hashedKey }) => hashedKey), + hashedKeys: updatedWorkspace.restrictedTokens.map( + ({ hashedKey }) => hashedKey, + ), }), ]); diff --git a/apps/web/app/(ee)/api/stripe/webhook/utils/update-workspace-plan.ts b/apps/web/app/(ee)/api/stripe/webhook/utils/update-workspace-plan.ts index e3798272f6f..15b12ec9fe5 100644 --- a/apps/web/app/(ee)/api/stripe/webhook/utils/update-workspace-plan.ts +++ b/apps/web/app/(ee)/api/stripe/webhook/utils/update-workspace-plan.ts @@ -1,7 +1,7 @@ import { deleteWorkspaceFolders } from "@/lib/api/folders/delete-workspace-folders"; import { deactivateProgram } from "@/lib/api/programs/deactivate-program"; +import { reactivateProgram } from "@/lib/api/programs/reactivate-program"; import { tokenCache } from "@/lib/auth/token-cache"; -import { qstash } from "@/lib/cron"; import { syncUserPlanToPlain } from "@/lib/plain/sync-user-plan"; import { getPlanCapabilities } from "@/lib/plan-capabilities"; import { @@ -11,7 +11,7 @@ import { import { WorkspaceProps } from "@/lib/types"; import { webhookCache } from "@/lib/webhook/cache"; import { prisma } from "@dub/prisma"; -import { APP_DOMAIN_WITH_NGROK, getPlanAndTierFromPriceId } from "@dub/utils"; +import { getPlanAndTierFromPriceId } from "@dub/utils"; import { NEW_BUSINESS_PRICE_IDS } from "@dub/utils/src"; import { waitUntil } from "@vercel/functions"; @@ -187,17 +187,7 @@ export async function updateWorkspacePlan({ }) && workspace.defaultProgramId ) { - const response = await qstash.publishJSON({ - url: `${APP_DOMAIN_WITH_NGROK}/api/cron/partners/reactivate`, - body: { - programId: workspace.defaultProgramId, - }, - deduplicationId: `reactivate-program-${workspace.defaultProgramId}`, - }); - - console.log("Reactivation job enqueued.", { - response, - }); + await reactivateProgram(workspace.defaultProgramId); } if ( diff --git a/apps/web/lib/api/programs/reactivate-program.ts b/apps/web/lib/api/programs/reactivate-program.ts new file mode 100644 index 00000000000..c75bf33e301 --- /dev/null +++ b/apps/web/lib/api/programs/reactivate-program.ts @@ -0,0 +1,47 @@ +import { qstash } from "@/lib/cron"; +import { DEFAULT_PARTNER_GROUP } from "@/lib/zod/schemas/groups"; +import { prisma } from "@dub/prisma"; +import { APP_DOMAIN_WITH_NGROK } from "@dub/utils"; + +export async function reactivateProgram(programId: string) { + if (!programId) { + throw new Error("[reactivateProgram] programId is required"); + } + + await prisma.$transaction([ + prisma.program.update({ + where: { + id: programId, + }, + data: { + deactivatedAt: null, + }, + }), + + // republish the default group + prisma.partnerGroup.update({ + where: { + programId_slug: { + programId, + slug: DEFAULT_PARTNER_GROUP.slug, + }, + }, + data: { + applicationFormPublishedAt: new Date(), + landerPublishedAt: new Date(), + }, + }), + ]); + + const response = await qstash.publishJSON({ + url: `${APP_DOMAIN_WITH_NGROK}/api/cron/programs/reactivate`, + body: { + programId, + }, + deduplicationId: `reactivate-program-${programId}`, + }); + + console.log("[reactivateProgram] Reactivation job enqueued.", { response }); + + return response; +} From 0caad237ecaae38458ff2c42a9af1c8a89282712 Mon Sep 17 00:00:00 2001 From: Steven Tey Date: Thu, 16 Apr 2026 20:09:48 -0700 Subject: [PATCH 3/3] add more GENERIC_EMAIL_DOMAINS --- apps/web/lib/is-generic-email.ts | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/apps/web/lib/is-generic-email.ts b/apps/web/lib/is-generic-email.ts index 0e7460eb275..5ef03fe3ed8 100644 --- a/apps/web/lib/is-generic-email.ts +++ b/apps/web/lib/is-generic-email.ts @@ -10,15 +10,16 @@ const GENERIC_EMAIL_DOMAINS = [ "verizon.net", "att.net", "me.com", - "mac.com", "msn.com", - "live.com", - "live.dk", "web.de", + "atomicmail.io", "protonmail.com", "proton.me", + "pm.me", "passinbox.com", + "passmail.net", "163.com", + "126.com", "duck.com", "qq.com", "zoho.com", @@ -27,6 +28,10 @@ const GENERIC_EMAIL_DOMAINS = [ "tuta.com", "privaterelay.appleid.com", "qyver.online", + "vk.com", + "tutamail.com", + "simplelogin.com", + "volny.cz", "naver.com", "yeah.net", "example.com", @@ -39,6 +44,10 @@ const GENERIC_EMAIL_DOMAINS = [ "email.de", "t-online.de", "sina.com", + "foxmail.com", + "ukr.net", + "otona.uk", + "instaddr.ch", ]; const GENERIC_EMAIL_DOMAIN_PREFIXES = [ @@ -47,6 +56,7 @@ const GENERIC_EMAIL_DOMAIN_PREFIXES = [ "outlook.", "gmx.", "yandex.", + "live.", ]; export const isGenericEmail = (email: string) => {