@myzonerocks/pact 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/adapters/paypal.d.ts +10 -2
- package/dist/src/adapters/paypal.js +49 -0
- package/dist/src/adapters/stripe.d.ts +3 -0
- package/dist/src/adapters/stripe.js +11 -1
- package/dist/test/paypal.test.js +21 -0
- package/package.json +10 -3
- package/src/adapters/paypal.ts +70 -2
- package/src/adapters/stripe.ts +17 -1
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import type { Quote, Authorization, Settlement } from "../message.js";
|
|
2
2
|
import { RefundKind, type AdapterEvent } from "../adapter.js";
|
|
3
|
-
import type {
|
|
3
|
+
import type { PayOutLeg, InteractivePayInLeg, PayInCapabilities, PayOutCapabilities, CollectResult, DisburseResult, PayInPreparation } from "../leg.js";
|
|
4
4
|
export declare const ErrSignatureMismatch = "paypal: webhook signature does not verify";
|
|
5
5
|
export interface CreateOrderParams {
|
|
6
6
|
value: string;
|
|
7
7
|
currencyCode: string;
|
|
8
8
|
payee: string;
|
|
9
9
|
referenceId: string;
|
|
10
|
+
platformFee?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface Order {
|
|
13
|
+
id: string;
|
|
14
|
+
status: string;
|
|
15
|
+
approveUrl: string;
|
|
10
16
|
}
|
|
11
17
|
export interface Capture {
|
|
12
18
|
orderId: string;
|
|
@@ -33,6 +39,7 @@ export interface Refund {
|
|
|
33
39
|
}
|
|
34
40
|
export interface PaypalApi {
|
|
35
41
|
createAndCaptureOrder(params: CreateOrderParams): Promise<Capture>;
|
|
42
|
+
createOrder(params: CreateOrderParams): Promise<Order>;
|
|
36
43
|
sendPayout(params: PayoutParams): Promise<Payout>;
|
|
37
44
|
refundCapture(params: RefundParams): Promise<Refund>;
|
|
38
45
|
verifyWebhook(headers: Record<string, string[]>, body: Uint8Array): Promise<boolean>;
|
|
@@ -43,7 +50,7 @@ export interface PaypalConfig {
|
|
|
43
50
|
api: PaypalApi;
|
|
44
51
|
ids: () => string;
|
|
45
52
|
}
|
|
46
|
-
export declare class PaypalLeg implements
|
|
53
|
+
export declare class PaypalLeg implements InteractivePayInLeg, PayOutLeg {
|
|
47
54
|
readonly id: string;
|
|
48
55
|
private readonly currencies;
|
|
49
56
|
private readonly api;
|
|
@@ -53,6 +60,7 @@ export declare class PaypalLeg implements PayInLeg, PayOutLeg {
|
|
|
53
60
|
payInCapabilities(): PayInCapabilities;
|
|
54
61
|
payOutCapabilities(): PayOutCapabilities;
|
|
55
62
|
collect(intentId: string, quote: Quote, _auth: Authorization, deliverTo: string): Promise<CollectResult>;
|
|
63
|
+
prepare(intentId: string, quote: Quote, _auth: Authorization, deliverTo: string): Promise<PayInPreparation>;
|
|
56
64
|
disburse(intentId: string, quote: Quote, recipientRef: string): Promise<DisburseResult>;
|
|
57
65
|
refundIn(intentId: string, kind: RefundKind, reason: string): Promise<Settlement>;
|
|
58
66
|
reverseOut(_intentId: string, _reason: string): Promise<Settlement>;
|
|
@@ -52,6 +52,25 @@ export class PaypalLeg {
|
|
|
52
52
|
this.captures.set(intentId, capture.captureId);
|
|
53
53
|
return { providerRef: capture.captureId, received: quote.srcAmount.sub(quote.fees) };
|
|
54
54
|
}
|
|
55
|
+
// prepare creates an order the payer approves and captures on their own device
|
|
56
|
+
// through the PayPal or Venmo flow — the interactive pay-in path — so nothing is
|
|
57
|
+
// captured here. The order names the recipient as payee and our cut as a platform
|
|
58
|
+
// fee; the buyer's capture then fires PAYMENT.CAPTURE.COMPLETED, which advances
|
|
59
|
+
// the corridor through the same pay-in path as a server-side collection. The
|
|
60
|
+
// order id is the token the payer's PayPal buttons need.
|
|
61
|
+
async prepare(intentId, quote, _auth, deliverTo) {
|
|
62
|
+
const params = {
|
|
63
|
+
value: majorAmount(quote.srcAmount.minor(), quote.srcAmount.exponent),
|
|
64
|
+
currencyCode: quote.srcAmount.currency,
|
|
65
|
+
payee: deliverTo,
|
|
66
|
+
referenceId: referencePrefix + intentId,
|
|
67
|
+
};
|
|
68
|
+
if (!quote.fees.isZero()) {
|
|
69
|
+
params.platformFee = majorAmount(quote.fees.minor(), quote.fees.exponent);
|
|
70
|
+
}
|
|
71
|
+
const order = await this.api.createOrder(params);
|
|
72
|
+
return { providerRef: order.id, clientSecret: order.id, method: "paypal" };
|
|
73
|
+
}
|
|
55
74
|
// disburse sends a payout of the recipient's amount to recipientRef and returns
|
|
56
75
|
// the batch id.
|
|
57
76
|
async disburse(intentId, quote, recipientRef) {
|
|
@@ -199,6 +218,36 @@ class HttpPaypalApi {
|
|
|
199
218
|
}
|
|
200
219
|
return { orderId: captured.id ?? "", captureId: capture.id ?? "", status: capture.status ?? "" };
|
|
201
220
|
}
|
|
221
|
+
// createOrder creates an order the buyer approves and captures on their own
|
|
222
|
+
// device. It carries custom_id so the capture webhook ties back to the intent,
|
|
223
|
+
// and a platform fee via payment_instruction when one is set.
|
|
224
|
+
async createOrder(params) {
|
|
225
|
+
const unit = {
|
|
226
|
+
reference_id: params.referenceId,
|
|
227
|
+
custom_id: params.referenceId,
|
|
228
|
+
amount: { currency_code: params.currencyCode, value: params.value },
|
|
229
|
+
};
|
|
230
|
+
if (params.payee) {
|
|
231
|
+
unit.payee = { email_address: params.payee };
|
|
232
|
+
}
|
|
233
|
+
if (params.platformFee) {
|
|
234
|
+
unit.payment_instruction = {
|
|
235
|
+
platform_fees: [{ amount: { currency_code: params.currencyCode, value: params.platformFee } }],
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
const created = await this.postJSON("/v2/checkout/orders", {
|
|
239
|
+
intent: "CAPTURE",
|
|
240
|
+
purchase_units: [unit],
|
|
241
|
+
});
|
|
242
|
+
let approve = "";
|
|
243
|
+
for (const link of created.links ?? []) {
|
|
244
|
+
if (link.rel === "approve" || link.rel === "payer-action") {
|
|
245
|
+
approve = link.href ?? "";
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return { id: created.id ?? "", status: created.status ?? "", approveUrl: approve };
|
|
250
|
+
}
|
|
202
251
|
async sendPayout(params) {
|
|
203
252
|
const out = await this.postJSON("/v1/payments/payouts", {
|
|
204
253
|
sender_batch_header: { sender_batch_id: params.referenceId },
|
|
@@ -26,6 +26,7 @@ export interface CreateIntentParams {
|
|
|
26
26
|
destination: string;
|
|
27
27
|
applicationFee?: number;
|
|
28
28
|
capture?: Capture;
|
|
29
|
+
methods?: string[];
|
|
29
30
|
metadata: Record<string, string>;
|
|
30
31
|
}
|
|
31
32
|
export interface CreateRefundParams {
|
|
@@ -47,6 +48,7 @@ export interface StripeConfig {
|
|
|
47
48
|
clock?: () => number;
|
|
48
49
|
tolerance?: number;
|
|
49
50
|
ids: () => string;
|
|
51
|
+
methods?: string[];
|
|
50
52
|
}
|
|
51
53
|
export declare class StripeLeg implements InteractivePayInLeg {
|
|
52
54
|
readonly id: string;
|
|
@@ -56,6 +58,7 @@ export declare class StripeLeg implements InteractivePayInLeg {
|
|
|
56
58
|
private readonly clock;
|
|
57
59
|
private readonly tolerance;
|
|
58
60
|
private readonly ids;
|
|
61
|
+
private readonly methods;
|
|
59
62
|
constructor(cfg: StripeConfig);
|
|
60
63
|
payInCapabilities(): PayInCapabilities;
|
|
61
64
|
collect(intentId: string, quote: Quote, _auth: Authorization, deliverTo: string): Promise<CollectResult>;
|
|
@@ -43,6 +43,7 @@ export class StripeLeg {
|
|
|
43
43
|
clock;
|
|
44
44
|
tolerance;
|
|
45
45
|
ids;
|
|
46
|
+
methods;
|
|
46
47
|
constructor(cfg) {
|
|
47
48
|
if (!cfg.webhookKey) {
|
|
48
49
|
throw new Error("stripe: config requires a webhook signing key");
|
|
@@ -54,6 +55,7 @@ export class StripeLeg {
|
|
|
54
55
|
this.clock = cfg.clock ?? (() => 0);
|
|
55
56
|
this.tolerance = cfg.tolerance ?? defaultToleranceSeconds;
|
|
56
57
|
this.ids = cfg.ids;
|
|
58
|
+
this.methods = cfg.methods;
|
|
57
59
|
}
|
|
58
60
|
// payInCapabilities advertises the card rail and the wallets that fund through
|
|
59
61
|
// it. The methods are informational; the corridor routes on the rail.
|
|
@@ -96,6 +98,7 @@ export class StripeLeg {
|
|
|
96
98
|
destination: deliverTo,
|
|
97
99
|
applicationFee: minorToInteger(quote.fees),
|
|
98
100
|
capture: Capture.OnConfirmation,
|
|
101
|
+
...(this.methods ? { methods: this.methods } : {}),
|
|
99
102
|
metadata: { [metadataIntentKey]: intentId },
|
|
100
103
|
}, idempotencyKey(intentId, "prepare"));
|
|
101
104
|
return { providerRef: pi.id, clientSecret: pi.clientSecret, method: "card" };
|
|
@@ -277,7 +280,14 @@ class HttpStripeApi {
|
|
|
277
280
|
if ((params.capture ?? Capture.Manual) === Capture.Manual) {
|
|
278
281
|
form.set("capture_method", "manual");
|
|
279
282
|
}
|
|
280
|
-
|
|
283
|
+
// Pinning the method types offers exactly those and nothing else; without a
|
|
284
|
+
// pin, dynamic payment methods surface whatever the dashboard has enabled.
|
|
285
|
+
if (params.methods && params.methods.length > 0) {
|
|
286
|
+
params.methods.forEach((m, i) => form.set(`payment_method_types[${i}]`, m));
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
form.set("automatic_payment_methods[enabled]", "true");
|
|
290
|
+
}
|
|
281
291
|
if (params.destination) {
|
|
282
292
|
form.set("transfer_data[destination]", params.destination);
|
|
283
293
|
}
|
package/dist/test/paypal.test.js
CHANGED
|
@@ -33,6 +33,10 @@ class FakePaypal {
|
|
|
33
33
|
const id = this.ids();
|
|
34
34
|
return { orderId: `order_${id}`, captureId: `capture_${id}`, status: "COMPLETED" };
|
|
35
35
|
}
|
|
36
|
+
async createOrder(params) {
|
|
37
|
+
this.lastOrder = params;
|
|
38
|
+
return { id: `order_${this.ids()}`, status: "CREATED", approveUrl: "https://paypal.test/approve" };
|
|
39
|
+
}
|
|
36
40
|
async sendPayout(params) {
|
|
37
41
|
this.lastPayout = params;
|
|
38
42
|
return { batchId: `batch_${this.ids()}`, status: "PENDING" };
|
|
@@ -127,6 +131,23 @@ describe("paypal direct corridor", () => {
|
|
|
127
131
|
await expect(leg.refundIn(intent.id, RefundKind.Partial, "half")).rejects.toThrow();
|
|
128
132
|
});
|
|
129
133
|
});
|
|
134
|
+
describe("paypal interactive pay-in", () => {
|
|
135
|
+
it("prepares an order to the recipient payee with our cut as a platform fee", async () => {
|
|
136
|
+
const api = new FakePaypal(counter("tx"));
|
|
137
|
+
const leg = buildLeg(api);
|
|
138
|
+
const prep = await leg.prepare("intent-1", quote({ srcAmount: Money.parse("USD", 2, "1500"), fees: Money.parse("USD", 2, "45") }), { intentId: "intent-1", quoteId: "q", signerIdentity: "alice", signature: new Uint8Array(0), signedAt: 0 }, "merchant@example.com");
|
|
139
|
+
expect(prep.providerRef).not.toBe("");
|
|
140
|
+
expect(prep.clientSecret).toBe(prep.providerRef);
|
|
141
|
+
expect(prep.method).toBe("paypal");
|
|
142
|
+
expect(api.lastOrder.payee).toBe("merchant@example.com");
|
|
143
|
+
expect(api.lastOrder.value).toBe("15.00");
|
|
144
|
+
expect(api.lastOrder.platformFee).toBe("0.45");
|
|
145
|
+
expect(api.lastOrder.referenceId).toBe("pact:intent-1");
|
|
146
|
+
// A zero fee omits the platform fee rather than sending "0.00".
|
|
147
|
+
await leg.prepare("intent-2", quote({ srcAmount: Money.parse("USD", 2, "1500"), fees: Money.parse("USD", 2, "0") }), { intentId: "intent-2", quoteId: "q", signerIdentity: "alice", signature: new Uint8Array(0), signedAt: 0 }, "merchant@example.com");
|
|
148
|
+
expect(api.lastOrder.platformFee).toBeUndefined();
|
|
149
|
+
});
|
|
150
|
+
});
|
|
130
151
|
describe("paypal pay-out", () => {
|
|
131
152
|
it("sends the recipient's amount and refuses a reversal", async () => {
|
|
132
153
|
const api = new FakePaypal(counter("tx"));
|
package/package.json
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@myzonerocks/pact",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "TypeScript SDK for the PACT payment abstraction protocol",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
|
-
"repository": {
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/myzonerocks/pact.git",
|
|
9
|
+
"directory": "sdk/ts"
|
|
10
|
+
},
|
|
7
11
|
"type": "module",
|
|
8
12
|
"main": "./dist/src/index.js",
|
|
9
13
|
"types": "./dist/src/index.d.ts",
|
|
@@ -33,7 +37,10 @@
|
|
|
33
37
|
"import": "./dist/src/policy.js"
|
|
34
38
|
}
|
|
35
39
|
},
|
|
36
|
-
"files": [
|
|
40
|
+
"files": [
|
|
41
|
+
"dist",
|
|
42
|
+
"src"
|
|
43
|
+
],
|
|
37
44
|
"scripts": {
|
|
38
45
|
"build": "tsc -p tsconfig.json",
|
|
39
46
|
"test": "vitest run",
|
package/src/adapters/paypal.ts
CHANGED
|
@@ -8,12 +8,13 @@ import { State } from "../state.js";
|
|
|
8
8
|
import type { Quote, Authorization, Settlement } from "../message.js";
|
|
9
9
|
import { RefundKind, type AdapterEvent } from "../adapter.js";
|
|
10
10
|
import type {
|
|
11
|
-
PayInLeg,
|
|
12
11
|
PayOutLeg,
|
|
12
|
+
InteractivePayInLeg,
|
|
13
13
|
PayInCapabilities,
|
|
14
14
|
PayOutCapabilities,
|
|
15
15
|
CollectResult,
|
|
16
16
|
DisburseResult,
|
|
17
|
+
PayInPreparation,
|
|
17
18
|
} from "../leg.js";
|
|
18
19
|
|
|
19
20
|
// The public base of the PayPal REST API. It is the same for every live
|
|
@@ -36,6 +37,18 @@ export interface CreateOrderParams {
|
|
|
36
37
|
currencyCode: string;
|
|
37
38
|
payee: string;
|
|
38
39
|
referenceId: string;
|
|
40
|
+
// platformFee, when set, is the decimal-major cut the platform takes from the
|
|
41
|
+
// payee's proceeds.
|
|
42
|
+
platformFee?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Order is a created-but-not-yet-captured order: the buyer approves it on their own
|
|
46
|
+
// device, then it is captured. approveUrl is PayPal's hosted approval link, offered
|
|
47
|
+
// for hosts that redirect rather than drive the JS SDK.
|
|
48
|
+
export interface Order {
|
|
49
|
+
id: string;
|
|
50
|
+
status: string;
|
|
51
|
+
approveUrl: string;
|
|
39
52
|
}
|
|
40
53
|
|
|
41
54
|
// Capture is the result of capturing an order: the capture id is the reference a
|
|
@@ -80,6 +93,9 @@ export interface Refund {
|
|
|
80
93
|
// endpoint, so the verification secret never lives in this package.
|
|
81
94
|
export interface PaypalApi {
|
|
82
95
|
createAndCaptureOrder(params: CreateOrderParams): Promise<Capture>;
|
|
96
|
+
// createOrder creates an order the buyer approves and captures on their own
|
|
97
|
+
// device — the interactive pay-in path — rather than capturing it server-side.
|
|
98
|
+
createOrder(params: CreateOrderParams): Promise<Order>;
|
|
83
99
|
sendPayout(params: PayoutParams): Promise<Payout>;
|
|
84
100
|
refundCapture(params: RefundParams): Promise<Refund>;
|
|
85
101
|
verifyWebhook(headers: Record<string, string[]>, body: Uint8Array): Promise<boolean>;
|
|
@@ -93,7 +109,7 @@ export interface PaypalConfig {
|
|
|
93
109
|
}
|
|
94
110
|
|
|
95
111
|
// PaypalLeg moves money over PayPal, serving both sides of a corridor.
|
|
96
|
-
export class PaypalLeg implements
|
|
112
|
+
export class PaypalLeg implements InteractivePayInLeg, PayOutLeg {
|
|
97
113
|
readonly id: string;
|
|
98
114
|
private readonly currencies: string[];
|
|
99
115
|
private readonly api: PaypalApi;
|
|
@@ -134,6 +150,26 @@ export class PaypalLeg implements PayInLeg, PayOutLeg {
|
|
|
134
150
|
return { providerRef: capture.captureId, received: quote.srcAmount.sub(quote.fees) };
|
|
135
151
|
}
|
|
136
152
|
|
|
153
|
+
// prepare creates an order the payer approves and captures on their own device
|
|
154
|
+
// through the PayPal or Venmo flow — the interactive pay-in path — so nothing is
|
|
155
|
+
// captured here. The order names the recipient as payee and our cut as a platform
|
|
156
|
+
// fee; the buyer's capture then fires PAYMENT.CAPTURE.COMPLETED, which advances
|
|
157
|
+
// the corridor through the same pay-in path as a server-side collection. The
|
|
158
|
+
// order id is the token the payer's PayPal buttons need.
|
|
159
|
+
async prepare(intentId: string, quote: Quote, _auth: Authorization, deliverTo: string): Promise<PayInPreparation> {
|
|
160
|
+
const params: CreateOrderParams = {
|
|
161
|
+
value: majorAmount(quote.srcAmount.minor(), quote.srcAmount.exponent),
|
|
162
|
+
currencyCode: quote.srcAmount.currency,
|
|
163
|
+
payee: deliverTo,
|
|
164
|
+
referenceId: referencePrefix + intentId,
|
|
165
|
+
};
|
|
166
|
+
if (!quote.fees.isZero()) {
|
|
167
|
+
params.platformFee = majorAmount(quote.fees.minor(), quote.fees.exponent);
|
|
168
|
+
}
|
|
169
|
+
const order = await this.api.createOrder(params);
|
|
170
|
+
return { providerRef: order.id, clientSecret: order.id, method: "paypal" };
|
|
171
|
+
}
|
|
172
|
+
|
|
137
173
|
// disburse sends a payout of the recipient's amount to recipientRef and returns
|
|
138
174
|
// the batch id.
|
|
139
175
|
async disburse(intentId: string, quote: Quote, recipientRef: string): Promise<DisburseResult> {
|
|
@@ -310,6 +346,37 @@ class HttpPaypalApi implements PaypalApi {
|
|
|
310
346
|
return { orderId: captured.id ?? "", captureId: capture.id ?? "", status: capture.status ?? "" };
|
|
311
347
|
}
|
|
312
348
|
|
|
349
|
+
// createOrder creates an order the buyer approves and captures on their own
|
|
350
|
+
// device. It carries custom_id so the capture webhook ties back to the intent,
|
|
351
|
+
// and a platform fee via payment_instruction when one is set.
|
|
352
|
+
async createOrder(params: CreateOrderParams): Promise<Order> {
|
|
353
|
+
const unit: Record<string, unknown> = {
|
|
354
|
+
reference_id: params.referenceId,
|
|
355
|
+
custom_id: params.referenceId,
|
|
356
|
+
amount: { currency_code: params.currencyCode, value: params.value },
|
|
357
|
+
};
|
|
358
|
+
if (params.payee) {
|
|
359
|
+
unit.payee = { email_address: params.payee };
|
|
360
|
+
}
|
|
361
|
+
if (params.platformFee) {
|
|
362
|
+
unit.payment_instruction = {
|
|
363
|
+
platform_fees: [{ amount: { currency_code: params.currencyCode, value: params.platformFee } }],
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
const created = await this.postJSON<PaypalOrder>("/v2/checkout/orders", {
|
|
367
|
+
intent: "CAPTURE",
|
|
368
|
+
purchase_units: [unit],
|
|
369
|
+
});
|
|
370
|
+
let approve = "";
|
|
371
|
+
for (const link of created.links ?? []) {
|
|
372
|
+
if (link.rel === "approve" || link.rel === "payer-action") {
|
|
373
|
+
approve = link.href ?? "";
|
|
374
|
+
break;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return { id: created.id ?? "", status: created.status ?? "", approveUrl: approve };
|
|
378
|
+
}
|
|
379
|
+
|
|
313
380
|
async sendPayout(params: PayoutParams): Promise<Payout> {
|
|
314
381
|
const out = await this.postJSON<{ batch_header?: { payout_batch_id?: string; batch_status?: string } }>(
|
|
315
382
|
"/v1/payments/payouts",
|
|
@@ -377,6 +444,7 @@ interface PaypalOrder {
|
|
|
377
444
|
id?: string;
|
|
378
445
|
status?: string;
|
|
379
446
|
purchase_units?: Array<{ payments?: { captures?: Array<{ id?: string; status?: string }> } }>;
|
|
447
|
+
links?: Array<{ href?: string; rel?: string }>;
|
|
380
448
|
}
|
|
381
449
|
|
|
382
450
|
function firstCapture(order: PaypalOrder): { id?: string; status?: string } | undefined {
|
package/src/adapters/stripe.ts
CHANGED
|
@@ -75,6 +75,10 @@ export interface CreateIntentParams {
|
|
|
75
75
|
destination: string;
|
|
76
76
|
applicationFee?: number;
|
|
77
77
|
capture?: Capture;
|
|
78
|
+
// methods pins the exact payment method types offered (e.g. card, cashapp, link);
|
|
79
|
+
// wallets like Apple Pay and Google Pay ride on "card". Empty falls back to the
|
|
80
|
+
// account's dynamic payment methods.
|
|
81
|
+
methods?: string[];
|
|
78
82
|
metadata: Record<string, string>;
|
|
79
83
|
}
|
|
80
84
|
|
|
@@ -110,6 +114,9 @@ export interface StripeConfig {
|
|
|
110
114
|
// tolerance is the webhook timestamp tolerance in seconds; defaults to 300.
|
|
111
115
|
tolerance?: number;
|
|
112
116
|
ids: () => string;
|
|
117
|
+
// methods pins the payment method types offered on the card rail (e.g. card,
|
|
118
|
+
// cashapp, link). Empty offers the account's dynamic payment methods.
|
|
119
|
+
methods?: string[];
|
|
113
120
|
}
|
|
114
121
|
|
|
115
122
|
// StripeLeg collects card payments through Stripe. It exposes the wallets that
|
|
@@ -123,6 +130,7 @@ export class StripeLeg implements InteractivePayInLeg {
|
|
|
123
130
|
private readonly clock: () => number;
|
|
124
131
|
private readonly tolerance: number;
|
|
125
132
|
private readonly ids: () => string;
|
|
133
|
+
private readonly methods: string[] | undefined;
|
|
126
134
|
|
|
127
135
|
constructor(cfg: StripeConfig) {
|
|
128
136
|
if (!cfg.webhookKey) {
|
|
@@ -135,6 +143,7 @@ export class StripeLeg implements InteractivePayInLeg {
|
|
|
135
143
|
this.clock = cfg.clock ?? (() => 0);
|
|
136
144
|
this.tolerance = cfg.tolerance ?? defaultToleranceSeconds;
|
|
137
145
|
this.ids = cfg.ids;
|
|
146
|
+
this.methods = cfg.methods;
|
|
138
147
|
}
|
|
139
148
|
|
|
140
149
|
// payInCapabilities advertises the card rail and the wallets that fund through
|
|
@@ -184,6 +193,7 @@ export class StripeLeg implements InteractivePayInLeg {
|
|
|
184
193
|
destination: deliverTo,
|
|
185
194
|
applicationFee: minorToInteger(quote.fees),
|
|
186
195
|
capture: Capture.OnConfirmation,
|
|
196
|
+
...(this.methods ? { methods: this.methods } : {}),
|
|
187
197
|
metadata: { [metadataIntentKey]: intentId },
|
|
188
198
|
},
|
|
189
199
|
idempotencyKey(intentId, "prepare"),
|
|
@@ -403,7 +413,13 @@ class HttpStripeApi implements StripeApi {
|
|
|
403
413
|
if ((params.capture ?? Capture.Manual) === Capture.Manual) {
|
|
404
414
|
form.set("capture_method", "manual");
|
|
405
415
|
}
|
|
406
|
-
|
|
416
|
+
// Pinning the method types offers exactly those and nothing else; without a
|
|
417
|
+
// pin, dynamic payment methods surface whatever the dashboard has enabled.
|
|
418
|
+
if (params.methods && params.methods.length > 0) {
|
|
419
|
+
params.methods.forEach((m, i) => form.set(`payment_method_types[${i}]`, m));
|
|
420
|
+
} else {
|
|
421
|
+
form.set("automatic_payment_methods[enabled]", "true");
|
|
422
|
+
}
|
|
407
423
|
if (params.destination) {
|
|
408
424
|
form.set("transfer_data[destination]", params.destination);
|
|
409
425
|
}
|