@flopay/js 1.2.0 → 1.2.4
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/README.md +22 -5
- package/dist/index.cjs +71 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +31 -5
- package/dist/index.d.ts +31 -5
- package/dist/index.mjs +71 -18
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -64,6 +64,12 @@ declare class FloPay {
|
|
|
64
64
|
sessionId: string;
|
|
65
65
|
email: string;
|
|
66
66
|
returnUrl: string;
|
|
67
|
+
/**
|
|
68
|
+
* Session-bound checkout token. Forwarded as `x-checkout-session-token`
|
|
69
|
+
* on the internal `POST /v1/checkouts/payments/intents` call so post-#640
|
|
70
|
+
* backends don't reject the round-trip with a 401.
|
|
71
|
+
*/
|
|
72
|
+
nonce?: string;
|
|
67
73
|
}): Promise<ConfirmCardPaymentResult>;
|
|
68
74
|
/** Resume a PayPal payment after redirect return. Returns null if no PayPal params in URL. */
|
|
69
75
|
resumePayPalPayment(): Promise<ConfirmCardPaymentResult | null>;
|
|
@@ -148,6 +154,7 @@ declare class StripeAdapter implements PaymentProviderAdapter {
|
|
|
148
154
|
sessionId: string;
|
|
149
155
|
email: string;
|
|
150
156
|
returnUrl: string;
|
|
157
|
+
nonce?: string;
|
|
151
158
|
}): Promise<ConfirmCardPaymentResult>;
|
|
152
159
|
resumePayPalPayment(): Promise<ConfirmCardPaymentResult | null>;
|
|
153
160
|
getRawProvider(): unknown;
|
|
@@ -281,11 +288,9 @@ declare class PaymentAPI {
|
|
|
281
288
|
*
|
|
282
289
|
* `nonce` is the session-bound checkout token returned when the session
|
|
283
290
|
* was created. When supplied it is sent as the `x-checkout-session-token`
|
|
284
|
-
* header
|
|
285
|
-
*
|
|
286
|
-
*
|
|
287
|
-
* session reads so the UUID alone is no longer sufficient to fetch a
|
|
288
|
-
* session. Backends that don't yet enforce it ignore the extra header.
|
|
291
|
+
* header that post-#640 backends match against `checkout_session.nonce`
|
|
292
|
+
* before returning the row — the UUID alone is no longer sufficient.
|
|
293
|
+
* Backends that don't yet enforce it ignore the extra header.
|
|
289
294
|
*/
|
|
290
295
|
getCheckoutSession(checkoutSessionId: string, nonce?: string): Promise<BillingResponse<RawCheckoutSession>>;
|
|
291
296
|
/**
|
|
@@ -329,6 +334,12 @@ declare class PaymentAPI {
|
|
|
329
334
|
* The backend will either succeed, return `type: '3ds_required'`
|
|
330
335
|
* (with a `threeDSecureToken`), or return `type: 'paypal_redirect_required'`.
|
|
331
336
|
*
|
|
337
|
+
* Hits the session-scoped route `POST /v1/checkouts/sessions/:id/process`
|
|
338
|
+
* and forwards `data.nonce` as `x-checkout-session-token`. Backend
|
|
339
|
+
* `TeamFloPay/backend#640` rejects callers without a matching nonce with a
|
|
340
|
+
* 401 — this method throws synchronously when `data.nonce` is missing so the
|
|
341
|
+
* problem surfaces before the network round trip.
|
|
342
|
+
*
|
|
332
343
|
* @param userId Vestigial — backend's GatewayInterceptor routes via session,
|
|
333
344
|
* not headers, so this value is no longer sent on the wire. Kept in the
|
|
334
345
|
* signature for back-compat with existing callers; will be removed in a
|
|
@@ -342,16 +353,26 @@ declare class PaymentAPI {
|
|
|
342
353
|
*
|
|
343
354
|
* Used by the Stripe flow to create a server-side PaymentIntent
|
|
344
355
|
* with the client's payment method attached.
|
|
356
|
+
*
|
|
357
|
+
* Forwards `options.nonce` as `x-checkout-session-token` when supplied — the
|
|
358
|
+
* session-bound checkout token returned by session creation. Post-#640
|
|
359
|
+
* backends reject this call with a 401 when the header is missing or does
|
|
360
|
+
* not match the session's stored nonce.
|
|
345
361
|
*/
|
|
346
362
|
createPaymentIntent(sessionId: string, email: string, paymentMethodType: string, options?: {
|
|
347
363
|
signal?: AbortSignal;
|
|
348
364
|
isPaypal?: string;
|
|
365
|
+
nonce?: string;
|
|
349
366
|
}): Promise<Response>;
|
|
350
367
|
/**
|
|
351
368
|
* Create a SetupIntent for saving payment methods.
|
|
369
|
+
*
|
|
370
|
+
* Forwards `options.nonce` as `x-checkout-session-token` when supplied —
|
|
371
|
+
* required by post-#640 backends, ignored by earlier versions.
|
|
352
372
|
*/
|
|
353
373
|
createSetupIntent(sessionId: string, email: string, paymentMethodType: 'card' | 'google_pay' | 'apple_pay' | 'paypal_express_checkout', options?: {
|
|
354
374
|
signal?: AbortSignal;
|
|
375
|
+
nonce?: string;
|
|
355
376
|
}): Promise<Response>;
|
|
356
377
|
/**
|
|
357
378
|
* Fetch user's prior payments by email.
|
|
@@ -380,6 +401,11 @@ declare class PaymentAPI {
|
|
|
380
401
|
waitForCheckoutSessionCompletion(checkoutSessionId: string, options?: {
|
|
381
402
|
initialDelayMs?: number;
|
|
382
403
|
timeoutMs?: number;
|
|
404
|
+
/**
|
|
405
|
+
* Session-bound checkout token; forwarded on the poll's
|
|
406
|
+
* `GET /v1/checkouts/sessions/:id`. Required by post-#640 backends.
|
|
407
|
+
*/
|
|
408
|
+
nonce?: string;
|
|
383
409
|
}): Promise<NormalizedCheckoutSession>;
|
|
384
410
|
/** Normalize a raw session into a provider-agnostic shape. */
|
|
385
411
|
private normalizeRawSession;
|
package/dist/index.mjs
CHANGED
|
@@ -318,9 +318,11 @@ var StripeAdapter = class {
|
|
|
318
318
|
};
|
|
319
319
|
}
|
|
320
320
|
}
|
|
321
|
+
const intentHeaders = { "Content-Type": "application/json" };
|
|
322
|
+
if (params.nonce) intentHeaders["x-checkout-session-token"] = params.nonce;
|
|
321
323
|
const intentResponse = await fetch(`${baseUrl}/v1/checkouts/payments/intents`, {
|
|
322
324
|
method: "POST",
|
|
323
|
-
headers:
|
|
325
|
+
headers: intentHeaders,
|
|
324
326
|
body: JSON.stringify({
|
|
325
327
|
sessionId: params.sessionId,
|
|
326
328
|
email: params.email,
|
|
@@ -582,6 +584,20 @@ async function buildApiErrorFromResponse(response, fallbackMessage) {
|
|
|
582
584
|
statusCode: response.status
|
|
583
585
|
});
|
|
584
586
|
}
|
|
587
|
+
var NETWORK_RETRY_ATTEMPTS = 2;
|
|
588
|
+
async function fetchWithNetworkRetry(input, init, attempts = NETWORK_RETRY_ATTEMPTS) {
|
|
589
|
+
let lastErr;
|
|
590
|
+
for (let attempt = 0; ; attempt++) {
|
|
591
|
+
try {
|
|
592
|
+
return await fetch(input, init);
|
|
593
|
+
} catch (err) {
|
|
594
|
+
if (err instanceof Error && err.name === "AbortError") throw err;
|
|
595
|
+
lastErr = err;
|
|
596
|
+
if (attempt >= attempts) throw lastErr;
|
|
597
|
+
await delay(150 * 2 ** attempt);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
585
601
|
var PaymentAPI = class {
|
|
586
602
|
constructor(billingApiUrl) {
|
|
587
603
|
this.baseUrl = billingApiUrl.replace(/\/+$/, "");
|
|
@@ -591,15 +607,13 @@ var PaymentAPI = class {
|
|
|
591
607
|
*
|
|
592
608
|
* `nonce` is the session-bound checkout token returned when the session
|
|
593
609
|
* was created. When supplied it is sent as the `x-checkout-session-token`
|
|
594
|
-
* header
|
|
595
|
-
*
|
|
596
|
-
*
|
|
597
|
-
* session reads so the UUID alone is no longer sufficient to fetch a
|
|
598
|
-
* session. Backends that don't yet enforce it ignore the extra header.
|
|
610
|
+
* header that post-#640 backends match against `checkout_session.nonce`
|
|
611
|
+
* before returning the row — the UUID alone is no longer sufficient.
|
|
612
|
+
* Backends that don't yet enforce it ignore the extra header.
|
|
599
613
|
*/
|
|
600
614
|
async getCheckoutSession(checkoutSessionId, nonce) {
|
|
601
|
-
const response = await
|
|
602
|
-
`${this.baseUrl}/v1/checkouts/sessions/${checkoutSessionId}`,
|
|
615
|
+
const response = await fetchWithNetworkRetry(
|
|
616
|
+
`${this.baseUrl}/v1/checkouts/sessions/${encodeURIComponent(checkoutSessionId)}`,
|
|
603
617
|
nonce ? { headers: { "x-checkout-session-token": nonce } } : void 0
|
|
604
618
|
);
|
|
605
619
|
if (!response.ok) {
|
|
@@ -654,34 +668,58 @@ var PaymentAPI = class {
|
|
|
654
668
|
* The backend will either succeed, return `type: '3ds_required'`
|
|
655
669
|
* (with a `threeDSecureToken`), or return `type: 'paypal_redirect_required'`.
|
|
656
670
|
*
|
|
671
|
+
* Hits the session-scoped route `POST /v1/checkouts/sessions/:id/process`
|
|
672
|
+
* and forwards `data.nonce` as `x-checkout-session-token`. Backend
|
|
673
|
+
* `TeamFloPay/backend#640` rejects callers without a matching nonce with a
|
|
674
|
+
* 401 — this method throws synchronously when `data.nonce` is missing so the
|
|
675
|
+
* problem surfaces before the network round trip.
|
|
676
|
+
*
|
|
657
677
|
* @param userId Vestigial — backend's GatewayInterceptor routes via session,
|
|
658
678
|
* not headers, so this value is no longer sent on the wire. Kept in the
|
|
659
679
|
* signature for back-compat with existing callers; will be removed in a
|
|
660
680
|
* future major version.
|
|
661
681
|
*/
|
|
662
682
|
async processPayment(_userId, data, options) {
|
|
683
|
+
if (!data.nonce) {
|
|
684
|
+
throw new FloPayError3(
|
|
685
|
+
"processPayment requires `nonce` \u2014 pass the value returned from session creation.",
|
|
686
|
+
"validation_error",
|
|
687
|
+
{ code: "MissingCheckoutSessionToken", param: "nonce" }
|
|
688
|
+
);
|
|
689
|
+
}
|
|
690
|
+
const { nonce, ...processBody } = data;
|
|
663
691
|
const response = await fetch(
|
|
664
|
-
`${this.baseUrl}/v1/checkouts/sessions/process`,
|
|
692
|
+
`${this.baseUrl}/v1/checkouts/sessions/${encodeURIComponent(data.sessionId)}/process`,
|
|
665
693
|
{
|
|
666
694
|
method: "POST",
|
|
667
|
-
headers: {
|
|
668
|
-
|
|
695
|
+
headers: {
|
|
696
|
+
"Content-Type": "application/json",
|
|
697
|
+
"x-checkout-session-token": nonce
|
|
698
|
+
},
|
|
699
|
+
body: JSON.stringify(processBody)
|
|
669
700
|
}
|
|
670
701
|
);
|
|
671
|
-
return this.resolveProcessResponse(response, data.sessionId, options);
|
|
702
|
+
return this.resolveProcessResponse(response, data.sessionId, { ...options, nonce });
|
|
672
703
|
}
|
|
673
704
|
/**
|
|
674
705
|
* Create a PaymentIntent on the backend.
|
|
675
706
|
*
|
|
676
707
|
* Used by the Stripe flow to create a server-side PaymentIntent
|
|
677
708
|
* with the client's payment method attached.
|
|
709
|
+
*
|
|
710
|
+
* Forwards `options.nonce` as `x-checkout-session-token` when supplied — the
|
|
711
|
+
* session-bound checkout token returned by session creation. Post-#640
|
|
712
|
+
* backends reject this call with a 401 when the header is missing or does
|
|
713
|
+
* not match the session's stored nonce.
|
|
678
714
|
*/
|
|
679
715
|
async createPaymentIntent(sessionId, email, paymentMethodType, options) {
|
|
716
|
+
const headers = { "Content-Type": "application/json" };
|
|
717
|
+
if (options?.nonce) headers["x-checkout-session-token"] = options.nonce;
|
|
680
718
|
return fetch(
|
|
681
719
|
`${this.baseUrl}/v1/checkouts/payments/intents`,
|
|
682
720
|
{
|
|
683
721
|
method: "POST",
|
|
684
|
-
headers
|
|
722
|
+
headers,
|
|
685
723
|
body: JSON.stringify({
|
|
686
724
|
sessionId,
|
|
687
725
|
email,
|
|
@@ -694,13 +732,18 @@ var PaymentAPI = class {
|
|
|
694
732
|
}
|
|
695
733
|
/**
|
|
696
734
|
* Create a SetupIntent for saving payment methods.
|
|
735
|
+
*
|
|
736
|
+
* Forwards `options.nonce` as `x-checkout-session-token` when supplied —
|
|
737
|
+
* required by post-#640 backends, ignored by earlier versions.
|
|
697
738
|
*/
|
|
698
739
|
async createSetupIntent(sessionId, email, paymentMethodType, options) {
|
|
740
|
+
const headers = { "Content-Type": "application/json" };
|
|
741
|
+
if (options?.nonce) headers["x-checkout-session-token"] = options.nonce;
|
|
699
742
|
return fetch(
|
|
700
743
|
`${this.baseUrl}/v1/checkouts/payments/setup-intents`,
|
|
701
744
|
{
|
|
702
745
|
method: "POST",
|
|
703
|
-
headers
|
|
746
|
+
headers,
|
|
704
747
|
body: JSON.stringify({ sessionId, email, paymentMethodType }),
|
|
705
748
|
signal: options?.signal
|
|
706
749
|
}
|
|
@@ -785,7 +828,7 @@ var PaymentAPI = class {
|
|
|
785
828
|
if (params.checkoutType) payload["checkoutType"] = params.checkoutType;
|
|
786
829
|
if (params.checkoutLayout) payload["checkoutLayout"] = params.checkoutLayout;
|
|
787
830
|
if (params.avsConfig) payload["avsConfig"] = params.avsConfig;
|
|
788
|
-
const response = await
|
|
831
|
+
const response = await fetchWithNetworkRetry(
|
|
789
832
|
`${this.baseUrl}/v1/checkouts/sessions?expand=true`,
|
|
790
833
|
{
|
|
791
834
|
method: "POST",
|
|
@@ -842,7 +885,7 @@ var PaymentAPI = class {
|
|
|
842
885
|
throw createCheckoutProcessingTimeoutError();
|
|
843
886
|
}
|
|
844
887
|
}
|
|
845
|
-
const session = await this.getUnifiedCheckoutSession(checkoutSessionId);
|
|
888
|
+
const session = await this.getUnifiedCheckoutSession(checkoutSessionId, options?.nonce);
|
|
846
889
|
const status = session.data.session?.status;
|
|
847
890
|
if (status === "complete" || status === "expired") {
|
|
848
891
|
return session;
|
|
@@ -963,7 +1006,8 @@ var PaymentAPI = class {
|
|
|
963
1006
|
const pending = this.toCheckoutProcessingPending(payload, response, checkoutSessionId);
|
|
964
1007
|
const session = await this.waitForCheckoutSessionCompletion(pending.sessionId, {
|
|
965
1008
|
initialDelayMs: pending.retryAfterMs,
|
|
966
|
-
timeoutMs: options?.pollTimeoutMs
|
|
1009
|
+
timeoutMs: options?.pollTimeoutMs,
|
|
1010
|
+
nonce: options?.nonce
|
|
967
1011
|
});
|
|
968
1012
|
if (session.data.session?.status === "complete") {
|
|
969
1013
|
return new Response(null, { status: 204, statusText: "No Content" });
|
|
@@ -1307,9 +1351,17 @@ async function createCheckoutSession(options) {
|
|
|
1307
1351
|
}
|
|
1308
1352
|
if (status === 201) {
|
|
1309
1353
|
const uuid = body?.data?.uuid;
|
|
1354
|
+
const nonce = body?.data?.nonce;
|
|
1310
1355
|
if (!uuid) {
|
|
1311
1356
|
throw new Error("Checkout session created but no UUID was returned by the billing API");
|
|
1312
1357
|
}
|
|
1358
|
+
if (!nonce) {
|
|
1359
|
+
throw new FloPayError6(
|
|
1360
|
+
"Checkout session created but no `nonce` was returned by the billing API. Upgrade the billing service to TeamFloPay/backend#640 or later.",
|
|
1361
|
+
"api_error",
|
|
1362
|
+
{ code: "MissingCheckoutSessionToken" }
|
|
1363
|
+
);
|
|
1364
|
+
}
|
|
1313
1365
|
if (wireProducts.length || sessionCurrency) {
|
|
1314
1366
|
cacheSessionDisplayData(uuid, {
|
|
1315
1367
|
currency: sessionCurrency,
|
|
@@ -1332,11 +1384,12 @@ async function createCheckoutSession(options) {
|
|
|
1332
1384
|
const checkoutData = JSON.stringify({ origin_url: cancelUrl });
|
|
1333
1385
|
const domain = window.location.hostname.split(".").slice(-2).join(".");
|
|
1334
1386
|
document.cookie = `checkout_data=${encodeURIComponent(checkoutData)}; domain=.${domain}; path=/; max-age=3600; SameSite=Lax; Secure;`;
|
|
1387
|
+
document.cookie = `flopay_checkout_token=${encodeURIComponent(nonce)}; domain=.${domain}; path=/; max-age=3600; SameSite=Lax; Secure;`;
|
|
1335
1388
|
}
|
|
1336
1389
|
if (typeof window !== "undefined") {
|
|
1337
1390
|
window.location.href = redirectUrl.toString();
|
|
1338
1391
|
}
|
|
1339
|
-
return { status: 201, redirectUrl: redirectUrl.toString() };
|
|
1392
|
+
return { status: 201, redirectUrl: redirectUrl.toString(), nonce };
|
|
1340
1393
|
}
|
|
1341
1394
|
if (status === 204) {
|
|
1342
1395
|
if (typeof window !== "undefined") {
|