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