From 9ae449ee3b7110b57055cc88ba62a2a5f9bef510 Mon Sep 17 00:00:00 2001 From: Steven Tey Date: Sun, 19 Oct 2025 23:07:48 -0700 Subject: [PATCH] Add Instant payouts feature --- .../charge-succeeded/send-stripe-payouts.ts | 1 - .../payouts/partner-payout-settings-sheet.tsx | 83 +--------------- .../payouts/payout-details-sheet.tsx | 95 ++++++++++--------- .../(dashboard)/payouts/payout-stats.tsx | 95 ++++++++++++++++++- .../(dashboard)/payouts/payout-table.tsx | 17 ++-- .../lib/actions/partners/force-withdrawal.ts | 62 ++++++++++++ .../update-partner-payout-settings.ts | 41 +------- apps/web/lib/partners/constants.ts | 4 +- .../lib/partners/create-stripe-transfer.ts | 48 +++++----- apps/web/lib/zod/schemas/partners.ts | 17 +--- apps/web/ui/partners/payout-row-menu.tsx | 82 ++++++++-------- .../ui/partners/payout-status-descriptions.ts | 6 +- .../templates/partner-payout-processed.tsx | 12 ++- packages/prisma/schema/partner.prisma | 1 - 14 files changed, 300 insertions(+), 264 deletions(-) create mode 100644 apps/web/lib/actions/partners/force-withdrawal.ts diff --git a/apps/web/app/(ee)/api/cron/payouts/charge-succeeded/send-stripe-payouts.ts b/apps/web/app/(ee)/api/cron/payouts/charge-succeeded/send-stripe-payouts.ts index 2f490d3b371..d1cbfd24e67 100644 --- a/apps/web/app/(ee)/api/cron/payouts/charge-succeeded/send-stripe-payouts.ts +++ b/apps/web/app/(ee)/api/cron/payouts/charge-succeeded/send-stripe-payouts.ts @@ -17,7 +17,6 @@ export async function sendStripePayouts({ id: true, email: true, stripeConnectId: true, - minWithdrawalAmount: true, }, }, program: { diff --git a/apps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/partner-payout-settings-sheet.tsx b/apps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/partner-payout-settings-sheet.tsx index ec66c65db61..07517592db0 100644 --- a/apps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/partner-payout-settings-sheet.tsx +++ b/apps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/partner-payout-settings-sheet.tsx @@ -1,33 +1,20 @@ "use client"; import { updatePartnerPayoutSettingsAction } from "@/lib/actions/partners/update-partner-payout-settings"; -import { - ALLOWED_MIN_WITHDRAWAL_AMOUNTS, - BELOW_MIN_WITHDRAWAL_FEE_CENTS, - MIN_WITHDRAWAL_AMOUNT_CENTS, -} from "@/lib/partners/constants"; import { mutatePrefix } from "@/lib/swr/mutate"; import usePartnerProfile from "@/lib/swr/use-partner-profile"; import { partnerPayoutSettingsSchema } from "@/lib/zod/schemas/partners"; import { ConnectPayoutButton } from "@/ui/partners/connect-payout-button"; import { PayoutMethodsDropdown } from "@/ui/partners/payout-methods-dropdown"; import { - AnimatedSizeContainer, Button, InfoTooltip, Sheet, SimpleTooltipContent, - Slider, useScrollProgress, } from "@dub/ui"; -import { - CONNECT_SUPPORTED_COUNTRIES, - COUNTRIES, - currencyFormatter, -} from "@dub/utils"; +import { CONNECT_SUPPORTED_COUNTRIES, COUNTRIES } from "@dub/utils"; import { COUNTRY_CURRENCY_CODES } from "@dub/utils/src"; -import NumberFlow from "@number-flow/react"; -import { PartyPopper } from "lucide-react"; import { useAction } from "next-safe-action/hooks"; import Link from "next/link"; import { @@ -82,7 +69,6 @@ function PartnerPayoutSettingsSheetInner({ companyName: partner?.companyName || undefined, address: partner?.invoiceSettings?.address || undefined, taxId: partner?.invoiceSettings?.taxId || undefined, - minWithdrawalAmount: partner?.minWithdrawalAmount, }, }); @@ -104,8 +90,6 @@ function PartnerPayoutSettingsSheetInner({ await executeAsync(data); }; - const minWithdrawalAmount = watch("minWithdrawalAmount"); - const scrollRef = useRef(null); const { scrollProgress, updateScrollProgress } = useScrollProgress(scrollRef); @@ -175,71 +159,6 @@ function PartnerPayoutSettingsSheetInner({ )} - {/* Minimum withdrawal amount */} - {partner?.country && - CONNECT_SUPPORTED_COUNTRIES.includes(partner.country) && ( -
-
-

- Minimum withdrawal amount -

-

- Set the minimum amount for funds to be automatically - withdrawn from Dub into your connected payout account. -

-
- -
- - - { - const closest = ALLOWED_MIN_WITHDRAWAL_AMOUNTS.reduce( - (prev, curr) => - Math.abs(curr - value) < Math.abs(prev - value) - ? curr - : prev, - ); - - setValue("minWithdrawalAmount", closest, { - shouldDirty: true, - }); - }} - marks={ALLOWED_MIN_WITHDRAWAL_AMOUNTS} - hint={ - - {minWithdrawalAmount < MIN_WITHDRAWAL_AMOUNT_CENTS ? ( - `${currencyFormatter(BELOW_MIN_WITHDRAWAL_FEE_CENTS / 100)} withdrawal fee for balances under ${currencyFormatter(MIN_WITHDRAWAL_AMOUNT_CENTS / 100)}. If you have any previously processed payouts, they will be automatically transferred to your connected bank account once the minimum withdrawal amount is reached.` - ) : ( -
- - Free withdrawals unlocked -
- )} -
- } - /> -
-
- )} - {/* Invoice details */}
diff --git a/apps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/payout-details-sheet.tsx b/apps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/payout-details-sheet.tsx index efa6c7cd655..e2e0295f30f 100644 --- a/apps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/payout-details-sheet.tsx +++ b/apps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/payout-details-sheet.tsx @@ -11,7 +11,6 @@ import { ConditionalLink } from "@/ui/shared/conditional-link"; import { X } from "@/ui/shared/icons"; import { Button, - buttonVariants, InvoiceDollar, LoadingSpinner, Sheet, @@ -177,57 +176,61 @@ function PayoutDetailsSheetContent({ payout }: PayoutDetailsSheetProps) { } as any); return ( -
-
- - Payout details - - -
-
-
- Invoice details -
-
- {Object.entries(invoiceData).map(([key, value]) => ( - -
- {key} -
-
{value}
-
- ))} +
+
+
+ + Payout details + + +
- {isLoading ? ( -
- + +
+
+
+ Invoice details +
+
+ {Object.entries(invoiceData).map(([key, value]) => ( + +
+ {key} +
+
{value}
+
+ ))} +
- ) : earnings?.length ? ( - <> + + {isLoading ? ( +
+ +
+ ) : earnings?.length ? (
-
- - View all - -
- - ) : null} + ) : null} + + +
+
+ +
+
); } diff --git a/apps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/payout-stats.tsx b/apps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/payout-stats.tsx index a30f47043f6..b1f46410212 100644 --- a/apps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/payout-stats.tsx +++ b/apps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/payout-stats.tsx @@ -1,13 +1,19 @@ "use client"; +import { forceWithdrawalAction } from "@/lib/actions/partners/force-withdrawal"; +import { + BELOW_MIN_WITHDRAWAL_FEE_CENTS, + MIN_WITHDRAWAL_AMOUNT_CENTS, +} from "@/lib/partners/constants"; import usePartnerPayoutsCount from "@/lib/swr/use-partner-payouts-count"; import usePartnerProfile from "@/lib/swr/use-partner-profile"; import { PayoutsCount } from "@/lib/types"; +import { useConfirmModal } from "@/ui/modals/confirm-modal"; import { PayoutStatusBadges } from "@/ui/partners/payout-status-badges"; import { PAYOUT_STATUS_DESCRIPTIONS } from "@/ui/partners/payout-status-descriptions"; import { AlertCircleFill } from "@/ui/shared/icons"; import { PayoutStatus } from "@dub/prisma/client"; -import { Tooltip } from "@dub/ui"; +import { Button, Tooltip } from "@dub/ui"; import { cn, CONNECT_SUPPORTED_COUNTRIES, @@ -15,6 +21,8 @@ import { PAYPAL_SUPPORTED_COUNTRIES, } from "@dub/utils"; import { HelpCircle } from "lucide-react"; +import { useAction } from "next-safe-action/hooks"; +import { toast } from "sonner"; function PayoutStatsCard({ label, @@ -23,6 +31,7 @@ function PayoutStatsCard({ iconClassName, tooltip, error, + setShowForceWithdrawalModal, }: { label: string; amount: number; @@ -30,6 +39,7 @@ function PayoutStatsCard({ iconClassName?: string; tooltip?: string; error?: boolean; + setShowForceWithdrawalModal: (show: boolean) => void; }) { const { partner } = usePartnerProfile(); @@ -82,6 +92,14 @@ function PayoutStatsCard({ <>{amount > 0 ? currencyFormatter(amount / 100) : "$0.00"} )} + {label === "Processed" && amount > 0 && ( +