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