@flopay/js 1.2.1 → 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 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
- const session = await api.getUnifiedCheckoutSession('session_uuid');
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: { "Content-Type": "application/json" },
370
+ headers: intentHeaders,
369
371
  body: JSON.stringify({
370
372
  sessionId: params.sessionId,
371
373
  email: params.email,
@@ -644,15 +646,13 @@ var PaymentAPI = class {
644
646
  *
645
647
  * `nonce` is the session-bound checkout token returned when the session
646
648
  * was created. When supplied it is sent as the `x-checkout-session-token`
647
- * header the same token the backend already validates on session-scoped
648
- * mutations (e.g. the payment-intent-decline endpoint). Sending it on the
649
- * GET is forward-compatible with TeamFloPay/backend#640, which tightens
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.
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.
652
652
  */
653
653
  async getCheckoutSession(checkoutSessionId, nonce) {
654
654
  const response = await fetchWithNetworkRetry(
655
- `${this.baseUrl}/v1/checkouts/sessions/${checkoutSessionId}`,
655
+ `${this.baseUrl}/v1/checkouts/sessions/${encodeURIComponent(checkoutSessionId)}`,
656
656
  nonce ? { headers: { "x-checkout-session-token": nonce } } : void 0
657
657
  );
658
658
  if (!response.ok) {
@@ -707,34 +707,58 @@ var PaymentAPI = class {
707
707
  * The backend will either succeed, return `type: '3ds_required'`
708
708
  * (with a `threeDSecureToken`), or return `type: 'paypal_redirect_required'`.
709
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
+ *
710
716
  * @param userId Vestigial — backend's GatewayInterceptor routes via session,
711
717
  * not headers, so this value is no longer sent on the wire. Kept in the
712
718
  * signature for back-compat with existing callers; will be removed in a
713
719
  * future major version.
714
720
  */
715
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;
716
730
  const response = await fetch(
717
- `${this.baseUrl}/v1/checkouts/sessions/process`,
731
+ `${this.baseUrl}/v1/checkouts/sessions/${encodeURIComponent(data.sessionId)}/process`,
718
732
  {
719
733
  method: "POST",
720
- headers: { "Content-Type": "application/json" },
721
- body: JSON.stringify(data)
734
+ headers: {
735
+ "Content-Type": "application/json",
736
+ "x-checkout-session-token": nonce
737
+ },
738
+ body: JSON.stringify(processBody)
722
739
  }
723
740
  );
724
- return this.resolveProcessResponse(response, data.sessionId, options);
741
+ return this.resolveProcessResponse(response, data.sessionId, { ...options, nonce });
725
742
  }
726
743
  /**
727
744
  * Create a PaymentIntent on the backend.
728
745
  *
729
746
  * Used by the Stripe flow to create a server-side PaymentIntent
730
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.
731
753
  */
732
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;
733
757
  return fetch(
734
758
  `${this.baseUrl}/v1/checkouts/payments/intents`,
735
759
  {
736
760
  method: "POST",
737
- headers: { "Content-Type": "application/json" },
761
+ headers,
738
762
  body: JSON.stringify({
739
763
  sessionId,
740
764
  email,
@@ -747,13 +771,18 @@ var PaymentAPI = class {
747
771
  }
748
772
  /**
749
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.
750
777
  */
751
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;
752
781
  return fetch(
753
782
  `${this.baseUrl}/v1/checkouts/payments/setup-intents`,
754
783
  {
755
784
  method: "POST",
756
- headers: { "Content-Type": "application/json" },
785
+ headers,
757
786
  body: JSON.stringify({ sessionId, email, paymentMethodType }),
758
787
  signal: options?.signal
759
788
  }
@@ -895,7 +924,7 @@ var PaymentAPI = class {
895
924
  throw createCheckoutProcessingTimeoutError();
896
925
  }
897
926
  }
898
- const session = await this.getUnifiedCheckoutSession(checkoutSessionId);
927
+ const session = await this.getUnifiedCheckoutSession(checkoutSessionId, options?.nonce);
899
928
  const status = session.data.session?.status;
900
929
  if (status === "complete" || status === "expired") {
901
930
  return session;
@@ -1016,7 +1045,8 @@ var PaymentAPI = class {
1016
1045
  const pending = this.toCheckoutProcessingPending(payload, response, checkoutSessionId);
1017
1046
  const session = await this.waitForCheckoutSessionCompletion(pending.sessionId, {
1018
1047
  initialDelayMs: pending.retryAfterMs,
1019
- timeoutMs: options?.pollTimeoutMs
1048
+ timeoutMs: options?.pollTimeoutMs,
1049
+ nonce: options?.nonce
1020
1050
  });
1021
1051
  if (session.data.session?.status === "complete") {
1022
1052
  return new Response(null, { status: 204, statusText: "No Content" });
@@ -1354,9 +1384,17 @@ async function createCheckoutSession(options) {
1354
1384
  }
1355
1385
  if (status === 201) {
1356
1386
  const uuid = body?.data?.uuid;
1387
+ const nonce = body?.data?.nonce;
1357
1388
  if (!uuid) {
1358
1389
  throw new Error("Checkout session created but no UUID was returned by the billing API");
1359
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
+ }
1360
1398
  if (wireProducts.length || sessionCurrency) {
1361
1399
  cacheSessionDisplayData(uuid, {
1362
1400
  currency: sessionCurrency,
@@ -1379,11 +1417,12 @@ async function createCheckoutSession(options) {
1379
1417
  const checkoutData = JSON.stringify({ origin_url: cancelUrl });
1380
1418
  const domain = window.location.hostname.split(".").slice(-2).join(".");
1381
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;`;
1382
1421
  }
1383
1422
  if (typeof window !== "undefined") {
1384
1423
  window.location.href = redirectUrl.toString();
1385
1424
  }
1386
- return { status: 201, redirectUrl: redirectUrl.toString() };
1425
+ return { status: 201, redirectUrl: redirectUrl.toString(), nonce };
1387
1426
  }
1388
1427
  if (status === 204) {
1389
1428
  if (typeof window !== "undefined") {