@flopay/js 1.2.1 → 1.2.5
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 +24 -7
- package/dist/index.cjs +65 -17
- 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 +65 -17
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -11
package/README.md
CHANGED
|
@@ -7,10 +7,10 @@ Currently backed by Stripe via the `StripeAdapter`. The adapter pattern (`Paymen
|
|
|
7
7
|
## Installation
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
pnpm add @flopay/js
|
|
10
|
+
pnpm add @flopay/js
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
`@stripe/stripe-js`
|
|
13
|
+
`@stripe/stripe-js` ships as a direct dependency of `@flopay/js` — consumers do not need to install it separately.
|
|
14
14
|
|
|
15
15
|
## Quick Start
|
|
16
16
|
|
|
@@ -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
|
@@ -108,7 +108,16 @@ var StripeAdapter = class {
|
|
|
108
108
|
if (typeof window === "undefined") {
|
|
109
109
|
return;
|
|
110
110
|
}
|
|
111
|
-
|
|
111
|
+
let loadStripe;
|
|
112
|
+
try {
|
|
113
|
+
({ loadStripe } = await import("@stripe/stripe-js"));
|
|
114
|
+
} catch {
|
|
115
|
+
throw new import_shared.FloPayError(
|
|
116
|
+
"Failed to load @stripe/stripe-js. Reinstall @flopay/js.",
|
|
117
|
+
"api_error",
|
|
118
|
+
{ code: "StripeJsLoadFailed" }
|
|
119
|
+
);
|
|
120
|
+
}
|
|
112
121
|
const stripe = await loadStripe(config.publishableKey, {
|
|
113
122
|
locale: config.locale ?? "auto"
|
|
114
123
|
});
|
|
@@ -363,9 +372,11 @@ var StripeAdapter = class {
|
|
|
363
372
|
};
|
|
364
373
|
}
|
|
365
374
|
}
|
|
375
|
+
const intentHeaders = { "Content-Type": "application/json" };
|
|
376
|
+
if (params.nonce) intentHeaders["x-checkout-session-token"] = params.nonce;
|
|
366
377
|
const intentResponse = await fetch(`${baseUrl}/v1/checkouts/payments/intents`, {
|
|
367
378
|
method: "POST",
|
|
368
|
-
headers:
|
|
379
|
+
headers: intentHeaders,
|
|
369
380
|
body: JSON.stringify({
|
|
370
381
|
sessionId: params.sessionId,
|
|
371
382
|
email: params.email,
|
|
@@ -644,15 +655,13 @@ var PaymentAPI = class {
|
|
|
644
655
|
*
|
|
645
656
|
* `nonce` is the session-bound checkout token returned when the session
|
|
646
657
|
* was created. When supplied it is sent as the `x-checkout-session-token`
|
|
647
|
-
* header
|
|
648
|
-
*
|
|
649
|
-
*
|
|
650
|
-
* session reads so the UUID alone is no longer sufficient to fetch a
|
|
651
|
-
* session. Backends that don't yet enforce it ignore the extra header.
|
|
658
|
+
* header that post-#640 backends match against `checkout_session.nonce`
|
|
659
|
+
* before returning the row — the UUID alone is no longer sufficient.
|
|
660
|
+
* Backends that don't yet enforce it ignore the extra header.
|
|
652
661
|
*/
|
|
653
662
|
async getCheckoutSession(checkoutSessionId, nonce) {
|
|
654
663
|
const response = await fetchWithNetworkRetry(
|
|
655
|
-
`${this.baseUrl}/v1/checkouts/sessions/${checkoutSessionId}`,
|
|
664
|
+
`${this.baseUrl}/v1/checkouts/sessions/${encodeURIComponent(checkoutSessionId)}`,
|
|
656
665
|
nonce ? { headers: { "x-checkout-session-token": nonce } } : void 0
|
|
657
666
|
);
|
|
658
667
|
if (!response.ok) {
|
|
@@ -707,34 +716,58 @@ var PaymentAPI = class {
|
|
|
707
716
|
* The backend will either succeed, return `type: '3ds_required'`
|
|
708
717
|
* (with a `threeDSecureToken`), or return `type: 'paypal_redirect_required'`.
|
|
709
718
|
*
|
|
719
|
+
* Hits the session-scoped route `POST /v1/checkouts/sessions/:id/process`
|
|
720
|
+
* and forwards `data.nonce` as `x-checkout-session-token`. Backend
|
|
721
|
+
* `TeamFloPay/backend#640` rejects callers without a matching nonce with a
|
|
722
|
+
* 401 — this method throws synchronously when `data.nonce` is missing so the
|
|
723
|
+
* problem surfaces before the network round trip.
|
|
724
|
+
*
|
|
710
725
|
* @param userId Vestigial — backend's GatewayInterceptor routes via session,
|
|
711
726
|
* not headers, so this value is no longer sent on the wire. Kept in the
|
|
712
727
|
* signature for back-compat with existing callers; will be removed in a
|
|
713
728
|
* future major version.
|
|
714
729
|
*/
|
|
715
730
|
async processPayment(_userId, data, options) {
|
|
731
|
+
if (!data.nonce) {
|
|
732
|
+
throw new import_shared3.FloPayError(
|
|
733
|
+
"processPayment requires `nonce` \u2014 pass the value returned from session creation.",
|
|
734
|
+
"validation_error",
|
|
735
|
+
{ code: "MissingCheckoutSessionToken", param: "nonce" }
|
|
736
|
+
);
|
|
737
|
+
}
|
|
738
|
+
const { nonce, ...processBody } = data;
|
|
716
739
|
const response = await fetch(
|
|
717
|
-
`${this.baseUrl}/v1/checkouts/sessions/process`,
|
|
740
|
+
`${this.baseUrl}/v1/checkouts/sessions/${encodeURIComponent(data.sessionId)}/process`,
|
|
718
741
|
{
|
|
719
742
|
method: "POST",
|
|
720
|
-
headers: {
|
|
721
|
-
|
|
743
|
+
headers: {
|
|
744
|
+
"Content-Type": "application/json",
|
|
745
|
+
"x-checkout-session-token": nonce
|
|
746
|
+
},
|
|
747
|
+
body: JSON.stringify(processBody)
|
|
722
748
|
}
|
|
723
749
|
);
|
|
724
|
-
return this.resolveProcessResponse(response, data.sessionId, options);
|
|
750
|
+
return this.resolveProcessResponse(response, data.sessionId, { ...options, nonce });
|
|
725
751
|
}
|
|
726
752
|
/**
|
|
727
753
|
* Create a PaymentIntent on the backend.
|
|
728
754
|
*
|
|
729
755
|
* Used by the Stripe flow to create a server-side PaymentIntent
|
|
730
756
|
* with the client's payment method attached.
|
|
757
|
+
*
|
|
758
|
+
* Forwards `options.nonce` as `x-checkout-session-token` when supplied — the
|
|
759
|
+
* session-bound checkout token returned by session creation. Post-#640
|
|
760
|
+
* backends reject this call with a 401 when the header is missing or does
|
|
761
|
+
* not match the session's stored nonce.
|
|
731
762
|
*/
|
|
732
763
|
async createPaymentIntent(sessionId, email, paymentMethodType, options) {
|
|
764
|
+
const headers = { "Content-Type": "application/json" };
|
|
765
|
+
if (options?.nonce) headers["x-checkout-session-token"] = options.nonce;
|
|
733
766
|
return fetch(
|
|
734
767
|
`${this.baseUrl}/v1/checkouts/payments/intents`,
|
|
735
768
|
{
|
|
736
769
|
method: "POST",
|
|
737
|
-
headers
|
|
770
|
+
headers,
|
|
738
771
|
body: JSON.stringify({
|
|
739
772
|
sessionId,
|
|
740
773
|
email,
|
|
@@ -747,13 +780,18 @@ var PaymentAPI = class {
|
|
|
747
780
|
}
|
|
748
781
|
/**
|
|
749
782
|
* Create a SetupIntent for saving payment methods.
|
|
783
|
+
*
|
|
784
|
+
* Forwards `options.nonce` as `x-checkout-session-token` when supplied —
|
|
785
|
+
* required by post-#640 backends, ignored by earlier versions.
|
|
750
786
|
*/
|
|
751
787
|
async createSetupIntent(sessionId, email, paymentMethodType, options) {
|
|
788
|
+
const headers = { "Content-Type": "application/json" };
|
|
789
|
+
if (options?.nonce) headers["x-checkout-session-token"] = options.nonce;
|
|
752
790
|
return fetch(
|
|
753
791
|
`${this.baseUrl}/v1/checkouts/payments/setup-intents`,
|
|
754
792
|
{
|
|
755
793
|
method: "POST",
|
|
756
|
-
headers
|
|
794
|
+
headers,
|
|
757
795
|
body: JSON.stringify({ sessionId, email, paymentMethodType }),
|
|
758
796
|
signal: options?.signal
|
|
759
797
|
}
|
|
@@ -895,7 +933,7 @@ var PaymentAPI = class {
|
|
|
895
933
|
throw createCheckoutProcessingTimeoutError();
|
|
896
934
|
}
|
|
897
935
|
}
|
|
898
|
-
const session = await this.getUnifiedCheckoutSession(checkoutSessionId);
|
|
936
|
+
const session = await this.getUnifiedCheckoutSession(checkoutSessionId, options?.nonce);
|
|
899
937
|
const status = session.data.session?.status;
|
|
900
938
|
if (status === "complete" || status === "expired") {
|
|
901
939
|
return session;
|
|
@@ -1016,7 +1054,8 @@ var PaymentAPI = class {
|
|
|
1016
1054
|
const pending = this.toCheckoutProcessingPending(payload, response, checkoutSessionId);
|
|
1017
1055
|
const session = await this.waitForCheckoutSessionCompletion(pending.sessionId, {
|
|
1018
1056
|
initialDelayMs: pending.retryAfterMs,
|
|
1019
|
-
timeoutMs: options?.pollTimeoutMs
|
|
1057
|
+
timeoutMs: options?.pollTimeoutMs,
|
|
1058
|
+
nonce: options?.nonce
|
|
1020
1059
|
});
|
|
1021
1060
|
if (session.data.session?.status === "complete") {
|
|
1022
1061
|
return new Response(null, { status: 204, statusText: "No Content" });
|
|
@@ -1354,9 +1393,17 @@ async function createCheckoutSession(options) {
|
|
|
1354
1393
|
}
|
|
1355
1394
|
if (status === 201) {
|
|
1356
1395
|
const uuid = body?.data?.uuid;
|
|
1396
|
+
const nonce = body?.data?.nonce;
|
|
1357
1397
|
if (!uuid) {
|
|
1358
1398
|
throw new Error("Checkout session created but no UUID was returned by the billing API");
|
|
1359
1399
|
}
|
|
1400
|
+
if (!nonce) {
|
|
1401
|
+
throw new import_shared6.FloPayError(
|
|
1402
|
+
"Checkout session created but no `nonce` was returned by the billing API. Upgrade the billing service to TeamFloPay/backend#640 or later.",
|
|
1403
|
+
"api_error",
|
|
1404
|
+
{ code: "MissingCheckoutSessionToken" }
|
|
1405
|
+
);
|
|
1406
|
+
}
|
|
1360
1407
|
if (wireProducts.length || sessionCurrency) {
|
|
1361
1408
|
cacheSessionDisplayData(uuid, {
|
|
1362
1409
|
currency: sessionCurrency,
|
|
@@ -1379,11 +1426,12 @@ async function createCheckoutSession(options) {
|
|
|
1379
1426
|
const checkoutData = JSON.stringify({ origin_url: cancelUrl });
|
|
1380
1427
|
const domain = window.location.hostname.split(".").slice(-2).join(".");
|
|
1381
1428
|
document.cookie = `checkout_data=${encodeURIComponent(checkoutData)}; domain=.${domain}; path=/; max-age=3600; SameSite=Lax; Secure;`;
|
|
1429
|
+
document.cookie = `flopay_checkout_token=${encodeURIComponent(nonce)}; domain=.${domain}; path=/; max-age=3600; SameSite=Lax; Secure;`;
|
|
1382
1430
|
}
|
|
1383
1431
|
if (typeof window !== "undefined") {
|
|
1384
1432
|
window.location.href = redirectUrl.toString();
|
|
1385
1433
|
}
|
|
1386
|
-
return { status: 201, redirectUrl: redirectUrl.toString() };
|
|
1434
|
+
return { status: 201, redirectUrl: redirectUrl.toString(), nonce };
|
|
1387
1435
|
}
|
|
1388
1436
|
if (status === 204) {
|
|
1389
1437
|
if (typeof window !== "undefined") {
|