@flopay/js 1.4.1 → 1.4.2

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/dist/index.cjs CHANGED
@@ -39,1065 +39,524 @@ var import_shared7 = require("@flopay/shared");
39
39
 
40
40
  // src/stripe-adapter.ts
41
41
  var import_stripe_js = require("@stripe/stripe-js");
42
- var import_shared = require("@flopay/shared");
42
+ var import_shared3 = require("@flopay/shared");
43
43
 
44
- // src/card-three-ds.ts
45
- var CARD_THREE_DS_ATTEMPT_TTL_MS = 15 * 6e4;
46
- var MAX_CARD_THREE_DS_ATTEMPTS = 32;
44
+ // src/payment-api.ts
45
+ var import_shared2 = require("@flopay/shared");
47
46
 
48
- // src/stripe-adapter.ts
49
- function toStripeElementType(type) {
50
- const map = {
51
- payment: "payment",
52
- card: "card",
53
- cardNumber: "cardNumber",
54
- cardExpiry: "cardExpiry",
55
- cardCvc: "cardCvc",
56
- address: "address"
57
- };
58
- return map[type];
47
+ // src/api-error.ts
48
+ function readErrorString(value) {
49
+ return typeof value === "string" && value.trim() ? value : void 0;
59
50
  }
60
- function toStripeAppearanceTheme(theme) {
61
- switch (theme) {
62
- case "night":
63
- return "night";
64
- case "flat":
65
- return "flat";
66
- // 'default', 'none', undefined, or any unexpected value → Stripe's baseline.
67
- default:
68
- return "stripe";
51
+ function readErrorMessage(value) {
52
+ if (typeof value === "string") return value.trim() ? value : void 0;
53
+ if (Array.isArray(value)) {
54
+ const joined = value.filter((entry) => typeof entry === "string" && entry.trim().length > 0).join("; ");
55
+ return joined || void 0;
69
56
  }
57
+ return void 0;
70
58
  }
71
- function cardThreeDsNow() {
72
- return globalThis.performance?.now() ?? Date.now();
73
- }
74
- function wrapStripeElement(stripeElement) {
75
- const el = stripeElement;
76
- return {
77
- mount(container) {
78
- el.mount(container);
79
- },
80
- unmount() {
81
- el.unmount();
82
- },
83
- update(options) {
84
- el.update(options);
85
- },
86
- on(event, handler) {
87
- el["on"]?.(event, handler);
88
- },
89
- off(event, handler) {
90
- el["off"]?.(event, handler);
91
- },
92
- destroy() {
93
- el.destroy();
94
- }
95
- };
96
- }
97
- function toStripeBillingDetails(billing) {
98
- return {
99
- billing_details: {
100
- ...billing.email ? { email: billing.email } : {},
101
- ...billing.name ? { name: billing.name } : {},
102
- ...billing.address ? {
103
- address: {
104
- ...billing.address.country ? { country: billing.address.country } : {},
105
- ...billing.address.postal_code ? { postal_code: billing.address.postal_code } : {},
106
- ...billing.address.city ? { city: billing.address.city } : {},
107
- ...billing.address.line1 ? { line1: billing.address.line1 } : {},
108
- ...billing.address.line2 ? { line2: billing.address.line2 } : {},
109
- ...billing.address.state ? { state: billing.address.state } : {}
110
- }
111
- } : {}
112
- }
113
- };
59
+
60
+ // src/session-display-cache.ts
61
+ var STORAGE_KEY_PREFIX = "flopay_session_display:";
62
+ var DEFAULT_TTL_MS = 60 * 60 * 1e3;
63
+ var memoryStore = /* @__PURE__ */ new Map();
64
+ function storageKey(sessionId) {
65
+ return `${STORAGE_KEY_PREFIX}${sessionId}`;
114
66
  }
115
- var StripeAdapter = class {
116
- constructor() {
117
- this.name = "stripe";
118
- this.stripe = null;
119
- this.elements = null;
120
- this.pendingCardThreeDsAttempts = /* @__PURE__ */ new Map();
121
- this.cardThreeDsAttemptSequence = 0;
122
- // Serialized appearance currently applied to `this.elements`. Used to detect
123
- // when consumers swap themes mid-session so we can live-update the Stripe
124
- // Elements group instead of returning a stale-styled cache. `null` while no
125
- // elements group exists.
126
- this.appliedAppearanceKey = null;
67
+ function getSessionStorage() {
68
+ if (typeof window === "undefined") return null;
69
+ try {
70
+ return window.sessionStorage;
71
+ } catch {
72
+ return null;
127
73
  }
128
- async initialize(config) {
129
- if (typeof window === "undefined") {
74
+ }
75
+ function cacheSessionDisplayData(sessionId, data, options) {
76
+ if (!sessionId) return;
77
+ const ttl = options?.ttlMs ?? DEFAULT_TTL_MS;
78
+ const entry = { data, expiresAt: Date.now() + ttl };
79
+ const storage = getSessionStorage();
80
+ if (storage) {
81
+ try {
82
+ storage.setItem(storageKey(sessionId), JSON.stringify(entry));
130
83
  return;
84
+ } catch {
131
85
  }
132
- const stripe = await (0, import_stripe_js.loadStripe)(config.publishableKey, {
133
- locale: config.locale ?? "auto"
134
- });
135
- if (!stripe) {
136
- throw new import_shared.FloPayError(
137
- "Failed to initialize Stripe. Check your publishable key.",
138
- "authentication_error"
139
- );
140
- }
141
- this.stripe = stripe;
142
86
  }
143
- /** Lazily creates the Stripe Elements group for the given options. */
144
- getElements(options) {
145
- if (!this.stripe) {
146
- throw new import_shared.FloPayError(
147
- "StripeAdapter not initialized. Call initialize() first.",
148
- "api_error"
149
- );
150
- }
151
- const stripeAppearance = options?.appearance ? {
152
- theme: toStripeAppearanceTheme(options.appearance.theme),
153
- variables: options.appearance.variables,
154
- rules: options.appearance.rules
155
- } : void 0;
156
- const nextAppearanceKey = stripeAppearance ? JSON.stringify(stripeAppearance) : null;
157
- if (!this.elements) {
158
- let elementsOptions;
159
- const deferredAmount = options?.amount ?? 0;
160
- const deferredCurrency = (options?.currency ?? "usd").toLowerCase();
161
- const paymentMethodCreation = options?.paymentMethodCreation ?? "manual";
162
- if (options?.clientSecret) {
163
- elementsOptions = { clientSecret: options.clientSecret };
164
- } else if (deferredAmount > 0) {
165
- elementsOptions = {
166
- mode: "payment",
167
- amount: deferredAmount,
168
- currency: deferredCurrency,
169
- paymentMethodCreation
170
- };
171
- if (options?.setupFutureUsage) {
172
- elementsOptions["setupFutureUsage"] = options.setupFutureUsage;
87
+ memoryStore.set(sessionId, entry);
88
+ }
89
+ function getSessionDisplayData(sessionId) {
90
+ if (!sessionId) return null;
91
+ const storage = getSessionStorage();
92
+ if (storage) {
93
+ try {
94
+ const raw = storage.getItem(storageKey(sessionId));
95
+ if (raw) {
96
+ const entry = JSON.parse(raw);
97
+ if (entry && typeof entry.expiresAt === "number" && entry.expiresAt > Date.now()) {
98
+ return entry.data;
173
99
  }
174
- } else {
175
- elementsOptions = {
176
- mode: "setup",
177
- currency: deferredCurrency,
178
- paymentMethodCreation
179
- };
180
- }
181
- if (stripeAppearance) {
182
- elementsOptions["appearance"] = stripeAppearance;
100
+ storage.removeItem(storageKey(sessionId));
183
101
  }
184
- this.elements = this.stripe.elements(elementsOptions);
185
- this.appliedAppearanceKey = nextAppearanceKey;
186
- } else if (nextAppearanceKey !== this.appliedAppearanceKey) {
187
- this.elements.update({
188
- appearance: stripeAppearance ?? {}
189
- });
190
- this.appliedAppearanceKey = nextAppearanceKey;
102
+ } catch {
191
103
  }
192
- return this.elements;
193
104
  }
194
- async createElement(type, options) {
195
- const elements = this.getElements(options);
196
- const stripeType = toStripeElementType(type);
197
- const elementOptions = {};
198
- if (options.layout) {
199
- elementOptions["layout"] = options.layout;
200
- }
201
- if (options.defaultValues) {
202
- elementOptions["defaultValues"] = options.defaultValues;
203
- }
204
- if (options.readOnly) {
205
- elementOptions["readOnly"] = options.readOnly;
206
- }
207
- if (options.mode) {
208
- elementOptions["mode"] = options.mode;
209
- }
210
- if (options.style) {
211
- elementOptions["style"] = options.style;
105
+ const memEntry = memoryStore.get(sessionId);
106
+ if (memEntry) {
107
+ if (memEntry.expiresAt > Date.now()) {
108
+ return memEntry.data;
212
109
  }
213
- const stripeElement = elements.create(stripeType, elementOptions);
214
- return wrapStripeElement(stripeElement);
215
- }
216
- getElement(type) {
217
- if (!this.elements) return null;
218
- const stripeType = toStripeElementType(type);
219
- const existing = this.elements.getElement(stripeType);
220
- if (!existing) return null;
221
- return wrapStripeElement(existing);
110
+ memoryStore.delete(sessionId);
222
111
  }
223
- async submitElements() {
224
- if (!this.stripe || !this.elements) {
225
- return { error: new import_shared.FloPayError("Stripe not initialized", "api_error") };
226
- }
227
- const { error } = await this.elements.submit();
228
- if (error) {
229
- return {
230
- error: new import_shared.FloPayError(error.message ?? "Validation failed", "validation_error")
231
- };
112
+ return null;
113
+ }
114
+ function clearSessionDisplayData(sessionId) {
115
+ if (!sessionId) return;
116
+ memoryStore.delete(sessionId);
117
+ const storage = getSessionStorage();
118
+ if (storage) {
119
+ try {
120
+ storage.removeItem(storageKey(sessionId));
121
+ } catch {
232
122
  }
233
- return {};
234
123
  }
235
- async createPaymentMethod(billingDetails) {
236
- if (!this.stripe || !this.elements) {
237
- return {
238
- paymentMethodId: null,
239
- error: new import_shared.FloPayError("Stripe not initialized", "api_error")
240
- };
241
- }
242
- const cardNumberEl = this.elements.getElement("cardNumber");
243
- const stripeBilling = billingDetails ? toStripeBillingDetails(billingDetails) : {};
244
- const { error, paymentMethod } = cardNumberEl ? await this.stripe.createPaymentMethod({
245
- type: "card",
246
- card: cardNumberEl,
247
- ...stripeBilling
248
- }) : await this.stripe.createPaymentMethod({
249
- elements: this.elements,
250
- ...stripeBilling
251
- });
252
- if (error) {
253
- return {
254
- paymentMethodId: null,
255
- error: new import_shared.FloPayError(
256
- error.message ?? "Failed to create payment method",
257
- "api_error",
258
- { code: error.code }
259
- )
260
- };
124
+ }
125
+
126
+ // src/telemetry-reporter.ts
127
+ var import_shared = require("@flopay/shared");
128
+ var TELEMETRY_PATH = "/v1/sdk-telemetry/events";
129
+ var MAX_BATCH_SIZE = 16;
130
+ var MAX_QUEUE_SIZE = 64;
131
+ var UPLOAD_TIMEOUT_MS = 1500;
132
+ var ERROR_DEDUPLICATION_WINDOW_MS = 1e3;
133
+ var MAX_REPORTED_FAILURES = 64;
134
+ var DEDUPLICATION_EVENT_ID = "00000000-0000-4000-8000-000000000000";
135
+ var EVENT_BUDGETS = {
136
+ technical_error: 8,
137
+ lifecycle: 32,
138
+ expected_outcome: 32,
139
+ performance: 24
140
+ };
141
+ function createUuidV4() {
142
+ try {
143
+ return globalThis.crypto.randomUUID();
144
+ } catch {
145
+ const bytes = new Uint8Array(16);
146
+ try {
147
+ globalThis.crypto.getRandomValues(bytes);
148
+ } catch {
149
+ for (let index = 0; index < bytes.length; index += 1) {
150
+ bytes[index] = Math.floor(Math.random() * 256);
151
+ }
261
152
  }
262
- return { paymentMethodId: paymentMethod.id };
153
+ bytes[6] = bytes[6] & 15 | 64;
154
+ bytes[8] = bytes[8] & 63 | 128;
155
+ const hex = [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
156
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
263
157
  }
264
- async confirmCardPayment(params) {
265
- if (!this.stripe) {
266
- return {
267
- status: "failed",
268
- error: new import_shared.FloPayError("Stripe not initialized", "api_error")
269
- };
158
+ }
159
+ function bodyByteLength(body) {
160
+ try {
161
+ return new TextEncoder().encode(body).byteLength;
162
+ } catch {
163
+ return body.length;
164
+ }
165
+ }
166
+ function failureDeduplicationKey(event) {
167
+ return JSON.stringify([
168
+ event.code,
169
+ event.stage,
170
+ event.provider,
171
+ event.attempt,
172
+ event.statusClass,
173
+ event.requestCategory,
174
+ event.paymentMethodCategory,
175
+ event.checkoutMode,
176
+ event.layout
177
+ ]);
178
+ }
179
+ function telemetryNow() {
180
+ return globalThis.performance?.now() ?? 0;
181
+ }
182
+ var TelemetryReporter = class {
183
+ constructor(options) {
184
+ this.ingestionDisabled = false;
185
+ this.queue = [];
186
+ this.sequence = 0;
187
+ this.flushTimer = null;
188
+ this.flushInFlight = null;
189
+ this.reportedFailures = /* @__PURE__ */ new Map();
190
+ this.checkoutContext = {};
191
+ this.checkoutStartedAt = null;
192
+ this.destroyed = false;
193
+ this.pageExitHandler = () => {
194
+ this.drainQueue();
195
+ };
196
+ this.visibilityHandler = () => {
197
+ if (document.visibilityState === "hidden") void this.flush();
198
+ };
199
+ this.eventCounts = {
200
+ technical_error: 0,
201
+ lifecycle: 0,
202
+ expected_outcome: 0,
203
+ performance: 0
204
+ };
205
+ this.endpoint = `${options.billingApiUrl.replace(/\/+$/, "")}${TELEMETRY_PATH}`;
206
+ this.sdkPackage = options.sdkPackage ?? "@flopay/js";
207
+ this.sdkVersion = options.sdkVersion;
208
+ this.correlationId = createUuidV4();
209
+ this.merchantEnabled = options.enabled !== false;
210
+ this.clock = options.clock ?? telemetryNow;
211
+ this.browserTransportAvailable = typeof window !== "undefined" && typeof document !== "undefined";
212
+ if (this.browserTransportAvailable) {
213
+ window.addEventListener("pagehide", this.pageExitHandler);
214
+ document.addEventListener("visibilitychange", this.visibilityHandler);
270
215
  }
271
- if ((0, import_shared.isSetupIntentClientSecret)(params.clientSecret)) {
272
- const { error: setupError, setupIntent } = await this.stripe.confirmCardSetup(
273
- params.clientSecret,
274
- { payment_method: params.paymentMethodId }
275
- );
276
- if (setupError) {
277
- return this.withCardThreeDsLifecycle(params, {
278
- status: "failed",
279
- error: new import_shared.FloPayError(
280
- setupError.message ?? "Payment failed",
281
- "api_error",
282
- { code: setupError.code, declineCode: setupError.decline_code }
283
- )
284
- });
285
- }
286
- return this.withCardThreeDsLifecycle(params, {
287
- status: setupIntent?.status ?? "failed",
288
- paymentIntentId: setupIntent?.id,
289
- paymentMethodId: this.extractPaymentMethodId(setupIntent?.payment_method)
216
+ }
217
+ log(input) {
218
+ if (!this.canCollect()) return;
219
+ this.enqueue((0, import_shared.buildTelemetryLogEvent)({
220
+ ...this.checkoutContext,
221
+ ...input,
222
+ eventId: createUuidV4(),
223
+ sequence: this.sequence++
224
+ }));
225
+ }
226
+ error(input) {
227
+ if (!this.canCollect()) return;
228
+ const normalizedFailure = (0, import_shared.buildTelemetryErrorEvent)({
229
+ ...this.checkoutContext,
230
+ ...input,
231
+ eventId: DEDUPLICATION_EVENT_ID,
232
+ sequence: 0
233
+ });
234
+ const deduplicationKey = failureDeduplicationKey(normalizedFailure);
235
+ const now = this.now();
236
+ this.pruneReportedFailures(now);
237
+ const previouslyReportedAt = this.reportedFailures.get(deduplicationKey);
238
+ if (previouslyReportedAt !== void 0 && now >= previouslyReportedAt && now - previouslyReportedAt < ERROR_DEDUPLICATION_WINDOW_MS) {
239
+ this.log({
240
+ name: "operation.deduplicated",
241
+ stage: input.stage,
242
+ provider: input.provider,
243
+ paymentMethodCategory: input.paymentMethodCategory,
244
+ attempt: input.attempt
290
245
  });
246
+ return;
291
247
  }
292
- const { error, paymentIntent } = await this.stripe.confirmCardPayment(
293
- params.clientSecret,
294
- { payment_method: params.paymentMethodId }
295
- );
296
- if (error) {
297
- return this.withCardThreeDsLifecycle(params, {
298
- status: "failed",
299
- error: new import_shared.FloPayError(
300
- error.message ?? "Payment failed",
301
- "api_error",
302
- { code: error.code, declineCode: error.decline_code }
303
- )
248
+ this.rememberReportedFailure(deduplicationKey, now);
249
+ this.enqueue((0, import_shared.buildTelemetryErrorEvent)({
250
+ ...this.checkoutContext,
251
+ ...input,
252
+ eventId: createUuidV4(),
253
+ sequence: this.sequence++
254
+ }));
255
+ }
256
+ performance(input) {
257
+ if (!this.canCollect()) return;
258
+ this.enqueue((0, import_shared.buildTelemetryPerformanceEvent)({
259
+ ...this.checkoutContext,
260
+ ...input,
261
+ eventId: createUuidV4(),
262
+ sequence: this.sequence++
263
+ }));
264
+ }
265
+ terminal(input) {
266
+ if (!this.canCollect()) return;
267
+ this.enqueue((0, import_shared.buildTelemetryTerminalEvent)({
268
+ ...this.checkoutContext,
269
+ ...input,
270
+ eventId: createUuidV4(),
271
+ sequence: this.sequence++
272
+ }));
273
+ if (input.outcome !== "action_required" && this.checkoutStartedAt !== null) {
274
+ const checkoutStartedAt = this.checkoutStartedAt;
275
+ this.checkoutStartedAt = null;
276
+ this.performance({
277
+ stage: "total_journey",
278
+ durationMs: Math.max(0, this.now() - checkoutStartedAt),
279
+ durationMode: "total",
280
+ provider: input.provider,
281
+ paymentMethodCategory: input.paymentMethodCategory
304
282
  });
305
283
  }
306
- return this.withCardThreeDsLifecycle(params, {
307
- status: paymentIntent?.status ?? "failed",
308
- paymentIntentId: paymentIntent?.id,
309
- paymentMethodId: this.extractPaymentMethodId(paymentIntent?.payment_method)
310
- });
311
284
  }
312
- /**
313
- * Bind provider-observed 3DS milestones to the exact confirmation context.
314
- * The sensitive client secret/payment method pair stays only in this private,
315
- * in-memory key; callers and telemetry receive an unrelated opaque id.
316
- */
317
- withCardThreeDsLifecycle(params, result) {
318
- const contextKey = `${params.clientSecret}\0${params.paymentMethodId}`;
319
- const now = cardThreeDsNow();
320
- this.pruneExpiredCardThreeDsAttempts(now);
321
- if (result.status === "requires_action") {
322
- let attempt2 = this.pendingCardThreeDsAttempts.get(contextKey);
323
- if (!attempt2) {
324
- while (this.pendingCardThreeDsAttempts.size >= MAX_CARD_THREE_DS_ATTEMPTS) {
325
- const oldestContextKey = this.pendingCardThreeDsAttempts.keys().next().value;
326
- if (oldestContextKey === void 0) break;
327
- this.pendingCardThreeDsAttempts.delete(oldestContextKey);
328
- }
329
- attempt2 = {
330
- attemptId: `card_3ds_${this.cardThreeDsAttemptSequence++}`,
331
- startedAt: now
332
- };
333
- this.pendingCardThreeDsAttempts.set(contextKey, attempt2);
334
- }
335
- return { ...result, threeDs: { attemptId: attempt2.attemptId, status: "handoff" } };
285
+ /** @internal Read the reporter's monotonic clock without Resource Timing. */
286
+ now() {
287
+ try {
288
+ return this.clock();
289
+ } catch {
290
+ return telemetryNow();
336
291
  }
337
- const attempt = this.pendingCardThreeDsAttempts.get(contextKey);
338
- if (!attempt) return result;
339
- this.pendingCardThreeDsAttempts.delete(contextKey);
340
- return {
341
- ...result,
342
- threeDs: {
343
- attemptId: attempt.attemptId,
344
- status: result.error || result.status === "failed" ? "failed" : "returned"
345
- }
346
- };
347
292
  }
348
- pruneExpiredCardThreeDsAttempts(now) {
349
- for (const [contextKey, attempt] of this.pendingCardThreeDsAttempts) {
350
- if (now - attempt.startedAt >= CARD_THREE_DS_ATTEMPT_TTL_MS) {
351
- this.pendingCardThreeDsAttempts.delete(contextKey);
293
+ pruneReportedFailures(now) {
294
+ for (const [key, reportedAt] of this.reportedFailures) {
295
+ if (now < reportedAt || now - reportedAt >= ERROR_DEDUPLICATION_WINDOW_MS) {
296
+ this.reportedFailures.delete(key);
352
297
  }
353
298
  }
354
299
  }
355
- async confirmPayment(params) {
356
- if (!this.stripe || !this.elements) {
357
- throw new import_shared.FloPayError(
358
- "StripeAdapter not initialized or no elements created.",
359
- "api_error"
360
- );
361
- }
362
- const billing = params.billingDetails;
363
- const paymentMethodData = billing ? toStripeBillingDetails(billing) : void 0;
364
- const { error, paymentIntent } = await this.stripe.confirmPayment({
365
- elements: this.elements,
366
- clientSecret: params.clientSecret,
367
- confirmParams: {
368
- return_url: params.returnUrl ?? window.location.href,
369
- ...paymentMethodData ? { payment_method_data: paymentMethodData } : {}
370
- },
371
- redirect: "if_required"
372
- });
373
- if (error) {
374
- return {
375
- status: "failed",
376
- error: new import_shared.FloPayError(
377
- error.message ?? "Payment failed",
378
- "api_error",
379
- {
380
- code: error.code,
381
- declineCode: error.decline_code
382
- }
383
- )
384
- };
385
- }
386
- if (!paymentIntent) {
387
- return { status: "failed", error: new import_shared.FloPayError("No payment intent returned", "api_error") };
300
+ rememberReportedFailure(key, reportedAt) {
301
+ while (this.reportedFailures.size >= MAX_REPORTED_FAILURES) {
302
+ const oldest = this.reportedFailures.keys().next();
303
+ if (oldest.done) break;
304
+ this.reportedFailures.delete(oldest.value);
388
305
  }
389
- const statusMap = {
390
- succeeded: "succeeded",
391
- processing: "processing",
392
- requires_action: "requires_action",
393
- requires_payment_method: "failed",
394
- canceled: "failed"
306
+ this.reportedFailures.set(key, reportedAt);
307
+ }
308
+ /** @internal Add closed checkout dimensions to subsequent SDK events. */
309
+ setCheckoutContext(context) {
310
+ this.checkoutContext = {
311
+ checkoutMode: context.checkoutMode,
312
+ layout: context.layout
395
313
  };
396
- return {
397
- status: statusMap[paymentIntent.status] ?? "failed",
398
- paymentIntentId: paymentIntent.id,
399
- paymentMethodId: this.extractPaymentMethodId(paymentIntent.payment_method)
314
+ }
315
+ /** @internal Start a fresh checkout budget, dedupe window, and total span. */
316
+ beginCheckout(context = {}) {
317
+ if (!this.canCollect()) return 0;
318
+ this.drainQueue();
319
+ this.setCheckoutContext(context);
320
+ this.sequence = 0;
321
+ this.reportedFailures.clear();
322
+ this.eventCounts = {
323
+ technical_error: 0,
324
+ lifecycle: 0,
325
+ expected_outcome: 0,
326
+ performance: 0
400
327
  };
328
+ this.checkoutStartedAt = this.now();
329
+ return this.checkoutStartedAt;
401
330
  }
402
- extractPaymentMethodId(paymentMethod) {
403
- if (typeof paymentMethod === "string" && paymentMethod.startsWith("pm_")) {
404
- return paymentMethod;
405
- }
406
- if (paymentMethod && typeof paymentMethod === "object" && typeof paymentMethod.id === "string") {
407
- return paymentMethod.id;
331
+ enqueue(event) {
332
+ if (!this.canCollect()) return;
333
+ if (this.queue.length >= MAX_QUEUE_SIZE || this.eventCounts[event.class] >= EVENT_BUDGETS[event.class]) return;
334
+ this.eventCounts[event.class] += 1;
335
+ this.queue.push(event);
336
+ if (this.queue.length >= MAX_BATCH_SIZE) {
337
+ void this.flush();
338
+ return;
408
339
  }
409
- return void 0;
340
+ this.scheduleFlush();
410
341
  }
411
- async confirmPayPalPayment(params) {
412
- if (!this.stripe) {
413
- return { status: "failed", error: new import_shared.FloPayError("Stripe not initialized", "api_error") };
414
- }
415
- const baseUrl = params.billingApiUrl.replace(/\/+$/, "");
416
- if (this.elements) {
417
- const { error: submitError } = await this.elements.submit();
418
- if (submitError) {
419
- return {
420
- status: "failed",
421
- error: new import_shared.FloPayError(
422
- submitError.message ?? "PayPal payment failed",
423
- "validation_error",
424
- { code: submitError.code }
425
- )
426
- };
427
- }
428
- }
429
- const intentHeaders = { "Content-Type": "application/json" };
430
- if (params.nonce) intentHeaders["x-checkout-session-token"] = params.nonce;
431
- const intentResponse = await fetch(`${baseUrl}/v1/checkouts/payments/intents`, {
432
- method: "POST",
433
- headers: intentHeaders,
434
- body: JSON.stringify({
435
- sessionId: params.sessionId,
436
- email: params.email,
437
- paymentMethodType: "paypal",
438
- isPaypal: "true"
439
- })
440
- });
441
- if (!intentResponse.ok) {
442
- return { status: "failed", error: new import_shared.FloPayError("Failed to create PayPal payment intent", "api_error") };
443
- }
444
- const intentJson = await intentResponse.json();
445
- const intentClientSecret = intentJson.data?.id;
446
- if (!intentClientSecret) {
447
- return { status: "failed", error: new import_shared.FloPayError("No client_secret in response", "api_error") };
448
- }
449
- const { error } = await this.stripe.confirmPayment({
450
- clientSecret: intentClientSecret,
451
- elements: this.elements ?? void 0,
452
- confirmParams: { return_url: params.returnUrl }
342
+ canCollect() {
343
+ return this.browserTransportAvailable && this.merchantEnabled && !this.ingestionDisabled && !this.destroyed;
344
+ }
345
+ /** Flush one bounded batch. Failures are intentionally dropped. */
346
+ async flush() {
347
+ if (this.flushInFlight) return this.flushInFlight;
348
+ if (!this.browserTransportAvailable || this.destroyed || this.ingestionDisabled || this.queue.length === 0) return;
349
+ this.clearFlushTimer();
350
+ const events = this.queue.splice(0, MAX_BATCH_SIZE);
351
+ this.flushInFlight = this.sendBatch(events).finally(() => {
352
+ this.flushInFlight = null;
353
+ if (this.queue.length > 0) this.scheduleFlush();
453
354
  });
454
- if (error) {
455
- return {
456
- status: "failed",
457
- error: new import_shared.FloPayError(error.message ?? "PayPal payment failed", "api_error", { code: error.code })
458
- };
459
- }
460
- return {
461
- status: "processing"
462
- };
355
+ return this.flushInFlight;
463
356
  }
464
- async resumePayPalPayment() {
465
- if (!this.stripe || typeof window === "undefined") return null;
466
- const params = new URLSearchParams(window.location.search);
467
- const paymentIntentId = params.get("payment_intent");
468
- const clientSecret = params.get("payment_intent_client_secret");
469
- const redirectStatus = params.get("redirect_status");
470
- if (!paymentIntentId || !clientSecret) return null;
471
- if (redirectStatus === "failed") {
472
- return {
473
- status: "failed",
474
- error: new import_shared.FloPayError("PayPal payment was declined. Please try again.", "api_error")
475
- };
476
- }
477
- const { paymentIntent, error } = await this.stripe.retrievePaymentIntent(clientSecret);
478
- if (error) {
479
- return {
480
- status: "failed",
481
- error: new import_shared.FloPayError(error.message ?? "Failed to retrieve PayPal payment", "api_error")
482
- };
483
- }
484
- if (paymentIntent && (paymentIntent.status === "requires_capture" || paymentIntent.status === "succeeded")) {
485
- const pmId = typeof paymentIntent.payment_method === "string" ? paymentIntent.payment_method : paymentIntent.payment_method?.id;
486
- const url = new URL(window.location.href);
487
- url.searchParams.delete("payment_intent");
488
- url.searchParams.delete("payment_intent_client_secret");
489
- url.searchParams.delete("redirect_status");
490
- window.history.replaceState({}, "", url.toString());
491
- return {
492
- status: paymentIntent.status,
493
- paymentIntentId: paymentIntent.id,
494
- paymentMethodId: pmId
495
- };
357
+ /** Flush pending work and detach browser lifecycle listeners. */
358
+ destroy() {
359
+ if (this.destroyed) return;
360
+ this.drainQueue();
361
+ this.destroyed = true;
362
+ this.clearFlushTimer();
363
+ if (this.browserTransportAvailable) {
364
+ window.removeEventListener("pagehide", this.pageExitHandler);
365
+ document.removeEventListener("visibilitychange", this.visibilityHandler);
496
366
  }
497
- return {
498
- status: "failed",
499
- error: new import_shared.FloPayError("PayPal payment was not completed. Please try again.", "api_error")
500
- };
367
+ this.queue.splice(0);
368
+ this.reportedFailures.clear();
501
369
  }
502
- getRawProvider() {
503
- return this.stripe;
370
+ /** Permanently honor a merchant opt-out and discard queued events. */
371
+ disable() {
372
+ this.merchantEnabled = false;
373
+ this.queue.splice(0);
374
+ this.reportedFailures.clear();
375
+ this.clearFlushTimer();
504
376
  }
505
- createPayPalElements(options) {
506
- if (!this.stripe) return null;
507
- const elementsOptions = {
508
- mode: "payment",
509
- amount: options.amount ?? 0,
510
- currency: (options.currency ?? "usd").toLowerCase(),
511
- captureMethod: "manual"
512
- };
513
- if (options.setupFutureUsage) {
514
- elementsOptions["setupFutureUsage"] = options.setupFutureUsage;
515
- }
516
- if (options.appearance) {
517
- elementsOptions["appearance"] = {
518
- theme: toStripeAppearanceTheme(options.appearance.theme),
519
- variables: options.appearance.variables,
520
- rules: options.appearance.rules
521
- };
377
+ /** Start every bounded keepalive request synchronously before page teardown. */
378
+ drainQueue() {
379
+ if (!this.browserTransportAvailable || this.destroyed || this.ingestionDisabled || this.queue.length === 0) return;
380
+ this.clearFlushTimer();
381
+ while (this.queue.length > 0) {
382
+ const events = this.queue.splice(0, MAX_BATCH_SIZE);
383
+ void this.sendBatch(events);
522
384
  }
523
- return this.stripe.elements(elementsOptions);
524
- }
525
- destroy() {
526
- this.pendingCardThreeDsAttempts.clear();
527
- this.elements = null;
528
- this.appliedAppearanceKey = null;
529
- this.stripe = null;
530
- }
531
- };
532
-
533
- // src/flopay.ts
534
- var import_shared6 = require("@flopay/shared");
535
-
536
- // src/elements.ts
537
- var import_shared2 = require("@flopay/shared");
538
- var FloPayElements = class {
539
- constructor(provider, options) {
540
- this.elementMap = /* @__PURE__ */ new Map();
541
- this.provider = provider;
542
- this.baseOptions = options ?? {};
543
385
  }
544
- /**
545
- * Creates a new element of the given type.
546
- * If an element of that type already exists, it is destroyed first.
547
- */
548
- async create(type, options) {
549
- const providerExisting = this.provider.getElement(type);
550
- if (providerExisting) {
551
- this.elementMap.set(type, providerExisting);
552
- return providerExisting;
553
- }
554
- const existing = this.elementMap.get(type);
555
- if (existing) {
556
- existing.destroy();
386
+ async sendBatch(events) {
387
+ if (!this.browserTransportAvailable || this.ingestionDisabled) return;
388
+ const body = (0, import_shared.serializeTelemetryBatch)(events, {
389
+ correlationId: this.correlationId,
390
+ sdkPackage: this.sdkPackage,
391
+ sdkVersion: this.sdkVersion,
392
+ batchId: createUuidV4()
393
+ });
394
+ if (bodyByteLength(body) > import_shared.TELEMETRY_MAX_BATCH_BYTES) return;
395
+ const controller = typeof AbortController === "undefined" ? null : new AbortController();
396
+ let timeout = null;
397
+ try {
398
+ const request = fetch(this.endpoint, {
399
+ method: "POST",
400
+ headers: { "content-type": "text/plain;charset=UTF-8" },
401
+ body,
402
+ credentials: "omit",
403
+ keepalive: true,
404
+ referrerPolicy: "no-referrer",
405
+ signal: controller?.signal
406
+ }).then(async (response) => {
407
+ if (response.status !== 202) return null;
408
+ const payload = await response.json().catch(() => null);
409
+ return payload?.status === "disabled" ? "disabled" : null;
410
+ }).catch(() => null);
411
+ const expired = new Promise((resolve) => {
412
+ timeout = setTimeout(() => {
413
+ controller?.abort();
414
+ resolve(null);
415
+ }, UPLOAD_TIMEOUT_MS);
416
+ });
417
+ const status = await Promise.race([request, expired]);
418
+ if (status === "disabled") this.disableFromIngestion();
419
+ } catch {
420
+ } finally {
421
+ if (timeout) clearTimeout(timeout);
557
422
  }
558
- const merged = { ...this.baseOptions, ...options };
559
- const element = await this.provider.createElement(type, merged);
560
- this.elementMap.set(type, element);
561
- return element;
562
423
  }
563
- /** Returns a previously created element, or `null`. */
564
- getElement(type) {
565
- return this.elementMap.get(type) ?? null;
424
+ disableFromIngestion() {
425
+ this.ingestionDisabled = true;
426
+ this.queue.splice(0);
427
+ this.reportedFailures.clear();
428
+ this.clearFlushTimer();
566
429
  }
567
- /**
568
- * Submits all mounted elements for validation.
569
- *
570
- * Returns an object with an optional error if validation fails.
571
- * This does NOT confirm the payment — call `floPay.confirmPayment()` for that.
572
- */
573
- async submit() {
574
- return {};
430
+ scheduleFlush() {
431
+ if (this.flushTimer || this.destroyed || this.ingestionDisabled) return;
432
+ this.flushTimer = setTimeout(() => {
433
+ this.flushTimer = null;
434
+ void this.flush();
435
+ }, 0);
575
436
  }
576
- /** Destroys all created elements and clears the internal map. */
577
- destroy() {
578
- for (const element of this.elementMap.values()) {
579
- element.destroy();
580
- }
581
- this.elementMap.clear();
437
+ clearFlushTimer() {
438
+ if (!this.flushTimer) return;
439
+ clearTimeout(this.flushTimer);
440
+ this.flushTimer = null;
582
441
  }
583
442
  };
443
+ var TELEMETRY_REPORTER_FACTORY = /* @__PURE__ */ Symbol.for("@flopay/js.telemetry.reporter-factory.v1");
444
+ var telemetryGlobal = globalThis;
445
+ if (telemetryGlobal[TELEMETRY_REPORTER_FACTORY] === void 0) {
446
+ Object.defineProperty(telemetryGlobal, TELEMETRY_REPORTER_FACTORY, {
447
+ configurable: true,
448
+ enumerable: false,
449
+ writable: false,
450
+ value: (options) => new TelemetryReporter(options)
451
+ });
452
+ }
584
453
 
585
454
  // src/payment-api.ts
586
- var import_shared4 = require("@flopay/shared");
587
-
588
- // src/api-error.ts
589
- function readErrorString(value) {
590
- return typeof value === "string" && value.trim() ? value : void 0;
455
+ var DEFAULT_PROCESSING_RETRY_AFTER_MS = 1e3;
456
+ var MIN_PROCESSING_RETRY_AFTER_MS = 500;
457
+ var DEFAULT_PROCESSING_TIMEOUT_MS = 15e3;
458
+ var MAX_PROCESSING_RETRY_AFTER_MS = 3e3;
459
+ var DEFAULT_ACCOUNT_SNAPSHOT_TIMEOUT_MS = 1e4;
460
+ function isRecord(value) {
461
+ return typeof value === "object" && value !== null;
591
462
  }
592
- function readErrorMessage(value) {
593
- if (typeof value === "string") return value.trim() ? value : void 0;
594
- if (Array.isArray(value)) {
595
- const joined = value.filter((entry) => typeof entry === "string" && entry.trim().length > 0).join("; ");
596
- return joined || void 0;
463
+ function telemetryStatusClass(status) {
464
+ if (status === void 0) return "network_error";
465
+ const statusClass = `${Math.floor(status / 100)}xx`;
466
+ return statusClass === "2xx" || statusClass === "3xx" || statusClass === "4xx" || statusClass === "5xx" ? statusClass : "unknown";
467
+ }
468
+ function telemetryFailure(error, fallbackCode) {
469
+ if (error instanceof Error && (error.name === "AbortError" || error instanceof import_shared2.FloPayError && error.code === "checkout_processing_timeout")) {
470
+ return { errorCode: "REQUEST_TIMEOUT", statusClass: "timeout" };
597
471
  }
598
- return void 0;
472
+ if (error instanceof TypeError) {
473
+ return { errorCode: "NETWORK_REQUEST_FAILED", statusClass: "network_error" };
474
+ }
475
+ return {
476
+ errorCode: fallbackCode,
477
+ statusClass: telemetryStatusClass(
478
+ error instanceof import_shared2.FloPayError ? error.statusCode : void 0
479
+ )
480
+ };
599
481
  }
600
-
601
- // src/session-display-cache.ts
602
- var STORAGE_KEY_PREFIX = "flopay_session_display:";
603
- var DEFAULT_TTL_MS = 60 * 60 * 1e3;
604
- var memoryStore = /* @__PURE__ */ new Map();
605
- function storageKey(sessionId) {
606
- return `${STORAGE_KEY_PREFIX}${sessionId}`;
482
+ function readString(payload, key) {
483
+ return readErrorString(payload?.[key]);
607
484
  }
608
- function getSessionStorage() {
609
- if (typeof window === "undefined") return null;
610
- try {
611
- return window.sessionStorage;
612
- } catch {
613
- return null;
614
- }
485
+ function readMessage(payload, key) {
486
+ return readErrorMessage(payload?.[key]);
615
487
  }
616
- function cacheSessionDisplayData(sessionId, data, options) {
617
- if (!sessionId) return;
618
- const ttl = options?.ttlMs ?? DEFAULT_TTL_MS;
619
- const entry = { data, expiresAt: Date.now() + ttl };
620
- const storage = getSessionStorage();
621
- if (storage) {
622
- try {
623
- storage.setItem(storageKey(sessionId), JSON.stringify(entry));
624
- return;
625
- } catch {
626
- }
627
- }
628
- memoryStore.set(sessionId, entry);
488
+ function readNumber(payload, key) {
489
+ const value = payload?.[key];
490
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
629
491
  }
630
- function getSessionDisplayData(sessionId) {
631
- if (!sessionId) return null;
632
- const storage = getSessionStorage();
633
- if (storage) {
634
- try {
635
- const raw = storage.getItem(storageKey(sessionId));
636
- if (raw) {
637
- const entry = JSON.parse(raw);
638
- if (entry && typeof entry.expiresAt === "number" && entry.expiresAt > Date.now()) {
639
- return entry.data;
640
- }
641
- storage.removeItem(storageKey(sessionId));
642
- }
643
- } catch {
644
- }
645
- }
646
- const memEntry = memoryStore.get(sessionId);
647
- if (memEntry) {
648
- if (memEntry.expiresAt > Date.now()) {
649
- return memEntry.data;
650
- }
651
- memoryStore.delete(sessionId);
652
- }
653
- return null;
492
+ function delay(ms) {
493
+ return new Promise((resolve) => setTimeout(resolve, ms));
654
494
  }
655
- function clearSessionDisplayData(sessionId) {
656
- if (!sessionId) return;
657
- memoryStore.delete(sessionId);
658
- const storage = getSessionStorage();
659
- if (storage) {
660
- try {
661
- storage.removeItem(storageKey(sessionId));
662
- } catch {
663
- }
664
- }
495
+ function createCheckoutProcessingTimeoutError() {
496
+ return new import_shared2.FloPayError(
497
+ "Checkout is still processing. Please try again shortly.",
498
+ "api_error",
499
+ { code: "checkout_processing_timeout" }
500
+ );
665
501
  }
666
-
667
- // src/telemetry-reporter.ts
668
- var import_shared3 = require("@flopay/shared");
669
- var TELEMETRY_PATH = "/v1/sdk-telemetry/events";
670
- var MAX_BATCH_SIZE = 16;
671
- var MAX_QUEUE_SIZE = 64;
672
- var UPLOAD_TIMEOUT_MS = 1500;
673
- var ERROR_DEDUPLICATION_WINDOW_MS = 1e3;
674
- var MAX_REPORTED_FAILURES = 64;
675
- var DEDUPLICATION_EVENT_ID = "00000000-0000-4000-8000-000000000000";
676
- var EVENT_BUDGETS = {
677
- technical_error: 8,
678
- lifecycle: 32,
679
- expected_outcome: 32,
680
- performance: 24
681
- };
682
- function createUuidV4() {
683
- try {
684
- return globalThis.crypto.randomUUID();
685
- } catch {
686
- const bytes = new Uint8Array(16);
502
+ async function buildApiErrorFromResponse(response, fallbackMessage) {
503
+ const payload = await response.json().catch(() => null);
504
+ const nestedError = isRecord(payload?.error) ? payload.error : null;
505
+ const message = readMessage(payload, "message") ?? readMessage(nestedError, "message") ?? fallbackMessage;
506
+ const code = readString(payload, "code") ?? readString(payload, "gatewayErrorCode") ?? readString(nestedError, "code") ?? `http_${response.status}`;
507
+ return new import_shared2.FloPayError(message, "api_error", {
508
+ code,
509
+ statusCode: response.status
510
+ });
511
+ }
512
+ var NETWORK_RETRY_ATTEMPTS = 2;
513
+ var IDEMPOTENCY_IN_PROGRESS_RETRY_ATTEMPTS = 2;
514
+ async function fetchWithNetworkRetry(input, init, attempts = NETWORK_RETRY_ATTEMPTS, onRetry) {
515
+ let lastErr;
516
+ for (let attempt = 0; ; attempt++) {
687
517
  try {
688
- globalThis.crypto.getRandomValues(bytes);
689
- } catch {
690
- for (let index = 0; index < bytes.length; index += 1) {
691
- bytes[index] = Math.floor(Math.random() * 256);
518
+ return await fetch(input, init);
519
+ } catch (err) {
520
+ if (err instanceof Error && err.name === "AbortError") throw err;
521
+ lastErr = err;
522
+ if (attempt >= attempts) throw lastErr;
523
+ try {
524
+ onRetry?.(attempt + 1);
525
+ } catch {
692
526
  }
527
+ await delay(150 * 2 ** attempt);
693
528
  }
694
- bytes[6] = bytes[6] & 15 | 64;
695
- bytes[8] = bytes[8] & 63 | 128;
696
- const hex = [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
697
- return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
698
- }
699
- }
700
- function bodyByteLength(body) {
701
- try {
702
- return new TextEncoder().encode(body).byteLength;
703
- } catch {
704
- return body.length;
705
529
  }
706
530
  }
707
- function failureDeduplicationKey(event) {
708
- return JSON.stringify([
709
- event.code,
710
- event.stage,
711
- event.provider,
712
- event.attempt,
713
- event.statusClass,
714
- event.requestCategory,
715
- event.paymentMethodCategory,
716
- event.checkoutMode,
717
- event.layout
718
- ]);
719
- }
720
- function telemetryNow() {
721
- return globalThis.performance?.now() ?? 0;
531
+ function isPaymentApiTelemetryHooks(value) {
532
+ return "now" in value || "onFirstByte" in value || "onSessionCreateFailure" in value || "onRetry" in value;
722
533
  }
723
- var TelemetryReporter = class {
724
- constructor(options) {
725
- this.ingestionDisabled = false;
726
- this.queue = [];
727
- this.sequence = 0;
728
- this.flushTimer = null;
729
- this.flushInFlight = null;
730
- this.reportedFailures = /* @__PURE__ */ new Map();
731
- this.checkoutContext = {};
732
- this.checkoutStartedAt = null;
733
- this.destroyed = false;
734
- this.pageExitHandler = () => {
735
- this.drainQueue();
736
- };
737
- this.visibilityHandler = () => {
738
- if (document.visibilityState === "hidden") void this.flush();
739
- };
740
- this.eventCounts = {
741
- technical_error: 0,
742
- lifecycle: 0,
743
- expected_outcome: 0,
744
- performance: 0
745
- };
746
- this.endpoint = `${options.billingApiUrl.replace(/\/+$/, "")}${TELEMETRY_PATH}`;
747
- this.sdkPackage = options.sdkPackage ?? "@flopay/js";
748
- this.sdkVersion = options.sdkVersion;
749
- this.correlationId = createUuidV4();
750
- this.merchantEnabled = options.enabled !== false;
751
- this.clock = options.clock ?? telemetryNow;
752
- this.browserTransportAvailable = typeof window !== "undefined" && typeof document !== "undefined";
753
- if (this.browserTransportAvailable) {
754
- window.addEventListener("pagehide", this.pageExitHandler);
755
- document.addEventListener("visibilitychange", this.visibilityHandler);
756
- }
757
- }
758
- log(input) {
759
- if (!this.canCollect()) return;
760
- this.enqueue((0, import_shared3.buildTelemetryLogEvent)({
761
- ...this.checkoutContext,
762
- ...input,
763
- eventId: createUuidV4(),
764
- sequence: this.sequence++
765
- }));
766
- }
767
- error(input) {
768
- if (!this.canCollect()) return;
769
- const normalizedFailure = (0, import_shared3.buildTelemetryErrorEvent)({
770
- ...this.checkoutContext,
771
- ...input,
772
- eventId: DEDUPLICATION_EVENT_ID,
773
- sequence: 0
534
+ var PaymentAPI = class {
535
+ constructor(billingApiUrl, telemetryOptionsOrHooks = {}) {
536
+ this.baseUrl = billingApiUrl.replace(/\/+$/, "");
537
+ const hasInternalHooks = isPaymentApiTelemetryHooks(telemetryOptionsOrHooks);
538
+ this.telemetryHooks = hasInternalHooks ? telemetryOptionsOrHooks : void 0;
539
+ this.directTelemetry = hasInternalHooks || telemetryOptionsOrHooks.telemetry === false ? void 0 : new TelemetryReporter({
540
+ billingApiUrl: this.baseUrl,
541
+ sdkVersion: import_shared2.SDK_VERSION
774
542
  });
775
- const deduplicationKey = failureDeduplicationKey(normalizedFailure);
776
- const now = this.now();
777
- this.pruneReportedFailures(now);
778
- const previouslyReportedAt = this.reportedFailures.get(deduplicationKey);
779
- if (previouslyReportedAt !== void 0 && now >= previouslyReportedAt && now - previouslyReportedAt < ERROR_DEDUPLICATION_WINDOW_MS) {
780
- this.log({
781
- name: "operation.deduplicated",
782
- stage: input.stage,
783
- provider: input.provider,
784
- paymentMethodCategory: input.paymentMethodCategory,
785
- attempt: input.attempt
786
- });
787
- return;
788
- }
789
- this.rememberReportedFailure(deduplicationKey, now);
790
- this.enqueue((0, import_shared3.buildTelemetryErrorEvent)({
791
- ...this.checkoutContext,
792
- ...input,
793
- eventId: createUuidV4(),
794
- sequence: this.sequence++
795
- }));
796
543
  }
797
- performance(input) {
798
- if (!this.canCollect()) return;
799
- this.enqueue((0, import_shared3.buildTelemetryPerformanceEvent)({
800
- ...this.checkoutContext,
801
- ...input,
802
- eventId: createUuidV4(),
803
- sequence: this.sequence++
804
- }));
544
+ /** Dispose the reporter owned by direct public usage. Internal hooks are never disposed here. */
545
+ destroy() {
546
+ this.directTelemetry?.destroy();
805
547
  }
806
- terminal(input) {
807
- if (!this.canCollect()) return;
808
- this.enqueue((0, import_shared3.buildTelemetryTerminalEvent)({
809
- ...this.checkoutContext,
810
- ...input,
811
- eventId: createUuidV4(),
812
- sequence: this.sequence++
813
- }));
814
- if (input.outcome !== "action_required" && this.checkoutStartedAt !== null) {
815
- const checkoutStartedAt = this.checkoutStartedAt;
816
- this.checkoutStartedAt = null;
817
- this.performance({
818
- stage: "total_journey",
819
- durationMs: Math.max(0, this.now() - checkoutStartedAt),
820
- durationMode: "total",
821
- provider: input.provider,
822
- paymentMethodCategory: input.paymentMethodCategory
823
- });
824
- }
548
+ reportDirectFailure(error, fallbackCode, stage, requestCategory, paymentMethodCategory = "unknown") {
549
+ const failure = telemetryFailure(error, fallbackCode);
550
+ this.directTelemetry?.error({
551
+ ...failure,
552
+ stage,
553
+ requestCategory,
554
+ paymentMethodCategory
555
+ });
825
556
  }
826
- /** @internal Read the reporter's monotonic clock without Resource Timing. */
827
- now() {
557
+ telemetryTimestamp() {
828
558
  try {
829
- return this.clock();
830
- } catch {
831
- return telemetryNow();
832
- }
833
- }
834
- pruneReportedFailures(now) {
835
- for (const [key, reportedAt] of this.reportedFailures) {
836
- if (now < reportedAt || now - reportedAt >= ERROR_DEDUPLICATION_WINDOW_MS) {
837
- this.reportedFailures.delete(key);
838
- }
839
- }
840
- }
841
- rememberReportedFailure(key, reportedAt) {
842
- while (this.reportedFailures.size >= MAX_REPORTED_FAILURES) {
843
- const oldest = this.reportedFailures.keys().next();
844
- if (oldest.done) break;
845
- this.reportedFailures.delete(oldest.value);
846
- }
847
- this.reportedFailures.set(key, reportedAt);
848
- }
849
- /** @internal Add closed checkout dimensions to subsequent SDK events. */
850
- setCheckoutContext(context) {
851
- this.checkoutContext = {
852
- checkoutMode: context.checkoutMode,
853
- layout: context.layout
854
- };
855
- }
856
- /** @internal Start a fresh checkout budget, dedupe window, and total span. */
857
- beginCheckout(context = {}) {
858
- if (!this.canCollect()) return 0;
859
- this.drainQueue();
860
- this.setCheckoutContext(context);
861
- this.sequence = 0;
862
- this.reportedFailures.clear();
863
- this.eventCounts = {
864
- technical_error: 0,
865
- lifecycle: 0,
866
- expected_outcome: 0,
867
- performance: 0
868
- };
869
- this.checkoutStartedAt = this.now();
870
- return this.checkoutStartedAt;
871
- }
872
- enqueue(event) {
873
- if (!this.canCollect()) return;
874
- if (this.queue.length >= MAX_QUEUE_SIZE || this.eventCounts[event.class] >= EVENT_BUDGETS[event.class]) return;
875
- this.eventCounts[event.class] += 1;
876
- this.queue.push(event);
877
- if (this.queue.length >= MAX_BATCH_SIZE) {
878
- void this.flush();
879
- return;
880
- }
881
- this.scheduleFlush();
882
- }
883
- canCollect() {
884
- return this.browserTransportAvailable && this.merchantEnabled && !this.ingestionDisabled && !this.destroyed;
885
- }
886
- /** Flush one bounded batch. Failures are intentionally dropped. */
887
- async flush() {
888
- if (this.flushInFlight) return this.flushInFlight;
889
- if (!this.browserTransportAvailable || this.destroyed || this.ingestionDisabled || this.queue.length === 0) return;
890
- this.clearFlushTimer();
891
- const events = this.queue.splice(0, MAX_BATCH_SIZE);
892
- this.flushInFlight = this.sendBatch(events).finally(() => {
893
- this.flushInFlight = null;
894
- if (this.queue.length > 0) this.scheduleFlush();
895
- });
896
- return this.flushInFlight;
897
- }
898
- /** Flush pending work and detach browser lifecycle listeners. */
899
- destroy() {
900
- if (this.destroyed) return;
901
- this.drainQueue();
902
- this.destroyed = true;
903
- this.clearFlushTimer();
904
- if (this.browserTransportAvailable) {
905
- window.removeEventListener("pagehide", this.pageExitHandler);
906
- document.removeEventListener("visibilitychange", this.visibilityHandler);
907
- }
908
- this.queue.splice(0);
909
- this.reportedFailures.clear();
910
- }
911
- /** Permanently honor a merchant opt-out and discard queued events. */
912
- disable() {
913
- this.merchantEnabled = false;
914
- this.queue.splice(0);
915
- this.reportedFailures.clear();
916
- this.clearFlushTimer();
917
- }
918
- /** Start every bounded keepalive request synchronously before page teardown. */
919
- drainQueue() {
920
- if (!this.browserTransportAvailable || this.destroyed || this.ingestionDisabled || this.queue.length === 0) return;
921
- this.clearFlushTimer();
922
- while (this.queue.length > 0) {
923
- const events = this.queue.splice(0, MAX_BATCH_SIZE);
924
- void this.sendBatch(events);
925
- }
926
- }
927
- async sendBatch(events) {
928
- if (!this.browserTransportAvailable || this.ingestionDisabled) return;
929
- const body = (0, import_shared3.serializeTelemetryBatch)(events, {
930
- correlationId: this.correlationId,
931
- sdkPackage: this.sdkPackage,
932
- sdkVersion: this.sdkVersion,
933
- batchId: createUuidV4()
934
- });
935
- if (bodyByteLength(body) > import_shared3.TELEMETRY_MAX_BATCH_BYTES) return;
936
- const controller = typeof AbortController === "undefined" ? null : new AbortController();
937
- let timeout = null;
938
- try {
939
- const request = fetch(this.endpoint, {
940
- method: "POST",
941
- headers: { "content-type": "text/plain;charset=UTF-8" },
942
- body,
943
- credentials: "omit",
944
- keepalive: true,
945
- referrerPolicy: "no-referrer",
946
- signal: controller?.signal
947
- }).then(async (response) => {
948
- if (response.status !== 202) return null;
949
- const payload = await response.json().catch(() => null);
950
- return payload?.status === "disabled" ? "disabled" : null;
951
- }).catch(() => null);
952
- const expired = new Promise((resolve) => {
953
- timeout = setTimeout(() => {
954
- controller?.abort();
955
- resolve(null);
956
- }, UPLOAD_TIMEOUT_MS);
957
- });
958
- const status = await Promise.race([request, expired]);
959
- if (status === "disabled") this.disableFromIngestion();
960
- } catch {
961
- } finally {
962
- if (timeout) clearTimeout(timeout);
963
- }
964
- }
965
- disableFromIngestion() {
966
- this.ingestionDisabled = true;
967
- this.queue.splice(0);
968
- this.reportedFailures.clear();
969
- this.clearFlushTimer();
970
- }
971
- scheduleFlush() {
972
- if (this.flushTimer || this.destroyed || this.ingestionDisabled) return;
973
- this.flushTimer = setTimeout(() => {
974
- this.flushTimer = null;
975
- void this.flush();
976
- }, 0);
977
- }
978
- clearFlushTimer() {
979
- if (!this.flushTimer) return;
980
- clearTimeout(this.flushTimer);
981
- this.flushTimer = null;
982
- }
983
- };
984
- var TELEMETRY_REPORTER_FACTORY = /* @__PURE__ */ Symbol.for("@flopay/js.telemetry.reporter-factory.v1");
985
- var telemetryGlobal = globalThis;
986
- if (telemetryGlobal[TELEMETRY_REPORTER_FACTORY] === void 0) {
987
- Object.defineProperty(telemetryGlobal, TELEMETRY_REPORTER_FACTORY, {
988
- configurable: true,
989
- enumerable: false,
990
- writable: false,
991
- value: (options) => new TelemetryReporter(options)
992
- });
993
- }
994
-
995
- // src/payment-api.ts
996
- var DEFAULT_PROCESSING_RETRY_AFTER_MS = 1e3;
997
- var MIN_PROCESSING_RETRY_AFTER_MS = 500;
998
- var DEFAULT_PROCESSING_TIMEOUT_MS = 15e3;
999
- var MAX_PROCESSING_RETRY_AFTER_MS = 3e3;
1000
- var DEFAULT_ACCOUNT_SNAPSHOT_TIMEOUT_MS = 1e4;
1001
- function isRecord(value) {
1002
- return typeof value === "object" && value !== null;
1003
- }
1004
- function telemetryStatusClass(status) {
1005
- if (status === void 0) return "network_error";
1006
- const statusClass = `${Math.floor(status / 100)}xx`;
1007
- return statusClass === "2xx" || statusClass === "3xx" || statusClass === "4xx" || statusClass === "5xx" ? statusClass : "unknown";
1008
- }
1009
- function telemetryFailure(error, fallbackCode) {
1010
- if (error instanceof Error && (error.name === "AbortError" || error instanceof import_shared4.FloPayError && error.code === "checkout_processing_timeout")) {
1011
- return { errorCode: "REQUEST_TIMEOUT", statusClass: "timeout" };
1012
- }
1013
- if (error instanceof TypeError) {
1014
- return { errorCode: "NETWORK_REQUEST_FAILED", statusClass: "network_error" };
1015
- }
1016
- return {
1017
- errorCode: fallbackCode,
1018
- statusClass: telemetryStatusClass(
1019
- error instanceof import_shared4.FloPayError ? error.statusCode : void 0
1020
- )
1021
- };
1022
- }
1023
- function readString(payload, key) {
1024
- return readErrorString(payload?.[key]);
1025
- }
1026
- function readMessage(payload, key) {
1027
- return readErrorMessage(payload?.[key]);
1028
- }
1029
- function readNumber(payload, key) {
1030
- const value = payload?.[key];
1031
- return typeof value === "number" && Number.isFinite(value) ? value : void 0;
1032
- }
1033
- function delay(ms) {
1034
- return new Promise((resolve) => setTimeout(resolve, ms));
1035
- }
1036
- function createCheckoutProcessingTimeoutError() {
1037
- return new import_shared4.FloPayError(
1038
- "Checkout is still processing. Please try again shortly.",
1039
- "api_error",
1040
- { code: "checkout_processing_timeout" }
1041
- );
1042
- }
1043
- async function buildApiErrorFromResponse(response, fallbackMessage) {
1044
- const payload = await response.json().catch(() => null);
1045
- const nestedError = isRecord(payload?.error) ? payload.error : null;
1046
- const message = readMessage(payload, "message") ?? readMessage(nestedError, "message") ?? fallbackMessage;
1047
- const code = readString(payload, "code") ?? readString(payload, "gatewayErrorCode") ?? readString(nestedError, "code") ?? `http_${response.status}`;
1048
- return new import_shared4.FloPayError(message, "api_error", {
1049
- code,
1050
- statusCode: response.status
1051
- });
1052
- }
1053
- var NETWORK_RETRY_ATTEMPTS = 2;
1054
- var IDEMPOTENCY_IN_PROGRESS_RETRY_ATTEMPTS = 2;
1055
- async function fetchWithNetworkRetry(input, init, attempts = NETWORK_RETRY_ATTEMPTS, onRetry) {
1056
- let lastErr;
1057
- for (let attempt = 0; ; attempt++) {
1058
- try {
1059
- return await fetch(input, init);
1060
- } catch (err) {
1061
- if (err instanceof Error && err.name === "AbortError") throw err;
1062
- lastErr = err;
1063
- if (attempt >= attempts) throw lastErr;
1064
- try {
1065
- onRetry?.(attempt + 1);
1066
- } catch {
1067
- }
1068
- await delay(150 * 2 ** attempt);
1069
- }
1070
- }
1071
- }
1072
- function isPaymentApiTelemetryHooks(value) {
1073
- return "now" in value || "onFirstByte" in value || "onSessionCreateFailure" in value || "onRetry" in value;
1074
- }
1075
- var PaymentAPI = class {
1076
- constructor(billingApiUrl, telemetryOptionsOrHooks = {}) {
1077
- this.baseUrl = billingApiUrl.replace(/\/+$/, "");
1078
- const hasInternalHooks = isPaymentApiTelemetryHooks(telemetryOptionsOrHooks);
1079
- this.telemetryHooks = hasInternalHooks ? telemetryOptionsOrHooks : void 0;
1080
- this.directTelemetry = hasInternalHooks || telemetryOptionsOrHooks.telemetry === false ? void 0 : new TelemetryReporter({
1081
- billingApiUrl: this.baseUrl,
1082
- sdkVersion: import_shared4.SDK_VERSION
1083
- });
1084
- }
1085
- /** Dispose the reporter owned by direct public usage. Internal hooks are never disposed here. */
1086
- destroy() {
1087
- this.directTelemetry?.destroy();
1088
- }
1089
- reportDirectFailure(error, fallbackCode, stage, requestCategory, paymentMethodCategory = "unknown") {
1090
- const failure = telemetryFailure(error, fallbackCode);
1091
- this.directTelemetry?.error({
1092
- ...failure,
1093
- stage,
1094
- requestCategory,
1095
- paymentMethodCategory
1096
- });
1097
- }
1098
- telemetryTimestamp() {
1099
- try {
1100
- return this.telemetryHooks?.now?.() ?? this.directTelemetry?.now() ?? telemetryNow();
559
+ return this.telemetryHooks?.now?.() ?? this.directTelemetry?.now() ?? telemetryNow();
1101
560
  } catch {
1102
561
  return telemetryNow();
1103
562
  }
@@ -1132,7 +591,7 @@ var PaymentAPI = class {
1132
591
  stage: "session_read",
1133
592
  requestCategory: "session_read"
1134
593
  });
1135
- const headers = { [import_shared4.FLO_SDK_VERSION_HEADER]: import_shared4.SDK_VERSION };
594
+ const headers = { [import_shared2.FLO_SDK_VERSION_HEADER]: import_shared2.SDK_VERSION };
1136
595
  if (nonce) headers["x-checkout-session-token"] = nonce;
1137
596
  try {
1138
597
  const response = await fetchWithNetworkRetry(
@@ -1187,9 +646,9 @@ var PaymentAPI = class {
1187
646
  });
1188
647
  return { ...body, data: this.mergeCachedDisplayData(body.data) };
1189
648
  } catch (error) {
1190
- const statusCode = error instanceof import_shared4.FloPayError ? error.statusCode : void 0;
649
+ const statusCode = error instanceof import_shared2.FloPayError ? error.statusCode : void 0;
1191
650
  this.directTelemetry?.error({
1192
- errorCode: error instanceof import_shared4.FloPayError && error.code === "checkout_processing_timeout" ? "REQUEST_TIMEOUT" : "NETWORK_REQUEST_FAILED",
651
+ errorCode: error instanceof import_shared2.FloPayError && error.code === "checkout_processing_timeout" ? "REQUEST_TIMEOUT" : "NETWORK_REQUEST_FAILED",
1193
652
  stage: "session_read",
1194
653
  requestCategory: "session_read",
1195
654
  statusClass: statusCode ? `${Math.floor(statusCode / 100)}xx` : "network_error"
@@ -1342,7 +801,7 @@ var PaymentAPI = class {
1342
801
  stage: "processing",
1343
802
  requestCategory: "process_payment"
1344
803
  });
1345
- throw new import_shared4.FloPayError(
804
+ throw new import_shared2.FloPayError(
1346
805
  "processPayment requires `nonce` \u2014 pass the value returned from session creation.",
1347
806
  "validation_error",
1348
807
  { code: "MissingCheckoutSessionToken", param: "nonce" }
@@ -1407,7 +866,7 @@ var PaymentAPI = class {
1407
866
  });
1408
867
  return result;
1409
868
  } catch (error) {
1410
- if (!(error instanceof import_shared4.FloPayError && error.code === "checkout_processing_timeout")) {
869
+ if (!(error instanceof import_shared2.FloPayError && error.code === "checkout_processing_timeout")) {
1411
870
  this.reportDirectFailure(
1412
871
  error,
1413
872
  "PAYMENT_PROCESSING_FAILED",
@@ -1419,788 +878,1341 @@ var PaymentAPI = class {
1419
878
  }
1420
879
  }
1421
880
  /**
1422
- * Patch the buyer's account snapshot (email, name, billing address, AVS
1423
- * intent) onto a checkout session via
1424
- * `PATCH /v1/checkouts/sessions/{id}/account` (TeamFloPay/backend#823).
1425
- *
1426
- * The vault path's hosted form owns the charge end-to-end so the SDK
1427
- * never calls `/process` on this path; the buyer-typed AVS / billing
1428
- * address would otherwise be lost. The SDK calls this just before
1429
- * submitting the vault widget so the downstream listener mints the
1430
- * Stripe PaymentMethod with the right `billing_details.address` and the
1431
- * per-attempt + per-PM address snapshots are populated.
1432
- *
1433
- * Body shape mirrors the relevant subset of `/process`'s
1434
- * `ProcessCheckoutBodyDto` — same keys, same validators. The endpoint
1435
- * is idempotent: empty/undefined fields are not written, addresses are
1436
- * last-writer-wins, AVS analytics are first-writer-wins.
881
+ * Patch the buyer's account snapshot (email, name, billing address, AVS
882
+ * intent) onto a checkout session via
883
+ * `PATCH /v1/checkouts/sessions/{id}/account` (TeamFloPay/backend#823).
884
+ *
885
+ * The vault path's hosted form owns the charge end-to-end so the SDK
886
+ * never calls `/process` on this path; the buyer-typed AVS / billing
887
+ * address would otherwise be lost. The SDK calls this just before
888
+ * submitting the vault widget so the downstream listener mints the
889
+ * Stripe PaymentMethod with the right `billing_details.address` and the
890
+ * per-attempt + per-PM address snapshots are populated.
891
+ *
892
+ * Body shape mirrors the relevant subset of `/process`'s
893
+ * `ProcessCheckoutBodyDto` — same keys, same validators. The endpoint
894
+ * is idempotent: empty/undefined fields are not written, addresses are
895
+ * last-writer-wins, AVS analytics are first-writer-wins.
896
+ *
897
+ * Wrapped in `fetchWithNetworkRetry` because a transient blip on this
898
+ * pre-pay PATCH would silently leave AVS unsent and cause an
899
+ * AVS-protected charge to decline downstream.
900
+ */
901
+ async patchAccountSnapshot(sessionId, nonce, body, options) {
902
+ this.beginDirectTelemetryCheckout(sessionId);
903
+ const startedAt = this.telemetryTimestamp();
904
+ this.directTelemetry?.log({
905
+ name: "operation.state_transition",
906
+ stage: "processing",
907
+ requestCategory: "account_snapshot"
908
+ });
909
+ const timeoutMs = options?.timeoutMs ?? DEFAULT_ACCOUNT_SNAPSHOT_TIMEOUT_MS;
910
+ const controller = new AbortController();
911
+ const onCallerAbort = () => controller.abort();
912
+ if (options?.signal) {
913
+ if (options.signal.aborted) controller.abort();
914
+ else options.signal.addEventListener("abort", onCallerAbort, { once: true });
915
+ }
916
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
917
+ try {
918
+ let response;
919
+ try {
920
+ response = await fetchWithNetworkRetry(
921
+ `${this.baseUrl}/v1/checkouts/sessions/${encodeURIComponent(sessionId)}/account`,
922
+ {
923
+ method: "PATCH",
924
+ headers: {
925
+ "Content-Type": "application/json",
926
+ "x-checkout-session-token": nonce
927
+ },
928
+ body: JSON.stringify(body),
929
+ signal: controller.signal
930
+ },
931
+ NETWORK_RETRY_ATTEMPTS,
932
+ (attempt) => {
933
+ this.telemetryHooks?.onRetry?.("account_snapshot", attempt);
934
+ this.directTelemetry?.log({
935
+ name: "operation.retry",
936
+ stage: "processing",
937
+ requestCategory: "account_snapshot",
938
+ attempt
939
+ });
940
+ }
941
+ );
942
+ } finally {
943
+ clearTimeout(timer);
944
+ options?.signal?.removeEventListener("abort", onCallerAbort);
945
+ }
946
+ if (!response.ok) {
947
+ throw await buildApiErrorFromResponse(response, "Failed to persist account snapshot");
948
+ }
949
+ this.directTelemetry?.performance({
950
+ stage: "processing",
951
+ durationMs: Math.max(0, this.telemetryTimestamp() - startedAt),
952
+ durationMode: "machine",
953
+ requestCategory: "account_snapshot",
954
+ statusClass: "2xx"
955
+ });
956
+ } catch (error) {
957
+ this.reportDirectFailure(
958
+ error,
959
+ "NETWORK_REQUEST_FAILED",
960
+ "processing",
961
+ "account_snapshot"
962
+ );
963
+ throw error;
964
+ }
965
+ }
966
+ /** Create a wallet/APM/PayPal intent through the session-scoped contract. */
967
+ async createSessionIntent(sessionId, nonce, request, options) {
968
+ if (!nonce) {
969
+ throw new import_shared2.FloPayError(
970
+ "createSessionIntent requires the checkout session nonce.",
971
+ "validation_error",
972
+ { code: "MissingCheckoutSessionToken", param: "nonce" }
973
+ );
974
+ }
975
+ const rawRequest = request;
976
+ const paymentMethodType = rawRequest["paymentMethodType"];
977
+ const directCardType = typeof paymentMethodType === "string" && paymentMethodType.trim().toLowerCase() === "card";
978
+ const commonRequestFieldsValid = typeof paymentMethodType === "string" && paymentMethodType.length > 0 && !directCardType && (typeof rawRequest["paymentMethodId"] === "string" || rawRequest["paymentMethodId"] === null);
979
+ const stripeRequestValid = rawRequest["provider"] === "stripe" && (rawRequest["paymentMethodCategory"] === "wallet" || rawRequest["paymentMethodCategory"] === "apm") && (rawRequest["intentKind"] === "payment" || rawRequest["intentKind"] === "setup");
980
+ const paypalRequestValid = rawRequest["provider"] === "paypal" && rawRequest["paymentMethodCategory"] === "wallet" && rawRequest["paymentMethodType"] === "paypal" && rawRequest["paymentMethodId"] === null && (rawRequest["intentKind"] === "order" || rawRequest["intentKind"] === "subscription");
981
+ if (!commonRequestFieldsValid || !stripeRequestValid && !paypalRequestValid) {
982
+ throw new import_shared2.FloPayError(
983
+ "Only wallet, APM, and PayPal session intents are supported.",
984
+ "validation_error",
985
+ { code: "InvalidSessionIntentRequest" }
986
+ );
987
+ }
988
+ const suppliedAttemptId = rawRequest["authorizationAttemptId"];
989
+ if (suppliedAttemptId !== void 0 && !(0, import_shared2.isUuidV4)(suppliedAttemptId)) {
990
+ throw new import_shared2.FloPayError(
991
+ "authorizationAttemptId must be a v4 UUID identifying one buyer authorization attempt.",
992
+ "validation_error",
993
+ { code: "InvalidAuthorizationAttemptId", param: "authorizationAttemptId" }
994
+ );
995
+ }
996
+ const authorizationAttemptId = (0, import_shared2.isUuidV4)(suppliedAttemptId) ? suppliedAttemptId : (0, import_shared2.randomUuidV4)();
997
+ this.beginDirectTelemetryCheckout(sessionId);
998
+ const startedAt = this.telemetryTimestamp();
999
+ this.directTelemetry?.log({
1000
+ name: "payment.intent.started",
1001
+ stage: "processing",
1002
+ requestCategory: "intent_create"
1003
+ });
1004
+ const headers = {
1005
+ "Content-Type": "application/json",
1006
+ "x-checkout-session-token": nonce,
1007
+ [import_shared2.IDEMPOTENCY_KEY_HEADER]: options?.idempotencyKey || authorizationAttemptId
1008
+ };
1009
+ let failureReported = false;
1010
+ try {
1011
+ const response = await fetch(
1012
+ `${this.baseUrl}/v1/checkouts/sessions/${encodeURIComponent(sessionId)}/intents`,
1013
+ {
1014
+ method: "POST",
1015
+ headers,
1016
+ body: JSON.stringify({ ...request, authorizationAttemptId }),
1017
+ signal: options?.signal
1018
+ }
1019
+ );
1020
+ if (!response.ok) {
1021
+ failureReported = true;
1022
+ this.directTelemetry?.error({
1023
+ errorCode: "PAYMENT_PROCESSING_FAILED",
1024
+ stage: "processing",
1025
+ requestCategory: "intent_create",
1026
+ statusClass: telemetryStatusClass(response.status)
1027
+ });
1028
+ throw await buildApiErrorFromResponse(response, "Failed to create checkout intent");
1029
+ }
1030
+ const body = await response.json();
1031
+ const data = body.data;
1032
+ if (!data || typeof data !== "object") {
1033
+ throw new import_shared2.FloPayError("Invalid checkout intent response.", "api_error", {
1034
+ code: "InvalidSessionIntentResponse"
1035
+ });
1036
+ }
1037
+ const intent = data;
1038
+ const responsePaymentMethodType = intent["paymentMethodType"];
1039
+ const commonFieldsValid = (intent["paymentMethodCategory"] === "wallet" || intent["paymentMethodCategory"] === "apm") && typeof responsePaymentMethodType === "string" && responsePaymentMethodType.trim().toLowerCase() !== "card" && (typeof intent["paymentMethodId"] === "string" || intent["paymentMethodId"] === null) && typeof intent["providerObjectId"] === "string";
1040
+ const stripeValid = intent["provider"] === "stripe" && (intent["intentKind"] === "payment" || intent["intentKind"] === "setup") && typeof intent["clientSecret"] === "string";
1041
+ const paypalValid = intent["provider"] === "paypal" && intent["paymentMethodCategory"] === "wallet" && intent["paymentMethodType"] === "paypal" && intent["paymentMethodId"] === null && (intent["intentKind"] === "order" || intent["intentKind"] === "subscription") && intent["clientSecret"] === null;
1042
+ const discriminantsMatchRequest = intent["provider"] === request.provider && intent["paymentMethodCategory"] === request.paymentMethodCategory && intent["paymentMethodType"] === request.paymentMethodType && intent["paymentMethodId"] === request.paymentMethodId && intent["intentKind"] === request.intentKind;
1043
+ if (!commonFieldsValid || !stripeValid && !paypalValid || !discriminantsMatchRequest) {
1044
+ throw new import_shared2.FloPayError("Invalid checkout intent response.", "api_error", {
1045
+ code: "InvalidSessionIntentResponse"
1046
+ });
1047
+ }
1048
+ this.directTelemetry?.log({
1049
+ name: "payment.intent.completed",
1050
+ stage: "processing",
1051
+ requestCategory: "intent_create",
1052
+ statusClass: telemetryStatusClass(response.status)
1053
+ });
1054
+ this.directTelemetry?.performance({
1055
+ stage: "processing",
1056
+ durationMs: Math.max(0, this.telemetryTimestamp() - startedAt),
1057
+ durationMode: "machine",
1058
+ requestCategory: "intent_create",
1059
+ statusClass: telemetryStatusClass(response.status)
1060
+ });
1061
+ return intent;
1062
+ } catch (error) {
1063
+ if (!failureReported) {
1064
+ this.reportDirectFailure(
1065
+ error,
1066
+ "PAYMENT_PROCESSING_FAILED",
1067
+ "processing",
1068
+ "intent_create"
1069
+ );
1070
+ }
1071
+ throw error;
1072
+ }
1073
+ }
1074
+ /** Record a provider-neutral non-card decline without sensitive identifiers. */
1075
+ async reportSessionIntentDecline(sessionId, nonce, request, options) {
1076
+ if (!nonce) {
1077
+ throw new import_shared2.FloPayError(
1078
+ "reportSessionIntentDecline requires the checkout session nonce.",
1079
+ "validation_error",
1080
+ { code: "MissingCheckoutSessionToken", param: "nonce" }
1081
+ );
1082
+ }
1083
+ const rawRequest = request;
1084
+ const reason = rawRequest["providerDeclineReason"];
1085
+ const paymentMethodType = rawRequest["paymentMethodType"];
1086
+ const safeReason = typeof reason === "string" && /^[a-z0-9][a-z0-9_.:-]{0,63}$/i.test(reason) && !/^(?:pm|pi|seti|tok|src|cus|sess|sk|pk)_/i.test(reason);
1087
+ const commonFieldsValid = typeof paymentMethodType === "string" && paymentMethodType.length > 0 && paymentMethodType.trim().toLowerCase() !== "card" && safeReason;
1088
+ const stripeFieldsValid = rawRequest["provider"] === "stripe" && (rawRequest["paymentMethodCategory"] === "wallet" || rawRequest["paymentMethodCategory"] === "apm");
1089
+ const paypalFieldsValid = rawRequest["provider"] === "paypal" && rawRequest["paymentMethodCategory"] === "wallet" && rawRequest["paymentMethodType"] === "paypal";
1090
+ if (!commonFieldsValid || !stripeFieldsValid && !paypalFieldsValid) {
1091
+ throw new import_shared2.FloPayError(
1092
+ "Invalid non-card decline classification.",
1093
+ "validation_error",
1094
+ { code: "InvalidSessionIntentDeclineRequest" }
1095
+ );
1096
+ }
1097
+ const safeRequest = paypalFieldsValid ? {
1098
+ provider: "paypal",
1099
+ paymentMethodCategory: "wallet",
1100
+ paymentMethodType: "paypal",
1101
+ providerDeclineReason: reason
1102
+ } : {
1103
+ provider: "stripe",
1104
+ paymentMethodCategory: rawRequest["paymentMethodCategory"],
1105
+ paymentMethodType: rawRequest["paymentMethodType"],
1106
+ providerDeclineReason: reason
1107
+ };
1108
+ const response = await fetch(
1109
+ `${this.baseUrl}/v1/checkouts/sessions/${encodeURIComponent(sessionId)}/intents/decline`,
1110
+ {
1111
+ method: "POST",
1112
+ headers: {
1113
+ "Content-Type": "application/json",
1114
+ "x-checkout-session-token": nonce
1115
+ },
1116
+ body: JSON.stringify(safeRequest),
1117
+ signal: options?.signal
1118
+ }
1119
+ );
1120
+ if (!response.ok) {
1121
+ throw await buildApiErrorFromResponse(response, "Failed to report checkout decline");
1122
+ }
1123
+ }
1124
+ /**
1125
+ * Fetch user's prior payments by email.
1126
+ * Used to determine if saved card UX should be shown.
1127
+ */
1128
+ async getPaymentsByEmail(email, options) {
1129
+ this.beginDirectTelemetryOperation();
1130
+ const startedAt = this.telemetryTimestamp();
1131
+ this.directTelemetry?.log({
1132
+ name: "operation.recovery.started",
1133
+ stage: "recovery",
1134
+ requestCategory: "other",
1135
+ paymentMethodCategory: "saved"
1136
+ });
1137
+ const page = options?.page ?? 1;
1138
+ const limit = options?.limit ?? 1;
1139
+ const params = new URLSearchParams({
1140
+ email,
1141
+ page: String(page),
1142
+ limit: String(limit),
1143
+ sortField: "createdAt",
1144
+ sortDirection: "DESC"
1145
+ });
1146
+ try {
1147
+ const response = await fetch(
1148
+ `${this.baseUrl}/v1/payments?${params.toString()}`,
1149
+ {
1150
+ method: "GET",
1151
+ signal: options?.signal,
1152
+ keepalive: true
1153
+ }
1154
+ );
1155
+ if (!response.ok) {
1156
+ throw new import_shared2.FloPayError(
1157
+ "Failed to fetch payments",
1158
+ "api_error",
1159
+ { statusCode: response.status }
1160
+ );
1161
+ }
1162
+ const result = await response.json();
1163
+ this.directTelemetry?.log({
1164
+ name: "operation.recovery.completed",
1165
+ stage: "recovery",
1166
+ requestCategory: "other",
1167
+ paymentMethodCategory: "saved",
1168
+ statusClass: "2xx"
1169
+ });
1170
+ this.directTelemetry?.performance({
1171
+ stage: "recovery",
1172
+ durationMs: Math.max(0, this.telemetryTimestamp() - startedAt),
1173
+ durationMode: "machine",
1174
+ requestCategory: "other",
1175
+ paymentMethodCategory: "saved",
1176
+ statusClass: "2xx"
1177
+ });
1178
+ return result;
1179
+ } catch (error) {
1180
+ this.reportDirectFailure(error, "RECOVERY_FAILED", "recovery", "other", "saved");
1181
+ throw error;
1182
+ }
1183
+ }
1184
+ /**
1185
+ * Create a checkout session AND return the full session data in one call.
1186
+ * Uses `?expand=true` so the backend returns the complete session
1187
+ * instead of just a UUID — eliminating the need for a second GET.
1437
1188
  *
1438
- * Wrapped in `fetchWithNetworkRetry` because a transient blip on this
1439
- * pre-pay PATCH would silently leave AVS unsent and cause an
1440
- * AVS-protected charge to decline downstream.
1189
+ * Falls back to create + GET if the backend doesn't support `expand`.
1441
1190
  */
1442
- async patchAccountSnapshot(sessionId, nonce, body, options) {
1443
- this.beginDirectTelemetryCheckout(sessionId);
1191
+ async createAndFetchSession(params) {
1192
+ this.beginDirectTelemetryOperation();
1444
1193
  const startedAt = this.telemetryTimestamp();
1445
1194
  this.directTelemetry?.log({
1446
- name: "operation.state_transition",
1447
- stage: "processing",
1448
- requestCategory: "account_snapshot"
1195
+ name: "session.create.started",
1196
+ stage: "session_create",
1197
+ requestCategory: "session_create"
1449
1198
  });
1450
- const timeoutMs = options?.timeoutMs ?? DEFAULT_ACCOUNT_SNAPSHOT_TIMEOUT_MS;
1451
- const controller = new AbortController();
1452
- const onCallerAbort = () => controller.abort();
1453
- if (options?.signal) {
1454
- if (options.signal.aborted) controller.abort();
1455
- else options.signal.addEventListener("abort", onCallerAbort, { once: true });
1456
- }
1457
- const timer = setTimeout(() => controller.abort(), timeoutMs);
1458
1199
  try {
1459
- let response;
1460
- try {
1461
- response = await fetchWithNetworkRetry(
1462
- `${this.baseUrl}/v1/checkouts/sessions/${encodeURIComponent(sessionId)}/account`,
1463
- {
1464
- method: "PATCH",
1465
- headers: {
1466
- "Content-Type": "application/json",
1467
- "x-checkout-session-token": nonce
1468
- },
1469
- body: JSON.stringify(body),
1470
- signal: controller.signal
1471
- },
1472
- NETWORK_RETRY_ATTEMPTS,
1473
- (attempt) => {
1474
- this.telemetryHooks?.onRetry?.("account_snapshot", attempt);
1475
- this.directTelemetry?.log({
1476
- name: "operation.retry",
1477
- stage: "processing",
1478
- requestCategory: "account_snapshot",
1479
- attempt
1480
- });
1481
- }
1482
- );
1483
- } finally {
1484
- clearTimeout(timer);
1485
- options?.signal?.removeEventListener("abort", onCallerAbort);
1486
- }
1487
- if (!response.ok) {
1488
- throw await buildApiErrorFromResponse(response, "Failed to persist account snapshot");
1489
- }
1200
+ const result = await this.createAndFetchSessionRequest(params, startedAt);
1201
+ this.adoptDirectTelemetryCheckout(result.data.session?.id);
1202
+ this.directTelemetry?.log({
1203
+ name: "session.request.completed",
1204
+ stage: "session_complete",
1205
+ requestCategory: "session_create",
1206
+ statusClass: "2xx"
1207
+ });
1490
1208
  this.directTelemetry?.performance({
1491
- stage: "processing",
1492
- durationMs: Math.max(0, this.telemetryTimestamp() - startedAt),
1209
+ stage: "session_create",
1210
+ durationMs: this.telemetryTimestamp() - startedAt,
1493
1211
  durationMode: "machine",
1494
- requestCategory: "account_snapshot",
1212
+ requestCategory: "session_create",
1495
1213
  statusClass: "2xx"
1496
1214
  });
1215
+ return result;
1497
1216
  } catch (error) {
1498
- this.reportDirectFailure(
1499
- error,
1500
- "NETWORK_REQUEST_FAILED",
1501
- "processing",
1502
- "account_snapshot"
1217
+ if (!(error instanceof import_shared2.FloPayError && error.code === "session_auto_completed")) {
1218
+ try {
1219
+ this.telemetryHooks?.onSessionCreateFailure?.(error);
1220
+ } catch {
1221
+ }
1222
+ }
1223
+ if (error instanceof import_shared2.FloPayError && error.type === "validation_error") {
1224
+ this.directTelemetry?.terminal({
1225
+ outcome: "validation_rejected",
1226
+ stage: "session_create",
1227
+ requestCategory: "session_create"
1228
+ });
1229
+ } else if (!(error instanceof import_shared2.FloPayError && error.code === "session_auto_completed")) {
1230
+ const statusCode = error instanceof import_shared2.FloPayError ? error.statusCode : void 0;
1231
+ this.directTelemetry?.error({
1232
+ errorCode: error instanceof Error && error.name === "AbortError" ? "REQUEST_TIMEOUT" : error instanceof TypeError ? "NETWORK_REQUEST_FAILED" : "CHECKOUT_SESSION_CREATE_FAILED",
1233
+ stage: "session_create",
1234
+ requestCategory: "session_create",
1235
+ statusClass: error instanceof Error && error.name === "AbortError" ? "timeout" : statusCode ? `${Math.floor(statusCode / 100)}xx` : "network_error"
1236
+ });
1237
+ }
1238
+ throw error;
1239
+ }
1240
+ }
1241
+ async createAndFetchSessionRequest(params, telemetryStartedAt) {
1242
+ const wireProducts = params.products ?? (0, import_shared2.foldIntoProducts)(params.items, params.subscriptions);
1243
+ const sessionCurrency = (0, import_shared2.resolveSessionCurrency)(
1244
+ params.currency,
1245
+ params.items,
1246
+ params.subscriptions,
1247
+ wireProducts
1248
+ );
1249
+ if (!sessionCurrency) {
1250
+ throw new import_shared2.FloPayError(
1251
+ "currency is required: pass `currency` on the session, or include a `currency` on the first item/subscription/product.",
1252
+ "validation_error",
1253
+ { code: "CurrencyRequired", param: "currency" }
1254
+ );
1255
+ }
1256
+ const payload = {
1257
+ clientId: params.clientId,
1258
+ checkoutVersion: import_shared2.SDK_VERSION,
1259
+ successUrl: params.successUrl,
1260
+ cancelUrl: params.cancelUrl,
1261
+ currency: sessionCurrency,
1262
+ checkoutMode: params.checkoutMode ?? "full",
1263
+ products: wireProducts.map((product) => (0, import_shared2.buildProductPayload)(product, sessionCurrency)),
1264
+ accountData: {
1265
+ userId: params.account.userId,
1266
+ firstName: params.account.firstName ?? null,
1267
+ lastName: params.account.lastName ?? null,
1268
+ email: params.account.email,
1269
+ country: params.account.country ?? null,
1270
+ gender: params.account.gender ?? null,
1271
+ city: params.account.city ?? null,
1272
+ state: params.account.state ?? null,
1273
+ zip: params.account.zip ?? null,
1274
+ addressLine1: params.account.addressLine1 ?? null,
1275
+ addressLine2: params.account.addressLine2 ?? null
1276
+ },
1277
+ couponCodes: params.couponCodes ?? []
1278
+ };
1279
+ if (params.tokenizedData) payload["tokenizedData"] = params.tokenizedData;
1280
+ if (params.tagsData) payload["tagsData"] = params.tagsData;
1281
+ if (params.utmMetadata?.length) payload["utmMetadata"] = params.utmMetadata;
1282
+ if (params.avsCheck !== void 0) payload["avsCheck"] = params.avsCheck;
1283
+ if (params.checkoutType) payload["checkoutType"] = params.checkoutType;
1284
+ if (params.checkoutLayout) payload["checkoutLayout"] = params.checkoutLayout;
1285
+ if (params.avsConfig) payload["avsConfig"] = params.avsConfig;
1286
+ const headers = {
1287
+ "Content-Type": "application/json",
1288
+ // Retain the SDK version header for compatibility visibility. Backends
1289
+ // must not gate the hosted vault block on this value.
1290
+ [import_shared2.FLO_SDK_VERSION_HEADER]: import_shared2.SDK_VERSION
1291
+ };
1292
+ const idempotencyKey = (0, import_shared2.resolveIdempotencyKey)(params.idempotencyKey);
1293
+ if (idempotencyKey) {
1294
+ headers[import_shared2.IDEMPOTENCY_KEY_HEADER] = idempotencyKey;
1295
+ }
1296
+ let response;
1297
+ let firstByteReported = false;
1298
+ for (let attempt = 0; ; attempt++) {
1299
+ response = await fetchWithNetworkRetry(
1300
+ `${this.baseUrl}/v1/checkouts/sessions?expand=true`,
1301
+ {
1302
+ method: "POST",
1303
+ headers,
1304
+ body: JSON.stringify(payload)
1305
+ },
1306
+ NETWORK_RETRY_ATTEMPTS,
1307
+ (networkAttempt) => {
1308
+ this.telemetryHooks?.onRetry?.("session_create", networkAttempt);
1309
+ this.directTelemetry?.log({
1310
+ name: "operation.retry",
1311
+ stage: "session_create",
1312
+ requestCategory: "session_create",
1313
+ attempt: networkAttempt
1314
+ });
1315
+ }
1503
1316
  );
1317
+ if (!firstByteReported) {
1318
+ firstByteReported = true;
1319
+ const statusClass = `${Math.floor(response.status / 100)}xx`;
1320
+ this.directTelemetry?.log({
1321
+ name: "session.request.first_byte",
1322
+ stage: "session_first_byte",
1323
+ requestCategory: "session_create",
1324
+ statusClass
1325
+ });
1326
+ this.directTelemetry?.performance({
1327
+ stage: "session_first_byte",
1328
+ durationMs: this.telemetryTimestamp() - telemetryStartedAt,
1329
+ durationMode: "machine",
1330
+ requestCategory: "session_create",
1331
+ statusClass
1332
+ });
1333
+ }
1334
+ if (response.status === 204) {
1335
+ throw new import_shared2.FloPayError(
1336
+ "Session auto-completed \u2014 payment method already on file",
1337
+ "api_error",
1338
+ { code: "session_auto_completed" }
1339
+ );
1340
+ }
1341
+ if (response.ok) break;
1342
+ const error = await buildApiErrorFromResponse(response, "Failed to create checkout session");
1343
+ if (error.code === import_shared2.IDEMPOTENCY_IN_PROGRESS_CODE && attempt < IDEMPOTENCY_IN_PROGRESS_RETRY_ATTEMPTS) {
1344
+ try {
1345
+ this.telemetryHooks?.onRetry?.("session_create", attempt + 1);
1346
+ this.directTelemetry?.log({
1347
+ name: "operation.retry",
1348
+ stage: "session_create",
1349
+ requestCategory: "session_create",
1350
+ attempt: attempt + 1
1351
+ });
1352
+ } catch {
1353
+ }
1354
+ await delay(150 * 2 ** attempt);
1355
+ continue;
1356
+ }
1504
1357
  throw error;
1505
1358
  }
1359
+ const body = await response.json();
1360
+ if (body.data && "gateways" in body.data) {
1361
+ this.autoCacheDisplayData(body.data.uuid, params);
1362
+ const merged = this.mergeCachedDisplayData(body.data);
1363
+ const normalized = this.normalizeRawSession(merged);
1364
+ if (body.vault && normalized.data.session) {
1365
+ normalized.data.session.vault = this.toVaultBlock(body.vault);
1366
+ }
1367
+ return {
1368
+ ...normalized,
1369
+ autoProcessingError: body.autoProcessingError,
1370
+ autoProcessingAttempted: body.autoProcessingAttempted,
1371
+ autoProcessingPending: body.autoProcessingPending
1372
+ };
1373
+ }
1374
+ const uuid = body.data?.uuid;
1375
+ if (!uuid) {
1376
+ throw new import_shared2.FloPayError("No session ID returned", "api_error");
1377
+ }
1378
+ this.autoCacheDisplayData(uuid, params);
1379
+ this.adoptDirectTelemetryCheckout(uuid);
1380
+ const unifiedSession = await this.getUnifiedCheckoutSession(uuid);
1381
+ return {
1382
+ ...unifiedSession,
1383
+ autoProcessingError: body.autoProcessingError,
1384
+ autoProcessingAttempted: body.autoProcessingAttempted,
1385
+ autoProcessingPending: body.autoProcessingPending
1386
+ };
1506
1387
  }
1507
- /**
1508
- * Create a PaymentIntent on the backend.
1509
- *
1510
- * Used by the Stripe flow to create a server-side PaymentIntent
1511
- * with the client's payment method attached.
1512
- *
1513
- * Forwards `options.nonce` as `x-checkout-session-token` when supplied — the
1514
- * session-bound checkout token returned by session creation. Post-#640
1515
- * backends reject this call with a 401 when the header is missing or does
1516
- * not match the session's stored nonce.
1517
- *
1518
- * `paymentMethodType` is optional for the vault card-capture flow
1519
- * (TeamFloPay/backend#823): the frontend starts checkout *without* an upfront
1520
- * card payment method, so it may be omitted (or `null`). The hosted vault PCI
1521
- * form captures the card afterwards and the backend attaches the resulting
1522
- * payment method to the PaymentIntent it returns here. Legacy callers keep
1523
- * passing the concrete payment method id / type.
1524
- */
1525
- async createPaymentIntent(sessionId, email, paymentMethodType, options) {
1526
- this.beginDirectTelemetryCheckout(sessionId);
1388
+ async waitForCheckoutSessionCompletion(checkoutSessionId, options) {
1389
+ this.beginDirectTelemetryCheckout(checkoutSessionId);
1527
1390
  const startedAt = this.telemetryTimestamp();
1528
1391
  this.directTelemetry?.log({
1529
- name: "payment.intent.started",
1530
- stage: "processing",
1531
- requestCategory: "intent_create"
1392
+ name: "operation.recovery.started",
1393
+ stage: "recovery",
1394
+ requestCategory: "session_read",
1395
+ paymentMethodCategory: "saved"
1532
1396
  });
1533
- const headers = { "Content-Type": "application/json" };
1534
- if (options?.nonce) headers["x-checkout-session-token"] = options.nonce;
1397
+ const timeoutMs = options?.timeoutMs ?? DEFAULT_PROCESSING_TIMEOUT_MS;
1398
+ const deadline = Date.now() + timeoutMs;
1399
+ let nextDelayMs = this.clampRetryAfterMs(options?.initialDelayMs ?? DEFAULT_PROCESSING_RETRY_AFTER_MS);
1400
+ let pollAttempt = 0;
1535
1401
  try {
1536
- const response = await fetch(
1537
- `${this.baseUrl}/v1/checkouts/payments/intents`,
1538
- {
1539
- method: "POST",
1540
- headers,
1541
- body: JSON.stringify({
1542
- sessionId,
1543
- email,
1544
- paymentMethodType: paymentMethodType ?? null,
1545
- isPaypal: options?.isPaypal ?? false
1546
- }),
1547
- signal: options?.signal
1402
+ while (true) {
1403
+ const remainingMs = deadline - Date.now();
1404
+ if (remainingMs <= 0) {
1405
+ throw createCheckoutProcessingTimeoutError();
1548
1406
  }
1549
- );
1550
- if (!response.ok) {
1551
- this.directTelemetry?.error({
1552
- errorCode: "PAYMENT_PROCESSING_FAILED",
1553
- stage: "processing",
1554
- requestCategory: "intent_create",
1555
- statusClass: telemetryStatusClass(response.status)
1556
- });
1557
- return response;
1407
+ if (nextDelayMs > 0) {
1408
+ try {
1409
+ pollAttempt += 1;
1410
+ this.telemetryHooks?.onRetry?.("session_read", pollAttempt);
1411
+ this.directTelemetry?.log({
1412
+ name: "operation.retry",
1413
+ stage: "recovery",
1414
+ requestCategory: "session_read",
1415
+ paymentMethodCategory: "saved",
1416
+ attempt: pollAttempt
1417
+ });
1418
+ } catch {
1419
+ }
1420
+ await delay(Math.min(nextDelayMs, remainingMs));
1421
+ if (Date.now() >= deadline) {
1422
+ throw createCheckoutProcessingTimeoutError();
1423
+ }
1424
+ }
1425
+ const session = await this.getUnifiedCheckoutSession(checkoutSessionId, options?.nonce);
1426
+ const status = session.data.session?.status;
1427
+ if (status === "complete" || status === "expired") {
1428
+ this.directTelemetry?.log({
1429
+ name: "operation.recovery.completed",
1430
+ stage: "recovery",
1431
+ requestCategory: "session_read",
1432
+ paymentMethodCategory: "saved"
1433
+ });
1434
+ this.directTelemetry?.performance({
1435
+ stage: "recovery",
1436
+ durationMs: Math.max(0, this.telemetryTimestamp() - startedAt),
1437
+ durationMode: "machine",
1438
+ requestCategory: "session_read",
1439
+ paymentMethodCategory: "saved"
1440
+ });
1441
+ return session;
1442
+ }
1443
+ if (Date.now() >= deadline) {
1444
+ throw createCheckoutProcessingTimeoutError();
1445
+ }
1446
+ nextDelayMs = this.clampRetryAfterMs(
1447
+ Math.max(nextDelayMs * 2, MIN_PROCESSING_RETRY_AFTER_MS)
1448
+ );
1558
1449
  }
1559
- this.directTelemetry?.log({
1560
- name: "payment.intent.completed",
1561
- stage: "processing",
1562
- requestCategory: "intent_create",
1563
- statusClass: telemetryStatusClass(response.status)
1564
- });
1565
- this.directTelemetry?.performance({
1566
- stage: "processing",
1567
- durationMs: Math.max(0, this.telemetryTimestamp() - startedAt),
1568
- durationMode: "machine",
1569
- requestCategory: "intent_create",
1570
- statusClass: telemetryStatusClass(response.status)
1571
- });
1572
- return response;
1573
1450
  } catch (error) {
1574
- this.reportDirectFailure(
1575
- error,
1576
- "PAYMENT_PROCESSING_FAILED",
1577
- "processing",
1578
- "intent_create"
1579
- );
1451
+ if (error instanceof import_shared2.FloPayError && error.code === "checkout_processing_timeout") {
1452
+ this.reportDirectFailure(
1453
+ error,
1454
+ "RECOVERY_FAILED",
1455
+ "recovery",
1456
+ "session_read",
1457
+ "saved"
1458
+ );
1459
+ }
1580
1460
  throw error;
1581
1461
  }
1582
1462
  }
1463
+ /** Normalize a raw session into a provider-agnostic shape. */
1464
+ normalizeRawSession(session) {
1465
+ const gateways = session.gateways ?? {};
1466
+ const providers = [];
1467
+ const data = {
1468
+ session: this.toCheckoutSession(session)
1469
+ };
1470
+ const stripeGateway = gateways.stripe;
1471
+ if (stripeGateway?.publishableKey) {
1472
+ providers.push("stripe");
1473
+ const rawSession = session;
1474
+ const stripeClientSecret = [
1475
+ rawSession["stripeClientSecret"],
1476
+ stripeGateway.stripeClientSecret
1477
+ ].find((value) => typeof value === "string" && value.length > 0);
1478
+ data.stripe = {
1479
+ clientSecret: stripeClientSecret ?? "",
1480
+ publishableKey: stripeGateway.publishableKey ?? void 0,
1481
+ paypalPublishableKey: stripeGateway.paypalPublishableKey ?? void 0,
1482
+ environment: stripeGateway.environment,
1483
+ enabledPaymentMethods: Array.isArray(stripeGateway.enabledPaymentMethods) ? stripeGateway.enabledPaymentMethods.filter((m) => typeof m === "string") : void 0
1484
+ };
1485
+ }
1486
+ const paypalGateway = gateways.paypal;
1487
+ if (paypalGateway?.publishableKey) {
1488
+ providers.push("paypal");
1489
+ data.paypal = {
1490
+ publishableKey: paypalGateway.publishableKey,
1491
+ environment: paypalGateway.environment
1492
+ };
1493
+ }
1494
+ return {
1495
+ providers,
1496
+ mode: "tokenize",
1497
+ data,
1498
+ raw: { data: session }
1499
+ };
1500
+ }
1501
+ /** Convert raw session to the SDK CheckoutSession shape. */
1502
+ toCheckoutSession(raw) {
1503
+ const rawProducts = raw.products ?? [];
1504
+ const hasBackendTotal = typeof raw.totalAmount === "number" && Number.isFinite(raw.totalAmount);
1505
+ const computedTotal = rawProducts.reduce(
1506
+ (sum, p) => sum + (p.overrideAmount ?? p.totalAmount ?? 0),
1507
+ 0
1508
+ );
1509
+ const totalAmount = hasBackendTotal ? raw.totalAmount : computedTotal;
1510
+ const amountInCents = Math.round(totalAmount * 100);
1511
+ const currency = raw.currency ?? rawProducts[0]?.currency ?? "USD";
1512
+ const mode = rawProducts.some((p) => p.type === "subscription") ? "subscription" : "payment";
1513
+ return {
1514
+ id: raw.uuid,
1515
+ clientSecret: raw.nonce,
1516
+ mode,
1517
+ status: this.toCheckoutSessionStatus(raw.status),
1518
+ amount: amountInCents,
1519
+ currency,
1520
+ customer: {
1521
+ id: raw.accountData.userId,
1522
+ email: raw.accountData.email,
1523
+ firstName: raw.accountData.firstName,
1524
+ lastName: raw.accountData.lastName,
1525
+ country: raw.accountData.country ?? void 0,
1526
+ city: raw.accountData.city ?? void 0,
1527
+ state: raw.accountData.state ?? void 0,
1528
+ zip: raw.accountData.zip ?? void 0,
1529
+ gender: raw.accountData.gender ?? void 0,
1530
+ line1: raw.accountData.addressLine1 ?? void 0,
1531
+ line2: raw.accountData.addressLine2 ?? void 0
1532
+ },
1533
+ metadata: {},
1534
+ checkoutMode: raw.checkoutMode,
1535
+ providerPaymentMethodId: typeof raw.providerPaymentMethodId === "string" ? raw.providerPaymentMethodId : null,
1536
+ products: rawProducts.map((p) => ({
1537
+ ...p,
1538
+ totalAmount: typeof p.totalAmount === "number" ? p.totalAmount : void 0,
1539
+ overrideAmount: typeof p.overrideAmount === "number" ? p.overrideAmount : null,
1540
+ currency: typeof p.currency === "string" ? p.currency : void 0,
1541
+ metadata: p.metadata ?? null
1542
+ })),
1543
+ successUrl: raw.successUrl,
1544
+ cancelUrl: raw.cancelUrl,
1545
+ coupons: raw.coupons,
1546
+ subtotalAmount: raw.subtotalAmount,
1547
+ discountAmount: raw.discountAmount,
1548
+ totalAmount: raw.totalAmount,
1549
+ createdAt: raw.createdAt,
1550
+ gateways: raw.gateways,
1551
+ accountData: raw.accountData,
1552
+ tagsData: raw.tagsData
1553
+ };
1554
+ }
1583
1555
  /**
1584
- * Create a SetupIntent for saving payment methods.
1585
- *
1586
- * Forwards `options.nonce` as `x-checkout-session-token` when supplied
1587
- * required by post-#640 backends, ignored by earlier versions.
1556
+ * Coerce a raw vault block into a typed {@link VaultCaptureBlock}. The
1557
+ * server-only PCIVault submit `secret` is deliberately dropped so it never
1558
+ * lands on the public session surface (logs / telemetry / client inspection).
1588
1559
  */
1589
- async createSetupIntent(sessionId, email, paymentMethodType, options) {
1590
- this.beginDirectTelemetryCheckout(sessionId);
1591
- const startedAt = this.telemetryTimestamp();
1592
- this.directTelemetry?.log({
1593
- name: "payment.intent.started",
1594
- stage: "processing",
1595
- requestCategory: "intent_create"
1596
- });
1597
- const headers = { "Content-Type": "application/json" };
1598
- if (options?.nonce) headers["x-checkout-session-token"] = options.nonce;
1599
- try {
1600
- const response = await fetch(
1601
- `${this.baseUrl}/v1/checkouts/payments/setup-intents`,
1602
- {
1603
- method: "POST",
1604
- headers,
1605
- body: JSON.stringify({ sessionId, email, paymentMethodType }),
1606
- signal: options?.signal
1607
- }
1608
- );
1609
- if (!response.ok) {
1610
- this.directTelemetry?.error({
1611
- errorCode: "PAYMENT_PROCESSING_FAILED",
1612
- stage: "processing",
1613
- requestCategory: "intent_create",
1614
- statusClass: telemetryStatusClass(response.status)
1615
- });
1616
- return response;
1617
- }
1618
- this.directTelemetry?.log({
1619
- name: "payment.intent.completed",
1620
- stage: "processing",
1621
- requestCategory: "intent_create",
1622
- statusClass: telemetryStatusClass(response.status)
1623
- });
1624
- this.directTelemetry?.performance({
1625
- stage: "processing",
1626
- durationMs: Math.max(0, this.telemetryTimestamp() - startedAt),
1627
- durationMode: "machine",
1628
- requestCategory: "intent_create",
1629
- statusClass: telemetryStatusClass(response.status)
1630
- });
1560
+ toVaultBlock(raw) {
1561
+ return {
1562
+ html: typeof raw.html === "string" ? raw.html : void 0,
1563
+ url: typeof raw.url === "string" ? raw.url : void 0,
1564
+ messageToken: typeof raw.messageToken === "string" ? raw.messageToken : void 0,
1565
+ expectedOrigin: typeof raw.expectedOrigin === "string" ? raw.expectedOrigin : void 0
1566
+ };
1567
+ }
1568
+ toCheckoutSessionStatus(status) {
1569
+ if (status === "completed") {
1570
+ return "complete";
1571
+ }
1572
+ if (status === "expired") {
1573
+ return "expired";
1574
+ }
1575
+ return "open";
1576
+ }
1577
+ async resolveProcessResponse(response, checkoutSessionId, options) {
1578
+ if (response.status !== 202) {
1631
1579
  return response;
1632
- } catch (error) {
1633
- this.reportDirectFailure(
1634
- error,
1635
- "PAYMENT_PROCESSING_FAILED",
1636
- "processing",
1637
- "intent_create"
1580
+ }
1581
+ const payload = await response.json().catch(() => null);
1582
+ const pending = this.toCheckoutProcessingPending(payload, response, checkoutSessionId);
1583
+ const session = await this.waitForCheckoutSessionCompletion(pending.sessionId, {
1584
+ initialDelayMs: pending.retryAfterMs,
1585
+ timeoutMs: options?.pollTimeoutMs,
1586
+ nonce: options?.nonce
1587
+ });
1588
+ if (session.data.session?.status === "complete") {
1589
+ return new Response(null, { status: 204, statusText: "No Content" });
1590
+ }
1591
+ if (session.data.session?.status === "expired") {
1592
+ throw new import_shared2.FloPayError(
1593
+ "Checkout session has expired.",
1594
+ "api_error",
1595
+ { code: "checkout_session_expired" }
1638
1596
  );
1639
- throw error;
1640
1597
  }
1598
+ throw createCheckoutProcessingTimeoutError();
1599
+ }
1600
+ toCheckoutProcessingPending(payload, response, checkoutSessionId) {
1601
+ const retryAfterHeader = response.headers.get("Retry-After");
1602
+ const headerRetryAfterSeconds = retryAfterHeader === null || retryAfterHeader.trim() === "" ? void 0 : Number(retryAfterHeader);
1603
+ const headerRetryAfterMs = headerRetryAfterSeconds !== void 0 && Number.isFinite(headerRetryAfterSeconds) ? headerRetryAfterSeconds * 1e3 : void 0;
1604
+ return {
1605
+ type: "checkout_processing",
1606
+ sessionId: readString(payload, "sessionId") ?? checkoutSessionId,
1607
+ retryAfterMs: this.clampRetryAfterMs(
1608
+ readNumber(payload, "retryAfterMs") ?? headerRetryAfterMs ?? DEFAULT_PROCESSING_RETRY_AFTER_MS
1609
+ ),
1610
+ statusUrl: readString(payload, "statusUrl"),
1611
+ sessionUrl: readString(payload, "sessionUrl")
1612
+ };
1613
+ }
1614
+ clampRetryAfterMs(retryAfterMs) {
1615
+ return Math.max(0, Math.min(retryAfterMs, MAX_PROCESSING_RETRY_AFTER_MS));
1641
1616
  }
1642
1617
  /**
1643
- * Fetch user's prior payments by email.
1644
- * Used to determine if saved card UX should be shown.
1618
+ * Stash the display-only fields the consumer passed into a create-session
1619
+ * call. Runs after the backend assigns a UUID so a later GET on the same
1620
+ * session (typically after a redirect) can fill in fields the backend no
1621
+ * longer persists — `overrideAmount`, `totalAmount`, `name`, etc.
1622
+ *
1623
+ * No-op when no UUID is available.
1645
1624
  */
1646
- async getPaymentsByEmail(email, options) {
1647
- this.beginDirectTelemetryOperation();
1648
- const startedAt = this.telemetryTimestamp();
1649
- this.directTelemetry?.log({
1650
- name: "operation.recovery.started",
1651
- stage: "recovery",
1652
- requestCategory: "other",
1653
- paymentMethodCategory: "saved"
1654
- });
1655
- const page = options?.page ?? 1;
1656
- const limit = options?.limit ?? 1;
1657
- const params = new URLSearchParams({
1658
- email,
1659
- page: String(page),
1660
- limit: String(limit),
1661
- sortField: "createdAt",
1662
- sortDirection: "DESC"
1663
- });
1664
- try {
1665
- const response = await fetch(
1666
- `${this.baseUrl}/v1/payments?${params.toString()}`,
1667
- {
1668
- method: "GET",
1669
- signal: options?.signal,
1670
- keepalive: true
1671
- }
1672
- );
1673
- if (!response.ok) {
1674
- throw new import_shared4.FloPayError(
1675
- "Failed to fetch payments",
1676
- "api_error",
1677
- { statusCode: response.status }
1678
- );
1679
- }
1680
- const result = await response.json();
1681
- this.directTelemetry?.log({
1682
- name: "operation.recovery.completed",
1683
- stage: "recovery",
1684
- requestCategory: "other",
1685
- paymentMethodCategory: "saved",
1686
- statusClass: "2xx"
1687
- });
1688
- this.directTelemetry?.performance({
1689
- stage: "recovery",
1690
- durationMs: Math.max(0, this.telemetryTimestamp() - startedAt),
1691
- durationMode: "machine",
1692
- requestCategory: "other",
1693
- paymentMethodCategory: "saved",
1694
- statusClass: "2xx"
1695
- });
1696
- return result;
1697
- } catch (error) {
1698
- this.reportDirectFailure(error, "RECOVERY_FAILED", "recovery", "other", "saved");
1699
- throw error;
1625
+ autoCacheDisplayData(sessionId, params) {
1626
+ if (!sessionId) return;
1627
+ const products = params.products ?? (0, import_shared2.foldIntoProducts)(params.items, params.subscriptions);
1628
+ if (products.length === 0 && !params.currency) {
1629
+ return;
1700
1630
  }
1631
+ const usingUnifiedProducts = params.products !== void 0;
1632
+ const sessionCurrency = (0, import_shared2.resolveSessionCurrency)(
1633
+ params.currency,
1634
+ usingUnifiedProducts ? void 0 : params.items,
1635
+ usingUnifiedProducts ? void 0 : params.subscriptions,
1636
+ products
1637
+ );
1638
+ cacheSessionDisplayData(sessionId, {
1639
+ currency: sessionCurrency ?? void 0,
1640
+ products: products.map((p) => ({
1641
+ code: p.code ?? p.providerItemId ?? p.providerPlanId,
1642
+ type: p.type,
1643
+ name: p.name ?? p.itemName ?? p.providerItemName ?? p.subscriptionName ?? p.providerPlanName ?? null,
1644
+ totalAmount: p.totalAmount,
1645
+ overrideAmount: p.overrideAmount,
1646
+ currency: p.currency ?? sessionCurrency ?? void 0
1647
+ }))
1648
+ });
1701
1649
  }
1702
1650
  /**
1703
- * Create a checkout session AND return the full session data in one call.
1704
- * Uses `?expand=true` so the backend returns the complete session
1705
- * instead of just a UUID — eliminating the need for a second GET.
1706
- *
1707
- * Falls back to create + GET if the backend doesn't support `expand`.
1651
+ * Merge cached display-only fields (set by {@link cacheSessionDisplayData})
1652
+ * into a raw session response. Server values always win — cache fills in
1653
+ * only where the server returned `null` / `undefined`.
1708
1654
  */
1709
- async createAndFetchSession(params) {
1710
- this.beginDirectTelemetryOperation();
1711
- const startedAt = this.telemetryTimestamp();
1712
- this.directTelemetry?.log({
1713
- name: "session.create.started",
1714
- stage: "session_create",
1715
- requestCategory: "session_create"
1655
+ mergeCachedDisplayData(raw) {
1656
+ const cached = getSessionDisplayData(raw.uuid);
1657
+ const cachedProducts = /* @__PURE__ */ new Map();
1658
+ const productKey = (code) => code ? `code:${code}` : void 0;
1659
+ for (const p of cached?.products ?? []) {
1660
+ const key = productKey(p.code);
1661
+ if (key) cachedProducts.set(key, p);
1662
+ }
1663
+ const mergedProducts = (raw.products ?? []).map((p) => {
1664
+ const key = productKey(p.code);
1665
+ const fallback = key ? cachedProducts.get(key) : void 0;
1666
+ return {
1667
+ ...p,
1668
+ name: p.name ?? fallback?.name ?? null,
1669
+ totalAmount: p.totalAmount ?? fallback?.totalAmount,
1670
+ overrideAmount: p.overrideAmount ?? fallback?.overrideAmount,
1671
+ currency: p.currency ?? fallback?.currency
1672
+ };
1716
1673
  });
1717
- try {
1718
- const result = await this.createAndFetchSessionRequest(params, startedAt);
1719
- this.adoptDirectTelemetryCheckout(result.data.session?.id);
1720
- this.directTelemetry?.log({
1721
- name: "session.request.completed",
1722
- stage: "session_complete",
1723
- requestCategory: "session_create",
1724
- statusClass: "2xx"
1725
- });
1726
- this.directTelemetry?.performance({
1727
- stage: "session_create",
1728
- durationMs: this.telemetryTimestamp() - startedAt,
1729
- durationMode: "machine",
1730
- requestCategory: "session_create",
1731
- statusClass: "2xx"
1732
- });
1733
- return result;
1734
- } catch (error) {
1735
- if (!(error instanceof import_shared4.FloPayError && error.code === "session_auto_completed")) {
1736
- try {
1737
- this.telemetryHooks?.onSessionCreateFailure?.(error);
1738
- } catch {
1674
+ return {
1675
+ ...raw,
1676
+ currency: raw.currency ?? cached?.currency,
1677
+ products: mergedProducts
1678
+ };
1679
+ }
1680
+ };
1681
+ function createInstrumentedPaymentAPI(billingApiUrl, hooks) {
1682
+ const InstrumentedPaymentAPI = PaymentAPI;
1683
+ return new InstrumentedPaymentAPI(billingApiUrl, hooks);
1684
+ }
1685
+
1686
+ // src/stripe-adapter.ts
1687
+ function toStripeElementType(type) {
1688
+ const map = {
1689
+ payment: "payment",
1690
+ address: "address"
1691
+ };
1692
+ return map[type];
1693
+ }
1694
+ function toStripeAppearanceTheme(theme) {
1695
+ switch (theme) {
1696
+ case "night":
1697
+ return "night";
1698
+ case "flat":
1699
+ return "flat";
1700
+ // 'default', 'none', undefined, or any unexpected value → Stripe's baseline.
1701
+ default:
1702
+ return "stripe";
1703
+ }
1704
+ }
1705
+ function paymentMethodTypesKey(paymentMethodTypes) {
1706
+ return paymentMethodTypes ? JSON.stringify(paymentMethodTypes.map((paymentMethodType) => paymentMethodType.trim().toLowerCase())) : null;
1707
+ }
1708
+ function wrapStripeElement(stripeElement) {
1709
+ const el = stripeElement;
1710
+ return {
1711
+ mount(container) {
1712
+ el.mount(container);
1713
+ },
1714
+ unmount() {
1715
+ el.unmount();
1716
+ },
1717
+ update(options) {
1718
+ el.update(options);
1719
+ },
1720
+ on(event, handler) {
1721
+ el["on"]?.(event, handler);
1722
+ },
1723
+ off(event, handler) {
1724
+ el["off"]?.(event, handler);
1725
+ },
1726
+ destroy() {
1727
+ el.destroy();
1728
+ }
1729
+ };
1730
+ }
1731
+ function toStripeBillingDetails(billing) {
1732
+ return {
1733
+ billing_details: {
1734
+ ...billing.email ? { email: billing.email } : {},
1735
+ ...billing.name ? { name: billing.name } : {},
1736
+ ...billing.address ? {
1737
+ address: {
1738
+ ...billing.address.country ? { country: billing.address.country } : {},
1739
+ ...billing.address.postal_code ? { postal_code: billing.address.postal_code } : {},
1740
+ ...billing.address.city ? { city: billing.address.city } : {},
1741
+ ...billing.address.line1 ? { line1: billing.address.line1 } : {},
1742
+ ...billing.address.line2 ? { line2: billing.address.line2 } : {},
1743
+ ...billing.address.state ? { state: billing.address.state } : {}
1744
+ }
1745
+ } : {}
1746
+ }
1747
+ };
1748
+ }
1749
+ var StripeAdapter = class {
1750
+ constructor() {
1751
+ this.name = "stripe";
1752
+ this.stripe = null;
1753
+ this.elements = null;
1754
+ // Serialized appearance currently applied to `this.elements`. Used to detect
1755
+ // when consumers swap themes mid-session so we can live-update the Stripe
1756
+ // Elements group instead of returning a stale-styled cache. `null` while no
1757
+ // elements group exists.
1758
+ this.appliedAppearanceKey = null;
1759
+ this.appliedPaymentMethodTypesKey = null;
1760
+ this.appliedClientSecret = null;
1761
+ this.verifiedClientSecret = null;
1762
+ this.verifiedPaymentMethodTypesKey = null;
1763
+ }
1764
+ async initialize(config) {
1765
+ if (typeof window === "undefined") {
1766
+ return;
1767
+ }
1768
+ const stripe = await (0, import_stripe_js.loadStripe)(config.publishableKey, {
1769
+ locale: config.locale ?? "auto"
1770
+ });
1771
+ if (!stripe) {
1772
+ throw new import_shared3.FloPayError(
1773
+ "Failed to initialize Stripe. Check your publishable key.",
1774
+ "authentication_error"
1775
+ );
1776
+ }
1777
+ this.stripe = stripe;
1778
+ }
1779
+ /** Lazily creates the Stripe Elements group for the given options. */
1780
+ getElements(options) {
1781
+ if (!this.stripe) {
1782
+ throw new import_shared3.FloPayError(
1783
+ "StripeAdapter not initialized. Call initialize() first.",
1784
+ "api_error"
1785
+ );
1786
+ }
1787
+ const stripeAppearance = options?.appearance ? {
1788
+ theme: toStripeAppearanceTheme(options.appearance.theme),
1789
+ variables: options.appearance.variables,
1790
+ rules: options.appearance.rules
1791
+ } : void 0;
1792
+ const nextAppearanceKey = stripeAppearance ? JSON.stringify(stripeAppearance) : null;
1793
+ const nextPaymentMethodTypesKey = paymentMethodTypesKey(options?.paymentMethodTypes);
1794
+ const nextClientSecret = options?.clientSecret ?? null;
1795
+ if (this.elements && nextPaymentMethodTypesKey && (nextPaymentMethodTypesKey !== this.appliedPaymentMethodTypesKey || nextClientSecret !== this.appliedClientSecret)) {
1796
+ this.elements = null;
1797
+ this.appliedAppearanceKey = null;
1798
+ this.appliedPaymentMethodTypesKey = null;
1799
+ this.appliedClientSecret = null;
1800
+ this.verifiedClientSecret = null;
1801
+ this.verifiedPaymentMethodTypesKey = null;
1802
+ }
1803
+ if (!this.elements) {
1804
+ let elementsOptions;
1805
+ const deferredAmount = options?.amount ?? 0;
1806
+ const deferredCurrency = (options?.currency ?? "usd").toLowerCase();
1807
+ const paymentMethodCreation = options?.paymentMethodCreation ?? "manual";
1808
+ if (options?.clientSecret) {
1809
+ elementsOptions = { clientSecret: options.clientSecret };
1810
+ } else if (deferredAmount > 0) {
1811
+ elementsOptions = {
1812
+ mode: "payment",
1813
+ amount: deferredAmount,
1814
+ currency: deferredCurrency,
1815
+ paymentMethodCreation
1816
+ };
1817
+ if (options?.setupFutureUsage) {
1818
+ elementsOptions["setupFutureUsage"] = options.setupFutureUsage;
1739
1819
  }
1820
+ } else {
1821
+ elementsOptions = {
1822
+ mode: "setup",
1823
+ currency: deferredCurrency,
1824
+ paymentMethodCreation
1825
+ };
1740
1826
  }
1741
- if (error instanceof import_shared4.FloPayError && error.type === "validation_error") {
1742
- this.directTelemetry?.terminal({
1743
- outcome: "validation_rejected",
1744
- stage: "session_create",
1745
- requestCategory: "session_create"
1746
- });
1747
- } else if (!(error instanceof import_shared4.FloPayError && error.code === "session_auto_completed")) {
1748
- const statusCode = error instanceof import_shared4.FloPayError ? error.statusCode : void 0;
1749
- this.directTelemetry?.error({
1750
- errorCode: error instanceof Error && error.name === "AbortError" ? "REQUEST_TIMEOUT" : error instanceof TypeError ? "NETWORK_REQUEST_FAILED" : "CHECKOUT_SESSION_CREATE_FAILED",
1751
- stage: "session_create",
1752
- requestCategory: "session_create",
1753
- statusClass: error instanceof Error && error.name === "AbortError" ? "timeout" : statusCode ? `${Math.floor(statusCode / 100)}xx` : "network_error"
1754
- });
1827
+ if (!options?.clientSecret && options?.paymentMethodTypes) {
1828
+ elementsOptions["paymentMethodTypes"] = options.paymentMethodTypes;
1755
1829
  }
1756
- throw error;
1830
+ if (stripeAppearance) {
1831
+ elementsOptions["appearance"] = stripeAppearance;
1832
+ }
1833
+ this.elements = this.stripe.elements(elementsOptions);
1834
+ this.appliedAppearanceKey = nextAppearanceKey;
1835
+ this.appliedPaymentMethodTypesKey = nextPaymentMethodTypesKey;
1836
+ this.appliedClientSecret = nextClientSecret;
1837
+ this.verifiedClientSecret = null;
1838
+ this.verifiedPaymentMethodTypesKey = null;
1839
+ } else if (nextAppearanceKey !== this.appliedAppearanceKey) {
1840
+ this.elements.update({
1841
+ appearance: stripeAppearance ?? {}
1842
+ });
1843
+ this.appliedAppearanceKey = nextAppearanceKey;
1757
1844
  }
1845
+ return this.elements;
1758
1846
  }
1759
- async createAndFetchSessionRequest(params, telemetryStartedAt) {
1760
- const wireProducts = params.products ?? (0, import_shared4.foldIntoProducts)(params.items, params.subscriptions);
1761
- const sessionCurrency = (0, import_shared4.resolveSessionCurrency)(
1762
- params.currency,
1763
- params.items,
1764
- params.subscriptions,
1765
- wireProducts
1847
+ async assertClientSecretPaymentMethods(clientSecret, allowedPaymentMethodTypes) {
1848
+ if (!this.stripe) {
1849
+ throw new import_shared3.FloPayError(
1850
+ "StripeAdapter not initialized. Call initialize() first.",
1851
+ "api_error"
1852
+ );
1853
+ }
1854
+ let intent;
1855
+ let retrievalFailed = false;
1856
+ if ((0, import_shared3.isSetupIntentClientSecret)(clientSecret)) {
1857
+ const { setupIntent, error } = await this.stripe.retrieveSetupIntent(clientSecret);
1858
+ intent = setupIntent;
1859
+ retrievalFailed = Boolean(error);
1860
+ } else {
1861
+ const { paymentIntent, error } = await this.stripe.retrievePaymentIntent(clientSecret);
1862
+ intent = paymentIntent;
1863
+ retrievalFailed = Boolean(error);
1864
+ }
1865
+ const providerPaymentMethodTypes = intent?.payment_method_types;
1866
+ if (retrievalFailed || !Array.isArray(providerPaymentMethodTypes)) {
1867
+ throw new import_shared3.FloPayError(
1868
+ "Unable to verify the payment methods configured for this client secret.",
1869
+ "api_error",
1870
+ { param: "clientSecret" }
1871
+ );
1872
+ }
1873
+ const allowlist = new Set(
1874
+ allowedPaymentMethodTypes.map((paymentMethodType) => paymentMethodType.toLowerCase())
1766
1875
  );
1767
- if (!sessionCurrency) {
1768
- throw new import_shared4.FloPayError(
1769
- "currency is required: pass `currency` on the session, or include a `currency` on the first item/subscription/product.",
1876
+ const hasDisallowedProviderMethod = providerPaymentMethodTypes.some(
1877
+ (paymentMethodType) => typeof paymentMethodType !== "string" || !allowlist.has(paymentMethodType.trim().toLowerCase())
1878
+ );
1879
+ if (hasDisallowedProviderMethod || providerPaymentMethodTypes.length === 0) {
1880
+ throw new import_shared3.FloPayError(
1881
+ "The client-secret intent must enable only declared non-card payment methods.",
1770
1882
  "validation_error",
1771
- { code: "CurrencyRequired", param: "currency" }
1883
+ { param: "clientSecret" }
1772
1884
  );
1773
1885
  }
1774
- const payload = {
1775
- clientId: params.clientId,
1776
- checkoutVersion: import_shared4.SDK_VERSION,
1777
- successUrl: params.successUrl,
1778
- cancelUrl: params.cancelUrl,
1779
- currency: sessionCurrency,
1780
- checkoutMode: params.checkoutMode ?? "full",
1781
- products: wireProducts.map((product) => (0, import_shared4.buildProductPayload)(product, sessionCurrency)),
1782
- accountData: {
1783
- userId: params.account.userId,
1784
- firstName: params.account.firstName ?? null,
1785
- lastName: params.account.lastName ?? null,
1786
- email: params.account.email,
1787
- country: params.account.country ?? null,
1788
- gender: params.account.gender ?? null,
1789
- city: params.account.city ?? null,
1790
- state: params.account.state ?? null,
1791
- zip: params.account.zip ?? null,
1792
- addressLine1: params.account.addressLine1 ?? null,
1793
- addressLine2: params.account.addressLine2 ?? null
1886
+ }
1887
+ async createElement(type, options) {
1888
+ let resolvedOptions = options;
1889
+ if (type === "payment") {
1890
+ const paymentMethodTypes = options.paymentMethodTypes?.map((paymentMethodType) => paymentMethodType.trim()).filter((paymentMethodType) => paymentMethodType && paymentMethodType.toLowerCase() !== "card");
1891
+ if (!paymentMethodTypes?.length) {
1892
+ throw new import_shared3.FloPayError(
1893
+ "At least one supported non-card payment method is required.",
1894
+ "validation_error",
1895
+ { param: "paymentMethodTypes" }
1896
+ );
1897
+ }
1898
+ resolvedOptions = { ...options, paymentMethodTypes };
1899
+ if (resolvedOptions.clientSecret) {
1900
+ const resolvedPaymentMethodTypesKey = paymentMethodTypesKey(paymentMethodTypes);
1901
+ const verificationIsCached = Boolean(
1902
+ this.elements && this.appliedClientSecret === resolvedOptions.clientSecret && this.appliedPaymentMethodTypesKey === resolvedPaymentMethodTypesKey && this.verifiedClientSecret === resolvedOptions.clientSecret && this.verifiedPaymentMethodTypesKey === resolvedPaymentMethodTypesKey
1903
+ );
1904
+ if (!verificationIsCached) {
1905
+ await this.assertClientSecretPaymentMethods(
1906
+ resolvedOptions.clientSecret,
1907
+ paymentMethodTypes
1908
+ );
1909
+ }
1910
+ }
1911
+ }
1912
+ const elements = this.getElements(resolvedOptions);
1913
+ if (type === "payment" && resolvedOptions.clientSecret) {
1914
+ this.verifiedClientSecret = resolvedOptions.clientSecret;
1915
+ this.verifiedPaymentMethodTypesKey = paymentMethodTypesKey(
1916
+ resolvedOptions.paymentMethodTypes
1917
+ );
1918
+ }
1919
+ const stripeType = toStripeElementType(type);
1920
+ const elementOptions = {};
1921
+ if (resolvedOptions.layout) {
1922
+ elementOptions["layout"] = resolvedOptions.layout;
1923
+ }
1924
+ if (resolvedOptions.defaultValues) {
1925
+ elementOptions["defaultValues"] = resolvedOptions.defaultValues;
1926
+ }
1927
+ if (resolvedOptions.readOnly) {
1928
+ elementOptions["readOnly"] = resolvedOptions.readOnly;
1929
+ }
1930
+ if (resolvedOptions.mode) {
1931
+ elementOptions["mode"] = resolvedOptions.mode;
1932
+ }
1933
+ const stripeElement = elements.create(stripeType, elementOptions);
1934
+ return wrapStripeElement(stripeElement);
1935
+ }
1936
+ getElement(type) {
1937
+ if (!this.elements) return null;
1938
+ const stripeType = toStripeElementType(type);
1939
+ const existing = this.elements.getElement(stripeType);
1940
+ if (!existing) return null;
1941
+ return wrapStripeElement(existing);
1942
+ }
1943
+ async submitElements() {
1944
+ if (!this.stripe || !this.elements) {
1945
+ return { error: new import_shared3.FloPayError("Stripe not initialized", "api_error") };
1946
+ }
1947
+ const { error } = await this.elements.submit();
1948
+ if (error) {
1949
+ return {
1950
+ error: new import_shared3.FloPayError(error.message ?? "Validation failed", "validation_error")
1951
+ };
1952
+ }
1953
+ return {};
1954
+ }
1955
+ async confirmPayment(params) {
1956
+ if (!this.stripe || !this.elements) {
1957
+ throw new import_shared3.FloPayError(
1958
+ "StripeAdapter not initialized or no elements created.",
1959
+ "api_error"
1960
+ );
1961
+ }
1962
+ const billing = params.billingDetails;
1963
+ const paymentMethodData = billing ? toStripeBillingDetails(billing) : void 0;
1964
+ const { error, paymentIntent } = await this.stripe.confirmPayment({
1965
+ elements: this.elements,
1966
+ clientSecret: params.clientSecret,
1967
+ confirmParams: {
1968
+ return_url: params.returnUrl ?? window.location.href,
1969
+ ...paymentMethodData ? { payment_method_data: paymentMethodData } : {}
1794
1970
  },
1795
- couponCodes: params.couponCodes ?? []
1971
+ redirect: "if_required"
1972
+ });
1973
+ if (error) {
1974
+ return {
1975
+ status: "failed",
1976
+ error: new import_shared3.FloPayError(
1977
+ error.message ?? "Payment failed",
1978
+ "api_error",
1979
+ {
1980
+ code: error.code,
1981
+ declineCode: error.decline_code
1982
+ }
1983
+ )
1984
+ };
1985
+ }
1986
+ if (!paymentIntent) {
1987
+ return { status: "failed", error: new import_shared3.FloPayError("No payment intent returned", "api_error") };
1988
+ }
1989
+ const statusMap = {
1990
+ succeeded: "succeeded",
1991
+ processing: "processing",
1992
+ requires_action: "requires_action",
1993
+ requires_payment_method: "failed",
1994
+ canceled: "failed"
1796
1995
  };
1797
- if (params.tokenizedData) payload["tokenizedData"] = params.tokenizedData;
1798
- if (params.tagsData) payload["tagsData"] = params.tagsData;
1799
- if (params.utmMetadata?.length) payload["utmMetadata"] = params.utmMetadata;
1800
- if (params.avsCheck !== void 0) payload["avsCheck"] = params.avsCheck;
1801
- if (params.checkoutType) payload["checkoutType"] = params.checkoutType;
1802
- if (params.checkoutLayout) payload["checkoutLayout"] = params.checkoutLayout;
1803
- if (params.avsConfig) payload["avsConfig"] = params.avsConfig;
1804
- const headers = {
1805
- "Content-Type": "application/json",
1806
- // Declare the SDK version so backends at TeamFloPay/backend#823 embed
1807
- // the hosted vault capture block (`body.vault`) in the response for
1808
- // SDKs ≥ 1.3.0. Older backends ignore the header.
1809
- [import_shared4.FLO_SDK_VERSION_HEADER]: import_shared4.SDK_VERSION
1996
+ return {
1997
+ status: statusMap[paymentIntent.status] ?? "failed",
1998
+ paymentIntentId: paymentIntent.id,
1999
+ paymentMethodId: this.extractPaymentMethodId(paymentIntent.payment_method)
1810
2000
  };
1811
- const idempotencyKey = (0, import_shared4.resolveIdempotencyKey)(params.idempotencyKey);
1812
- if (idempotencyKey) {
1813
- headers[import_shared4.IDEMPOTENCY_KEY_HEADER] = idempotencyKey;
2001
+ }
2002
+ extractPaymentMethodId(paymentMethod) {
2003
+ if (typeof paymentMethod === "string" && paymentMethod.startsWith("pm_")) {
2004
+ return paymentMethod;
1814
2005
  }
1815
- let response;
1816
- let firstByteReported = false;
1817
- for (let attempt = 0; ; attempt++) {
1818
- response = await fetchWithNetworkRetry(
1819
- `${this.baseUrl}/v1/checkouts/sessions?expand=true`,
1820
- {
1821
- method: "POST",
1822
- headers,
1823
- body: JSON.stringify(payload)
1824
- },
1825
- NETWORK_RETRY_ATTEMPTS,
1826
- (networkAttempt) => {
1827
- this.telemetryHooks?.onRetry?.("session_create", networkAttempt);
1828
- this.directTelemetry?.log({
1829
- name: "operation.retry",
1830
- stage: "session_create",
1831
- requestCategory: "session_create",
1832
- attempt: networkAttempt
1833
- });
1834
- }
1835
- );
1836
- if (!firstByteReported) {
1837
- firstByteReported = true;
1838
- const statusClass = `${Math.floor(response.status / 100)}xx`;
1839
- this.directTelemetry?.log({
1840
- name: "session.request.first_byte",
1841
- stage: "session_first_byte",
1842
- requestCategory: "session_create",
1843
- statusClass
1844
- });
1845
- this.directTelemetry?.performance({
1846
- stage: "session_first_byte",
1847
- durationMs: this.telemetryTimestamp() - telemetryStartedAt,
1848
- durationMode: "machine",
1849
- requestCategory: "session_create",
1850
- statusClass
1851
- });
2006
+ if (paymentMethod && typeof paymentMethod === "object" && typeof paymentMethod.id === "string") {
2007
+ return paymentMethod.id;
2008
+ }
2009
+ return void 0;
2010
+ }
2011
+ async confirmPayPalPayment(params) {
2012
+ if (!this.stripe) {
2013
+ return { status: "failed", error: new import_shared3.FloPayError("Stripe not initialized", "api_error") };
2014
+ }
2015
+ const baseUrl = params.billingApiUrl.replace(/\/+$/, "");
2016
+ if (this.elements) {
2017
+ const { error: submitError } = await this.elements.submit();
2018
+ if (submitError) {
2019
+ return {
2020
+ status: "failed",
2021
+ error: new import_shared3.FloPayError(
2022
+ submitError.message ?? "PayPal payment failed",
2023
+ "validation_error",
2024
+ { code: submitError.code }
2025
+ )
2026
+ };
1852
2027
  }
1853
- if (response.status === 204) {
1854
- throw new import_shared4.FloPayError(
1855
- "Session auto-completed \u2014 payment method already on file",
1856
- "api_error",
1857
- { code: "session_auto_completed" }
1858
- );
2028
+ }
2029
+ let intentClientSecret;
2030
+ try {
2031
+ const intent = await new PaymentAPI(baseUrl).createSessionIntent(
2032
+ params.sessionId,
2033
+ params.nonce ?? "",
2034
+ {
2035
+ provider: "stripe",
2036
+ paymentMethodCategory: "wallet",
2037
+ paymentMethodType: "paypal",
2038
+ paymentMethodId: null,
2039
+ intentKind: "payment"
2040
+ }
2041
+ );
2042
+ if (intent.provider !== "stripe") {
2043
+ throw new import_shared3.FloPayError("Invalid provider returned for PayPal intent", "api_error");
1859
2044
  }
1860
- if (response.ok) break;
1861
- const error = await buildApiErrorFromResponse(response, "Failed to create checkout session");
1862
- if (error.code === import_shared4.IDEMPOTENCY_IN_PROGRESS_CODE && attempt < IDEMPOTENCY_IN_PROGRESS_RETRY_ATTEMPTS) {
2045
+ intentClientSecret = intent.clientSecret;
2046
+ } catch (error2) {
2047
+ return {
2048
+ status: "failed",
2049
+ error: error2 instanceof import_shared3.FloPayError ? error2 : new import_shared3.FloPayError("Failed to create PayPal payment intent", "api_error")
2050
+ };
2051
+ }
2052
+ const { error } = await this.stripe.confirmPayment({
2053
+ clientSecret: intentClientSecret,
2054
+ elements: this.elements ?? void 0,
2055
+ confirmParams: { return_url: params.returnUrl }
2056
+ });
2057
+ if (error) {
2058
+ if (params.nonce) {
1863
2059
  try {
1864
- this.telemetryHooks?.onRetry?.("session_create", attempt + 1);
1865
- this.directTelemetry?.log({
1866
- name: "operation.retry",
1867
- stage: "session_create",
1868
- requestCategory: "session_create",
1869
- attempt: attempt + 1
1870
- });
2060
+ await new PaymentAPI(baseUrl).reportSessionIntentDecline(
2061
+ params.sessionId,
2062
+ params.nonce,
2063
+ {
2064
+ provider: "stripe",
2065
+ paymentMethodCategory: "wallet",
2066
+ paymentMethodType: "paypal",
2067
+ providerDeclineReason: error.code ?? "provider_declined"
2068
+ }
2069
+ );
1871
2070
  } catch {
1872
2071
  }
1873
- await delay(150 * 2 ** attempt);
1874
- continue;
1875
- }
1876
- throw error;
1877
- }
1878
- const body = await response.json();
1879
- if (body.data && "gateways" in body.data) {
1880
- this.autoCacheDisplayData(body.data.uuid, params);
1881
- const merged = this.mergeCachedDisplayData(body.data);
1882
- const normalized = this.normalizeRawSession(merged);
1883
- if (body.vault && normalized.data.session) {
1884
- normalized.data.session.vault = this.toVaultBlock(body.vault);
1885
2072
  }
1886
2073
  return {
1887
- ...normalized,
1888
- autoProcessingError: body.autoProcessingError,
1889
- autoProcessingAttempted: body.autoProcessingAttempted,
1890
- autoProcessingPending: body.autoProcessingPending
2074
+ status: "failed",
2075
+ error: new import_shared3.FloPayError(error.message ?? "PayPal payment failed", "api_error", { code: error.code })
1891
2076
  };
1892
2077
  }
1893
- const uuid = body.data?.uuid;
1894
- if (!uuid) {
1895
- throw new import_shared4.FloPayError("No session ID returned", "api_error");
1896
- }
1897
- this.autoCacheDisplayData(uuid, params);
1898
- this.adoptDirectTelemetryCheckout(uuid);
1899
- const unifiedSession = await this.getUnifiedCheckoutSession(uuid);
1900
2078
  return {
1901
- ...unifiedSession,
1902
- autoProcessingError: body.autoProcessingError,
1903
- autoProcessingAttempted: body.autoProcessingAttempted,
1904
- autoProcessingPending: body.autoProcessingPending
2079
+ status: "processing"
1905
2080
  };
1906
2081
  }
1907
- async waitForCheckoutSessionCompletion(checkoutSessionId, options) {
1908
- this.beginDirectTelemetryCheckout(checkoutSessionId);
1909
- const startedAt = this.telemetryTimestamp();
1910
- this.directTelemetry?.log({
1911
- name: "operation.recovery.started",
1912
- stage: "recovery",
1913
- requestCategory: "session_read",
1914
- paymentMethodCategory: "saved"
1915
- });
1916
- const timeoutMs = options?.timeoutMs ?? DEFAULT_PROCESSING_TIMEOUT_MS;
1917
- const deadline = Date.now() + timeoutMs;
1918
- let nextDelayMs = this.clampRetryAfterMs(options?.initialDelayMs ?? DEFAULT_PROCESSING_RETRY_AFTER_MS);
1919
- let pollAttempt = 0;
1920
- try {
1921
- while (true) {
1922
- const remainingMs = deadline - Date.now();
1923
- if (remainingMs <= 0) {
1924
- throw createCheckoutProcessingTimeoutError();
1925
- }
1926
- if (nextDelayMs > 0) {
1927
- try {
1928
- pollAttempt += 1;
1929
- this.telemetryHooks?.onRetry?.("session_read", pollAttempt);
1930
- this.directTelemetry?.log({
1931
- name: "operation.retry",
1932
- stage: "recovery",
1933
- requestCategory: "session_read",
1934
- paymentMethodCategory: "saved",
1935
- attempt: pollAttempt
1936
- });
1937
- } catch {
1938
- }
1939
- await delay(Math.min(nextDelayMs, remainingMs));
1940
- if (Date.now() >= deadline) {
1941
- throw createCheckoutProcessingTimeoutError();
1942
- }
1943
- }
1944
- const session = await this.getUnifiedCheckoutSession(checkoutSessionId, options?.nonce);
1945
- const status = session.data.session?.status;
1946
- if (status === "complete" || status === "expired") {
1947
- this.directTelemetry?.log({
1948
- name: "operation.recovery.completed",
1949
- stage: "recovery",
1950
- requestCategory: "session_read",
1951
- paymentMethodCategory: "saved"
1952
- });
1953
- this.directTelemetry?.performance({
1954
- stage: "recovery",
1955
- durationMs: Math.max(0, this.telemetryTimestamp() - startedAt),
1956
- durationMode: "machine",
1957
- requestCategory: "session_read",
1958
- paymentMethodCategory: "saved"
1959
- });
1960
- return session;
1961
- }
1962
- if (Date.now() >= deadline) {
1963
- throw createCheckoutProcessingTimeoutError();
1964
- }
1965
- nextDelayMs = this.clampRetryAfterMs(
1966
- Math.max(nextDelayMs * 2, MIN_PROCESSING_RETRY_AFTER_MS)
1967
- );
1968
- }
1969
- } catch (error) {
1970
- if (error instanceof import_shared4.FloPayError && error.code === "checkout_processing_timeout") {
1971
- this.reportDirectFailure(
1972
- error,
1973
- "RECOVERY_FAILED",
1974
- "recovery",
1975
- "session_read",
1976
- "saved"
1977
- );
1978
- }
1979
- throw error;
2082
+ async resumePayPalPayment() {
2083
+ if (!this.stripe || typeof window === "undefined") return null;
2084
+ const params = new URLSearchParams(window.location.search);
2085
+ const paymentIntentId = params.get("payment_intent");
2086
+ const clientSecret = params.get("payment_intent_client_secret");
2087
+ const redirectStatus = params.get("redirect_status");
2088
+ if (!paymentIntentId || !clientSecret) return null;
2089
+ if (redirectStatus === "failed") {
2090
+ return {
2091
+ status: "failed",
2092
+ error: new import_shared3.FloPayError("PayPal payment was declined. Please try again.", "api_error")
2093
+ };
1980
2094
  }
1981
- }
1982
- /** Normalize a raw session into a provider-agnostic shape. */
1983
- normalizeRawSession(session) {
1984
- const gateways = session.gateways ?? {};
1985
- const providers = [];
1986
- const data = {
1987
- session: this.toCheckoutSession(session)
1988
- };
1989
- const stripeGateway = gateways.stripe;
1990
- if (stripeGateway?.publishableKey) {
1991
- providers.push("stripe");
1992
- const rawSession = session;
1993
- const stripeClientSecret = [
1994
- rawSession["stripeClientSecret"],
1995
- stripeGateway.stripeClientSecret
1996
- ].find((value) => typeof value === "string" && value.length > 0);
1997
- data.stripe = {
1998
- clientSecret: stripeClientSecret ?? "",
1999
- publishableKey: stripeGateway.publishableKey ?? void 0,
2000
- paypalPublishableKey: stripeGateway.paypalPublishableKey ?? void 0,
2001
- environment: stripeGateway.environment,
2002
- enabledPaymentMethods: Array.isArray(stripeGateway.enabledPaymentMethods) ? stripeGateway.enabledPaymentMethods.filter((m) => typeof m === "string") : void 0
2095
+ const { paymentIntent, error } = await this.stripe.retrievePaymentIntent(clientSecret);
2096
+ if (error) {
2097
+ return {
2098
+ status: "failed",
2099
+ error: new import_shared3.FloPayError(error.message ?? "Failed to retrieve PayPal payment", "api_error")
2003
2100
  };
2004
2101
  }
2005
- const paypalGateway = gateways.paypal;
2006
- if (paypalGateway?.publishableKey) {
2007
- providers.push("paypal");
2008
- data.paypal = {
2009
- publishableKey: paypalGateway.publishableKey,
2010
- environment: paypalGateway.environment
2102
+ if (paymentIntent && (paymentIntent.status === "requires_capture" || paymentIntent.status === "succeeded")) {
2103
+ const pmId = typeof paymentIntent.payment_method === "string" ? paymentIntent.payment_method : paymentIntent.payment_method?.id;
2104
+ const url = new URL(window.location.href);
2105
+ url.searchParams.delete("payment_intent");
2106
+ url.searchParams.delete("payment_intent_client_secret");
2107
+ url.searchParams.delete("redirect_status");
2108
+ window.history.replaceState({}, "", url.toString());
2109
+ return {
2110
+ status: paymentIntent.status,
2111
+ paymentIntentId: paymentIntent.id,
2112
+ paymentMethodId: pmId
2011
2113
  };
2012
2114
  }
2013
2115
  return {
2014
- providers,
2015
- mode: "tokenize",
2016
- data,
2017
- raw: { data: session }
2116
+ status: "failed",
2117
+ error: new import_shared3.FloPayError("PayPal payment was not completed. Please try again.", "api_error")
2018
2118
  };
2019
2119
  }
2020
- /** Convert raw session to the SDK CheckoutSession shape. */
2021
- toCheckoutSession(raw) {
2022
- const rawProducts = raw.products ?? [];
2023
- const hasBackendTotal = typeof raw.totalAmount === "number" && Number.isFinite(raw.totalAmount);
2024
- const computedTotal = rawProducts.reduce(
2025
- (sum, p) => sum + (p.overrideAmount ?? p.totalAmount ?? 0),
2026
- 0
2027
- );
2028
- const totalAmount = hasBackendTotal ? raw.totalAmount : computedTotal;
2029
- const amountInCents = Math.round(totalAmount * 100);
2030
- const currency = raw.currency ?? rawProducts[0]?.currency ?? "USD";
2031
- const mode = rawProducts.some((p) => p.type === "subscription") ? "subscription" : "payment";
2032
- return {
2033
- id: raw.uuid,
2034
- clientSecret: raw.nonce,
2035
- mode,
2036
- status: this.toCheckoutSessionStatus(raw.status),
2037
- amount: amountInCents,
2038
- currency,
2039
- customer: {
2040
- id: raw.accountData.userId,
2041
- email: raw.accountData.email,
2042
- firstName: raw.accountData.firstName,
2043
- lastName: raw.accountData.lastName,
2044
- country: raw.accountData.country ?? void 0,
2045
- city: raw.accountData.city ?? void 0,
2046
- state: raw.accountData.state ?? void 0,
2047
- zip: raw.accountData.zip ?? void 0,
2048
- gender: raw.accountData.gender ?? void 0,
2049
- line1: raw.accountData.addressLine1 ?? void 0,
2050
- line2: raw.accountData.addressLine2 ?? void 0
2051
- },
2052
- metadata: {},
2053
- checkoutMode: raw.checkoutMode,
2054
- providerPaymentMethodId: typeof raw.providerPaymentMethodId === "string" ? raw.providerPaymentMethodId : null,
2055
- products: rawProducts.map((p) => ({
2056
- ...p,
2057
- totalAmount: typeof p.totalAmount === "number" ? p.totalAmount : void 0,
2058
- overrideAmount: typeof p.overrideAmount === "number" ? p.overrideAmount : null,
2059
- currency: typeof p.currency === "string" ? p.currency : void 0,
2060
- metadata: p.metadata ?? null
2061
- })),
2062
- successUrl: raw.successUrl,
2063
- cancelUrl: raw.cancelUrl,
2064
- coupons: raw.coupons,
2065
- subtotalAmount: raw.subtotalAmount,
2066
- discountAmount: raw.discountAmount,
2067
- totalAmount: raw.totalAmount,
2068
- createdAt: raw.createdAt,
2069
- gateways: raw.gateways,
2070
- accountData: raw.accountData,
2071
- tagsData: raw.tagsData
2072
- };
2120
+ getRawProvider() {
2121
+ return this.stripe;
2073
2122
  }
2074
- /**
2075
- * Coerce a raw vault block into a typed {@link VaultCaptureBlock}. The
2076
- * server-only PCIVault submit `secret` is deliberately dropped so it never
2077
- * lands on the public session surface (logs / telemetry / client inspection).
2078
- */
2079
- toVaultBlock(raw) {
2080
- return {
2081
- html: typeof raw.html === "string" ? raw.html : void 0,
2082
- url: typeof raw.url === "string" ? raw.url : void 0,
2083
- messageToken: typeof raw.messageToken === "string" ? raw.messageToken : void 0,
2084
- expectedOrigin: typeof raw.expectedOrigin === "string" ? raw.expectedOrigin : void 0
2123
+ createPayPalElements(options) {
2124
+ if (!this.stripe) return null;
2125
+ const elementsOptions = {
2126
+ mode: "payment",
2127
+ amount: options.amount ?? 0,
2128
+ currency: (options.currency ?? "usd").toLowerCase(),
2129
+ captureMethod: "manual"
2085
2130
  };
2086
- }
2087
- toCheckoutSessionStatus(status) {
2088
- if (status === "completed") {
2089
- return "complete";
2131
+ if (options.setupFutureUsage) {
2132
+ elementsOptions["setupFutureUsage"] = options.setupFutureUsage;
2090
2133
  }
2091
- if (status === "expired") {
2092
- return "expired";
2134
+ if (options.appearance) {
2135
+ elementsOptions["appearance"] = {
2136
+ theme: toStripeAppearanceTheme(options.appearance.theme),
2137
+ variables: options.appearance.variables,
2138
+ rules: options.appearance.rules
2139
+ };
2093
2140
  }
2094
- return "open";
2141
+ return this.stripe.elements(elementsOptions);
2095
2142
  }
2096
- async resolveProcessResponse(response, checkoutSessionId, options) {
2097
- if (response.status !== 202) {
2098
- return response;
2143
+ destroy() {
2144
+ this.elements = null;
2145
+ this.appliedAppearanceKey = null;
2146
+ this.appliedPaymentMethodTypesKey = null;
2147
+ this.appliedClientSecret = null;
2148
+ this.verifiedClientSecret = null;
2149
+ this.verifiedPaymentMethodTypesKey = null;
2150
+ this.stripe = null;
2151
+ }
2152
+ };
2153
+
2154
+ // src/flopay.ts
2155
+ var import_shared6 = require("@flopay/shared");
2156
+
2157
+ // src/elements.ts
2158
+ var import_shared4 = require("@flopay/shared");
2159
+ var FloPayElements = class {
2160
+ constructor(provider, options) {
2161
+ this.elementMap = /* @__PURE__ */ new Map();
2162
+ this.provider = provider;
2163
+ this.baseOptions = options ?? {};
2164
+ }
2165
+ /**
2166
+ * Creates a new element of the given type.
2167
+ * If an element of that type already exists, it is destroyed first.
2168
+ */
2169
+ async create(type, options) {
2170
+ const merged = { ...this.baseOptions, ...options };
2171
+ if (type === "payment") {
2172
+ const paymentMethodTypes = merged.paymentMethodTypes?.map((paymentMethodType) => paymentMethodType.trim()).filter((paymentMethodType) => paymentMethodType && paymentMethodType.toLowerCase() !== "card");
2173
+ if (!paymentMethodTypes?.length) {
2174
+ throw new import_shared4.FloPayError(
2175
+ "At least one supported non-card payment method is required.",
2176
+ "validation_error",
2177
+ { param: "paymentMethodTypes" }
2178
+ );
2179
+ }
2180
+ merged.paymentMethodTypes = paymentMethodTypes;
2099
2181
  }
2100
- const payload = await response.json().catch(() => null);
2101
- const pending = this.toCheckoutProcessingPending(payload, response, checkoutSessionId);
2102
- const session = await this.waitForCheckoutSessionCompletion(pending.sessionId, {
2103
- initialDelayMs: pending.retryAfterMs,
2104
- timeoutMs: options?.pollTimeoutMs,
2105
- nonce: options?.nonce
2106
- });
2107
- if (session.data.session?.status === "complete") {
2108
- return new Response(null, { status: 204, statusText: "No Content" });
2182
+ const providerExisting = this.provider.getElement(type);
2183
+ if (providerExisting) {
2184
+ this.elementMap.set(type, providerExisting);
2185
+ return providerExisting;
2109
2186
  }
2110
- if (session.data.session?.status === "expired") {
2111
- throw new import_shared4.FloPayError(
2112
- "Checkout session has expired.",
2113
- "api_error",
2114
- { code: "checkout_session_expired" }
2115
- );
2187
+ const existing = this.elementMap.get(type);
2188
+ if (existing) {
2189
+ existing.destroy();
2116
2190
  }
2117
- throw createCheckoutProcessingTimeoutError();
2118
- }
2119
- toCheckoutProcessingPending(payload, response, checkoutSessionId) {
2120
- const retryAfterHeader = response.headers.get("Retry-After");
2121
- const headerRetryAfterSeconds = retryAfterHeader === null || retryAfterHeader.trim() === "" ? void 0 : Number(retryAfterHeader);
2122
- const headerRetryAfterMs = headerRetryAfterSeconds !== void 0 && Number.isFinite(headerRetryAfterSeconds) ? headerRetryAfterSeconds * 1e3 : void 0;
2123
- return {
2124
- type: "checkout_processing",
2125
- sessionId: readString(payload, "sessionId") ?? checkoutSessionId,
2126
- retryAfterMs: this.clampRetryAfterMs(
2127
- readNumber(payload, "retryAfterMs") ?? headerRetryAfterMs ?? DEFAULT_PROCESSING_RETRY_AFTER_MS
2128
- ),
2129
- statusUrl: readString(payload, "statusUrl"),
2130
- sessionUrl: readString(payload, "sessionUrl")
2131
- };
2191
+ const element = await this.provider.createElement(type, merged);
2192
+ this.elementMap.set(type, element);
2193
+ return element;
2132
2194
  }
2133
- clampRetryAfterMs(retryAfterMs) {
2134
- return Math.max(0, Math.min(retryAfterMs, MAX_PROCESSING_RETRY_AFTER_MS));
2195
+ /** Returns a previously created element, or `null`. */
2196
+ getElement(type) {
2197
+ return this.elementMap.get(type) ?? null;
2135
2198
  }
2136
2199
  /**
2137
- * Stash the display-only fields the consumer passed into a create-session
2138
- * call. Runs after the backend assigns a UUID so a later GET on the same
2139
- * session (typically after a redirect) can fill in fields the backend no
2140
- * longer persists — `overrideAmount`, `totalAmount`, `name`, etc.
2200
+ * Submits all mounted elements for validation.
2141
2201
  *
2142
- * No-op when no UUID is available.
2202
+ * Returns an object with an optional error if validation fails.
2203
+ * This does NOT confirm the payment — call `floPay.confirmPayment()` for that.
2143
2204
  */
2144
- autoCacheDisplayData(sessionId, params) {
2145
- if (!sessionId) return;
2146
- const products = params.products ?? (0, import_shared4.foldIntoProducts)(params.items, params.subscriptions);
2147
- if (products.length === 0 && !params.currency) {
2148
- return;
2149
- }
2150
- const usingUnifiedProducts = params.products !== void 0;
2151
- const sessionCurrency = (0, import_shared4.resolveSessionCurrency)(
2152
- params.currency,
2153
- usingUnifiedProducts ? void 0 : params.items,
2154
- usingUnifiedProducts ? void 0 : params.subscriptions,
2155
- products
2156
- );
2157
- cacheSessionDisplayData(sessionId, {
2158
- currency: sessionCurrency ?? void 0,
2159
- products: products.map((p) => ({
2160
- code: p.code ?? p.providerItemId ?? p.providerPlanId,
2161
- type: p.type,
2162
- name: p.name ?? p.itemName ?? p.providerItemName ?? p.subscriptionName ?? p.providerPlanName ?? null,
2163
- totalAmount: p.totalAmount,
2164
- overrideAmount: p.overrideAmount,
2165
- currency: p.currency ?? sessionCurrency ?? void 0
2166
- }))
2167
- });
2205
+ async submit() {
2206
+ return {};
2168
2207
  }
2169
- /**
2170
- * Merge cached display-only fields (set by {@link cacheSessionDisplayData})
2171
- * into a raw session response. Server values always win — cache fills in
2172
- * only where the server returned `null` / `undefined`.
2173
- */
2174
- mergeCachedDisplayData(raw) {
2175
- const cached = getSessionDisplayData(raw.uuid);
2176
- const cachedProducts = /* @__PURE__ */ new Map();
2177
- const productKey = (code) => code ? `code:${code}` : void 0;
2178
- for (const p of cached?.products ?? []) {
2179
- const key = productKey(p.code);
2180
- if (key) cachedProducts.set(key, p);
2208
+ /** Destroys all created elements and clears the internal map. */
2209
+ destroy() {
2210
+ for (const element of this.elementMap.values()) {
2211
+ element.destroy();
2181
2212
  }
2182
- const mergedProducts = (raw.products ?? []).map((p) => {
2183
- const key = productKey(p.code);
2184
- const fallback = key ? cachedProducts.get(key) : void 0;
2185
- return {
2186
- ...p,
2187
- name: p.name ?? fallback?.name ?? null,
2188
- totalAmount: p.totalAmount ?? fallback?.totalAmount,
2189
- overrideAmount: p.overrideAmount ?? fallback?.overrideAmount,
2190
- currency: p.currency ?? fallback?.currency
2191
- };
2192
- });
2193
- return {
2194
- ...raw,
2195
- currency: raw.currency ?? cached?.currency,
2196
- products: mergedProducts
2197
- };
2213
+ this.elementMap.clear();
2198
2214
  }
2199
2215
  };
2200
- function createInstrumentedPaymentAPI(billingApiUrl, hooks) {
2201
- const InstrumentedPaymentAPI = PaymentAPI;
2202
- return new InstrumentedPaymentAPI(billingApiUrl, hooks);
2203
- }
2204
2216
 
2205
2217
  // src/pci-vault-card-capture.ts
2206
2218
  var import_shared5 = require("@flopay/shared");
@@ -2859,7 +2871,6 @@ function telemetryProvider(name) {
2859
2871
  var FloPay = class {
2860
2872
  constructor(provider, config, telemetryReporter) {
2861
2873
  this.currentElements = null;
2862
- this.cardThreeDsStartedAt = /* @__PURE__ */ new Map();
2863
2874
  this.provider = provider;
2864
2875
  this.config = config;
2865
2876
  this.telemetryReporter = telemetryReporter ?? new TelemetryReporter({
@@ -2892,176 +2903,9 @@ var FloPay = class {
2892
2903
  async submitElements() {
2893
2904
  return this.provider.submitElements();
2894
2905
  }
2895
- /** Create a payment method from the current elements (tokenize card). */
2896
- async createPaymentMethod(billingDetails) {
2897
- const started = this.now();
2898
- const provider = telemetryProvider(this.provider.name);
2899
- this.telemetryReporter?.log({
2900
- name: "payment.tokenization.started",
2901
- stage: "tokenization",
2902
- provider,
2903
- paymentMethodCategory: "card"
2904
- });
2905
- try {
2906
- const result = await this.provider.createPaymentMethod(billingDetails);
2907
- this.telemetryReporter?.performance({
2908
- stage: "tokenization",
2909
- durationMs: this.now() - started,
2910
- durationMode: "machine",
2911
- provider,
2912
- paymentMethodCategory: "card"
2913
- });
2914
- if (!result.error) {
2915
- this.telemetryReporter?.log({
2916
- name: "payment.tokenization.completed",
2917
- stage: "tokenization",
2918
- provider,
2919
- paymentMethodCategory: "card"
2920
- });
2921
- } else if (result.error.type !== "validation_error") {
2922
- this.telemetryReporter?.error({
2923
- errorCode: "TOKENIZATION_FAILED",
2924
- stage: "tokenization",
2925
- provider,
2926
- paymentMethodCategory: "card"
2927
- });
2928
- }
2929
- return result;
2930
- } catch (error) {
2931
- this.telemetryReporter?.performance({
2932
- stage: "tokenization",
2933
- durationMs: this.now() - started,
2934
- durationMode: "machine",
2935
- provider,
2936
- paymentMethodCategory: "card"
2937
- });
2938
- this.telemetryReporter?.error({
2939
- errorCode: "TOKENIZATION_FAILED",
2940
- stage: "tokenization",
2941
- provider,
2942
- paymentMethodCategory: "card"
2943
- });
2944
- throw error;
2945
- }
2946
- }
2947
- /** Confirm a card payment with a known client secret and payment method ID. */
2948
- async confirmCardPayment(params) {
2949
- const provider = telemetryProvider(this.provider.name);
2950
- const operationStartedAt = this.now();
2951
- try {
2952
- const result = await this.provider.confirmCardPayment(params);
2953
- const completedAt = this.now();
2954
- const threeDs = result.threeDs;
2955
- let threeDsFailed = false;
2956
- if (threeDs?.status === "handoff") {
2957
- if (this.trackCardThreeDsAttempt(threeDs.attemptId, completedAt)) {
2958
- this.telemetryReporter?.log({
2959
- name: "payment.three_ds.handoff",
2960
- stage: "three_ds_handoff",
2961
- provider,
2962
- paymentMethodCategory: "card"
2963
- });
2964
- this.telemetryReporter?.performance({
2965
- stage: "three_ds_handoff",
2966
- durationMs: completedAt - operationStartedAt,
2967
- durationMode: "machine",
2968
- provider,
2969
- paymentMethodCategory: "card"
2970
- });
2971
- this.telemetryReporter?.terminal({
2972
- outcome: "action_required",
2973
- stage: "three_ds_handoff",
2974
- provider,
2975
- paymentMethodCategory: "card"
2976
- });
2977
- }
2978
- } else if (threeDs) {
2979
- const threeDsStartedAt = this.takeCardThreeDsAttempt(threeDs.attemptId, completedAt);
2980
- if (threeDsStartedAt !== void 0) {
2981
- if (threeDs.status === "returned") {
2982
- this.telemetryReporter?.log({
2983
- name: "payment.three_ds.returned",
2984
- stage: "three_ds_return",
2985
- provider,
2986
- paymentMethodCategory: "card"
2987
- });
2988
- this.telemetryReporter?.performance({
2989
- stage: "three_ds_return",
2990
- durationMs: completedAt - threeDsStartedAt,
2991
- durationMode: "machine",
2992
- provider,
2993
- paymentMethodCategory: "card"
2994
- });
2995
- } else {
2996
- threeDsFailed = true;
2997
- }
2998
- }
2999
- }
3000
- if (isExpectedDecline(result.error)) {
3001
- this.telemetryReporter?.terminal({
3002
- outcome: "payment_declined",
3003
- provider,
3004
- paymentMethodCategory: "card"
3005
- });
3006
- } else if (result.error?.type === "validation_error") {
3007
- this.telemetryReporter?.terminal({
3008
- outcome: "validation_rejected",
3009
- provider,
3010
- paymentMethodCategory: "card"
3011
- });
3012
- } else if (result.error) {
3013
- this.telemetryReporter?.error({
3014
- errorCode: threeDsFailed ? "THREE_DS_FAILED" : "PROVIDER_RUNTIME_FAILED",
3015
- stage: threeDsFailed ? "three_ds_return" : "processing",
3016
- provider,
3017
- paymentMethodCategory: "card"
3018
- });
3019
- } else if (!threeDsFailed && threeDs?.status !== "handoff" && result.status === "succeeded") {
3020
- this.telemetryReporter?.terminal({
3021
- outcome: "payment_succeeded",
3022
- provider,
3023
- paymentMethodCategory: "card"
3024
- });
3025
- }
3026
- return result;
3027
- } catch (error) {
3028
- this.telemetryReporter?.error({
3029
- errorCode: "PROVIDER_RUNTIME_FAILED",
3030
- stage: "processing",
3031
- provider,
3032
- paymentMethodCategory: "card"
3033
- });
3034
- throw error;
3035
- }
3036
- }
3037
- trackCardThreeDsAttempt(attemptId, startedAt) {
3038
- this.pruneExpiredCardThreeDsAttempts(startedAt);
3039
- if (this.cardThreeDsStartedAt.has(attemptId)) return false;
3040
- while (this.cardThreeDsStartedAt.size >= MAX_CARD_THREE_DS_ATTEMPTS) {
3041
- const oldestAttemptId = this.cardThreeDsStartedAt.keys().next().value;
3042
- if (oldestAttemptId === void 0) break;
3043
- this.cardThreeDsStartedAt.delete(oldestAttemptId);
3044
- }
3045
- this.cardThreeDsStartedAt.set(attemptId, startedAt);
3046
- return true;
3047
- }
3048
- takeCardThreeDsAttempt(attemptId, now) {
3049
- this.pruneExpiredCardThreeDsAttempts(now);
3050
- const startedAt = this.cardThreeDsStartedAt.get(attemptId);
3051
- if (startedAt !== void 0) this.cardThreeDsStartedAt.delete(attemptId);
3052
- return startedAt;
3053
- }
3054
- pruneExpiredCardThreeDsAttempts(now) {
3055
- for (const [attemptId, startedAt] of this.cardThreeDsStartedAt) {
3056
- if (now - startedAt >= CARD_THREE_DS_ATTEMPT_TTL_MS) {
3057
- this.cardThreeDsStartedAt.delete(attemptId);
3058
- }
3059
- }
3060
- }
3061
2906
  /**
3062
2907
  * Create a {@link CardCaptureAdapter} for collecting card details through the
3063
- * backend-rendered hosted vault PCI widget instead of provider-owned (Stripe)
3064
- * card fields (TeamFloPay/backend#823).
2908
+ * backend-rendered hosted vault PCI widget (TeamFloPay/backend#823).
3065
2909
  *
3066
2910
  * The returned adapter injects the server-supplied widget HTML (the session's
3067
2911
  * {@link CheckoutSession.vault} block, or one fetched via
@@ -3250,15 +3094,22 @@ var FloPay = class {
3250
3094
  throw error;
3251
3095
  }
3252
3096
  }
3253
- /** Confirms a payment using the mounted elements. */
3097
+ /** Confirms a non-card wallet/APM payment using the mounted PaymentElement. */
3254
3098
  async confirmPayment(params) {
3099
+ if (params.paymentMethodCategory !== "wallet" && params.paymentMethodCategory !== "apm" || !params.paymentMethodType?.trim() || params.paymentMethodType.trim().toLowerCase() === "card") {
3100
+ throw new import_shared6.FloPayError(
3101
+ "A supported non-card payment method is required.",
3102
+ "validation_error",
3103
+ { param: "paymentMethodType" }
3104
+ );
3105
+ }
3255
3106
  const started = this.now();
3256
3107
  const provider = telemetryProvider(this.provider.name);
3257
3108
  this.telemetryReporter?.log({
3258
3109
  name: "payment.processing.started",
3259
3110
  stage: "processing",
3260
3111
  provider,
3261
- paymentMethodCategory: "unknown"
3112
+ paymentMethodCategory: params.paymentMethodCategory
3262
3113
  });
3263
3114
  try {
3264
3115
  const result = await this.provider.confirmPayment(params);
@@ -3268,39 +3119,39 @@ var FloPay = class {
3268
3119
  durationMs,
3269
3120
  durationMode: "machine",
3270
3121
  provider,
3271
- paymentMethodCategory: "unknown"
3122
+ paymentMethodCategory: params.paymentMethodCategory
3272
3123
  });
3273
3124
  if (result.status === "succeeded") {
3274
3125
  this.telemetryReporter?.terminal({
3275
3126
  outcome: "payment_succeeded",
3276
3127
  provider,
3277
- paymentMethodCategory: "unknown"
3128
+ paymentMethodCategory: params.paymentMethodCategory
3278
3129
  });
3279
3130
  } else if (isExpectedDecline(result.error)) {
3280
3131
  this.telemetryReporter?.terminal({
3281
3132
  outcome: "payment_declined",
3282
3133
  provider,
3283
- paymentMethodCategory: "unknown"
3134
+ paymentMethodCategory: params.paymentMethodCategory
3284
3135
  });
3285
3136
  } else if (result.error?.type === "validation_error") {
3286
3137
  this.telemetryReporter?.terminal({
3287
3138
  outcome: "validation_rejected",
3288
3139
  provider,
3289
- paymentMethodCategory: "unknown"
3140
+ paymentMethodCategory: params.paymentMethodCategory
3290
3141
  });
3291
3142
  } else if (result.status === "requires_action") {
3292
3143
  this.telemetryReporter?.terminal({
3293
3144
  outcome: "action_required",
3294
3145
  stage: "three_ds_handoff",
3295
3146
  provider,
3296
- paymentMethodCategory: "unknown"
3147
+ paymentMethodCategory: params.paymentMethodCategory
3297
3148
  });
3298
3149
  } else if (result.status === "failed" && result.error) {
3299
3150
  this.telemetryReporter?.error({
3300
3151
  errorCode: "PAYMENT_PROCESSING_FAILED",
3301
3152
  stage: "processing",
3302
3153
  provider,
3303
- paymentMethodCategory: "unknown"
3154
+ paymentMethodCategory: params.paymentMethodCategory
3304
3155
  });
3305
3156
  }
3306
3157
  if (result.status === "succeeded" || result.status === "failed") {
@@ -3308,14 +3159,14 @@ var FloPay = class {
3308
3159
  name: "payment.processing.completed",
3309
3160
  stage: "processing",
3310
3161
  provider,
3311
- paymentMethodCategory: "unknown"
3162
+ paymentMethodCategory: params.paymentMethodCategory
3312
3163
  });
3313
3164
  } else {
3314
3165
  this.telemetryReporter?.log({
3315
3166
  name: "operation.state_transition",
3316
3167
  stage: result.status === "requires_action" ? "three_ds_handoff" : "processing",
3317
3168
  provider,
3318
- paymentMethodCategory: "unknown"
3169
+ paymentMethodCategory: params.paymentMethodCategory
3319
3170
  });
3320
3171
  }
3321
3172
  return result;
@@ -3325,13 +3176,13 @@ var FloPay = class {
3325
3176
  durationMs: this.now() - started,
3326
3177
  durationMode: "machine",
3327
3178
  provider,
3328
- paymentMethodCategory: "unknown"
3179
+ paymentMethodCategory: params.paymentMethodCategory
3329
3180
  });
3330
3181
  this.telemetryReporter?.error({
3331
3182
  errorCode: "PAYMENT_PROCESSING_FAILED",
3332
3183
  stage: "processing",
3333
3184
  provider,
3334
- paymentMethodCategory: "unknown"
3185
+ paymentMethodCategory: params.paymentMethodCategory
3335
3186
  });
3336
3187
  throw error;
3337
3188
  }
@@ -3461,7 +3312,6 @@ var FloPay = class {
3461
3312
  destroy() {
3462
3313
  this.telemetryReporter?.log({ name: "checkout.unmount", stage: "unmount" });
3463
3314
  this.telemetryReporter?.destroy();
3464
- this.cardThreeDsStartedAt.clear();
3465
3315
  this.currentElements?.destroy();
3466
3316
  this.currentElements = null;
3467
3317
  this.provider.destroy();