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