@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.mjs
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
// src/load.ts
|
|
2
|
-
import { FloPayError as FloPayError6 } from "@flopay/shared";
|
|
2
|
+
import { FloPayError as FloPayError6, resolveBillingApiUrl as resolveBillingApiUrl3, SDK_VERSION as SDK_VERSION4 } from "@flopay/shared";
|
|
3
3
|
|
|
4
4
|
// src/stripe-adapter.ts
|
|
5
|
+
import { loadStripe } from "@stripe/stripe-js";
|
|
5
6
|
import { FloPayError, isSetupIntentClientSecret } from "@flopay/shared";
|
|
7
|
+
|
|
8
|
+
// src/card-three-ds.ts
|
|
9
|
+
var CARD_THREE_DS_ATTEMPT_TTL_MS = 15 * 6e4;
|
|
10
|
+
var MAX_CARD_THREE_DS_ATTEMPTS = 32;
|
|
11
|
+
|
|
12
|
+
// src/stripe-adapter.ts
|
|
6
13
|
function toStripeElementType(type) {
|
|
7
14
|
const map = {
|
|
8
15
|
payment: "payment",
|
|
@@ -25,6 +32,9 @@ function toStripeAppearanceTheme(theme) {
|
|
|
25
32
|
return "stripe";
|
|
26
33
|
}
|
|
27
34
|
}
|
|
35
|
+
function cardThreeDsNow() {
|
|
36
|
+
return globalThis.performance?.now() ?? Date.now();
|
|
37
|
+
}
|
|
28
38
|
function wrapStripeElement(stripeElement) {
|
|
29
39
|
const el = stripeElement;
|
|
30
40
|
return {
|
|
@@ -48,11 +58,31 @@ function wrapStripeElement(stripeElement) {
|
|
|
48
58
|
}
|
|
49
59
|
};
|
|
50
60
|
}
|
|
61
|
+
function toStripeBillingDetails(billing) {
|
|
62
|
+
return {
|
|
63
|
+
billing_details: {
|
|
64
|
+
...billing.email ? { email: billing.email } : {},
|
|
65
|
+
...billing.name ? { name: billing.name } : {},
|
|
66
|
+
...billing.address ? {
|
|
67
|
+
address: {
|
|
68
|
+
...billing.address.country ? { country: billing.address.country } : {},
|
|
69
|
+
...billing.address.postal_code ? { postal_code: billing.address.postal_code } : {},
|
|
70
|
+
...billing.address.city ? { city: billing.address.city } : {},
|
|
71
|
+
...billing.address.line1 ? { line1: billing.address.line1 } : {},
|
|
72
|
+
...billing.address.line2 ? { line2: billing.address.line2 } : {},
|
|
73
|
+
...billing.address.state ? { state: billing.address.state } : {}
|
|
74
|
+
}
|
|
75
|
+
} : {}
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
}
|
|
51
79
|
var StripeAdapter = class {
|
|
52
80
|
constructor() {
|
|
53
81
|
this.name = "stripe";
|
|
54
82
|
this.stripe = null;
|
|
55
83
|
this.elements = null;
|
|
84
|
+
this.pendingCardThreeDsAttempts = /* @__PURE__ */ new Map();
|
|
85
|
+
this.cardThreeDsAttemptSequence = 0;
|
|
56
86
|
// Serialized appearance currently applied to `this.elements`. Used to detect
|
|
57
87
|
// when consumers swap themes mid-session so we can live-update the Stripe
|
|
58
88
|
// Elements group instead of returning a stale-styled cache. `null` while no
|
|
@@ -63,16 +93,6 @@ var StripeAdapter = class {
|
|
|
63
93
|
if (typeof window === "undefined") {
|
|
64
94
|
return;
|
|
65
95
|
}
|
|
66
|
-
let loadStripe;
|
|
67
|
-
try {
|
|
68
|
-
({ loadStripe } = await import("@stripe/stripe-js"));
|
|
69
|
-
} catch {
|
|
70
|
-
throw new FloPayError(
|
|
71
|
-
"Failed to load @stripe/stripe-js. Reinstall @flopay/js.",
|
|
72
|
-
"api_error",
|
|
73
|
-
{ code: "StripeJsLoadFailed" }
|
|
74
|
-
);
|
|
75
|
-
}
|
|
76
96
|
const stripe = await loadStripe(config.publishableKey, {
|
|
77
97
|
locale: config.locale ?? "auto"
|
|
78
98
|
});
|
|
@@ -184,22 +204,7 @@ var StripeAdapter = class {
|
|
|
184
204
|
};
|
|
185
205
|
}
|
|
186
206
|
const cardNumberEl = this.elements.getElement("cardNumber");
|
|
187
|
-
const stripeBilling = billingDetails ? {
|
|
188
|
-
billing_details: {
|
|
189
|
-
...billingDetails.email ? { email: billingDetails.email } : {},
|
|
190
|
-
...billingDetails.name ? { name: billingDetails.name } : {},
|
|
191
|
-
...billingDetails.address ? {
|
|
192
|
-
address: {
|
|
193
|
-
...billingDetails.address.country ? { country: billingDetails.address.country } : {},
|
|
194
|
-
...billingDetails.address.postal_code ? { postal_code: billingDetails.address.postal_code } : {},
|
|
195
|
-
...billingDetails.address.city ? { city: billingDetails.address.city } : {},
|
|
196
|
-
...billingDetails.address.line1 ? { line1: billingDetails.address.line1 } : {},
|
|
197
|
-
...billingDetails.address.line2 ? { line2: billingDetails.address.line2 } : {},
|
|
198
|
-
...billingDetails.address.state ? { state: billingDetails.address.state } : {}
|
|
199
|
-
}
|
|
200
|
-
} : {}
|
|
201
|
-
}
|
|
202
|
-
} : {};
|
|
207
|
+
const stripeBilling = billingDetails ? toStripeBillingDetails(billingDetails) : {};
|
|
203
208
|
const { error, paymentMethod } = cardNumberEl ? await this.stripe.createPaymentMethod({
|
|
204
209
|
type: "card",
|
|
205
210
|
card: cardNumberEl,
|
|
@@ -233,41 +238,84 @@ var StripeAdapter = class {
|
|
|
233
238
|
{ payment_method: params.paymentMethodId }
|
|
234
239
|
);
|
|
235
240
|
if (setupError) {
|
|
236
|
-
return {
|
|
241
|
+
return this.withCardThreeDsLifecycle(params, {
|
|
237
242
|
status: "failed",
|
|
238
243
|
error: new FloPayError(
|
|
239
244
|
setupError.message ?? "Payment failed",
|
|
240
245
|
"api_error",
|
|
241
246
|
{ code: setupError.code, declineCode: setupError.decline_code }
|
|
242
247
|
)
|
|
243
|
-
};
|
|
248
|
+
});
|
|
244
249
|
}
|
|
245
|
-
return {
|
|
250
|
+
return this.withCardThreeDsLifecycle(params, {
|
|
246
251
|
status: setupIntent?.status ?? "failed",
|
|
247
252
|
paymentIntentId: setupIntent?.id,
|
|
248
253
|
paymentMethodId: this.extractPaymentMethodId(setupIntent?.payment_method)
|
|
249
|
-
};
|
|
254
|
+
});
|
|
250
255
|
}
|
|
251
256
|
const { error, paymentIntent } = await this.stripe.confirmCardPayment(
|
|
252
257
|
params.clientSecret,
|
|
253
258
|
{ payment_method: params.paymentMethodId }
|
|
254
259
|
);
|
|
255
260
|
if (error) {
|
|
256
|
-
return {
|
|
261
|
+
return this.withCardThreeDsLifecycle(params, {
|
|
257
262
|
status: "failed",
|
|
258
263
|
error: new FloPayError(
|
|
259
264
|
error.message ?? "Payment failed",
|
|
260
265
|
"api_error",
|
|
261
266
|
{ code: error.code, declineCode: error.decline_code }
|
|
262
267
|
)
|
|
263
|
-
};
|
|
268
|
+
});
|
|
264
269
|
}
|
|
265
|
-
return {
|
|
270
|
+
return this.withCardThreeDsLifecycle(params, {
|
|
266
271
|
status: paymentIntent?.status ?? "failed",
|
|
267
272
|
paymentIntentId: paymentIntent?.id,
|
|
268
273
|
paymentMethodId: this.extractPaymentMethodId(paymentIntent?.payment_method)
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Bind provider-observed 3DS milestones to the exact confirmation context.
|
|
278
|
+
* The sensitive client secret/payment method pair stays only in this private,
|
|
279
|
+
* in-memory key; callers and telemetry receive an unrelated opaque id.
|
|
280
|
+
*/
|
|
281
|
+
withCardThreeDsLifecycle(params, result) {
|
|
282
|
+
const contextKey = `${params.clientSecret}\0${params.paymentMethodId}`;
|
|
283
|
+
const now = cardThreeDsNow();
|
|
284
|
+
this.pruneExpiredCardThreeDsAttempts(now);
|
|
285
|
+
if (result.status === "requires_action") {
|
|
286
|
+
let attempt2 = this.pendingCardThreeDsAttempts.get(contextKey);
|
|
287
|
+
if (!attempt2) {
|
|
288
|
+
while (this.pendingCardThreeDsAttempts.size >= MAX_CARD_THREE_DS_ATTEMPTS) {
|
|
289
|
+
const oldestContextKey = this.pendingCardThreeDsAttempts.keys().next().value;
|
|
290
|
+
if (oldestContextKey === void 0) break;
|
|
291
|
+
this.pendingCardThreeDsAttempts.delete(oldestContextKey);
|
|
292
|
+
}
|
|
293
|
+
attempt2 = {
|
|
294
|
+
attemptId: `card_3ds_${this.cardThreeDsAttemptSequence++}`,
|
|
295
|
+
startedAt: now
|
|
296
|
+
};
|
|
297
|
+
this.pendingCardThreeDsAttempts.set(contextKey, attempt2);
|
|
298
|
+
}
|
|
299
|
+
return { ...result, threeDs: { attemptId: attempt2.attemptId, status: "handoff" } };
|
|
300
|
+
}
|
|
301
|
+
const attempt = this.pendingCardThreeDsAttempts.get(contextKey);
|
|
302
|
+
if (!attempt) return result;
|
|
303
|
+
this.pendingCardThreeDsAttempts.delete(contextKey);
|
|
304
|
+
return {
|
|
305
|
+
...result,
|
|
306
|
+
threeDs: {
|
|
307
|
+
attemptId: attempt.attemptId,
|
|
308
|
+
status: result.error || result.status === "failed" ? "failed" : "returned"
|
|
309
|
+
}
|
|
269
310
|
};
|
|
270
311
|
}
|
|
312
|
+
pruneExpiredCardThreeDsAttempts(now) {
|
|
313
|
+
for (const [contextKey, attempt] of this.pendingCardThreeDsAttempts) {
|
|
314
|
+
if (now - attempt.startedAt >= CARD_THREE_DS_ATTEMPT_TTL_MS) {
|
|
315
|
+
this.pendingCardThreeDsAttempts.delete(contextKey);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
271
319
|
async confirmPayment(params) {
|
|
272
320
|
if (!this.stripe || !this.elements) {
|
|
273
321
|
throw new FloPayError(
|
|
@@ -276,22 +324,7 @@ var StripeAdapter = class {
|
|
|
276
324
|
);
|
|
277
325
|
}
|
|
278
326
|
const billing = params.billingDetails;
|
|
279
|
-
const paymentMethodData = billing ?
|
|
280
|
-
billing_details: {
|
|
281
|
-
...billing.email ? { email: billing.email } : {},
|
|
282
|
-
...billing.name ? { name: billing.name } : {},
|
|
283
|
-
...billing.address ? {
|
|
284
|
-
address: {
|
|
285
|
-
...billing.address.country ? { country: billing.address.country } : {},
|
|
286
|
-
...billing.address.postal_code ? { postal_code: billing.address.postal_code } : {},
|
|
287
|
-
...billing.address.city ? { city: billing.address.city } : {},
|
|
288
|
-
...billing.address.line1 ? { line1: billing.address.line1 } : {},
|
|
289
|
-
...billing.address.line2 ? { line2: billing.address.line2 } : {},
|
|
290
|
-
...billing.address.state ? { state: billing.address.state } : {}
|
|
291
|
-
}
|
|
292
|
-
} : {}
|
|
293
|
-
}
|
|
294
|
-
} : void 0;
|
|
327
|
+
const paymentMethodData = billing ? toStripeBillingDetails(billing) : void 0;
|
|
295
328
|
const { error, paymentIntent } = await this.stripe.confirmPayment({
|
|
296
329
|
elements: this.elements,
|
|
297
330
|
clientSecret: params.clientSecret,
|
|
@@ -454,6 +487,7 @@ var StripeAdapter = class {
|
|
|
454
487
|
return this.stripe.elements(elementsOptions);
|
|
455
488
|
}
|
|
456
489
|
destroy() {
|
|
490
|
+
this.pendingCardThreeDsAttempts.clear();
|
|
457
491
|
this.elements = null;
|
|
458
492
|
this.appliedAppearanceKey = null;
|
|
459
493
|
this.stripe = null;
|
|
@@ -461,7 +495,7 @@ var StripeAdapter = class {
|
|
|
461
495
|
};
|
|
462
496
|
|
|
463
497
|
// src/flopay.ts
|
|
464
|
-
import { FloPayError as FloPayError5, resolveBillingApiUrl } from "@flopay/shared";
|
|
498
|
+
import { FloPayError as FloPayError5, resolveBillingApiUrl as resolveBillingApiUrl2, SDK_VERSION as SDK_VERSION3 } from "@flopay/shared";
|
|
465
499
|
|
|
466
500
|
// src/elements.ts
|
|
467
501
|
import "@flopay/shared";
|
|
@@ -525,6 +559,19 @@ import {
|
|
|
525
559
|
resolveSessionCurrency
|
|
526
560
|
} from "@flopay/shared";
|
|
527
561
|
|
|
562
|
+
// src/api-error.ts
|
|
563
|
+
function readErrorString(value) {
|
|
564
|
+
return typeof value === "string" && value.trim() ? value : void 0;
|
|
565
|
+
}
|
|
566
|
+
function readErrorMessage(value) {
|
|
567
|
+
if (typeof value === "string") return value.trim() ? value : void 0;
|
|
568
|
+
if (Array.isArray(value)) {
|
|
569
|
+
const joined = value.filter((entry) => typeof entry === "string" && entry.trim().length > 0).join("; ");
|
|
570
|
+
return joined || void 0;
|
|
571
|
+
}
|
|
572
|
+
return void 0;
|
|
573
|
+
}
|
|
574
|
+
|
|
528
575
|
// src/session-display-cache.ts
|
|
529
576
|
var STORAGE_KEY_PREFIX = "flopay_session_display:";
|
|
530
577
|
var DEFAULT_TTL_MS = 60 * 60 * 1e3;
|
|
@@ -591,6 +638,341 @@ function clearSessionDisplayData(sessionId) {
|
|
|
591
638
|
}
|
|
592
639
|
}
|
|
593
640
|
|
|
641
|
+
// src/telemetry-reporter.ts
|
|
642
|
+
import {
|
|
643
|
+
buildTelemetryErrorEvent,
|
|
644
|
+
buildTelemetryLogEvent,
|
|
645
|
+
buildTelemetryPerformanceEvent,
|
|
646
|
+
buildTelemetryTerminalEvent,
|
|
647
|
+
serializeTelemetryBatch,
|
|
648
|
+
TELEMETRY_MAX_BATCH_BYTES
|
|
649
|
+
} from "@flopay/shared";
|
|
650
|
+
var TELEMETRY_PATH = "/v1/sdk-telemetry/events";
|
|
651
|
+
var MAX_BATCH_SIZE = 16;
|
|
652
|
+
var MAX_QUEUE_SIZE = 64;
|
|
653
|
+
var UPLOAD_TIMEOUT_MS = 1500;
|
|
654
|
+
var ERROR_DEDUPLICATION_WINDOW_MS = 1e3;
|
|
655
|
+
var MAX_REPORTED_FAILURES = 64;
|
|
656
|
+
var DEDUPLICATION_EVENT_ID = "00000000-0000-4000-8000-000000000000";
|
|
657
|
+
var EVENT_BUDGETS = {
|
|
658
|
+
technical_error: 8,
|
|
659
|
+
lifecycle: 32,
|
|
660
|
+
expected_outcome: 32,
|
|
661
|
+
performance: 24
|
|
662
|
+
};
|
|
663
|
+
function createUuidV4() {
|
|
664
|
+
try {
|
|
665
|
+
return globalThis.crypto.randomUUID();
|
|
666
|
+
} catch {
|
|
667
|
+
const bytes = new Uint8Array(16);
|
|
668
|
+
try {
|
|
669
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
670
|
+
} catch {
|
|
671
|
+
for (let index = 0; index < bytes.length; index += 1) {
|
|
672
|
+
bytes[index] = Math.floor(Math.random() * 256);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
bytes[6] = bytes[6] & 15 | 64;
|
|
676
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
677
|
+
const hex = [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
678
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
function bodyByteLength(body) {
|
|
682
|
+
try {
|
|
683
|
+
return new TextEncoder().encode(body).byteLength;
|
|
684
|
+
} catch {
|
|
685
|
+
return body.length;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
function failureDeduplicationKey(event) {
|
|
689
|
+
return JSON.stringify([
|
|
690
|
+
event.code,
|
|
691
|
+
event.stage,
|
|
692
|
+
event.provider,
|
|
693
|
+
event.attempt,
|
|
694
|
+
event.statusClass,
|
|
695
|
+
event.requestCategory,
|
|
696
|
+
event.paymentMethodCategory,
|
|
697
|
+
event.checkoutMode,
|
|
698
|
+
event.layout
|
|
699
|
+
]);
|
|
700
|
+
}
|
|
701
|
+
function telemetryNow() {
|
|
702
|
+
return globalThis.performance?.now() ?? 0;
|
|
703
|
+
}
|
|
704
|
+
var TelemetryReporter = class {
|
|
705
|
+
constructor(options) {
|
|
706
|
+
this.ingestionDisabled = false;
|
|
707
|
+
this.queue = [];
|
|
708
|
+
this.sequence = 0;
|
|
709
|
+
this.flushTimer = null;
|
|
710
|
+
this.flushInFlight = null;
|
|
711
|
+
this.reportedFailures = /* @__PURE__ */ new Map();
|
|
712
|
+
this.checkoutContext = {};
|
|
713
|
+
this.checkoutStartedAt = null;
|
|
714
|
+
this.destroyed = false;
|
|
715
|
+
this.pageExitHandler = () => {
|
|
716
|
+
this.drainQueue();
|
|
717
|
+
};
|
|
718
|
+
this.visibilityHandler = () => {
|
|
719
|
+
if (document.visibilityState === "hidden") void this.flush();
|
|
720
|
+
};
|
|
721
|
+
this.eventCounts = {
|
|
722
|
+
technical_error: 0,
|
|
723
|
+
lifecycle: 0,
|
|
724
|
+
expected_outcome: 0,
|
|
725
|
+
performance: 0
|
|
726
|
+
};
|
|
727
|
+
this.endpoint = `${options.billingApiUrl.replace(/\/+$/, "")}${TELEMETRY_PATH}`;
|
|
728
|
+
this.sdkPackage = options.sdkPackage ?? "@flopay/js";
|
|
729
|
+
this.sdkVersion = options.sdkVersion;
|
|
730
|
+
this.correlationId = createUuidV4();
|
|
731
|
+
this.merchantEnabled = options.enabled !== false;
|
|
732
|
+
this.clock = options.clock ?? telemetryNow;
|
|
733
|
+
this.browserTransportAvailable = typeof window !== "undefined" && typeof document !== "undefined";
|
|
734
|
+
if (this.browserTransportAvailable) {
|
|
735
|
+
window.addEventListener("pagehide", this.pageExitHandler);
|
|
736
|
+
document.addEventListener("visibilitychange", this.visibilityHandler);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
log(input) {
|
|
740
|
+
if (!this.canCollect()) return;
|
|
741
|
+
this.enqueue(buildTelemetryLogEvent({
|
|
742
|
+
...this.checkoutContext,
|
|
743
|
+
...input,
|
|
744
|
+
eventId: createUuidV4(),
|
|
745
|
+
sequence: this.sequence++
|
|
746
|
+
}));
|
|
747
|
+
}
|
|
748
|
+
error(input) {
|
|
749
|
+
if (!this.canCollect()) return;
|
|
750
|
+
const normalizedFailure = buildTelemetryErrorEvent({
|
|
751
|
+
...this.checkoutContext,
|
|
752
|
+
...input,
|
|
753
|
+
eventId: DEDUPLICATION_EVENT_ID,
|
|
754
|
+
sequence: 0
|
|
755
|
+
});
|
|
756
|
+
const deduplicationKey = failureDeduplicationKey(normalizedFailure);
|
|
757
|
+
const now = this.now();
|
|
758
|
+
this.pruneReportedFailures(now);
|
|
759
|
+
const previouslyReportedAt = this.reportedFailures.get(deduplicationKey);
|
|
760
|
+
if (previouslyReportedAt !== void 0 && now >= previouslyReportedAt && now - previouslyReportedAt < ERROR_DEDUPLICATION_WINDOW_MS) {
|
|
761
|
+
this.log({
|
|
762
|
+
name: "operation.deduplicated",
|
|
763
|
+
stage: input.stage,
|
|
764
|
+
provider: input.provider,
|
|
765
|
+
paymentMethodCategory: input.paymentMethodCategory,
|
|
766
|
+
attempt: input.attempt
|
|
767
|
+
});
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
this.rememberReportedFailure(deduplicationKey, now);
|
|
771
|
+
this.enqueue(buildTelemetryErrorEvent({
|
|
772
|
+
...this.checkoutContext,
|
|
773
|
+
...input,
|
|
774
|
+
eventId: createUuidV4(),
|
|
775
|
+
sequence: this.sequence++
|
|
776
|
+
}));
|
|
777
|
+
}
|
|
778
|
+
performance(input) {
|
|
779
|
+
if (!this.canCollect()) return;
|
|
780
|
+
this.enqueue(buildTelemetryPerformanceEvent({
|
|
781
|
+
...this.checkoutContext,
|
|
782
|
+
...input,
|
|
783
|
+
eventId: createUuidV4(),
|
|
784
|
+
sequence: this.sequence++
|
|
785
|
+
}));
|
|
786
|
+
}
|
|
787
|
+
terminal(input) {
|
|
788
|
+
if (!this.canCollect()) return;
|
|
789
|
+
this.enqueue(buildTelemetryTerminalEvent({
|
|
790
|
+
...this.checkoutContext,
|
|
791
|
+
...input,
|
|
792
|
+
eventId: createUuidV4(),
|
|
793
|
+
sequence: this.sequence++
|
|
794
|
+
}));
|
|
795
|
+
if (input.outcome !== "action_required" && this.checkoutStartedAt !== null) {
|
|
796
|
+
const checkoutStartedAt = this.checkoutStartedAt;
|
|
797
|
+
this.checkoutStartedAt = null;
|
|
798
|
+
this.performance({
|
|
799
|
+
stage: "total_journey",
|
|
800
|
+
durationMs: Math.max(0, this.now() - checkoutStartedAt),
|
|
801
|
+
durationMode: "total",
|
|
802
|
+
provider: input.provider,
|
|
803
|
+
paymentMethodCategory: input.paymentMethodCategory
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
/** @internal Read the reporter's monotonic clock without Resource Timing. */
|
|
808
|
+
now() {
|
|
809
|
+
try {
|
|
810
|
+
return this.clock();
|
|
811
|
+
} catch {
|
|
812
|
+
return telemetryNow();
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
pruneReportedFailures(now) {
|
|
816
|
+
for (const [key, reportedAt] of this.reportedFailures) {
|
|
817
|
+
if (now < reportedAt || now - reportedAt >= ERROR_DEDUPLICATION_WINDOW_MS) {
|
|
818
|
+
this.reportedFailures.delete(key);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
rememberReportedFailure(key, reportedAt) {
|
|
823
|
+
while (this.reportedFailures.size >= MAX_REPORTED_FAILURES) {
|
|
824
|
+
const oldest = this.reportedFailures.keys().next();
|
|
825
|
+
if (oldest.done) break;
|
|
826
|
+
this.reportedFailures.delete(oldest.value);
|
|
827
|
+
}
|
|
828
|
+
this.reportedFailures.set(key, reportedAt);
|
|
829
|
+
}
|
|
830
|
+
/** @internal Add closed checkout dimensions to subsequent SDK events. */
|
|
831
|
+
setCheckoutContext(context) {
|
|
832
|
+
this.checkoutContext = {
|
|
833
|
+
checkoutMode: context.checkoutMode,
|
|
834
|
+
layout: context.layout
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
/** @internal Start a fresh checkout budget, dedupe window, and total span. */
|
|
838
|
+
beginCheckout(context = {}) {
|
|
839
|
+
if (!this.canCollect()) return 0;
|
|
840
|
+
this.drainQueue();
|
|
841
|
+
this.setCheckoutContext(context);
|
|
842
|
+
this.sequence = 0;
|
|
843
|
+
this.reportedFailures.clear();
|
|
844
|
+
this.eventCounts = {
|
|
845
|
+
technical_error: 0,
|
|
846
|
+
lifecycle: 0,
|
|
847
|
+
expected_outcome: 0,
|
|
848
|
+
performance: 0
|
|
849
|
+
};
|
|
850
|
+
this.checkoutStartedAt = this.now();
|
|
851
|
+
return this.checkoutStartedAt;
|
|
852
|
+
}
|
|
853
|
+
enqueue(event) {
|
|
854
|
+
if (!this.canCollect()) return;
|
|
855
|
+
if (this.queue.length >= MAX_QUEUE_SIZE || this.eventCounts[event.class] >= EVENT_BUDGETS[event.class]) return;
|
|
856
|
+
this.eventCounts[event.class] += 1;
|
|
857
|
+
this.queue.push(event);
|
|
858
|
+
if (this.queue.length >= MAX_BATCH_SIZE) {
|
|
859
|
+
void this.flush();
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
this.scheduleFlush();
|
|
863
|
+
}
|
|
864
|
+
canCollect() {
|
|
865
|
+
return this.browserTransportAvailable && this.merchantEnabled && !this.ingestionDisabled && !this.destroyed;
|
|
866
|
+
}
|
|
867
|
+
/** Flush one bounded batch. Failures are intentionally dropped. */
|
|
868
|
+
async flush() {
|
|
869
|
+
if (this.flushInFlight) return this.flushInFlight;
|
|
870
|
+
if (!this.browserTransportAvailable || this.destroyed || this.ingestionDisabled || this.queue.length === 0) return;
|
|
871
|
+
this.clearFlushTimer();
|
|
872
|
+
const events = this.queue.splice(0, MAX_BATCH_SIZE);
|
|
873
|
+
this.flushInFlight = this.sendBatch(events).finally(() => {
|
|
874
|
+
this.flushInFlight = null;
|
|
875
|
+
if (this.queue.length > 0) this.scheduleFlush();
|
|
876
|
+
});
|
|
877
|
+
return this.flushInFlight;
|
|
878
|
+
}
|
|
879
|
+
/** Flush pending work and detach browser lifecycle listeners. */
|
|
880
|
+
destroy() {
|
|
881
|
+
if (this.destroyed) return;
|
|
882
|
+
this.drainQueue();
|
|
883
|
+
this.destroyed = true;
|
|
884
|
+
this.clearFlushTimer();
|
|
885
|
+
if (this.browserTransportAvailable) {
|
|
886
|
+
window.removeEventListener("pagehide", this.pageExitHandler);
|
|
887
|
+
document.removeEventListener("visibilitychange", this.visibilityHandler);
|
|
888
|
+
}
|
|
889
|
+
this.queue.splice(0);
|
|
890
|
+
this.reportedFailures.clear();
|
|
891
|
+
}
|
|
892
|
+
/** Permanently honor a merchant opt-out and discard queued events. */
|
|
893
|
+
disable() {
|
|
894
|
+
this.merchantEnabled = false;
|
|
895
|
+
this.queue.splice(0);
|
|
896
|
+
this.reportedFailures.clear();
|
|
897
|
+
this.clearFlushTimer();
|
|
898
|
+
}
|
|
899
|
+
/** Start every bounded keepalive request synchronously before page teardown. */
|
|
900
|
+
drainQueue() {
|
|
901
|
+
if (!this.browserTransportAvailable || this.destroyed || this.ingestionDisabled || this.queue.length === 0) return;
|
|
902
|
+
this.clearFlushTimer();
|
|
903
|
+
while (this.queue.length > 0) {
|
|
904
|
+
const events = this.queue.splice(0, MAX_BATCH_SIZE);
|
|
905
|
+
void this.sendBatch(events);
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
async sendBatch(events) {
|
|
909
|
+
if (!this.browserTransportAvailable || this.ingestionDisabled) return;
|
|
910
|
+
const body = serializeTelemetryBatch(events, {
|
|
911
|
+
correlationId: this.correlationId,
|
|
912
|
+
sdkPackage: this.sdkPackage,
|
|
913
|
+
sdkVersion: this.sdkVersion,
|
|
914
|
+
batchId: createUuidV4()
|
|
915
|
+
});
|
|
916
|
+
if (bodyByteLength(body) > TELEMETRY_MAX_BATCH_BYTES) return;
|
|
917
|
+
const controller = typeof AbortController === "undefined" ? null : new AbortController();
|
|
918
|
+
let timeout = null;
|
|
919
|
+
try {
|
|
920
|
+
const request = fetch(this.endpoint, {
|
|
921
|
+
method: "POST",
|
|
922
|
+
headers: { "content-type": "text/plain;charset=UTF-8" },
|
|
923
|
+
body,
|
|
924
|
+
credentials: "omit",
|
|
925
|
+
keepalive: true,
|
|
926
|
+
referrerPolicy: "no-referrer",
|
|
927
|
+
signal: controller?.signal
|
|
928
|
+
}).then(async (response) => {
|
|
929
|
+
if (response.status !== 202) return null;
|
|
930
|
+
const payload = await response.json().catch(() => null);
|
|
931
|
+
return payload?.status === "disabled" ? "disabled" : null;
|
|
932
|
+
}).catch(() => null);
|
|
933
|
+
const expired = new Promise((resolve) => {
|
|
934
|
+
timeout = setTimeout(() => {
|
|
935
|
+
controller?.abort();
|
|
936
|
+
resolve(null);
|
|
937
|
+
}, UPLOAD_TIMEOUT_MS);
|
|
938
|
+
});
|
|
939
|
+
const status = await Promise.race([request, expired]);
|
|
940
|
+
if (status === "disabled") this.disableFromIngestion();
|
|
941
|
+
} catch {
|
|
942
|
+
} finally {
|
|
943
|
+
if (timeout) clearTimeout(timeout);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
disableFromIngestion() {
|
|
947
|
+
this.ingestionDisabled = true;
|
|
948
|
+
this.queue.splice(0);
|
|
949
|
+
this.reportedFailures.clear();
|
|
950
|
+
this.clearFlushTimer();
|
|
951
|
+
}
|
|
952
|
+
scheduleFlush() {
|
|
953
|
+
if (this.flushTimer || this.destroyed || this.ingestionDisabled) return;
|
|
954
|
+
this.flushTimer = setTimeout(() => {
|
|
955
|
+
this.flushTimer = null;
|
|
956
|
+
void this.flush();
|
|
957
|
+
}, 0);
|
|
958
|
+
}
|
|
959
|
+
clearFlushTimer() {
|
|
960
|
+
if (!this.flushTimer) return;
|
|
961
|
+
clearTimeout(this.flushTimer);
|
|
962
|
+
this.flushTimer = null;
|
|
963
|
+
}
|
|
964
|
+
};
|
|
965
|
+
var TELEMETRY_REPORTER_FACTORY = /* @__PURE__ */ Symbol.for("@flopay/js.telemetry.reporter-factory.v1");
|
|
966
|
+
var telemetryGlobal = globalThis;
|
|
967
|
+
if (telemetryGlobal[TELEMETRY_REPORTER_FACTORY] === void 0) {
|
|
968
|
+
Object.defineProperty(telemetryGlobal, TELEMETRY_REPORTER_FACTORY, {
|
|
969
|
+
configurable: true,
|
|
970
|
+
enumerable: false,
|
|
971
|
+
writable: false,
|
|
972
|
+
value: (options) => new TelemetryReporter(options)
|
|
973
|
+
});
|
|
974
|
+
}
|
|
975
|
+
|
|
594
976
|
// src/payment-api.ts
|
|
595
977
|
var DEFAULT_PROCESSING_RETRY_AFTER_MS = 1e3;
|
|
596
978
|
var MIN_PROCESSING_RETRY_AFTER_MS = 500;
|
|
@@ -600,18 +982,30 @@ var DEFAULT_ACCOUNT_SNAPSHOT_TIMEOUT_MS = 1e4;
|
|
|
600
982
|
function isRecord(value) {
|
|
601
983
|
return typeof value === "object" && value !== null;
|
|
602
984
|
}
|
|
985
|
+
function telemetryStatusClass(status) {
|
|
986
|
+
if (status === void 0) return "network_error";
|
|
987
|
+
const statusClass = `${Math.floor(status / 100)}xx`;
|
|
988
|
+
return statusClass === "2xx" || statusClass === "3xx" || statusClass === "4xx" || statusClass === "5xx" ? statusClass : "unknown";
|
|
989
|
+
}
|
|
990
|
+
function telemetryFailure(error, fallbackCode) {
|
|
991
|
+
if (error instanceof Error && (error.name === "AbortError" || error instanceof FloPayError3 && error.code === "checkout_processing_timeout")) {
|
|
992
|
+
return { errorCode: "REQUEST_TIMEOUT", statusClass: "timeout" };
|
|
993
|
+
}
|
|
994
|
+
if (error instanceof TypeError) {
|
|
995
|
+
return { errorCode: "NETWORK_REQUEST_FAILED", statusClass: "network_error" };
|
|
996
|
+
}
|
|
997
|
+
return {
|
|
998
|
+
errorCode: fallbackCode,
|
|
999
|
+
statusClass: telemetryStatusClass(
|
|
1000
|
+
error instanceof FloPayError3 ? error.statusCode : void 0
|
|
1001
|
+
)
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
603
1004
|
function readString(payload, key) {
|
|
604
|
-
|
|
605
|
-
return typeof value === "string" && value.trim() ? value : void 0;
|
|
1005
|
+
return readErrorString(payload?.[key]);
|
|
606
1006
|
}
|
|
607
1007
|
function readMessage(payload, key) {
|
|
608
|
-
|
|
609
|
-
if (typeof value === "string") return value.trim() ? value : void 0;
|
|
610
|
-
if (Array.isArray(value)) {
|
|
611
|
-
const joined = value.filter((entry) => typeof entry === "string" && entry.trim().length > 0).join("; ");
|
|
612
|
-
return joined || void 0;
|
|
613
|
-
}
|
|
614
|
-
return void 0;
|
|
1008
|
+
return readErrorMessage(payload?.[key]);
|
|
615
1009
|
}
|
|
616
1010
|
function readNumber(payload, key) {
|
|
617
1011
|
const value = payload?.[key];
|
|
@@ -639,7 +1033,7 @@ async function buildApiErrorFromResponse(response, fallbackMessage) {
|
|
|
639
1033
|
}
|
|
640
1034
|
var NETWORK_RETRY_ATTEMPTS = 2;
|
|
641
1035
|
var IDEMPOTENCY_IN_PROGRESS_RETRY_ATTEMPTS = 2;
|
|
642
|
-
async function fetchWithNetworkRetry(input, init, attempts = NETWORK_RETRY_ATTEMPTS) {
|
|
1036
|
+
async function fetchWithNetworkRetry(input, init, attempts = NETWORK_RETRY_ATTEMPTS, onRetry) {
|
|
643
1037
|
let lastErr;
|
|
644
1038
|
for (let attempt = 0; ; attempt++) {
|
|
645
1039
|
try {
|
|
@@ -648,13 +1042,59 @@ async function fetchWithNetworkRetry(input, init, attempts = NETWORK_RETRY_ATTEM
|
|
|
648
1042
|
if (err instanceof Error && err.name === "AbortError") throw err;
|
|
649
1043
|
lastErr = err;
|
|
650
1044
|
if (attempt >= attempts) throw lastErr;
|
|
1045
|
+
try {
|
|
1046
|
+
onRetry?.(attempt + 1);
|
|
1047
|
+
} catch {
|
|
1048
|
+
}
|
|
651
1049
|
await delay(150 * 2 ** attempt);
|
|
652
1050
|
}
|
|
653
1051
|
}
|
|
654
1052
|
}
|
|
1053
|
+
function isPaymentApiTelemetryHooks(value) {
|
|
1054
|
+
return "now" in value || "onFirstByte" in value || "onSessionCreateFailure" in value || "onRetry" in value;
|
|
1055
|
+
}
|
|
655
1056
|
var PaymentAPI = class {
|
|
656
|
-
constructor(billingApiUrl) {
|
|
1057
|
+
constructor(billingApiUrl, telemetryOptionsOrHooks = {}) {
|
|
657
1058
|
this.baseUrl = billingApiUrl.replace(/\/+$/, "");
|
|
1059
|
+
const hasInternalHooks = isPaymentApiTelemetryHooks(telemetryOptionsOrHooks);
|
|
1060
|
+
this.telemetryHooks = hasInternalHooks ? telemetryOptionsOrHooks : void 0;
|
|
1061
|
+
this.directTelemetry = hasInternalHooks || telemetryOptionsOrHooks.telemetry === false ? void 0 : new TelemetryReporter({
|
|
1062
|
+
billingApiUrl: this.baseUrl,
|
|
1063
|
+
sdkVersion: SDK_VERSION
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
/** Dispose the reporter owned by direct public usage. Internal hooks are never disposed here. */
|
|
1067
|
+
destroy() {
|
|
1068
|
+
this.directTelemetry?.destroy();
|
|
1069
|
+
}
|
|
1070
|
+
reportDirectFailure(error, fallbackCode, stage, requestCategory, paymentMethodCategory = "unknown") {
|
|
1071
|
+
const failure = telemetryFailure(error, fallbackCode);
|
|
1072
|
+
this.directTelemetry?.error({
|
|
1073
|
+
...failure,
|
|
1074
|
+
stage,
|
|
1075
|
+
requestCategory,
|
|
1076
|
+
paymentMethodCategory
|
|
1077
|
+
});
|
|
1078
|
+
}
|
|
1079
|
+
telemetryTimestamp() {
|
|
1080
|
+
try {
|
|
1081
|
+
return this.telemetryHooks?.now?.() ?? this.directTelemetry?.now() ?? telemetryNow();
|
|
1082
|
+
} catch {
|
|
1083
|
+
return telemetryNow();
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
beginDirectTelemetryCheckout(checkoutSessionId) {
|
|
1087
|
+
if (!this.directTelemetry || this.directTelemetryCheckoutId === checkoutSessionId) return;
|
|
1088
|
+
this.directTelemetryCheckoutId = checkoutSessionId;
|
|
1089
|
+
this.directTelemetry.beginCheckout();
|
|
1090
|
+
}
|
|
1091
|
+
beginDirectTelemetryOperation() {
|
|
1092
|
+
if (!this.directTelemetry) return;
|
|
1093
|
+
this.directTelemetryCheckoutId = void 0;
|
|
1094
|
+
this.directTelemetry.beginCheckout();
|
|
1095
|
+
}
|
|
1096
|
+
adoptDirectTelemetryCheckout(checkoutSessionId) {
|
|
1097
|
+
if (checkoutSessionId) this.directTelemetryCheckoutId = checkoutSessionId;
|
|
658
1098
|
}
|
|
659
1099
|
/**
|
|
660
1100
|
* Fetch a raw checkout session by ID.
|
|
@@ -666,17 +1106,77 @@ var PaymentAPI = class {
|
|
|
666
1106
|
* Backends that don't yet enforce it ignore the extra header.
|
|
667
1107
|
*/
|
|
668
1108
|
async getCheckoutSession(checkoutSessionId, nonce) {
|
|
1109
|
+
this.beginDirectTelemetryCheckout(checkoutSessionId);
|
|
1110
|
+
const requestStarted = this.telemetryTimestamp();
|
|
1111
|
+
this.directTelemetry?.log({
|
|
1112
|
+
name: "session.read.started",
|
|
1113
|
+
stage: "session_read",
|
|
1114
|
+
requestCategory: "session_read"
|
|
1115
|
+
});
|
|
669
1116
|
const headers = { [FLO_SDK_VERSION_HEADER]: SDK_VERSION };
|
|
670
1117
|
if (nonce) headers["x-checkout-session-token"] = nonce;
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
1118
|
+
try {
|
|
1119
|
+
const response = await fetchWithNetworkRetry(
|
|
1120
|
+
`${this.baseUrl}/v1/checkouts/sessions/${encodeURIComponent(checkoutSessionId)}`,
|
|
1121
|
+
{ headers },
|
|
1122
|
+
NETWORK_RETRY_ATTEMPTS,
|
|
1123
|
+
(attempt) => {
|
|
1124
|
+
this.telemetryHooks?.onRetry?.("session_read", attempt);
|
|
1125
|
+
this.directTelemetry?.log({
|
|
1126
|
+
name: "operation.retry",
|
|
1127
|
+
stage: "session_read",
|
|
1128
|
+
requestCategory: "session_read",
|
|
1129
|
+
attempt
|
|
1130
|
+
});
|
|
1131
|
+
}
|
|
1132
|
+
);
|
|
1133
|
+
const firstByteDuration = Math.max(0, this.telemetryTimestamp() - requestStarted);
|
|
1134
|
+
try {
|
|
1135
|
+
this.telemetryHooks?.onFirstByte?.(firstByteDuration);
|
|
1136
|
+
} catch {
|
|
1137
|
+
}
|
|
1138
|
+
const statusClass = `${Math.floor(response.status / 100)}xx`;
|
|
1139
|
+
this.directTelemetry?.log({
|
|
1140
|
+
name: "session.request.first_byte",
|
|
1141
|
+
stage: "session_first_byte",
|
|
1142
|
+
requestCategory: "session_read",
|
|
1143
|
+
statusClass
|
|
1144
|
+
});
|
|
1145
|
+
this.directTelemetry?.performance({
|
|
1146
|
+
stage: "session_first_byte",
|
|
1147
|
+
durationMs: firstByteDuration,
|
|
1148
|
+
durationMode: "machine",
|
|
1149
|
+
requestCategory: "session_read",
|
|
1150
|
+
statusClass
|
|
1151
|
+
});
|
|
1152
|
+
if (!response.ok) {
|
|
1153
|
+
throw await buildApiErrorFromResponse(response, "Failed to get checkout session");
|
|
1154
|
+
}
|
|
1155
|
+
const body = await response.json();
|
|
1156
|
+
this.directTelemetry?.log({
|
|
1157
|
+
name: "session.request.completed",
|
|
1158
|
+
stage: "session_complete",
|
|
1159
|
+
requestCategory: "session_read",
|
|
1160
|
+
statusClass
|
|
1161
|
+
});
|
|
1162
|
+
this.directTelemetry?.performance({
|
|
1163
|
+
stage: "session_complete",
|
|
1164
|
+
durationMs: Math.max(0, this.telemetryTimestamp() - requestStarted),
|
|
1165
|
+
durationMode: "machine",
|
|
1166
|
+
requestCategory: "session_read",
|
|
1167
|
+
statusClass
|
|
1168
|
+
});
|
|
1169
|
+
return { ...body, data: this.mergeCachedDisplayData(body.data) };
|
|
1170
|
+
} catch (error) {
|
|
1171
|
+
const statusCode = error instanceof FloPayError3 ? error.statusCode : void 0;
|
|
1172
|
+
this.directTelemetry?.error({
|
|
1173
|
+
errorCode: error instanceof FloPayError3 && error.code === "checkout_processing_timeout" ? "REQUEST_TIMEOUT" : "NETWORK_REQUEST_FAILED",
|
|
1174
|
+
stage: "session_read",
|
|
1175
|
+
requestCategory: "session_read",
|
|
1176
|
+
statusClass: statusCode ? `${Math.floor(statusCode / 100)}xx` : "network_error"
|
|
1177
|
+
});
|
|
1178
|
+
throw error;
|
|
677
1179
|
}
|
|
678
|
-
const body = await response.json();
|
|
679
|
-
return { ...body, data: this.mergeCachedDisplayData(body.data) };
|
|
680
1180
|
}
|
|
681
1181
|
/**
|
|
682
1182
|
* Stash display-only data for a session so subsequent fetches can fill in
|
|
@@ -732,17 +1232,55 @@ var PaymentAPI = class {
|
|
|
732
1232
|
* backends, matched against the session's stored nonce).
|
|
733
1233
|
*/
|
|
734
1234
|
async getVaultCapture(checkoutSessionId, nonce) {
|
|
1235
|
+
this.beginDirectTelemetryCheckout(checkoutSessionId);
|
|
1236
|
+
const startedAt = this.telemetryTimestamp();
|
|
1237
|
+
this.directTelemetry?.log({
|
|
1238
|
+
name: "vault.capture.requested",
|
|
1239
|
+
stage: "vault_request",
|
|
1240
|
+
requestCategory: "vault_capture",
|
|
1241
|
+
paymentMethodCategory: "card"
|
|
1242
|
+
});
|
|
735
1243
|
const headers = { "Content-Type": "application/json" };
|
|
736
1244
|
if (nonce) headers["x-checkout-session-token"] = nonce;
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
1245
|
+
try {
|
|
1246
|
+
const response = await fetchWithNetworkRetry(
|
|
1247
|
+
`${this.baseUrl}/v1/checkouts/sessions/${encodeURIComponent(checkoutSessionId)}/vault/capture`,
|
|
1248
|
+
{ method: "POST", headers },
|
|
1249
|
+
NETWORK_RETRY_ATTEMPTS,
|
|
1250
|
+
(attempt) => {
|
|
1251
|
+
this.telemetryHooks?.onRetry?.("vault_capture", attempt);
|
|
1252
|
+
this.directTelemetry?.log({
|
|
1253
|
+
name: "operation.retry",
|
|
1254
|
+
stage: "vault_request",
|
|
1255
|
+
requestCategory: "vault_capture",
|
|
1256
|
+
paymentMethodCategory: "card",
|
|
1257
|
+
attempt
|
|
1258
|
+
});
|
|
1259
|
+
}
|
|
1260
|
+
);
|
|
1261
|
+
if (!response.ok) {
|
|
1262
|
+
throw await buildApiErrorFromResponse(response, "Failed to load the secure card form");
|
|
1263
|
+
}
|
|
1264
|
+
const block = await response.json();
|
|
1265
|
+
this.directTelemetry?.performance({
|
|
1266
|
+
stage: "vault_request",
|
|
1267
|
+
durationMs: Math.max(0, this.telemetryTimestamp() - startedAt),
|
|
1268
|
+
durationMode: "machine",
|
|
1269
|
+
requestCategory: "vault_capture",
|
|
1270
|
+
paymentMethodCategory: "card",
|
|
1271
|
+
statusClass: "2xx"
|
|
1272
|
+
});
|
|
1273
|
+
return this.toVaultBlock(block);
|
|
1274
|
+
} catch (error) {
|
|
1275
|
+
this.reportDirectFailure(
|
|
1276
|
+
error,
|
|
1277
|
+
"VAULT_LOAD_FAILED",
|
|
1278
|
+
"vault_request",
|
|
1279
|
+
"vault_capture",
|
|
1280
|
+
"card"
|
|
1281
|
+
);
|
|
1282
|
+
throw error;
|
|
743
1283
|
}
|
|
744
|
-
const block = await response.json();
|
|
745
|
-
return this.toVaultBlock(block);
|
|
746
1284
|
}
|
|
747
1285
|
/**
|
|
748
1286
|
* Fetch and normalize a checkout session.
|
|
@@ -778,26 +1316,88 @@ var PaymentAPI = class {
|
|
|
778
1316
|
* future major version.
|
|
779
1317
|
*/
|
|
780
1318
|
async processPayment(_userId, data, options) {
|
|
1319
|
+
this.beginDirectTelemetryCheckout(data.sessionId);
|
|
781
1320
|
if (!data.nonce) {
|
|
1321
|
+
this.directTelemetry?.terminal({
|
|
1322
|
+
outcome: "validation_rejected",
|
|
1323
|
+
stage: "processing",
|
|
1324
|
+
requestCategory: "process_payment"
|
|
1325
|
+
});
|
|
782
1326
|
throw new FloPayError3(
|
|
783
1327
|
"processPayment requires `nonce` \u2014 pass the value returned from session creation.",
|
|
784
1328
|
"validation_error",
|
|
785
1329
|
{ code: "MissingCheckoutSessionToken", param: "nonce" }
|
|
786
1330
|
);
|
|
787
1331
|
}
|
|
1332
|
+
const startedAt = this.telemetryTimestamp();
|
|
1333
|
+
this.directTelemetry?.log({
|
|
1334
|
+
name: "payment.processing.started",
|
|
1335
|
+
stage: "processing",
|
|
1336
|
+
requestCategory: "process_payment"
|
|
1337
|
+
});
|
|
788
1338
|
const { nonce, ...processBody } = data;
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
1339
|
+
let response;
|
|
1340
|
+
try {
|
|
1341
|
+
response = await fetch(
|
|
1342
|
+
`${this.baseUrl}/v1/checkouts/sessions/${encodeURIComponent(data.sessionId)}/process`,
|
|
1343
|
+
{
|
|
1344
|
+
method: "POST",
|
|
1345
|
+
headers: {
|
|
1346
|
+
"Content-Type": "application/json",
|
|
1347
|
+
"x-checkout-session-token": nonce
|
|
1348
|
+
},
|
|
1349
|
+
body: JSON.stringify(processBody)
|
|
1350
|
+
}
|
|
1351
|
+
);
|
|
1352
|
+
} catch (error) {
|
|
1353
|
+
this.reportDirectFailure(
|
|
1354
|
+
error,
|
|
1355
|
+
"PAYMENT_PROCESSING_FAILED",
|
|
1356
|
+
"processing",
|
|
1357
|
+
"process_payment"
|
|
1358
|
+
);
|
|
1359
|
+
throw error;
|
|
1360
|
+
}
|
|
1361
|
+
if (!response.ok && response.status !== 202) {
|
|
1362
|
+
this.directTelemetry?.error({
|
|
1363
|
+
errorCode: "PAYMENT_PROCESSING_FAILED",
|
|
1364
|
+
stage: "processing",
|
|
1365
|
+
requestCategory: "process_payment",
|
|
1366
|
+
statusClass: telemetryStatusClass(response.status)
|
|
1367
|
+
});
|
|
1368
|
+
return response;
|
|
1369
|
+
}
|
|
1370
|
+
try {
|
|
1371
|
+
const result = await this.resolveProcessResponse(
|
|
1372
|
+
response,
|
|
1373
|
+
data.sessionId,
|
|
1374
|
+
{ ...options, nonce }
|
|
1375
|
+
);
|
|
1376
|
+
this.directTelemetry?.log({
|
|
1377
|
+
name: "payment.processing.completed",
|
|
1378
|
+
stage: "processing",
|
|
1379
|
+
requestCategory: "process_payment",
|
|
1380
|
+
statusClass: telemetryStatusClass(result.status)
|
|
1381
|
+
});
|
|
1382
|
+
this.directTelemetry?.performance({
|
|
1383
|
+
stage: "processing",
|
|
1384
|
+
durationMs: Math.max(0, this.telemetryTimestamp() - startedAt),
|
|
1385
|
+
durationMode: "machine",
|
|
1386
|
+
requestCategory: "process_payment",
|
|
1387
|
+
statusClass: telemetryStatusClass(result.status)
|
|
1388
|
+
});
|
|
1389
|
+
return result;
|
|
1390
|
+
} catch (error) {
|
|
1391
|
+
if (!(error instanceof FloPayError3 && error.code === "checkout_processing_timeout")) {
|
|
1392
|
+
this.reportDirectFailure(
|
|
1393
|
+
error,
|
|
1394
|
+
"PAYMENT_PROCESSING_FAILED",
|
|
1395
|
+
"processing",
|
|
1396
|
+
"process_payment"
|
|
1397
|
+
);
|
|
798
1398
|
}
|
|
799
|
-
|
|
800
|
-
|
|
1399
|
+
throw error;
|
|
1400
|
+
}
|
|
801
1401
|
}
|
|
802
1402
|
/**
|
|
803
1403
|
* Patch the buyer's account snapshot (email, name, billing address, AVS
|
|
@@ -821,6 +1421,13 @@ var PaymentAPI = class {
|
|
|
821
1421
|
* AVS-protected charge to decline downstream.
|
|
822
1422
|
*/
|
|
823
1423
|
async patchAccountSnapshot(sessionId, nonce, body, options) {
|
|
1424
|
+
this.beginDirectTelemetryCheckout(sessionId);
|
|
1425
|
+
const startedAt = this.telemetryTimestamp();
|
|
1426
|
+
this.directTelemetry?.log({
|
|
1427
|
+
name: "operation.state_transition",
|
|
1428
|
+
stage: "processing",
|
|
1429
|
+
requestCategory: "account_snapshot"
|
|
1430
|
+
});
|
|
824
1431
|
const timeoutMs = options?.timeoutMs ?? DEFAULT_ACCOUNT_SNAPSHOT_TIMEOUT_MS;
|
|
825
1432
|
const controller = new AbortController();
|
|
826
1433
|
const onCallerAbort = () => controller.abort();
|
|
@@ -829,26 +1436,53 @@ var PaymentAPI = class {
|
|
|
829
1436
|
else options.signal.addEventListener("abort", onCallerAbort, { once: true });
|
|
830
1437
|
}
|
|
831
1438
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
832
|
-
let response;
|
|
833
1439
|
try {
|
|
834
|
-
response
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
1440
|
+
let response;
|
|
1441
|
+
try {
|
|
1442
|
+
response = await fetchWithNetworkRetry(
|
|
1443
|
+
`${this.baseUrl}/v1/checkouts/sessions/${encodeURIComponent(sessionId)}/account`,
|
|
1444
|
+
{
|
|
1445
|
+
method: "PATCH",
|
|
1446
|
+
headers: {
|
|
1447
|
+
"Content-Type": "application/json",
|
|
1448
|
+
"x-checkout-session-token": nonce
|
|
1449
|
+
},
|
|
1450
|
+
body: JSON.stringify(body),
|
|
1451
|
+
signal: controller.signal
|
|
841
1452
|
},
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
1453
|
+
NETWORK_RETRY_ATTEMPTS,
|
|
1454
|
+
(attempt) => {
|
|
1455
|
+
this.telemetryHooks?.onRetry?.("account_snapshot", attempt);
|
|
1456
|
+
this.directTelemetry?.log({
|
|
1457
|
+
name: "operation.retry",
|
|
1458
|
+
stage: "processing",
|
|
1459
|
+
requestCategory: "account_snapshot",
|
|
1460
|
+
attempt
|
|
1461
|
+
});
|
|
1462
|
+
}
|
|
1463
|
+
);
|
|
1464
|
+
} finally {
|
|
1465
|
+
clearTimeout(timer);
|
|
1466
|
+
options?.signal?.removeEventListener("abort", onCallerAbort);
|
|
1467
|
+
}
|
|
1468
|
+
if (!response.ok) {
|
|
1469
|
+
throw await buildApiErrorFromResponse(response, "Failed to persist account snapshot");
|
|
1470
|
+
}
|
|
1471
|
+
this.directTelemetry?.performance({
|
|
1472
|
+
stage: "processing",
|
|
1473
|
+
durationMs: Math.max(0, this.telemetryTimestamp() - startedAt),
|
|
1474
|
+
durationMode: "machine",
|
|
1475
|
+
requestCategory: "account_snapshot",
|
|
1476
|
+
statusClass: "2xx"
|
|
1477
|
+
});
|
|
1478
|
+
} catch (error) {
|
|
1479
|
+
this.reportDirectFailure(
|
|
1480
|
+
error,
|
|
1481
|
+
"NETWORK_REQUEST_FAILED",
|
|
1482
|
+
"processing",
|
|
1483
|
+
"account_snapshot"
|
|
845
1484
|
);
|
|
846
|
-
|
|
847
|
-
clearTimeout(timer);
|
|
848
|
-
options?.signal?.removeEventListener("abort", onCallerAbort);
|
|
849
|
-
}
|
|
850
|
-
if (!response.ok) {
|
|
851
|
-
throw await buildApiErrorFromResponse(response, "Failed to persist account snapshot");
|
|
1485
|
+
throw error;
|
|
852
1486
|
}
|
|
853
1487
|
}
|
|
854
1488
|
/**
|
|
@@ -870,22 +1504,62 @@ var PaymentAPI = class {
|
|
|
870
1504
|
* passing the concrete payment method id / type.
|
|
871
1505
|
*/
|
|
872
1506
|
async createPaymentIntent(sessionId, email, paymentMethodType, options) {
|
|
1507
|
+
this.beginDirectTelemetryCheckout(sessionId);
|
|
1508
|
+
const startedAt = this.telemetryTimestamp();
|
|
1509
|
+
this.directTelemetry?.log({
|
|
1510
|
+
name: "payment.intent.started",
|
|
1511
|
+
stage: "processing",
|
|
1512
|
+
requestCategory: "intent_create"
|
|
1513
|
+
});
|
|
873
1514
|
const headers = { "Content-Type": "application/json" };
|
|
874
1515
|
if (options?.nonce) headers["x-checkout-session-token"] = options.nonce;
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
1516
|
+
try {
|
|
1517
|
+
const response = await fetch(
|
|
1518
|
+
`${this.baseUrl}/v1/checkouts/payments/intents`,
|
|
1519
|
+
{
|
|
1520
|
+
method: "POST",
|
|
1521
|
+
headers,
|
|
1522
|
+
body: JSON.stringify({
|
|
1523
|
+
sessionId,
|
|
1524
|
+
email,
|
|
1525
|
+
paymentMethodType: paymentMethodType ?? null,
|
|
1526
|
+
isPaypal: options?.isPaypal ?? false
|
|
1527
|
+
}),
|
|
1528
|
+
signal: options?.signal
|
|
1529
|
+
}
|
|
1530
|
+
);
|
|
1531
|
+
if (!response.ok) {
|
|
1532
|
+
this.directTelemetry?.error({
|
|
1533
|
+
errorCode: "PAYMENT_PROCESSING_FAILED",
|
|
1534
|
+
stage: "processing",
|
|
1535
|
+
requestCategory: "intent_create",
|
|
1536
|
+
statusClass: telemetryStatusClass(response.status)
|
|
1537
|
+
});
|
|
1538
|
+
return response;
|
|
887
1539
|
}
|
|
888
|
-
|
|
1540
|
+
this.directTelemetry?.log({
|
|
1541
|
+
name: "payment.intent.completed",
|
|
1542
|
+
stage: "processing",
|
|
1543
|
+
requestCategory: "intent_create",
|
|
1544
|
+
statusClass: telemetryStatusClass(response.status)
|
|
1545
|
+
});
|
|
1546
|
+
this.directTelemetry?.performance({
|
|
1547
|
+
stage: "processing",
|
|
1548
|
+
durationMs: Math.max(0, this.telemetryTimestamp() - startedAt),
|
|
1549
|
+
durationMode: "machine",
|
|
1550
|
+
requestCategory: "intent_create",
|
|
1551
|
+
statusClass: telemetryStatusClass(response.status)
|
|
1552
|
+
});
|
|
1553
|
+
return response;
|
|
1554
|
+
} catch (error) {
|
|
1555
|
+
this.reportDirectFailure(
|
|
1556
|
+
error,
|
|
1557
|
+
"PAYMENT_PROCESSING_FAILED",
|
|
1558
|
+
"processing",
|
|
1559
|
+
"intent_create"
|
|
1560
|
+
);
|
|
1561
|
+
throw error;
|
|
1562
|
+
}
|
|
889
1563
|
}
|
|
890
1564
|
/**
|
|
891
1565
|
* Create a SetupIntent for saving payment methods.
|
|
@@ -894,23 +1568,71 @@ var PaymentAPI = class {
|
|
|
894
1568
|
* required by post-#640 backends, ignored by earlier versions.
|
|
895
1569
|
*/
|
|
896
1570
|
async createSetupIntent(sessionId, email, paymentMethodType, options) {
|
|
1571
|
+
this.beginDirectTelemetryCheckout(sessionId);
|
|
1572
|
+
const startedAt = this.telemetryTimestamp();
|
|
1573
|
+
this.directTelemetry?.log({
|
|
1574
|
+
name: "payment.intent.started",
|
|
1575
|
+
stage: "processing",
|
|
1576
|
+
requestCategory: "intent_create"
|
|
1577
|
+
});
|
|
897
1578
|
const headers = { "Content-Type": "application/json" };
|
|
898
1579
|
if (options?.nonce) headers["x-checkout-session-token"] = options.nonce;
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
1580
|
+
try {
|
|
1581
|
+
const response = await fetch(
|
|
1582
|
+
`${this.baseUrl}/v1/checkouts/payments/setup-intents`,
|
|
1583
|
+
{
|
|
1584
|
+
method: "POST",
|
|
1585
|
+
headers,
|
|
1586
|
+
body: JSON.stringify({ sessionId, email, paymentMethodType }),
|
|
1587
|
+
signal: options?.signal
|
|
1588
|
+
}
|
|
1589
|
+
);
|
|
1590
|
+
if (!response.ok) {
|
|
1591
|
+
this.directTelemetry?.error({
|
|
1592
|
+
errorCode: "PAYMENT_PROCESSING_FAILED",
|
|
1593
|
+
stage: "processing",
|
|
1594
|
+
requestCategory: "intent_create",
|
|
1595
|
+
statusClass: telemetryStatusClass(response.status)
|
|
1596
|
+
});
|
|
1597
|
+
return response;
|
|
906
1598
|
}
|
|
907
|
-
|
|
1599
|
+
this.directTelemetry?.log({
|
|
1600
|
+
name: "payment.intent.completed",
|
|
1601
|
+
stage: "processing",
|
|
1602
|
+
requestCategory: "intent_create",
|
|
1603
|
+
statusClass: telemetryStatusClass(response.status)
|
|
1604
|
+
});
|
|
1605
|
+
this.directTelemetry?.performance({
|
|
1606
|
+
stage: "processing",
|
|
1607
|
+
durationMs: Math.max(0, this.telemetryTimestamp() - startedAt),
|
|
1608
|
+
durationMode: "machine",
|
|
1609
|
+
requestCategory: "intent_create",
|
|
1610
|
+
statusClass: telemetryStatusClass(response.status)
|
|
1611
|
+
});
|
|
1612
|
+
return response;
|
|
1613
|
+
} catch (error) {
|
|
1614
|
+
this.reportDirectFailure(
|
|
1615
|
+
error,
|
|
1616
|
+
"PAYMENT_PROCESSING_FAILED",
|
|
1617
|
+
"processing",
|
|
1618
|
+
"intent_create"
|
|
1619
|
+
);
|
|
1620
|
+
throw error;
|
|
1621
|
+
}
|
|
908
1622
|
}
|
|
909
1623
|
/**
|
|
910
1624
|
* Fetch user's prior payments by email.
|
|
911
1625
|
* Used to determine if saved card UX should be shown.
|
|
912
1626
|
*/
|
|
913
1627
|
async getPaymentsByEmail(email, options) {
|
|
1628
|
+
this.beginDirectTelemetryOperation();
|
|
1629
|
+
const startedAt = this.telemetryTimestamp();
|
|
1630
|
+
this.directTelemetry?.log({
|
|
1631
|
+
name: "operation.recovery.started",
|
|
1632
|
+
stage: "recovery",
|
|
1633
|
+
requestCategory: "other",
|
|
1634
|
+
paymentMethodCategory: "saved"
|
|
1635
|
+
});
|
|
914
1636
|
const page = options?.page ?? 1;
|
|
915
1637
|
const limit = options?.limit ?? 1;
|
|
916
1638
|
const params = new URLSearchParams({
|
|
@@ -920,18 +1642,43 @@ var PaymentAPI = class {
|
|
|
920
1642
|
sortField: "createdAt",
|
|
921
1643
|
sortDirection: "DESC"
|
|
922
1644
|
});
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
1645
|
+
try {
|
|
1646
|
+
const response = await fetch(
|
|
1647
|
+
`${this.baseUrl}/v1/payments?${params.toString()}`,
|
|
1648
|
+
{
|
|
1649
|
+
method: "GET",
|
|
1650
|
+
signal: options?.signal,
|
|
1651
|
+
keepalive: true
|
|
1652
|
+
}
|
|
1653
|
+
);
|
|
1654
|
+
if (!response.ok) {
|
|
1655
|
+
throw new FloPayError3(
|
|
1656
|
+
"Failed to fetch payments",
|
|
1657
|
+
"api_error",
|
|
1658
|
+
{ statusCode: response.status }
|
|
1659
|
+
);
|
|
929
1660
|
}
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
1661
|
+
const result = await response.json();
|
|
1662
|
+
this.directTelemetry?.log({
|
|
1663
|
+
name: "operation.recovery.completed",
|
|
1664
|
+
stage: "recovery",
|
|
1665
|
+
requestCategory: "other",
|
|
1666
|
+
paymentMethodCategory: "saved",
|
|
1667
|
+
statusClass: "2xx"
|
|
1668
|
+
});
|
|
1669
|
+
this.directTelemetry?.performance({
|
|
1670
|
+
stage: "recovery",
|
|
1671
|
+
durationMs: Math.max(0, this.telemetryTimestamp() - startedAt),
|
|
1672
|
+
durationMode: "machine",
|
|
1673
|
+
requestCategory: "other",
|
|
1674
|
+
paymentMethodCategory: "saved",
|
|
1675
|
+
statusClass: "2xx"
|
|
1676
|
+
});
|
|
1677
|
+
return result;
|
|
1678
|
+
} catch (error) {
|
|
1679
|
+
this.reportDirectFailure(error, "RECOVERY_FAILED", "recovery", "other", "saved");
|
|
1680
|
+
throw error;
|
|
933
1681
|
}
|
|
934
|
-
return response.json();
|
|
935
1682
|
}
|
|
936
1683
|
/**
|
|
937
1684
|
* Create a checkout session AND return the full session data in one call.
|
|
@@ -941,6 +1688,56 @@ var PaymentAPI = class {
|
|
|
941
1688
|
* Falls back to create + GET if the backend doesn't support `expand`.
|
|
942
1689
|
*/
|
|
943
1690
|
async createAndFetchSession(params) {
|
|
1691
|
+
this.beginDirectTelemetryOperation();
|
|
1692
|
+
const startedAt = this.telemetryTimestamp();
|
|
1693
|
+
this.directTelemetry?.log({
|
|
1694
|
+
name: "session.create.started",
|
|
1695
|
+
stage: "session_create",
|
|
1696
|
+
requestCategory: "session_create"
|
|
1697
|
+
});
|
|
1698
|
+
try {
|
|
1699
|
+
const result = await this.createAndFetchSessionRequest(params, startedAt);
|
|
1700
|
+
this.adoptDirectTelemetryCheckout(result.data.session?.id);
|
|
1701
|
+
this.directTelemetry?.log({
|
|
1702
|
+
name: "session.request.completed",
|
|
1703
|
+
stage: "session_complete",
|
|
1704
|
+
requestCategory: "session_create",
|
|
1705
|
+
statusClass: "2xx"
|
|
1706
|
+
});
|
|
1707
|
+
this.directTelemetry?.performance({
|
|
1708
|
+
stage: "session_create",
|
|
1709
|
+
durationMs: this.telemetryTimestamp() - startedAt,
|
|
1710
|
+
durationMode: "machine",
|
|
1711
|
+
requestCategory: "session_create",
|
|
1712
|
+
statusClass: "2xx"
|
|
1713
|
+
});
|
|
1714
|
+
return result;
|
|
1715
|
+
} catch (error) {
|
|
1716
|
+
if (!(error instanceof FloPayError3 && error.code === "session_auto_completed")) {
|
|
1717
|
+
try {
|
|
1718
|
+
this.telemetryHooks?.onSessionCreateFailure?.(error);
|
|
1719
|
+
} catch {
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
if (error instanceof FloPayError3 && error.type === "validation_error") {
|
|
1723
|
+
this.directTelemetry?.terminal({
|
|
1724
|
+
outcome: "validation_rejected",
|
|
1725
|
+
stage: "session_create",
|
|
1726
|
+
requestCategory: "session_create"
|
|
1727
|
+
});
|
|
1728
|
+
} else if (!(error instanceof FloPayError3 && error.code === "session_auto_completed")) {
|
|
1729
|
+
const statusCode = error instanceof FloPayError3 ? error.statusCode : void 0;
|
|
1730
|
+
this.directTelemetry?.error({
|
|
1731
|
+
errorCode: error instanceof Error && error.name === "AbortError" ? "REQUEST_TIMEOUT" : error instanceof TypeError ? "NETWORK_REQUEST_FAILED" : "CHECKOUT_SESSION_CREATE_FAILED",
|
|
1732
|
+
stage: "session_create",
|
|
1733
|
+
requestCategory: "session_create",
|
|
1734
|
+
statusClass: error instanceof Error && error.name === "AbortError" ? "timeout" : statusCode ? `${Math.floor(statusCode / 100)}xx` : "network_error"
|
|
1735
|
+
});
|
|
1736
|
+
}
|
|
1737
|
+
throw error;
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
async createAndFetchSessionRequest(params, telemetryStartedAt) {
|
|
944
1741
|
const wireProducts = params.products ?? foldIntoProducts(params.items, params.subscriptions);
|
|
945
1742
|
const sessionCurrency = resolveSessionCurrency(
|
|
946
1743
|
params.currency,
|
|
@@ -997,6 +1794,7 @@ var PaymentAPI = class {
|
|
|
997
1794
|
headers[IDEMPOTENCY_KEY_HEADER] = idempotencyKey;
|
|
998
1795
|
}
|
|
999
1796
|
let response;
|
|
1797
|
+
let firstByteReported = false;
|
|
1000
1798
|
for (let attempt = 0; ; attempt++) {
|
|
1001
1799
|
response = await fetchWithNetworkRetry(
|
|
1002
1800
|
`${this.baseUrl}/v1/checkouts/sessions?expand=true`,
|
|
@@ -1004,8 +1802,35 @@ var PaymentAPI = class {
|
|
|
1004
1802
|
method: "POST",
|
|
1005
1803
|
headers,
|
|
1006
1804
|
body: JSON.stringify(payload)
|
|
1805
|
+
},
|
|
1806
|
+
NETWORK_RETRY_ATTEMPTS,
|
|
1807
|
+
(networkAttempt) => {
|
|
1808
|
+
this.telemetryHooks?.onRetry?.("session_create", networkAttempt);
|
|
1809
|
+
this.directTelemetry?.log({
|
|
1810
|
+
name: "operation.retry",
|
|
1811
|
+
stage: "session_create",
|
|
1812
|
+
requestCategory: "session_create",
|
|
1813
|
+
attempt: networkAttempt
|
|
1814
|
+
});
|
|
1007
1815
|
}
|
|
1008
1816
|
);
|
|
1817
|
+
if (!firstByteReported) {
|
|
1818
|
+
firstByteReported = true;
|
|
1819
|
+
const statusClass = `${Math.floor(response.status / 100)}xx`;
|
|
1820
|
+
this.directTelemetry?.log({
|
|
1821
|
+
name: "session.request.first_byte",
|
|
1822
|
+
stage: "session_first_byte",
|
|
1823
|
+
requestCategory: "session_create",
|
|
1824
|
+
statusClass
|
|
1825
|
+
});
|
|
1826
|
+
this.directTelemetry?.performance({
|
|
1827
|
+
stage: "session_first_byte",
|
|
1828
|
+
durationMs: this.telemetryTimestamp() - telemetryStartedAt,
|
|
1829
|
+
durationMode: "machine",
|
|
1830
|
+
requestCategory: "session_create",
|
|
1831
|
+
statusClass
|
|
1832
|
+
});
|
|
1833
|
+
}
|
|
1009
1834
|
if (response.status === 204) {
|
|
1010
1835
|
throw new FloPayError3(
|
|
1011
1836
|
"Session auto-completed \u2014 payment method already on file",
|
|
@@ -1016,6 +1841,16 @@ var PaymentAPI = class {
|
|
|
1016
1841
|
if (response.ok) break;
|
|
1017
1842
|
const error = await buildApiErrorFromResponse(response, "Failed to create checkout session");
|
|
1018
1843
|
if (error.code === IDEMPOTENCY_IN_PROGRESS_CODE && attempt < IDEMPOTENCY_IN_PROGRESS_RETRY_ATTEMPTS) {
|
|
1844
|
+
try {
|
|
1845
|
+
this.telemetryHooks?.onRetry?.("session_create", attempt + 1);
|
|
1846
|
+
this.directTelemetry?.log({
|
|
1847
|
+
name: "operation.retry",
|
|
1848
|
+
stage: "session_create",
|
|
1849
|
+
requestCategory: "session_create",
|
|
1850
|
+
attempt: attempt + 1
|
|
1851
|
+
});
|
|
1852
|
+
} catch {
|
|
1853
|
+
}
|
|
1019
1854
|
await delay(150 * 2 ** attempt);
|
|
1020
1855
|
continue;
|
|
1021
1856
|
}
|
|
@@ -1041,6 +1876,7 @@ var PaymentAPI = class {
|
|
|
1041
1876
|
throw new FloPayError3("No session ID returned", "api_error");
|
|
1042
1877
|
}
|
|
1043
1878
|
this.autoCacheDisplayData(uuid, params);
|
|
1879
|
+
this.adoptDirectTelemetryCheckout(uuid);
|
|
1044
1880
|
const unifiedSession = await this.getUnifiedCheckoutSession(uuid);
|
|
1045
1881
|
return {
|
|
1046
1882
|
...unifiedSession,
|
|
@@ -1050,31 +1886,78 @@ var PaymentAPI = class {
|
|
|
1050
1886
|
};
|
|
1051
1887
|
}
|
|
1052
1888
|
async waitForCheckoutSessionCompletion(checkoutSessionId, options) {
|
|
1889
|
+
this.beginDirectTelemetryCheckout(checkoutSessionId);
|
|
1890
|
+
const startedAt = this.telemetryTimestamp();
|
|
1891
|
+
this.directTelemetry?.log({
|
|
1892
|
+
name: "operation.recovery.started",
|
|
1893
|
+
stage: "recovery",
|
|
1894
|
+
requestCategory: "session_read",
|
|
1895
|
+
paymentMethodCategory: "saved"
|
|
1896
|
+
});
|
|
1053
1897
|
const timeoutMs = options?.timeoutMs ?? DEFAULT_PROCESSING_TIMEOUT_MS;
|
|
1054
1898
|
const deadline = Date.now() + timeoutMs;
|
|
1055
1899
|
let nextDelayMs = this.clampRetryAfterMs(options?.initialDelayMs ?? DEFAULT_PROCESSING_RETRY_AFTER_MS);
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1900
|
+
let pollAttempt = 0;
|
|
1901
|
+
try {
|
|
1902
|
+
while (true) {
|
|
1903
|
+
const remainingMs = deadline - Date.now();
|
|
1904
|
+
if (remainingMs <= 0) {
|
|
1905
|
+
throw createCheckoutProcessingTimeoutError();
|
|
1906
|
+
}
|
|
1907
|
+
if (nextDelayMs > 0) {
|
|
1908
|
+
try {
|
|
1909
|
+
pollAttempt += 1;
|
|
1910
|
+
this.telemetryHooks?.onRetry?.("session_read", pollAttempt);
|
|
1911
|
+
this.directTelemetry?.log({
|
|
1912
|
+
name: "operation.retry",
|
|
1913
|
+
stage: "recovery",
|
|
1914
|
+
requestCategory: "session_read",
|
|
1915
|
+
paymentMethodCategory: "saved",
|
|
1916
|
+
attempt: pollAttempt
|
|
1917
|
+
});
|
|
1918
|
+
} catch {
|
|
1919
|
+
}
|
|
1920
|
+
await delay(Math.min(nextDelayMs, remainingMs));
|
|
1921
|
+
if (Date.now() >= deadline) {
|
|
1922
|
+
throw createCheckoutProcessingTimeoutError();
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
const session = await this.getUnifiedCheckoutSession(checkoutSessionId, options?.nonce);
|
|
1926
|
+
const status = session.data.session?.status;
|
|
1927
|
+
if (status === "complete" || status === "expired") {
|
|
1928
|
+
this.directTelemetry?.log({
|
|
1929
|
+
name: "operation.recovery.completed",
|
|
1930
|
+
stage: "recovery",
|
|
1931
|
+
requestCategory: "session_read",
|
|
1932
|
+
paymentMethodCategory: "saved"
|
|
1933
|
+
});
|
|
1934
|
+
this.directTelemetry?.performance({
|
|
1935
|
+
stage: "recovery",
|
|
1936
|
+
durationMs: Math.max(0, this.telemetryTimestamp() - startedAt),
|
|
1937
|
+
durationMode: "machine",
|
|
1938
|
+
requestCategory: "session_read",
|
|
1939
|
+
paymentMethodCategory: "saved"
|
|
1940
|
+
});
|
|
1941
|
+
return session;
|
|
1942
|
+
}
|
|
1063
1943
|
if (Date.now() >= deadline) {
|
|
1064
1944
|
throw createCheckoutProcessingTimeoutError();
|
|
1065
1945
|
}
|
|
1946
|
+
nextDelayMs = this.clampRetryAfterMs(
|
|
1947
|
+
Math.max(nextDelayMs * 2, MIN_PROCESSING_RETRY_AFTER_MS)
|
|
1948
|
+
);
|
|
1066
1949
|
}
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1950
|
+
} catch (error) {
|
|
1951
|
+
if (error instanceof FloPayError3 && error.code === "checkout_processing_timeout") {
|
|
1952
|
+
this.reportDirectFailure(
|
|
1953
|
+
error,
|
|
1954
|
+
"RECOVERY_FAILED",
|
|
1955
|
+
"recovery",
|
|
1956
|
+
"session_read",
|
|
1957
|
+
"saved"
|
|
1958
|
+
);
|
|
1074
1959
|
}
|
|
1075
|
-
|
|
1076
|
-
Math.max(nextDelayMs * 2, MIN_PROCESSING_RETRY_AFTER_MS)
|
|
1077
|
-
);
|
|
1960
|
+
throw error;
|
|
1078
1961
|
}
|
|
1079
1962
|
}
|
|
1080
1963
|
/** Normalize a raw session into a provider-agnostic shape. */
|
|
@@ -1295,13 +2178,86 @@ var PaymentAPI = class {
|
|
|
1295
2178
|
};
|
|
1296
2179
|
}
|
|
1297
2180
|
};
|
|
2181
|
+
function createInstrumentedPaymentAPI(billingApiUrl, hooks) {
|
|
2182
|
+
const InstrumentedPaymentAPI = PaymentAPI;
|
|
2183
|
+
return new InstrumentedPaymentAPI(billingApiUrl, hooks);
|
|
2184
|
+
}
|
|
1298
2185
|
|
|
1299
2186
|
// src/pci-vault-card-capture.ts
|
|
1300
|
-
import {
|
|
2187
|
+
import {
|
|
2188
|
+
buildTelemetryErrorEvent as buildTelemetryErrorEvent2,
|
|
2189
|
+
buildTelemetryLogEvent as buildTelemetryLogEvent2,
|
|
2190
|
+
buildTelemetryTerminalEvent as buildTelemetryTerminalEvent2,
|
|
2191
|
+
FloPayError as FloPayError4,
|
|
2192
|
+
resolveBillingApiUrl,
|
|
2193
|
+
SDK_VERSION as SDK_VERSION2
|
|
2194
|
+
} from "@flopay/shared";
|
|
1301
2195
|
var VAULT_MESSAGE_SOURCE = "flopay-vault";
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
2196
|
+
var VAULT_NON_TERMINAL_OUTCOME_LOGS = {
|
|
2197
|
+
ready: ["vault.widget.ready", "vault_ready"],
|
|
2198
|
+
submitting: ["vault.submission.started", "vault_submit"],
|
|
2199
|
+
blocked: ["operation.state_transition", "vault_submit"],
|
|
2200
|
+
action_required: ["vault.action.required", "three_ds_handoff"]
|
|
2201
|
+
};
|
|
2202
|
+
function addBreadcrumb(event) {
|
|
2203
|
+
const data = {
|
|
2204
|
+
class: event.class,
|
|
2205
|
+
stage: event.stage
|
|
2206
|
+
};
|
|
2207
|
+
if ("code" in event) data["code"] = event.code;
|
|
2208
|
+
if ("provider" in event && event.provider) data["provider"] = event.provider;
|
|
2209
|
+
if ("paymentMethodCategory" in event && event.paymentMethodCategory) {
|
|
2210
|
+
data["paymentMethodCategory"] = event.paymentMethodCategory;
|
|
2211
|
+
}
|
|
2212
|
+
if ("outcome" in event) data["outcome"] = event.outcome;
|
|
2213
|
+
if ("durationMs" in event && event.durationMs !== void 0) data["durationMs"] = event.durationMs;
|
|
2214
|
+
if ("durationMode" in event && event.durationMode) data["durationMode"] = event.durationMode;
|
|
2215
|
+
try {
|
|
2216
|
+
const sentry = globalThis.Sentry;
|
|
2217
|
+
sentry?.addBreadcrumb?.({
|
|
2218
|
+
category: "flopay.telemetry",
|
|
2219
|
+
level: event.class === "technical_error" ? "error" : "info",
|
|
2220
|
+
message: event.class === "lifecycle" ? event.name : event.class === "technical_error" ? event.code : event.class === "expected_outcome" ? event.outcome : "sdk.performance",
|
|
2221
|
+
data
|
|
2222
|
+
});
|
|
2223
|
+
} catch {
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
function vaultLog(name, stage) {
|
|
2227
|
+
return buildTelemetryLogEvent2({
|
|
2228
|
+
eventId: "11111111-1111-4111-8111-111111111111",
|
|
2229
|
+
name,
|
|
2230
|
+
stage,
|
|
2231
|
+
sequence: 0,
|
|
2232
|
+
provider: "pcivault",
|
|
2233
|
+
paymentMethodCategory: "card"
|
|
2234
|
+
});
|
|
2235
|
+
}
|
|
2236
|
+
function vaultErrorClassification(submissionStarted) {
|
|
2237
|
+
return submissionStarted ? { errorCode: "VAULT_SUBMIT_FAILED", stage: "vault_submit" } : { errorCode: "VAULT_LOAD_FAILED", stage: "vault_mount" };
|
|
2238
|
+
}
|
|
2239
|
+
function vaultOutcomeBreadcrumb(type, submissionStarted) {
|
|
2240
|
+
if (type === "complete" || type === "decline") {
|
|
2241
|
+
return buildTelemetryTerminalEvent2({
|
|
2242
|
+
eventId: "22222222-2222-4222-8222-222222222222",
|
|
2243
|
+
outcome: type === "complete" ? "payment_succeeded" : "payment_declined",
|
|
2244
|
+
sequence: 0,
|
|
2245
|
+
provider: "pcivault",
|
|
2246
|
+
paymentMethodCategory: "card"
|
|
2247
|
+
});
|
|
2248
|
+
}
|
|
2249
|
+
if (type === "error") {
|
|
2250
|
+
const classification = vaultErrorClassification(submissionStarted);
|
|
2251
|
+
return buildTelemetryErrorEvent2({
|
|
2252
|
+
eventId: "33333333-3333-4333-8333-333333333333",
|
|
2253
|
+
...classification,
|
|
2254
|
+
sequence: 0,
|
|
2255
|
+
provider: "pcivault",
|
|
2256
|
+
paymentMethodCategory: "card"
|
|
2257
|
+
});
|
|
2258
|
+
}
|
|
2259
|
+
const [name, stage] = VAULT_NON_TERMINAL_OUTCOME_LOGS[type];
|
|
2260
|
+
return vaultLog(name, stage);
|
|
1305
2261
|
}
|
|
1306
2262
|
function isVaultResultMessage(value) {
|
|
1307
2263
|
if (typeof value !== "object" || value === null) return false;
|
|
@@ -1319,7 +2275,7 @@ function isVaultResizeMessage(value) {
|
|
|
1319
2275
|
return record["source"] === VAULT_MESSAGE_SOURCE && record["type"] === "resize" && typeof record["height"] === "number" && Number.isFinite(record["height"]);
|
|
1320
2276
|
}
|
|
1321
2277
|
var PciVaultCardCapture = class {
|
|
1322
|
-
constructor(config = {}) {
|
|
2278
|
+
constructor(config = {}, internalTelemetry) {
|
|
1323
2279
|
this.provider = "pcivault";
|
|
1324
2280
|
this.container = null;
|
|
1325
2281
|
this.messageHandler = null;
|
|
@@ -1351,11 +2307,33 @@ var PciVaultCardCapture = class {
|
|
|
1351
2307
|
/** Latest card-field order + autofocus directive to push into the widget. */
|
|
1352
2308
|
this.cardFieldOrder = null;
|
|
1353
2309
|
this.cardAutoFocus = true;
|
|
2310
|
+
this.captureRequestedAt = 0;
|
|
2311
|
+
this.vaultReadyReported = false;
|
|
2312
|
+
this.submissionStarted = false;
|
|
2313
|
+
this.submissionStartedAt = null;
|
|
1354
2314
|
this.listeners = /* @__PURE__ */ new Map();
|
|
1355
2315
|
this.config = config;
|
|
2316
|
+
this.ownsTelemetryReporter = !internalTelemetry && config.telemetry !== false;
|
|
2317
|
+
this.telemetryReporter = internalTelemetry?.reporter ?? (this.ownsTelemetryReporter ? new TelemetryReporter({
|
|
2318
|
+
billingApiUrl: resolveBillingApiUrl(),
|
|
2319
|
+
sdkVersion: SDK_VERSION2
|
|
2320
|
+
}) : void 0);
|
|
2321
|
+
this.requestedAt = internalTelemetry?.requestedAt;
|
|
1356
2322
|
}
|
|
1357
2323
|
async mount(container, options) {
|
|
2324
|
+
if (this.ownsTelemetryReporter && !this.telemetryReporter) {
|
|
2325
|
+
this.telemetryReporter = new TelemetryReporter({
|
|
2326
|
+
billingApiUrl: resolveBillingApiUrl(),
|
|
2327
|
+
sdkVersion: SDK_VERSION2
|
|
2328
|
+
});
|
|
2329
|
+
}
|
|
2330
|
+
const mountedAt = this.telemetryReporter?.now?.() ?? telemetryNow();
|
|
2331
|
+
this.captureRequestedAt = this.requestedAt ?? mountedAt;
|
|
2332
|
+
this.vaultReadyReported = false;
|
|
2333
|
+
this.submissionStarted = false;
|
|
2334
|
+
this.submissionStartedAt = null;
|
|
1358
2335
|
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
2336
|
+
this.reportVaultLoadFailure();
|
|
1359
2337
|
throw new FloPayError4(
|
|
1360
2338
|
"The vault card form is only available in the browser.",
|
|
1361
2339
|
"api_error",
|
|
@@ -1363,6 +2341,7 @@ var PciVaultCardCapture = class {
|
|
|
1363
2341
|
);
|
|
1364
2342
|
}
|
|
1365
2343
|
if (!options?.html || !options.html.trim()) {
|
|
2344
|
+
this.reportVaultLoadFailure();
|
|
1366
2345
|
throw new FloPayError4(
|
|
1367
2346
|
"No vault capture widget HTML was provided to mount the secure card form.",
|
|
1368
2347
|
"api_error",
|
|
@@ -1373,14 +2352,33 @@ var PciVaultCardCapture = class {
|
|
|
1373
2352
|
this.messageToken = options.messageToken ?? null;
|
|
1374
2353
|
this.expectedOrigin = options.expectedOrigin ?? this.config.expectedOrigin ?? null;
|
|
1375
2354
|
this.theme = options.theme ?? null;
|
|
1376
|
-
|
|
1377
|
-
|
|
2355
|
+
try {
|
|
2356
|
+
this.attachMessageListener();
|
|
2357
|
+
this.injectWidget(container, options.html);
|
|
2358
|
+
} catch (error) {
|
|
2359
|
+
this.reportVaultLoadFailure();
|
|
2360
|
+
throw error;
|
|
2361
|
+
}
|
|
1378
2362
|
this.postTheme();
|
|
1379
2363
|
this.postSubmitGate();
|
|
1380
2364
|
this.postCardFieldOrder();
|
|
1381
|
-
addBreadcrumb("vault
|
|
2365
|
+
addBreadcrumb(vaultLog("vault.widget.mounted", "vault_mount"));
|
|
2366
|
+
this.telemetryReporter?.log({
|
|
2367
|
+
name: "vault.widget.mounted",
|
|
2368
|
+
stage: "vault_mount",
|
|
2369
|
+
provider: "pcivault",
|
|
2370
|
+
paymentMethodCategory: "card"
|
|
2371
|
+
});
|
|
1382
2372
|
this.emit("ready", { sessionId: this.config.sessionId });
|
|
1383
2373
|
}
|
|
2374
|
+
reportVaultLoadFailure() {
|
|
2375
|
+
this.telemetryReporter?.error({
|
|
2376
|
+
errorCode: "VAULT_LOAD_FAILED",
|
|
2377
|
+
stage: "vault_mount",
|
|
2378
|
+
provider: "pcivault",
|
|
2379
|
+
paymentMethodCategory: "card"
|
|
2380
|
+
});
|
|
2381
|
+
}
|
|
1384
2382
|
on(event, handler) {
|
|
1385
2383
|
let set = this.listeners.get(event);
|
|
1386
2384
|
if (!set) {
|
|
@@ -1404,6 +2402,10 @@ var PciVaultCardCapture = class {
|
|
|
1404
2402
|
}
|
|
1405
2403
|
this.messageToken = null;
|
|
1406
2404
|
this.expectedOrigin = null;
|
|
2405
|
+
if (this.ownsTelemetryReporter) {
|
|
2406
|
+
this.telemetryReporter?.destroy();
|
|
2407
|
+
this.telemetryReporter = void 0;
|
|
2408
|
+
}
|
|
1407
2409
|
}
|
|
1408
2410
|
// ── internals ──
|
|
1409
2411
|
/**
|
|
@@ -1457,10 +2459,12 @@ var PciVaultCardCapture = class {
|
|
|
1457
2459
|
message: data.message,
|
|
1458
2460
|
nextActionRedirectUrl: data.nextActionRedirectUrl
|
|
1459
2461
|
};
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
}
|
|
2462
|
+
if (data.type === "submitting") {
|
|
2463
|
+
this.submissionStarted = true;
|
|
2464
|
+
this.submissionStartedAt = this.telemetryReporter?.now?.() ?? telemetryNow();
|
|
2465
|
+
}
|
|
2466
|
+
addBreadcrumb(vaultOutcomeBreadcrumb(data.type, this.submissionStarted));
|
|
2467
|
+
this.reportOutcome(data.type);
|
|
1464
2468
|
if (data.type === "ready") {
|
|
1465
2469
|
this.postTheme();
|
|
1466
2470
|
this.postSubmitGate();
|
|
@@ -1477,6 +2481,88 @@ var PciVaultCardCapture = class {
|
|
|
1477
2481
|
this.messageHandler = handler;
|
|
1478
2482
|
window.addEventListener("message", handler);
|
|
1479
2483
|
}
|
|
2484
|
+
reportOutcome(type) {
|
|
2485
|
+
const reporter = this.telemetryReporter;
|
|
2486
|
+
if (!reporter) return;
|
|
2487
|
+
if (type === "ready" && !this.vaultReadyReported) {
|
|
2488
|
+
this.vaultReadyReported = true;
|
|
2489
|
+
reporter.performance({
|
|
2490
|
+
stage: "vault_ready",
|
|
2491
|
+
durationMs: Math.max(0, reporter.now() - this.captureRequestedAt),
|
|
2492
|
+
durationMode: "machine",
|
|
2493
|
+
provider: "pcivault",
|
|
2494
|
+
paymentMethodCategory: "card"
|
|
2495
|
+
});
|
|
2496
|
+
}
|
|
2497
|
+
if (type === "complete" || type === "decline") {
|
|
2498
|
+
reporter.log({
|
|
2499
|
+
name: "vault.terminal",
|
|
2500
|
+
stage: "completion",
|
|
2501
|
+
provider: "pcivault",
|
|
2502
|
+
paymentMethodCategory: "card"
|
|
2503
|
+
});
|
|
2504
|
+
reporter.terminal({
|
|
2505
|
+
outcome: type === "complete" ? "payment_succeeded" : "payment_declined",
|
|
2506
|
+
provider: "pcivault",
|
|
2507
|
+
paymentMethodCategory: "card"
|
|
2508
|
+
});
|
|
2509
|
+
return;
|
|
2510
|
+
}
|
|
2511
|
+
if (type === "error") {
|
|
2512
|
+
const classification = vaultErrorClassification(this.submissionStarted);
|
|
2513
|
+
reporter.log({
|
|
2514
|
+
name: "vault.terminal",
|
|
2515
|
+
stage: classification.stage,
|
|
2516
|
+
provider: "pcivault",
|
|
2517
|
+
paymentMethodCategory: "card"
|
|
2518
|
+
});
|
|
2519
|
+
reporter.error({
|
|
2520
|
+
...classification,
|
|
2521
|
+
provider: "pcivault",
|
|
2522
|
+
paymentMethodCategory: "card"
|
|
2523
|
+
});
|
|
2524
|
+
return;
|
|
2525
|
+
}
|
|
2526
|
+
if (type === "action_required") {
|
|
2527
|
+
reporter.log({
|
|
2528
|
+
name: "vault.action.required",
|
|
2529
|
+
stage: "three_ds_handoff",
|
|
2530
|
+
provider: "pcivault",
|
|
2531
|
+
paymentMethodCategory: "card"
|
|
2532
|
+
});
|
|
2533
|
+
reporter.log({
|
|
2534
|
+
name: "vault.three_ds.handoff",
|
|
2535
|
+
stage: "three_ds_handoff",
|
|
2536
|
+
provider: "pcivault",
|
|
2537
|
+
paymentMethodCategory: "card"
|
|
2538
|
+
});
|
|
2539
|
+
const submissionStartedAt = this.submissionStartedAt;
|
|
2540
|
+
this.submissionStartedAt = null;
|
|
2541
|
+
if (submissionStartedAt !== null) {
|
|
2542
|
+
reporter.performance({
|
|
2543
|
+
stage: "three_ds_handoff",
|
|
2544
|
+
durationMs: Math.max(0, reporter.now() - submissionStartedAt),
|
|
2545
|
+
durationMode: "machine",
|
|
2546
|
+
provider: "pcivault",
|
|
2547
|
+
paymentMethodCategory: "card"
|
|
2548
|
+
});
|
|
2549
|
+
}
|
|
2550
|
+
reporter.terminal({
|
|
2551
|
+
outcome: "action_required",
|
|
2552
|
+
stage: "three_ds_handoff",
|
|
2553
|
+
provider: "pcivault",
|
|
2554
|
+
paymentMethodCategory: "card"
|
|
2555
|
+
});
|
|
2556
|
+
return;
|
|
2557
|
+
}
|
|
2558
|
+
const [name, stage] = VAULT_NON_TERMINAL_OUTCOME_LOGS[type];
|
|
2559
|
+
reporter.log({
|
|
2560
|
+
name,
|
|
2561
|
+
stage,
|
|
2562
|
+
provider: "pcivault",
|
|
2563
|
+
paymentMethodCategory: "card"
|
|
2564
|
+
});
|
|
2565
|
+
}
|
|
1480
2566
|
/**
|
|
1481
2567
|
* Push merchant theme colors into the hosted widget (live). The host calls
|
|
1482
2568
|
* this on a runtime theme switch; the widget applies them to its CSS variables
|
|
@@ -1604,12 +2690,43 @@ var PciVaultCardCapture = class {
|
|
|
1604
2690
|
].join(";");
|
|
1605
2691
|
frame.src = challengeUrl;
|
|
1606
2692
|
backdrop.appendChild(frame);
|
|
2693
|
+
const closeButton = document.createElement("button");
|
|
2694
|
+
closeButton.type = "button";
|
|
2695
|
+
closeButton.setAttribute("aria-label", "Close card authentication");
|
|
2696
|
+
closeButton.textContent = "\xD7";
|
|
2697
|
+
closeButton.style.cssText = [
|
|
2698
|
+
"position:fixed",
|
|
2699
|
+
"top:20px",
|
|
2700
|
+
"right:20px",
|
|
2701
|
+
"width:40px",
|
|
2702
|
+
"height:40px",
|
|
2703
|
+
"border:0",
|
|
2704
|
+
"border-radius:9999px",
|
|
2705
|
+
"background:#fff",
|
|
2706
|
+
"color:#0f172a",
|
|
2707
|
+
"font-size:28px",
|
|
2708
|
+
"line-height:40px",
|
|
2709
|
+
"cursor:pointer",
|
|
2710
|
+
"box-shadow:0 4px 14px rgba(0,0,0,0.25)"
|
|
2711
|
+
].join(";");
|
|
2712
|
+
closeButton.addEventListener("click", () => this.abandonActionRequiredOverlay());
|
|
2713
|
+
backdrop.appendChild(closeButton);
|
|
2714
|
+
backdrop.addEventListener("click", (event) => {
|
|
2715
|
+
if (event.target === backdrop) this.abandonActionRequiredOverlay();
|
|
2716
|
+
});
|
|
1607
2717
|
const returnHandler = (event) => {
|
|
1608
2718
|
if (event.source !== frame.contentWindow) return;
|
|
1609
2719
|
const data = event.data;
|
|
1610
2720
|
if (!data || typeof data !== "object") return;
|
|
1611
2721
|
const record = data;
|
|
1612
2722
|
if (record["source"] !== "flopay-vault-3ds-return") return;
|
|
2723
|
+
addBreadcrumb(vaultLog("vault.three_ds.returned", "three_ds_return"));
|
|
2724
|
+
this.telemetryReporter?.log({
|
|
2725
|
+
name: "vault.three_ds.returned",
|
|
2726
|
+
stage: "three_ds_return",
|
|
2727
|
+
provider: "pcivault",
|
|
2728
|
+
paymentMethodCategory: "card"
|
|
2729
|
+
});
|
|
1613
2730
|
this.hideActionRequiredOverlay();
|
|
1614
2731
|
this.postActionCompleted(record["status"]);
|
|
1615
2732
|
};
|
|
@@ -1617,7 +2734,7 @@ var PciVaultCardCapture = class {
|
|
|
1617
2734
|
this.threeDsReturnHandler = returnHandler;
|
|
1618
2735
|
document.body.appendChild(backdrop);
|
|
1619
2736
|
this.actionOverlay = backdrop;
|
|
1620
|
-
addBreadcrumb("vault
|
|
2737
|
+
addBreadcrumb(vaultLog("vault.three_ds.handoff", "three_ds_handoff"));
|
|
1621
2738
|
}
|
|
1622
2739
|
/**
|
|
1623
2740
|
* Tell the vault widget that the buyer has completed (or abandoned) the
|
|
@@ -1643,6 +2760,26 @@ var PciVaultCardCapture = class {
|
|
|
1643
2760
|
} catch {
|
|
1644
2761
|
}
|
|
1645
2762
|
}
|
|
2763
|
+
abandonActionRequiredOverlay() {
|
|
2764
|
+
if (!this.actionOverlay) return;
|
|
2765
|
+
const breadcrumb = buildTelemetryTerminalEvent2({
|
|
2766
|
+
eventId: "77777777-7777-4777-8777-777777777777",
|
|
2767
|
+
outcome: "customer_abandoned",
|
|
2768
|
+
stage: "three_ds_handoff",
|
|
2769
|
+
sequence: 0,
|
|
2770
|
+
provider: "pcivault",
|
|
2771
|
+
paymentMethodCategory: "card"
|
|
2772
|
+
});
|
|
2773
|
+
addBreadcrumb(breadcrumb);
|
|
2774
|
+
this.telemetryReporter?.terminal({
|
|
2775
|
+
outcome: "customer_abandoned",
|
|
2776
|
+
stage: "three_ds_handoff",
|
|
2777
|
+
provider: "pcivault",
|
|
2778
|
+
paymentMethodCategory: "card"
|
|
2779
|
+
});
|
|
2780
|
+
this.hideActionRequiredOverlay();
|
|
2781
|
+
this.postActionCompleted("abandoned");
|
|
2782
|
+
}
|
|
1646
2783
|
hideActionRequiredOverlay() {
|
|
1647
2784
|
if (this.threeDsReturnHandler) {
|
|
1648
2785
|
window.removeEventListener("message", this.threeDsReturnHandler);
|
|
@@ -1651,7 +2788,6 @@ var PciVaultCardCapture = class {
|
|
|
1651
2788
|
if (!this.actionOverlay) return;
|
|
1652
2789
|
this.actionOverlay.parentNode?.removeChild(this.actionOverlay);
|
|
1653
2790
|
this.actionOverlay = null;
|
|
1654
|
-
addBreadcrumb("vault 3ds challenge overlay hidden");
|
|
1655
2791
|
}
|
|
1656
2792
|
/**
|
|
1657
2793
|
* Size the hosted-widget iframe to the height reported by the form inside it.
|
|
@@ -1667,13 +2803,62 @@ var PciVaultCardCapture = class {
|
|
|
1667
2803
|
iframe.style.height = `${clamped}px`;
|
|
1668
2804
|
}
|
|
1669
2805
|
};
|
|
2806
|
+
function createInstrumentedPciVaultCardCapture(config, reporter, requestedAt) {
|
|
2807
|
+
const InstrumentedCapture = PciVaultCardCapture;
|
|
2808
|
+
return new InstrumentedCapture(config, { reporter, requestedAt });
|
|
2809
|
+
}
|
|
2810
|
+
|
|
2811
|
+
// src/telemetry-bridge.ts
|
|
2812
|
+
var FLOPAY_TELEMETRY_BRIDGE = /* @__PURE__ */ Symbol.for("@flopay/js.telemetry.bridge.v1");
|
|
2813
|
+
function attachFloPayTelemetryBridge(target, reporter, fallbackNow) {
|
|
2814
|
+
const now = () => reporter?.now() ?? fallbackNow();
|
|
2815
|
+
const bridge = {
|
|
2816
|
+
error: (input) => reporter?.error(input),
|
|
2817
|
+
log: (input) => reporter?.log(input),
|
|
2818
|
+
performance: (input) => reporter?.performance(input),
|
|
2819
|
+
terminal: (input) => reporter?.terminal(input),
|
|
2820
|
+
now,
|
|
2821
|
+
elapsed: (startedAt) => Math.max(0, now() - startedAt),
|
|
2822
|
+
setCheckoutContext: (context) => reporter?.setCheckoutContext(context),
|
|
2823
|
+
beginCheckout: (context = {}) => reporter?.beginCheckout(context) ?? now(),
|
|
2824
|
+
disable: () => reporter?.disable()
|
|
2825
|
+
};
|
|
2826
|
+
Object.defineProperty(target, FLOPAY_TELEMETRY_BRIDGE, {
|
|
2827
|
+
configurable: false,
|
|
2828
|
+
enumerable: false,
|
|
2829
|
+
writable: false,
|
|
2830
|
+
value: bridge
|
|
2831
|
+
});
|
|
2832
|
+
}
|
|
2833
|
+
function getFloPayTelemetryBridge(target) {
|
|
2834
|
+
return target[FLOPAY_TELEMETRY_BRIDGE];
|
|
2835
|
+
}
|
|
1670
2836
|
|
|
1671
2837
|
// src/flopay.ts
|
|
2838
|
+
function isExpectedDecline(error) {
|
|
2839
|
+
if (!error) return false;
|
|
2840
|
+
const code = error.code?.toLowerCase() ?? "";
|
|
2841
|
+
return Boolean(error.declineCode) || code.includes("declin");
|
|
2842
|
+
}
|
|
2843
|
+
function telemetryProvider(name) {
|
|
2844
|
+
if (name === "stripe" || name === "paypal" || name === "pcivault") return name;
|
|
2845
|
+
return "other";
|
|
2846
|
+
}
|
|
1672
2847
|
var FloPay = class {
|
|
1673
|
-
constructor(provider, config) {
|
|
2848
|
+
constructor(provider, config, telemetryReporter) {
|
|
1674
2849
|
this.currentElements = null;
|
|
2850
|
+
this.cardThreeDsStartedAt = /* @__PURE__ */ new Map();
|
|
1675
2851
|
this.provider = provider;
|
|
1676
2852
|
this.config = config;
|
|
2853
|
+
this.telemetryReporter = telemetryReporter ?? new TelemetryReporter({
|
|
2854
|
+
billingApiUrl: resolveBillingApiUrl2(config.billingApiUrl),
|
|
2855
|
+
sdkVersion: SDK_VERSION3,
|
|
2856
|
+
enabled: config.telemetry !== false
|
|
2857
|
+
});
|
|
2858
|
+
attachFloPayTelemetryBridge(this, this.telemetryReporter, telemetryNow);
|
|
2859
|
+
}
|
|
2860
|
+
now() {
|
|
2861
|
+
return this.telemetryReporter?.now?.() ?? telemetryNow();
|
|
1677
2862
|
}
|
|
1678
2863
|
/**
|
|
1679
2864
|
* Creates a new `FloPayElements` group for mounting payment fields.
|
|
@@ -1697,11 +2882,169 @@ var FloPay = class {
|
|
|
1697
2882
|
}
|
|
1698
2883
|
/** Create a payment method from the current elements (tokenize card). */
|
|
1699
2884
|
async createPaymentMethod(billingDetails) {
|
|
1700
|
-
|
|
2885
|
+
const started = this.now();
|
|
2886
|
+
const provider = telemetryProvider(this.provider.name);
|
|
2887
|
+
this.telemetryReporter?.log({
|
|
2888
|
+
name: "payment.tokenization.started",
|
|
2889
|
+
stage: "tokenization",
|
|
2890
|
+
provider,
|
|
2891
|
+
paymentMethodCategory: "card"
|
|
2892
|
+
});
|
|
2893
|
+
try {
|
|
2894
|
+
const result = await this.provider.createPaymentMethod(billingDetails);
|
|
2895
|
+
this.telemetryReporter?.performance({
|
|
2896
|
+
stage: "tokenization",
|
|
2897
|
+
durationMs: this.now() - started,
|
|
2898
|
+
durationMode: "machine",
|
|
2899
|
+
provider,
|
|
2900
|
+
paymentMethodCategory: "card"
|
|
2901
|
+
});
|
|
2902
|
+
if (!result.error) {
|
|
2903
|
+
this.telemetryReporter?.log({
|
|
2904
|
+
name: "payment.tokenization.completed",
|
|
2905
|
+
stage: "tokenization",
|
|
2906
|
+
provider,
|
|
2907
|
+
paymentMethodCategory: "card"
|
|
2908
|
+
});
|
|
2909
|
+
} else if (result.error.type !== "validation_error") {
|
|
2910
|
+
this.telemetryReporter?.error({
|
|
2911
|
+
errorCode: "TOKENIZATION_FAILED",
|
|
2912
|
+
stage: "tokenization",
|
|
2913
|
+
provider,
|
|
2914
|
+
paymentMethodCategory: "card"
|
|
2915
|
+
});
|
|
2916
|
+
}
|
|
2917
|
+
return result;
|
|
2918
|
+
} catch (error) {
|
|
2919
|
+
this.telemetryReporter?.performance({
|
|
2920
|
+
stage: "tokenization",
|
|
2921
|
+
durationMs: this.now() - started,
|
|
2922
|
+
durationMode: "machine",
|
|
2923
|
+
provider,
|
|
2924
|
+
paymentMethodCategory: "card"
|
|
2925
|
+
});
|
|
2926
|
+
this.telemetryReporter?.error({
|
|
2927
|
+
errorCode: "TOKENIZATION_FAILED",
|
|
2928
|
+
stage: "tokenization",
|
|
2929
|
+
provider,
|
|
2930
|
+
paymentMethodCategory: "card"
|
|
2931
|
+
});
|
|
2932
|
+
throw error;
|
|
2933
|
+
}
|
|
1701
2934
|
}
|
|
1702
2935
|
/** Confirm a card payment with a known client secret and payment method ID. */
|
|
1703
2936
|
async confirmCardPayment(params) {
|
|
1704
|
-
|
|
2937
|
+
const provider = telemetryProvider(this.provider.name);
|
|
2938
|
+
const operationStartedAt = this.now();
|
|
2939
|
+
try {
|
|
2940
|
+
const result = await this.provider.confirmCardPayment(params);
|
|
2941
|
+
const completedAt = this.now();
|
|
2942
|
+
const threeDs = result.threeDs;
|
|
2943
|
+
let threeDsFailed = false;
|
|
2944
|
+
if (threeDs?.status === "handoff") {
|
|
2945
|
+
if (this.trackCardThreeDsAttempt(threeDs.attemptId, completedAt)) {
|
|
2946
|
+
this.telemetryReporter?.log({
|
|
2947
|
+
name: "payment.three_ds.handoff",
|
|
2948
|
+
stage: "three_ds_handoff",
|
|
2949
|
+
provider,
|
|
2950
|
+
paymentMethodCategory: "card"
|
|
2951
|
+
});
|
|
2952
|
+
this.telemetryReporter?.performance({
|
|
2953
|
+
stage: "three_ds_handoff",
|
|
2954
|
+
durationMs: completedAt - operationStartedAt,
|
|
2955
|
+
durationMode: "machine",
|
|
2956
|
+
provider,
|
|
2957
|
+
paymentMethodCategory: "card"
|
|
2958
|
+
});
|
|
2959
|
+
this.telemetryReporter?.terminal({
|
|
2960
|
+
outcome: "action_required",
|
|
2961
|
+
stage: "three_ds_handoff",
|
|
2962
|
+
provider,
|
|
2963
|
+
paymentMethodCategory: "card"
|
|
2964
|
+
});
|
|
2965
|
+
}
|
|
2966
|
+
} else if (threeDs) {
|
|
2967
|
+
const threeDsStartedAt = this.takeCardThreeDsAttempt(threeDs.attemptId, completedAt);
|
|
2968
|
+
if (threeDsStartedAt !== void 0) {
|
|
2969
|
+
if (threeDs.status === "returned") {
|
|
2970
|
+
this.telemetryReporter?.log({
|
|
2971
|
+
name: "payment.three_ds.returned",
|
|
2972
|
+
stage: "three_ds_return",
|
|
2973
|
+
provider,
|
|
2974
|
+
paymentMethodCategory: "card"
|
|
2975
|
+
});
|
|
2976
|
+
this.telemetryReporter?.performance({
|
|
2977
|
+
stage: "three_ds_return",
|
|
2978
|
+
durationMs: completedAt - threeDsStartedAt,
|
|
2979
|
+
durationMode: "machine",
|
|
2980
|
+
provider,
|
|
2981
|
+
paymentMethodCategory: "card"
|
|
2982
|
+
});
|
|
2983
|
+
} else {
|
|
2984
|
+
threeDsFailed = true;
|
|
2985
|
+
}
|
|
2986
|
+
}
|
|
2987
|
+
}
|
|
2988
|
+
if (isExpectedDecline(result.error)) {
|
|
2989
|
+
this.telemetryReporter?.terminal({
|
|
2990
|
+
outcome: "payment_declined",
|
|
2991
|
+
provider,
|
|
2992
|
+
paymentMethodCategory: "card"
|
|
2993
|
+
});
|
|
2994
|
+
} else if (result.error?.type === "validation_error") {
|
|
2995
|
+
this.telemetryReporter?.terminal({
|
|
2996
|
+
outcome: "validation_rejected",
|
|
2997
|
+
provider,
|
|
2998
|
+
paymentMethodCategory: "card"
|
|
2999
|
+
});
|
|
3000
|
+
} else if (result.error) {
|
|
3001
|
+
this.telemetryReporter?.error({
|
|
3002
|
+
errorCode: threeDsFailed ? "THREE_DS_FAILED" : "PROVIDER_RUNTIME_FAILED",
|
|
3003
|
+
stage: threeDsFailed ? "three_ds_return" : "processing",
|
|
3004
|
+
provider,
|
|
3005
|
+
paymentMethodCategory: "card"
|
|
3006
|
+
});
|
|
3007
|
+
} else if (!threeDsFailed && threeDs?.status !== "handoff" && result.status === "succeeded") {
|
|
3008
|
+
this.telemetryReporter?.terminal({
|
|
3009
|
+
outcome: "payment_succeeded",
|
|
3010
|
+
provider,
|
|
3011
|
+
paymentMethodCategory: "card"
|
|
3012
|
+
});
|
|
3013
|
+
}
|
|
3014
|
+
return result;
|
|
3015
|
+
} catch (error) {
|
|
3016
|
+
this.telemetryReporter?.error({
|
|
3017
|
+
errorCode: "PROVIDER_RUNTIME_FAILED",
|
|
3018
|
+
stage: "processing",
|
|
3019
|
+
provider,
|
|
3020
|
+
paymentMethodCategory: "card"
|
|
3021
|
+
});
|
|
3022
|
+
throw error;
|
|
3023
|
+
}
|
|
3024
|
+
}
|
|
3025
|
+
trackCardThreeDsAttempt(attemptId, startedAt) {
|
|
3026
|
+
this.pruneExpiredCardThreeDsAttempts(startedAt);
|
|
3027
|
+
if (this.cardThreeDsStartedAt.has(attemptId)) return false;
|
|
3028
|
+
while (this.cardThreeDsStartedAt.size >= MAX_CARD_THREE_DS_ATTEMPTS) {
|
|
3029
|
+
const oldestAttemptId = this.cardThreeDsStartedAt.keys().next().value;
|
|
3030
|
+
if (oldestAttemptId === void 0) break;
|
|
3031
|
+
this.cardThreeDsStartedAt.delete(oldestAttemptId);
|
|
3032
|
+
}
|
|
3033
|
+
this.cardThreeDsStartedAt.set(attemptId, startedAt);
|
|
3034
|
+
return true;
|
|
3035
|
+
}
|
|
3036
|
+
takeCardThreeDsAttempt(attemptId, now) {
|
|
3037
|
+
this.pruneExpiredCardThreeDsAttempts(now);
|
|
3038
|
+
const startedAt = this.cardThreeDsStartedAt.get(attemptId);
|
|
3039
|
+
if (startedAt !== void 0) this.cardThreeDsStartedAt.delete(attemptId);
|
|
3040
|
+
return startedAt;
|
|
3041
|
+
}
|
|
3042
|
+
pruneExpiredCardThreeDsAttempts(now) {
|
|
3043
|
+
for (const [attemptId, startedAt] of this.cardThreeDsStartedAt) {
|
|
3044
|
+
if (now - startedAt >= CARD_THREE_DS_ATTEMPT_TTL_MS) {
|
|
3045
|
+
this.cardThreeDsStartedAt.delete(attemptId);
|
|
3046
|
+
}
|
|
3047
|
+
}
|
|
1705
3048
|
}
|
|
1706
3049
|
/**
|
|
1707
3050
|
* Create a {@link CardCaptureAdapter} for collecting card details through the
|
|
@@ -1716,21 +3059,270 @@ var FloPay = class {
|
|
|
1716
3059
|
* the SDK runtime.
|
|
1717
3060
|
*/
|
|
1718
3061
|
cardCapture(options) {
|
|
3062
|
+
const requestedAt = this.now();
|
|
3063
|
+
this.telemetryReporter?.log({
|
|
3064
|
+
name: "vault.capture.requested",
|
|
3065
|
+
stage: "vault_request",
|
|
3066
|
+
provider: "pcivault",
|
|
3067
|
+
paymentMethodCategory: "card"
|
|
3068
|
+
});
|
|
3069
|
+
if (this.telemetryReporter) {
|
|
3070
|
+
return createInstrumentedPciVaultCardCapture(
|
|
3071
|
+
{ sessionId: options?.sessionId },
|
|
3072
|
+
this.telemetryReporter,
|
|
3073
|
+
requestedAt
|
|
3074
|
+
);
|
|
3075
|
+
}
|
|
1719
3076
|
return new PciVaultCardCapture({
|
|
1720
|
-
sessionId: options?.sessionId
|
|
3077
|
+
sessionId: options?.sessionId,
|
|
3078
|
+
telemetry: false
|
|
1721
3079
|
});
|
|
1722
3080
|
}
|
|
1723
3081
|
/** Confirm a PayPal payment: create intent via billing API → confirm → redirect if needed. */
|
|
1724
3082
|
async confirmPayPalPayment(params) {
|
|
1725
|
-
|
|
3083
|
+
const startedAt = this.now();
|
|
3084
|
+
this.telemetryReporter?.log({
|
|
3085
|
+
name: "payment.method.selected",
|
|
3086
|
+
stage: "processing",
|
|
3087
|
+
provider: "paypal",
|
|
3088
|
+
paymentMethodCategory: "paypal"
|
|
3089
|
+
});
|
|
3090
|
+
this.telemetryReporter?.log({
|
|
3091
|
+
name: "payment.intent.started",
|
|
3092
|
+
stage: "processing",
|
|
3093
|
+
provider: "paypal",
|
|
3094
|
+
paymentMethodCategory: "paypal",
|
|
3095
|
+
requestCategory: "intent_create"
|
|
3096
|
+
});
|
|
3097
|
+
try {
|
|
3098
|
+
const result = await this.provider.confirmPayPalPayment(params);
|
|
3099
|
+
this.telemetryReporter?.performance({
|
|
3100
|
+
stage: "processing",
|
|
3101
|
+
durationMs: this.now() - startedAt,
|
|
3102
|
+
durationMode: "machine",
|
|
3103
|
+
provider: "paypal",
|
|
3104
|
+
paymentMethodCategory: "paypal"
|
|
3105
|
+
});
|
|
3106
|
+
if (!result.error) {
|
|
3107
|
+
this.telemetryReporter?.log({
|
|
3108
|
+
name: "payment.intent.completed",
|
|
3109
|
+
stage: "processing",
|
|
3110
|
+
provider: "paypal",
|
|
3111
|
+
paymentMethodCategory: "paypal",
|
|
3112
|
+
requestCategory: "intent_create",
|
|
3113
|
+
statusClass: "2xx"
|
|
3114
|
+
});
|
|
3115
|
+
}
|
|
3116
|
+
if (result.status === "requires_action") {
|
|
3117
|
+
this.telemetryReporter?.log({
|
|
3118
|
+
name: "provider.redirect.started",
|
|
3119
|
+
stage: "redirect",
|
|
3120
|
+
provider: "paypal",
|
|
3121
|
+
paymentMethodCategory: "paypal"
|
|
3122
|
+
});
|
|
3123
|
+
this.telemetryReporter?.terminal({
|
|
3124
|
+
outcome: "action_required",
|
|
3125
|
+
stage: "redirect",
|
|
3126
|
+
provider: "paypal",
|
|
3127
|
+
paymentMethodCategory: "paypal"
|
|
3128
|
+
});
|
|
3129
|
+
} else if (result.status === "succeeded") {
|
|
3130
|
+
this.telemetryReporter?.terminal({
|
|
3131
|
+
outcome: "payment_succeeded",
|
|
3132
|
+
provider: "paypal",
|
|
3133
|
+
paymentMethodCategory: "paypal"
|
|
3134
|
+
});
|
|
3135
|
+
} else if (result.status !== "processing") {
|
|
3136
|
+
if (isExpectedDecline(result.error)) {
|
|
3137
|
+
this.telemetryReporter?.terminal({
|
|
3138
|
+
outcome: "payment_declined",
|
|
3139
|
+
provider: "paypal",
|
|
3140
|
+
paymentMethodCategory: "paypal"
|
|
3141
|
+
});
|
|
3142
|
+
} else if (result.error?.type === "validation_error") {
|
|
3143
|
+
this.telemetryReporter?.terminal({
|
|
3144
|
+
outcome: "validation_rejected",
|
|
3145
|
+
provider: "paypal",
|
|
3146
|
+
paymentMethodCategory: "paypal"
|
|
3147
|
+
});
|
|
3148
|
+
} else if (result.error) {
|
|
3149
|
+
this.telemetryReporter?.error({
|
|
3150
|
+
errorCode: "PAYMENT_PROCESSING_FAILED",
|
|
3151
|
+
stage: "processing",
|
|
3152
|
+
provider: "paypal",
|
|
3153
|
+
paymentMethodCategory: "paypal",
|
|
3154
|
+
requestCategory: "intent_create"
|
|
3155
|
+
});
|
|
3156
|
+
}
|
|
3157
|
+
}
|
|
3158
|
+
return result;
|
|
3159
|
+
} catch (error) {
|
|
3160
|
+
this.telemetryReporter?.performance({
|
|
3161
|
+
stage: "processing",
|
|
3162
|
+
durationMs: this.now() - startedAt,
|
|
3163
|
+
durationMode: "machine",
|
|
3164
|
+
provider: "paypal",
|
|
3165
|
+
paymentMethodCategory: "paypal"
|
|
3166
|
+
});
|
|
3167
|
+
this.telemetryReporter?.error({
|
|
3168
|
+
errorCode: "NETWORK_REQUEST_FAILED",
|
|
3169
|
+
stage: "processing",
|
|
3170
|
+
provider: "paypal",
|
|
3171
|
+
paymentMethodCategory: "paypal",
|
|
3172
|
+
requestCategory: "intent_create",
|
|
3173
|
+
statusClass: "network_error"
|
|
3174
|
+
});
|
|
3175
|
+
throw error;
|
|
3176
|
+
}
|
|
1726
3177
|
}
|
|
1727
3178
|
/** Resume a PayPal payment after redirect return. Returns null if no PayPal params in URL. */
|
|
1728
3179
|
async resumePayPalPayment() {
|
|
1729
|
-
|
|
3180
|
+
const startedAt = this.now();
|
|
3181
|
+
try {
|
|
3182
|
+
const result = await this.provider.resumePayPalPayment();
|
|
3183
|
+
if (result === null) return null;
|
|
3184
|
+
this.telemetryReporter?.log({
|
|
3185
|
+
name: "provider.redirect.resumed",
|
|
3186
|
+
stage: "redirect_resume",
|
|
3187
|
+
provider: "paypal",
|
|
3188
|
+
paymentMethodCategory: "paypal"
|
|
3189
|
+
});
|
|
3190
|
+
this.telemetryReporter?.performance({
|
|
3191
|
+
stage: "redirect_resume",
|
|
3192
|
+
durationMs: this.now() - startedAt,
|
|
3193
|
+
durationMode: "machine",
|
|
3194
|
+
provider: "paypal",
|
|
3195
|
+
paymentMethodCategory: "paypal"
|
|
3196
|
+
});
|
|
3197
|
+
if (result.status === "succeeded") {
|
|
3198
|
+
this.telemetryReporter?.terminal({
|
|
3199
|
+
outcome: "payment_succeeded",
|
|
3200
|
+
provider: "paypal",
|
|
3201
|
+
paymentMethodCategory: "paypal"
|
|
3202
|
+
});
|
|
3203
|
+
} else if (isExpectedDecline(result.error)) {
|
|
3204
|
+
this.telemetryReporter?.terminal({
|
|
3205
|
+
outcome: "payment_declined",
|
|
3206
|
+
provider: "paypal",
|
|
3207
|
+
paymentMethodCategory: "paypal"
|
|
3208
|
+
});
|
|
3209
|
+
} else if (result.error?.type === "validation_error") {
|
|
3210
|
+
this.telemetryReporter?.terminal({
|
|
3211
|
+
outcome: "validation_rejected",
|
|
3212
|
+
provider: "paypal",
|
|
3213
|
+
paymentMethodCategory: "paypal"
|
|
3214
|
+
});
|
|
3215
|
+
} else if (result.error) {
|
|
3216
|
+
this.telemetryReporter?.error({
|
|
3217
|
+
errorCode: "REDIRECT_RESUME_FAILED",
|
|
3218
|
+
stage: "redirect_resume",
|
|
3219
|
+
provider: "paypal",
|
|
3220
|
+
paymentMethodCategory: "paypal"
|
|
3221
|
+
});
|
|
3222
|
+
}
|
|
3223
|
+
return result;
|
|
3224
|
+
} catch (error) {
|
|
3225
|
+
this.telemetryReporter?.performance({
|
|
3226
|
+
stage: "redirect_resume",
|
|
3227
|
+
durationMs: this.now() - startedAt,
|
|
3228
|
+
durationMode: "machine",
|
|
3229
|
+
provider: "paypal",
|
|
3230
|
+
paymentMethodCategory: "paypal"
|
|
3231
|
+
});
|
|
3232
|
+
this.telemetryReporter?.error({
|
|
3233
|
+
errorCode: "REDIRECT_RESUME_FAILED",
|
|
3234
|
+
stage: "redirect_resume",
|
|
3235
|
+
provider: "paypal",
|
|
3236
|
+
paymentMethodCategory: "paypal"
|
|
3237
|
+
});
|
|
3238
|
+
throw error;
|
|
3239
|
+
}
|
|
1730
3240
|
}
|
|
1731
3241
|
/** Confirms a payment using the mounted elements. */
|
|
1732
3242
|
async confirmPayment(params) {
|
|
1733
|
-
|
|
3243
|
+
const started = this.now();
|
|
3244
|
+
const provider = telemetryProvider(this.provider.name);
|
|
3245
|
+
this.telemetryReporter?.log({
|
|
3246
|
+
name: "payment.processing.started",
|
|
3247
|
+
stage: "processing",
|
|
3248
|
+
provider,
|
|
3249
|
+
paymentMethodCategory: "unknown"
|
|
3250
|
+
});
|
|
3251
|
+
try {
|
|
3252
|
+
const result = await this.provider.confirmPayment(params);
|
|
3253
|
+
const durationMs = this.now() - started;
|
|
3254
|
+
this.telemetryReporter?.performance({
|
|
3255
|
+
stage: "processing",
|
|
3256
|
+
durationMs,
|
|
3257
|
+
durationMode: "machine",
|
|
3258
|
+
provider,
|
|
3259
|
+
paymentMethodCategory: "unknown"
|
|
3260
|
+
});
|
|
3261
|
+
if (result.status === "succeeded") {
|
|
3262
|
+
this.telemetryReporter?.terminal({
|
|
3263
|
+
outcome: "payment_succeeded",
|
|
3264
|
+
provider,
|
|
3265
|
+
paymentMethodCategory: "unknown"
|
|
3266
|
+
});
|
|
3267
|
+
} else if (isExpectedDecline(result.error)) {
|
|
3268
|
+
this.telemetryReporter?.terminal({
|
|
3269
|
+
outcome: "payment_declined",
|
|
3270
|
+
provider,
|
|
3271
|
+
paymentMethodCategory: "unknown"
|
|
3272
|
+
});
|
|
3273
|
+
} else if (result.error?.type === "validation_error") {
|
|
3274
|
+
this.telemetryReporter?.terminal({
|
|
3275
|
+
outcome: "validation_rejected",
|
|
3276
|
+
provider,
|
|
3277
|
+
paymentMethodCategory: "unknown"
|
|
3278
|
+
});
|
|
3279
|
+
} else if (result.status === "requires_action") {
|
|
3280
|
+
this.telemetryReporter?.terminal({
|
|
3281
|
+
outcome: "action_required",
|
|
3282
|
+
stage: "three_ds_handoff",
|
|
3283
|
+
provider,
|
|
3284
|
+
paymentMethodCategory: "unknown"
|
|
3285
|
+
});
|
|
3286
|
+
} else if (result.status === "failed" && result.error) {
|
|
3287
|
+
this.telemetryReporter?.error({
|
|
3288
|
+
errorCode: "PAYMENT_PROCESSING_FAILED",
|
|
3289
|
+
stage: "processing",
|
|
3290
|
+
provider,
|
|
3291
|
+
paymentMethodCategory: "unknown"
|
|
3292
|
+
});
|
|
3293
|
+
}
|
|
3294
|
+
if (result.status === "succeeded" || result.status === "failed") {
|
|
3295
|
+
this.telemetryReporter?.log({
|
|
3296
|
+
name: "payment.processing.completed",
|
|
3297
|
+
stage: "processing",
|
|
3298
|
+
provider,
|
|
3299
|
+
paymentMethodCategory: "unknown"
|
|
3300
|
+
});
|
|
3301
|
+
} else {
|
|
3302
|
+
this.telemetryReporter?.log({
|
|
3303
|
+
name: "operation.state_transition",
|
|
3304
|
+
stage: result.status === "requires_action" ? "three_ds_handoff" : "processing",
|
|
3305
|
+
provider,
|
|
3306
|
+
paymentMethodCategory: "unknown"
|
|
3307
|
+
});
|
|
3308
|
+
}
|
|
3309
|
+
return result;
|
|
3310
|
+
} catch (error) {
|
|
3311
|
+
this.telemetryReporter?.performance({
|
|
3312
|
+
stage: "processing",
|
|
3313
|
+
durationMs: this.now() - started,
|
|
3314
|
+
durationMode: "machine",
|
|
3315
|
+
provider,
|
|
3316
|
+
paymentMethodCategory: "unknown"
|
|
3317
|
+
});
|
|
3318
|
+
this.telemetryReporter?.error({
|
|
3319
|
+
errorCode: "PAYMENT_PROCESSING_FAILED",
|
|
3320
|
+
stage: "processing",
|
|
3321
|
+
provider,
|
|
3322
|
+
paymentMethodCategory: "unknown"
|
|
3323
|
+
});
|
|
3324
|
+
throw error;
|
|
3325
|
+
}
|
|
1734
3326
|
}
|
|
1735
3327
|
/**
|
|
1736
3328
|
* Retrieves a checkout session by ID via the billing API.
|
|
@@ -1749,9 +3341,7 @@ var FloPay = class {
|
|
|
1749
3341
|
{ param: "sessionId" }
|
|
1750
3342
|
);
|
|
1751
3343
|
}
|
|
1752
|
-
const
|
|
1753
|
-
const api = new PaymentAPI(apiUrl);
|
|
1754
|
-
const unified = await api.getUnifiedCheckoutSession(sessionId);
|
|
3344
|
+
const unified = await this.retrieveUnifiedSession(sessionId, billingApiUrl);
|
|
1755
3345
|
if (!unified.data.session) {
|
|
1756
3346
|
throw new FloPayError5("Session not found", "api_error");
|
|
1757
3347
|
}
|
|
@@ -1772,9 +3362,80 @@ var FloPay = class {
|
|
|
1772
3362
|
{ param: "sessionId" }
|
|
1773
3363
|
);
|
|
1774
3364
|
}
|
|
1775
|
-
const apiUrl =
|
|
1776
|
-
const
|
|
1777
|
-
|
|
3365
|
+
const apiUrl = resolveBillingApiUrl2(billingApiUrl ?? this.config.billingApiUrl);
|
|
3366
|
+
const started = this.now();
|
|
3367
|
+
this.telemetryReporter?.log({
|
|
3368
|
+
name: "session.read.started",
|
|
3369
|
+
stage: "session_read",
|
|
3370
|
+
requestCategory: "session_read"
|
|
3371
|
+
});
|
|
3372
|
+
let firstByteDuration;
|
|
3373
|
+
const api = createInstrumentedPaymentAPI(apiUrl, {
|
|
3374
|
+
now: () => this.telemetryReporter?.now() ?? telemetryNow(),
|
|
3375
|
+
onFirstByte: (durationMs) => {
|
|
3376
|
+
firstByteDuration = durationMs;
|
|
3377
|
+
},
|
|
3378
|
+
onRetry: (requestCategory, attempt) => {
|
|
3379
|
+
this.telemetryReporter?.log({
|
|
3380
|
+
name: "operation.retry",
|
|
3381
|
+
stage: requestCategory === "session_read" ? "session_read" : "processing",
|
|
3382
|
+
requestCategory,
|
|
3383
|
+
attempt
|
|
3384
|
+
});
|
|
3385
|
+
}
|
|
3386
|
+
});
|
|
3387
|
+
try {
|
|
3388
|
+
const result = await api.getUnifiedCheckoutSession(sessionId);
|
|
3389
|
+
if (firstByteDuration !== void 0) {
|
|
3390
|
+
this.telemetryReporter?.log({
|
|
3391
|
+
name: "session.request.first_byte",
|
|
3392
|
+
stage: "session_first_byte",
|
|
3393
|
+
requestCategory: "session_read",
|
|
3394
|
+
statusClass: "2xx"
|
|
3395
|
+
});
|
|
3396
|
+
this.telemetryReporter?.performance({
|
|
3397
|
+
stage: "session_first_byte",
|
|
3398
|
+
durationMs: firstByteDuration,
|
|
3399
|
+
durationMode: "machine",
|
|
3400
|
+
requestCategory: "session_read",
|
|
3401
|
+
statusClass: "2xx"
|
|
3402
|
+
});
|
|
3403
|
+
}
|
|
3404
|
+
this.telemetryReporter?.log({
|
|
3405
|
+
name: "session.request.completed",
|
|
3406
|
+
stage: "session_complete",
|
|
3407
|
+
requestCategory: "session_read",
|
|
3408
|
+
statusClass: "2xx"
|
|
3409
|
+
});
|
|
3410
|
+
this.telemetryReporter?.log({
|
|
3411
|
+
name: "checkout.data.ready",
|
|
3412
|
+
stage: "checkout_data_ready"
|
|
3413
|
+
});
|
|
3414
|
+
this.telemetryReporter?.performance({
|
|
3415
|
+
stage: "session_complete",
|
|
3416
|
+
durationMs: this.now() - started,
|
|
3417
|
+
durationMode: "machine",
|
|
3418
|
+
requestCategory: "session_read",
|
|
3419
|
+
statusClass: "2xx"
|
|
3420
|
+
});
|
|
3421
|
+
return result;
|
|
3422
|
+
} catch (error) {
|
|
3423
|
+
const statusCode = error instanceof FloPayError5 ? error.statusCode : void 0;
|
|
3424
|
+
this.telemetryReporter?.error({
|
|
3425
|
+
errorCode: error instanceof FloPayError5 && error.code === "checkout_processing_timeout" ? "REQUEST_TIMEOUT" : "NETWORK_REQUEST_FAILED",
|
|
3426
|
+
stage: "session_read",
|
|
3427
|
+
provider: "flo",
|
|
3428
|
+
paymentMethodCategory: "unknown"
|
|
3429
|
+
});
|
|
3430
|
+
this.telemetryReporter?.performance({
|
|
3431
|
+
stage: "session_complete",
|
|
3432
|
+
durationMs: this.now() - started,
|
|
3433
|
+
durationMode: "machine",
|
|
3434
|
+
requestCategory: "session_read",
|
|
3435
|
+
statusClass: statusCode ? `${Math.floor(statusCode / 100)}xx` : "network_error"
|
|
3436
|
+
});
|
|
3437
|
+
throw error;
|
|
3438
|
+
}
|
|
1778
3439
|
}
|
|
1779
3440
|
/**
|
|
1780
3441
|
* Returns the raw underlying provider instance (e.g. Stripe object).
|
|
@@ -1786,33 +3447,137 @@ var FloPay = class {
|
|
|
1786
3447
|
}
|
|
1787
3448
|
/** Tears down the SDK instance and releases resources. */
|
|
1788
3449
|
destroy() {
|
|
3450
|
+
this.telemetryReporter?.log({ name: "checkout.unmount", stage: "unmount" });
|
|
3451
|
+
this.telemetryReporter?.destroy();
|
|
3452
|
+
this.cardThreeDsStartedAt.clear();
|
|
1789
3453
|
this.currentElements?.destroy();
|
|
1790
3454
|
this.currentElements = null;
|
|
1791
3455
|
this.provider.destroy();
|
|
1792
3456
|
}
|
|
1793
3457
|
};
|
|
3458
|
+
function createInstrumentedFloPay(provider, config, reporter) {
|
|
3459
|
+
const InstrumentedFloPay = FloPay;
|
|
3460
|
+
return new InstrumentedFloPay(provider, config, reporter);
|
|
3461
|
+
}
|
|
1794
3462
|
|
|
1795
3463
|
// src/load.ts
|
|
1796
3464
|
var instanceCache = /* @__PURE__ */ new Map();
|
|
3465
|
+
function stableCacheValue(value) {
|
|
3466
|
+
if (Array.isArray(value)) return value.map(stableCacheValue);
|
|
3467
|
+
if (value && typeof value === "object") {
|
|
3468
|
+
return Object.fromEntries(
|
|
3469
|
+
Object.entries(value).filter(([, entry]) => entry !== void 0).sort(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => [key, stableCacheValue(entry)])
|
|
3470
|
+
);
|
|
3471
|
+
}
|
|
3472
|
+
return value;
|
|
3473
|
+
}
|
|
3474
|
+
function instanceCacheKey(publishableKey, options) {
|
|
3475
|
+
return JSON.stringify([
|
|
3476
|
+
publishableKey,
|
|
3477
|
+
resolveBillingApiUrl3(options?.billingApiUrl),
|
|
3478
|
+
options?.telemetry !== false,
|
|
3479
|
+
options?.locale ?? "auto",
|
|
3480
|
+
options?.apiVersion ?? null,
|
|
3481
|
+
stableCacheValue(options?.appearance ?? null)
|
|
3482
|
+
]);
|
|
3483
|
+
}
|
|
3484
|
+
var initializationCache = /* @__PURE__ */ new Map();
|
|
1797
3485
|
async function loadFloPay(publishableKey, options) {
|
|
1798
3486
|
if (!publishableKey) {
|
|
3487
|
+
const reporter = new TelemetryReporter({
|
|
3488
|
+
billingApiUrl: resolveBillingApiUrl3(options?.billingApiUrl),
|
|
3489
|
+
sdkVersion: SDK_VERSION4,
|
|
3490
|
+
enabled: options?.telemetry !== false
|
|
3491
|
+
});
|
|
3492
|
+
reporter.error({
|
|
3493
|
+
errorCode: "CONFIGURATION_INVALID",
|
|
3494
|
+
stage: "sdk_initialize",
|
|
3495
|
+
paymentMethodCategory: "unknown"
|
|
3496
|
+
});
|
|
3497
|
+
void reporter.flush().catch(() => {
|
|
3498
|
+
}).finally(() => reporter.destroy());
|
|
1799
3499
|
throw new FloPayError6(
|
|
1800
3500
|
"A publishable key is required to initialize FloPay.",
|
|
1801
3501
|
"validation_error",
|
|
1802
3502
|
{ param: "publishableKey" }
|
|
1803
3503
|
);
|
|
1804
3504
|
}
|
|
1805
|
-
const
|
|
1806
|
-
|
|
3505
|
+
const cacheKey = instanceCacheKey(publishableKey, options);
|
|
3506
|
+
const cached = instanceCache.get(cacheKey);
|
|
3507
|
+
if (cached) {
|
|
3508
|
+
if (options?.telemetry !== false) {
|
|
3509
|
+
getFloPayTelemetryBridge(cached)?.log({
|
|
3510
|
+
name: "sdk.cache.hit",
|
|
3511
|
+
stage: "sdk_initialize"
|
|
3512
|
+
});
|
|
3513
|
+
}
|
|
3514
|
+
return cached;
|
|
3515
|
+
}
|
|
3516
|
+
const initializing = initializationCache.get(cacheKey);
|
|
3517
|
+
if (initializing) return initializing;
|
|
1807
3518
|
const config = {
|
|
1808
|
-
|
|
1809
|
-
|
|
3519
|
+
...options,
|
|
3520
|
+
publishableKey
|
|
1810
3521
|
};
|
|
1811
|
-
const
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
3522
|
+
const initialization = (async () => {
|
|
3523
|
+
const reporter = new TelemetryReporter({
|
|
3524
|
+
billingApiUrl: resolveBillingApiUrl3(config.billingApiUrl),
|
|
3525
|
+
sdkVersion: SDK_VERSION4,
|
|
3526
|
+
enabled: config.telemetry !== false
|
|
3527
|
+
});
|
|
3528
|
+
const initializationStarted = reporter.now();
|
|
3529
|
+
reporter.log({ name: "sdk.initialize.started", stage: "sdk_initialize" });
|
|
3530
|
+
reporter.log({ name: "sdk.cache.miss", stage: "sdk_initialize" });
|
|
3531
|
+
reporter.log({
|
|
3532
|
+
name: "provider.load.started",
|
|
3533
|
+
stage: "provider_load",
|
|
3534
|
+
provider: "stripe"
|
|
3535
|
+
});
|
|
3536
|
+
const adapter = new StripeAdapter();
|
|
3537
|
+
try {
|
|
3538
|
+
await adapter.initialize(config);
|
|
3539
|
+
} catch (error) {
|
|
3540
|
+
reporter.error({
|
|
3541
|
+
errorCode: "SDK_INITIALIZATION_FAILED",
|
|
3542
|
+
stage: "sdk_initialize",
|
|
3543
|
+
provider: "stripe",
|
|
3544
|
+
paymentMethodCategory: "unknown"
|
|
3545
|
+
});
|
|
3546
|
+
reporter.destroy();
|
|
3547
|
+
throw error;
|
|
3548
|
+
}
|
|
3549
|
+
reporter.log({ name: "provider.ready", stage: "provider_ready", provider: "stripe" });
|
|
3550
|
+
reporter.log({
|
|
3551
|
+
name: "provider.availability.checked",
|
|
3552
|
+
stage: "provider_ready",
|
|
3553
|
+
provider: "stripe"
|
|
3554
|
+
});
|
|
3555
|
+
reporter.log({ name: "sdk.initialize.ready", stage: "sdk_initialize" });
|
|
3556
|
+
const initializationDuration = reporter.now() - initializationStarted;
|
|
3557
|
+
reporter.performance({
|
|
3558
|
+
stage: "sdk_initialize",
|
|
3559
|
+
durationMs: initializationDuration,
|
|
3560
|
+
durationMode: "machine",
|
|
3561
|
+
provider: "stripe"
|
|
3562
|
+
});
|
|
3563
|
+
reporter.performance({
|
|
3564
|
+
stage: "provider_ready",
|
|
3565
|
+
durationMs: initializationDuration,
|
|
3566
|
+
durationMode: "machine",
|
|
3567
|
+
provider: "stripe"
|
|
3568
|
+
});
|
|
3569
|
+
const instance = createInstrumentedFloPay(adapter, config, reporter);
|
|
3570
|
+
instanceCache.set(cacheKey, instance);
|
|
3571
|
+
return instance;
|
|
3572
|
+
})();
|
|
3573
|
+
initializationCache.set(cacheKey, initialization);
|
|
3574
|
+
try {
|
|
3575
|
+
return await initialization;
|
|
3576
|
+
} finally {
|
|
3577
|
+
if (initializationCache.get(cacheKey) === initialization) {
|
|
3578
|
+
initializationCache.delete(cacheKey);
|
|
3579
|
+
}
|
|
3580
|
+
}
|
|
1816
3581
|
}
|
|
1817
3582
|
|
|
1818
3583
|
// src/create-checkout-session.ts
|
|
@@ -1820,20 +3585,17 @@ import {
|
|
|
1820
3585
|
FloPayError as FloPayError7,
|
|
1821
3586
|
IDEMPOTENCY_IN_PROGRESS_CODE as IDEMPOTENCY_IN_PROGRESS_CODE2,
|
|
1822
3587
|
IDEMPOTENCY_KEY_HEADER as IDEMPOTENCY_KEY_HEADER2,
|
|
1823
|
-
SDK_VERSION as
|
|
3588
|
+
SDK_VERSION as SDK_VERSION5,
|
|
1824
3589
|
buildProductPayload as buildProductPayload2,
|
|
1825
3590
|
foldIntoProducts as foldIntoProducts2,
|
|
1826
3591
|
resolveIdempotencyKey as resolveIdempotencyKey2,
|
|
1827
3592
|
resolveSessionCurrency as resolveSessionCurrency2
|
|
1828
3593
|
} from "@flopay/shared";
|
|
1829
3594
|
var MAX_COUPON_CODES = 5;
|
|
1830
|
-
function readString2(value) {
|
|
1831
|
-
return typeof value === "string" && value.trim() ? value : void 0;
|
|
1832
|
-
}
|
|
1833
3595
|
function buildCheckoutSessionError(status, payload) {
|
|
1834
3596
|
const nested = payload?.error;
|
|
1835
|
-
const code =
|
|
1836
|
-
const message =
|
|
3597
|
+
const code = readErrorString(payload?.code) ?? readErrorString(nested?.code) ?? `http_${status}`;
|
|
3598
|
+
const message = readErrorString(payload?.message) ?? readErrorString(nested?.message) ?? defaultMessageForCode(code, status);
|
|
1837
3599
|
return new FloPayError7(message, "api_error", { code, statusCode: status });
|
|
1838
3600
|
}
|
|
1839
3601
|
function defaultMessageForCode(code, status) {
|
|
@@ -1846,7 +3608,7 @@ function defaultMessageForCode(code, status) {
|
|
|
1846
3608
|
return `Failed to create checkout session (HTTP ${status}).`;
|
|
1847
3609
|
}
|
|
1848
3610
|
}
|
|
1849
|
-
async function
|
|
3611
|
+
async function createCheckoutSessionCore(options, onFirstByte) {
|
|
1850
3612
|
const {
|
|
1851
3613
|
billingApiUrl,
|
|
1852
3614
|
checkoutBaseUrl,
|
|
@@ -1886,7 +3648,7 @@ async function createCheckoutSession(options) {
|
|
|
1886
3648
|
}
|
|
1887
3649
|
const payload = {
|
|
1888
3650
|
clientId,
|
|
1889
|
-
checkoutVersion:
|
|
3651
|
+
checkoutVersion: SDK_VERSION5,
|
|
1890
3652
|
successUrl,
|
|
1891
3653
|
cancelUrl,
|
|
1892
3654
|
currency: sessionCurrency,
|
|
@@ -1929,6 +3691,10 @@ async function createCheckoutSession(options) {
|
|
|
1929
3691
|
body: JSON.stringify(payload),
|
|
1930
3692
|
signal: controller.signal
|
|
1931
3693
|
});
|
|
3694
|
+
try {
|
|
3695
|
+
onFirstByte?.(response.status);
|
|
3696
|
+
} catch {
|
|
3697
|
+
}
|
|
1932
3698
|
status = response.status;
|
|
1933
3699
|
try {
|
|
1934
3700
|
body = await response.json();
|
|
@@ -1990,9 +3756,103 @@ async function createCheckoutSession(options) {
|
|
|
1990
3756
|
}
|
|
1991
3757
|
return { status };
|
|
1992
3758
|
}
|
|
3759
|
+
async function createCheckoutSession(options) {
|
|
3760
|
+
const telemetry = beginCreateSessionTelemetry(options);
|
|
3761
|
+
const recordFirstByte = createFirstByteRecorder(telemetry);
|
|
3762
|
+
try {
|
|
3763
|
+
const result = await createCheckoutSessionCore(options, recordFirstByte);
|
|
3764
|
+
completeCreateSessionTelemetry(telemetry, result);
|
|
3765
|
+
return result;
|
|
3766
|
+
} catch (error) {
|
|
3767
|
+
failCreateSessionTelemetry(telemetry.reporter, error);
|
|
3768
|
+
throw error;
|
|
3769
|
+
} finally {
|
|
3770
|
+
finishCreateSessionTelemetry(telemetry.reporter);
|
|
3771
|
+
}
|
|
3772
|
+
}
|
|
3773
|
+
function createFirstByteRecorder(telemetry) {
|
|
3774
|
+
let recorded = false;
|
|
3775
|
+
return (status) => {
|
|
3776
|
+
if (recorded) return;
|
|
3777
|
+
recorded = true;
|
|
3778
|
+
const statusClass = `${Math.floor(status / 100)}xx`;
|
|
3779
|
+
telemetry.reporter.log({
|
|
3780
|
+
name: "session.request.first_byte",
|
|
3781
|
+
stage: "session_first_byte",
|
|
3782
|
+
requestCategory: "session_create",
|
|
3783
|
+
statusClass
|
|
3784
|
+
});
|
|
3785
|
+
telemetry.reporter.performance({
|
|
3786
|
+
stage: "session_first_byte",
|
|
3787
|
+
durationMs: telemetry.reporter.now() - telemetry.startedAt,
|
|
3788
|
+
durationMode: "machine",
|
|
3789
|
+
requestCategory: "session_create",
|
|
3790
|
+
statusClass
|
|
3791
|
+
});
|
|
3792
|
+
};
|
|
3793
|
+
}
|
|
3794
|
+
function beginCreateSessionTelemetry(options) {
|
|
3795
|
+
const reporter = new TelemetryReporter({
|
|
3796
|
+
billingApiUrl: options.billingApiUrl,
|
|
3797
|
+
sdkVersion: SDK_VERSION5,
|
|
3798
|
+
enabled: options.telemetry !== false
|
|
3799
|
+
});
|
|
3800
|
+
const startedAt = reporter.now();
|
|
3801
|
+
reporter.log({
|
|
3802
|
+
name: "session.create.started",
|
|
3803
|
+
stage: "session_create",
|
|
3804
|
+
requestCategory: "session_create"
|
|
3805
|
+
});
|
|
3806
|
+
return { reporter, startedAt };
|
|
3807
|
+
}
|
|
3808
|
+
function completeCreateSessionTelemetry(telemetry, result) {
|
|
3809
|
+
const statusClass = `${Math.floor(result.status / 100)}xx`;
|
|
3810
|
+
telemetry.reporter.log({
|
|
3811
|
+
name: "session.request.completed",
|
|
3812
|
+
stage: "session_complete",
|
|
3813
|
+
requestCategory: "session_create",
|
|
3814
|
+
statusClass
|
|
3815
|
+
});
|
|
3816
|
+
telemetry.reporter.performance({
|
|
3817
|
+
stage: "session_complete",
|
|
3818
|
+
durationMs: telemetry.reporter.now() - telemetry.startedAt,
|
|
3819
|
+
durationMode: "machine",
|
|
3820
|
+
requestCategory: "session_create",
|
|
3821
|
+
statusClass
|
|
3822
|
+
});
|
|
3823
|
+
}
|
|
3824
|
+
function failCreateSessionTelemetry(reporter, error) {
|
|
3825
|
+
if (error instanceof FloPayError7 && error.type === "validation_error") {
|
|
3826
|
+
reporter.terminal({
|
|
3827
|
+
outcome: "validation_rejected",
|
|
3828
|
+
stage: "session_create",
|
|
3829
|
+
requestCategory: "session_create"
|
|
3830
|
+
});
|
|
3831
|
+
return;
|
|
3832
|
+
}
|
|
3833
|
+
const statusCode = error instanceof FloPayError7 ? error.statusCode : void 0;
|
|
3834
|
+
reporter.error({
|
|
3835
|
+
errorCode: error instanceof Error && error.name === "AbortError" ? "REQUEST_TIMEOUT" : error instanceof TypeError ? "NETWORK_REQUEST_FAILED" : "CHECKOUT_SESSION_CREATE_FAILED",
|
|
3836
|
+
stage: "session_create",
|
|
3837
|
+
requestCategory: "session_create",
|
|
3838
|
+
statusClass: error instanceof Error && error.name === "AbortError" ? "timeout" : statusCode ? `${Math.floor(statusCode / 100)}xx` : "network_error"
|
|
3839
|
+
});
|
|
3840
|
+
}
|
|
3841
|
+
function finishCreateSessionTelemetry(reporter) {
|
|
3842
|
+
void reporter.flush().catch(() => {
|
|
3843
|
+
}).finally(() => reporter.destroy());
|
|
3844
|
+
}
|
|
1993
3845
|
async function createCheckoutSessionWithRetries(options) {
|
|
1994
3846
|
const { maxRetries = 3, ...sessionOptions } = options;
|
|
3847
|
+
const telemetry = beginCreateSessionTelemetry(options);
|
|
3848
|
+
const recordFirstByte = createFirstByteRecorder(telemetry);
|
|
1995
3849
|
if (!Number.isFinite(maxRetries) || !Number.isInteger(maxRetries) || maxRetries <= 0) {
|
|
3850
|
+
telemetry.reporter.terminal({
|
|
3851
|
+
outcome: "validation_rejected",
|
|
3852
|
+
stage: "session_create",
|
|
3853
|
+
requestCategory: "session_create"
|
|
3854
|
+
});
|
|
3855
|
+
finishCreateSessionTelemetry(telemetry.reporter);
|
|
1996
3856
|
throw new Error("Number of retries must be greater than 0");
|
|
1997
3857
|
}
|
|
1998
3858
|
const attemptOptions = {
|
|
@@ -2002,18 +3862,31 @@ async function createCheckoutSessionWithRetries(options) {
|
|
|
2002
3862
|
let lastErr;
|
|
2003
3863
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
2004
3864
|
try {
|
|
2005
|
-
|
|
3865
|
+
const result = await createCheckoutSessionCore(attemptOptions, recordFirstByte);
|
|
3866
|
+
completeCreateSessionTelemetry(telemetry, result);
|
|
3867
|
+
finishCreateSessionTelemetry(telemetry.reporter);
|
|
3868
|
+
return result;
|
|
2006
3869
|
} catch (err) {
|
|
2007
3870
|
lastErr = err;
|
|
2008
3871
|
const isTransportAbort = err instanceof Error && err.name === "AbortError";
|
|
2009
3872
|
const isInProgressReplay = err instanceof FloPayError7 && err.code === IDEMPOTENCY_IN_PROGRESS_CODE2;
|
|
2010
3873
|
if ((isTransportAbort || isInProgressReplay) && attempt < maxRetries) {
|
|
3874
|
+
telemetry.reporter.log({
|
|
3875
|
+
name: "operation.retry",
|
|
3876
|
+
stage: "session_create",
|
|
3877
|
+
requestCategory: "session_create",
|
|
3878
|
+
attempt: attempt + 1
|
|
3879
|
+
});
|
|
2011
3880
|
await new Promise((r) => setTimeout(r, 100 * Math.pow(2, attempt)));
|
|
2012
3881
|
continue;
|
|
2013
3882
|
}
|
|
3883
|
+
failCreateSessionTelemetry(telemetry.reporter, err);
|
|
3884
|
+
finishCreateSessionTelemetry(telemetry.reporter);
|
|
2014
3885
|
throw err;
|
|
2015
3886
|
}
|
|
2016
3887
|
}
|
|
3888
|
+
failCreateSessionTelemetry(telemetry.reporter, lastErr);
|
|
3889
|
+
finishCreateSessionTelemetry(telemetry.reporter);
|
|
2017
3890
|
throw lastErr ?? new Error("Unknown error during checkout session creation");
|
|
2018
3891
|
}
|
|
2019
3892
|
export {
|