diff --git a/apps/web/app/(ee)/api/cron/payouts/confirm/confirm-payouts.ts b/apps/web/app/(ee)/api/cron/payouts/confirm/confirm-payouts.ts index b382310f22a..d065ab99b66 100644 --- a/apps/web/app/(ee)/api/cron/payouts/confirm/confirm-payouts.ts +++ b/apps/web/app/(ee)/api/cron/payouts/confirm/confirm-payouts.ts @@ -1,9 +1,13 @@ import { createId } from "@/lib/api/create-id"; -import { PAYOUT_FEES } from "@/lib/partners/constants"; +import { + DIRECT_DEBIT_PAYMENT_METHOD_TYPES, + PAYMENT_METHOD_TYPES, +} from "@/lib/partners/constants"; import { CUTOFF_PERIOD, CUTOFF_PERIOD_TYPES, } from "@/lib/partners/cutoff-period"; +import { calculatePayoutFee } from "@/lib/payment-methods"; import { stripe } from "@/lib/stripe"; import { resend } from "@dub/email/resend"; import { VARIANT_TO_FROM_MAP } from "@dub/email/resend/constants"; @@ -12,8 +16,6 @@ import { prisma } from "@dub/prisma"; import { chunk, log } from "@dub/utils"; import { Program, Project } from "@prisma/client"; -const allowedPaymentMethods = ["us_bank_account", "card", "link"]; - export async function confirmPayouts({ workspace, program, @@ -83,9 +85,10 @@ export async function confirmPayouts({ const fee = amount * - PAYOUT_FEES[workspace.plan?.split(" ")[0] ?? "business"][ - paymentMethod.type === "us_bank_account" ? "ach" : "card" - ]; + calculatePayoutFee({ + paymentMethod: paymentMethod.type, + plan: workspace.plan, + }); const total = amount + fee; @@ -117,7 +120,7 @@ export async function confirmPayouts({ await stripe.paymentIntents.create({ amount: invoice.total, customer: workspace.stripeId!, - payment_method_types: allowedPaymentMethods, + payment_method_types: PAYMENT_METHOD_TYPES, payment_method: paymentMethod.id, currency: "usd", confirmation_method: "automatic", @@ -154,9 +157,12 @@ export async function confirmPayouts({ return invoice; }); - // Send emails to all the partners involved in the payouts if the payout method is ACH - // This is because ACH takes 4 business days to process, so we want to give partners a heads up - if (newInvoice && paymentMethod.type === "us_bank_account") { + // Send emails to all the partners involved in the payouts if the payout method is Direct Debit + // This is because Direct Debit takes 4 business days to process, so we want to give partners a heads up + if ( + newInvoice && + DIRECT_DEBIT_PAYMENT_METHOD_TYPES.includes(paymentMethod.type) + ) { if (!resend) { // this should never happen, but just in case await log({ diff --git a/apps/web/app/(ee)/api/stripe/webhook/charge-failed.ts b/apps/web/app/(ee)/api/stripe/webhook/charge-failed.ts index a742e9e6938..dce73529fa3 100644 --- a/apps/web/app/(ee)/api/stripe/webhook/charge-failed.ts +++ b/apps/web/app/(ee)/api/stripe/webhook/charge-failed.ts @@ -1,4 +1,7 @@ -import { PAYOUT_FAILURE_FEE_CENTS } from "@/lib/partners/constants"; +import { + DIRECT_DEBIT_PAYMENT_METHOD_TYPES, + PAYOUT_FAILURE_FEE_CENTS, +} from "@/lib/partners/constants"; import { stripe } from "@/lib/stripe"; import { sendEmail } from "@dub/email"; import PartnerPayoutFailed from "@dub/email/templates/partner-payout-failed"; @@ -80,8 +83,13 @@ export async function chargeFailed(event: Stripe.Event) { let cardLast4: string | undefined; let chargedFailureFee = false; - // Charge failure fee for ACH payment failures - if (charge.payment_method_details?.type === "us_bank_account") { + // Charge failure fee for direct debit payment failures + if ( + charge.payment_method_details && + DIRECT_DEBIT_PAYMENT_METHOD_TYPES.includes( + charge.payment_method_details.type as Stripe.PaymentMethod.Type, + ) + ) { const [cards, links] = await Promise.all([ stripe.paymentMethods.list({ customer: workspace.stripeId, diff --git a/apps/web/app/api/workspaces/[idOrSlug]/billing/payment-methods/route.ts b/apps/web/app/api/workspaces/[idOrSlug]/billing/payment-methods/route.ts index a9505268db3..0e96a9c645f 100644 --- a/apps/web/app/api/workspaces/[idOrSlug]/billing/payment-methods/route.ts +++ b/apps/web/app/api/workspaces/[idOrSlug]/billing/payment-methods/route.ts @@ -1,10 +1,22 @@ +import { DubApiError } from "@/lib/api/errors"; import { parseRequestBody } from "@/lib/api/utils"; import { withWorkspace } from "@/lib/auth"; +import { + DIRECT_DEBIT_PAYMENT_METHOD_TYPES, + DIRECT_DEBIT_PAYMENT_TYPES_INFO, + PAYMENT_METHOD_TYPES, +} from "@/lib/partners/constants"; import { stripe } from "@/lib/stripe"; import { APP_DOMAIN } from "@dub/utils"; import { NextResponse } from "next/server"; +import Stripe from "stripe"; import { z } from "zod"; +const addPaymentMethodSchema = z.object({ + method: z.enum(PAYMENT_METHOD_TYPES as [string, ...string[]]).optional(), +}); + +// GET /api/workspaces/[idOrSlug]/billing/payment-methods - get all payment methods export const GET = withWorkspace(async ({ workspace }) => { if (!workspace.stripeId) { return NextResponse.json([]); @@ -15,14 +27,14 @@ export const GET = withWorkspace(async ({ workspace }) => { customer: workspace.stripeId, }); - // reorder to put ACH first - const ach = paymentMethods.data.find( - (method) => method.type === "us_bank_account", + // reorder to put direct debit first + const directDebit = paymentMethods.data.find((method) => + DIRECT_DEBIT_PAYMENT_METHOD_TYPES.includes(method.type), ); return NextResponse.json([ - ...(ach ? [ach] : []), - ...paymentMethods.data.filter((method) => method.id !== ach?.id), + ...(directDebit ? [directDebit] : []), + ...paymentMethods.data.filter((method) => method.id !== directDebit?.id), ]); } catch (error) { console.error(error); @@ -30,13 +42,13 @@ export const GET = withWorkspace(async ({ workspace }) => { } }); -const addPaymentMethodSchema = z.object({ - method: z.enum(["card", "us_bank_account"]).optional(), -}); - +// POST /api/workspaces/[idOrSlug]/billing/payment-methods - add a payment method for the workspace export const POST = withWorkspace(async ({ workspace, req }) => { if (!workspace.stripeId) { - return NextResponse.json({ error: "Workspace does not have a Stripe ID" }); + throw new DubApiError({ + code: "bad_request", + message: "Workspace does not have a Stripe ID.", + }); } const { method } = addPaymentMethodSchema.parse(await parseRequestBody(req)); @@ -53,10 +65,20 @@ export const POST = withWorkspace(async ({ workspace, req }) => { return NextResponse.json({ url }); } + const paymentMethodOption = DIRECT_DEBIT_PAYMENT_TYPES_INFO.find( + (type) => type.type === method, + )?.option; + const { url } = await stripe.checkout.sessions.create({ mode: "setup", customer: workspace.stripeId, - payment_method_types: [method], + payment_method_types: [ + method as Stripe.Checkout.SessionCreateParams.PaymentMethodType, + ], + payment_method_options: { + [method]: paymentMethodOption, + }, + currency: "usd", success_url: `${APP_DOMAIN}/${workspace.slug}/settings/billing`, cancel_url: `${APP_DOMAIN}/${workspace.slug}/settings/billing`, }); diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/billing/payment-method-types.ts b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/billing/payment-method-types.ts index 8cef0321b9a..2eb6cd1bfc0 100644 --- a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/billing/payment-method-types.ts +++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/billing/payment-method-types.ts @@ -38,6 +38,22 @@ export const PaymentMethodTypesList = (paymentMethod?: Stripe.PaymentMethod) => ? `Account ending in ****${paymentMethod.us_bank_account.last4}` : "Not connected", }, + { + type: "acss_debit", + title: "ACSS Debit", + icon: GreekTemple, + description: paymentMethod?.acss_debit + ? `Account ending in ****${paymentMethod.acss_debit.last4}` + : "Not connected", + }, + { + type: "sepa_debit", + title: "SEPA Debit", + icon: GreekTemple, + description: paymentMethod?.sepa_debit + ? `Account ending in ****${paymentMethod.sepa_debit.last4}` + : "Not connected", + }, { type: "link", title: "Link", diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/billing/payment-methods.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/billing/payment-methods.tsx index f9f33ddff85..8968dc6898a 100644 --- a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/billing/payment-methods.tsx +++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/billing/payment-methods.tsx @@ -1,9 +1,11 @@ "use client"; +import { DIRECT_DEBIT_PAYMENT_METHOD_TYPES } from "@/lib/partners/constants"; import usePaymentMethods from "@/lib/swr/use-payment-methods"; import useWorkspace from "@/lib/swr/use-workspace"; +import { useAddPaymentMethodModal } from "@/ui/modals/add-payment-method-modal"; import { AnimatedEmptyState } from "@/ui/shared/animated-empty-state"; -import { Badge, Button, CreditCard, MoneyBill2 } from "@dub/ui"; +import { Badge, Button, CreditCard, GreekTemple, MoneyBill2 } from "@dub/ui"; import { cn } from "@dub/utils"; import Link from "next/link"; import { useRouter } from "next/navigation"; @@ -13,19 +15,18 @@ import { PaymentMethodTypesList } from "./payment-method-types"; export default function PaymentMethods() { const router = useRouter(); - const { slug, stripeId, partnersEnabled, plan } = useWorkspace(); const { paymentMethods } = usePaymentMethods(); + const [isLoading, setIsLoading] = useState(false); + const { slug, stripeId, partnersEnabled, plan } = useWorkspace(); const regularPaymentMethods = paymentMethods?.filter( - (pm) => pm.type !== "us_bank_account", + (pm) => !DIRECT_DEBIT_PAYMENT_METHOD_TYPES.includes(pm.type), ); - const achPaymentMethods = paymentMethods?.filter( - (pm) => pm.type === "us_bank_account", + const partnerPaymentMethods = paymentMethods?.filter((pm) => + DIRECT_DEBIT_PAYMENT_METHOD_TYPES.includes(pm.type), ); - const [isLoading, setIsLoading] = useState(false); - const managePaymentMethods = async () => { setIsLoading(true); const { url } = await fetch( @@ -86,23 +87,26 @@ export default function PaymentMethods() { /> ) ) : ( - <> - - - + )} + {partnersEnabled && ( <> - {achPaymentMethods && achPaymentMethods.length > 0 ? ( - achPaymentMethods.map((paymentMethod) => ( - - )) + {partnerPaymentMethods ? ( + partnerPaymentMethods.length > 0 ? ( + partnerPaymentMethods.map((paymentMethod) => ( + + )) + ) : ( + + ) ) : ( - + )} )} @@ -114,14 +118,12 @@ export default function PaymentMethods() { const PaymentMethodCard = ({ type, paymentMethod, + forPayouts = false, }: { type: Stripe.PaymentMethod.Type; paymentMethod?: Stripe.PaymentMethod; + forPayouts?: boolean; }) => { - const router = useRouter(); - const { slug } = useWorkspace(); - const [isLoading, setIsLoading] = useState(false); - const result = PaymentMethodTypesList(paymentMethod); const { @@ -131,55 +133,74 @@ const PaymentMethodCard = ({ description, } = result.find((method) => method.type === type) || result[0]; - const addPaymentMethod = async (method: string) => { - setIsLoading(true); - const { url } = await fetch( - `/api/workspaces/${slug}/billing/payment-methods`, - { - method: "POST", - body: JSON.stringify({ method }), - }, - ).then((res) => res.json()); + return ( + <> + +
+
+
+ +
+
+
+

{title}

+ {paymentMethod && + (DIRECT_DEBIT_PAYMENT_METHOD_TYPES.includes(type) || + paymentMethod.link?.email) && ( + + Connected + + )} +
+

{description}

+
+
+
+
+ + ); +}; - router.push(url); - }; +const NoPartnerPaymentMethods = () => { + const { setShowAddPaymentMethodModal, AddPaymentMethodModal } = + useAddPaymentMethodModal(); return ( - -
-
-
- -
-
-
-

{title}

- {paymentMethod && - (type === "us_bank_account" || paymentMethod.link?.email) && ( - - Connected - - )} + <> + {AddPaymentMethodModal} + +
+
+
+ +
+ +
+
+

Bank account

+
+

Not connected

-

{description}

-
- {!paymentMethod && ( +
- +
+ + ); }; 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 71b1ccc6598..a5e2b6d1ec4 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 @@ -59,7 +59,9 @@ export default function PlanUsage() { status: "approved", }); - const payoutFees = plan ? PAYOUT_FEES[plan.toLowerCase()]?.ach : null; + const payoutFees = plan + ? PAYOUT_FEES[plan.toLowerCase()]?.direct_debit + : null; const { data: tags } = useTagsCount(); const { users } = useUsers(); diff --git a/apps/web/lib/actions/partners/confirm-payouts.ts b/apps/web/lib/actions/partners/confirm-payouts.ts index 087b89989ed..69893fd39ca 100644 --- a/apps/web/lib/actions/partners/confirm-payouts.ts +++ b/apps/web/lib/actions/partners/confirm-payouts.ts @@ -1,6 +1,7 @@ "use server"; import { qstash } from "@/lib/cron"; +import { PAYMENT_METHOD_TYPES } from "@/lib/partners/constants"; import { CUTOFF_PERIOD_ENUM } from "@/lib/partners/cutoff-period"; import { stripe } from "@/lib/stripe"; import { APP_DOMAIN_WITH_NGROK } from "@dub/utils"; @@ -13,8 +14,7 @@ const confirmPayoutsSchema = z.object({ cutoffPeriod: CUTOFF_PERIOD_ENUM, }); -const allowedPaymentMethods = ["us_bank_account", "card", "link"]; - +// Confirm payouts export const confirmPayoutsAction = authActionClient .schema(confirmPayoutsSchema) .action(async ({ parsedInput, ctx }) => { @@ -35,9 +35,11 @@ export const confirmPayoutsAction = authActionClient throw new Error("Invalid payout method."); } - if (!allowedPaymentMethods.includes(paymentMethod.type)) { + if (!PAYMENT_METHOD_TYPES.includes(paymentMethod.type)) { throw new Error( - "We only support ACH and Card for now. Please update your payout method to one of these.", + `We only support ${PAYMENT_METHOD_TYPES.join( + ", ", + )} for now. Please update your payout method to one of these.`, ); } diff --git a/apps/web/lib/partners/constants.ts b/apps/web/lib/partners/constants.ts index 81c66711984..02d987ccb9b 100644 --- a/apps/web/lib/partners/constants.ts +++ b/apps/web/lib/partners/constants.ts @@ -1,22 +1,73 @@ +import Stripe from "stripe"; +import { PaymentMethodOption } from "../types"; + export const PAYOUTS_SHEET_ITEMS_LIMIT = 10; export const REFERRALS_EMBED_EARNINGS_LIMIT = 8; export const CUSTOMER_PAGE_EVENTS_LIMIT = 8; export const PAYOUT_FEES = { business: { - ach: 0.05, - card: 0.08, + direct_debit: 0.05, + card: 0.1, }, advanced: { - ach: 0.05, + direct_debit: 0.05, card: 0.08, }, enterprise: { - ach: 0.03, + direct_debit: 0.03, card: 0.06, }, -}; - -export const DUB_MIN_PAYOUT_AMOUNT_CENTS = 10000; // 100 USD +} as const; +export const DUB_MIN_PAYOUT_AMOUNT_CENTS = 10000; export const PAYOUT_FAILURE_FEE_CENTS = 1000; // 10 USD + +// Direct debit payment types for Partner payout +export const DIRECT_DEBIT_PAYMENT_TYPES_INFO: { + type: Stripe.PaymentMethod.Type; + location: string; + title: string; + icon: string; + option: PaymentMethodOption; +}[] = [ + { + type: "us_bank_account", + location: "US", + title: "ACH", + icon: "https://hatscripts.github.io/circle-flags/flags/us.svg", + option: {}, + }, + { + type: "acss_debit", + location: "CA", + title: "ACSS Debit", + icon: "https://hatscripts.github.io/circle-flags/flags/ca.svg", + option: { + currency: "cad", + mandate_options: { + payment_schedule: "sporadic", + transaction_type: "business", + }, + }, + }, + { + type: "sepa_debit", + location: "EU", + title: "SEPA Debit", + icon: "https://hatscripts.github.io/circle-flags/flags/eu.svg", + option: {}, + }, +]; + +export const DIRECT_DEBIT_PAYMENT_METHOD_TYPES: Stripe.PaymentMethod.Type[] = [ + "us_bank_account", + "acss_debit", + "sepa_debit", +]; + +export const PAYMENT_METHOD_TYPES: Stripe.PaymentMethod.Type[] = [ + "card", + "link", + ...DIRECT_DEBIT_PAYMENT_METHOD_TYPES, +]; diff --git a/apps/web/lib/payment-methods.ts b/apps/web/lib/payment-methods.ts new file mode 100644 index 00000000000..bb20e872dcb --- /dev/null +++ b/apps/web/lib/payment-methods.ts @@ -0,0 +1,31 @@ +import Stripe from "stripe"; +import { + DIRECT_DEBIT_PAYMENT_METHOD_TYPES, + PAYOUT_FEES, +} from "./partners/constants"; + +export const calculatePayoutFee = ({ + paymentMethod, + plan, +}: { + paymentMethod: Stripe.PaymentMethod.Type; + plan: string | undefined; +}) => { + if (!paymentMethod) { + return null; + } + + const planType = plan?.split(" ")[0] ?? "business"; + + if (!Object.keys(PAYOUT_FEES).includes(planType)) { + return null; + } + + if (["link", "card"].includes(paymentMethod)) { + return PAYOUT_FEES[planType].card; + } + + if (DIRECT_DEBIT_PAYMENT_METHOD_TYPES.includes(paymentMethod)) { + return PAYOUT_FEES[planType].direct_debit; + } +}; diff --git a/apps/web/lib/types.ts b/apps/web/lib/types.ts index 0f9a0cb799e..7d9eaa716f1 100644 --- a/apps/web/lib/types.ts +++ b/apps/web/lib/types.ts @@ -478,3 +478,11 @@ export type ProgramData = z.infer; export type ProgramMetrics = z.infer; export type PayoutMethod = "stripe" | "paypal"; + +export type PaymentMethodOption = { + currency?: string; + mandate_options?: { + payment_schedule?: string; + transaction_type?: string; + }; +}; diff --git a/apps/web/ui/modals/add-payment-method-modal.tsx b/apps/web/ui/modals/add-payment-method-modal.tsx new file mode 100644 index 00000000000..f353cc3ba87 --- /dev/null +++ b/apps/web/ui/modals/add-payment-method-modal.tsx @@ -0,0 +1,146 @@ +"use client"; + +import { DIRECT_DEBIT_PAYMENT_TYPES_INFO } from "@/lib/partners/constants"; +import useWorkspace from "@/lib/swr/use-workspace"; +import { X } from "@/ui/shared/icons"; +import { AnimatedSizeContainer, GreekTemple, Modal } from "@dub/ui"; +import { useRouter } from "next/navigation"; +import { CSSProperties, Dispatch, SetStateAction, useState } from "react"; +import { toast } from "sonner"; +import Stripe from "stripe"; + +function AddPaymentMethodModal({ + showAddPaymentMethodModal, + setShowAddPaymentMethodModal, +}: { + showAddPaymentMethodModal: boolean; + setShowAddPaymentMethodModal: Dispatch>; +}) { + return ( + + + + ); +} + +function AddPaymentMethodModalInner({ + setShowAddPaymentMethodModal, +}: { + setShowAddPaymentMethodModal: Dispatch>; +}) { + const router = useRouter(); + const { slug } = useWorkspace(); + const [isLoading, setIsLoading] = useState(false); + + const addPaymentMethod = async (type: Stripe.PaymentMethod.Type) => { + setIsLoading(true); + + const response = await fetch( + `/api/workspaces/${slug}/billing/payment-methods`, + { + method: "POST", + body: JSON.stringify({ + method: type, + }), + }, + ); + + if (!response.ok) { + setIsLoading(false); + toast.error("Failed to add payment method. Please try again."); + return; + } + + const data = (await response.json()) as { url: string }; + + router.push(data.url); + }; + + return ( + +
+ + +
+
+ +
+ +
+

+ Connect your bank account +

+

+ Select your bank’s location to connect your bank account. +

+
+ +
+ {DIRECT_DEBIT_PAYMENT_TYPES_INFO.map( + ({ type, location, title, icon: Icon }, index) => ( + + ), + )} +
+
+
+
+ ); +} + +export function useAddPaymentMethodModal() { + const [showAddPaymentMethodModal, setShowAddPaymentMethodModal] = + useState(false); + + return { + setShowAddPaymentMethodModal, + AddPaymentMethodModal: ( + + ), + }; +} diff --git a/apps/web/ui/partners/payout-invoice-sheet.tsx b/apps/web/ui/partners/payout-invoice-sheet.tsx index 62d09fb8ef6..297a2ea91e3 100644 --- a/apps/web/ui/partners/payout-invoice-sheet.tsx +++ b/apps/web/ui/partners/payout-invoice-sheet.tsx @@ -1,9 +1,10 @@ import { confirmPayoutsAction } from "@/lib/actions/partners/confirm-payouts"; -import { PAYOUT_FEES } from "@/lib/partners/constants"; +import { DIRECT_DEBIT_PAYMENT_METHOD_TYPES } from "@/lib/partners/constants"; import { CUTOFF_PERIOD, CUTOFF_PERIOD_TYPES, } from "@/lib/partners/cutoff-period"; +import { calculatePayoutFee } from "@/lib/payment-methods"; import { mutatePrefix } from "@/lib/swr/mutate"; import usePaymentMethods from "@/lib/swr/use-payment-methods"; import useWorkspace from "@/lib/swr/use-workspace"; @@ -34,45 +35,57 @@ import { import { useAction } from "next-safe-action/hooks"; import { Fragment, useEffect, useMemo, useState } from "react"; import { toast } from "sonner"; +import Stripe from "stripe"; import useSWR from "swr"; +const PAYMENT_METHODS = Object.freeze({ + link: { + label: "link", + type: "link", + icon: CreditCard, + duration: "Instantly", + }, + card: { + label: "card", + type: "card", + icon: CreditCard, + duration: "Instantly", + }, + us_bank_account: { + label: "ACH", + type: "us_bank_account", + icon: GreekTemple, + duration: "4 business days", + }, + acss_debit: { + label: "ACSS Debit", + type: "acss_debit", + icon: GreekTemple, + duration: "5 business days", + }, + sepa_debit: { + label: "SEPA Debit", + type: "sepa_debit", + icon: GreekTemple, + duration: "5 business days", + }, +}); + +type SelectPaymentMethod = + (typeof PAYMENT_METHODS)[keyof typeof PAYMENT_METHODS] & { + id: string; + fee: number; + }; + function PayoutInvoiceSheetContent() { - const { id: workspaceId, slug, plan, defaultProgramId } = useWorkspace(); const { queryParams } = useRouterStuff(); + const { id: workspaceId, slug, plan, defaultProgramId } = useWorkspace(); + const { paymentMethods, loading: paymentMethodsLoading } = usePaymentMethods(); - const paymentMethodsTypes = Object.freeze({ - link: { - label: "link", - type: "link", - icon: CreditCard, - fee: PAYOUT_FEES[plan?.split(" ")[0] ?? "business"].card, - duration: "Instantly", - }, - card: { - label: "card", - type: "card", - icon: CreditCard, - fee: PAYOUT_FEES[plan?.split(" ")[0] ?? "business"].card, - duration: "Instantly", - }, - us_bank_account: { - label: "ACH", - type: "us_bank_account", - icon: GreekTemple, - fee: PAYOUT_FEES[plan?.split(" ")[0] ?? "business"].ach, - duration: "4 business days", - }, - }); - - type PaymentMethodWithFee = - (typeof paymentMethodsTypes)[keyof typeof paymentMethodsTypes] & { - id: string; - }; - const [selectedPaymentMethod, setSelectedPaymentMethod] = - useState(null); + useState(null); const [cutoffPeriod, setCutoffPeriod] = useState("today"); @@ -104,35 +117,52 @@ function PayoutInvoiceSheetContent() { }, }); - // Set the first payment method as the selected payment method - useEffect(() => { - if (!paymentMethods || !paymentMethods.length) { - return; - } + const finalPaymentMethods = useMemo( + () => + paymentMethods?.map((pm) => { + const paymentMethod = PAYMENT_METHODS[pm.type]; - if (!selectedPaymentMethod) { - const firstPaymentMethod = paymentMethods[0]; - setSelectedPaymentMethod({ - ...paymentMethodsTypes[firstPaymentMethod.type], - id: firstPaymentMethod.id, - }); - } - }, [paymentMethods, selectedPaymentMethod]); + const base = { + ...paymentMethod, + id: pm.id, + fee: calculatePayoutFee({ + paymentMethod: pm.type, + plan, + }), + }; - const paymentMethodsWithFee = useMemo( - () => - paymentMethods?.map((pm) => ({ - ...paymentMethodsTypes[pm.type], - id: pm.id, - title: pm.link - ? `Link – ${truncate(pm.link.email, 24)}` - : pm.card - ? `${capitalize(pm.card?.brand)} **** ${pm.card?.last4}` - : `ACH **** ${pm.us_bank_account?.last4}`, - })), - [paymentMethods], + if (pm.link) { + return { + ...base, + title: `Link – ${truncate(pm.link.email, 16)}`, + }; + } + + if (pm.card) { + return { + ...base, + title: `${capitalize(pm.card.brand)} **** ${pm.card.last4}`, + }; + } + + return { + ...base, + title: `${paymentMethod.label} **** ${pm[paymentMethod.type]?.last4}`, + }; + }), + [paymentMethods, plan], ); + useEffect(() => { + if ( + !selectedPaymentMethod && + finalPaymentMethods && + finalPaymentMethods.length > 0 + ) { + setSelectedPaymentMethod(finalPaymentMethods[0]); + } + }, [finalPaymentMethods, selectedPaymentMethod]); + const amount = useMemo( () => eligiblePayouts?.reduce((acc, payout) => { @@ -162,13 +192,13 @@ function PayoutInvoiceSheetContent() { value={selectedPaymentMethod?.id || ""} onChange={(e) => setSelectedPaymentMethod( - paymentMethodsWithFee?.find( + finalPaymentMethods?.find( (pm) => pm.id === e.target.value, ) || null, ) } > - {paymentMethodsWithFee?.map(({ id, title }) => ( + {finalPaymentMethods?.map(({ id, title }) => ( @@ -237,7 +267,7 @@ function PayoutInvoiceSheetContent() { ), tooltipContent: selectedPaymentMethod ? (