Skip to content

Commit a494493

Browse files
committed
Stablecoin payouts: account link useCase, collection_options, recommended UI, backfill
1 parent 2ef3f55 commit a494493

File tree

6 files changed

+107
-28
lines changed

6 files changed

+107
-28
lines changed

apps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/partner-payout-settings-sheet.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,13 +207,26 @@ function PayoutMethodsSection() {
207207
return null;
208208
}
209209

210+
// Show stablecoin as a recommended option when available but not yet connected, to encourage partners to add it
211+
const showStablecoinRecommended =
212+
availablePayoutMethods.includes("stablecoin") &&
213+
!payoutMethodsData?.some((m) => m.type === "stablecoin" && m.connected);
214+
210215
return (
211216
<div>
212217
<h4 className="text-content-emphasis mb-3 text-base font-semibold leading-6">
213218
Payout account
214219
</h4>
215220
{hasConnectedAccount ? (
216-
<PayoutMethodsDropdown />
221+
<div className="space-y-3">
222+
<PayoutMethodsDropdown />
223+
{showStablecoinRecommended && (
224+
<PayoutMethodSelector
225+
payoutMethods={["stablecoin"]}
226+
variant="compact"
227+
/>
228+
)}
229+
</div>
217230
) : (
218231
<PayoutMethodSelector
219232
payoutMethods={currentMethod ? [currentMethod.id] : []}

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ export const generateStripeRecipientAccountLink =
1818
permission: "payout_settings.update",
1919
});
2020

21+
let useCase: "account_onboarding" | "account_update" = "account_update";
22+
2123
if (!partner.stripeRecipientId) {
2224
if (!partner.email) {
2325
throw new Error(
@@ -58,11 +60,13 @@ export const generateStripeRecipientAccountLink =
5860
stripeRecipientId: recipientAccount.id,
5961
},
6062
});
63+
64+
useCase = "account_onboarding";
6165
}
6266

6367
const accountLink = await createStripeRecipientAccountLink({
64-
stripeRecipientId: partner.stripeRecipientId,
65-
useCase: "account_update",
68+
stripeRecipientId: partner.stripeRecipientId!,
69+
useCase,
6670
});
6771

6872
return {

apps/web/lib/stripe/create-stripe-recipient-account-link.ts

Lines changed: 11 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -10,32 +10,20 @@ export async function createStripeRecipientAccountLink({
1010
stripeRecipientId,
1111
useCase,
1212
}: CreateStripeRecipientAccountLinkParams) {
13-
const returnUrl = `${PARTNERS_DOMAIN}/payouts`;
14-
const refreshUrl = `${PARTNERS_DOMAIN}/payouts`;
15-
16-
const useCaseConfig =
17-
useCase === "account_onboarding"
18-
? {
19-
type: "account_onboarding" as const,
20-
account_onboarding: {
21-
configurations: ["recipient" as const],
22-
return_url: returnUrl,
23-
refresh_url: refreshUrl,
24-
},
25-
}
26-
: {
27-
type: "account_update" as const,
28-
account_update: {
29-
configurations: ["recipient" as const],
30-
return_url: returnUrl,
31-
refresh_url: refreshUrl,
32-
},
33-
};
34-
3513
const { data, error } = await stripeV2Fetch("/v2/core/account_links", {
3614
body: {
3715
account: stripeRecipientId,
38-
use_case: useCaseConfig,
16+
use_case: {
17+
type: useCase,
18+
[useCase]: {
19+
configurations: ["recipient"],
20+
return_url: `${PARTNERS_DOMAIN}/payouts`,
21+
refresh_url: `${PARTNERS_DOMAIN}/payouts`,
22+
collection_options: {
23+
fields: "eventually_due",
24+
},
25+
},
26+
},
3927
},
4028
});
4129

apps/web/lib/stripe/create-stripe-recipient-account.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ import type { Partner } from "@dub/prisma/client";
22
import { stripeV2Fetch } from "./stripe-v2-client";
33

44
interface CreateStripeRecipientAccountParams
5-
extends Pick<Partner, "name" | "country" | "profileType"> {
5+
extends Pick<Partner, "name" | "profileType"> {
66
email: NonNullable<Partner["email"]>;
7+
country: NonNullable<Partner["country"]>;
78
}
89

910
export async function createStripeRecipientAccount({
@@ -17,7 +18,7 @@ export async function createStripeRecipientAccount({
1718
contact_email: email,
1819
display_name: name,
1920
identity: {
20-
country: (country ?? "us").toLowerCase(),
21+
country: country.toLowerCase(),
2122
entity_type: profileType,
2223
},
2324
configuration: {

apps/web/lib/stripe/stripe-v2-schemas.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ export const createAccountLinkInputSchema = z.object({
3333
configurations: z.array(z.literal("recipient")),
3434
refresh_url: z.url(),
3535
return_url: z.url().optional(),
36+
collection_options: z.object({
37+
fields: z.enum(["currently_due", "eventually_due"]),
38+
}),
3639
})
3740
.optional(),
3841
account_update: z
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { prisma } from "@dub/prisma";
2+
import { PartnerPayoutMethod } from "@dub/prisma/client";
3+
import "dotenv-flow/config";
4+
5+
const BATCH_SIZE = 100;
6+
7+
async function main() {
8+
let cursor: string | undefined;
9+
let totalUpdated = 0;
10+
11+
while (true) {
12+
const partners = await prisma.partner.findMany({
13+
where: {
14+
defaultPayoutMethod: null,
15+
},
16+
select: {
17+
id: true,
18+
stripeConnectId: true,
19+
stripeRecipientId: true,
20+
paypalEmail: true,
21+
},
22+
...(cursor
23+
? {
24+
skip: 1,
25+
cursor: {
26+
id: cursor,
27+
},
28+
}
29+
: {}),
30+
take: BATCH_SIZE,
31+
orderBy: {
32+
id: "asc",
33+
},
34+
});
35+
36+
if (partners.length === 0) {
37+
break;
38+
}
39+
40+
for (const partner of partners) {
41+
let defaultPayoutMethod: PartnerPayoutMethod | null = null;
42+
43+
if (partner.stripeConnectId) {
44+
defaultPayoutMethod = PartnerPayoutMethod.connect;
45+
} else if (partner.paypalEmail) {
46+
defaultPayoutMethod = PartnerPayoutMethod.paypal;
47+
} else if (partner.stripeRecipientId) {
48+
defaultPayoutMethod = PartnerPayoutMethod.stablecoin;
49+
}
50+
51+
if (defaultPayoutMethod) {
52+
await prisma.partner.update({
53+
where: {
54+
id: partner.id,
55+
defaultPayoutMethod: null,
56+
},
57+
data: {
58+
defaultPayoutMethod,
59+
},
60+
});
61+
}
62+
}
63+
64+
cursor = partners[partners.length - 1].id;
65+
}
66+
67+
console.log("Backfill finished.");
68+
}
69+
70+
main();

0 commit comments

Comments
 (0)