@flopay/js 1.2.7 → 1.3.0
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 +45 -2
- package/dist/index.cjs +559 -26
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +227 -3
- package/dist/index.d.ts +227 -3
- package/dist/index.mjs +554 -21
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -5
package/dist/index.cjs
CHANGED
|
@@ -33,6 +33,7 @@ __export(index_exports, {
|
|
|
33
33
|
FloPay: () => FloPay,
|
|
34
34
|
FloPayElements: () => FloPayElements,
|
|
35
35
|
PaymentAPI: () => PaymentAPI,
|
|
36
|
+
PciVaultCardCapture: () => PciVaultCardCapture,
|
|
36
37
|
StripeAdapter: () => StripeAdapter,
|
|
37
38
|
cacheSessionDisplayData: () => cacheSessionDisplayData,
|
|
38
39
|
clearSessionDisplayData: () => clearSessionDisplayData,
|
|
@@ -44,7 +45,7 @@ __export(index_exports, {
|
|
|
44
45
|
module.exports = __toCommonJS(index_exports);
|
|
45
46
|
|
|
46
47
|
// src/load.ts
|
|
47
|
-
var
|
|
48
|
+
var import_shared6 = require("@flopay/shared");
|
|
48
49
|
|
|
49
50
|
// src/stripe-adapter.ts
|
|
50
51
|
var import_shared = require("@flopay/shared");
|
|
@@ -145,18 +146,27 @@ var StripeAdapter = class {
|
|
|
145
146
|
const nextAppearanceKey = stripeAppearance ? JSON.stringify(stripeAppearance) : null;
|
|
146
147
|
if (!this.elements) {
|
|
147
148
|
let elementsOptions;
|
|
149
|
+
const deferredAmount = options?.amount ?? 0;
|
|
150
|
+
const deferredCurrency = (options?.currency ?? "usd").toLowerCase();
|
|
151
|
+
const paymentMethodCreation = options?.paymentMethodCreation ?? "manual";
|
|
148
152
|
if (options?.clientSecret) {
|
|
149
153
|
elementsOptions = { clientSecret: options.clientSecret };
|
|
150
|
-
} else {
|
|
154
|
+
} else if (deferredAmount > 0) {
|
|
151
155
|
elementsOptions = {
|
|
152
156
|
mode: "payment",
|
|
153
|
-
amount:
|
|
154
|
-
currency:
|
|
155
|
-
paymentMethodCreation
|
|
157
|
+
amount: deferredAmount,
|
|
158
|
+
currency: deferredCurrency,
|
|
159
|
+
paymentMethodCreation
|
|
156
160
|
};
|
|
157
161
|
if (options?.setupFutureUsage) {
|
|
158
162
|
elementsOptions["setupFutureUsage"] = options.setupFutureUsage;
|
|
159
163
|
}
|
|
164
|
+
} else {
|
|
165
|
+
elementsOptions = {
|
|
166
|
+
mode: "setup",
|
|
167
|
+
currency: deferredCurrency,
|
|
168
|
+
paymentMethodCreation
|
|
169
|
+
};
|
|
160
170
|
}
|
|
161
171
|
if (stripeAppearance) {
|
|
162
172
|
elementsOptions["appearance"] = stripeAppearance;
|
|
@@ -263,6 +273,27 @@ var StripeAdapter = class {
|
|
|
263
273
|
error: new import_shared.FloPayError("Stripe not initialized", "api_error")
|
|
264
274
|
};
|
|
265
275
|
}
|
|
276
|
+
if ((0, import_shared.isSetupIntentClientSecret)(params.clientSecret)) {
|
|
277
|
+
const { error: setupError, setupIntent } = await this.stripe.confirmCardSetup(
|
|
278
|
+
params.clientSecret,
|
|
279
|
+
{ payment_method: params.paymentMethodId }
|
|
280
|
+
);
|
|
281
|
+
if (setupError) {
|
|
282
|
+
return {
|
|
283
|
+
status: "failed",
|
|
284
|
+
error: new import_shared.FloPayError(
|
|
285
|
+
setupError.message ?? "Payment failed",
|
|
286
|
+
"api_error",
|
|
287
|
+
{ code: setupError.code, declineCode: setupError.decline_code }
|
|
288
|
+
)
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
return {
|
|
292
|
+
status: setupIntent?.status ?? "failed",
|
|
293
|
+
paymentIntentId: setupIntent?.id,
|
|
294
|
+
paymentMethodId: this.extractPaymentMethodId(setupIntent?.payment_method)
|
|
295
|
+
};
|
|
296
|
+
}
|
|
266
297
|
const { error, paymentIntent } = await this.stripe.confirmCardPayment(
|
|
267
298
|
params.clientSecret,
|
|
268
299
|
{ payment_method: params.paymentMethodId }
|
|
@@ -476,7 +507,7 @@ var StripeAdapter = class {
|
|
|
476
507
|
};
|
|
477
508
|
|
|
478
509
|
// src/flopay.ts
|
|
479
|
-
var
|
|
510
|
+
var import_shared5 = require("@flopay/shared");
|
|
480
511
|
|
|
481
512
|
// src/elements.ts
|
|
482
513
|
var import_shared2 = require("@flopay/shared");
|
|
@@ -660,9 +691,11 @@ var PaymentAPI = class {
|
|
|
660
691
|
* Backends that don't yet enforce it ignore the extra header.
|
|
661
692
|
*/
|
|
662
693
|
async getCheckoutSession(checkoutSessionId, nonce) {
|
|
694
|
+
const headers = { [import_shared3.FLO_SDK_VERSION_HEADER]: import_shared3.SDK_VERSION };
|
|
695
|
+
if (nonce) headers["x-checkout-session-token"] = nonce;
|
|
663
696
|
const response = await fetchWithNetworkRetry(
|
|
664
697
|
`${this.baseUrl}/v1/checkouts/sessions/${encodeURIComponent(checkoutSessionId)}`,
|
|
665
|
-
|
|
698
|
+
{ headers }
|
|
666
699
|
);
|
|
667
700
|
if (!response.ok) {
|
|
668
701
|
throw await buildApiErrorFromResponse(response, "Failed to get checkout session");
|
|
@@ -699,6 +732,43 @@ var PaymentAPI = class {
|
|
|
699
732
|
clearSessionDisplayData(sessionId) {
|
|
700
733
|
clearSessionDisplayData(sessionId);
|
|
701
734
|
}
|
|
735
|
+
/**
|
|
736
|
+
* Fetch (re-mint) the hosted vault capture widget for a session
|
|
737
|
+
* (TeamFloPay/backend#823).
|
|
738
|
+
*
|
|
739
|
+
* `POST /v1/checkouts/sessions/{id}/vault/capture` returns the SDK-ready
|
|
740
|
+
* {@link VaultCaptureBlock} (`html` + `url`, plus `messageToken` /
|
|
741
|
+
* `expectedOrigin` once the backend mints them). The SDK injects `html` as
|
|
742
|
+
* the card-capture widget. This is the fallback path for sessions that did
|
|
743
|
+
* not receive the embedded `vault` block on create (e.g. a session loaded by
|
|
744
|
+
* id via `GET`, or a pre-1.3.0 create); the endpoint is idempotent and reuses
|
|
745
|
+
* session-cached creds when available.
|
|
746
|
+
*
|
|
747
|
+
* Because the endpoint is idempotent, the request is wrapped in
|
|
748
|
+
* `fetchWithNetworkRetry`: a transient network blip (dropped connection, DNS
|
|
749
|
+
* hiccup, failed CORS preflight) would otherwise leave the secure card form
|
|
750
|
+
* unable to load and hard-block checkout.
|
|
751
|
+
*
|
|
752
|
+
* The PCIVault submit *secret* the backend may include in the response is
|
|
753
|
+
* intentionally **not** read or surfaced — it is server-only and never enters
|
|
754
|
+
* the SDK runtime.
|
|
755
|
+
*
|
|
756
|
+
* `nonce` is forwarded as `x-checkout-session-token` (required by post-#640
|
|
757
|
+
* backends, matched against the session's stored nonce).
|
|
758
|
+
*/
|
|
759
|
+
async getVaultCapture(checkoutSessionId, nonce) {
|
|
760
|
+
const headers = { "Content-Type": "application/json" };
|
|
761
|
+
if (nonce) headers["x-checkout-session-token"] = nonce;
|
|
762
|
+
const response = await fetchWithNetworkRetry(
|
|
763
|
+
`${this.baseUrl}/v1/checkouts/sessions/${encodeURIComponent(checkoutSessionId)}/vault/capture`,
|
|
764
|
+
{ method: "POST", headers }
|
|
765
|
+
);
|
|
766
|
+
if (!response.ok) {
|
|
767
|
+
throw await buildApiErrorFromResponse(response, "Failed to load the secure card form");
|
|
768
|
+
}
|
|
769
|
+
const block = await response.json();
|
|
770
|
+
return this.toVaultBlock(block);
|
|
771
|
+
}
|
|
702
772
|
/**
|
|
703
773
|
* Fetch and normalize a checkout session.
|
|
704
774
|
*
|
|
@@ -708,7 +778,12 @@ var PaymentAPI = class {
|
|
|
708
778
|
*/
|
|
709
779
|
async getUnifiedCheckoutSession(checkoutSessionId, nonce) {
|
|
710
780
|
const res = await this.getCheckoutSession(checkoutSessionId, nonce);
|
|
711
|
-
|
|
781
|
+
const normalized = this.normalizeRawSession(res.data);
|
|
782
|
+
const vault = res.vault;
|
|
783
|
+
if (vault && normalized.data.session) {
|
|
784
|
+
normalized.data.session.vault = this.toVaultBlock(vault);
|
|
785
|
+
}
|
|
786
|
+
return normalized;
|
|
712
787
|
}
|
|
713
788
|
/**
|
|
714
789
|
* Submit a tokenized payment to the billing backend.
|
|
@@ -749,6 +824,43 @@ var PaymentAPI = class {
|
|
|
749
824
|
);
|
|
750
825
|
return this.resolveProcessResponse(response, data.sessionId, { ...options, nonce });
|
|
751
826
|
}
|
|
827
|
+
/**
|
|
828
|
+
* Patch the buyer's account snapshot (email, name, billing address, AVS
|
|
829
|
+
* intent) onto a checkout session via
|
|
830
|
+
* `PATCH /v1/checkouts/sessions/{id}/account` (TeamFloPay/backend#823).
|
|
831
|
+
*
|
|
832
|
+
* The vault path's hosted form owns the charge end-to-end so the SDK
|
|
833
|
+
* never calls `/process` on this path; the buyer-typed AVS / billing
|
|
834
|
+
* address would otherwise be lost. The SDK calls this just before
|
|
835
|
+
* submitting the vault widget so the downstream listener mints the
|
|
836
|
+
* Stripe PaymentMethod with the right `billing_details.address` and the
|
|
837
|
+
* per-attempt + per-PM address snapshots are populated.
|
|
838
|
+
*
|
|
839
|
+
* Body shape mirrors the relevant subset of `/process`'s
|
|
840
|
+
* `ProcessCheckoutBodyDto` — same keys, same validators. The endpoint
|
|
841
|
+
* is idempotent: empty/undefined fields are not written, addresses are
|
|
842
|
+
* last-writer-wins, AVS analytics are first-writer-wins.
|
|
843
|
+
*
|
|
844
|
+
* Wrapped in `fetchWithNetworkRetry` because a transient blip on this
|
|
845
|
+
* pre-pay PATCH would silently leave AVS unsent and cause an
|
|
846
|
+
* AVS-protected charge to decline downstream.
|
|
847
|
+
*/
|
|
848
|
+
async patchAccountSnapshot(sessionId, nonce, body) {
|
|
849
|
+
const response = await fetchWithNetworkRetry(
|
|
850
|
+
`${this.baseUrl}/v1/checkouts/sessions/${encodeURIComponent(sessionId)}/account`,
|
|
851
|
+
{
|
|
852
|
+
method: "PATCH",
|
|
853
|
+
headers: {
|
|
854
|
+
"Content-Type": "application/json",
|
|
855
|
+
"x-checkout-session-token": nonce
|
|
856
|
+
},
|
|
857
|
+
body: JSON.stringify(body)
|
|
858
|
+
}
|
|
859
|
+
);
|
|
860
|
+
if (!response.ok) {
|
|
861
|
+
throw await buildApiErrorFromResponse(response, "Failed to persist account snapshot");
|
|
862
|
+
}
|
|
863
|
+
}
|
|
752
864
|
/**
|
|
753
865
|
* Create a PaymentIntent on the backend.
|
|
754
866
|
*
|
|
@@ -759,6 +871,13 @@ var PaymentAPI = class {
|
|
|
759
871
|
* session-bound checkout token returned by session creation. Post-#640
|
|
760
872
|
* backends reject this call with a 401 when the header is missing or does
|
|
761
873
|
* not match the session's stored nonce.
|
|
874
|
+
*
|
|
875
|
+
* `paymentMethodType` is optional for the vault card-capture flow
|
|
876
|
+
* (TeamFloPay/backend#823): the frontend starts checkout *without* an upfront
|
|
877
|
+
* card payment method, so it may be omitted (or `null`). The hosted vault PCI
|
|
878
|
+
* form captures the card afterwards and the backend attaches the resulting
|
|
879
|
+
* payment method to the PaymentIntent it returns here. Legacy callers keep
|
|
880
|
+
* passing the concrete payment method id / type.
|
|
762
881
|
*/
|
|
763
882
|
async createPaymentIntent(sessionId, email, paymentMethodType, options) {
|
|
764
883
|
const headers = { "Content-Type": "application/json" };
|
|
@@ -771,7 +890,7 @@ var PaymentAPI = class {
|
|
|
771
890
|
body: JSON.stringify({
|
|
772
891
|
sessionId,
|
|
773
892
|
email,
|
|
774
|
-
paymentMethodType,
|
|
893
|
+
paymentMethodType: paymentMethodType ?? null,
|
|
775
894
|
isPaypal: options?.isPaypal ?? false
|
|
776
895
|
}),
|
|
777
896
|
signal: options?.signal
|
|
@@ -880,7 +999,13 @@ var PaymentAPI = class {
|
|
|
880
999
|
`${this.baseUrl}/v1/checkouts/sessions?expand=true`,
|
|
881
1000
|
{
|
|
882
1001
|
method: "POST",
|
|
883
|
-
|
|
1002
|
+
// Declare the SDK version so backends at TeamFloPay/backend#823 embed
|
|
1003
|
+
// the hosted vault capture block (`body.vault`) in the response for
|
|
1004
|
+
// SDKs ≥ 1.3.0. Older backends ignore the header.
|
|
1005
|
+
headers: {
|
|
1006
|
+
"Content-Type": "application/json",
|
|
1007
|
+
[import_shared3.FLO_SDK_VERSION_HEADER]: import_shared3.SDK_VERSION
|
|
1008
|
+
},
|
|
884
1009
|
body: JSON.stringify(payload)
|
|
885
1010
|
}
|
|
886
1011
|
);
|
|
@@ -898,8 +1023,12 @@ var PaymentAPI = class {
|
|
|
898
1023
|
if (body.data && "gateways" in body.data) {
|
|
899
1024
|
this.autoCacheDisplayData(body.data.uuid, params);
|
|
900
1025
|
const merged = this.mergeCachedDisplayData(body.data);
|
|
1026
|
+
const normalized = this.normalizeRawSession(merged);
|
|
1027
|
+
if (body.vault && normalized.data.session) {
|
|
1028
|
+
normalized.data.session.vault = this.toVaultBlock(body.vault);
|
|
1029
|
+
}
|
|
901
1030
|
return {
|
|
902
|
-
...
|
|
1031
|
+
...normalized,
|
|
903
1032
|
autoProcessingError: body.autoProcessingError,
|
|
904
1033
|
autoProcessingAttempted: body.autoProcessingAttempted,
|
|
905
1034
|
autoProcessingPending: body.autoProcessingPending
|
|
@@ -1018,6 +1147,7 @@ var PaymentAPI = class {
|
|
|
1018
1147
|
},
|
|
1019
1148
|
metadata: {},
|
|
1020
1149
|
checkoutMode: raw.checkoutMode,
|
|
1150
|
+
providerPaymentMethodId: typeof raw.providerPaymentMethodId === "string" ? raw.providerPaymentMethodId : null,
|
|
1021
1151
|
products: rawProducts.map((p) => ({
|
|
1022
1152
|
...p,
|
|
1023
1153
|
totalAmount: typeof p.totalAmount === "number" ? p.totalAmount : void 0,
|
|
@@ -1037,6 +1167,19 @@ var PaymentAPI = class {
|
|
|
1037
1167
|
tagsData: raw.tagsData
|
|
1038
1168
|
};
|
|
1039
1169
|
}
|
|
1170
|
+
/**
|
|
1171
|
+
* Coerce a raw vault block into a typed {@link VaultCaptureBlock}. The
|
|
1172
|
+
* server-only PCIVault submit `secret` is deliberately dropped so it never
|
|
1173
|
+
* lands on the public session surface (logs / telemetry / client inspection).
|
|
1174
|
+
*/
|
|
1175
|
+
toVaultBlock(raw) {
|
|
1176
|
+
return {
|
|
1177
|
+
html: typeof raw.html === "string" ? raw.html : void 0,
|
|
1178
|
+
url: typeof raw.url === "string" ? raw.url : void 0,
|
|
1179
|
+
messageToken: typeof raw.messageToken === "string" ? raw.messageToken : void 0,
|
|
1180
|
+
expectedOrigin: typeof raw.expectedOrigin === "string" ? raw.expectedOrigin : void 0
|
|
1181
|
+
};
|
|
1182
|
+
}
|
|
1040
1183
|
toCheckoutSessionStatus(status) {
|
|
1041
1184
|
if (status === "completed") {
|
|
1042
1185
|
return "complete";
|
|
@@ -1151,6 +1294,378 @@ var PaymentAPI = class {
|
|
|
1151
1294
|
}
|
|
1152
1295
|
};
|
|
1153
1296
|
|
|
1297
|
+
// src/pci-vault-card-capture.ts
|
|
1298
|
+
var import_shared4 = require("@flopay/shared");
|
|
1299
|
+
var VAULT_MESSAGE_SOURCE = "flopay-vault";
|
|
1300
|
+
function addBreadcrumb(message, data) {
|
|
1301
|
+
const sentry = globalThis.Sentry;
|
|
1302
|
+
sentry?.addBreadcrumb?.({ category: "flopay.card-capture", level: "info", message, data });
|
|
1303
|
+
}
|
|
1304
|
+
function isVaultResultMessage(value) {
|
|
1305
|
+
if (typeof value !== "object" || value === null) return false;
|
|
1306
|
+
const record = value;
|
|
1307
|
+
return record["source"] === VAULT_MESSAGE_SOURCE && (record["type"] === "ready" || record["type"] === "submitting" || record["type"] === "blocked" || record["type"] === "complete" || record["type"] === "decline" || record["type"] === "error" || record["type"] === "action_required");
|
|
1308
|
+
}
|
|
1309
|
+
function isVaultValidationMessage(value) {
|
|
1310
|
+
if (typeof value !== "object" || value === null) return false;
|
|
1311
|
+
const record = value;
|
|
1312
|
+
return record["source"] === VAULT_MESSAGE_SOURCE && record["type"] === "validation" && Array.isArray(record["messages"]);
|
|
1313
|
+
}
|
|
1314
|
+
function isVaultResizeMessage(value) {
|
|
1315
|
+
if (typeof value !== "object" || value === null) return false;
|
|
1316
|
+
const record = value;
|
|
1317
|
+
return record["source"] === VAULT_MESSAGE_SOURCE && record["type"] === "resize" && typeof record["height"] === "number" && Number.isFinite(record["height"]);
|
|
1318
|
+
}
|
|
1319
|
+
var PciVaultCardCapture = class {
|
|
1320
|
+
constructor(config = {}) {
|
|
1321
|
+
this.provider = "pcivault";
|
|
1322
|
+
this.container = null;
|
|
1323
|
+
this.messageHandler = null;
|
|
1324
|
+
/**
|
|
1325
|
+
* Parent-page-level overlay rendering the provider's verification challenge
|
|
1326
|
+
* (3DS-2 iframe) on `action_required`. Owned by the adapter — not the
|
|
1327
|
+
* widget — so it can sit above the host SDK's processing backdrop, which
|
|
1328
|
+
* would otherwise visually cover an in-widget challenge iframe.
|
|
1329
|
+
*/
|
|
1330
|
+
this.actionOverlay = null;
|
|
1331
|
+
/**
|
|
1332
|
+
* Listener that catches the `flopay-vault-3ds-return` postMessage from the
|
|
1333
|
+
* provider's challenge return page. When the SDK owns the challenge iframe
|
|
1334
|
+
* the return page lives inside *that* iframe (not the widget's), so
|
|
1335
|
+
* `window.parent` is the host page — the widget's existing message
|
|
1336
|
+
* listener can't see it. The SDK forwards completion into the widget via
|
|
1337
|
+
* `action_completed` so the widget kicks `/3ds/complete` immediately
|
|
1338
|
+
* instead of waiting on the eventual provider webhook.
|
|
1339
|
+
*/
|
|
1340
|
+
this.threeDsReturnHandler = null;
|
|
1341
|
+
/** Per-session integrity token to require on outcomes (from mount options). */
|
|
1342
|
+
this.messageToken = null;
|
|
1343
|
+
/** Strict origin to require on outcomes, when configured. */
|
|
1344
|
+
this.expectedOrigin = null;
|
|
1345
|
+
/** Latest merchant theme to push into the (cross-origin) widget. */
|
|
1346
|
+
this.theme = null;
|
|
1347
|
+
/** Latest host submit-gate state to push into the widget (block its submit). */
|
|
1348
|
+
this.submitGateBlocked = false;
|
|
1349
|
+
/** Latest card-field order + autofocus directive to push into the widget. */
|
|
1350
|
+
this.cardFieldOrder = null;
|
|
1351
|
+
this.cardAutoFocus = true;
|
|
1352
|
+
this.listeners = /* @__PURE__ */ new Map();
|
|
1353
|
+
this.config = config;
|
|
1354
|
+
}
|
|
1355
|
+
async mount(container, options) {
|
|
1356
|
+
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
1357
|
+
throw new import_shared4.FloPayError(
|
|
1358
|
+
"The vault card form is only available in the browser.",
|
|
1359
|
+
"api_error",
|
|
1360
|
+
{ code: "card_capture_no_window" }
|
|
1361
|
+
);
|
|
1362
|
+
}
|
|
1363
|
+
if (!options?.html || !options.html.trim()) {
|
|
1364
|
+
throw new import_shared4.FloPayError(
|
|
1365
|
+
"No vault capture widget HTML was provided to mount the secure card form.",
|
|
1366
|
+
"api_error",
|
|
1367
|
+
{ code: "card_capture_no_widget_html" }
|
|
1368
|
+
);
|
|
1369
|
+
}
|
|
1370
|
+
this.container = container;
|
|
1371
|
+
this.messageToken = options.messageToken ?? null;
|
|
1372
|
+
this.expectedOrigin = options.expectedOrigin ?? this.config.expectedOrigin ?? null;
|
|
1373
|
+
this.theme = options.theme ?? null;
|
|
1374
|
+
this.attachMessageListener();
|
|
1375
|
+
this.injectWidget(container, options.html);
|
|
1376
|
+
this.postTheme();
|
|
1377
|
+
this.postSubmitGate();
|
|
1378
|
+
this.postCardFieldOrder();
|
|
1379
|
+
addBreadcrumb("vault widget mounted", { sessionId: this.config.sessionId });
|
|
1380
|
+
this.emit("ready", { sessionId: this.config.sessionId });
|
|
1381
|
+
}
|
|
1382
|
+
on(event, handler) {
|
|
1383
|
+
let set = this.listeners.get(event);
|
|
1384
|
+
if (!set) {
|
|
1385
|
+
set = /* @__PURE__ */ new Set();
|
|
1386
|
+
this.listeners.set(event, set);
|
|
1387
|
+
}
|
|
1388
|
+
set.add(handler);
|
|
1389
|
+
return () => {
|
|
1390
|
+
this.listeners.get(event)?.delete(handler);
|
|
1391
|
+
};
|
|
1392
|
+
}
|
|
1393
|
+
unmount() {
|
|
1394
|
+
this.hideActionRequiredOverlay();
|
|
1395
|
+
if (this.messageHandler) {
|
|
1396
|
+
window.removeEventListener("message", this.messageHandler);
|
|
1397
|
+
this.messageHandler = null;
|
|
1398
|
+
}
|
|
1399
|
+
if (this.container) {
|
|
1400
|
+
this.container.replaceChildren();
|
|
1401
|
+
this.container = null;
|
|
1402
|
+
}
|
|
1403
|
+
this.messageToken = null;
|
|
1404
|
+
this.expectedOrigin = null;
|
|
1405
|
+
}
|
|
1406
|
+
// ── internals ──
|
|
1407
|
+
/**
|
|
1408
|
+
* Inject the server-rendered widget HTML. `innerHTML` does not execute
|
|
1409
|
+
* embedded `<script>` tags, so each script node is replaced with a freshly
|
|
1410
|
+
* created element that the browser will load and run (this is what boots the
|
|
1411
|
+
* PCIVault form bundle against the `data-flopay-config` container).
|
|
1412
|
+
*/
|
|
1413
|
+
injectWidget(container, html) {
|
|
1414
|
+
container.innerHTML = html;
|
|
1415
|
+
const scripts = Array.from(container.querySelectorAll("script"));
|
|
1416
|
+
for (const oldScript of scripts) {
|
|
1417
|
+
const script = document.createElement("script");
|
|
1418
|
+
for (const attr of Array.from(oldScript.attributes)) {
|
|
1419
|
+
script.setAttribute(attr.name, attr.value);
|
|
1420
|
+
}
|
|
1421
|
+
script.text = oldScript.text;
|
|
1422
|
+
oldScript.replaceWith(script);
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
attachMessageListener() {
|
|
1426
|
+
if (this.messageHandler) return;
|
|
1427
|
+
const handler = (event) => {
|
|
1428
|
+
if (this.expectedOrigin && event.origin !== this.expectedOrigin) return;
|
|
1429
|
+
const data = event.data;
|
|
1430
|
+
if (isVaultResizeMessage(data)) {
|
|
1431
|
+
if (this.messageToken && data.messageToken !== this.messageToken) return;
|
|
1432
|
+
this.applyHeight(data.height);
|
|
1433
|
+
return;
|
|
1434
|
+
}
|
|
1435
|
+
if (isVaultValidationMessage(data)) {
|
|
1436
|
+
if (this.messageToken && data.messageToken !== this.messageToken) return;
|
|
1437
|
+
const text = data.messages.filter((m) => typeof m === "string" && m.trim()).join(" ");
|
|
1438
|
+
this.emit("validation", { sessionId: this.config.sessionId, message: text || void 0 });
|
|
1439
|
+
return;
|
|
1440
|
+
}
|
|
1441
|
+
if (!isVaultResultMessage(data)) return;
|
|
1442
|
+
if (this.messageToken && data.messageToken !== this.messageToken) return;
|
|
1443
|
+
const boundSession = this.config.sessionId;
|
|
1444
|
+
const incomingSession = typeof data.sessionId === "string" ? data.sessionId : void 0;
|
|
1445
|
+
if (boundSession && incomingSession && incomingSession !== boundSession) {
|
|
1446
|
+
return;
|
|
1447
|
+
}
|
|
1448
|
+
if ((data.type === "complete" || data.type === "decline") && boundSession && incomingSession !== boundSession) {
|
|
1449
|
+
return;
|
|
1450
|
+
}
|
|
1451
|
+
const outcome = {
|
|
1452
|
+
sessionId: data.sessionId ?? this.config.sessionId,
|
|
1453
|
+
intentId: data.intentId,
|
|
1454
|
+
declineReason: data.declineReason,
|
|
1455
|
+
message: data.message,
|
|
1456
|
+
nextActionRedirectUrl: data.nextActionRedirectUrl
|
|
1457
|
+
};
|
|
1458
|
+
addBreadcrumb(`vault widget ${data.type}`, {
|
|
1459
|
+
sessionId: outcome.sessionId,
|
|
1460
|
+
declineReason: outcome.declineReason
|
|
1461
|
+
});
|
|
1462
|
+
if (data.type === "ready") {
|
|
1463
|
+
this.postTheme();
|
|
1464
|
+
this.postSubmitGate();
|
|
1465
|
+
this.postCardFieldOrder();
|
|
1466
|
+
}
|
|
1467
|
+
if (data.type === "action_required" && data.nextActionRedirectUrl) {
|
|
1468
|
+
this.showActionRequiredOverlay(data.nextActionRedirectUrl);
|
|
1469
|
+
}
|
|
1470
|
+
if (data.type === "complete" || data.type === "decline" || data.type === "error" || data.type === "submitting") {
|
|
1471
|
+
this.hideActionRequiredOverlay();
|
|
1472
|
+
}
|
|
1473
|
+
this.emit(data.type, outcome);
|
|
1474
|
+
};
|
|
1475
|
+
this.messageHandler = handler;
|
|
1476
|
+
window.addEventListener("message", handler);
|
|
1477
|
+
}
|
|
1478
|
+
/**
|
|
1479
|
+
* Push merchant theme colors into the hosted widget (live). The host calls
|
|
1480
|
+
* this on a runtime theme switch; the widget applies them to its CSS variables
|
|
1481
|
+
* without a remount. Stores the latest theme so `ready` can re-push it.
|
|
1482
|
+
*/
|
|
1483
|
+
applyTheme(theme) {
|
|
1484
|
+
this.theme = theme;
|
|
1485
|
+
this.postTheme();
|
|
1486
|
+
}
|
|
1487
|
+
/** postMessage the current theme to the widget's (cross-origin) document. */
|
|
1488
|
+
postTheme() {
|
|
1489
|
+
if (!this.theme || !this.container) return;
|
|
1490
|
+
const iframe = this.container.querySelector("iframe");
|
|
1491
|
+
const target = iframe?.contentWindow;
|
|
1492
|
+
if (!target) return;
|
|
1493
|
+
try {
|
|
1494
|
+
target.postMessage({ source: "flopay-vault-host", type: "theme", theme: this.theme }, "*");
|
|
1495
|
+
} catch {
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
/**
|
|
1499
|
+
* Gate the widget's submit from the host. When `blocked`, the widget cancels
|
|
1500
|
+
* its next submit and emits `'blocked'` instead of `'submitting'` so the host
|
|
1501
|
+
* can validate merchant-DOM fields (AVS) first. Stored so `ready` re-pushes it.
|
|
1502
|
+
*/
|
|
1503
|
+
setSubmitGate(blocked) {
|
|
1504
|
+
this.submitGateBlocked = blocked;
|
|
1505
|
+
this.postSubmitGate();
|
|
1506
|
+
}
|
|
1507
|
+
/** postMessage the current submit-gate state to the widget's document. */
|
|
1508
|
+
postSubmitGate() {
|
|
1509
|
+
if (!this.container) return;
|
|
1510
|
+
const iframe = this.container.querySelector("iframe");
|
|
1511
|
+
const target = iframe?.contentWindow;
|
|
1512
|
+
if (!target) return;
|
|
1513
|
+
try {
|
|
1514
|
+
target.postMessage(
|
|
1515
|
+
{ source: "flopay-vault-host", type: "gate", blocked: this.submitGateBlocked },
|
|
1516
|
+
"*"
|
|
1517
|
+
);
|
|
1518
|
+
} catch {
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
/**
|
|
1522
|
+
* Push the card-field order + autofocus directive into the widget (live). The
|
|
1523
|
+
* widget re-sequences its rows (DOM order, so tab order follows) and focuses
|
|
1524
|
+
* its first field unless `autoFocus` is false. Stored so `ready` re-pushes it.
|
|
1525
|
+
*/
|
|
1526
|
+
setCardFieldOrder(order, autoFocus) {
|
|
1527
|
+
this.cardFieldOrder = order;
|
|
1528
|
+
this.cardAutoFocus = autoFocus;
|
|
1529
|
+
this.postCardFieldOrder();
|
|
1530
|
+
}
|
|
1531
|
+
/** postMessage the current field order + autofocus to the widget's document. */
|
|
1532
|
+
postCardFieldOrder() {
|
|
1533
|
+
if (!this.container) return;
|
|
1534
|
+
const iframe = this.container.querySelector("iframe");
|
|
1535
|
+
const target = iframe?.contentWindow;
|
|
1536
|
+
if (!target) return;
|
|
1537
|
+
try {
|
|
1538
|
+
target.postMessage(
|
|
1539
|
+
{
|
|
1540
|
+
source: "flopay-vault-host",
|
|
1541
|
+
type: "fieldOrder",
|
|
1542
|
+
order: this.cardFieldOrder,
|
|
1543
|
+
autoFocus: this.cardAutoFocus
|
|
1544
|
+
},
|
|
1545
|
+
"*"
|
|
1546
|
+
);
|
|
1547
|
+
} catch {
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
emit(event, payload) {
|
|
1551
|
+
for (const handler of this.listeners.get(event) ?? []) {
|
|
1552
|
+
handler(payload);
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
/**
|
|
1556
|
+
* Render the provider-hosted verification challenge (e.g. Stripe 3DS-2) in a
|
|
1557
|
+
* full-page overlay at the PARENT page level. The widget's inline-iframe
|
|
1558
|
+
* approach is unusable because the SDK's processing backdrop sits above the
|
|
1559
|
+
* vault iframe, hiding any challenge mounted inside it — by lifting the
|
|
1560
|
+
* iframe to the host page the adapter can give it a z-index that wins.
|
|
1561
|
+
*
|
|
1562
|
+
* The overlay tears down on the next terminal outcome
|
|
1563
|
+
* (`complete`/`decline`/`error`) or when the buyer closes it via the backdrop
|
|
1564
|
+
* close button. Closing manually is a soft abandon — the next `/status` poll
|
|
1565
|
+
* either reveals a real outcome (the challenge completed via the issuer's
|
|
1566
|
+
* own redirect to `/vault/3ds/return`, which posts back into the widget) or
|
|
1567
|
+
* surfaces `requires_action` again so the host can decide what to do.
|
|
1568
|
+
*/
|
|
1569
|
+
showActionRequiredOverlay(challengeUrl) {
|
|
1570
|
+
if (typeof document === "undefined") return;
|
|
1571
|
+
if (this.actionOverlay) {
|
|
1572
|
+
const existingIframe = this.actionOverlay.querySelector("iframe");
|
|
1573
|
+
if (existingIframe instanceof HTMLIFrameElement) {
|
|
1574
|
+
existingIframe.src = challengeUrl;
|
|
1575
|
+
}
|
|
1576
|
+
return;
|
|
1577
|
+
}
|
|
1578
|
+
const backdrop = document.createElement("div");
|
|
1579
|
+
backdrop.setAttribute("data-flopay-action-required", "1");
|
|
1580
|
+
backdrop.style.cssText = [
|
|
1581
|
+
"position:fixed",
|
|
1582
|
+
"inset:0",
|
|
1583
|
+
// Maximum signed 32-bit z-index; the SDK's own processing backdrop sits
|
|
1584
|
+
// well below this so the challenge is visible and interactive.
|
|
1585
|
+
"z-index:2147483647",
|
|
1586
|
+
"background:rgba(15,23,42,0.6)",
|
|
1587
|
+
"display:flex",
|
|
1588
|
+
"align-items:center",
|
|
1589
|
+
"justify-content:center",
|
|
1590
|
+
"padding:16px"
|
|
1591
|
+
].join(";");
|
|
1592
|
+
const frame = document.createElement("iframe");
|
|
1593
|
+
frame.setAttribute("title", "Card authentication");
|
|
1594
|
+
frame.setAttribute("allow", "payment");
|
|
1595
|
+
frame.style.cssText = [
|
|
1596
|
+
"width:min(100%,460px)",
|
|
1597
|
+
"height:min(100%,640px)",
|
|
1598
|
+
"border:0",
|
|
1599
|
+
"border-radius:12px",
|
|
1600
|
+
"background:#fff",
|
|
1601
|
+
"box-shadow:0 12px 30px rgba(0,0,0,0.35)"
|
|
1602
|
+
].join(";");
|
|
1603
|
+
frame.src = challengeUrl;
|
|
1604
|
+
backdrop.appendChild(frame);
|
|
1605
|
+
const returnHandler = (event) => {
|
|
1606
|
+
if (event.source !== frame.contentWindow) return;
|
|
1607
|
+
const data = event.data;
|
|
1608
|
+
if (!data || typeof data !== "object") return;
|
|
1609
|
+
const record = data;
|
|
1610
|
+
if (record["source"] !== "flopay-vault-3ds-return") return;
|
|
1611
|
+
this.hideActionRequiredOverlay();
|
|
1612
|
+
this.postActionCompleted(record["status"]);
|
|
1613
|
+
};
|
|
1614
|
+
window.addEventListener("message", returnHandler);
|
|
1615
|
+
this.threeDsReturnHandler = returnHandler;
|
|
1616
|
+
document.body.appendChild(backdrop);
|
|
1617
|
+
this.actionOverlay = backdrop;
|
|
1618
|
+
addBreadcrumb("vault 3ds challenge overlay shown");
|
|
1619
|
+
}
|
|
1620
|
+
/**
|
|
1621
|
+
* Tell the vault widget that the buyer has completed (or abandoned) the
|
|
1622
|
+
* challenge. The widget responds by POSTing `/3ds/complete` — its
|
|
1623
|
+
* sub-300ms sync resolver writes the follow-up attempt row immediately,
|
|
1624
|
+
* so the next `/status` poll resolves to a terminal outcome instead of
|
|
1625
|
+
* waiting for the eventual provider webhook.
|
|
1626
|
+
*/
|
|
1627
|
+
postActionCompleted(status) {
|
|
1628
|
+
if (!this.container) return;
|
|
1629
|
+
const iframe = this.container.querySelector("iframe");
|
|
1630
|
+
const target = iframe?.contentWindow;
|
|
1631
|
+
if (!target) return;
|
|
1632
|
+
try {
|
|
1633
|
+
target.postMessage(
|
|
1634
|
+
{
|
|
1635
|
+
source: "flopay-vault-host",
|
|
1636
|
+
type: "action_completed",
|
|
1637
|
+
status: typeof status === "string" ? status : "unknown"
|
|
1638
|
+
},
|
|
1639
|
+
"*"
|
|
1640
|
+
);
|
|
1641
|
+
} catch {
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
hideActionRequiredOverlay() {
|
|
1645
|
+
if (this.threeDsReturnHandler) {
|
|
1646
|
+
window.removeEventListener("message", this.threeDsReturnHandler);
|
|
1647
|
+
this.threeDsReturnHandler = null;
|
|
1648
|
+
}
|
|
1649
|
+
if (!this.actionOverlay) return;
|
|
1650
|
+
this.actionOverlay.parentNode?.removeChild(this.actionOverlay);
|
|
1651
|
+
this.actionOverlay = null;
|
|
1652
|
+
addBreadcrumb("vault 3ds challenge overlay hidden");
|
|
1653
|
+
}
|
|
1654
|
+
/**
|
|
1655
|
+
* Size the hosted-widget iframe to the height reported by the form inside it.
|
|
1656
|
+
* Cross-origin iframes don't auto-size to their content, so the widget posts
|
|
1657
|
+
* its measured height and we apply it here (clamped to a sane range). This is
|
|
1658
|
+
* what lets the card form shrink/grow to fit instead of sitting at a fixed
|
|
1659
|
+
* height.
|
|
1660
|
+
*/
|
|
1661
|
+
applyHeight(height) {
|
|
1662
|
+
const iframe = this.container?.querySelector("iframe");
|
|
1663
|
+
if (!iframe) return;
|
|
1664
|
+
const clamped = Math.max(0, Math.min(Math.ceil(height), 2e3));
|
|
1665
|
+
iframe.style.height = `${clamped}px`;
|
|
1666
|
+
}
|
|
1667
|
+
};
|
|
1668
|
+
|
|
1154
1669
|
// src/flopay.ts
|
|
1155
1670
|
var FloPay = class {
|
|
1156
1671
|
constructor(provider, config) {
|
|
@@ -1186,6 +1701,23 @@ var FloPay = class {
|
|
|
1186
1701
|
async confirmCardPayment(params) {
|
|
1187
1702
|
return this.provider.confirmCardPayment(params);
|
|
1188
1703
|
}
|
|
1704
|
+
/**
|
|
1705
|
+
* Create a {@link CardCaptureAdapter} for collecting card details through the
|
|
1706
|
+
* backend-rendered hosted vault PCI widget instead of provider-owned (Stripe)
|
|
1707
|
+
* card fields (TeamFloPay/backend#823).
|
|
1708
|
+
*
|
|
1709
|
+
* The returned adapter injects the server-supplied widget HTML (the session's
|
|
1710
|
+
* {@link CheckoutSession.vault} block, or one fetched via
|
|
1711
|
+
* `PaymentAPI.getVaultCapture`) and relays the widget's terminal outcome. The
|
|
1712
|
+
* backend owns tokenization, the PaymentIntent, 3DS, and fulfilment — no
|
|
1713
|
+
* Stripe.js is involved on the card path and PCI-sensitive fields never enter
|
|
1714
|
+
* the SDK runtime.
|
|
1715
|
+
*/
|
|
1716
|
+
cardCapture(options) {
|
|
1717
|
+
return new PciVaultCardCapture({
|
|
1718
|
+
sessionId: options?.sessionId
|
|
1719
|
+
});
|
|
1720
|
+
}
|
|
1189
1721
|
/** Confirm a PayPal payment: create intent via billing API → confirm → redirect if needed. */
|
|
1190
1722
|
async confirmPayPalPayment(params) {
|
|
1191
1723
|
return this.provider.confirmPayPalPayment(params);
|
|
@@ -1209,17 +1741,17 @@ var FloPay = class {
|
|
|
1209
1741
|
*/
|
|
1210
1742
|
async retrieveSession(sessionId, billingApiUrl) {
|
|
1211
1743
|
if (!sessionId) {
|
|
1212
|
-
throw new
|
|
1744
|
+
throw new import_shared5.FloPayError(
|
|
1213
1745
|
"sessionId is required to retrieve a session.",
|
|
1214
1746
|
"validation_error",
|
|
1215
1747
|
{ param: "sessionId" }
|
|
1216
1748
|
);
|
|
1217
1749
|
}
|
|
1218
|
-
const apiUrl = (0,
|
|
1750
|
+
const apiUrl = (0, import_shared5.resolveBillingApiUrl)(billingApiUrl ?? this.config.billingApiUrl);
|
|
1219
1751
|
const api = new PaymentAPI(apiUrl);
|
|
1220
1752
|
const unified = await api.getUnifiedCheckoutSession(sessionId);
|
|
1221
1753
|
if (!unified.data.session) {
|
|
1222
|
-
throw new
|
|
1754
|
+
throw new import_shared5.FloPayError("Session not found", "api_error");
|
|
1223
1755
|
}
|
|
1224
1756
|
return unified.data.session;
|
|
1225
1757
|
}
|
|
@@ -1232,13 +1764,13 @@ var FloPay = class {
|
|
|
1232
1764
|
*/
|
|
1233
1765
|
async retrieveUnifiedSession(sessionId, billingApiUrl) {
|
|
1234
1766
|
if (!sessionId) {
|
|
1235
|
-
throw new
|
|
1767
|
+
throw new import_shared5.FloPayError(
|
|
1236
1768
|
"sessionId is required.",
|
|
1237
1769
|
"validation_error",
|
|
1238
1770
|
{ param: "sessionId" }
|
|
1239
1771
|
);
|
|
1240
1772
|
}
|
|
1241
|
-
const apiUrl = (0,
|
|
1773
|
+
const apiUrl = (0, import_shared5.resolveBillingApiUrl)(billingApiUrl ?? this.config.billingApiUrl);
|
|
1242
1774
|
const api = new PaymentAPI(apiUrl);
|
|
1243
1775
|
return api.getUnifiedCheckoutSession(sessionId);
|
|
1244
1776
|
}
|
|
@@ -1262,7 +1794,7 @@ var FloPay = class {
|
|
|
1262
1794
|
var instanceCache = /* @__PURE__ */ new Map();
|
|
1263
1795
|
async function loadFloPay(publishableKey, options) {
|
|
1264
1796
|
if (!publishableKey) {
|
|
1265
|
-
throw new
|
|
1797
|
+
throw new import_shared6.FloPayError(
|
|
1266
1798
|
"A publishable key is required to initialize FloPay.",
|
|
1267
1799
|
"validation_error",
|
|
1268
1800
|
{ param: "publishableKey" }
|
|
@@ -1282,7 +1814,7 @@ async function loadFloPay(publishableKey, options) {
|
|
|
1282
1814
|
}
|
|
1283
1815
|
|
|
1284
1816
|
// src/create-checkout-session.ts
|
|
1285
|
-
var
|
|
1817
|
+
var import_shared7 = require("@flopay/shared");
|
|
1286
1818
|
var MAX_COUPON_CODES = 5;
|
|
1287
1819
|
function readString2(value) {
|
|
1288
1820
|
return typeof value === "string" && value.trim() ? value : void 0;
|
|
@@ -1291,7 +1823,7 @@ function buildCheckoutSessionError(status, payload) {
|
|
|
1291
1823
|
const nested = payload?.error;
|
|
1292
1824
|
const code = readString2(payload?.code) ?? readString2(nested?.code) ?? `http_${status}`;
|
|
1293
1825
|
const message = readString2(payload?.message) ?? readString2(nested?.message) ?? defaultMessageForCode(code, status);
|
|
1294
|
-
return new
|
|
1826
|
+
return new import_shared7.FloPayError(message, "api_error", { code, statusCode: status });
|
|
1295
1827
|
}
|
|
1296
1828
|
function defaultMessageForCode(code, status) {
|
|
1297
1829
|
switch (code) {
|
|
@@ -1324,16 +1856,16 @@ async function createCheckoutSession(options) {
|
|
|
1324
1856
|
utmMetadata
|
|
1325
1857
|
} = options;
|
|
1326
1858
|
if (couponCodes.length > MAX_COUPON_CODES) {
|
|
1327
|
-
throw new
|
|
1859
|
+
throw new import_shared7.FloPayError(
|
|
1328
1860
|
`Too many coupon codes \u2014 a checkout session accepts at most ${MAX_COUPON_CODES}.`,
|
|
1329
1861
|
"validation_error",
|
|
1330
1862
|
{ code: "CouponLimitExceeded", param: "couponCodes" }
|
|
1331
1863
|
);
|
|
1332
1864
|
}
|
|
1333
|
-
const wireProducts = products ?? (0,
|
|
1334
|
-
const sessionCurrency = (0,
|
|
1865
|
+
const wireProducts = products ?? (0, import_shared7.foldIntoProducts)(items, subscriptions);
|
|
1866
|
+
const sessionCurrency = (0, import_shared7.resolveSessionCurrency)(currency, items, subscriptions, wireProducts);
|
|
1335
1867
|
if (!sessionCurrency) {
|
|
1336
|
-
throw new
|
|
1868
|
+
throw new import_shared7.FloPayError(
|
|
1337
1869
|
"currency is required: pass `currency` on the session, or include a `currency` on the first item/subscription/product.",
|
|
1338
1870
|
"validation_error",
|
|
1339
1871
|
{ code: "CurrencyRequired", param: "currency" }
|
|
@@ -1341,12 +1873,12 @@ async function createCheckoutSession(options) {
|
|
|
1341
1873
|
}
|
|
1342
1874
|
const payload = {
|
|
1343
1875
|
clientId,
|
|
1344
|
-
checkoutVersion:
|
|
1876
|
+
checkoutVersion: import_shared7.SDK_VERSION,
|
|
1345
1877
|
successUrl,
|
|
1346
1878
|
cancelUrl,
|
|
1347
1879
|
currency: sessionCurrency,
|
|
1348
1880
|
checkoutMode,
|
|
1349
|
-
products: wireProducts.map((product) => (0,
|
|
1881
|
+
products: wireProducts.map((product) => (0, import_shared7.buildProductPayload)(product, sessionCurrency)),
|
|
1350
1882
|
accountData: {
|
|
1351
1883
|
userId: account.userId,
|
|
1352
1884
|
firstName: account.firstName ?? null,
|
|
@@ -1398,7 +1930,7 @@ async function createCheckoutSession(options) {
|
|
|
1398
1930
|
throw new Error("Checkout session created but no UUID was returned by the billing API");
|
|
1399
1931
|
}
|
|
1400
1932
|
if (!nonce) {
|
|
1401
|
-
throw new
|
|
1933
|
+
throw new import_shared7.FloPayError(
|
|
1402
1934
|
"Checkout session created but no `nonce` was returned by the billing API. Upgrade the billing service to TeamFloPay/backend#640 or later.",
|
|
1403
1935
|
"api_error",
|
|
1404
1936
|
{ code: "MissingCheckoutSessionToken" }
|
|
@@ -1466,6 +1998,7 @@ async function createCheckoutSessionWithRetries(options) {
|
|
|
1466
1998
|
FloPay,
|
|
1467
1999
|
FloPayElements,
|
|
1468
2000
|
PaymentAPI,
|
|
2001
|
+
PciVaultCardCapture,
|
|
1469
2002
|
StripeAdapter,
|
|
1470
2003
|
cacheSessionDisplayData,
|
|
1471
2004
|
clearSessionDisplayData,
|