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