From b4a9d6a254e9e8ea3ee662511384c8d88420342b Mon Sep 17 00:00:00 2001 From: Steven Tey Date: Tue, 7 Apr 2026 17:33:45 -0700 Subject: [PATCH 1/7] fix permalink for payouts table in partner pages --- .../partners/[partnerId]/customers/page.tsx | 14 ++++++++++++-- .../partners/[partnerId]/payouts/page.tsx | 18 ++++++++++++------ 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/[partnerId]/customers/page.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/[partnerId]/customers/page.tsx index 5caa0ee5b14..42dfbf98be0 100644 --- a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/[partnerId]/customers/page.tsx +++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/[partnerId]/customers/page.tsx @@ -14,7 +14,7 @@ import { } from "@dub/ui"; import { COUNTRIES, cn, currencyFormatter, formatDate } from "@dub/utils"; import Link from "next/link"; -import { useParams } from "next/navigation"; +import { useParams, useRouter } from "next/navigation"; export default function ProgramPartnerCustomersPage() { const { partnerId } = useParams() as { partnerId: string }; @@ -36,6 +36,7 @@ export default function ProgramPartnerCustomersPage() { } function PartnerCustomers({ partner }: { partner: EnrolledPartnerProps }) { + const router = useRouter(); const { slug } = useWorkspace(); const { @@ -111,8 +112,17 @@ function PartnerCustomers({ partner }: { partner: EnrolledPartnerProps }) { ), }, ], - onRowClick: (row) => + onRowClick: (row, e) => { + const url = `/${slug}/program/customers/${row.original.id}`; + if (e.metaKey || e.ctrlKey) window.open(url, "_blank"); + else router.push(url); + }, + onRowAuxClick: (row) => window.open(`/${slug}/program/customers/${row.original.id}`, "_blank"), + rowProps: (row) => ({ + onPointerEnter: () => + router.prefetch(`/${slug}/program/customers/${row.original.id}`), + }), resourceName: (p) => `customer${p ? "s" : ""}`, sortBy: "createdAt", sortOrder: "desc", diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/[partnerId]/payouts/page.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/[partnerId]/payouts/page.tsx index cad14c3c545..1c9b75d8f4c 100644 --- a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/[partnerId]/payouts/page.tsx +++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners/[partnerId]/payouts/page.tsx @@ -16,7 +16,7 @@ import { import { cn, currencyFormatter, formatPeriod } from "@dub/utils"; import { PayoutPaidCell } from "app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/payout-paid-cell"; import Link from "next/link"; -import { useParams } from "next/navigation"; +import { useParams, useRouter } from "next/navigation"; export default function ProgramPartnerPayoutsPage() { const { partnerId } = useParams() as { partnerId: string }; @@ -38,6 +38,7 @@ export default function ProgramPartnerPayoutsPage() { } function PartnerPayouts({ partner }: { partner: EnrolledPartnerProps }) { + const router = useRouter(); const { slug } = useWorkspace(); const { @@ -101,12 +102,17 @@ function PartnerPayouts({ partner }: { partner: EnrolledPartnerProps }) { }, }, ], - onRowClick: (row) => { - window.open( - `/${slug}/program/payouts?partnerId=${partner.id}&payoutId=${row.original.id}&sortBy=initiatedAt`, - "_blank", - ); + onRowClick: (row, e) => { + const url = `/${slug}/program/payouts/${row.original.id}`; + if (e.metaKey || e.ctrlKey) window.open(url, "_blank"); + else router.push(url); }, + onRowAuxClick: (row) => + window.open(`/${slug}/program/payouts/${row.original.id}`, "_blank"), + rowProps: (row) => ({ + onPointerEnter: () => + router.prefetch(`/${slug}/program/payouts/${row.original.id}`), + }), resourceName: (p) => `payout${p ? "s" : ""}`, thClassName: () => "border-l-0", tdClassName: () => "border-l-0", From 8c700516433cdb085de4948d365c4bfa56c4740c Mon Sep 17 00:00:00 2001 From: Marcus Farrell Date: Tue, 7 Apr 2026 17:58:14 -0700 Subject: [PATCH 2/7] Single payout alignment fix (#3709) --- .../[slug]/(ee)/program/payouts/[payoutId]/page-client.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/[payoutId]/page-client.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/[payoutId]/page-client.tsx index 2acabd3145f..a0ac81e61f9 100644 --- a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/[payoutId]/page-client.tsx +++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/[payoutId]/page-client.tsx @@ -168,12 +168,13 @@ function PayoutDetailsContent({ Partner: ( - {payout.partner.name} + {payout.partner.name} ), From ffc6ce878942c79ccffd8336cd1563dbda37c713 Mon Sep 17 00:00:00 2001 From: Marcus Farrell Date: Tue, 7 Apr 2026 17:59:11 -0700 Subject: [PATCH 3/7] Content update to match the help doc changes (#3715) --- apps/web/lib/api/fraud/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/lib/api/fraud/constants.ts b/apps/web/lib/api/fraud/constants.ts index bd8239ce178..1a54b779da9 100644 --- a/apps/web/lib/api/fraud/constants.ts +++ b/apps/web/lib/api/fraud/constants.ts @@ -51,7 +51,7 @@ export const FRAUD_RULES: FraudRuleInfo[] = [ type: "partnerDuplicatePayoutMethod", name: "Duplicate payout method", description: - "This partner is using a payout method that is already linked to another partner account, which may indicate account duplication or fraudulent behavior.", + "This payout method is already linked to another partner. May indicate duplicate accounts or fraud, and is flagged to prevent abuse of restrictions, caps, or bonuses.", scope: "partner", severity: "high", configurable: true, From c44edbdfaeb62d35507cfb683c707cccb831533c Mon Sep 17 00:00:00 2001 From: Marcus Farrell Date: Tue, 7 Apr 2026 18:04:36 -0700 Subject: [PATCH 4/7] Analytics responsive consistency (#3717) --- .../program/analytics/analytics-chart.tsx | 27 +++++++- .../(ee)/program/analytics/page-client.tsx | 62 +++++++++++-------- apps/web/ui/analytics/toggle.tsx | 6 +- 3 files changed, 65 insertions(+), 30 deletions(-) diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/analytics/analytics-chart.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/analytics/analytics-chart.tsx index f03acc05030..13c317b84fa 100644 --- a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/analytics/analytics-chart.tsx +++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/analytics/analytics-chart.tsx @@ -3,7 +3,7 @@ import { AnalyticsFunnelChart } from "@/ui/analytics/analytics-funnel-chart"; import { AnalyticsContext } from "@/ui/analytics/analytics-provider"; import { AnalyticsTabs } from "@/ui/analytics/analytics-tabs"; import { ChartViewSwitcher } from "@/ui/analytics/chart-view-switcher"; -import { useRouterStuff } from "@dub/ui"; +import { ToggleGroup, useRouterStuff } from "@dub/ui"; import { LoadingSpinner } from "@dub/ui/icons"; import { fetcher } from "@dub/utils"; import { useContext, useMemo } from "react"; @@ -89,7 +89,30 @@ export function AnalyticsChart() { ) : ( )} - +
+ $
, + value: "saleAmount", + }, + { + label:
123
, + value: "sales", + }, + ]} + selected={saleUnit} + selectAction={(option) => + queryParams({ + set: { saleUnit: option }, + }) + } + /> + + )} diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/analytics/page-client.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/analytics/page-client.tsx index 2321472d5f2..4f883e37123 100644 --- a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/analytics/page-client.tsx +++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/analytics/page-client.tsx @@ -89,34 +89,46 @@ export function ProgramAnalyticsPageClient() { programPage: true, }); + const filterSelect = ( + + ); + + const dateRangePicker = ( + + ); + return (
-
- - -
- -
+
diff --git a/apps/web/ui/analytics/toggle.tsx b/apps/web/ui/analytics/toggle.tsx index 08e56c39322..579fbb619b5 100644 --- a/apps/web/ui/analytics/toggle.tsx +++ b/apps/web/ui/analytics/toggle.tsx @@ -225,17 +225,17 @@ export function AnalyticsToggle({ ))}
- {isMobile ? dateRangePicker : filterSelect} + {filterSelect}
- {isMobile ? filterSelect : dateRangePicker} + {dateRangePicker} {!dashboardProps && (
{page === "analytics" && ( From 18e27ec40d5506df2b2a746bef03f2676212d5b3 Mon Sep 17 00:00:00 2001 From: Kiran K Date: Wed, 8 Apr 2026 06:38:07 +0530 Subject: [PATCH 5/7] Remove duplicate AXIOM_TOKEN / AXIOM_DATASET env from playwright.yaml (#3708) --- .github/workflows/playwright.yaml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/playwright.yaml b/.github/workflows/playwright.yaml index fb53499af0d..b6790749e3a 100644 --- a/.github/workflows/playwright.yaml +++ b/.github/workflows/playwright.yaml @@ -51,9 +51,6 @@ jobs: QSTASH_CURRENT_SIGNING_KEY: "xx" QSTASH_NEXT_SIGNING_KEY: "xx" - AXIOM_TOKEN: "" - AXIOM_DATASET: "" - # RESEND_API_KEY must be unset so emails route through SMTP to MailHog SMTP_HOST: "localhost" SMTP_PORT: "1025" @@ -70,6 +67,7 @@ jobs: STRIPE_APP_SECRET_KEY_TEST: "xx" STRIPE_CONNECT_V2_WEBHOOK_SECRET: "xx" STRIPE_APP_SECRET_KEY_SANDBOX: "xx" + STRIPE_APP_SECRET_KEY: "xx" services: mysql: From 44c5f14ab37934cae995cc98ee1cc2800bff648d Mon Sep 17 00:00:00 2001 From: Steven Tey Date: Tue, 7 Apr 2026 18:41:11 -0700 Subject: [PATCH 6/7] fix calculation --- .../api/admin/payouts/stablecoin/route.ts | 41 +++++++++---------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/apps/web/app/(ee)/api/admin/payouts/stablecoin/route.ts b/apps/web/app/(ee)/api/admin/payouts/stablecoin/route.ts index 2b31fc904fd..b98eeed99af 100644 --- a/apps/web/app/(ee)/api/admin/payouts/stablecoin/route.ts +++ b/apps/web/app/(ee)/api/admin/payouts/stablecoin/route.ts @@ -37,19 +37,26 @@ export const GET = withAdmin(async ({ searchParams }) => { where: { ...(status && { status }), ...(programId && { programId }), - partner: { - defaultPayoutMethod: "stablecoin", - stripeRecipientId: { - not: null, + OR: [ + { + method: "stablecoin", }, - cryptoWalletAddress: { - not: null, + { + status: "pending", + partner: { + stripeRecipientId: { + not: null, + }, + cryptoWalletAddress: { + not: null, + }, + payoutsEnabledAt: { + not: null, + }, + ...(country && { country }), + }, }, - payoutsEnabledAt: { - not: null, - }, - ...(country && { country }), - }, + ], }, orderBy: { amount: "desc", @@ -66,17 +73,7 @@ export const GET = withAdmin(async ({ searchParams }) => { .parse( payouts.filter( (payout) => - ( - payout as typeof payout & { - program?: { minPayoutAmount: number }; - } - ).program && - payout.amount >= - ( - payout as typeof payout & { - program: { minPayoutAmount: number }; - } - ).program.minPayoutAmount, + payout.program && payout.amount >= payout.program.minPayoutAmount, ), ), ); From 0fc01fa3c5aee3788fb2c017fe0dffbbdb5175ae Mon Sep 17 00:00:00 2001 From: Steven Tey Date: Tue, 7 Apr 2026 22:19:51 -0700 Subject: [PATCH 7/7] Add internal `approved_invited` status to show both `approved` and `invited` statuses (#3718) --- .../(ee)/program/partners/partners-table.tsx | 43 +++++++++++++------ .../actions/partners/bulk-archive-partners.ts | 7 ++- .../lib/actions/partners/bulk-ban-partners.ts | 7 ++- .../lib/api/partners/get-partners-count.ts | 14 +++++- .../api/partners/program-enrollment-query.ts | 7 ++- apps/web/lib/zod/schemas/partners.ts | 4 ++ .../customers-table/customers-table.tsx | 5 ++- .../customers-table/use-customer-filters.tsx | 15 ++++++- 8 files changed, 80 insertions(+), 22 deletions(-) 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 08aef1c3851..469491037d1 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 @@ -8,6 +8,7 @@ import usePartnersCount from "@/lib/swr/use-partners-count"; import useProgram from "@/lib/swr/use-program"; import useWorkspace from "@/lib/swr/use-workspace"; import { EnrolledPartnerProps } from "@/lib/types"; +import { ACTIVE_ENROLLMENT_STATUSES } from "@/lib/zod/schemas/partners"; import { useArchivePartnerModal } from "@/ui/modals/archive-partner-modal"; import { useBanPartnerModal } from "@/ui/modals/ban-partner-modal"; import { useBulkArchivePartnersModal } from "@/ui/modals/bulk-archive-partners-modal"; @@ -27,6 +28,7 @@ import { ProgramEnrollmentStatus } from "@dub/prisma/client"; import { AnimatedSizeContainer, Button, + DynamicTooltipWrapper, EditColumnsButton, Filter, Icon, @@ -122,7 +124,7 @@ export function PartnersTable() { const status = ( searchParams.get("status") || searchParams.get("search") ? undefined - : "approved" + : "approved_invited" ) as ProgramEnrollmentStatus; const sortBy = @@ -529,7 +531,7 @@ export function PartnersTable() { }} /> - {(status === "approved" || + {(!searchParams.get("status") || searchParams.get("status") === "approved") && ( !ACTIVE_ENROLLMENT_STATUSES.includes(partner.status), + ) + ? `You cannot perform this action because one or more partners are not in ${ACTIVE_ENROLLMENT_STATUSES.join(", ")} statuses.` + : undefined; + return ( @@ -939,11 +950,13 @@ function MenuItem({ label, onSelect, variant = "default", + disabledTooltip, }: { icon: Icon; label: string; onSelect: () => void; variant?: "default" | "danger"; + disabledTooltip?: string | boolean; }) { const variantStyles = { default: { @@ -959,16 +972,22 @@ function MenuItem({ const { text, icon } = variantStyles[variant]; return ( - - - {label} - + + + {label} + + ); } diff --git a/apps/web/lib/actions/partners/bulk-archive-partners.ts b/apps/web/lib/actions/partners/bulk-archive-partners.ts index 40618da1a37..fffa0db594a 100644 --- a/apps/web/lib/actions/partners/bulk-archive-partners.ts +++ b/apps/web/lib/actions/partners/bulk-archive-partners.ts @@ -2,7 +2,10 @@ import { recordAuditLog } from "@/lib/api/audit-logs/record-audit-log"; import { getDefaultProgramIdOrThrow } from "@/lib/api/programs/get-default-program-id-or-throw"; -import { bulkArchivePartnersSchema } from "@/lib/zod/schemas/partners"; +import { + ACTIVE_ENROLLMENT_STATUSES, + bulkArchivePartnersSchema, +} from "@/lib/zod/schemas/partners"; import { prisma } from "@dub/prisma"; import { waitUntil } from "@vercel/functions"; import { authActionClient } from "../safe-action"; @@ -28,7 +31,7 @@ export const bulkArchivePartnersAction = authActionClient }, programId, status: { - not: "archived", + in: ACTIVE_ENROLLMENT_STATUSES, }, }, select: { diff --git a/apps/web/lib/actions/partners/bulk-ban-partners.ts b/apps/web/lib/actions/partners/bulk-ban-partners.ts index 0ee0d972d35..aa4ca613a93 100644 --- a/apps/web/lib/actions/partners/bulk-ban-partners.ts +++ b/apps/web/lib/actions/partners/bulk-ban-partners.ts @@ -4,7 +4,10 @@ import { recordAuditLog } from "@/lib/api/audit-logs/record-audit-log"; import { resolveFraudGroups } from "@/lib/api/fraud/resolve-fraud-groups"; import { getDefaultProgramIdOrThrow } from "@/lib/api/programs/get-default-program-id-or-throw"; import { enqueueBatchJobs } from "@/lib/cron/enqueue-batch-jobs"; -import { bulkBanPartnersSchema } from "@/lib/zod/schemas/partners"; +import { + ACTIVE_ENROLLMENT_STATUSES, + bulkBanPartnersSchema, +} from "@/lib/zod/schemas/partners"; import { prisma } from "@dub/prisma"; import { ProgramEnrollmentStatus } from "@dub/prisma/client"; import { APP_DOMAIN_WITH_NGROK } from "@dub/utils"; @@ -32,7 +35,7 @@ export const bulkBanPartnersAction = authActionClient }, programId, status: { - not: "banned", + in: ACTIVE_ENROLLMENT_STATUSES, }, }, select: { diff --git a/apps/web/lib/api/partners/get-partners-count.ts b/apps/web/lib/api/partners/get-partners-count.ts index 6d7e2c7489d..2dcec84e6dd 100644 --- a/apps/web/lib/api/partners/get-partners-count.ts +++ b/apps/web/lib/api/partners/get-partners-count.ts @@ -41,7 +41,12 @@ export async function getPartnersCount( ...(groupId && { groupId, }), - status, + status: + status === "approved_invited" + ? { + in: ["approved", "invited"], + } + : status, ...enrollmentMetricWhere, }, }, @@ -108,7 +113,12 @@ export async function getPartnersCount( }), ...commonWhere, }, - status, + status: + status === "approved_invited" + ? { + in: ["approved", "invited"], + } + : status, ...enrollmentMetricWhere, }, _count: true, diff --git a/apps/web/lib/api/partners/program-enrollment-query.ts b/apps/web/lib/api/partners/program-enrollment-query.ts index ef78eb51b2c..38e057e9449 100644 --- a/apps/web/lib/api/partners/program-enrollment-query.ts +++ b/apps/web/lib/api/partners/program-enrollment-query.ts @@ -153,7 +153,12 @@ export function buildProgramEnrollmentWhereForList( in: partnerIds, }, }), - status, + status: + status === "approved_invited" + ? { + in: ["approved", "invited"], + } + : status, groupId, ...(country || search || email ? { diff --git a/apps/web/lib/zod/schemas/partners.ts b/apps/web/lib/zod/schemas/partners.ts index ef4530f7ad0..7aadd18b77b 100644 --- a/apps/web/lib/zod/schemas/partners.ts +++ b/apps/web/lib/zod/schemas/partners.ts @@ -191,6 +191,10 @@ export const getPartnersQuerySchema = z .extend(getPaginationQuerySchema({ pageSize: PARTNERS_MAX_PAGE_SIZE })); export const getPartnersQuerySchemaExtended = getPartnersQuerySchema.extend({ + status: z + .enum(ProgramEnrollmentStatus) + .or(z.enum(["approved_invited"])) + .optional(), partnerIds: z .union([z.string(), z.array(z.string())]) .transform((v) => (Array.isArray(v) ? v : v.split(","))) diff --git a/apps/web/ui/customers/customers-table/customers-table.tsx b/apps/web/ui/customers/customers-table/customers-table.tsx index 34437b72cc2..b91c5613f25 100644 --- a/apps/web/ui/customers/customers-table/customers-table.tsx +++ b/apps/web/ui/customers/customers-table/customers-table.tsx @@ -404,6 +404,7 @@ export function CustomersTable({ sortBy={sortBy} sortOrder={sortOrder} enabled={canManageCustomers} + isProgramPage={isProgramPage} /> {!canManageCustomers || customers?.length !== 0 ? ( diff --git a/apps/web/ui/customers/customers-table/use-customer-filters.tsx b/apps/web/ui/customers/customers-table/use-customer-filters.tsx index 99636e72d01..e6d374cb2c6 100644 --- a/apps/web/ui/customers/customers-table/use-customer-filters.tsx +++ b/apps/web/ui/customers/customers-table/use-customer-filters.tsx @@ -16,10 +16,13 @@ import { useDebounce } from "use-debounce"; export function useCustomerFilters( extraSearchParams: Record, - { enabled = true }: { enabled?: boolean } = {}, + { + enabled = true, + isProgramPage = false, + }: { enabled?: boolean; isProgramPage?: boolean } = {}, ) { const { searchParamsObj, queryParams } = useRouterStuff(); - const { id: workspaceId, slug } = useWorkspace(); + const { id: workspaceId, slug, defaultProgramId } = useWorkspace(); const [selectedFilter, setSelectedFilter] = useState(null); const [search, setSearch] = useState(""); @@ -38,6 +41,10 @@ export function useCustomerFilters( >({ query: { groupBy: "country", + ...(isProgramPage && + defaultProgramId && { + programId: defaultProgramId, + }), }, enabled, }); @@ -53,6 +60,10 @@ export function useCustomerFilters( >({ query: { groupBy: "linkId", + ...(isProgramPage && + defaultProgramId && { + programId: defaultProgramId, + }), }, enabled, });