@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/README.md
CHANGED
|
@@ -103,21 +103,32 @@ import { PaymentAPI } from '@flopay/js';
|
|
|
103
103
|
|
|
104
104
|
const api = new PaymentAPI('https://billing.example.com');
|
|
105
105
|
|
|
106
|
-
// Fetch and normalize a checkout session
|
|
107
|
-
|
|
106
|
+
// Fetch and normalize a checkout session. `nonce` is the session-bound
|
|
107
|
+
// checkout token returned from session creation — required by post-#640
|
|
108
|
+
// backends, which match it against `checkout_session.nonce` before returning
|
|
109
|
+
// the row.
|
|
110
|
+
const session = await api.getUnifiedCheckoutSession('session_uuid', sessionNonce);
|
|
108
111
|
// session.provider === 'stripe' | 'chargebee' | 'recurly'
|
|
109
112
|
// session.data.session contains the normalized CheckoutSession
|
|
113
|
+
// session.data.session.clientSecret carries the same nonce so downstream
|
|
114
|
+
// code can re-use it.
|
|
110
115
|
|
|
111
|
-
// Create a PaymentIntent
|
|
116
|
+
// Create a PaymentIntent — nonce becomes the `x-checkout-session-token`
|
|
117
|
+
// header automatically.
|
|
112
118
|
const intentResponse = await api.createPaymentIntent(
|
|
113
119
|
'session_uuid',
|
|
114
120
|
'user@example.com',
|
|
115
121
|
'pm_xxx',
|
|
122
|
+
{ nonce: sessionNonce },
|
|
116
123
|
);
|
|
117
124
|
|
|
118
|
-
// Process a tokenized payment
|
|
125
|
+
// Process a tokenized payment. `nonce` is required: the SDK throws a
|
|
126
|
+
// `FloPayError` with code `MissingCheckoutSessionToken` when it is missing,
|
|
127
|
+
// and otherwise POSTs to `/v1/checkouts/sessions/<id>/process` with the
|
|
128
|
+
// header set.
|
|
119
129
|
const processResponse = await api.processPayment('user_id', {
|
|
120
130
|
sessionId: 'session_uuid',
|
|
131
|
+
nonce: sessionNonce,
|
|
121
132
|
tokenizedData: { id: 'pm_xxx', type: 'card' },
|
|
122
133
|
accountData: { userId: 'user_id', email: 'user@example.com', firstName: 'John', lastName: 'Doe' },
|
|
123
134
|
});
|
|
@@ -148,7 +159,13 @@ const result = await createCheckoutSession({
|
|
|
148
159
|
cancelUrl: '/cancel',
|
|
149
160
|
redirectParams: { email: 'user@example.com', bg: 'courses', mode: 'confirm' },
|
|
150
161
|
});
|
|
151
|
-
// On 201: browser redirects to checkout page
|
|
162
|
+
// On 201: browser redirects to checkout page. The resolved
|
|
163
|
+
// `CheckoutSessionResult` is `{ status: 201, redirectUrl, nonce }` — `nonce` is
|
|
164
|
+
// the session-bound checkout token returned by the billing API. A matching
|
|
165
|
+
// `flopay_checkout_token` cookie is written to the same parent domain as
|
|
166
|
+
// `checkout_data`, so a hosted-checkout page can read the token from
|
|
167
|
+
// `document.cookie` and forward it on continuation calls without having to
|
|
168
|
+
// pull it from the URL (kept out of history/Referer).
|
|
152
169
|
// On 204: browser redirects to successUrl (payment method on file)
|
|
153
170
|
```
|
|
154
171
|
|
package/dist/index.cjs
CHANGED
|
@@ -363,9 +363,11 @@ var StripeAdapter = class {
|
|
|
363
363
|
};
|
|
364
364
|
}
|
|
365
365
|
}
|
|
366
|
+
const intentHeaders = { "Content-Type": "application/json" };
|
|
367
|
+
if (params.nonce) intentHeaders["x-checkout-session-token"] = params.nonce;
|
|
366
368
|
const intentResponse = await fetch(`${baseUrl}/v1/checkouts/payments/intents`, {
|
|
367
369
|
method: "POST",
|
|
368
|
-
headers:
|
|
370
|
+
headers: intentHeaders,
|
|
369
371
|
body: JSON.stringify({
|
|
370
372
|
sessionId: params.sessionId,
|
|
371
373
|
email: params.email,
|
|
@@ -621,6 +623,20 @@ async function buildApiErrorFromResponse(response, fallbackMessage) {
|
|
|
621
623
|
statusCode: response.status
|
|
622
624
|
});
|
|
623
625
|
}
|
|
626
|
+
var NETWORK_RETRY_ATTEMPTS = 2;
|
|
627
|
+
async function fetchWithNetworkRetry(input, init, attempts = NETWORK_RETRY_ATTEMPTS) {
|
|
628
|
+
let lastErr;
|
|
629
|
+
for (let attempt = 0; ; attempt++) {
|
|
630
|
+
try {
|
|
631
|
+
return await fetch(input, init);
|
|
632
|
+
} catch (err) {
|
|
633
|
+
if (err instanceof Error && err.name === "AbortError") throw err;
|
|
634
|
+
lastErr = err;
|
|
635
|
+
if (attempt >= attempts) throw lastErr;
|
|
636
|
+
await delay(150 * 2 ** attempt);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
624
640
|
var PaymentAPI = class {
|
|
625
641
|
constructor(billingApiUrl) {
|
|
626
642
|
this.baseUrl = billingApiUrl.replace(/\/+$/, "");
|
|
@@ -630,15 +646,13 @@ var PaymentAPI = class {
|
|
|
630
646
|
*
|
|
631
647
|
* `nonce` is the session-bound checkout token returned when the session
|
|
632
648
|
* was created. When supplied it is sent as the `x-checkout-session-token`
|
|
633
|
-
* header
|
|
634
|
-
*
|
|
635
|
-
*
|
|
636
|
-
* session reads so the UUID alone is no longer sufficient to fetch a
|
|
637
|
-
* session. Backends that don't yet enforce it ignore the extra header.
|
|
649
|
+
* header that post-#640 backends match against `checkout_session.nonce`
|
|
650
|
+
* before returning the row — the UUID alone is no longer sufficient.
|
|
651
|
+
* Backends that don't yet enforce it ignore the extra header.
|
|
638
652
|
*/
|
|
639
653
|
async getCheckoutSession(checkoutSessionId, nonce) {
|
|
640
|
-
const response = await
|
|
641
|
-
`${this.baseUrl}/v1/checkouts/sessions/${checkoutSessionId}`,
|
|
654
|
+
const response = await fetchWithNetworkRetry(
|
|
655
|
+
`${this.baseUrl}/v1/checkouts/sessions/${encodeURIComponent(checkoutSessionId)}`,
|
|
642
656
|
nonce ? { headers: { "x-checkout-session-token": nonce } } : void 0
|
|
643
657
|
);
|
|
644
658
|
if (!response.ok) {
|
|
@@ -693,34 +707,58 @@ var PaymentAPI = class {
|
|
|
693
707
|
* The backend will either succeed, return `type: '3ds_required'`
|
|
694
708
|
* (with a `threeDSecureToken`), or return `type: 'paypal_redirect_required'`.
|
|
695
709
|
*
|
|
710
|
+
* Hits the session-scoped route `POST /v1/checkouts/sessions/:id/process`
|
|
711
|
+
* and forwards `data.nonce` as `x-checkout-session-token`. Backend
|
|
712
|
+
* `TeamFloPay/backend#640` rejects callers without a matching nonce with a
|
|
713
|
+
* 401 — this method throws synchronously when `data.nonce` is missing so the
|
|
714
|
+
* problem surfaces before the network round trip.
|
|
715
|
+
*
|
|
696
716
|
* @param userId Vestigial — backend's GatewayInterceptor routes via session,
|
|
697
717
|
* not headers, so this value is no longer sent on the wire. Kept in the
|
|
698
718
|
* signature for back-compat with existing callers; will be removed in a
|
|
699
719
|
* future major version.
|
|
700
720
|
*/
|
|
701
721
|
async processPayment(_userId, data, options) {
|
|
722
|
+
if (!data.nonce) {
|
|
723
|
+
throw new import_shared3.FloPayError(
|
|
724
|
+
"processPayment requires `nonce` \u2014 pass the value returned from session creation.",
|
|
725
|
+
"validation_error",
|
|
726
|
+
{ code: "MissingCheckoutSessionToken", param: "nonce" }
|
|
727
|
+
);
|
|
728
|
+
}
|
|
729
|
+
const { nonce, ...processBody } = data;
|
|
702
730
|
const response = await fetch(
|
|
703
|
-
`${this.baseUrl}/v1/checkouts/sessions/process`,
|
|
731
|
+
`${this.baseUrl}/v1/checkouts/sessions/${encodeURIComponent(data.sessionId)}/process`,
|
|
704
732
|
{
|
|
705
733
|
method: "POST",
|
|
706
|
-
headers: {
|
|
707
|
-
|
|
734
|
+
headers: {
|
|
735
|
+
"Content-Type": "application/json",
|
|
736
|
+
"x-checkout-session-token": nonce
|
|
737
|
+
},
|
|
738
|
+
body: JSON.stringify(processBody)
|
|
708
739
|
}
|
|
709
740
|
);
|
|
710
|
-
return this.resolveProcessResponse(response, data.sessionId, options);
|
|
741
|
+
return this.resolveProcessResponse(response, data.sessionId, { ...options, nonce });
|
|
711
742
|
}
|
|
712
743
|
/**
|
|
713
744
|
* Create a PaymentIntent on the backend.
|
|
714
745
|
*
|
|
715
746
|
* Used by the Stripe flow to create a server-side PaymentIntent
|
|
716
747
|
* with the client's payment method attached.
|
|
748
|
+
*
|
|
749
|
+
* Forwards `options.nonce` as `x-checkout-session-token` when supplied — the
|
|
750
|
+
* session-bound checkout token returned by session creation. Post-#640
|
|
751
|
+
* backends reject this call with a 401 when the header is missing or does
|
|
752
|
+
* not match the session's stored nonce.
|
|
717
753
|
*/
|
|
718
754
|
async createPaymentIntent(sessionId, email, paymentMethodType, options) {
|
|
755
|
+
const headers = { "Content-Type": "application/json" };
|
|
756
|
+
if (options?.nonce) headers["x-checkout-session-token"] = options.nonce;
|
|
719
757
|
return fetch(
|
|
720
758
|
`${this.baseUrl}/v1/checkouts/payments/intents`,
|
|
721
759
|
{
|
|
722
760
|
method: "POST",
|
|
723
|
-
headers
|
|
761
|
+
headers,
|
|
724
762
|
body: JSON.stringify({
|
|
725
763
|
sessionId,
|
|
726
764
|
email,
|
|
@@ -733,13 +771,18 @@ var PaymentAPI = class {
|
|
|
733
771
|
}
|
|
734
772
|
/**
|
|
735
773
|
* Create a SetupIntent for saving payment methods.
|
|
774
|
+
*
|
|
775
|
+
* Forwards `options.nonce` as `x-checkout-session-token` when supplied —
|
|
776
|
+
* required by post-#640 backends, ignored by earlier versions.
|
|
736
777
|
*/
|
|
737
778
|
async createSetupIntent(sessionId, email, paymentMethodType, options) {
|
|
779
|
+
const headers = { "Content-Type": "application/json" };
|
|
780
|
+
if (options?.nonce) headers["x-checkout-session-token"] = options.nonce;
|
|
738
781
|
return fetch(
|
|
739
782
|
`${this.baseUrl}/v1/checkouts/payments/setup-intents`,
|
|
740
783
|
{
|
|
741
784
|
method: "POST",
|
|
742
|
-
headers
|
|
785
|
+
headers,
|
|
743
786
|
body: JSON.stringify({ sessionId, email, paymentMethodType }),
|
|
744
787
|
signal: options?.signal
|
|
745
788
|
}
|
|
@@ -824,7 +867,7 @@ var PaymentAPI = class {
|
|
|
824
867
|
if (params.checkoutType) payload["checkoutType"] = params.checkoutType;
|
|
825
868
|
if (params.checkoutLayout) payload["checkoutLayout"] = params.checkoutLayout;
|
|
826
869
|
if (params.avsConfig) payload["avsConfig"] = params.avsConfig;
|
|
827
|
-
const response = await
|
|
870
|
+
const response = await fetchWithNetworkRetry(
|
|
828
871
|
`${this.baseUrl}/v1/checkouts/sessions?expand=true`,
|
|
829
872
|
{
|
|
830
873
|
method: "POST",
|
|
@@ -881,7 +924,7 @@ var PaymentAPI = class {
|
|
|
881
924
|
throw createCheckoutProcessingTimeoutError();
|
|
882
925
|
}
|
|
883
926
|
}
|
|
884
|
-
const session = await this.getUnifiedCheckoutSession(checkoutSessionId);
|
|
927
|
+
const session = await this.getUnifiedCheckoutSession(checkoutSessionId, options?.nonce);
|
|
885
928
|
const status = session.data.session?.status;
|
|
886
929
|
if (status === "complete" || status === "expired") {
|
|
887
930
|
return session;
|
|
@@ -1002,7 +1045,8 @@ var PaymentAPI = class {
|
|
|
1002
1045
|
const pending = this.toCheckoutProcessingPending(payload, response, checkoutSessionId);
|
|
1003
1046
|
const session = await this.waitForCheckoutSessionCompletion(pending.sessionId, {
|
|
1004
1047
|
initialDelayMs: pending.retryAfterMs,
|
|
1005
|
-
timeoutMs: options?.pollTimeoutMs
|
|
1048
|
+
timeoutMs: options?.pollTimeoutMs,
|
|
1049
|
+
nonce: options?.nonce
|
|
1006
1050
|
});
|
|
1007
1051
|
if (session.data.session?.status === "complete") {
|
|
1008
1052
|
return new Response(null, { status: 204, statusText: "No Content" });
|
|
@@ -1340,9 +1384,17 @@ async function createCheckoutSession(options) {
|
|
|
1340
1384
|
}
|
|
1341
1385
|
if (status === 201) {
|
|
1342
1386
|
const uuid = body?.data?.uuid;
|
|
1387
|
+
const nonce = body?.data?.nonce;
|
|
1343
1388
|
if (!uuid) {
|
|
1344
1389
|
throw new Error("Checkout session created but no UUID was returned by the billing API");
|
|
1345
1390
|
}
|
|
1391
|
+
if (!nonce) {
|
|
1392
|
+
throw new import_shared6.FloPayError(
|
|
1393
|
+
"Checkout session created but no `nonce` was returned by the billing API. Upgrade the billing service to TeamFloPay/backend#640 or later.",
|
|
1394
|
+
"api_error",
|
|
1395
|
+
{ code: "MissingCheckoutSessionToken" }
|
|
1396
|
+
);
|
|
1397
|
+
}
|
|
1346
1398
|
if (wireProducts.length || sessionCurrency) {
|
|
1347
1399
|
cacheSessionDisplayData(uuid, {
|
|
1348
1400
|
currency: sessionCurrency,
|
|
@@ -1365,11 +1417,12 @@ async function createCheckoutSession(options) {
|
|
|
1365
1417
|
const checkoutData = JSON.stringify({ origin_url: cancelUrl });
|
|
1366
1418
|
const domain = window.location.hostname.split(".").slice(-2).join(".");
|
|
1367
1419
|
document.cookie = `checkout_data=${encodeURIComponent(checkoutData)}; domain=.${domain}; path=/; max-age=3600; SameSite=Lax; Secure;`;
|
|
1420
|
+
document.cookie = `flopay_checkout_token=${encodeURIComponent(nonce)}; domain=.${domain}; path=/; max-age=3600; SameSite=Lax; Secure;`;
|
|
1368
1421
|
}
|
|
1369
1422
|
if (typeof window !== "undefined") {
|
|
1370
1423
|
window.location.href = redirectUrl.toString();
|
|
1371
1424
|
}
|
|
1372
|
-
return { status: 201, redirectUrl: redirectUrl.toString() };
|
|
1425
|
+
return { status: 201, redirectUrl: redirectUrl.toString(), nonce };
|
|
1373
1426
|
}
|
|
1374
1427
|
if (status === 204) {
|
|
1375
1428
|
if (typeof window !== "undefined") {
|