@flopay/js 1.3.4 → 1.4.1
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 +39 -3
- package/dist/index.cjs +2095 -246
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +47 -35
- package/dist/index.d.ts +47 -35
- package/dist/index.mjs +2072 -199
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __create = Object.create;
|
|
3
2
|
var __defProp = Object.defineProperty;
|
|
4
3
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
4
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
7
5
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
6
|
var __export = (target, all) => {
|
|
9
7
|
for (var name in all)
|
|
@@ -17,14 +15,6 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
17
15
|
}
|
|
18
16
|
return to;
|
|
19
17
|
};
|
|
20
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
-
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
-
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
-
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
-
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
-
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
-
mod
|
|
27
|
-
));
|
|
28
18
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
19
|
|
|
30
20
|
// src/index.ts
|
|
@@ -45,10 +35,17 @@ __export(index_exports, {
|
|
|
45
35
|
module.exports = __toCommonJS(index_exports);
|
|
46
36
|
|
|
47
37
|
// src/load.ts
|
|
48
|
-
var
|
|
38
|
+
var import_shared7 = require("@flopay/shared");
|
|
49
39
|
|
|
50
40
|
// src/stripe-adapter.ts
|
|
41
|
+
var import_stripe_js = require("@stripe/stripe-js");
|
|
51
42
|
var import_shared = require("@flopay/shared");
|
|
43
|
+
|
|
44
|
+
// src/card-three-ds.ts
|
|
45
|
+
var CARD_THREE_DS_ATTEMPT_TTL_MS = 15 * 6e4;
|
|
46
|
+
var MAX_CARD_THREE_DS_ATTEMPTS = 32;
|
|
47
|
+
|
|
48
|
+
// src/stripe-adapter.ts
|
|
52
49
|
function toStripeElementType(type) {
|
|
53
50
|
const map = {
|
|
54
51
|
payment: "payment",
|
|
@@ -71,6 +68,9 @@ function toStripeAppearanceTheme(theme) {
|
|
|
71
68
|
return "stripe";
|
|
72
69
|
}
|
|
73
70
|
}
|
|
71
|
+
function cardThreeDsNow() {
|
|
72
|
+
return globalThis.performance?.now() ?? Date.now();
|
|
73
|
+
}
|
|
74
74
|
function wrapStripeElement(stripeElement) {
|
|
75
75
|
const el = stripeElement;
|
|
76
76
|
return {
|
|
@@ -94,11 +94,31 @@ function wrapStripeElement(stripeElement) {
|
|
|
94
94
|
}
|
|
95
95
|
};
|
|
96
96
|
}
|
|
97
|
+
function toStripeBillingDetails(billing) {
|
|
98
|
+
return {
|
|
99
|
+
billing_details: {
|
|
100
|
+
...billing.email ? { email: billing.email } : {},
|
|
101
|
+
...billing.name ? { name: billing.name } : {},
|
|
102
|
+
...billing.address ? {
|
|
103
|
+
address: {
|
|
104
|
+
...billing.address.country ? { country: billing.address.country } : {},
|
|
105
|
+
...billing.address.postal_code ? { postal_code: billing.address.postal_code } : {},
|
|
106
|
+
...billing.address.city ? { city: billing.address.city } : {},
|
|
107
|
+
...billing.address.line1 ? { line1: billing.address.line1 } : {},
|
|
108
|
+
...billing.address.line2 ? { line2: billing.address.line2 } : {},
|
|
109
|
+
...billing.address.state ? { state: billing.address.state } : {}
|
|
110
|
+
}
|
|
111
|
+
} : {}
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
}
|
|
97
115
|
var StripeAdapter = class {
|
|
98
116
|
constructor() {
|
|
99
117
|
this.name = "stripe";
|
|
100
118
|
this.stripe = null;
|
|
101
119
|
this.elements = null;
|
|
120
|
+
this.pendingCardThreeDsAttempts = /* @__PURE__ */ new Map();
|
|
121
|
+
this.cardThreeDsAttemptSequence = 0;
|
|
102
122
|
// Serialized appearance currently applied to `this.elements`. Used to detect
|
|
103
123
|
// when consumers swap themes mid-session so we can live-update the Stripe
|
|
104
124
|
// Elements group instead of returning a stale-styled cache. `null` while no
|
|
@@ -109,17 +129,7 @@ var StripeAdapter = class {
|
|
|
109
129
|
if (typeof window === "undefined") {
|
|
110
130
|
return;
|
|
111
131
|
}
|
|
112
|
-
|
|
113
|
-
try {
|
|
114
|
-
({ loadStripe } = await import("@stripe/stripe-js"));
|
|
115
|
-
} catch {
|
|
116
|
-
throw new import_shared.FloPayError(
|
|
117
|
-
"Failed to load @stripe/stripe-js. Reinstall @flopay/js.",
|
|
118
|
-
"api_error",
|
|
119
|
-
{ code: "StripeJsLoadFailed" }
|
|
120
|
-
);
|
|
121
|
-
}
|
|
122
|
-
const stripe = await loadStripe(config.publishableKey, {
|
|
132
|
+
const stripe = await (0, import_stripe_js.loadStripe)(config.publishableKey, {
|
|
123
133
|
locale: config.locale ?? "auto"
|
|
124
134
|
});
|
|
125
135
|
if (!stripe) {
|
|
@@ -230,22 +240,7 @@ var StripeAdapter = class {
|
|
|
230
240
|
};
|
|
231
241
|
}
|
|
232
242
|
const cardNumberEl = this.elements.getElement("cardNumber");
|
|
233
|
-
const stripeBilling = billingDetails ? {
|
|
234
|
-
billing_details: {
|
|
235
|
-
...billingDetails.email ? { email: billingDetails.email } : {},
|
|
236
|
-
...billingDetails.name ? { name: billingDetails.name } : {},
|
|
237
|
-
...billingDetails.address ? {
|
|
238
|
-
address: {
|
|
239
|
-
...billingDetails.address.country ? { country: billingDetails.address.country } : {},
|
|
240
|
-
...billingDetails.address.postal_code ? { postal_code: billingDetails.address.postal_code } : {},
|
|
241
|
-
...billingDetails.address.city ? { city: billingDetails.address.city } : {},
|
|
242
|
-
...billingDetails.address.line1 ? { line1: billingDetails.address.line1 } : {},
|
|
243
|
-
...billingDetails.address.line2 ? { line2: billingDetails.address.line2 } : {},
|
|
244
|
-
...billingDetails.address.state ? { state: billingDetails.address.state } : {}
|
|
245
|
-
}
|
|
246
|
-
} : {}
|
|
247
|
-
}
|
|
248
|
-
} : {};
|
|
243
|
+
const stripeBilling = billingDetails ? toStripeBillingDetails(billingDetails) : {};
|
|
249
244
|
const { error, paymentMethod } = cardNumberEl ? await this.stripe.createPaymentMethod({
|
|
250
245
|
type: "card",
|
|
251
246
|
card: cardNumberEl,
|
|
@@ -279,41 +274,84 @@ var StripeAdapter = class {
|
|
|
279
274
|
{ payment_method: params.paymentMethodId }
|
|
280
275
|
);
|
|
281
276
|
if (setupError) {
|
|
282
|
-
return {
|
|
277
|
+
return this.withCardThreeDsLifecycle(params, {
|
|
283
278
|
status: "failed",
|
|
284
279
|
error: new import_shared.FloPayError(
|
|
285
280
|
setupError.message ?? "Payment failed",
|
|
286
281
|
"api_error",
|
|
287
282
|
{ code: setupError.code, declineCode: setupError.decline_code }
|
|
288
283
|
)
|
|
289
|
-
};
|
|
284
|
+
});
|
|
290
285
|
}
|
|
291
|
-
return {
|
|
286
|
+
return this.withCardThreeDsLifecycle(params, {
|
|
292
287
|
status: setupIntent?.status ?? "failed",
|
|
293
288
|
paymentIntentId: setupIntent?.id,
|
|
294
289
|
paymentMethodId: this.extractPaymentMethodId(setupIntent?.payment_method)
|
|
295
|
-
};
|
|
290
|
+
});
|
|
296
291
|
}
|
|
297
292
|
const { error, paymentIntent } = await this.stripe.confirmCardPayment(
|
|
298
293
|
params.clientSecret,
|
|
299
294
|
{ payment_method: params.paymentMethodId }
|
|
300
295
|
);
|
|
301
296
|
if (error) {
|
|
302
|
-
return {
|
|
297
|
+
return this.withCardThreeDsLifecycle(params, {
|
|
303
298
|
status: "failed",
|
|
304
299
|
error: new import_shared.FloPayError(
|
|
305
300
|
error.message ?? "Payment failed",
|
|
306
301
|
"api_error",
|
|
307
302
|
{ code: error.code, declineCode: error.decline_code }
|
|
308
303
|
)
|
|
309
|
-
};
|
|
304
|
+
});
|
|
310
305
|
}
|
|
311
|
-
return {
|
|
306
|
+
return this.withCardThreeDsLifecycle(params, {
|
|
312
307
|
status: paymentIntent?.status ?? "failed",
|
|
313
308
|
paymentIntentId: paymentIntent?.id,
|
|
314
309
|
paymentMethodId: this.extractPaymentMethodId(paymentIntent?.payment_method)
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Bind provider-observed 3DS milestones to the exact confirmation context.
|
|
314
|
+
* The sensitive client secret/payment method pair stays only in this private,
|
|
315
|
+
* in-memory key; callers and telemetry receive an unrelated opaque id.
|
|
316
|
+
*/
|
|
317
|
+
withCardThreeDsLifecycle(params, result) {
|
|
318
|
+
const contextKey = `${params.clientSecret}\0${params.paymentMethodId}`;
|
|
319
|
+
const now = cardThreeDsNow();
|
|
320
|
+
this.pruneExpiredCardThreeDsAttempts(now);
|
|
321
|
+
if (result.status === "requires_action") {
|
|
322
|
+
let attempt2 = this.pendingCardThreeDsAttempts.get(contextKey);
|
|
323
|
+
if (!attempt2) {
|
|
324
|
+
while (this.pendingCardThreeDsAttempts.size >= MAX_CARD_THREE_DS_ATTEMPTS) {
|
|
325
|
+
const oldestContextKey = this.pendingCardThreeDsAttempts.keys().next().value;
|
|
326
|
+
if (oldestContextKey === void 0) break;
|
|
327
|
+
this.pendingCardThreeDsAttempts.delete(oldestContextKey);
|
|
328
|
+
}
|
|
329
|
+
attempt2 = {
|
|
330
|
+
attemptId: `card_3ds_${this.cardThreeDsAttemptSequence++}`,
|
|
331
|
+
startedAt: now
|
|
332
|
+
};
|
|
333
|
+
this.pendingCardThreeDsAttempts.set(contextKey, attempt2);
|
|
334
|
+
}
|
|
335
|
+
return { ...result, threeDs: { attemptId: attempt2.attemptId, status: "handoff" } };
|
|
336
|
+
}
|
|
337
|
+
const attempt = this.pendingCardThreeDsAttempts.get(contextKey);
|
|
338
|
+
if (!attempt) return result;
|
|
339
|
+
this.pendingCardThreeDsAttempts.delete(contextKey);
|
|
340
|
+
return {
|
|
341
|
+
...result,
|
|
342
|
+
threeDs: {
|
|
343
|
+
attemptId: attempt.attemptId,
|
|
344
|
+
status: result.error || result.status === "failed" ? "failed" : "returned"
|
|
345
|
+
}
|
|
315
346
|
};
|
|
316
347
|
}
|
|
348
|
+
pruneExpiredCardThreeDsAttempts(now) {
|
|
349
|
+
for (const [contextKey, attempt] of this.pendingCardThreeDsAttempts) {
|
|
350
|
+
if (now - attempt.startedAt >= CARD_THREE_DS_ATTEMPT_TTL_MS) {
|
|
351
|
+
this.pendingCardThreeDsAttempts.delete(contextKey);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
317
355
|
async confirmPayment(params) {
|
|
318
356
|
if (!this.stripe || !this.elements) {
|
|
319
357
|
throw new import_shared.FloPayError(
|
|
@@ -322,22 +360,7 @@ var StripeAdapter = class {
|
|
|
322
360
|
);
|
|
323
361
|
}
|
|
324
362
|
const billing = params.billingDetails;
|
|
325
|
-
const paymentMethodData = billing ?
|
|
326
|
-
billing_details: {
|
|
327
|
-
...billing.email ? { email: billing.email } : {},
|
|
328
|
-
...billing.name ? { name: billing.name } : {},
|
|
329
|
-
...billing.address ? {
|
|
330
|
-
address: {
|
|
331
|
-
...billing.address.country ? { country: billing.address.country } : {},
|
|
332
|
-
...billing.address.postal_code ? { postal_code: billing.address.postal_code } : {},
|
|
333
|
-
...billing.address.city ? { city: billing.address.city } : {},
|
|
334
|
-
...billing.address.line1 ? { line1: billing.address.line1 } : {},
|
|
335
|
-
...billing.address.line2 ? { line2: billing.address.line2 } : {},
|
|
336
|
-
...billing.address.state ? { state: billing.address.state } : {}
|
|
337
|
-
}
|
|
338
|
-
} : {}
|
|
339
|
-
}
|
|
340
|
-
} : void 0;
|
|
363
|
+
const paymentMethodData = billing ? toStripeBillingDetails(billing) : void 0;
|
|
341
364
|
const { error, paymentIntent } = await this.stripe.confirmPayment({
|
|
342
365
|
elements: this.elements,
|
|
343
366
|
clientSecret: params.clientSecret,
|
|
@@ -500,6 +523,7 @@ var StripeAdapter = class {
|
|
|
500
523
|
return this.stripe.elements(elementsOptions);
|
|
501
524
|
}
|
|
502
525
|
destroy() {
|
|
526
|
+
this.pendingCardThreeDsAttempts.clear();
|
|
503
527
|
this.elements = null;
|
|
504
528
|
this.appliedAppearanceKey = null;
|
|
505
529
|
this.stripe = null;
|
|
@@ -507,7 +531,7 @@ var StripeAdapter = class {
|
|
|
507
531
|
};
|
|
508
532
|
|
|
509
533
|
// src/flopay.ts
|
|
510
|
-
var
|
|
534
|
+
var import_shared6 = require("@flopay/shared");
|
|
511
535
|
|
|
512
536
|
// src/elements.ts
|
|
513
537
|
var import_shared2 = require("@flopay/shared");
|
|
@@ -559,7 +583,20 @@ var FloPayElements = class {
|
|
|
559
583
|
};
|
|
560
584
|
|
|
561
585
|
// src/payment-api.ts
|
|
562
|
-
var
|
|
586
|
+
var import_shared4 = require("@flopay/shared");
|
|
587
|
+
|
|
588
|
+
// src/api-error.ts
|
|
589
|
+
function readErrorString(value) {
|
|
590
|
+
return typeof value === "string" && value.trim() ? value : void 0;
|
|
591
|
+
}
|
|
592
|
+
function readErrorMessage(value) {
|
|
593
|
+
if (typeof value === "string") return value.trim() ? value : void 0;
|
|
594
|
+
if (Array.isArray(value)) {
|
|
595
|
+
const joined = value.filter((entry) => typeof entry === "string" && entry.trim().length > 0).join("; ");
|
|
596
|
+
return joined || void 0;
|
|
597
|
+
}
|
|
598
|
+
return void 0;
|
|
599
|
+
}
|
|
563
600
|
|
|
564
601
|
// src/session-display-cache.ts
|
|
565
602
|
var STORAGE_KEY_PREFIX = "flopay_session_display:";
|
|
@@ -627,6 +664,334 @@ function clearSessionDisplayData(sessionId) {
|
|
|
627
664
|
}
|
|
628
665
|
}
|
|
629
666
|
|
|
667
|
+
// src/telemetry-reporter.ts
|
|
668
|
+
var import_shared3 = require("@flopay/shared");
|
|
669
|
+
var TELEMETRY_PATH = "/v1/sdk-telemetry/events";
|
|
670
|
+
var MAX_BATCH_SIZE = 16;
|
|
671
|
+
var MAX_QUEUE_SIZE = 64;
|
|
672
|
+
var UPLOAD_TIMEOUT_MS = 1500;
|
|
673
|
+
var ERROR_DEDUPLICATION_WINDOW_MS = 1e3;
|
|
674
|
+
var MAX_REPORTED_FAILURES = 64;
|
|
675
|
+
var DEDUPLICATION_EVENT_ID = "00000000-0000-4000-8000-000000000000";
|
|
676
|
+
var EVENT_BUDGETS = {
|
|
677
|
+
technical_error: 8,
|
|
678
|
+
lifecycle: 32,
|
|
679
|
+
expected_outcome: 32,
|
|
680
|
+
performance: 24
|
|
681
|
+
};
|
|
682
|
+
function createUuidV4() {
|
|
683
|
+
try {
|
|
684
|
+
return globalThis.crypto.randomUUID();
|
|
685
|
+
} catch {
|
|
686
|
+
const bytes = new Uint8Array(16);
|
|
687
|
+
try {
|
|
688
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
689
|
+
} catch {
|
|
690
|
+
for (let index = 0; index < bytes.length; index += 1) {
|
|
691
|
+
bytes[index] = Math.floor(Math.random() * 256);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
bytes[6] = bytes[6] & 15 | 64;
|
|
695
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
696
|
+
const hex = [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
697
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
function bodyByteLength(body) {
|
|
701
|
+
try {
|
|
702
|
+
return new TextEncoder().encode(body).byteLength;
|
|
703
|
+
} catch {
|
|
704
|
+
return body.length;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
function failureDeduplicationKey(event) {
|
|
708
|
+
return JSON.stringify([
|
|
709
|
+
event.code,
|
|
710
|
+
event.stage,
|
|
711
|
+
event.provider,
|
|
712
|
+
event.attempt,
|
|
713
|
+
event.statusClass,
|
|
714
|
+
event.requestCategory,
|
|
715
|
+
event.paymentMethodCategory,
|
|
716
|
+
event.checkoutMode,
|
|
717
|
+
event.layout
|
|
718
|
+
]);
|
|
719
|
+
}
|
|
720
|
+
function telemetryNow() {
|
|
721
|
+
return globalThis.performance?.now() ?? 0;
|
|
722
|
+
}
|
|
723
|
+
var TelemetryReporter = class {
|
|
724
|
+
constructor(options) {
|
|
725
|
+
this.ingestionDisabled = false;
|
|
726
|
+
this.queue = [];
|
|
727
|
+
this.sequence = 0;
|
|
728
|
+
this.flushTimer = null;
|
|
729
|
+
this.flushInFlight = null;
|
|
730
|
+
this.reportedFailures = /* @__PURE__ */ new Map();
|
|
731
|
+
this.checkoutContext = {};
|
|
732
|
+
this.checkoutStartedAt = null;
|
|
733
|
+
this.destroyed = false;
|
|
734
|
+
this.pageExitHandler = () => {
|
|
735
|
+
this.drainQueue();
|
|
736
|
+
};
|
|
737
|
+
this.visibilityHandler = () => {
|
|
738
|
+
if (document.visibilityState === "hidden") void this.flush();
|
|
739
|
+
};
|
|
740
|
+
this.eventCounts = {
|
|
741
|
+
technical_error: 0,
|
|
742
|
+
lifecycle: 0,
|
|
743
|
+
expected_outcome: 0,
|
|
744
|
+
performance: 0
|
|
745
|
+
};
|
|
746
|
+
this.endpoint = `${options.billingApiUrl.replace(/\/+$/, "")}${TELEMETRY_PATH}`;
|
|
747
|
+
this.sdkPackage = options.sdkPackage ?? "@flopay/js";
|
|
748
|
+
this.sdkVersion = options.sdkVersion;
|
|
749
|
+
this.correlationId = createUuidV4();
|
|
750
|
+
this.merchantEnabled = options.enabled !== false;
|
|
751
|
+
this.clock = options.clock ?? telemetryNow;
|
|
752
|
+
this.browserTransportAvailable = typeof window !== "undefined" && typeof document !== "undefined";
|
|
753
|
+
if (this.browserTransportAvailable) {
|
|
754
|
+
window.addEventListener("pagehide", this.pageExitHandler);
|
|
755
|
+
document.addEventListener("visibilitychange", this.visibilityHandler);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
log(input) {
|
|
759
|
+
if (!this.canCollect()) return;
|
|
760
|
+
this.enqueue((0, import_shared3.buildTelemetryLogEvent)({
|
|
761
|
+
...this.checkoutContext,
|
|
762
|
+
...input,
|
|
763
|
+
eventId: createUuidV4(),
|
|
764
|
+
sequence: this.sequence++
|
|
765
|
+
}));
|
|
766
|
+
}
|
|
767
|
+
error(input) {
|
|
768
|
+
if (!this.canCollect()) return;
|
|
769
|
+
const normalizedFailure = (0, import_shared3.buildTelemetryErrorEvent)({
|
|
770
|
+
...this.checkoutContext,
|
|
771
|
+
...input,
|
|
772
|
+
eventId: DEDUPLICATION_EVENT_ID,
|
|
773
|
+
sequence: 0
|
|
774
|
+
});
|
|
775
|
+
const deduplicationKey = failureDeduplicationKey(normalizedFailure);
|
|
776
|
+
const now = this.now();
|
|
777
|
+
this.pruneReportedFailures(now);
|
|
778
|
+
const previouslyReportedAt = this.reportedFailures.get(deduplicationKey);
|
|
779
|
+
if (previouslyReportedAt !== void 0 && now >= previouslyReportedAt && now - previouslyReportedAt < ERROR_DEDUPLICATION_WINDOW_MS) {
|
|
780
|
+
this.log({
|
|
781
|
+
name: "operation.deduplicated",
|
|
782
|
+
stage: input.stage,
|
|
783
|
+
provider: input.provider,
|
|
784
|
+
paymentMethodCategory: input.paymentMethodCategory,
|
|
785
|
+
attempt: input.attempt
|
|
786
|
+
});
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
this.rememberReportedFailure(deduplicationKey, now);
|
|
790
|
+
this.enqueue((0, import_shared3.buildTelemetryErrorEvent)({
|
|
791
|
+
...this.checkoutContext,
|
|
792
|
+
...input,
|
|
793
|
+
eventId: createUuidV4(),
|
|
794
|
+
sequence: this.sequence++
|
|
795
|
+
}));
|
|
796
|
+
}
|
|
797
|
+
performance(input) {
|
|
798
|
+
if (!this.canCollect()) return;
|
|
799
|
+
this.enqueue((0, import_shared3.buildTelemetryPerformanceEvent)({
|
|
800
|
+
...this.checkoutContext,
|
|
801
|
+
...input,
|
|
802
|
+
eventId: createUuidV4(),
|
|
803
|
+
sequence: this.sequence++
|
|
804
|
+
}));
|
|
805
|
+
}
|
|
806
|
+
terminal(input) {
|
|
807
|
+
if (!this.canCollect()) return;
|
|
808
|
+
this.enqueue((0, import_shared3.buildTelemetryTerminalEvent)({
|
|
809
|
+
...this.checkoutContext,
|
|
810
|
+
...input,
|
|
811
|
+
eventId: createUuidV4(),
|
|
812
|
+
sequence: this.sequence++
|
|
813
|
+
}));
|
|
814
|
+
if (input.outcome !== "action_required" && this.checkoutStartedAt !== null) {
|
|
815
|
+
const checkoutStartedAt = this.checkoutStartedAt;
|
|
816
|
+
this.checkoutStartedAt = null;
|
|
817
|
+
this.performance({
|
|
818
|
+
stage: "total_journey",
|
|
819
|
+
durationMs: Math.max(0, this.now() - checkoutStartedAt),
|
|
820
|
+
durationMode: "total",
|
|
821
|
+
provider: input.provider,
|
|
822
|
+
paymentMethodCategory: input.paymentMethodCategory
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
/** @internal Read the reporter's monotonic clock without Resource Timing. */
|
|
827
|
+
now() {
|
|
828
|
+
try {
|
|
829
|
+
return this.clock();
|
|
830
|
+
} catch {
|
|
831
|
+
return telemetryNow();
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
pruneReportedFailures(now) {
|
|
835
|
+
for (const [key, reportedAt] of this.reportedFailures) {
|
|
836
|
+
if (now < reportedAt || now - reportedAt >= ERROR_DEDUPLICATION_WINDOW_MS) {
|
|
837
|
+
this.reportedFailures.delete(key);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
rememberReportedFailure(key, reportedAt) {
|
|
842
|
+
while (this.reportedFailures.size >= MAX_REPORTED_FAILURES) {
|
|
843
|
+
const oldest = this.reportedFailures.keys().next();
|
|
844
|
+
if (oldest.done) break;
|
|
845
|
+
this.reportedFailures.delete(oldest.value);
|
|
846
|
+
}
|
|
847
|
+
this.reportedFailures.set(key, reportedAt);
|
|
848
|
+
}
|
|
849
|
+
/** @internal Add closed checkout dimensions to subsequent SDK events. */
|
|
850
|
+
setCheckoutContext(context) {
|
|
851
|
+
this.checkoutContext = {
|
|
852
|
+
checkoutMode: context.checkoutMode,
|
|
853
|
+
layout: context.layout
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
/** @internal Start a fresh checkout budget, dedupe window, and total span. */
|
|
857
|
+
beginCheckout(context = {}) {
|
|
858
|
+
if (!this.canCollect()) return 0;
|
|
859
|
+
this.drainQueue();
|
|
860
|
+
this.setCheckoutContext(context);
|
|
861
|
+
this.sequence = 0;
|
|
862
|
+
this.reportedFailures.clear();
|
|
863
|
+
this.eventCounts = {
|
|
864
|
+
technical_error: 0,
|
|
865
|
+
lifecycle: 0,
|
|
866
|
+
expected_outcome: 0,
|
|
867
|
+
performance: 0
|
|
868
|
+
};
|
|
869
|
+
this.checkoutStartedAt = this.now();
|
|
870
|
+
return this.checkoutStartedAt;
|
|
871
|
+
}
|
|
872
|
+
enqueue(event) {
|
|
873
|
+
if (!this.canCollect()) return;
|
|
874
|
+
if (this.queue.length >= MAX_QUEUE_SIZE || this.eventCounts[event.class] >= EVENT_BUDGETS[event.class]) return;
|
|
875
|
+
this.eventCounts[event.class] += 1;
|
|
876
|
+
this.queue.push(event);
|
|
877
|
+
if (this.queue.length >= MAX_BATCH_SIZE) {
|
|
878
|
+
void this.flush();
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
this.scheduleFlush();
|
|
882
|
+
}
|
|
883
|
+
canCollect() {
|
|
884
|
+
return this.browserTransportAvailable && this.merchantEnabled && !this.ingestionDisabled && !this.destroyed;
|
|
885
|
+
}
|
|
886
|
+
/** Flush one bounded batch. Failures are intentionally dropped. */
|
|
887
|
+
async flush() {
|
|
888
|
+
if (this.flushInFlight) return this.flushInFlight;
|
|
889
|
+
if (!this.browserTransportAvailable || this.destroyed || this.ingestionDisabled || this.queue.length === 0) return;
|
|
890
|
+
this.clearFlushTimer();
|
|
891
|
+
const events = this.queue.splice(0, MAX_BATCH_SIZE);
|
|
892
|
+
this.flushInFlight = this.sendBatch(events).finally(() => {
|
|
893
|
+
this.flushInFlight = null;
|
|
894
|
+
if (this.queue.length > 0) this.scheduleFlush();
|
|
895
|
+
});
|
|
896
|
+
return this.flushInFlight;
|
|
897
|
+
}
|
|
898
|
+
/** Flush pending work and detach browser lifecycle listeners. */
|
|
899
|
+
destroy() {
|
|
900
|
+
if (this.destroyed) return;
|
|
901
|
+
this.drainQueue();
|
|
902
|
+
this.destroyed = true;
|
|
903
|
+
this.clearFlushTimer();
|
|
904
|
+
if (this.browserTransportAvailable) {
|
|
905
|
+
window.removeEventListener("pagehide", this.pageExitHandler);
|
|
906
|
+
document.removeEventListener("visibilitychange", this.visibilityHandler);
|
|
907
|
+
}
|
|
908
|
+
this.queue.splice(0);
|
|
909
|
+
this.reportedFailures.clear();
|
|
910
|
+
}
|
|
911
|
+
/** Permanently honor a merchant opt-out and discard queued events. */
|
|
912
|
+
disable() {
|
|
913
|
+
this.merchantEnabled = false;
|
|
914
|
+
this.queue.splice(0);
|
|
915
|
+
this.reportedFailures.clear();
|
|
916
|
+
this.clearFlushTimer();
|
|
917
|
+
}
|
|
918
|
+
/** Start every bounded keepalive request synchronously before page teardown. */
|
|
919
|
+
drainQueue() {
|
|
920
|
+
if (!this.browserTransportAvailable || this.destroyed || this.ingestionDisabled || this.queue.length === 0) return;
|
|
921
|
+
this.clearFlushTimer();
|
|
922
|
+
while (this.queue.length > 0) {
|
|
923
|
+
const events = this.queue.splice(0, MAX_BATCH_SIZE);
|
|
924
|
+
void this.sendBatch(events);
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
async sendBatch(events) {
|
|
928
|
+
if (!this.browserTransportAvailable || this.ingestionDisabled) return;
|
|
929
|
+
const body = (0, import_shared3.serializeTelemetryBatch)(events, {
|
|
930
|
+
correlationId: this.correlationId,
|
|
931
|
+
sdkPackage: this.sdkPackage,
|
|
932
|
+
sdkVersion: this.sdkVersion,
|
|
933
|
+
batchId: createUuidV4()
|
|
934
|
+
});
|
|
935
|
+
if (bodyByteLength(body) > import_shared3.TELEMETRY_MAX_BATCH_BYTES) return;
|
|
936
|
+
const controller = typeof AbortController === "undefined" ? null : new AbortController();
|
|
937
|
+
let timeout = null;
|
|
938
|
+
try {
|
|
939
|
+
const request = fetch(this.endpoint, {
|
|
940
|
+
method: "POST",
|
|
941
|
+
headers: { "content-type": "text/plain;charset=UTF-8" },
|
|
942
|
+
body,
|
|
943
|
+
credentials: "omit",
|
|
944
|
+
keepalive: true,
|
|
945
|
+
referrerPolicy: "no-referrer",
|
|
946
|
+
signal: controller?.signal
|
|
947
|
+
}).then(async (response) => {
|
|
948
|
+
if (response.status !== 202) return null;
|
|
949
|
+
const payload = await response.json().catch(() => null);
|
|
950
|
+
return payload?.status === "disabled" ? "disabled" : null;
|
|
951
|
+
}).catch(() => null);
|
|
952
|
+
const expired = new Promise((resolve) => {
|
|
953
|
+
timeout = setTimeout(() => {
|
|
954
|
+
controller?.abort();
|
|
955
|
+
resolve(null);
|
|
956
|
+
}, UPLOAD_TIMEOUT_MS);
|
|
957
|
+
});
|
|
958
|
+
const status = await Promise.race([request, expired]);
|
|
959
|
+
if (status === "disabled") this.disableFromIngestion();
|
|
960
|
+
} catch {
|
|
961
|
+
} finally {
|
|
962
|
+
if (timeout) clearTimeout(timeout);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
disableFromIngestion() {
|
|
966
|
+
this.ingestionDisabled = true;
|
|
967
|
+
this.queue.splice(0);
|
|
968
|
+
this.reportedFailures.clear();
|
|
969
|
+
this.clearFlushTimer();
|
|
970
|
+
}
|
|
971
|
+
scheduleFlush() {
|
|
972
|
+
if (this.flushTimer || this.destroyed || this.ingestionDisabled) return;
|
|
973
|
+
this.flushTimer = setTimeout(() => {
|
|
974
|
+
this.flushTimer = null;
|
|
975
|
+
void this.flush();
|
|
976
|
+
}, 0);
|
|
977
|
+
}
|
|
978
|
+
clearFlushTimer() {
|
|
979
|
+
if (!this.flushTimer) return;
|
|
980
|
+
clearTimeout(this.flushTimer);
|
|
981
|
+
this.flushTimer = null;
|
|
982
|
+
}
|
|
983
|
+
};
|
|
984
|
+
var TELEMETRY_REPORTER_FACTORY = /* @__PURE__ */ Symbol.for("@flopay/js.telemetry.reporter-factory.v1");
|
|
985
|
+
var telemetryGlobal = globalThis;
|
|
986
|
+
if (telemetryGlobal[TELEMETRY_REPORTER_FACTORY] === void 0) {
|
|
987
|
+
Object.defineProperty(telemetryGlobal, TELEMETRY_REPORTER_FACTORY, {
|
|
988
|
+
configurable: true,
|
|
989
|
+
enumerable: false,
|
|
990
|
+
writable: false,
|
|
991
|
+
value: (options) => new TelemetryReporter(options)
|
|
992
|
+
});
|
|
993
|
+
}
|
|
994
|
+
|
|
630
995
|
// src/payment-api.ts
|
|
631
996
|
var DEFAULT_PROCESSING_RETRY_AFTER_MS = 1e3;
|
|
632
997
|
var MIN_PROCESSING_RETRY_AFTER_MS = 500;
|
|
@@ -636,18 +1001,30 @@ var DEFAULT_ACCOUNT_SNAPSHOT_TIMEOUT_MS = 1e4;
|
|
|
636
1001
|
function isRecord(value) {
|
|
637
1002
|
return typeof value === "object" && value !== null;
|
|
638
1003
|
}
|
|
1004
|
+
function telemetryStatusClass(status) {
|
|
1005
|
+
if (status === void 0) return "network_error";
|
|
1006
|
+
const statusClass = `${Math.floor(status / 100)}xx`;
|
|
1007
|
+
return statusClass === "2xx" || statusClass === "3xx" || statusClass === "4xx" || statusClass === "5xx" ? statusClass : "unknown";
|
|
1008
|
+
}
|
|
1009
|
+
function telemetryFailure(error, fallbackCode) {
|
|
1010
|
+
if (error instanceof Error && (error.name === "AbortError" || error instanceof import_shared4.FloPayError && error.code === "checkout_processing_timeout")) {
|
|
1011
|
+
return { errorCode: "REQUEST_TIMEOUT", statusClass: "timeout" };
|
|
1012
|
+
}
|
|
1013
|
+
if (error instanceof TypeError) {
|
|
1014
|
+
return { errorCode: "NETWORK_REQUEST_FAILED", statusClass: "network_error" };
|
|
1015
|
+
}
|
|
1016
|
+
return {
|
|
1017
|
+
errorCode: fallbackCode,
|
|
1018
|
+
statusClass: telemetryStatusClass(
|
|
1019
|
+
error instanceof import_shared4.FloPayError ? error.statusCode : void 0
|
|
1020
|
+
)
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
639
1023
|
function readString(payload, key) {
|
|
640
|
-
|
|
641
|
-
return typeof value === "string" && value.trim() ? value : void 0;
|
|
1024
|
+
return readErrorString(payload?.[key]);
|
|
642
1025
|
}
|
|
643
1026
|
function readMessage(payload, key) {
|
|
644
|
-
|
|
645
|
-
if (typeof value === "string") return value.trim() ? value : void 0;
|
|
646
|
-
if (Array.isArray(value)) {
|
|
647
|
-
const joined = value.filter((entry) => typeof entry === "string" && entry.trim().length > 0).join("; ");
|
|
648
|
-
return joined || void 0;
|
|
649
|
-
}
|
|
650
|
-
return void 0;
|
|
1027
|
+
return readErrorMessage(payload?.[key]);
|
|
651
1028
|
}
|
|
652
1029
|
function readNumber(payload, key) {
|
|
653
1030
|
const value = payload?.[key];
|
|
@@ -657,7 +1034,7 @@ function delay(ms) {
|
|
|
657
1034
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
658
1035
|
}
|
|
659
1036
|
function createCheckoutProcessingTimeoutError() {
|
|
660
|
-
return new
|
|
1037
|
+
return new import_shared4.FloPayError(
|
|
661
1038
|
"Checkout is still processing. Please try again shortly.",
|
|
662
1039
|
"api_error",
|
|
663
1040
|
{ code: "checkout_processing_timeout" }
|
|
@@ -668,14 +1045,14 @@ async function buildApiErrorFromResponse(response, fallbackMessage) {
|
|
|
668
1045
|
const nestedError = isRecord(payload?.error) ? payload.error : null;
|
|
669
1046
|
const message = readMessage(payload, "message") ?? readMessage(nestedError, "message") ?? fallbackMessage;
|
|
670
1047
|
const code = readString(payload, "code") ?? readString(payload, "gatewayErrorCode") ?? readString(nestedError, "code") ?? `http_${response.status}`;
|
|
671
|
-
return new
|
|
1048
|
+
return new import_shared4.FloPayError(message, "api_error", {
|
|
672
1049
|
code,
|
|
673
1050
|
statusCode: response.status
|
|
674
1051
|
});
|
|
675
1052
|
}
|
|
676
1053
|
var NETWORK_RETRY_ATTEMPTS = 2;
|
|
677
1054
|
var IDEMPOTENCY_IN_PROGRESS_RETRY_ATTEMPTS = 2;
|
|
678
|
-
async function fetchWithNetworkRetry(input, init, attempts = NETWORK_RETRY_ATTEMPTS) {
|
|
1055
|
+
async function fetchWithNetworkRetry(input, init, attempts = NETWORK_RETRY_ATTEMPTS, onRetry) {
|
|
679
1056
|
let lastErr;
|
|
680
1057
|
for (let attempt = 0; ; attempt++) {
|
|
681
1058
|
try {
|
|
@@ -684,13 +1061,59 @@ async function fetchWithNetworkRetry(input, init, attempts = NETWORK_RETRY_ATTEM
|
|
|
684
1061
|
if (err instanceof Error && err.name === "AbortError") throw err;
|
|
685
1062
|
lastErr = err;
|
|
686
1063
|
if (attempt >= attempts) throw lastErr;
|
|
1064
|
+
try {
|
|
1065
|
+
onRetry?.(attempt + 1);
|
|
1066
|
+
} catch {
|
|
1067
|
+
}
|
|
687
1068
|
await delay(150 * 2 ** attempt);
|
|
688
1069
|
}
|
|
689
1070
|
}
|
|
690
1071
|
}
|
|
1072
|
+
function isPaymentApiTelemetryHooks(value) {
|
|
1073
|
+
return "now" in value || "onFirstByte" in value || "onSessionCreateFailure" in value || "onRetry" in value;
|
|
1074
|
+
}
|
|
691
1075
|
var PaymentAPI = class {
|
|
692
|
-
constructor(billingApiUrl) {
|
|
1076
|
+
constructor(billingApiUrl, telemetryOptionsOrHooks = {}) {
|
|
693
1077
|
this.baseUrl = billingApiUrl.replace(/\/+$/, "");
|
|
1078
|
+
const hasInternalHooks = isPaymentApiTelemetryHooks(telemetryOptionsOrHooks);
|
|
1079
|
+
this.telemetryHooks = hasInternalHooks ? telemetryOptionsOrHooks : void 0;
|
|
1080
|
+
this.directTelemetry = hasInternalHooks || telemetryOptionsOrHooks.telemetry === false ? void 0 : new TelemetryReporter({
|
|
1081
|
+
billingApiUrl: this.baseUrl,
|
|
1082
|
+
sdkVersion: import_shared4.SDK_VERSION
|
|
1083
|
+
});
|
|
1084
|
+
}
|
|
1085
|
+
/** Dispose the reporter owned by direct public usage. Internal hooks are never disposed here. */
|
|
1086
|
+
destroy() {
|
|
1087
|
+
this.directTelemetry?.destroy();
|
|
1088
|
+
}
|
|
1089
|
+
reportDirectFailure(error, fallbackCode, stage, requestCategory, paymentMethodCategory = "unknown") {
|
|
1090
|
+
const failure = telemetryFailure(error, fallbackCode);
|
|
1091
|
+
this.directTelemetry?.error({
|
|
1092
|
+
...failure,
|
|
1093
|
+
stage,
|
|
1094
|
+
requestCategory,
|
|
1095
|
+
paymentMethodCategory
|
|
1096
|
+
});
|
|
1097
|
+
}
|
|
1098
|
+
telemetryTimestamp() {
|
|
1099
|
+
try {
|
|
1100
|
+
return this.telemetryHooks?.now?.() ?? this.directTelemetry?.now() ?? telemetryNow();
|
|
1101
|
+
} catch {
|
|
1102
|
+
return telemetryNow();
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
beginDirectTelemetryCheckout(checkoutSessionId) {
|
|
1106
|
+
if (!this.directTelemetry || this.directTelemetryCheckoutId === checkoutSessionId) return;
|
|
1107
|
+
this.directTelemetryCheckoutId = checkoutSessionId;
|
|
1108
|
+
this.directTelemetry.beginCheckout();
|
|
1109
|
+
}
|
|
1110
|
+
beginDirectTelemetryOperation() {
|
|
1111
|
+
if (!this.directTelemetry) return;
|
|
1112
|
+
this.directTelemetryCheckoutId = void 0;
|
|
1113
|
+
this.directTelemetry.beginCheckout();
|
|
1114
|
+
}
|
|
1115
|
+
adoptDirectTelemetryCheckout(checkoutSessionId) {
|
|
1116
|
+
if (checkoutSessionId) this.directTelemetryCheckoutId = checkoutSessionId;
|
|
694
1117
|
}
|
|
695
1118
|
/**
|
|
696
1119
|
* Fetch a raw checkout session by ID.
|
|
@@ -702,17 +1125,77 @@ var PaymentAPI = class {
|
|
|
702
1125
|
* Backends that don't yet enforce it ignore the extra header.
|
|
703
1126
|
*/
|
|
704
1127
|
async getCheckoutSession(checkoutSessionId, nonce) {
|
|
705
|
-
|
|
1128
|
+
this.beginDirectTelemetryCheckout(checkoutSessionId);
|
|
1129
|
+
const requestStarted = this.telemetryTimestamp();
|
|
1130
|
+
this.directTelemetry?.log({
|
|
1131
|
+
name: "session.read.started",
|
|
1132
|
+
stage: "session_read",
|
|
1133
|
+
requestCategory: "session_read"
|
|
1134
|
+
});
|
|
1135
|
+
const headers = { [import_shared4.FLO_SDK_VERSION_HEADER]: import_shared4.SDK_VERSION };
|
|
706
1136
|
if (nonce) headers["x-checkout-session-token"] = nonce;
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
1137
|
+
try {
|
|
1138
|
+
const response = await fetchWithNetworkRetry(
|
|
1139
|
+
`${this.baseUrl}/v1/checkouts/sessions/${encodeURIComponent(checkoutSessionId)}`,
|
|
1140
|
+
{ headers },
|
|
1141
|
+
NETWORK_RETRY_ATTEMPTS,
|
|
1142
|
+
(attempt) => {
|
|
1143
|
+
this.telemetryHooks?.onRetry?.("session_read", attempt);
|
|
1144
|
+
this.directTelemetry?.log({
|
|
1145
|
+
name: "operation.retry",
|
|
1146
|
+
stage: "session_read",
|
|
1147
|
+
requestCategory: "session_read",
|
|
1148
|
+
attempt
|
|
1149
|
+
});
|
|
1150
|
+
}
|
|
1151
|
+
);
|
|
1152
|
+
const firstByteDuration = Math.max(0, this.telemetryTimestamp() - requestStarted);
|
|
1153
|
+
try {
|
|
1154
|
+
this.telemetryHooks?.onFirstByte?.(firstByteDuration);
|
|
1155
|
+
} catch {
|
|
1156
|
+
}
|
|
1157
|
+
const statusClass = `${Math.floor(response.status / 100)}xx`;
|
|
1158
|
+
this.directTelemetry?.log({
|
|
1159
|
+
name: "session.request.first_byte",
|
|
1160
|
+
stage: "session_first_byte",
|
|
1161
|
+
requestCategory: "session_read",
|
|
1162
|
+
statusClass
|
|
1163
|
+
});
|
|
1164
|
+
this.directTelemetry?.performance({
|
|
1165
|
+
stage: "session_first_byte",
|
|
1166
|
+
durationMs: firstByteDuration,
|
|
1167
|
+
durationMode: "machine",
|
|
1168
|
+
requestCategory: "session_read",
|
|
1169
|
+
statusClass
|
|
1170
|
+
});
|
|
1171
|
+
if (!response.ok) {
|
|
1172
|
+
throw await buildApiErrorFromResponse(response, "Failed to get checkout session");
|
|
1173
|
+
}
|
|
1174
|
+
const body = await response.json();
|
|
1175
|
+
this.directTelemetry?.log({
|
|
1176
|
+
name: "session.request.completed",
|
|
1177
|
+
stage: "session_complete",
|
|
1178
|
+
requestCategory: "session_read",
|
|
1179
|
+
statusClass
|
|
1180
|
+
});
|
|
1181
|
+
this.directTelemetry?.performance({
|
|
1182
|
+
stage: "session_complete",
|
|
1183
|
+
durationMs: Math.max(0, this.telemetryTimestamp() - requestStarted),
|
|
1184
|
+
durationMode: "machine",
|
|
1185
|
+
requestCategory: "session_read",
|
|
1186
|
+
statusClass
|
|
1187
|
+
});
|
|
1188
|
+
return { ...body, data: this.mergeCachedDisplayData(body.data) };
|
|
1189
|
+
} catch (error) {
|
|
1190
|
+
const statusCode = error instanceof import_shared4.FloPayError ? error.statusCode : void 0;
|
|
1191
|
+
this.directTelemetry?.error({
|
|
1192
|
+
errorCode: error instanceof import_shared4.FloPayError && error.code === "checkout_processing_timeout" ? "REQUEST_TIMEOUT" : "NETWORK_REQUEST_FAILED",
|
|
1193
|
+
stage: "session_read",
|
|
1194
|
+
requestCategory: "session_read",
|
|
1195
|
+
statusClass: statusCode ? `${Math.floor(statusCode / 100)}xx` : "network_error"
|
|
1196
|
+
});
|
|
1197
|
+
throw error;
|
|
713
1198
|
}
|
|
714
|
-
const body = await response.json();
|
|
715
|
-
return { ...body, data: this.mergeCachedDisplayData(body.data) };
|
|
716
1199
|
}
|
|
717
1200
|
/**
|
|
718
1201
|
* Stash display-only data for a session so subsequent fetches can fill in
|
|
@@ -768,17 +1251,55 @@ var PaymentAPI = class {
|
|
|
768
1251
|
* backends, matched against the session's stored nonce).
|
|
769
1252
|
*/
|
|
770
1253
|
async getVaultCapture(checkoutSessionId, nonce) {
|
|
1254
|
+
this.beginDirectTelemetryCheckout(checkoutSessionId);
|
|
1255
|
+
const startedAt = this.telemetryTimestamp();
|
|
1256
|
+
this.directTelemetry?.log({
|
|
1257
|
+
name: "vault.capture.requested",
|
|
1258
|
+
stage: "vault_request",
|
|
1259
|
+
requestCategory: "vault_capture",
|
|
1260
|
+
paymentMethodCategory: "card"
|
|
1261
|
+
});
|
|
771
1262
|
const headers = { "Content-Type": "application/json" };
|
|
772
1263
|
if (nonce) headers["x-checkout-session-token"] = nonce;
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
1264
|
+
try {
|
|
1265
|
+
const response = await fetchWithNetworkRetry(
|
|
1266
|
+
`${this.baseUrl}/v1/checkouts/sessions/${encodeURIComponent(checkoutSessionId)}/vault/capture`,
|
|
1267
|
+
{ method: "POST", headers },
|
|
1268
|
+
NETWORK_RETRY_ATTEMPTS,
|
|
1269
|
+
(attempt) => {
|
|
1270
|
+
this.telemetryHooks?.onRetry?.("vault_capture", attempt);
|
|
1271
|
+
this.directTelemetry?.log({
|
|
1272
|
+
name: "operation.retry",
|
|
1273
|
+
stage: "vault_request",
|
|
1274
|
+
requestCategory: "vault_capture",
|
|
1275
|
+
paymentMethodCategory: "card",
|
|
1276
|
+
attempt
|
|
1277
|
+
});
|
|
1278
|
+
}
|
|
1279
|
+
);
|
|
1280
|
+
if (!response.ok) {
|
|
1281
|
+
throw await buildApiErrorFromResponse(response, "Failed to load the secure card form");
|
|
1282
|
+
}
|
|
1283
|
+
const block = await response.json();
|
|
1284
|
+
this.directTelemetry?.performance({
|
|
1285
|
+
stage: "vault_request",
|
|
1286
|
+
durationMs: Math.max(0, this.telemetryTimestamp() - startedAt),
|
|
1287
|
+
durationMode: "machine",
|
|
1288
|
+
requestCategory: "vault_capture",
|
|
1289
|
+
paymentMethodCategory: "card",
|
|
1290
|
+
statusClass: "2xx"
|
|
1291
|
+
});
|
|
1292
|
+
return this.toVaultBlock(block);
|
|
1293
|
+
} catch (error) {
|
|
1294
|
+
this.reportDirectFailure(
|
|
1295
|
+
error,
|
|
1296
|
+
"VAULT_LOAD_FAILED",
|
|
1297
|
+
"vault_request",
|
|
1298
|
+
"vault_capture",
|
|
1299
|
+
"card"
|
|
1300
|
+
);
|
|
1301
|
+
throw error;
|
|
779
1302
|
}
|
|
780
|
-
const block = await response.json();
|
|
781
|
-
return this.toVaultBlock(block);
|
|
782
1303
|
}
|
|
783
1304
|
/**
|
|
784
1305
|
* Fetch and normalize a checkout session.
|
|
@@ -814,26 +1335,88 @@ var PaymentAPI = class {
|
|
|
814
1335
|
* future major version.
|
|
815
1336
|
*/
|
|
816
1337
|
async processPayment(_userId, data, options) {
|
|
1338
|
+
this.beginDirectTelemetryCheckout(data.sessionId);
|
|
817
1339
|
if (!data.nonce) {
|
|
818
|
-
|
|
1340
|
+
this.directTelemetry?.terminal({
|
|
1341
|
+
outcome: "validation_rejected",
|
|
1342
|
+
stage: "processing",
|
|
1343
|
+
requestCategory: "process_payment"
|
|
1344
|
+
});
|
|
1345
|
+
throw new import_shared4.FloPayError(
|
|
819
1346
|
"processPayment requires `nonce` \u2014 pass the value returned from session creation.",
|
|
820
1347
|
"validation_error",
|
|
821
1348
|
{ code: "MissingCheckoutSessionToken", param: "nonce" }
|
|
822
1349
|
);
|
|
823
1350
|
}
|
|
1351
|
+
const startedAt = this.telemetryTimestamp();
|
|
1352
|
+
this.directTelemetry?.log({
|
|
1353
|
+
name: "payment.processing.started",
|
|
1354
|
+
stage: "processing",
|
|
1355
|
+
requestCategory: "process_payment"
|
|
1356
|
+
});
|
|
824
1357
|
const { nonce, ...processBody } = data;
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
1358
|
+
let response;
|
|
1359
|
+
try {
|
|
1360
|
+
response = await fetch(
|
|
1361
|
+
`${this.baseUrl}/v1/checkouts/sessions/${encodeURIComponent(data.sessionId)}/process`,
|
|
1362
|
+
{
|
|
1363
|
+
method: "POST",
|
|
1364
|
+
headers: {
|
|
1365
|
+
"Content-Type": "application/json",
|
|
1366
|
+
"x-checkout-session-token": nonce
|
|
1367
|
+
},
|
|
1368
|
+
body: JSON.stringify(processBody)
|
|
1369
|
+
}
|
|
1370
|
+
);
|
|
1371
|
+
} catch (error) {
|
|
1372
|
+
this.reportDirectFailure(
|
|
1373
|
+
error,
|
|
1374
|
+
"PAYMENT_PROCESSING_FAILED",
|
|
1375
|
+
"processing",
|
|
1376
|
+
"process_payment"
|
|
1377
|
+
);
|
|
1378
|
+
throw error;
|
|
1379
|
+
}
|
|
1380
|
+
if (!response.ok && response.status !== 202) {
|
|
1381
|
+
this.directTelemetry?.error({
|
|
1382
|
+
errorCode: "PAYMENT_PROCESSING_FAILED",
|
|
1383
|
+
stage: "processing",
|
|
1384
|
+
requestCategory: "process_payment",
|
|
1385
|
+
statusClass: telemetryStatusClass(response.status)
|
|
1386
|
+
});
|
|
1387
|
+
return response;
|
|
1388
|
+
}
|
|
1389
|
+
try {
|
|
1390
|
+
const result = await this.resolveProcessResponse(
|
|
1391
|
+
response,
|
|
1392
|
+
data.sessionId,
|
|
1393
|
+
{ ...options, nonce }
|
|
1394
|
+
);
|
|
1395
|
+
this.directTelemetry?.log({
|
|
1396
|
+
name: "payment.processing.completed",
|
|
1397
|
+
stage: "processing",
|
|
1398
|
+
requestCategory: "process_payment",
|
|
1399
|
+
statusClass: telemetryStatusClass(result.status)
|
|
1400
|
+
});
|
|
1401
|
+
this.directTelemetry?.performance({
|
|
1402
|
+
stage: "processing",
|
|
1403
|
+
durationMs: Math.max(0, this.telemetryTimestamp() - startedAt),
|
|
1404
|
+
durationMode: "machine",
|
|
1405
|
+
requestCategory: "process_payment",
|
|
1406
|
+
statusClass: telemetryStatusClass(result.status)
|
|
1407
|
+
});
|
|
1408
|
+
return result;
|
|
1409
|
+
} catch (error) {
|
|
1410
|
+
if (!(error instanceof import_shared4.FloPayError && error.code === "checkout_processing_timeout")) {
|
|
1411
|
+
this.reportDirectFailure(
|
|
1412
|
+
error,
|
|
1413
|
+
"PAYMENT_PROCESSING_FAILED",
|
|
1414
|
+
"processing",
|
|
1415
|
+
"process_payment"
|
|
1416
|
+
);
|
|
834
1417
|
}
|
|
835
|
-
|
|
836
|
-
|
|
1418
|
+
throw error;
|
|
1419
|
+
}
|
|
837
1420
|
}
|
|
838
1421
|
/**
|
|
839
1422
|
* Patch the buyer's account snapshot (email, name, billing address, AVS
|
|
@@ -857,6 +1440,13 @@ var PaymentAPI = class {
|
|
|
857
1440
|
* AVS-protected charge to decline downstream.
|
|
858
1441
|
*/
|
|
859
1442
|
async patchAccountSnapshot(sessionId, nonce, body, options) {
|
|
1443
|
+
this.beginDirectTelemetryCheckout(sessionId);
|
|
1444
|
+
const startedAt = this.telemetryTimestamp();
|
|
1445
|
+
this.directTelemetry?.log({
|
|
1446
|
+
name: "operation.state_transition",
|
|
1447
|
+
stage: "processing",
|
|
1448
|
+
requestCategory: "account_snapshot"
|
|
1449
|
+
});
|
|
860
1450
|
const timeoutMs = options?.timeoutMs ?? DEFAULT_ACCOUNT_SNAPSHOT_TIMEOUT_MS;
|
|
861
1451
|
const controller = new AbortController();
|
|
862
1452
|
const onCallerAbort = () => controller.abort();
|
|
@@ -865,26 +1455,53 @@ var PaymentAPI = class {
|
|
|
865
1455
|
else options.signal.addEventListener("abort", onCallerAbort, { once: true });
|
|
866
1456
|
}
|
|
867
1457
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
868
|
-
let response;
|
|
869
1458
|
try {
|
|
870
|
-
response
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
1459
|
+
let response;
|
|
1460
|
+
try {
|
|
1461
|
+
response = await fetchWithNetworkRetry(
|
|
1462
|
+
`${this.baseUrl}/v1/checkouts/sessions/${encodeURIComponent(sessionId)}/account`,
|
|
1463
|
+
{
|
|
1464
|
+
method: "PATCH",
|
|
1465
|
+
headers: {
|
|
1466
|
+
"Content-Type": "application/json",
|
|
1467
|
+
"x-checkout-session-token": nonce
|
|
1468
|
+
},
|
|
1469
|
+
body: JSON.stringify(body),
|
|
1470
|
+
signal: controller.signal
|
|
877
1471
|
},
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
1472
|
+
NETWORK_RETRY_ATTEMPTS,
|
|
1473
|
+
(attempt) => {
|
|
1474
|
+
this.telemetryHooks?.onRetry?.("account_snapshot", attempt);
|
|
1475
|
+
this.directTelemetry?.log({
|
|
1476
|
+
name: "operation.retry",
|
|
1477
|
+
stage: "processing",
|
|
1478
|
+
requestCategory: "account_snapshot",
|
|
1479
|
+
attempt
|
|
1480
|
+
});
|
|
1481
|
+
}
|
|
1482
|
+
);
|
|
1483
|
+
} finally {
|
|
1484
|
+
clearTimeout(timer);
|
|
1485
|
+
options?.signal?.removeEventListener("abort", onCallerAbort);
|
|
1486
|
+
}
|
|
1487
|
+
if (!response.ok) {
|
|
1488
|
+
throw await buildApiErrorFromResponse(response, "Failed to persist account snapshot");
|
|
1489
|
+
}
|
|
1490
|
+
this.directTelemetry?.performance({
|
|
1491
|
+
stage: "processing",
|
|
1492
|
+
durationMs: Math.max(0, this.telemetryTimestamp() - startedAt),
|
|
1493
|
+
durationMode: "machine",
|
|
1494
|
+
requestCategory: "account_snapshot",
|
|
1495
|
+
statusClass: "2xx"
|
|
1496
|
+
});
|
|
1497
|
+
} catch (error) {
|
|
1498
|
+
this.reportDirectFailure(
|
|
1499
|
+
error,
|
|
1500
|
+
"NETWORK_REQUEST_FAILED",
|
|
1501
|
+
"processing",
|
|
1502
|
+
"account_snapshot"
|
|
881
1503
|
);
|
|
882
|
-
|
|
883
|
-
clearTimeout(timer);
|
|
884
|
-
options?.signal?.removeEventListener("abort", onCallerAbort);
|
|
885
|
-
}
|
|
886
|
-
if (!response.ok) {
|
|
887
|
-
throw await buildApiErrorFromResponse(response, "Failed to persist account snapshot");
|
|
1504
|
+
throw error;
|
|
888
1505
|
}
|
|
889
1506
|
}
|
|
890
1507
|
/**
|
|
@@ -906,22 +1523,62 @@ var PaymentAPI = class {
|
|
|
906
1523
|
* passing the concrete payment method id / type.
|
|
907
1524
|
*/
|
|
908
1525
|
async createPaymentIntent(sessionId, email, paymentMethodType, options) {
|
|
1526
|
+
this.beginDirectTelemetryCheckout(sessionId);
|
|
1527
|
+
const startedAt = this.telemetryTimestamp();
|
|
1528
|
+
this.directTelemetry?.log({
|
|
1529
|
+
name: "payment.intent.started",
|
|
1530
|
+
stage: "processing",
|
|
1531
|
+
requestCategory: "intent_create"
|
|
1532
|
+
});
|
|
909
1533
|
const headers = { "Content-Type": "application/json" };
|
|
910
1534
|
if (options?.nonce) headers["x-checkout-session-token"] = options.nonce;
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
1535
|
+
try {
|
|
1536
|
+
const response = await fetch(
|
|
1537
|
+
`${this.baseUrl}/v1/checkouts/payments/intents`,
|
|
1538
|
+
{
|
|
1539
|
+
method: "POST",
|
|
1540
|
+
headers,
|
|
1541
|
+
body: JSON.stringify({
|
|
1542
|
+
sessionId,
|
|
1543
|
+
email,
|
|
1544
|
+
paymentMethodType: paymentMethodType ?? null,
|
|
1545
|
+
isPaypal: options?.isPaypal ?? false
|
|
1546
|
+
}),
|
|
1547
|
+
signal: options?.signal
|
|
1548
|
+
}
|
|
1549
|
+
);
|
|
1550
|
+
if (!response.ok) {
|
|
1551
|
+
this.directTelemetry?.error({
|
|
1552
|
+
errorCode: "PAYMENT_PROCESSING_FAILED",
|
|
1553
|
+
stage: "processing",
|
|
1554
|
+
requestCategory: "intent_create",
|
|
1555
|
+
statusClass: telemetryStatusClass(response.status)
|
|
1556
|
+
});
|
|
1557
|
+
return response;
|
|
923
1558
|
}
|
|
924
|
-
|
|
1559
|
+
this.directTelemetry?.log({
|
|
1560
|
+
name: "payment.intent.completed",
|
|
1561
|
+
stage: "processing",
|
|
1562
|
+
requestCategory: "intent_create",
|
|
1563
|
+
statusClass: telemetryStatusClass(response.status)
|
|
1564
|
+
});
|
|
1565
|
+
this.directTelemetry?.performance({
|
|
1566
|
+
stage: "processing",
|
|
1567
|
+
durationMs: Math.max(0, this.telemetryTimestamp() - startedAt),
|
|
1568
|
+
durationMode: "machine",
|
|
1569
|
+
requestCategory: "intent_create",
|
|
1570
|
+
statusClass: telemetryStatusClass(response.status)
|
|
1571
|
+
});
|
|
1572
|
+
return response;
|
|
1573
|
+
} catch (error) {
|
|
1574
|
+
this.reportDirectFailure(
|
|
1575
|
+
error,
|
|
1576
|
+
"PAYMENT_PROCESSING_FAILED",
|
|
1577
|
+
"processing",
|
|
1578
|
+
"intent_create"
|
|
1579
|
+
);
|
|
1580
|
+
throw error;
|
|
1581
|
+
}
|
|
925
1582
|
}
|
|
926
1583
|
/**
|
|
927
1584
|
* Create a SetupIntent for saving payment methods.
|
|
@@ -930,23 +1587,71 @@ var PaymentAPI = class {
|
|
|
930
1587
|
* required by post-#640 backends, ignored by earlier versions.
|
|
931
1588
|
*/
|
|
932
1589
|
async createSetupIntent(sessionId, email, paymentMethodType, options) {
|
|
1590
|
+
this.beginDirectTelemetryCheckout(sessionId);
|
|
1591
|
+
const startedAt = this.telemetryTimestamp();
|
|
1592
|
+
this.directTelemetry?.log({
|
|
1593
|
+
name: "payment.intent.started",
|
|
1594
|
+
stage: "processing",
|
|
1595
|
+
requestCategory: "intent_create"
|
|
1596
|
+
});
|
|
933
1597
|
const headers = { "Content-Type": "application/json" };
|
|
934
1598
|
if (options?.nonce) headers["x-checkout-session-token"] = options.nonce;
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
1599
|
+
try {
|
|
1600
|
+
const response = await fetch(
|
|
1601
|
+
`${this.baseUrl}/v1/checkouts/payments/setup-intents`,
|
|
1602
|
+
{
|
|
1603
|
+
method: "POST",
|
|
1604
|
+
headers,
|
|
1605
|
+
body: JSON.stringify({ sessionId, email, paymentMethodType }),
|
|
1606
|
+
signal: options?.signal
|
|
1607
|
+
}
|
|
1608
|
+
);
|
|
1609
|
+
if (!response.ok) {
|
|
1610
|
+
this.directTelemetry?.error({
|
|
1611
|
+
errorCode: "PAYMENT_PROCESSING_FAILED",
|
|
1612
|
+
stage: "processing",
|
|
1613
|
+
requestCategory: "intent_create",
|
|
1614
|
+
statusClass: telemetryStatusClass(response.status)
|
|
1615
|
+
});
|
|
1616
|
+
return response;
|
|
942
1617
|
}
|
|
943
|
-
|
|
1618
|
+
this.directTelemetry?.log({
|
|
1619
|
+
name: "payment.intent.completed",
|
|
1620
|
+
stage: "processing",
|
|
1621
|
+
requestCategory: "intent_create",
|
|
1622
|
+
statusClass: telemetryStatusClass(response.status)
|
|
1623
|
+
});
|
|
1624
|
+
this.directTelemetry?.performance({
|
|
1625
|
+
stage: "processing",
|
|
1626
|
+
durationMs: Math.max(0, this.telemetryTimestamp() - startedAt),
|
|
1627
|
+
durationMode: "machine",
|
|
1628
|
+
requestCategory: "intent_create",
|
|
1629
|
+
statusClass: telemetryStatusClass(response.status)
|
|
1630
|
+
});
|
|
1631
|
+
return response;
|
|
1632
|
+
} catch (error) {
|
|
1633
|
+
this.reportDirectFailure(
|
|
1634
|
+
error,
|
|
1635
|
+
"PAYMENT_PROCESSING_FAILED",
|
|
1636
|
+
"processing",
|
|
1637
|
+
"intent_create"
|
|
1638
|
+
);
|
|
1639
|
+
throw error;
|
|
1640
|
+
}
|
|
944
1641
|
}
|
|
945
1642
|
/**
|
|
946
1643
|
* Fetch user's prior payments by email.
|
|
947
1644
|
* Used to determine if saved card UX should be shown.
|
|
948
1645
|
*/
|
|
949
1646
|
async getPaymentsByEmail(email, options) {
|
|
1647
|
+
this.beginDirectTelemetryOperation();
|
|
1648
|
+
const startedAt = this.telemetryTimestamp();
|
|
1649
|
+
this.directTelemetry?.log({
|
|
1650
|
+
name: "operation.recovery.started",
|
|
1651
|
+
stage: "recovery",
|
|
1652
|
+
requestCategory: "other",
|
|
1653
|
+
paymentMethodCategory: "saved"
|
|
1654
|
+
});
|
|
950
1655
|
const page = options?.page ?? 1;
|
|
951
1656
|
const limit = options?.limit ?? 1;
|
|
952
1657
|
const params = new URLSearchParams({
|
|
@@ -956,18 +1661,43 @@ var PaymentAPI = class {
|
|
|
956
1661
|
sortField: "createdAt",
|
|
957
1662
|
sortDirection: "DESC"
|
|
958
1663
|
});
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
1664
|
+
try {
|
|
1665
|
+
const response = await fetch(
|
|
1666
|
+
`${this.baseUrl}/v1/payments?${params.toString()}`,
|
|
1667
|
+
{
|
|
1668
|
+
method: "GET",
|
|
1669
|
+
signal: options?.signal,
|
|
1670
|
+
keepalive: true
|
|
1671
|
+
}
|
|
1672
|
+
);
|
|
1673
|
+
if (!response.ok) {
|
|
1674
|
+
throw new import_shared4.FloPayError(
|
|
1675
|
+
"Failed to fetch payments",
|
|
1676
|
+
"api_error",
|
|
1677
|
+
{ statusCode: response.status }
|
|
1678
|
+
);
|
|
965
1679
|
}
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
1680
|
+
const result = await response.json();
|
|
1681
|
+
this.directTelemetry?.log({
|
|
1682
|
+
name: "operation.recovery.completed",
|
|
1683
|
+
stage: "recovery",
|
|
1684
|
+
requestCategory: "other",
|
|
1685
|
+
paymentMethodCategory: "saved",
|
|
1686
|
+
statusClass: "2xx"
|
|
1687
|
+
});
|
|
1688
|
+
this.directTelemetry?.performance({
|
|
1689
|
+
stage: "recovery",
|
|
1690
|
+
durationMs: Math.max(0, this.telemetryTimestamp() - startedAt),
|
|
1691
|
+
durationMode: "machine",
|
|
1692
|
+
requestCategory: "other",
|
|
1693
|
+
paymentMethodCategory: "saved",
|
|
1694
|
+
statusClass: "2xx"
|
|
1695
|
+
});
|
|
1696
|
+
return result;
|
|
1697
|
+
} catch (error) {
|
|
1698
|
+
this.reportDirectFailure(error, "RECOVERY_FAILED", "recovery", "other", "saved");
|
|
1699
|
+
throw error;
|
|
969
1700
|
}
|
|
970
|
-
return response.json();
|
|
971
1701
|
}
|
|
972
1702
|
/**
|
|
973
1703
|
* Create a checkout session AND return the full session data in one call.
|
|
@@ -977,15 +1707,65 @@ var PaymentAPI = class {
|
|
|
977
1707
|
* Falls back to create + GET if the backend doesn't support `expand`.
|
|
978
1708
|
*/
|
|
979
1709
|
async createAndFetchSession(params) {
|
|
980
|
-
|
|
981
|
-
const
|
|
1710
|
+
this.beginDirectTelemetryOperation();
|
|
1711
|
+
const startedAt = this.telemetryTimestamp();
|
|
1712
|
+
this.directTelemetry?.log({
|
|
1713
|
+
name: "session.create.started",
|
|
1714
|
+
stage: "session_create",
|
|
1715
|
+
requestCategory: "session_create"
|
|
1716
|
+
});
|
|
1717
|
+
try {
|
|
1718
|
+
const result = await this.createAndFetchSessionRequest(params, startedAt);
|
|
1719
|
+
this.adoptDirectTelemetryCheckout(result.data.session?.id);
|
|
1720
|
+
this.directTelemetry?.log({
|
|
1721
|
+
name: "session.request.completed",
|
|
1722
|
+
stage: "session_complete",
|
|
1723
|
+
requestCategory: "session_create",
|
|
1724
|
+
statusClass: "2xx"
|
|
1725
|
+
});
|
|
1726
|
+
this.directTelemetry?.performance({
|
|
1727
|
+
stage: "session_create",
|
|
1728
|
+
durationMs: this.telemetryTimestamp() - startedAt,
|
|
1729
|
+
durationMode: "machine",
|
|
1730
|
+
requestCategory: "session_create",
|
|
1731
|
+
statusClass: "2xx"
|
|
1732
|
+
});
|
|
1733
|
+
return result;
|
|
1734
|
+
} catch (error) {
|
|
1735
|
+
if (!(error instanceof import_shared4.FloPayError && error.code === "session_auto_completed")) {
|
|
1736
|
+
try {
|
|
1737
|
+
this.telemetryHooks?.onSessionCreateFailure?.(error);
|
|
1738
|
+
} catch {
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
if (error instanceof import_shared4.FloPayError && error.type === "validation_error") {
|
|
1742
|
+
this.directTelemetry?.terminal({
|
|
1743
|
+
outcome: "validation_rejected",
|
|
1744
|
+
stage: "session_create",
|
|
1745
|
+
requestCategory: "session_create"
|
|
1746
|
+
});
|
|
1747
|
+
} else if (!(error instanceof import_shared4.FloPayError && error.code === "session_auto_completed")) {
|
|
1748
|
+
const statusCode = error instanceof import_shared4.FloPayError ? error.statusCode : void 0;
|
|
1749
|
+
this.directTelemetry?.error({
|
|
1750
|
+
errorCode: error instanceof Error && error.name === "AbortError" ? "REQUEST_TIMEOUT" : error instanceof TypeError ? "NETWORK_REQUEST_FAILED" : "CHECKOUT_SESSION_CREATE_FAILED",
|
|
1751
|
+
stage: "session_create",
|
|
1752
|
+
requestCategory: "session_create",
|
|
1753
|
+
statusClass: error instanceof Error && error.name === "AbortError" ? "timeout" : statusCode ? `${Math.floor(statusCode / 100)}xx` : "network_error"
|
|
1754
|
+
});
|
|
1755
|
+
}
|
|
1756
|
+
throw error;
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
async createAndFetchSessionRequest(params, telemetryStartedAt) {
|
|
1760
|
+
const wireProducts = params.products ?? (0, import_shared4.foldIntoProducts)(params.items, params.subscriptions);
|
|
1761
|
+
const sessionCurrency = (0, import_shared4.resolveSessionCurrency)(
|
|
982
1762
|
params.currency,
|
|
983
1763
|
params.items,
|
|
984
1764
|
params.subscriptions,
|
|
985
1765
|
wireProducts
|
|
986
1766
|
);
|
|
987
1767
|
if (!sessionCurrency) {
|
|
988
|
-
throw new
|
|
1768
|
+
throw new import_shared4.FloPayError(
|
|
989
1769
|
"currency is required: pass `currency` on the session, or include a `currency` on the first item/subscription/product.",
|
|
990
1770
|
"validation_error",
|
|
991
1771
|
{ code: "CurrencyRequired", param: "currency" }
|
|
@@ -993,12 +1773,12 @@ var PaymentAPI = class {
|
|
|
993
1773
|
}
|
|
994
1774
|
const payload = {
|
|
995
1775
|
clientId: params.clientId,
|
|
996
|
-
checkoutVersion:
|
|
1776
|
+
checkoutVersion: import_shared4.SDK_VERSION,
|
|
997
1777
|
successUrl: params.successUrl,
|
|
998
1778
|
cancelUrl: params.cancelUrl,
|
|
999
1779
|
currency: sessionCurrency,
|
|
1000
1780
|
checkoutMode: params.checkoutMode ?? "full",
|
|
1001
|
-
products: wireProducts.map((product) => (0,
|
|
1781
|
+
products: wireProducts.map((product) => (0, import_shared4.buildProductPayload)(product, sessionCurrency)),
|
|
1002
1782
|
accountData: {
|
|
1003
1783
|
userId: params.account.userId,
|
|
1004
1784
|
firstName: params.account.firstName ?? null,
|
|
@@ -1026,13 +1806,14 @@ var PaymentAPI = class {
|
|
|
1026
1806
|
// Declare the SDK version so backends at TeamFloPay/backend#823 embed
|
|
1027
1807
|
// the hosted vault capture block (`body.vault`) in the response for
|
|
1028
1808
|
// SDKs ≥ 1.3.0. Older backends ignore the header.
|
|
1029
|
-
[
|
|
1809
|
+
[import_shared4.FLO_SDK_VERSION_HEADER]: import_shared4.SDK_VERSION
|
|
1030
1810
|
};
|
|
1031
|
-
const idempotencyKey = (0,
|
|
1811
|
+
const idempotencyKey = (0, import_shared4.resolveIdempotencyKey)(params.idempotencyKey);
|
|
1032
1812
|
if (idempotencyKey) {
|
|
1033
|
-
headers[
|
|
1813
|
+
headers[import_shared4.IDEMPOTENCY_KEY_HEADER] = idempotencyKey;
|
|
1034
1814
|
}
|
|
1035
1815
|
let response;
|
|
1816
|
+
let firstByteReported = false;
|
|
1036
1817
|
for (let attempt = 0; ; attempt++) {
|
|
1037
1818
|
response = await fetchWithNetworkRetry(
|
|
1038
1819
|
`${this.baseUrl}/v1/checkouts/sessions?expand=true`,
|
|
@@ -1040,10 +1821,37 @@ var PaymentAPI = class {
|
|
|
1040
1821
|
method: "POST",
|
|
1041
1822
|
headers,
|
|
1042
1823
|
body: JSON.stringify(payload)
|
|
1824
|
+
},
|
|
1825
|
+
NETWORK_RETRY_ATTEMPTS,
|
|
1826
|
+
(networkAttempt) => {
|
|
1827
|
+
this.telemetryHooks?.onRetry?.("session_create", networkAttempt);
|
|
1828
|
+
this.directTelemetry?.log({
|
|
1829
|
+
name: "operation.retry",
|
|
1830
|
+
stage: "session_create",
|
|
1831
|
+
requestCategory: "session_create",
|
|
1832
|
+
attempt: networkAttempt
|
|
1833
|
+
});
|
|
1043
1834
|
}
|
|
1044
1835
|
);
|
|
1836
|
+
if (!firstByteReported) {
|
|
1837
|
+
firstByteReported = true;
|
|
1838
|
+
const statusClass = `${Math.floor(response.status / 100)}xx`;
|
|
1839
|
+
this.directTelemetry?.log({
|
|
1840
|
+
name: "session.request.first_byte",
|
|
1841
|
+
stage: "session_first_byte",
|
|
1842
|
+
requestCategory: "session_create",
|
|
1843
|
+
statusClass
|
|
1844
|
+
});
|
|
1845
|
+
this.directTelemetry?.performance({
|
|
1846
|
+
stage: "session_first_byte",
|
|
1847
|
+
durationMs: this.telemetryTimestamp() - telemetryStartedAt,
|
|
1848
|
+
durationMode: "machine",
|
|
1849
|
+
requestCategory: "session_create",
|
|
1850
|
+
statusClass
|
|
1851
|
+
});
|
|
1852
|
+
}
|
|
1045
1853
|
if (response.status === 204) {
|
|
1046
|
-
throw new
|
|
1854
|
+
throw new import_shared4.FloPayError(
|
|
1047
1855
|
"Session auto-completed \u2014 payment method already on file",
|
|
1048
1856
|
"api_error",
|
|
1049
1857
|
{ code: "session_auto_completed" }
|
|
@@ -1051,7 +1859,17 @@ var PaymentAPI = class {
|
|
|
1051
1859
|
}
|
|
1052
1860
|
if (response.ok) break;
|
|
1053
1861
|
const error = await buildApiErrorFromResponse(response, "Failed to create checkout session");
|
|
1054
|
-
if (error.code ===
|
|
1862
|
+
if (error.code === import_shared4.IDEMPOTENCY_IN_PROGRESS_CODE && attempt < IDEMPOTENCY_IN_PROGRESS_RETRY_ATTEMPTS) {
|
|
1863
|
+
try {
|
|
1864
|
+
this.telemetryHooks?.onRetry?.("session_create", attempt + 1);
|
|
1865
|
+
this.directTelemetry?.log({
|
|
1866
|
+
name: "operation.retry",
|
|
1867
|
+
stage: "session_create",
|
|
1868
|
+
requestCategory: "session_create",
|
|
1869
|
+
attempt: attempt + 1
|
|
1870
|
+
});
|
|
1871
|
+
} catch {
|
|
1872
|
+
}
|
|
1055
1873
|
await delay(150 * 2 ** attempt);
|
|
1056
1874
|
continue;
|
|
1057
1875
|
}
|
|
@@ -1074,9 +1892,10 @@ var PaymentAPI = class {
|
|
|
1074
1892
|
}
|
|
1075
1893
|
const uuid = body.data?.uuid;
|
|
1076
1894
|
if (!uuid) {
|
|
1077
|
-
throw new
|
|
1895
|
+
throw new import_shared4.FloPayError("No session ID returned", "api_error");
|
|
1078
1896
|
}
|
|
1079
1897
|
this.autoCacheDisplayData(uuid, params);
|
|
1898
|
+
this.adoptDirectTelemetryCheckout(uuid);
|
|
1080
1899
|
const unifiedSession = await this.getUnifiedCheckoutSession(uuid);
|
|
1081
1900
|
return {
|
|
1082
1901
|
...unifiedSession,
|
|
@@ -1086,31 +1905,78 @@ var PaymentAPI = class {
|
|
|
1086
1905
|
};
|
|
1087
1906
|
}
|
|
1088
1907
|
async waitForCheckoutSessionCompletion(checkoutSessionId, options) {
|
|
1908
|
+
this.beginDirectTelemetryCheckout(checkoutSessionId);
|
|
1909
|
+
const startedAt = this.telemetryTimestamp();
|
|
1910
|
+
this.directTelemetry?.log({
|
|
1911
|
+
name: "operation.recovery.started",
|
|
1912
|
+
stage: "recovery",
|
|
1913
|
+
requestCategory: "session_read",
|
|
1914
|
+
paymentMethodCategory: "saved"
|
|
1915
|
+
});
|
|
1089
1916
|
const timeoutMs = options?.timeoutMs ?? DEFAULT_PROCESSING_TIMEOUT_MS;
|
|
1090
1917
|
const deadline = Date.now() + timeoutMs;
|
|
1091
1918
|
let nextDelayMs = this.clampRetryAfterMs(options?.initialDelayMs ?? DEFAULT_PROCESSING_RETRY_AFTER_MS);
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1919
|
+
let pollAttempt = 0;
|
|
1920
|
+
try {
|
|
1921
|
+
while (true) {
|
|
1922
|
+
const remainingMs = deadline - Date.now();
|
|
1923
|
+
if (remainingMs <= 0) {
|
|
1924
|
+
throw createCheckoutProcessingTimeoutError();
|
|
1925
|
+
}
|
|
1926
|
+
if (nextDelayMs > 0) {
|
|
1927
|
+
try {
|
|
1928
|
+
pollAttempt += 1;
|
|
1929
|
+
this.telemetryHooks?.onRetry?.("session_read", pollAttempt);
|
|
1930
|
+
this.directTelemetry?.log({
|
|
1931
|
+
name: "operation.retry",
|
|
1932
|
+
stage: "recovery",
|
|
1933
|
+
requestCategory: "session_read",
|
|
1934
|
+
paymentMethodCategory: "saved",
|
|
1935
|
+
attempt: pollAttempt
|
|
1936
|
+
});
|
|
1937
|
+
} catch {
|
|
1938
|
+
}
|
|
1939
|
+
await delay(Math.min(nextDelayMs, remainingMs));
|
|
1940
|
+
if (Date.now() >= deadline) {
|
|
1941
|
+
throw createCheckoutProcessingTimeoutError();
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
const session = await this.getUnifiedCheckoutSession(checkoutSessionId, options?.nonce);
|
|
1945
|
+
const status = session.data.session?.status;
|
|
1946
|
+
if (status === "complete" || status === "expired") {
|
|
1947
|
+
this.directTelemetry?.log({
|
|
1948
|
+
name: "operation.recovery.completed",
|
|
1949
|
+
stage: "recovery",
|
|
1950
|
+
requestCategory: "session_read",
|
|
1951
|
+
paymentMethodCategory: "saved"
|
|
1952
|
+
});
|
|
1953
|
+
this.directTelemetry?.performance({
|
|
1954
|
+
stage: "recovery",
|
|
1955
|
+
durationMs: Math.max(0, this.telemetryTimestamp() - startedAt),
|
|
1956
|
+
durationMode: "machine",
|
|
1957
|
+
requestCategory: "session_read",
|
|
1958
|
+
paymentMethodCategory: "saved"
|
|
1959
|
+
});
|
|
1960
|
+
return session;
|
|
1961
|
+
}
|
|
1099
1962
|
if (Date.now() >= deadline) {
|
|
1100
1963
|
throw createCheckoutProcessingTimeoutError();
|
|
1101
1964
|
}
|
|
1965
|
+
nextDelayMs = this.clampRetryAfterMs(
|
|
1966
|
+
Math.max(nextDelayMs * 2, MIN_PROCESSING_RETRY_AFTER_MS)
|
|
1967
|
+
);
|
|
1102
1968
|
}
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1969
|
+
} catch (error) {
|
|
1970
|
+
if (error instanceof import_shared4.FloPayError && error.code === "checkout_processing_timeout") {
|
|
1971
|
+
this.reportDirectFailure(
|
|
1972
|
+
error,
|
|
1973
|
+
"RECOVERY_FAILED",
|
|
1974
|
+
"recovery",
|
|
1975
|
+
"session_read",
|
|
1976
|
+
"saved"
|
|
1977
|
+
);
|
|
1110
1978
|
}
|
|
1111
|
-
|
|
1112
|
-
Math.max(nextDelayMs * 2, MIN_PROCESSING_RETRY_AFTER_MS)
|
|
1113
|
-
);
|
|
1979
|
+
throw error;
|
|
1114
1980
|
}
|
|
1115
1981
|
}
|
|
1116
1982
|
/** Normalize a raw session into a provider-agnostic shape. */
|
|
@@ -1242,7 +2108,7 @@ var PaymentAPI = class {
|
|
|
1242
2108
|
return new Response(null, { status: 204, statusText: "No Content" });
|
|
1243
2109
|
}
|
|
1244
2110
|
if (session.data.session?.status === "expired") {
|
|
1245
|
-
throw new
|
|
2111
|
+
throw new import_shared4.FloPayError(
|
|
1246
2112
|
"Checkout session has expired.",
|
|
1247
2113
|
"api_error",
|
|
1248
2114
|
{ code: "checkout_session_expired" }
|
|
@@ -1277,12 +2143,12 @@ var PaymentAPI = class {
|
|
|
1277
2143
|
*/
|
|
1278
2144
|
autoCacheDisplayData(sessionId, params) {
|
|
1279
2145
|
if (!sessionId) return;
|
|
1280
|
-
const products = params.products ?? (0,
|
|
2146
|
+
const products = params.products ?? (0, import_shared4.foldIntoProducts)(params.items, params.subscriptions);
|
|
1281
2147
|
if (products.length === 0 && !params.currency) {
|
|
1282
2148
|
return;
|
|
1283
2149
|
}
|
|
1284
2150
|
const usingUnifiedProducts = params.products !== void 0;
|
|
1285
|
-
const sessionCurrency = (0,
|
|
2151
|
+
const sessionCurrency = (0, import_shared4.resolveSessionCurrency)(
|
|
1286
2152
|
params.currency,
|
|
1287
2153
|
usingUnifiedProducts ? void 0 : params.items,
|
|
1288
2154
|
usingUnifiedProducts ? void 0 : params.subscriptions,
|
|
@@ -1331,13 +2197,79 @@ var PaymentAPI = class {
|
|
|
1331
2197
|
};
|
|
1332
2198
|
}
|
|
1333
2199
|
};
|
|
2200
|
+
function createInstrumentedPaymentAPI(billingApiUrl, hooks) {
|
|
2201
|
+
const InstrumentedPaymentAPI = PaymentAPI;
|
|
2202
|
+
return new InstrumentedPaymentAPI(billingApiUrl, hooks);
|
|
2203
|
+
}
|
|
1334
2204
|
|
|
1335
2205
|
// src/pci-vault-card-capture.ts
|
|
1336
|
-
var
|
|
2206
|
+
var import_shared5 = require("@flopay/shared");
|
|
1337
2207
|
var VAULT_MESSAGE_SOURCE = "flopay-vault";
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
2208
|
+
var VAULT_NON_TERMINAL_OUTCOME_LOGS = {
|
|
2209
|
+
ready: ["vault.widget.ready", "vault_ready"],
|
|
2210
|
+
submitting: ["vault.submission.started", "vault_submit"],
|
|
2211
|
+
blocked: ["operation.state_transition", "vault_submit"],
|
|
2212
|
+
action_required: ["vault.action.required", "three_ds_handoff"]
|
|
2213
|
+
};
|
|
2214
|
+
function addBreadcrumb(event) {
|
|
2215
|
+
const data = {
|
|
2216
|
+
class: event.class,
|
|
2217
|
+
stage: event.stage
|
|
2218
|
+
};
|
|
2219
|
+
if ("code" in event) data["code"] = event.code;
|
|
2220
|
+
if ("provider" in event && event.provider) data["provider"] = event.provider;
|
|
2221
|
+
if ("paymentMethodCategory" in event && event.paymentMethodCategory) {
|
|
2222
|
+
data["paymentMethodCategory"] = event.paymentMethodCategory;
|
|
2223
|
+
}
|
|
2224
|
+
if ("outcome" in event) data["outcome"] = event.outcome;
|
|
2225
|
+
if ("durationMs" in event && event.durationMs !== void 0) data["durationMs"] = event.durationMs;
|
|
2226
|
+
if ("durationMode" in event && event.durationMode) data["durationMode"] = event.durationMode;
|
|
2227
|
+
try {
|
|
2228
|
+
const sentry = globalThis.Sentry;
|
|
2229
|
+
sentry?.addBreadcrumb?.({
|
|
2230
|
+
category: "flopay.telemetry",
|
|
2231
|
+
level: event.class === "technical_error" ? "error" : "info",
|
|
2232
|
+
message: event.class === "lifecycle" ? event.name : event.class === "technical_error" ? event.code : event.class === "expected_outcome" ? event.outcome : "sdk.performance",
|
|
2233
|
+
data
|
|
2234
|
+
});
|
|
2235
|
+
} catch {
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
function vaultLog(name, stage) {
|
|
2239
|
+
return (0, import_shared5.buildTelemetryLogEvent)({
|
|
2240
|
+
eventId: "11111111-1111-4111-8111-111111111111",
|
|
2241
|
+
name,
|
|
2242
|
+
stage,
|
|
2243
|
+
sequence: 0,
|
|
2244
|
+
provider: "pcivault",
|
|
2245
|
+
paymentMethodCategory: "card"
|
|
2246
|
+
});
|
|
2247
|
+
}
|
|
2248
|
+
function vaultErrorClassification(submissionStarted) {
|
|
2249
|
+
return submissionStarted ? { errorCode: "VAULT_SUBMIT_FAILED", stage: "vault_submit" } : { errorCode: "VAULT_LOAD_FAILED", stage: "vault_mount" };
|
|
2250
|
+
}
|
|
2251
|
+
function vaultOutcomeBreadcrumb(type, submissionStarted) {
|
|
2252
|
+
if (type === "complete" || type === "decline") {
|
|
2253
|
+
return (0, import_shared5.buildTelemetryTerminalEvent)({
|
|
2254
|
+
eventId: "22222222-2222-4222-8222-222222222222",
|
|
2255
|
+
outcome: type === "complete" ? "payment_succeeded" : "payment_declined",
|
|
2256
|
+
sequence: 0,
|
|
2257
|
+
provider: "pcivault",
|
|
2258
|
+
paymentMethodCategory: "card"
|
|
2259
|
+
});
|
|
2260
|
+
}
|
|
2261
|
+
if (type === "error") {
|
|
2262
|
+
const classification = vaultErrorClassification(submissionStarted);
|
|
2263
|
+
return (0, import_shared5.buildTelemetryErrorEvent)({
|
|
2264
|
+
eventId: "33333333-3333-4333-8333-333333333333",
|
|
2265
|
+
...classification,
|
|
2266
|
+
sequence: 0,
|
|
2267
|
+
provider: "pcivault",
|
|
2268
|
+
paymentMethodCategory: "card"
|
|
2269
|
+
});
|
|
2270
|
+
}
|
|
2271
|
+
const [name, stage] = VAULT_NON_TERMINAL_OUTCOME_LOGS[type];
|
|
2272
|
+
return vaultLog(name, stage);
|
|
1341
2273
|
}
|
|
1342
2274
|
function isVaultResultMessage(value) {
|
|
1343
2275
|
if (typeof value !== "object" || value === null) return false;
|
|
@@ -1355,7 +2287,7 @@ function isVaultResizeMessage(value) {
|
|
|
1355
2287
|
return record["source"] === VAULT_MESSAGE_SOURCE && record["type"] === "resize" && typeof record["height"] === "number" && Number.isFinite(record["height"]);
|
|
1356
2288
|
}
|
|
1357
2289
|
var PciVaultCardCapture = class {
|
|
1358
|
-
constructor(config = {}) {
|
|
2290
|
+
constructor(config = {}, internalTelemetry) {
|
|
1359
2291
|
this.provider = "pcivault";
|
|
1360
2292
|
this.container = null;
|
|
1361
2293
|
this.messageHandler = null;
|
|
@@ -1387,19 +2319,42 @@ var PciVaultCardCapture = class {
|
|
|
1387
2319
|
/** Latest card-field order + autofocus directive to push into the widget. */
|
|
1388
2320
|
this.cardFieldOrder = null;
|
|
1389
2321
|
this.cardAutoFocus = true;
|
|
2322
|
+
this.captureRequestedAt = 0;
|
|
2323
|
+
this.vaultReadyReported = false;
|
|
2324
|
+
this.submissionStarted = false;
|
|
2325
|
+
this.submissionStartedAt = null;
|
|
1390
2326
|
this.listeners = /* @__PURE__ */ new Map();
|
|
1391
2327
|
this.config = config;
|
|
2328
|
+
this.ownsTelemetryReporter = !internalTelemetry && config.telemetry !== false;
|
|
2329
|
+
this.telemetryReporter = internalTelemetry?.reporter ?? (this.ownsTelemetryReporter ? new TelemetryReporter({
|
|
2330
|
+
billingApiUrl: (0, import_shared5.resolveBillingApiUrl)(),
|
|
2331
|
+
sdkVersion: import_shared5.SDK_VERSION
|
|
2332
|
+
}) : void 0);
|
|
2333
|
+
this.requestedAt = internalTelemetry?.requestedAt;
|
|
1392
2334
|
}
|
|
1393
2335
|
async mount(container, options) {
|
|
2336
|
+
if (this.ownsTelemetryReporter && !this.telemetryReporter) {
|
|
2337
|
+
this.telemetryReporter = new TelemetryReporter({
|
|
2338
|
+
billingApiUrl: (0, import_shared5.resolveBillingApiUrl)(),
|
|
2339
|
+
sdkVersion: import_shared5.SDK_VERSION
|
|
2340
|
+
});
|
|
2341
|
+
}
|
|
2342
|
+
const mountedAt = this.telemetryReporter?.now?.() ?? telemetryNow();
|
|
2343
|
+
this.captureRequestedAt = this.requestedAt ?? mountedAt;
|
|
2344
|
+
this.vaultReadyReported = false;
|
|
2345
|
+
this.submissionStarted = false;
|
|
2346
|
+
this.submissionStartedAt = null;
|
|
1394
2347
|
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
1395
|
-
|
|
2348
|
+
this.reportVaultLoadFailure();
|
|
2349
|
+
throw new import_shared5.FloPayError(
|
|
1396
2350
|
"The vault card form is only available in the browser.",
|
|
1397
2351
|
"api_error",
|
|
1398
2352
|
{ code: "card_capture_no_window" }
|
|
1399
2353
|
);
|
|
1400
2354
|
}
|
|
1401
2355
|
if (!options?.html || !options.html.trim()) {
|
|
1402
|
-
|
|
2356
|
+
this.reportVaultLoadFailure();
|
|
2357
|
+
throw new import_shared5.FloPayError(
|
|
1403
2358
|
"No vault capture widget HTML was provided to mount the secure card form.",
|
|
1404
2359
|
"api_error",
|
|
1405
2360
|
{ code: "card_capture_no_widget_html" }
|
|
@@ -1409,14 +2364,33 @@ var PciVaultCardCapture = class {
|
|
|
1409
2364
|
this.messageToken = options.messageToken ?? null;
|
|
1410
2365
|
this.expectedOrigin = options.expectedOrigin ?? this.config.expectedOrigin ?? null;
|
|
1411
2366
|
this.theme = options.theme ?? null;
|
|
1412
|
-
|
|
1413
|
-
|
|
2367
|
+
try {
|
|
2368
|
+
this.attachMessageListener();
|
|
2369
|
+
this.injectWidget(container, options.html);
|
|
2370
|
+
} catch (error) {
|
|
2371
|
+
this.reportVaultLoadFailure();
|
|
2372
|
+
throw error;
|
|
2373
|
+
}
|
|
1414
2374
|
this.postTheme();
|
|
1415
2375
|
this.postSubmitGate();
|
|
1416
2376
|
this.postCardFieldOrder();
|
|
1417
|
-
addBreadcrumb("vault
|
|
2377
|
+
addBreadcrumb(vaultLog("vault.widget.mounted", "vault_mount"));
|
|
2378
|
+
this.telemetryReporter?.log({
|
|
2379
|
+
name: "vault.widget.mounted",
|
|
2380
|
+
stage: "vault_mount",
|
|
2381
|
+
provider: "pcivault",
|
|
2382
|
+
paymentMethodCategory: "card"
|
|
2383
|
+
});
|
|
1418
2384
|
this.emit("ready", { sessionId: this.config.sessionId });
|
|
1419
2385
|
}
|
|
2386
|
+
reportVaultLoadFailure() {
|
|
2387
|
+
this.telemetryReporter?.error({
|
|
2388
|
+
errorCode: "VAULT_LOAD_FAILED",
|
|
2389
|
+
stage: "vault_mount",
|
|
2390
|
+
provider: "pcivault",
|
|
2391
|
+
paymentMethodCategory: "card"
|
|
2392
|
+
});
|
|
2393
|
+
}
|
|
1420
2394
|
on(event, handler) {
|
|
1421
2395
|
let set = this.listeners.get(event);
|
|
1422
2396
|
if (!set) {
|
|
@@ -1440,6 +2414,10 @@ var PciVaultCardCapture = class {
|
|
|
1440
2414
|
}
|
|
1441
2415
|
this.messageToken = null;
|
|
1442
2416
|
this.expectedOrigin = null;
|
|
2417
|
+
if (this.ownsTelemetryReporter) {
|
|
2418
|
+
this.telemetryReporter?.destroy();
|
|
2419
|
+
this.telemetryReporter = void 0;
|
|
2420
|
+
}
|
|
1443
2421
|
}
|
|
1444
2422
|
// ── internals ──
|
|
1445
2423
|
/**
|
|
@@ -1493,10 +2471,12 @@ var PciVaultCardCapture = class {
|
|
|
1493
2471
|
message: data.message,
|
|
1494
2472
|
nextActionRedirectUrl: data.nextActionRedirectUrl
|
|
1495
2473
|
};
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
}
|
|
2474
|
+
if (data.type === "submitting") {
|
|
2475
|
+
this.submissionStarted = true;
|
|
2476
|
+
this.submissionStartedAt = this.telemetryReporter?.now?.() ?? telemetryNow();
|
|
2477
|
+
}
|
|
2478
|
+
addBreadcrumb(vaultOutcomeBreadcrumb(data.type, this.submissionStarted));
|
|
2479
|
+
this.reportOutcome(data.type);
|
|
1500
2480
|
if (data.type === "ready") {
|
|
1501
2481
|
this.postTheme();
|
|
1502
2482
|
this.postSubmitGate();
|
|
@@ -1513,6 +2493,88 @@ var PciVaultCardCapture = class {
|
|
|
1513
2493
|
this.messageHandler = handler;
|
|
1514
2494
|
window.addEventListener("message", handler);
|
|
1515
2495
|
}
|
|
2496
|
+
reportOutcome(type) {
|
|
2497
|
+
const reporter = this.telemetryReporter;
|
|
2498
|
+
if (!reporter) return;
|
|
2499
|
+
if (type === "ready" && !this.vaultReadyReported) {
|
|
2500
|
+
this.vaultReadyReported = true;
|
|
2501
|
+
reporter.performance({
|
|
2502
|
+
stage: "vault_ready",
|
|
2503
|
+
durationMs: Math.max(0, reporter.now() - this.captureRequestedAt),
|
|
2504
|
+
durationMode: "machine",
|
|
2505
|
+
provider: "pcivault",
|
|
2506
|
+
paymentMethodCategory: "card"
|
|
2507
|
+
});
|
|
2508
|
+
}
|
|
2509
|
+
if (type === "complete" || type === "decline") {
|
|
2510
|
+
reporter.log({
|
|
2511
|
+
name: "vault.terminal",
|
|
2512
|
+
stage: "completion",
|
|
2513
|
+
provider: "pcivault",
|
|
2514
|
+
paymentMethodCategory: "card"
|
|
2515
|
+
});
|
|
2516
|
+
reporter.terminal({
|
|
2517
|
+
outcome: type === "complete" ? "payment_succeeded" : "payment_declined",
|
|
2518
|
+
provider: "pcivault",
|
|
2519
|
+
paymentMethodCategory: "card"
|
|
2520
|
+
});
|
|
2521
|
+
return;
|
|
2522
|
+
}
|
|
2523
|
+
if (type === "error") {
|
|
2524
|
+
const classification = vaultErrorClassification(this.submissionStarted);
|
|
2525
|
+
reporter.log({
|
|
2526
|
+
name: "vault.terminal",
|
|
2527
|
+
stage: classification.stage,
|
|
2528
|
+
provider: "pcivault",
|
|
2529
|
+
paymentMethodCategory: "card"
|
|
2530
|
+
});
|
|
2531
|
+
reporter.error({
|
|
2532
|
+
...classification,
|
|
2533
|
+
provider: "pcivault",
|
|
2534
|
+
paymentMethodCategory: "card"
|
|
2535
|
+
});
|
|
2536
|
+
return;
|
|
2537
|
+
}
|
|
2538
|
+
if (type === "action_required") {
|
|
2539
|
+
reporter.log({
|
|
2540
|
+
name: "vault.action.required",
|
|
2541
|
+
stage: "three_ds_handoff",
|
|
2542
|
+
provider: "pcivault",
|
|
2543
|
+
paymentMethodCategory: "card"
|
|
2544
|
+
});
|
|
2545
|
+
reporter.log({
|
|
2546
|
+
name: "vault.three_ds.handoff",
|
|
2547
|
+
stage: "three_ds_handoff",
|
|
2548
|
+
provider: "pcivault",
|
|
2549
|
+
paymentMethodCategory: "card"
|
|
2550
|
+
});
|
|
2551
|
+
const submissionStartedAt = this.submissionStartedAt;
|
|
2552
|
+
this.submissionStartedAt = null;
|
|
2553
|
+
if (submissionStartedAt !== null) {
|
|
2554
|
+
reporter.performance({
|
|
2555
|
+
stage: "three_ds_handoff",
|
|
2556
|
+
durationMs: Math.max(0, reporter.now() - submissionStartedAt),
|
|
2557
|
+
durationMode: "machine",
|
|
2558
|
+
provider: "pcivault",
|
|
2559
|
+
paymentMethodCategory: "card"
|
|
2560
|
+
});
|
|
2561
|
+
}
|
|
2562
|
+
reporter.terminal({
|
|
2563
|
+
outcome: "action_required",
|
|
2564
|
+
stage: "three_ds_handoff",
|
|
2565
|
+
provider: "pcivault",
|
|
2566
|
+
paymentMethodCategory: "card"
|
|
2567
|
+
});
|
|
2568
|
+
return;
|
|
2569
|
+
}
|
|
2570
|
+
const [name, stage] = VAULT_NON_TERMINAL_OUTCOME_LOGS[type];
|
|
2571
|
+
reporter.log({
|
|
2572
|
+
name,
|
|
2573
|
+
stage,
|
|
2574
|
+
provider: "pcivault",
|
|
2575
|
+
paymentMethodCategory: "card"
|
|
2576
|
+
});
|
|
2577
|
+
}
|
|
1516
2578
|
/**
|
|
1517
2579
|
* Push merchant theme colors into the hosted widget (live). The host calls
|
|
1518
2580
|
* this on a runtime theme switch; the widget applies them to its CSS variables
|
|
@@ -1640,12 +2702,43 @@ var PciVaultCardCapture = class {
|
|
|
1640
2702
|
].join(";");
|
|
1641
2703
|
frame.src = challengeUrl;
|
|
1642
2704
|
backdrop.appendChild(frame);
|
|
2705
|
+
const closeButton = document.createElement("button");
|
|
2706
|
+
closeButton.type = "button";
|
|
2707
|
+
closeButton.setAttribute("aria-label", "Close card authentication");
|
|
2708
|
+
closeButton.textContent = "\xD7";
|
|
2709
|
+
closeButton.style.cssText = [
|
|
2710
|
+
"position:fixed",
|
|
2711
|
+
"top:20px",
|
|
2712
|
+
"right:20px",
|
|
2713
|
+
"width:40px",
|
|
2714
|
+
"height:40px",
|
|
2715
|
+
"border:0",
|
|
2716
|
+
"border-radius:9999px",
|
|
2717
|
+
"background:#fff",
|
|
2718
|
+
"color:#0f172a",
|
|
2719
|
+
"font-size:28px",
|
|
2720
|
+
"line-height:40px",
|
|
2721
|
+
"cursor:pointer",
|
|
2722
|
+
"box-shadow:0 4px 14px rgba(0,0,0,0.25)"
|
|
2723
|
+
].join(";");
|
|
2724
|
+
closeButton.addEventListener("click", () => this.abandonActionRequiredOverlay());
|
|
2725
|
+
backdrop.appendChild(closeButton);
|
|
2726
|
+
backdrop.addEventListener("click", (event) => {
|
|
2727
|
+
if (event.target === backdrop) this.abandonActionRequiredOverlay();
|
|
2728
|
+
});
|
|
1643
2729
|
const returnHandler = (event) => {
|
|
1644
2730
|
if (event.source !== frame.contentWindow) return;
|
|
1645
2731
|
const data = event.data;
|
|
1646
2732
|
if (!data || typeof data !== "object") return;
|
|
1647
2733
|
const record = data;
|
|
1648
2734
|
if (record["source"] !== "flopay-vault-3ds-return") return;
|
|
2735
|
+
addBreadcrumb(vaultLog("vault.three_ds.returned", "three_ds_return"));
|
|
2736
|
+
this.telemetryReporter?.log({
|
|
2737
|
+
name: "vault.three_ds.returned",
|
|
2738
|
+
stage: "three_ds_return",
|
|
2739
|
+
provider: "pcivault",
|
|
2740
|
+
paymentMethodCategory: "card"
|
|
2741
|
+
});
|
|
1649
2742
|
this.hideActionRequiredOverlay();
|
|
1650
2743
|
this.postActionCompleted(record["status"]);
|
|
1651
2744
|
};
|
|
@@ -1653,7 +2746,7 @@ var PciVaultCardCapture = class {
|
|
|
1653
2746
|
this.threeDsReturnHandler = returnHandler;
|
|
1654
2747
|
document.body.appendChild(backdrop);
|
|
1655
2748
|
this.actionOverlay = backdrop;
|
|
1656
|
-
addBreadcrumb("vault
|
|
2749
|
+
addBreadcrumb(vaultLog("vault.three_ds.handoff", "three_ds_handoff"));
|
|
1657
2750
|
}
|
|
1658
2751
|
/**
|
|
1659
2752
|
* Tell the vault widget that the buyer has completed (or abandoned) the
|
|
@@ -1679,6 +2772,26 @@ var PciVaultCardCapture = class {
|
|
|
1679
2772
|
} catch {
|
|
1680
2773
|
}
|
|
1681
2774
|
}
|
|
2775
|
+
abandonActionRequiredOverlay() {
|
|
2776
|
+
if (!this.actionOverlay) return;
|
|
2777
|
+
const breadcrumb = (0, import_shared5.buildTelemetryTerminalEvent)({
|
|
2778
|
+
eventId: "77777777-7777-4777-8777-777777777777",
|
|
2779
|
+
outcome: "customer_abandoned",
|
|
2780
|
+
stage: "three_ds_handoff",
|
|
2781
|
+
sequence: 0,
|
|
2782
|
+
provider: "pcivault",
|
|
2783
|
+
paymentMethodCategory: "card"
|
|
2784
|
+
});
|
|
2785
|
+
addBreadcrumb(breadcrumb);
|
|
2786
|
+
this.telemetryReporter?.terminal({
|
|
2787
|
+
outcome: "customer_abandoned",
|
|
2788
|
+
stage: "three_ds_handoff",
|
|
2789
|
+
provider: "pcivault",
|
|
2790
|
+
paymentMethodCategory: "card"
|
|
2791
|
+
});
|
|
2792
|
+
this.hideActionRequiredOverlay();
|
|
2793
|
+
this.postActionCompleted("abandoned");
|
|
2794
|
+
}
|
|
1682
2795
|
hideActionRequiredOverlay() {
|
|
1683
2796
|
if (this.threeDsReturnHandler) {
|
|
1684
2797
|
window.removeEventListener("message", this.threeDsReturnHandler);
|
|
@@ -1687,7 +2800,6 @@ var PciVaultCardCapture = class {
|
|
|
1687
2800
|
if (!this.actionOverlay) return;
|
|
1688
2801
|
this.actionOverlay.parentNode?.removeChild(this.actionOverlay);
|
|
1689
2802
|
this.actionOverlay = null;
|
|
1690
|
-
addBreadcrumb("vault 3ds challenge overlay hidden");
|
|
1691
2803
|
}
|
|
1692
2804
|
/**
|
|
1693
2805
|
* Size the hosted-widget iframe to the height reported by the form inside it.
|
|
@@ -1703,13 +2815,62 @@ var PciVaultCardCapture = class {
|
|
|
1703
2815
|
iframe.style.height = `${clamped}px`;
|
|
1704
2816
|
}
|
|
1705
2817
|
};
|
|
2818
|
+
function createInstrumentedPciVaultCardCapture(config, reporter, requestedAt) {
|
|
2819
|
+
const InstrumentedCapture = PciVaultCardCapture;
|
|
2820
|
+
return new InstrumentedCapture(config, { reporter, requestedAt });
|
|
2821
|
+
}
|
|
2822
|
+
|
|
2823
|
+
// src/telemetry-bridge.ts
|
|
2824
|
+
var FLOPAY_TELEMETRY_BRIDGE = /* @__PURE__ */ Symbol.for("@flopay/js.telemetry.bridge.v1");
|
|
2825
|
+
function attachFloPayTelemetryBridge(target, reporter, fallbackNow) {
|
|
2826
|
+
const now = () => reporter?.now() ?? fallbackNow();
|
|
2827
|
+
const bridge = {
|
|
2828
|
+
error: (input) => reporter?.error(input),
|
|
2829
|
+
log: (input) => reporter?.log(input),
|
|
2830
|
+
performance: (input) => reporter?.performance(input),
|
|
2831
|
+
terminal: (input) => reporter?.terminal(input),
|
|
2832
|
+
now,
|
|
2833
|
+
elapsed: (startedAt) => Math.max(0, now() - startedAt),
|
|
2834
|
+
setCheckoutContext: (context) => reporter?.setCheckoutContext(context),
|
|
2835
|
+
beginCheckout: (context = {}) => reporter?.beginCheckout(context) ?? now(),
|
|
2836
|
+
disable: () => reporter?.disable()
|
|
2837
|
+
};
|
|
2838
|
+
Object.defineProperty(target, FLOPAY_TELEMETRY_BRIDGE, {
|
|
2839
|
+
configurable: false,
|
|
2840
|
+
enumerable: false,
|
|
2841
|
+
writable: false,
|
|
2842
|
+
value: bridge
|
|
2843
|
+
});
|
|
2844
|
+
}
|
|
2845
|
+
function getFloPayTelemetryBridge(target) {
|
|
2846
|
+
return target[FLOPAY_TELEMETRY_BRIDGE];
|
|
2847
|
+
}
|
|
1706
2848
|
|
|
1707
2849
|
// src/flopay.ts
|
|
2850
|
+
function isExpectedDecline(error) {
|
|
2851
|
+
if (!error) return false;
|
|
2852
|
+
const code = error.code?.toLowerCase() ?? "";
|
|
2853
|
+
return Boolean(error.declineCode) || code.includes("declin");
|
|
2854
|
+
}
|
|
2855
|
+
function telemetryProvider(name) {
|
|
2856
|
+
if (name === "stripe" || name === "paypal" || name === "pcivault") return name;
|
|
2857
|
+
return "other";
|
|
2858
|
+
}
|
|
1708
2859
|
var FloPay = class {
|
|
1709
|
-
constructor(provider, config) {
|
|
2860
|
+
constructor(provider, config, telemetryReporter) {
|
|
1710
2861
|
this.currentElements = null;
|
|
2862
|
+
this.cardThreeDsStartedAt = /* @__PURE__ */ new Map();
|
|
1711
2863
|
this.provider = provider;
|
|
1712
2864
|
this.config = config;
|
|
2865
|
+
this.telemetryReporter = telemetryReporter ?? new TelemetryReporter({
|
|
2866
|
+
billingApiUrl: (0, import_shared6.resolveBillingApiUrl)(config.billingApiUrl),
|
|
2867
|
+
sdkVersion: import_shared6.SDK_VERSION,
|
|
2868
|
+
enabled: config.telemetry !== false
|
|
2869
|
+
});
|
|
2870
|
+
attachFloPayTelemetryBridge(this, this.telemetryReporter, telemetryNow);
|
|
2871
|
+
}
|
|
2872
|
+
now() {
|
|
2873
|
+
return this.telemetryReporter?.now?.() ?? telemetryNow();
|
|
1713
2874
|
}
|
|
1714
2875
|
/**
|
|
1715
2876
|
* Creates a new `FloPayElements` group for mounting payment fields.
|
|
@@ -1733,11 +2894,169 @@ var FloPay = class {
|
|
|
1733
2894
|
}
|
|
1734
2895
|
/** Create a payment method from the current elements (tokenize card). */
|
|
1735
2896
|
async createPaymentMethod(billingDetails) {
|
|
1736
|
-
|
|
2897
|
+
const started = this.now();
|
|
2898
|
+
const provider = telemetryProvider(this.provider.name);
|
|
2899
|
+
this.telemetryReporter?.log({
|
|
2900
|
+
name: "payment.tokenization.started",
|
|
2901
|
+
stage: "tokenization",
|
|
2902
|
+
provider,
|
|
2903
|
+
paymentMethodCategory: "card"
|
|
2904
|
+
});
|
|
2905
|
+
try {
|
|
2906
|
+
const result = await this.provider.createPaymentMethod(billingDetails);
|
|
2907
|
+
this.telemetryReporter?.performance({
|
|
2908
|
+
stage: "tokenization",
|
|
2909
|
+
durationMs: this.now() - started,
|
|
2910
|
+
durationMode: "machine",
|
|
2911
|
+
provider,
|
|
2912
|
+
paymentMethodCategory: "card"
|
|
2913
|
+
});
|
|
2914
|
+
if (!result.error) {
|
|
2915
|
+
this.telemetryReporter?.log({
|
|
2916
|
+
name: "payment.tokenization.completed",
|
|
2917
|
+
stage: "tokenization",
|
|
2918
|
+
provider,
|
|
2919
|
+
paymentMethodCategory: "card"
|
|
2920
|
+
});
|
|
2921
|
+
} else if (result.error.type !== "validation_error") {
|
|
2922
|
+
this.telemetryReporter?.error({
|
|
2923
|
+
errorCode: "TOKENIZATION_FAILED",
|
|
2924
|
+
stage: "tokenization",
|
|
2925
|
+
provider,
|
|
2926
|
+
paymentMethodCategory: "card"
|
|
2927
|
+
});
|
|
2928
|
+
}
|
|
2929
|
+
return result;
|
|
2930
|
+
} catch (error) {
|
|
2931
|
+
this.telemetryReporter?.performance({
|
|
2932
|
+
stage: "tokenization",
|
|
2933
|
+
durationMs: this.now() - started,
|
|
2934
|
+
durationMode: "machine",
|
|
2935
|
+
provider,
|
|
2936
|
+
paymentMethodCategory: "card"
|
|
2937
|
+
});
|
|
2938
|
+
this.telemetryReporter?.error({
|
|
2939
|
+
errorCode: "TOKENIZATION_FAILED",
|
|
2940
|
+
stage: "tokenization",
|
|
2941
|
+
provider,
|
|
2942
|
+
paymentMethodCategory: "card"
|
|
2943
|
+
});
|
|
2944
|
+
throw error;
|
|
2945
|
+
}
|
|
1737
2946
|
}
|
|
1738
2947
|
/** Confirm a card payment with a known client secret and payment method ID. */
|
|
1739
2948
|
async confirmCardPayment(params) {
|
|
1740
|
-
|
|
2949
|
+
const provider = telemetryProvider(this.provider.name);
|
|
2950
|
+
const operationStartedAt = this.now();
|
|
2951
|
+
try {
|
|
2952
|
+
const result = await this.provider.confirmCardPayment(params);
|
|
2953
|
+
const completedAt = this.now();
|
|
2954
|
+
const threeDs = result.threeDs;
|
|
2955
|
+
let threeDsFailed = false;
|
|
2956
|
+
if (threeDs?.status === "handoff") {
|
|
2957
|
+
if (this.trackCardThreeDsAttempt(threeDs.attemptId, completedAt)) {
|
|
2958
|
+
this.telemetryReporter?.log({
|
|
2959
|
+
name: "payment.three_ds.handoff",
|
|
2960
|
+
stage: "three_ds_handoff",
|
|
2961
|
+
provider,
|
|
2962
|
+
paymentMethodCategory: "card"
|
|
2963
|
+
});
|
|
2964
|
+
this.telemetryReporter?.performance({
|
|
2965
|
+
stage: "three_ds_handoff",
|
|
2966
|
+
durationMs: completedAt - operationStartedAt,
|
|
2967
|
+
durationMode: "machine",
|
|
2968
|
+
provider,
|
|
2969
|
+
paymentMethodCategory: "card"
|
|
2970
|
+
});
|
|
2971
|
+
this.telemetryReporter?.terminal({
|
|
2972
|
+
outcome: "action_required",
|
|
2973
|
+
stage: "three_ds_handoff",
|
|
2974
|
+
provider,
|
|
2975
|
+
paymentMethodCategory: "card"
|
|
2976
|
+
});
|
|
2977
|
+
}
|
|
2978
|
+
} else if (threeDs) {
|
|
2979
|
+
const threeDsStartedAt = this.takeCardThreeDsAttempt(threeDs.attemptId, completedAt);
|
|
2980
|
+
if (threeDsStartedAt !== void 0) {
|
|
2981
|
+
if (threeDs.status === "returned") {
|
|
2982
|
+
this.telemetryReporter?.log({
|
|
2983
|
+
name: "payment.three_ds.returned",
|
|
2984
|
+
stage: "three_ds_return",
|
|
2985
|
+
provider,
|
|
2986
|
+
paymentMethodCategory: "card"
|
|
2987
|
+
});
|
|
2988
|
+
this.telemetryReporter?.performance({
|
|
2989
|
+
stage: "three_ds_return",
|
|
2990
|
+
durationMs: completedAt - threeDsStartedAt,
|
|
2991
|
+
durationMode: "machine",
|
|
2992
|
+
provider,
|
|
2993
|
+
paymentMethodCategory: "card"
|
|
2994
|
+
});
|
|
2995
|
+
} else {
|
|
2996
|
+
threeDsFailed = true;
|
|
2997
|
+
}
|
|
2998
|
+
}
|
|
2999
|
+
}
|
|
3000
|
+
if (isExpectedDecline(result.error)) {
|
|
3001
|
+
this.telemetryReporter?.terminal({
|
|
3002
|
+
outcome: "payment_declined",
|
|
3003
|
+
provider,
|
|
3004
|
+
paymentMethodCategory: "card"
|
|
3005
|
+
});
|
|
3006
|
+
} else if (result.error?.type === "validation_error") {
|
|
3007
|
+
this.telemetryReporter?.terminal({
|
|
3008
|
+
outcome: "validation_rejected",
|
|
3009
|
+
provider,
|
|
3010
|
+
paymentMethodCategory: "card"
|
|
3011
|
+
});
|
|
3012
|
+
} else if (result.error) {
|
|
3013
|
+
this.telemetryReporter?.error({
|
|
3014
|
+
errorCode: threeDsFailed ? "THREE_DS_FAILED" : "PROVIDER_RUNTIME_FAILED",
|
|
3015
|
+
stage: threeDsFailed ? "three_ds_return" : "processing",
|
|
3016
|
+
provider,
|
|
3017
|
+
paymentMethodCategory: "card"
|
|
3018
|
+
});
|
|
3019
|
+
} else if (!threeDsFailed && threeDs?.status !== "handoff" && result.status === "succeeded") {
|
|
3020
|
+
this.telemetryReporter?.terminal({
|
|
3021
|
+
outcome: "payment_succeeded",
|
|
3022
|
+
provider,
|
|
3023
|
+
paymentMethodCategory: "card"
|
|
3024
|
+
});
|
|
3025
|
+
}
|
|
3026
|
+
return result;
|
|
3027
|
+
} catch (error) {
|
|
3028
|
+
this.telemetryReporter?.error({
|
|
3029
|
+
errorCode: "PROVIDER_RUNTIME_FAILED",
|
|
3030
|
+
stage: "processing",
|
|
3031
|
+
provider,
|
|
3032
|
+
paymentMethodCategory: "card"
|
|
3033
|
+
});
|
|
3034
|
+
throw error;
|
|
3035
|
+
}
|
|
3036
|
+
}
|
|
3037
|
+
trackCardThreeDsAttempt(attemptId, startedAt) {
|
|
3038
|
+
this.pruneExpiredCardThreeDsAttempts(startedAt);
|
|
3039
|
+
if (this.cardThreeDsStartedAt.has(attemptId)) return false;
|
|
3040
|
+
while (this.cardThreeDsStartedAt.size >= MAX_CARD_THREE_DS_ATTEMPTS) {
|
|
3041
|
+
const oldestAttemptId = this.cardThreeDsStartedAt.keys().next().value;
|
|
3042
|
+
if (oldestAttemptId === void 0) break;
|
|
3043
|
+
this.cardThreeDsStartedAt.delete(oldestAttemptId);
|
|
3044
|
+
}
|
|
3045
|
+
this.cardThreeDsStartedAt.set(attemptId, startedAt);
|
|
3046
|
+
return true;
|
|
3047
|
+
}
|
|
3048
|
+
takeCardThreeDsAttempt(attemptId, now) {
|
|
3049
|
+
this.pruneExpiredCardThreeDsAttempts(now);
|
|
3050
|
+
const startedAt = this.cardThreeDsStartedAt.get(attemptId);
|
|
3051
|
+
if (startedAt !== void 0) this.cardThreeDsStartedAt.delete(attemptId);
|
|
3052
|
+
return startedAt;
|
|
3053
|
+
}
|
|
3054
|
+
pruneExpiredCardThreeDsAttempts(now) {
|
|
3055
|
+
for (const [attemptId, startedAt] of this.cardThreeDsStartedAt) {
|
|
3056
|
+
if (now - startedAt >= CARD_THREE_DS_ATTEMPT_TTL_MS) {
|
|
3057
|
+
this.cardThreeDsStartedAt.delete(attemptId);
|
|
3058
|
+
}
|
|
3059
|
+
}
|
|
1741
3060
|
}
|
|
1742
3061
|
/**
|
|
1743
3062
|
* Create a {@link CardCaptureAdapter} for collecting card details through the
|
|
@@ -1752,21 +3071,270 @@ var FloPay = class {
|
|
|
1752
3071
|
* the SDK runtime.
|
|
1753
3072
|
*/
|
|
1754
3073
|
cardCapture(options) {
|
|
3074
|
+
const requestedAt = this.now();
|
|
3075
|
+
this.telemetryReporter?.log({
|
|
3076
|
+
name: "vault.capture.requested",
|
|
3077
|
+
stage: "vault_request",
|
|
3078
|
+
provider: "pcivault",
|
|
3079
|
+
paymentMethodCategory: "card"
|
|
3080
|
+
});
|
|
3081
|
+
if (this.telemetryReporter) {
|
|
3082
|
+
return createInstrumentedPciVaultCardCapture(
|
|
3083
|
+
{ sessionId: options?.sessionId },
|
|
3084
|
+
this.telemetryReporter,
|
|
3085
|
+
requestedAt
|
|
3086
|
+
);
|
|
3087
|
+
}
|
|
1755
3088
|
return new PciVaultCardCapture({
|
|
1756
|
-
sessionId: options?.sessionId
|
|
3089
|
+
sessionId: options?.sessionId,
|
|
3090
|
+
telemetry: false
|
|
1757
3091
|
});
|
|
1758
3092
|
}
|
|
1759
3093
|
/** Confirm a PayPal payment: create intent via billing API → confirm → redirect if needed. */
|
|
1760
3094
|
async confirmPayPalPayment(params) {
|
|
1761
|
-
|
|
3095
|
+
const startedAt = this.now();
|
|
3096
|
+
this.telemetryReporter?.log({
|
|
3097
|
+
name: "payment.method.selected",
|
|
3098
|
+
stage: "processing",
|
|
3099
|
+
provider: "paypal",
|
|
3100
|
+
paymentMethodCategory: "paypal"
|
|
3101
|
+
});
|
|
3102
|
+
this.telemetryReporter?.log({
|
|
3103
|
+
name: "payment.intent.started",
|
|
3104
|
+
stage: "processing",
|
|
3105
|
+
provider: "paypal",
|
|
3106
|
+
paymentMethodCategory: "paypal",
|
|
3107
|
+
requestCategory: "intent_create"
|
|
3108
|
+
});
|
|
3109
|
+
try {
|
|
3110
|
+
const result = await this.provider.confirmPayPalPayment(params);
|
|
3111
|
+
this.telemetryReporter?.performance({
|
|
3112
|
+
stage: "processing",
|
|
3113
|
+
durationMs: this.now() - startedAt,
|
|
3114
|
+
durationMode: "machine",
|
|
3115
|
+
provider: "paypal",
|
|
3116
|
+
paymentMethodCategory: "paypal"
|
|
3117
|
+
});
|
|
3118
|
+
if (!result.error) {
|
|
3119
|
+
this.telemetryReporter?.log({
|
|
3120
|
+
name: "payment.intent.completed",
|
|
3121
|
+
stage: "processing",
|
|
3122
|
+
provider: "paypal",
|
|
3123
|
+
paymentMethodCategory: "paypal",
|
|
3124
|
+
requestCategory: "intent_create",
|
|
3125
|
+
statusClass: "2xx"
|
|
3126
|
+
});
|
|
3127
|
+
}
|
|
3128
|
+
if (result.status === "requires_action") {
|
|
3129
|
+
this.telemetryReporter?.log({
|
|
3130
|
+
name: "provider.redirect.started",
|
|
3131
|
+
stage: "redirect",
|
|
3132
|
+
provider: "paypal",
|
|
3133
|
+
paymentMethodCategory: "paypal"
|
|
3134
|
+
});
|
|
3135
|
+
this.telemetryReporter?.terminal({
|
|
3136
|
+
outcome: "action_required",
|
|
3137
|
+
stage: "redirect",
|
|
3138
|
+
provider: "paypal",
|
|
3139
|
+
paymentMethodCategory: "paypal"
|
|
3140
|
+
});
|
|
3141
|
+
} else if (result.status === "succeeded") {
|
|
3142
|
+
this.telemetryReporter?.terminal({
|
|
3143
|
+
outcome: "payment_succeeded",
|
|
3144
|
+
provider: "paypal",
|
|
3145
|
+
paymentMethodCategory: "paypal"
|
|
3146
|
+
});
|
|
3147
|
+
} else if (result.status !== "processing") {
|
|
3148
|
+
if (isExpectedDecline(result.error)) {
|
|
3149
|
+
this.telemetryReporter?.terminal({
|
|
3150
|
+
outcome: "payment_declined",
|
|
3151
|
+
provider: "paypal",
|
|
3152
|
+
paymentMethodCategory: "paypal"
|
|
3153
|
+
});
|
|
3154
|
+
} else if (result.error?.type === "validation_error") {
|
|
3155
|
+
this.telemetryReporter?.terminal({
|
|
3156
|
+
outcome: "validation_rejected",
|
|
3157
|
+
provider: "paypal",
|
|
3158
|
+
paymentMethodCategory: "paypal"
|
|
3159
|
+
});
|
|
3160
|
+
} else if (result.error) {
|
|
3161
|
+
this.telemetryReporter?.error({
|
|
3162
|
+
errorCode: "PAYMENT_PROCESSING_FAILED",
|
|
3163
|
+
stage: "processing",
|
|
3164
|
+
provider: "paypal",
|
|
3165
|
+
paymentMethodCategory: "paypal",
|
|
3166
|
+
requestCategory: "intent_create"
|
|
3167
|
+
});
|
|
3168
|
+
}
|
|
3169
|
+
}
|
|
3170
|
+
return result;
|
|
3171
|
+
} catch (error) {
|
|
3172
|
+
this.telemetryReporter?.performance({
|
|
3173
|
+
stage: "processing",
|
|
3174
|
+
durationMs: this.now() - startedAt,
|
|
3175
|
+
durationMode: "machine",
|
|
3176
|
+
provider: "paypal",
|
|
3177
|
+
paymentMethodCategory: "paypal"
|
|
3178
|
+
});
|
|
3179
|
+
this.telemetryReporter?.error({
|
|
3180
|
+
errorCode: "NETWORK_REQUEST_FAILED",
|
|
3181
|
+
stage: "processing",
|
|
3182
|
+
provider: "paypal",
|
|
3183
|
+
paymentMethodCategory: "paypal",
|
|
3184
|
+
requestCategory: "intent_create",
|
|
3185
|
+
statusClass: "network_error"
|
|
3186
|
+
});
|
|
3187
|
+
throw error;
|
|
3188
|
+
}
|
|
1762
3189
|
}
|
|
1763
3190
|
/** Resume a PayPal payment after redirect return. Returns null if no PayPal params in URL. */
|
|
1764
3191
|
async resumePayPalPayment() {
|
|
1765
|
-
|
|
3192
|
+
const startedAt = this.now();
|
|
3193
|
+
try {
|
|
3194
|
+
const result = await this.provider.resumePayPalPayment();
|
|
3195
|
+
if (result === null) return null;
|
|
3196
|
+
this.telemetryReporter?.log({
|
|
3197
|
+
name: "provider.redirect.resumed",
|
|
3198
|
+
stage: "redirect_resume",
|
|
3199
|
+
provider: "paypal",
|
|
3200
|
+
paymentMethodCategory: "paypal"
|
|
3201
|
+
});
|
|
3202
|
+
this.telemetryReporter?.performance({
|
|
3203
|
+
stage: "redirect_resume",
|
|
3204
|
+
durationMs: this.now() - startedAt,
|
|
3205
|
+
durationMode: "machine",
|
|
3206
|
+
provider: "paypal",
|
|
3207
|
+
paymentMethodCategory: "paypal"
|
|
3208
|
+
});
|
|
3209
|
+
if (result.status === "succeeded") {
|
|
3210
|
+
this.telemetryReporter?.terminal({
|
|
3211
|
+
outcome: "payment_succeeded",
|
|
3212
|
+
provider: "paypal",
|
|
3213
|
+
paymentMethodCategory: "paypal"
|
|
3214
|
+
});
|
|
3215
|
+
} else if (isExpectedDecline(result.error)) {
|
|
3216
|
+
this.telemetryReporter?.terminal({
|
|
3217
|
+
outcome: "payment_declined",
|
|
3218
|
+
provider: "paypal",
|
|
3219
|
+
paymentMethodCategory: "paypal"
|
|
3220
|
+
});
|
|
3221
|
+
} else if (result.error?.type === "validation_error") {
|
|
3222
|
+
this.telemetryReporter?.terminal({
|
|
3223
|
+
outcome: "validation_rejected",
|
|
3224
|
+
provider: "paypal",
|
|
3225
|
+
paymentMethodCategory: "paypal"
|
|
3226
|
+
});
|
|
3227
|
+
} else if (result.error) {
|
|
3228
|
+
this.telemetryReporter?.error({
|
|
3229
|
+
errorCode: "REDIRECT_RESUME_FAILED",
|
|
3230
|
+
stage: "redirect_resume",
|
|
3231
|
+
provider: "paypal",
|
|
3232
|
+
paymentMethodCategory: "paypal"
|
|
3233
|
+
});
|
|
3234
|
+
}
|
|
3235
|
+
return result;
|
|
3236
|
+
} catch (error) {
|
|
3237
|
+
this.telemetryReporter?.performance({
|
|
3238
|
+
stage: "redirect_resume",
|
|
3239
|
+
durationMs: this.now() - startedAt,
|
|
3240
|
+
durationMode: "machine",
|
|
3241
|
+
provider: "paypal",
|
|
3242
|
+
paymentMethodCategory: "paypal"
|
|
3243
|
+
});
|
|
3244
|
+
this.telemetryReporter?.error({
|
|
3245
|
+
errorCode: "REDIRECT_RESUME_FAILED",
|
|
3246
|
+
stage: "redirect_resume",
|
|
3247
|
+
provider: "paypal",
|
|
3248
|
+
paymentMethodCategory: "paypal"
|
|
3249
|
+
});
|
|
3250
|
+
throw error;
|
|
3251
|
+
}
|
|
1766
3252
|
}
|
|
1767
3253
|
/** Confirms a payment using the mounted elements. */
|
|
1768
3254
|
async confirmPayment(params) {
|
|
1769
|
-
|
|
3255
|
+
const started = this.now();
|
|
3256
|
+
const provider = telemetryProvider(this.provider.name);
|
|
3257
|
+
this.telemetryReporter?.log({
|
|
3258
|
+
name: "payment.processing.started",
|
|
3259
|
+
stage: "processing",
|
|
3260
|
+
provider,
|
|
3261
|
+
paymentMethodCategory: "unknown"
|
|
3262
|
+
});
|
|
3263
|
+
try {
|
|
3264
|
+
const result = await this.provider.confirmPayment(params);
|
|
3265
|
+
const durationMs = this.now() - started;
|
|
3266
|
+
this.telemetryReporter?.performance({
|
|
3267
|
+
stage: "processing",
|
|
3268
|
+
durationMs,
|
|
3269
|
+
durationMode: "machine",
|
|
3270
|
+
provider,
|
|
3271
|
+
paymentMethodCategory: "unknown"
|
|
3272
|
+
});
|
|
3273
|
+
if (result.status === "succeeded") {
|
|
3274
|
+
this.telemetryReporter?.terminal({
|
|
3275
|
+
outcome: "payment_succeeded",
|
|
3276
|
+
provider,
|
|
3277
|
+
paymentMethodCategory: "unknown"
|
|
3278
|
+
});
|
|
3279
|
+
} else if (isExpectedDecline(result.error)) {
|
|
3280
|
+
this.telemetryReporter?.terminal({
|
|
3281
|
+
outcome: "payment_declined",
|
|
3282
|
+
provider,
|
|
3283
|
+
paymentMethodCategory: "unknown"
|
|
3284
|
+
});
|
|
3285
|
+
} else if (result.error?.type === "validation_error") {
|
|
3286
|
+
this.telemetryReporter?.terminal({
|
|
3287
|
+
outcome: "validation_rejected",
|
|
3288
|
+
provider,
|
|
3289
|
+
paymentMethodCategory: "unknown"
|
|
3290
|
+
});
|
|
3291
|
+
} else if (result.status === "requires_action") {
|
|
3292
|
+
this.telemetryReporter?.terminal({
|
|
3293
|
+
outcome: "action_required",
|
|
3294
|
+
stage: "three_ds_handoff",
|
|
3295
|
+
provider,
|
|
3296
|
+
paymentMethodCategory: "unknown"
|
|
3297
|
+
});
|
|
3298
|
+
} else if (result.status === "failed" && result.error) {
|
|
3299
|
+
this.telemetryReporter?.error({
|
|
3300
|
+
errorCode: "PAYMENT_PROCESSING_FAILED",
|
|
3301
|
+
stage: "processing",
|
|
3302
|
+
provider,
|
|
3303
|
+
paymentMethodCategory: "unknown"
|
|
3304
|
+
});
|
|
3305
|
+
}
|
|
3306
|
+
if (result.status === "succeeded" || result.status === "failed") {
|
|
3307
|
+
this.telemetryReporter?.log({
|
|
3308
|
+
name: "payment.processing.completed",
|
|
3309
|
+
stage: "processing",
|
|
3310
|
+
provider,
|
|
3311
|
+
paymentMethodCategory: "unknown"
|
|
3312
|
+
});
|
|
3313
|
+
} else {
|
|
3314
|
+
this.telemetryReporter?.log({
|
|
3315
|
+
name: "operation.state_transition",
|
|
3316
|
+
stage: result.status === "requires_action" ? "three_ds_handoff" : "processing",
|
|
3317
|
+
provider,
|
|
3318
|
+
paymentMethodCategory: "unknown"
|
|
3319
|
+
});
|
|
3320
|
+
}
|
|
3321
|
+
return result;
|
|
3322
|
+
} catch (error) {
|
|
3323
|
+
this.telemetryReporter?.performance({
|
|
3324
|
+
stage: "processing",
|
|
3325
|
+
durationMs: this.now() - started,
|
|
3326
|
+
durationMode: "machine",
|
|
3327
|
+
provider,
|
|
3328
|
+
paymentMethodCategory: "unknown"
|
|
3329
|
+
});
|
|
3330
|
+
this.telemetryReporter?.error({
|
|
3331
|
+
errorCode: "PAYMENT_PROCESSING_FAILED",
|
|
3332
|
+
stage: "processing",
|
|
3333
|
+
provider,
|
|
3334
|
+
paymentMethodCategory: "unknown"
|
|
3335
|
+
});
|
|
3336
|
+
throw error;
|
|
3337
|
+
}
|
|
1770
3338
|
}
|
|
1771
3339
|
/**
|
|
1772
3340
|
* Retrieves a checkout session by ID via the billing API.
|
|
@@ -1779,17 +3347,15 @@ var FloPay = class {
|
|
|
1779
3347
|
*/
|
|
1780
3348
|
async retrieveSession(sessionId, billingApiUrl) {
|
|
1781
3349
|
if (!sessionId) {
|
|
1782
|
-
throw new
|
|
3350
|
+
throw new import_shared6.FloPayError(
|
|
1783
3351
|
"sessionId is required to retrieve a session.",
|
|
1784
3352
|
"validation_error",
|
|
1785
3353
|
{ param: "sessionId" }
|
|
1786
3354
|
);
|
|
1787
3355
|
}
|
|
1788
|
-
const
|
|
1789
|
-
const api = new PaymentAPI(apiUrl);
|
|
1790
|
-
const unified = await api.getUnifiedCheckoutSession(sessionId);
|
|
3356
|
+
const unified = await this.retrieveUnifiedSession(sessionId, billingApiUrl);
|
|
1791
3357
|
if (!unified.data.session) {
|
|
1792
|
-
throw new
|
|
3358
|
+
throw new import_shared6.FloPayError("Session not found", "api_error");
|
|
1793
3359
|
}
|
|
1794
3360
|
return unified.data.session;
|
|
1795
3361
|
}
|
|
@@ -1802,15 +3368,86 @@ var FloPay = class {
|
|
|
1802
3368
|
*/
|
|
1803
3369
|
async retrieveUnifiedSession(sessionId, billingApiUrl) {
|
|
1804
3370
|
if (!sessionId) {
|
|
1805
|
-
throw new
|
|
3371
|
+
throw new import_shared6.FloPayError(
|
|
1806
3372
|
"sessionId is required.",
|
|
1807
3373
|
"validation_error",
|
|
1808
3374
|
{ param: "sessionId" }
|
|
1809
3375
|
);
|
|
1810
3376
|
}
|
|
1811
|
-
const apiUrl = (0,
|
|
1812
|
-
const
|
|
1813
|
-
|
|
3377
|
+
const apiUrl = (0, import_shared6.resolveBillingApiUrl)(billingApiUrl ?? this.config.billingApiUrl);
|
|
3378
|
+
const started = this.now();
|
|
3379
|
+
this.telemetryReporter?.log({
|
|
3380
|
+
name: "session.read.started",
|
|
3381
|
+
stage: "session_read",
|
|
3382
|
+
requestCategory: "session_read"
|
|
3383
|
+
});
|
|
3384
|
+
let firstByteDuration;
|
|
3385
|
+
const api = createInstrumentedPaymentAPI(apiUrl, {
|
|
3386
|
+
now: () => this.telemetryReporter?.now() ?? telemetryNow(),
|
|
3387
|
+
onFirstByte: (durationMs) => {
|
|
3388
|
+
firstByteDuration = durationMs;
|
|
3389
|
+
},
|
|
3390
|
+
onRetry: (requestCategory, attempt) => {
|
|
3391
|
+
this.telemetryReporter?.log({
|
|
3392
|
+
name: "operation.retry",
|
|
3393
|
+
stage: requestCategory === "session_read" ? "session_read" : "processing",
|
|
3394
|
+
requestCategory,
|
|
3395
|
+
attempt
|
|
3396
|
+
});
|
|
3397
|
+
}
|
|
3398
|
+
});
|
|
3399
|
+
try {
|
|
3400
|
+
const result = await api.getUnifiedCheckoutSession(sessionId);
|
|
3401
|
+
if (firstByteDuration !== void 0) {
|
|
3402
|
+
this.telemetryReporter?.log({
|
|
3403
|
+
name: "session.request.first_byte",
|
|
3404
|
+
stage: "session_first_byte",
|
|
3405
|
+
requestCategory: "session_read",
|
|
3406
|
+
statusClass: "2xx"
|
|
3407
|
+
});
|
|
3408
|
+
this.telemetryReporter?.performance({
|
|
3409
|
+
stage: "session_first_byte",
|
|
3410
|
+
durationMs: firstByteDuration,
|
|
3411
|
+
durationMode: "machine",
|
|
3412
|
+
requestCategory: "session_read",
|
|
3413
|
+
statusClass: "2xx"
|
|
3414
|
+
});
|
|
3415
|
+
}
|
|
3416
|
+
this.telemetryReporter?.log({
|
|
3417
|
+
name: "session.request.completed",
|
|
3418
|
+
stage: "session_complete",
|
|
3419
|
+
requestCategory: "session_read",
|
|
3420
|
+
statusClass: "2xx"
|
|
3421
|
+
});
|
|
3422
|
+
this.telemetryReporter?.log({
|
|
3423
|
+
name: "checkout.data.ready",
|
|
3424
|
+
stage: "checkout_data_ready"
|
|
3425
|
+
});
|
|
3426
|
+
this.telemetryReporter?.performance({
|
|
3427
|
+
stage: "session_complete",
|
|
3428
|
+
durationMs: this.now() - started,
|
|
3429
|
+
durationMode: "machine",
|
|
3430
|
+
requestCategory: "session_read",
|
|
3431
|
+
statusClass: "2xx"
|
|
3432
|
+
});
|
|
3433
|
+
return result;
|
|
3434
|
+
} catch (error) {
|
|
3435
|
+
const statusCode = error instanceof import_shared6.FloPayError ? error.statusCode : void 0;
|
|
3436
|
+
this.telemetryReporter?.error({
|
|
3437
|
+
errorCode: error instanceof import_shared6.FloPayError && error.code === "checkout_processing_timeout" ? "REQUEST_TIMEOUT" : "NETWORK_REQUEST_FAILED",
|
|
3438
|
+
stage: "session_read",
|
|
3439
|
+
provider: "flo",
|
|
3440
|
+
paymentMethodCategory: "unknown"
|
|
3441
|
+
});
|
|
3442
|
+
this.telemetryReporter?.performance({
|
|
3443
|
+
stage: "session_complete",
|
|
3444
|
+
durationMs: this.now() - started,
|
|
3445
|
+
durationMode: "machine",
|
|
3446
|
+
requestCategory: "session_read",
|
|
3447
|
+
statusClass: statusCode ? `${Math.floor(statusCode / 100)}xx` : "network_error"
|
|
3448
|
+
});
|
|
3449
|
+
throw error;
|
|
3450
|
+
}
|
|
1814
3451
|
}
|
|
1815
3452
|
/**
|
|
1816
3453
|
* Returns the raw underlying provider instance (e.g. Stripe object).
|
|
@@ -1822,46 +3459,147 @@ var FloPay = class {
|
|
|
1822
3459
|
}
|
|
1823
3460
|
/** Tears down the SDK instance and releases resources. */
|
|
1824
3461
|
destroy() {
|
|
3462
|
+
this.telemetryReporter?.log({ name: "checkout.unmount", stage: "unmount" });
|
|
3463
|
+
this.telemetryReporter?.destroy();
|
|
3464
|
+
this.cardThreeDsStartedAt.clear();
|
|
1825
3465
|
this.currentElements?.destroy();
|
|
1826
3466
|
this.currentElements = null;
|
|
1827
3467
|
this.provider.destroy();
|
|
1828
3468
|
}
|
|
1829
3469
|
};
|
|
3470
|
+
function createInstrumentedFloPay(provider, config, reporter) {
|
|
3471
|
+
const InstrumentedFloPay = FloPay;
|
|
3472
|
+
return new InstrumentedFloPay(provider, config, reporter);
|
|
3473
|
+
}
|
|
1830
3474
|
|
|
1831
3475
|
// src/load.ts
|
|
1832
3476
|
var instanceCache = /* @__PURE__ */ new Map();
|
|
3477
|
+
function stableCacheValue(value) {
|
|
3478
|
+
if (Array.isArray(value)) return value.map(stableCacheValue);
|
|
3479
|
+
if (value && typeof value === "object") {
|
|
3480
|
+
return Object.fromEntries(
|
|
3481
|
+
Object.entries(value).filter(([, entry]) => entry !== void 0).sort(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => [key, stableCacheValue(entry)])
|
|
3482
|
+
);
|
|
3483
|
+
}
|
|
3484
|
+
return value;
|
|
3485
|
+
}
|
|
3486
|
+
function instanceCacheKey(publishableKey, options) {
|
|
3487
|
+
return JSON.stringify([
|
|
3488
|
+
publishableKey,
|
|
3489
|
+
(0, import_shared7.resolveBillingApiUrl)(options?.billingApiUrl),
|
|
3490
|
+
options?.telemetry !== false,
|
|
3491
|
+
options?.locale ?? "auto",
|
|
3492
|
+
options?.apiVersion ?? null,
|
|
3493
|
+
stableCacheValue(options?.appearance ?? null)
|
|
3494
|
+
]);
|
|
3495
|
+
}
|
|
3496
|
+
var initializationCache = /* @__PURE__ */ new Map();
|
|
1833
3497
|
async function loadFloPay(publishableKey, options) {
|
|
1834
3498
|
if (!publishableKey) {
|
|
1835
|
-
|
|
3499
|
+
const reporter = new TelemetryReporter({
|
|
3500
|
+
billingApiUrl: (0, import_shared7.resolveBillingApiUrl)(options?.billingApiUrl),
|
|
3501
|
+
sdkVersion: import_shared7.SDK_VERSION,
|
|
3502
|
+
enabled: options?.telemetry !== false
|
|
3503
|
+
});
|
|
3504
|
+
reporter.error({
|
|
3505
|
+
errorCode: "CONFIGURATION_INVALID",
|
|
3506
|
+
stage: "sdk_initialize",
|
|
3507
|
+
paymentMethodCategory: "unknown"
|
|
3508
|
+
});
|
|
3509
|
+
void reporter.flush().catch(() => {
|
|
3510
|
+
}).finally(() => reporter.destroy());
|
|
3511
|
+
throw new import_shared7.FloPayError(
|
|
1836
3512
|
"A publishable key is required to initialize FloPay.",
|
|
1837
3513
|
"validation_error",
|
|
1838
3514
|
{ param: "publishableKey" }
|
|
1839
3515
|
);
|
|
1840
3516
|
}
|
|
1841
|
-
const
|
|
1842
|
-
|
|
3517
|
+
const cacheKey = instanceCacheKey(publishableKey, options);
|
|
3518
|
+
const cached = instanceCache.get(cacheKey);
|
|
3519
|
+
if (cached) {
|
|
3520
|
+
if (options?.telemetry !== false) {
|
|
3521
|
+
getFloPayTelemetryBridge(cached)?.log({
|
|
3522
|
+
name: "sdk.cache.hit",
|
|
3523
|
+
stage: "sdk_initialize"
|
|
3524
|
+
});
|
|
3525
|
+
}
|
|
3526
|
+
return cached;
|
|
3527
|
+
}
|
|
3528
|
+
const initializing = initializationCache.get(cacheKey);
|
|
3529
|
+
if (initializing) return initializing;
|
|
1843
3530
|
const config = {
|
|
1844
|
-
|
|
1845
|
-
|
|
3531
|
+
...options,
|
|
3532
|
+
publishableKey
|
|
1846
3533
|
};
|
|
1847
|
-
const
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
3534
|
+
const initialization = (async () => {
|
|
3535
|
+
const reporter = new TelemetryReporter({
|
|
3536
|
+
billingApiUrl: (0, import_shared7.resolveBillingApiUrl)(config.billingApiUrl),
|
|
3537
|
+
sdkVersion: import_shared7.SDK_VERSION,
|
|
3538
|
+
enabled: config.telemetry !== false
|
|
3539
|
+
});
|
|
3540
|
+
const initializationStarted = reporter.now();
|
|
3541
|
+
reporter.log({ name: "sdk.initialize.started", stage: "sdk_initialize" });
|
|
3542
|
+
reporter.log({ name: "sdk.cache.miss", stage: "sdk_initialize" });
|
|
3543
|
+
reporter.log({
|
|
3544
|
+
name: "provider.load.started",
|
|
3545
|
+
stage: "provider_load",
|
|
3546
|
+
provider: "stripe"
|
|
3547
|
+
});
|
|
3548
|
+
const adapter = new StripeAdapter();
|
|
3549
|
+
try {
|
|
3550
|
+
await adapter.initialize(config);
|
|
3551
|
+
} catch (error) {
|
|
3552
|
+
reporter.error({
|
|
3553
|
+
errorCode: "SDK_INITIALIZATION_FAILED",
|
|
3554
|
+
stage: "sdk_initialize",
|
|
3555
|
+
provider: "stripe",
|
|
3556
|
+
paymentMethodCategory: "unknown"
|
|
3557
|
+
});
|
|
3558
|
+
reporter.destroy();
|
|
3559
|
+
throw error;
|
|
3560
|
+
}
|
|
3561
|
+
reporter.log({ name: "provider.ready", stage: "provider_ready", provider: "stripe" });
|
|
3562
|
+
reporter.log({
|
|
3563
|
+
name: "provider.availability.checked",
|
|
3564
|
+
stage: "provider_ready",
|
|
3565
|
+
provider: "stripe"
|
|
3566
|
+
});
|
|
3567
|
+
reporter.log({ name: "sdk.initialize.ready", stage: "sdk_initialize" });
|
|
3568
|
+
const initializationDuration = reporter.now() - initializationStarted;
|
|
3569
|
+
reporter.performance({
|
|
3570
|
+
stage: "sdk_initialize",
|
|
3571
|
+
durationMs: initializationDuration,
|
|
3572
|
+
durationMode: "machine",
|
|
3573
|
+
provider: "stripe"
|
|
3574
|
+
});
|
|
3575
|
+
reporter.performance({
|
|
3576
|
+
stage: "provider_ready",
|
|
3577
|
+
durationMs: initializationDuration,
|
|
3578
|
+
durationMode: "machine",
|
|
3579
|
+
provider: "stripe"
|
|
3580
|
+
});
|
|
3581
|
+
const instance = createInstrumentedFloPay(adapter, config, reporter);
|
|
3582
|
+
instanceCache.set(cacheKey, instance);
|
|
3583
|
+
return instance;
|
|
3584
|
+
})();
|
|
3585
|
+
initializationCache.set(cacheKey, initialization);
|
|
3586
|
+
try {
|
|
3587
|
+
return await initialization;
|
|
3588
|
+
} finally {
|
|
3589
|
+
if (initializationCache.get(cacheKey) === initialization) {
|
|
3590
|
+
initializationCache.delete(cacheKey);
|
|
3591
|
+
}
|
|
3592
|
+
}
|
|
1852
3593
|
}
|
|
1853
3594
|
|
|
1854
3595
|
// src/create-checkout-session.ts
|
|
1855
|
-
var
|
|
3596
|
+
var import_shared8 = require("@flopay/shared");
|
|
1856
3597
|
var MAX_COUPON_CODES = 5;
|
|
1857
|
-
function readString2(value) {
|
|
1858
|
-
return typeof value === "string" && value.trim() ? value : void 0;
|
|
1859
|
-
}
|
|
1860
3598
|
function buildCheckoutSessionError(status, payload) {
|
|
1861
3599
|
const nested = payload?.error;
|
|
1862
|
-
const code =
|
|
1863
|
-
const message =
|
|
1864
|
-
return new
|
|
3600
|
+
const code = readErrorString(payload?.code) ?? readErrorString(nested?.code) ?? `http_${status}`;
|
|
3601
|
+
const message = readErrorString(payload?.message) ?? readErrorString(nested?.message) ?? defaultMessageForCode(code, status);
|
|
3602
|
+
return new import_shared8.FloPayError(message, "api_error", { code, statusCode: status });
|
|
1865
3603
|
}
|
|
1866
3604
|
function defaultMessageForCode(code, status) {
|
|
1867
3605
|
switch (code) {
|
|
@@ -1873,7 +3611,7 @@ function defaultMessageForCode(code, status) {
|
|
|
1873
3611
|
return `Failed to create checkout session (HTTP ${status}).`;
|
|
1874
3612
|
}
|
|
1875
3613
|
}
|
|
1876
|
-
async function
|
|
3614
|
+
async function createCheckoutSessionCore(options, onFirstByte) {
|
|
1877
3615
|
const {
|
|
1878
3616
|
billingApiUrl,
|
|
1879
3617
|
checkoutBaseUrl,
|
|
@@ -1894,18 +3632,18 @@ async function createCheckoutSession(options) {
|
|
|
1894
3632
|
utmMetadata,
|
|
1895
3633
|
idempotencyKey
|
|
1896
3634
|
} = options;
|
|
1897
|
-
const resolvedIdempotencyKey = (0,
|
|
3635
|
+
const resolvedIdempotencyKey = (0, import_shared8.resolveIdempotencyKey)(idempotencyKey);
|
|
1898
3636
|
if (couponCodes.length > MAX_COUPON_CODES) {
|
|
1899
|
-
throw new
|
|
3637
|
+
throw new import_shared8.FloPayError(
|
|
1900
3638
|
`Too many coupon codes \u2014 a checkout session accepts at most ${MAX_COUPON_CODES}.`,
|
|
1901
3639
|
"validation_error",
|
|
1902
3640
|
{ code: "CouponLimitExceeded", param: "couponCodes" }
|
|
1903
3641
|
);
|
|
1904
3642
|
}
|
|
1905
|
-
const wireProducts = products ?? (0,
|
|
1906
|
-
const sessionCurrency = (0,
|
|
3643
|
+
const wireProducts = products ?? (0, import_shared8.foldIntoProducts)(items, subscriptions);
|
|
3644
|
+
const sessionCurrency = (0, import_shared8.resolveSessionCurrency)(currency, items, subscriptions, wireProducts);
|
|
1907
3645
|
if (!sessionCurrency) {
|
|
1908
|
-
throw new
|
|
3646
|
+
throw new import_shared8.FloPayError(
|
|
1909
3647
|
"currency is required: pass `currency` on the session, or include a `currency` on the first item/subscription/product.",
|
|
1910
3648
|
"validation_error",
|
|
1911
3649
|
{ code: "CurrencyRequired", param: "currency" }
|
|
@@ -1913,12 +3651,12 @@ async function createCheckoutSession(options) {
|
|
|
1913
3651
|
}
|
|
1914
3652
|
const payload = {
|
|
1915
3653
|
clientId,
|
|
1916
|
-
checkoutVersion:
|
|
3654
|
+
checkoutVersion: import_shared8.SDK_VERSION,
|
|
1917
3655
|
successUrl,
|
|
1918
3656
|
cancelUrl,
|
|
1919
3657
|
currency: sessionCurrency,
|
|
1920
3658
|
checkoutMode,
|
|
1921
|
-
products: wireProducts.map((product) => (0,
|
|
3659
|
+
products: wireProducts.map((product) => (0, import_shared8.buildProductPayload)(product, sessionCurrency)),
|
|
1922
3660
|
accountData: {
|
|
1923
3661
|
userId: account.userId,
|
|
1924
3662
|
firstName: account.firstName ?? null,
|
|
@@ -1945,7 +3683,7 @@ async function createCheckoutSession(options) {
|
|
|
1945
3683
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
1946
3684
|
const headers = { "Content-Type": "application/json" };
|
|
1947
3685
|
if (resolvedIdempotencyKey) {
|
|
1948
|
-
headers[
|
|
3686
|
+
headers[import_shared8.IDEMPOTENCY_KEY_HEADER] = resolvedIdempotencyKey;
|
|
1949
3687
|
}
|
|
1950
3688
|
let status;
|
|
1951
3689
|
let body;
|
|
@@ -1956,6 +3694,10 @@ async function createCheckoutSession(options) {
|
|
|
1956
3694
|
body: JSON.stringify(payload),
|
|
1957
3695
|
signal: controller.signal
|
|
1958
3696
|
});
|
|
3697
|
+
try {
|
|
3698
|
+
onFirstByte?.(response.status);
|
|
3699
|
+
} catch {
|
|
3700
|
+
}
|
|
1959
3701
|
status = response.status;
|
|
1960
3702
|
try {
|
|
1961
3703
|
body = await response.json();
|
|
@@ -1974,7 +3716,7 @@ async function createCheckoutSession(options) {
|
|
|
1974
3716
|
throw new Error("Checkout session created but no UUID was returned by the billing API");
|
|
1975
3717
|
}
|
|
1976
3718
|
if (!nonce) {
|
|
1977
|
-
throw new
|
|
3719
|
+
throw new import_shared8.FloPayError(
|
|
1978
3720
|
"Checkout session created but no `nonce` was returned by the billing API. Upgrade the billing service to TeamFloPay/backend#640 or later.",
|
|
1979
3721
|
"api_error",
|
|
1980
3722
|
{ code: "MissingCheckoutSessionToken" }
|
|
@@ -2017,30 +3759,137 @@ async function createCheckoutSession(options) {
|
|
|
2017
3759
|
}
|
|
2018
3760
|
return { status };
|
|
2019
3761
|
}
|
|
3762
|
+
async function createCheckoutSession(options) {
|
|
3763
|
+
const telemetry = beginCreateSessionTelemetry(options);
|
|
3764
|
+
const recordFirstByte = createFirstByteRecorder(telemetry);
|
|
3765
|
+
try {
|
|
3766
|
+
const result = await createCheckoutSessionCore(options, recordFirstByte);
|
|
3767
|
+
completeCreateSessionTelemetry(telemetry, result);
|
|
3768
|
+
return result;
|
|
3769
|
+
} catch (error) {
|
|
3770
|
+
failCreateSessionTelemetry(telemetry.reporter, error);
|
|
3771
|
+
throw error;
|
|
3772
|
+
} finally {
|
|
3773
|
+
finishCreateSessionTelemetry(telemetry.reporter);
|
|
3774
|
+
}
|
|
3775
|
+
}
|
|
3776
|
+
function createFirstByteRecorder(telemetry) {
|
|
3777
|
+
let recorded = false;
|
|
3778
|
+
return (status) => {
|
|
3779
|
+
if (recorded) return;
|
|
3780
|
+
recorded = true;
|
|
3781
|
+
const statusClass = `${Math.floor(status / 100)}xx`;
|
|
3782
|
+
telemetry.reporter.log({
|
|
3783
|
+
name: "session.request.first_byte",
|
|
3784
|
+
stage: "session_first_byte",
|
|
3785
|
+
requestCategory: "session_create",
|
|
3786
|
+
statusClass
|
|
3787
|
+
});
|
|
3788
|
+
telemetry.reporter.performance({
|
|
3789
|
+
stage: "session_first_byte",
|
|
3790
|
+
durationMs: telemetry.reporter.now() - telemetry.startedAt,
|
|
3791
|
+
durationMode: "machine",
|
|
3792
|
+
requestCategory: "session_create",
|
|
3793
|
+
statusClass
|
|
3794
|
+
});
|
|
3795
|
+
};
|
|
3796
|
+
}
|
|
3797
|
+
function beginCreateSessionTelemetry(options) {
|
|
3798
|
+
const reporter = new TelemetryReporter({
|
|
3799
|
+
billingApiUrl: options.billingApiUrl,
|
|
3800
|
+
sdkVersion: import_shared8.SDK_VERSION,
|
|
3801
|
+
enabled: options.telemetry !== false
|
|
3802
|
+
});
|
|
3803
|
+
const startedAt = reporter.now();
|
|
3804
|
+
reporter.log({
|
|
3805
|
+
name: "session.create.started",
|
|
3806
|
+
stage: "session_create",
|
|
3807
|
+
requestCategory: "session_create"
|
|
3808
|
+
});
|
|
3809
|
+
return { reporter, startedAt };
|
|
3810
|
+
}
|
|
3811
|
+
function completeCreateSessionTelemetry(telemetry, result) {
|
|
3812
|
+
const statusClass = `${Math.floor(result.status / 100)}xx`;
|
|
3813
|
+
telemetry.reporter.log({
|
|
3814
|
+
name: "session.request.completed",
|
|
3815
|
+
stage: "session_complete",
|
|
3816
|
+
requestCategory: "session_create",
|
|
3817
|
+
statusClass
|
|
3818
|
+
});
|
|
3819
|
+
telemetry.reporter.performance({
|
|
3820
|
+
stage: "session_complete",
|
|
3821
|
+
durationMs: telemetry.reporter.now() - telemetry.startedAt,
|
|
3822
|
+
durationMode: "machine",
|
|
3823
|
+
requestCategory: "session_create",
|
|
3824
|
+
statusClass
|
|
3825
|
+
});
|
|
3826
|
+
}
|
|
3827
|
+
function failCreateSessionTelemetry(reporter, error) {
|
|
3828
|
+
if (error instanceof import_shared8.FloPayError && error.type === "validation_error") {
|
|
3829
|
+
reporter.terminal({
|
|
3830
|
+
outcome: "validation_rejected",
|
|
3831
|
+
stage: "session_create",
|
|
3832
|
+
requestCategory: "session_create"
|
|
3833
|
+
});
|
|
3834
|
+
return;
|
|
3835
|
+
}
|
|
3836
|
+
const statusCode = error instanceof import_shared8.FloPayError ? error.statusCode : void 0;
|
|
3837
|
+
reporter.error({
|
|
3838
|
+
errorCode: error instanceof Error && error.name === "AbortError" ? "REQUEST_TIMEOUT" : error instanceof TypeError ? "NETWORK_REQUEST_FAILED" : "CHECKOUT_SESSION_CREATE_FAILED",
|
|
3839
|
+
stage: "session_create",
|
|
3840
|
+
requestCategory: "session_create",
|
|
3841
|
+
statusClass: error instanceof Error && error.name === "AbortError" ? "timeout" : statusCode ? `${Math.floor(statusCode / 100)}xx` : "network_error"
|
|
3842
|
+
});
|
|
3843
|
+
}
|
|
3844
|
+
function finishCreateSessionTelemetry(reporter) {
|
|
3845
|
+
void reporter.flush().catch(() => {
|
|
3846
|
+
}).finally(() => reporter.destroy());
|
|
3847
|
+
}
|
|
2020
3848
|
async function createCheckoutSessionWithRetries(options) {
|
|
2021
3849
|
const { maxRetries = 3, ...sessionOptions } = options;
|
|
3850
|
+
const telemetry = beginCreateSessionTelemetry(options);
|
|
3851
|
+
const recordFirstByte = createFirstByteRecorder(telemetry);
|
|
2022
3852
|
if (!Number.isFinite(maxRetries) || !Number.isInteger(maxRetries) || maxRetries <= 0) {
|
|
3853
|
+
telemetry.reporter.terminal({
|
|
3854
|
+
outcome: "validation_rejected",
|
|
3855
|
+
stage: "session_create",
|
|
3856
|
+
requestCategory: "session_create"
|
|
3857
|
+
});
|
|
3858
|
+
finishCreateSessionTelemetry(telemetry.reporter);
|
|
2023
3859
|
throw new Error("Number of retries must be greater than 0");
|
|
2024
3860
|
}
|
|
2025
3861
|
const attemptOptions = {
|
|
2026
3862
|
...sessionOptions,
|
|
2027
|
-
idempotencyKey: (0,
|
|
3863
|
+
idempotencyKey: (0, import_shared8.resolveIdempotencyKey)(sessionOptions.idempotencyKey)
|
|
2028
3864
|
};
|
|
2029
3865
|
let lastErr;
|
|
2030
3866
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
2031
3867
|
try {
|
|
2032
|
-
|
|
3868
|
+
const result = await createCheckoutSessionCore(attemptOptions, recordFirstByte);
|
|
3869
|
+
completeCreateSessionTelemetry(telemetry, result);
|
|
3870
|
+
finishCreateSessionTelemetry(telemetry.reporter);
|
|
3871
|
+
return result;
|
|
2033
3872
|
} catch (err) {
|
|
2034
3873
|
lastErr = err;
|
|
2035
3874
|
const isTransportAbort = err instanceof Error && err.name === "AbortError";
|
|
2036
|
-
const isInProgressReplay = err instanceof
|
|
3875
|
+
const isInProgressReplay = err instanceof import_shared8.FloPayError && err.code === import_shared8.IDEMPOTENCY_IN_PROGRESS_CODE;
|
|
2037
3876
|
if ((isTransportAbort || isInProgressReplay) && attempt < maxRetries) {
|
|
3877
|
+
telemetry.reporter.log({
|
|
3878
|
+
name: "operation.retry",
|
|
3879
|
+
stage: "session_create",
|
|
3880
|
+
requestCategory: "session_create",
|
|
3881
|
+
attempt: attempt + 1
|
|
3882
|
+
});
|
|
2038
3883
|
await new Promise((r) => setTimeout(r, 100 * Math.pow(2, attempt)));
|
|
2039
3884
|
continue;
|
|
2040
3885
|
}
|
|
3886
|
+
failCreateSessionTelemetry(telemetry.reporter, err);
|
|
3887
|
+
finishCreateSessionTelemetry(telemetry.reporter);
|
|
2041
3888
|
throw err;
|
|
2042
3889
|
}
|
|
2043
3890
|
}
|
|
3891
|
+
failCreateSessionTelemetry(telemetry.reporter, lastErr);
|
|
3892
|
+
finishCreateSessionTelemetry(telemetry.reporter);
|
|
2044
3893
|
throw lastErr ?? new Error("Unknown error during checkout session creation");
|
|
2045
3894
|
}
|
|
2046
3895
|
// Annotate the CommonJS export names for ESM import in node:
|