From 63f62d73dbcac07d5ca12cfff83a389c48a3b308 Mon Sep 17 00:00:00 2001 From: Steven Tey Date: Mon, 4 May 2026 17:38:45 -0700 Subject: [PATCH 1/3] small touchups --- .../(ee)/program/partners/partners-table.tsx | 4 ++-- .../(ee)/settings/billing/plan-usage.tsx | 14 ++++++------- .../[slug]/links/tags/page-client.tsx | 4 ++-- ...e-tags-count.ts => use-link-tags-count.ts} | 2 +- apps/web/ui/links/link-builder/tag-select.tsx | 4 ++-- apps/web/ui/links/use-link-filters.tsx | 4 ++-- .../pricing/pricing-plan-compare-features.tsx | 21 +++++++++++++++++++ 7 files changed, 37 insertions(+), 16 deletions(-) rename apps/web/lib/swr/{use-tags-count.ts => use-link-tags-count.ts} (94%) diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/partners-table.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/partners-table.tsx index aad941f3deb..da4ed5d7a14 100644 --- a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/partners-table.tsx +++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/partners-table.tsx @@ -79,8 +79,8 @@ const partnersColumns = { all: [ "partner", "group", - "createdAt", "tags", + "createdAt", "status", "location", "totalClicks", @@ -166,7 +166,7 @@ export function PartnersTable() { const { groups } = useGroups(); const { columnVisibility, setColumnVisibility } = useColumnVisibility( - "partners-table-columns-v2", + "partners-table-columns-v3", partnersColumns, ); diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/billing/plan-usage.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/billing/plan-usage.tsx index d1eb05054fd..2286873aaf2 100644 --- a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/billing/plan-usage.tsx +++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/billing/plan-usage.tsx @@ -3,7 +3,7 @@ import { clientAccessCheck } from "@/lib/client-access-check"; import { MEGA_WORKSPACE_LINKS_LIMIT } from "@/lib/constants/misc"; import useGroupsCount from "@/lib/swr/use-groups-count"; -import useTagsCount from "@/lib/swr/use-tags-count"; +import { useLinkTagsCount } from "@/lib/swr/use-link-tags-count"; import { useUsageTimeseries } from "@/lib/swr/use-usage-timeseries"; import useWorkspace from "@/lib/swr/use-workspace"; import useWorkspaceUsers from "@/lib/swr/use-workspace-users"; @@ -88,7 +88,7 @@ export default function PlanUsage() { const { StartPaidPlanModal, setShowStartPaidPlanModal } = useStartPaidPlanModal(); - const { data: tags } = useTagsCount(); + const { data: tags } = useLinkTagsCount(); const { users } = useWorkspaceUsers(); const { groupsCount } = useGroupsCount(); @@ -367,21 +367,21 @@ export default function PlanUsage() { )} > } = {}) { const { id: workspaceId } = useWorkspace(); diff --git a/apps/web/ui/links/link-builder/tag-select.tsx b/apps/web/ui/links/link-builder/tag-select.tsx index cd7c9bde1de..61b4f37264a 100644 --- a/apps/web/ui/links/link-builder/tag-select.tsx +++ b/apps/web/ui/links/link-builder/tag-select.tsx @@ -1,5 +1,5 @@ +import { useLinkTagsCount } from "@/lib/swr/use-link-tags-count"; import useTags from "@/lib/swr/use-tags"; -import useTagsCount from "@/lib/swr/use-tags-count"; import useWorkspace from "@/lib/swr/use-workspace"; import { TagProps } from "@/lib/types"; import { TAGS_MAX_PAGE_SIZE } from "@/lib/zod/schemas/tags"; @@ -43,7 +43,7 @@ export const TagSelect = memo(() => { const [search, setSearch] = useState(""); const [debouncedSearch] = useDebounce(search, 500); - const { data: tagsCount } = useTagsCount(); + const { data: tagsCount } = useLinkTagsCount(); const useAsync = tagsCount && tagsCount > TAGS_MAX_PAGE_SIZE; const { tags: availableTags, loading: loadingTags } = useTags({ diff --git a/apps/web/ui/links/use-link-filters.tsx b/apps/web/ui/links/use-link-filters.tsx index 69bf43955d2..8ff2cddf0dc 100644 --- a/apps/web/ui/links/use-link-filters.tsx +++ b/apps/web/ui/links/use-link-filters.tsx @@ -1,7 +1,7 @@ import useCurrentFolderId from "@/lib/swr/use-current-folder-id"; +import { useLinkTagsCount } from "@/lib/swr/use-link-tags-count"; import useLinksCount from "@/lib/swr/use-links-count"; import useTags from "@/lib/swr/use-tags"; -import useTagsCount from "@/lib/swr/use-tags-count"; import useWorkspaceUsers from "@/lib/swr/use-workspace-users"; import { TagProps } from "@/lib/types"; import { TAGS_MAX_PAGE_SIZE } from "@/lib/zod/schemas/tags"; @@ -197,7 +197,7 @@ function useTagFilterOptions({ [searchParamsObj.tagIds], ); - const { data: tagsCount } = useTagsCount(); + const { data: tagsCount } = useLinkTagsCount(); const tagsAsync = Boolean(tagsCount && tagsCount > TAGS_MAX_PAGE_SIZE); const { tags, loading: loadingTags } = useTags({ query: { search: tagsAsync ? search : "" }, diff --git a/packages/utils/src/constants/pricing/pricing-plan-compare-features.tsx b/packages/utils/src/constants/pricing/pricing-plan-compare-features.tsx index 8bbb5e62d3d..31c896f86df 100644 --- a/packages/utils/src/constants/pricing/pricing-plan-compare-features.tsx +++ b/packages/utils/src/constants/pricing/pricing-plan-compare-features.tsx @@ -296,6 +296,27 @@ export const PRICING_PLAN_COMPARE_FEATURES: { ), href: "https://dub.co/help/article/partner-groups", }, + { + check: { + default: false, + business: true, + advanced: true, + enterprise: true, + }, + text: ({ plan }) => ( + <> + + {plan.limits.partnerTags === 0 + ? "No" + : plan.limits.partnerTags === INFINITY_NUMBER + ? "Unlimited" + : nFormatter(plan.limits.partnerTags)} + {" "} + partner tags + + ), + href: "https://dub.co/help/article/partner-groups", + }, { check: { default: false, From e48101056421dd4d0df3aa570a746874eb4ce631 Mon Sep 17 00:00:00 2001 From: Steven Tey Date: Mon, 4 May 2026 22:29:18 -0700 Subject: [PATCH 2/3] fix sendOtpAction logic --- apps/web/.env.example | 3 - apps/web/lib/actions/send-otp.ts | 90 +++---- apps/web/lib/analytics/get-analytics.ts | 4 +- apps/web/lib/api/links/utils/key-checks.ts | 2 +- apps/web/lib/edge-config/get-feature-flags.ts | 2 +- .../lib/edge-config/is-blacklisted-domain.ts | 2 +- .../lib/edge-config/is-blacklisted-email.ts | 2 +- .../web/lib/edge-config/is-blacklisted-key.ts | 2 +- .../edge-config/is-blacklisted-referrer.ts | 2 +- .../lib/edge-config/is-reserved-username.ts | 2 +- packages/utils/src/constants/dub-domains.ts | 246 +++++++++--------- 11 files changed, 171 insertions(+), 186 deletions(-) diff --git a/apps/web/.env.example b/apps/web/.env.example index 81f74945101..ac8c3dc3b3c 100644 --- a/apps/web/.env.example +++ b/apps/web/.env.example @@ -140,9 +140,6 @@ PLAIN_WEBHOOK_SECRET= ###### DUB.CO INTERNAL USE ONLY ###### ###################################### -# For the official Dub app -NEXT_PUBLIC_IS_DUB= - # For storing vector embeddings (/api/support/chat) UPSTASH_VECTOR_REST_URL= UPSTASH_VECTOR_REST_TOKEN= diff --git a/apps/web/lib/actions/send-otp.ts b/apps/web/lib/actions/send-otp.ts index a70d2d55123..ed977276d60 100644 --- a/apps/web/lib/actions/send-otp.ts +++ b/apps/web/lib/actions/send-otp.ts @@ -38,56 +38,52 @@ export const sendOtpAction = actionClient throw new Error("Too many requests. Please try again later."); } - if (email.includes("+") && isGenericEmail(email)) { - throw new Error( - "Email addresses with + are not allowed. Please use your work email instead.", - ); - } + const isGenericEmailWithPlus = email.includes("+") && isGenericEmail(email); - const domain = email.split("@")[1]; + const emailDomain = email.split("@")[1]; - if (process.env.NEXT_PUBLIC_IS_DUB) { - const [isDisposable, emailDomainTerms] = await Promise.all([ - redis.sismember("disposableEmailDomains", domain), - process.env.EDGE_CONFIG ? get("emailDomainTerms") : [], - ]); + const [isDisposable, emailDomainTerms] = await Promise.all([ + redis.sismember("disposableEmailDomains", emailDomain), + process.env.EDGE_CONFIG ? get("emailDomainTerms") : [], + ]); - // Only build the regex if we have at least one term; otherwise set to null - const blacklistedEmailDomainTermsRegex = - emailDomainTerms && Array.isArray(emailDomainTerms) - ? new RegExp( - emailDomainTerms - .map((term: string) => - term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), - ) // replace special characters with escape sequences - .join("|"), - ) - : null; - - if ( - isDisposable || - (blacklistedEmailDomainTermsRegex && - blacklistedEmailDomainTermsRegex.test(domain)) - ) { - // edge case: the user already has a partner account on Dub with this email address, - // or they have an existing application for a program, we can allow them to continue - const [isPartnerAccount, hasExistingApplications] = await Promise.all([ - prisma.partner.findUnique({ - where: { - email, - }, - }), - prisma.programApplication.findFirst({ - where: { - email, - }, - }), - ]); - if (!isPartnerAccount && !hasExistingApplications) { - throw new Error( - "Invalid email address – please use your work email instead. If you think this is a mistake, please contact us at dub.co/support", - ); - } + // Only build the regex if we have at least one term; otherwise set to null + const blacklistedEmailDomainTermsRegex = + emailDomainTerms && Array.isArray(emailDomainTerms) + ? new RegExp( + emailDomainTerms + .map((term: string) => + term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), + ) // replace special characters with escape sequences + .join("|"), + ) + : null; + + // if any of the flags match, run one final edge case check, before throwing an error + if ( + isGenericEmailWithPlus || + isDisposable || + (blacklistedEmailDomainTermsRegex && + blacklistedEmailDomainTermsRegex.test(emailDomain)) + ) { + // edge case: the user already has a partner account on Dub with this email address, + // or they have an existing application for a program, we can allow them to continue + const [isPartnerAccount, hasExistingApplications] = await Promise.all([ + prisma.partner.findUnique({ + where: { + email, + }, + }), + prisma.programApplication.findFirst({ + where: { + email, + }, + }), + ]); + if (!isPartnerAccount && !hasExistingApplications) { + throw new Error( + "Invalid email address – please use your work email instead. If you think this is a mistake, please contact us at dub.co/support", + ); } } diff --git a/apps/web/lib/analytics/get-analytics.ts b/apps/web/lib/analytics/get-analytics.ts index f94b1f8cebf..af7655fa431 100644 --- a/apps/web/lib/analytics/get-analytics.ts +++ b/apps/web/lib/analytics/get-analytics.ts @@ -70,8 +70,8 @@ export const getAnalytics = async (params: AnalyticsFilters) => { : `SUM(${event}) as ${event}`; const response = await conn.execute( - `SELECT ${aggregateColumns} FROM Link WHERE id IN (${linkIdPlaceholders})`, - normalizedLinkId.values, + `SELECT ${aggregateColumns} FROM Link WHERE id IN (${linkIdPlaceholders}) AND projectId = ?`, + [...normalizedLinkId.values, workspaceId], ); return analyticsResponse["count"].parse(response.rows[0]); diff --git a/apps/web/lib/api/links/utils/key-checks.ts b/apps/web/lib/api/links/utils/key-checks.ts index 3be0fa6850c..cc34e7fdf13 100644 --- a/apps/web/lib/api/links/utils/key-checks.ts +++ b/apps/web/lib/api/links/utils/key-checks.ts @@ -41,7 +41,7 @@ export async function keyChecks({ }; } - if (isDubDomain(domain) && process.env.NEXT_PUBLIC_IS_DUB) { + if (isDubDomain(domain)) { if (domain === "dub.sh" || domain === "dub.link") { if (DEFAULT_REDIRECTS[key] || RESERVED_SLUGS.includes(key)) { return { diff --git a/apps/web/lib/edge-config/get-feature-flags.ts b/apps/web/lib/edge-config/get-feature-flags.ts index ce05812779e..02fb5820438 100644 --- a/apps/web/lib/edge-config/get-feature-flags.ts +++ b/apps/web/lib/edge-config/get-feature-flags.ts @@ -19,7 +19,7 @@ export const getFeatureFlags = async ({ analyticsSettingsSiteVisitTracking: false, }; - if (!process.env.NEXT_PUBLIC_IS_DUB || !process.env.EDGE_CONFIG) { + if (!process.env.EDGE_CONFIG) { // return all features as true if edge config is not available return Object.fromEntries( Object.entries(workspaceFeatures).map(([key, _v]) => [key, true]), diff --git a/apps/web/lib/edge-config/is-blacklisted-domain.ts b/apps/web/lib/edge-config/is-blacklisted-domain.ts index 1c71a12fed6..fed69705240 100644 --- a/apps/web/lib/edge-config/is-blacklisted-domain.ts +++ b/apps/web/lib/edge-config/is-blacklisted-domain.ts @@ -1,7 +1,7 @@ import { getAll } from "@vercel/edge-config"; export const isBlacklistedDomain = async (domain: string): Promise => { - if (!process.env.NEXT_PUBLIC_IS_DUB || !process.env.EDGE_CONFIG) { + if (!process.env.EDGE_CONFIG) { return false; } diff --git a/apps/web/lib/edge-config/is-blacklisted-email.ts b/apps/web/lib/edge-config/is-blacklisted-email.ts index 52aa985331b..434e5ee27fe 100644 --- a/apps/web/lib/edge-config/is-blacklisted-email.ts +++ b/apps/web/lib/edge-config/is-blacklisted-email.ts @@ -1,7 +1,7 @@ import { get } from "@vercel/edge-config"; export const isBlacklistedEmail = async (email: string | string[]) => { - if (!process.env.NEXT_PUBLIC_IS_DUB || !process.env.EDGE_CONFIG) { + if (!process.env.EDGE_CONFIG) { return false; } diff --git a/apps/web/lib/edge-config/is-blacklisted-key.ts b/apps/web/lib/edge-config/is-blacklisted-key.ts index 86fae891ad8..976e2d00879 100644 --- a/apps/web/lib/edge-config/is-blacklisted-key.ts +++ b/apps/web/lib/edge-config/is-blacklisted-key.ts @@ -1,7 +1,7 @@ import { get } from "@vercel/edge-config"; export const isBlacklistedKey = async (key: string) => { - if (!process.env.NEXT_PUBLIC_IS_DUB || !process.env.EDGE_CONFIG) { + if (!process.env.EDGE_CONFIG) { return false; } diff --git a/apps/web/lib/edge-config/is-blacklisted-referrer.ts b/apps/web/lib/edge-config/is-blacklisted-referrer.ts index b1b530ae176..5741e5ea80e 100644 --- a/apps/web/lib/edge-config/is-blacklisted-referrer.ts +++ b/apps/web/lib/edge-config/is-blacklisted-referrer.ts @@ -2,7 +2,7 @@ import { getDomainWithoutWWW } from "@dub/utils"; import { get } from "@vercel/edge-config"; export const isBlacklistedReferrer = async (referrer: string | null) => { - if (!process.env.NEXT_PUBLIC_IS_DUB || !process.env.EDGE_CONFIG) { + if (!process.env.EDGE_CONFIG) { return false; } diff --git a/apps/web/lib/edge-config/is-reserved-username.ts b/apps/web/lib/edge-config/is-reserved-username.ts index 48a4e23a666..9b9fc45876f 100644 --- a/apps/web/lib/edge-config/is-reserved-username.ts +++ b/apps/web/lib/edge-config/is-reserved-username.ts @@ -5,7 +5,7 @@ import { get } from "@vercel/edge-config"; * Check if a username is reserved – should only be available on Pro+ */ export const isReservedUsername = async (key: string) => { - if (!process.env.NEXT_PUBLIC_IS_DUB || !process.env.EDGE_CONFIG) { + if (!process.env.EDGE_CONFIG) { return false; } diff --git a/packages/utils/src/constants/dub-domains.ts b/packages/utils/src/constants/dub-domains.ts index 10a33eebd73..f53afec7333 100644 --- a/packages/utils/src/constants/dub-domains.ts +++ b/packages/utils/src/constants/dub-domains.ts @@ -12,133 +12,125 @@ export const DUB_DOMAINS = [ description: "The default domain for all new accounts.", projectId: DUB_WORKSPACE_ID, }, - ...(process.env.NEXT_PUBLIC_IS_DUB - ? [ - { - id: "clce1z7cs00y8rbstk4xtnj0k", - slug: "chatg.pt", - verified: true, - primary: false, - archived: false, - placeholder: "https://chat.openai.com/g/g-UGjKKONEe-domainsgpt", - allowedHostnames: ["openai.com", "chatgpt.com"], - description: - "Branded domain for ChatGPT links (convos, custom GPTs).", - projectId: DUB_WORKSPACE_ID, - }, - { - id: "cloxw8y2u0003js08a7mqg1j8", - slug: "spti.fi", - verified: true, - primary: false, - archived: false, - placeholder: "https://open.spotify.com/album/1SCyi9a5pOasikidToUY5y", - allowedHostnames: ["spotify.com"], - description: - "Branded domain for Spotify links (songs, playlists, etc.).", - projectId: DUB_WORKSPACE_ID, - }, - { - id: "cltgtd6s5000341itdszz20u2", - slug: "git.new", - verified: true, - primary: false, - archived: false, - placeholder: "https://github.com/dubinc/dub", - allowedHostnames: ["github.com"], - description: - "Branded domain for GitHub links (repositories, gists, etc.).", - projectId: DUB_WORKSPACE_ID, - }, - { - id: "cm23qevp4000412mqwvtkthzw", - slug: "cal.link", - verified: true, - primary: false, - archived: false, - placeholder: "https://cal.com/steven", - allowedHostnames: [ - "app.acuityscheduling.com", - "app.apollo.io", - "cal.com", - "calendly.com", - "calendar.app.google", - "calendar.google.com", - "calendar.notion.so", - "chilipiper.com", - "fantastical.app", - "fillout.com", - "hubspot.com", - "mentordeck.com", - "savvycal.com", - "scheduler.default.com", - "tidycal.com", - "you.ashbyhq.com", - "zcal.co", - ], - description: - "Branded domain for your scheduling links (Cal.com, Calendly, etc.).", - projectId: DUB_WORKSPACE_ID, - }, - { - id: "cloxw8qtk000bjt08n9b812vs", - slug: "amzn.id", - verified: true, - primary: false, - archived: false, - placeholder: "https://www.amazon.com/dp/B0BW4SWNC8", - allowedHostnames: [ - "amazon.com", - "amazon.co.uk", - "amazon.ca", - "amazon.es", - "amazon.fr", - ], - description: - "Branded domain for Amazon links (products, wishlists, etc.).", - projectId: DUB_WORKSPACE_ID, - }, - { - id: "clymd6zkc0001elilr1215tj9", - slug: "ggl.link", - verified: true, - primary: false, - archived: false, - placeholder: - "https://docs.google.com/document/d/15-GOZA12SXGEh8lNwU5QI1jBi04TCPgY2-LChTbXVpA", - allowedHostnames: [ - "google.com", - "google.co.uk", - "google.co.id", - "google.ca", - "google.es", - "google.fr", - "googleblog.com", - "blog.google", - "g.co", - "g.page", - "youtube.com", - "youtu.be", - ], - description: - "Branded domain for Google links (Search, Docs, Sheets, Slides, Drive, Maps, etc.).", - projectId: DUB_WORKSPACE_ID, - }, - { - id: "clymczttm0001jgkore3ltr37", - slug: "fig.page", - verified: true, - primary: false, - archived: false, - placeholder: - "https://www.figma.com/design/YAfTk6SGV2HcSvL2415oED/Dub.co-Brand-Assets-(Public)?node-id=1-36593&t=QMKQNtUzxSSzG3hX-1", - allowedHostnames: ["figma.com"], - description: - "Branded domain for Figma links (portfolios, prototypes, presentations, etc.).", - projectId: DUB_WORKSPACE_ID, - }, - ] - : []), + { + id: "clce1z7cs00y8rbstk4xtnj0k", + slug: "chatg.pt", + verified: true, + primary: false, + archived: false, + placeholder: "https://chat.openai.com/g/g-UGjKKONEe-domainsgpt", + allowedHostnames: ["openai.com", "chatgpt.com"], + description: "Branded domain for ChatGPT links (convos, custom GPTs).", + projectId: DUB_WORKSPACE_ID, + }, + { + id: "cloxw8y2u0003js08a7mqg1j8", + slug: "spti.fi", + verified: true, + primary: false, + archived: false, + placeholder: "https://open.spotify.com/album/1SCyi9a5pOasikidToUY5y", + allowedHostnames: ["spotify.com"], + description: "Branded domain for Spotify links (songs, playlists, etc.).", + projectId: DUB_WORKSPACE_ID, + }, + { + id: "cltgtd6s5000341itdszz20u2", + slug: "git.new", + verified: true, + primary: false, + archived: false, + placeholder: "https://github.com/dubinc/dub", + allowedHostnames: ["github.com"], + description: "Branded domain for GitHub links (repositories, gists, etc.).", + projectId: DUB_WORKSPACE_ID, + }, + { + id: "cm23qevp4000412mqwvtkthzw", + slug: "cal.link", + verified: true, + primary: false, + archived: false, + placeholder: "https://cal.com/steven", + allowedHostnames: [ + "app.acuityscheduling.com", + "app.apollo.io", + "cal.com", + "calendly.com", + "calendar.app.google", + "calendar.google.com", + "calendar.notion.so", + "chilipiper.com", + "fantastical.app", + "fillout.com", + "hubspot.com", + "mentordeck.com", + "savvycal.com", + "scheduler.default.com", + "tidycal.com", + "you.ashbyhq.com", + "zcal.co", + ], + description: + "Branded domain for your scheduling links (Cal.com, Calendly, etc.).", + projectId: DUB_WORKSPACE_ID, + }, + { + id: "cloxw8qtk000bjt08n9b812vs", + slug: "amzn.id", + verified: true, + primary: false, + archived: false, + placeholder: "https://www.amazon.com/dp/B0BW4SWNC8", + allowedHostnames: [ + "amazon.com", + "amazon.co.uk", + "amazon.ca", + "amazon.es", + "amazon.fr", + ], + description: "Branded domain for Amazon links (products, wishlists, etc.).", + projectId: DUB_WORKSPACE_ID, + }, + { + id: "clymd6zkc0001elilr1215tj9", + slug: "ggl.link", + verified: true, + primary: false, + archived: false, + placeholder: + "https://docs.google.com/document/d/15-GOZA12SXGEh8lNwU5QI1jBi04TCPgY2-LChTbXVpA", + allowedHostnames: [ + "google.com", + "google.co.uk", + "google.co.id", + "google.ca", + "google.es", + "google.fr", + "googleblog.com", + "blog.google", + "g.co", + "g.page", + "youtube.com", + "youtu.be", + ], + description: + "Branded domain for Google links (Search, Docs, Sheets, Slides, Drive, Maps, etc.).", + projectId: DUB_WORKSPACE_ID, + }, + { + id: "clymczttm0001jgkore3ltr37", + slug: "fig.page", + verified: true, + primary: false, + archived: false, + placeholder: + "https://www.figma.com/design/YAfTk6SGV2HcSvL2415oED/Dub.co-Brand-Assets-(Public)?node-id=1-36593&t=QMKQNtUzxSSzG3hX-1", + allowedHostnames: ["figma.com"], + description: + "Branded domain for Figma links (portfolios, prototypes, presentations, etc.).", + projectId: DUB_WORKSPACE_ID, + }, ]; export const DUB_DOMAINS_ARRAY = DUB_DOMAINS.map((domain) => domain.slug); From a6ea1142eb91059b92fddccb32ed3ffd8f89981c Mon Sep 17 00:00:00 2001 From: Pedro Ladeira <57876830+pepeladeira@users.noreply.github.com> Date: Tue, 5 May 2026 03:08:20 -0300 Subject: [PATCH 3/3] Approve target enrollment when merging an approved source program (#3852) --- .../api/cron/partners/merge-accounts/route.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/apps/web/app/(ee)/api/cron/partners/merge-accounts/route.ts b/apps/web/app/(ee)/api/cron/partners/merge-accounts/route.ts index 9f9aea291a7..f268ad0f645 100644 --- a/apps/web/app/(ee)/api/cron/partners/merge-accounts/route.ts +++ b/apps/web/app/(ee)/api/cron/partners/merge-accounts/route.ts @@ -67,6 +67,7 @@ export async function POST(req: Request) { select: { programId: true, tenantId: true, + status: true, }, }, users: { @@ -262,8 +263,24 @@ export async function POST(req: Request) { const targetEnrollment = targetPartnerEnrollments.find( ({ programId }) => programId === sourceEnrollment.programId, ); + await prisma.$transaction(async (tx) => { - // delete old source enrollment + if ( + targetEnrollment && + sourceEnrollment.status === "approved" && + ["pending", "invited"].includes(targetEnrollment.status) + ) { + await tx.programEnrollment.update({ + where: { + partnerId_programId: { + partnerId: targetPartnerId, + programId: sourceEnrollment.programId, + }, + }, + data: { status: "approved" }, + }); + } + await tx.programEnrollment.delete({ where: { partnerId_programId: {