Skip to content

Commit 8075ae8

Browse files
committed
Add stablecoin payout support with modal and country validation
1 parent 261b3b8 commit 8075ae8

File tree

7 files changed

+237
-147
lines changed

7 files changed

+237
-147
lines changed

apps/web/lib/actions/partners/generate-stripe-recipient-account-link.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { throwIfNoPermission } from "@/lib/auth/partner-users/throw-if-no-permis
44
import { createStripeRecipientAccount } from "@/lib/stripe/create-stripe-recipient-account";
55
import { createStripeRecipientAccountLink } from "@/lib/stripe/create-stripe-recipient-account-link";
66
import { prisma } from "@dub/prisma";
7+
import { COUNTRIES, STABLECOIN_SUPPORTED_COUNTRIES } from "@dub/utils";
78
import { authPartnerActionClient } from "../safe-action";
89

910
export const generateStripeRecipientAccountLink =
@@ -28,6 +29,12 @@ export const generateStripeRecipientAccountLink =
2829
);
2930
}
3031

32+
if (!STABLECOIN_SUPPORTED_COUNTRIES.includes(partner.country)) {
33+
throw new Error(
34+
`Your current country (${COUNTRIES[partner.country]}) is not supported for Stablecoin payouts. Please go to partners.dub.co/settings to update your country, or contact support.`,
35+
);
36+
}
37+
3138
const recipientAccount = await createStripeRecipientAccount({
3239
name: partner.name,
3340
email: partner.email,

apps/web/ui/partners/bank-account-requirements-modal.tsx renamed to apps/web/ui/partners/payouts/bank-account-requirements-modal.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { Button, Modal } from "@dub/ui";
55
import { TriangleWarning } from "@dub/ui/icons";
66
import { COUNTRIES, COUNTRY_CURRENCY_CODES } from "@dub/utils";
77
import { Dispatch, SetStateAction, useMemo, useState } from "react";
8-
import { Markdown } from "../shared/markdown";
8+
import { Markdown } from "../../shared/markdown";
99

1010
function BankAccountRequirementsModal({
1111
showModal,

apps/web/ui/partners/payouts/connect-payout-button.tsx

Lines changed: 109 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -1,122 +1,123 @@
11
"use client";
22

3+
import { generatePaypalOAuthUrl } from "@/lib/actions/partners/generate-paypal-oauth-url";
4+
import { generateStripeAccountLink } from "@/lib/actions/partners/generate-stripe-account-link";
5+
import { generateStripeRecipientAccountLink } from "@/lib/actions/partners/generate-stripe-recipient-account-link";
36
import { hasPermission } from "@/lib/auth/partner-users/partner-user-permissions";
47
import usePartnerProfile from "@/lib/swr/use-partner-profile";
8+
import { useBankAccountRequirementsModal } from "@/ui/partners/payouts/bank-account-requirements-modal";
59
import { useSelectPayoutMethodModal } from "@/ui/partners/payouts/select-payout-method-modal";
10+
import { useStablecoinPayoutModal } from "@/ui/partners/payouts/stablecoin-payout-modal";
611
import { PartnerPayoutMethod } from "@dub/prisma/client";
712
import { Button, ButtonProps, TooltipContent } from "@dub/ui";
813
import { COUNTRIES } from "@dub/utils";
9-
import { useMemo } from "react";
14+
import { useAction } from "next-safe-action/hooks";
15+
import { useRouter } from "next/navigation";
16+
import { useCallback, useMemo } from "react";
17+
import { toast } from "sonner";
18+
19+
interface ConnectPayoutButtonProps extends ButtonProps {
20+
payoutMethod?: PartnerPayoutMethod;
21+
}
1022

1123
export function ConnectPayoutButton({
12-
payoutMethodType,
24+
payoutMethod,
1325
...props
14-
}: ButtonProps & { payoutMethodType?: PartnerPayoutMethod }) {
26+
}: ConnectPayoutButtonProps) {
27+
const router = useRouter();
1528
const { partner, availablePayoutMethods } = usePartnerProfile();
1629

30+
const {
31+
executeAsync: executeStripeConnect,
32+
isPending: isStripeConnectPending,
33+
} = useAction(generateStripeAccountLink, {
34+
onSuccess: ({ data }) => {
35+
router.push(data.url);
36+
},
37+
onError: ({ error }) => {
38+
toast.error(error.serverError);
39+
},
40+
});
41+
42+
const {
43+
executeAsync: executeStablecoinConnect,
44+
isPending: isStablecoinConnectPending,
45+
} = useAction(generateStripeRecipientAccountLink, {
46+
onSuccess: ({ data }) => {
47+
router.push(data.url);
48+
},
49+
onError: ({ error }) => {
50+
toast.error(error.serverError);
51+
},
52+
});
53+
54+
const {
55+
executeAsync: executePaypalConnect,
56+
isPending: isPaypalConnectPending,
57+
} = useAction(generatePaypalOAuthUrl, {
58+
onSuccess: ({ data }) => {
59+
router.push(data.url);
60+
},
61+
onError: ({ error }) => {
62+
toast.error(error.serverError);
63+
},
64+
});
65+
1766
const { setShowSelectPayoutMethodModal, SelectPayoutMethodModal } =
1867
useSelectPayoutMethodModal();
1968

20-
// const { executeAsync: executeStripeAsync, isPending: isStripePending } =
21-
// useAction(generateStripeAccountLink, {
22-
// onSuccess: ({ data }) => {
23-
// if (!data?.url) {
24-
// toast.error("Unable to create account link. Please contact support.");
25-
// return;
26-
// }
27-
// router.push(data.url);
28-
// },
29-
// onError: ({ error }) => {
30-
// toast.error(error.serverError);
31-
// },
32-
// });
33-
34-
// const { executeAsync: executePaypalAsync, isPending: isPaypalPending } =
35-
// useAction(generatePaypalOAuthUrl, {
36-
// onSuccess: ({ data }) => {
37-
// if (!data?.url) {
38-
// toast.error("Unable to redirect to Paypal. Please contact support.");
39-
// return;
40-
// }
41-
// router.push(data.url);
42-
// },
43-
// onError: ({ error }) => {
44-
// toast.error(error.serverError);
45-
// },
46-
// });
47-
48-
// const connectPayout = useCallback(async () => {
49-
// if (!partner) {
50-
// toast.error("Invalid partner profile. Please log out and log back in.");
51-
// return;
52-
// }
53-
54-
// if (!partner.country) {
55-
// toast.error(
56-
// "You haven't set your country yet. Please go to partners.dub.co/settings to set your country.",
57-
// );
58-
// }
59-
60-
// if (payoutMethod === "paypal") {
61-
// await executePaypalAsync();
62-
// } else if (payoutMethod === "stripe") {
63-
// await executeStripeAsync();
64-
// } else {
65-
// toast.error(
66-
// "Your country is not supported for payout. Please go to partners.dub.co/settings to update your country, or contact support.",
67-
// );
68-
// return;
69-
// }
70-
// }, [executeStripeAsync, executePaypalAsync, partner]);
71-
72-
// const { setShowBankAccountRequirementsModal, BankAccountRequirementsModal } =
73-
// useBankAccountRequirementsModal({
74-
// onContinue: connectPayout,
75-
// });
76-
77-
// const { setShowSelectPayoutMethodModal, SelectPayoutMethodModal } =
78-
// useSelectPayoutMethodModal();
79-
80-
// const handleClick = useCallback(() => {
81-
// if (payoutMethodType === "paypal") {
82-
// executePaypalAsync();
83-
// return;
84-
// }
85-
86-
// if (payoutMethod === "paypal" && !payoutMethodType) {
87-
// connectPayout();
88-
// return;
89-
// }
90-
91-
// if (
92-
// payoutMethod === "stripe" ||
93-
// availablePayoutMethods?.includes("crypto") ||
94-
// availablePayoutMethods?.includes("bankAccount")
95-
// ) {
96-
// if (payoutMethodType === "crypto") {
97-
// connectPayout();
98-
// } else if (payoutMethodType === "bankAccount") {
99-
// setShowBankAccountRequirementsModal(true);
100-
// } else if (!payoutMethodType) {
101-
// setShowSelectPayoutMethodModal(true);
102-
// } else {
103-
// connectPayout();
104-
// }
105-
// return;
106-
// }
107-
108-
// toast.error(
109-
// "Your country is not supported for payout. Please go to partners.dub.co/settings to update your country, or contact support.",
110-
// );
111-
// }, [
112-
// connectPayout,
113-
// executePaypalAsync,
114-
// // payoutMethod,
115-
// // payoutMethodType,
116-
// availablePayoutMethods,
117-
// setShowBankAccountRequirementsModal,
118-
// setShowSelectPayoutMethodModal,
119-
// ]);
69+
const { setShowBankAccountRequirementsModal, BankAccountRequirementsModal } =
70+
useBankAccountRequirementsModal({
71+
onContinue: async () => {
72+
await executeStripeConnect();
73+
},
74+
});
75+
76+
const { setShowStablecoinPayoutModal, StablecoinPayoutModal } =
77+
useStablecoinPayoutModal({
78+
onContinue: async () => {
79+
await executeStablecoinConnect();
80+
},
81+
});
82+
83+
const handleClick = useCallback(() => {
84+
if (payoutMethod === "connect") {
85+
setShowSelectPayoutMethodModal(false);
86+
setShowBankAccountRequirementsModal(true);
87+
return;
88+
}
89+
90+
if (payoutMethod === "stablecoin") {
91+
setShowSelectPayoutMethodModal(false);
92+
setShowStablecoinPayoutModal(true);
93+
return;
94+
}
95+
96+
if (payoutMethod === "paypal") {
97+
executePaypalConnect();
98+
return;
99+
}
100+
101+
setShowSelectPayoutMethodModal(true);
102+
}, [
103+
payoutMethod,
104+
setShowSelectPayoutMethodModal,
105+
setShowBankAccountRequirementsModal,
106+
setShowStablecoinPayoutModal,
107+
executePaypalConnect,
108+
]);
109+
110+
const isPending = useMemo(
111+
() =>
112+
isStripeConnectPending ||
113+
isStablecoinConnectPending ||
114+
isPaypalConnectPending,
115+
[
116+
isStripeConnectPending,
117+
isStablecoinConnectPending,
118+
isPaypalConnectPending,
119+
],
120+
);
120121

121122
const errorMessage = useMemo(
122123
() =>
@@ -135,9 +136,12 @@ export function ConnectPayoutButton({
135136
return (
136137
<>
137138
{SelectPayoutMethodModal}
139+
{BankAccountRequirementsModal}
140+
{StablecoinPayoutModal}
138141
<Button
139-
onClick={() => setShowSelectPayoutMethodModal(true)}
140-
text="Connect payout"
142+
onClick={handleClick}
143+
text={payoutMethod ? "Connect" : "Connect payout"}
144+
loading={isPending}
141145
disabledTooltip={
142146
errorMessage && (
143147
<TooltipContent

apps/web/ui/partners/payouts/payout-method-selector.tsx

Lines changed: 7 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,33 @@
11
"use client";
22

33
import usePartnerProfile from "@/lib/swr/use-partner-profile";
4-
import { PartnerPayoutMethod } from "@dub/prisma/client";
54
import {
65
Badge,
76
CircleDollar,
87
GreekTemple,
98
Paypal,
10-
StripeStablecoinIcon,
9+
StablecoinIcon,
1110
} from "@dub/ui";
1211
import { Calendar, Globe, MapPin, Zap } from "lucide-react";
1312
import { ReactNode } from "react";
1413
import { ConnectPayoutButton } from "./connect-payout-button";
1514

15+
// TODO:
16+
// Combine icon and iconSingle
17+
1618
const PAYOUT_METHODS = [
1719
{
1820
id: "stablecoin" as const,
1921
title: "Stablecoin",
2022
recommended: true,
2123
icon: (
2224
<div className="flex size-10 items-center justify-center rounded-lg border border-[#1717170D] bg-[#EDE9FE]">
23-
<StripeStablecoinIcon className="size-5" />
25+
<StablecoinIcon className="size-5" />
2426
</div>
2527
),
2628
iconSingle: (
2729
<div className="flex size-14 items-center justify-center rounded-lg border border-[#1717170D] bg-[#EDE9FE]">
28-
<StripeStablecoinIcon className="size-8" />
30+
<StablecoinIcon className="size-8" />
2931
</div>
3032
),
3133
features: [
@@ -163,43 +165,9 @@ export function PayoutMethodSelector() {
163165
recommended={method.recommended}
164166
action={
165167
<ConnectPayoutButton
168+
payoutMethod={method.id}
166169
text="Connect"
167170
className="h-9 w-full rounded-lg"
168-
payoutMethodType={method.id}
169-
/>
170-
}
171-
isSingle={isSingleOption}
172-
/>
173-
))}
174-
</div>
175-
);
176-
}
177-
178-
export function StripePayoutMethodOptions({
179-
availableMethods,
180-
}: {
181-
availableMethods: PartnerPayoutMethod[];
182-
}) {
183-
const filteredMethods = PAYOUT_METHODS.filter((m) =>
184-
availableMethods.includes(m.id),
185-
);
186-
187-
const isSingleOption = filteredMethods.length === 1;
188-
189-
return (
190-
<div className={isSingleOption ? "w-full" : "grid gap-3 sm:grid-cols-2"}>
191-
{filteredMethods.map((method) => (
192-
<PayoutMethodCard
193-
key={method.id}
194-
icon={isSingleOption ? method.iconSingle : method.icon}
195-
title={method.title}
196-
features={method.features}
197-
recommended={method.recommended}
198-
action={
199-
<ConnectPayoutButton
200-
text="Connect"
201-
className="h-9 w-full rounded-lg"
202-
payoutMethodType={method.id}
203171
/>
204172
}
205173
isSingle={isSingleOption}

0 commit comments

Comments
 (0)