@flopay/js 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +190 -0
- package/dist/index.cjs +894 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +317 -0
- package/dist/index.d.ts +317 -0
- package/dist/index.mjs +851 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +51 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,894 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
FloPay: () => FloPay,
|
|
34
|
+
FloPayElements: () => FloPayElements,
|
|
35
|
+
PaymentAPI: () => PaymentAPI,
|
|
36
|
+
StripeAdapter: () => StripeAdapter,
|
|
37
|
+
createCheckoutSession: () => createCheckoutSession,
|
|
38
|
+
createCheckoutSessionWithRetries: () => createCheckoutSessionWithRetries,
|
|
39
|
+
loadFloPay: () => loadFloPay
|
|
40
|
+
});
|
|
41
|
+
module.exports = __toCommonJS(index_exports);
|
|
42
|
+
|
|
43
|
+
// src/load.ts
|
|
44
|
+
var import_shared5 = require("@flopay/shared");
|
|
45
|
+
|
|
46
|
+
// src/stripe-adapter.ts
|
|
47
|
+
var import_shared = require("@flopay/shared");
|
|
48
|
+
function toStripeElementType(type) {
|
|
49
|
+
const map = {
|
|
50
|
+
payment: "payment",
|
|
51
|
+
card: "card",
|
|
52
|
+
cardNumber: "cardNumber",
|
|
53
|
+
cardExpiry: "cardExpiry",
|
|
54
|
+
cardCvc: "cardCvc",
|
|
55
|
+
address: "address"
|
|
56
|
+
};
|
|
57
|
+
return map[type];
|
|
58
|
+
}
|
|
59
|
+
function wrapStripeElement(stripeElement) {
|
|
60
|
+
const el = stripeElement;
|
|
61
|
+
return {
|
|
62
|
+
mount(container) {
|
|
63
|
+
el.mount(container);
|
|
64
|
+
},
|
|
65
|
+
unmount() {
|
|
66
|
+
el.unmount();
|
|
67
|
+
},
|
|
68
|
+
update(options) {
|
|
69
|
+
el.update(options);
|
|
70
|
+
},
|
|
71
|
+
on(event, handler) {
|
|
72
|
+
el["on"]?.(event, handler);
|
|
73
|
+
},
|
|
74
|
+
off(event, handler) {
|
|
75
|
+
el["off"]?.(event, handler);
|
|
76
|
+
},
|
|
77
|
+
destroy() {
|
|
78
|
+
el.destroy();
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
var StripeAdapter = class {
|
|
83
|
+
constructor() {
|
|
84
|
+
this.name = "stripe";
|
|
85
|
+
this.stripe = null;
|
|
86
|
+
this.elements = null;
|
|
87
|
+
}
|
|
88
|
+
async initialize(config) {
|
|
89
|
+
if (typeof window === "undefined") {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const { loadStripe } = await import("@stripe/stripe-js");
|
|
93
|
+
const stripe = await loadStripe(config.publishableKey, {
|
|
94
|
+
locale: config.locale ?? "auto"
|
|
95
|
+
});
|
|
96
|
+
if (!stripe) {
|
|
97
|
+
throw new import_shared.FloPayError(
|
|
98
|
+
"Failed to initialize Stripe. Check your publishable key.",
|
|
99
|
+
"authentication_error"
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
this.stripe = stripe;
|
|
103
|
+
}
|
|
104
|
+
/** Lazily creates the Stripe Elements group for the given options. */
|
|
105
|
+
getElements(options) {
|
|
106
|
+
if (!this.stripe) {
|
|
107
|
+
throw new import_shared.FloPayError(
|
|
108
|
+
"StripeAdapter not initialized. Call initialize() first.",
|
|
109
|
+
"api_error"
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
if (!this.elements) {
|
|
113
|
+
let elementsOptions;
|
|
114
|
+
if (options?.clientSecret) {
|
|
115
|
+
elementsOptions = { clientSecret: options.clientSecret };
|
|
116
|
+
} else {
|
|
117
|
+
elementsOptions = {
|
|
118
|
+
mode: "payment",
|
|
119
|
+
amount: options?.amount ?? 0,
|
|
120
|
+
currency: (options?.currency ?? "usd").toLowerCase(),
|
|
121
|
+
paymentMethodCreation: options?.paymentMethodCreation ?? "manual"
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
if (options?.appearance) {
|
|
125
|
+
elementsOptions["appearance"] = {
|
|
126
|
+
theme: options.appearance.theme ?? "stripe",
|
|
127
|
+
variables: options.appearance.variables,
|
|
128
|
+
rules: options.appearance.rules
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
this.elements = this.stripe.elements(elementsOptions);
|
|
132
|
+
}
|
|
133
|
+
return this.elements;
|
|
134
|
+
}
|
|
135
|
+
async createElement(type, options) {
|
|
136
|
+
const elements = this.getElements(options);
|
|
137
|
+
const stripeType = toStripeElementType(type);
|
|
138
|
+
const elementOptions = {};
|
|
139
|
+
if (options.layout) {
|
|
140
|
+
elementOptions["layout"] = options.layout;
|
|
141
|
+
}
|
|
142
|
+
if (options.defaultValues) {
|
|
143
|
+
elementOptions["defaultValues"] = options.defaultValues;
|
|
144
|
+
}
|
|
145
|
+
if (options.readOnly) {
|
|
146
|
+
elementOptions["readOnly"] = options.readOnly;
|
|
147
|
+
}
|
|
148
|
+
if (options.mode) {
|
|
149
|
+
elementOptions["mode"] = options.mode;
|
|
150
|
+
}
|
|
151
|
+
const stripeElement = elements.create(stripeType, elementOptions);
|
|
152
|
+
return wrapStripeElement(stripeElement);
|
|
153
|
+
}
|
|
154
|
+
getElement(type) {
|
|
155
|
+
if (!this.elements) return null;
|
|
156
|
+
const stripeType = toStripeElementType(type);
|
|
157
|
+
const existing = this.elements.getElement(stripeType);
|
|
158
|
+
if (!existing) return null;
|
|
159
|
+
return wrapStripeElement(existing);
|
|
160
|
+
}
|
|
161
|
+
async submitElements() {
|
|
162
|
+
if (!this.stripe || !this.elements) {
|
|
163
|
+
return { error: new import_shared.FloPayError("Stripe not initialized", "api_error") };
|
|
164
|
+
}
|
|
165
|
+
const { error } = await this.elements.submit();
|
|
166
|
+
if (error) {
|
|
167
|
+
return {
|
|
168
|
+
error: new import_shared.FloPayError(error.message ?? "Validation failed", "validation_error")
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
return {};
|
|
172
|
+
}
|
|
173
|
+
async createPaymentMethod() {
|
|
174
|
+
if (!this.stripe || !this.elements) {
|
|
175
|
+
return {
|
|
176
|
+
paymentMethodId: null,
|
|
177
|
+
error: new import_shared.FloPayError("Stripe not initialized", "api_error")
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
const cardNumberEl = this.elements.getElement("cardNumber");
|
|
181
|
+
const { error, paymentMethod } = cardNumberEl ? await this.stripe.createPaymentMethod({
|
|
182
|
+
type: "card",
|
|
183
|
+
card: cardNumberEl
|
|
184
|
+
}) : await this.stripe.createPaymentMethod({
|
|
185
|
+
elements: this.elements
|
|
186
|
+
});
|
|
187
|
+
if (error) {
|
|
188
|
+
return {
|
|
189
|
+
paymentMethodId: null,
|
|
190
|
+
error: new import_shared.FloPayError(
|
|
191
|
+
error.message ?? "Failed to create payment method",
|
|
192
|
+
"api_error",
|
|
193
|
+
{ code: error.code }
|
|
194
|
+
)
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
return { paymentMethodId: paymentMethod.id };
|
|
198
|
+
}
|
|
199
|
+
async confirmCardPayment(params) {
|
|
200
|
+
if (!this.stripe) {
|
|
201
|
+
return {
|
|
202
|
+
status: "failed",
|
|
203
|
+
error: new import_shared.FloPayError("Stripe not initialized", "api_error")
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
const { error, paymentIntent } = await this.stripe.confirmCardPayment(
|
|
207
|
+
params.clientSecret,
|
|
208
|
+
{ payment_method: params.paymentMethodId }
|
|
209
|
+
);
|
|
210
|
+
if (error) {
|
|
211
|
+
return {
|
|
212
|
+
status: "failed",
|
|
213
|
+
error: new import_shared.FloPayError(
|
|
214
|
+
error.message ?? "Payment failed",
|
|
215
|
+
"api_error",
|
|
216
|
+
{ code: error.code, declineCode: error.decline_code }
|
|
217
|
+
)
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
return {
|
|
221
|
+
status: paymentIntent?.status ?? "failed",
|
|
222
|
+
paymentIntentId: paymentIntent?.id
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
async confirmPayment(params) {
|
|
226
|
+
if (!this.stripe || !this.elements) {
|
|
227
|
+
throw new import_shared.FloPayError(
|
|
228
|
+
"StripeAdapter not initialized or no elements created.",
|
|
229
|
+
"api_error"
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
const { error, paymentIntent } = await this.stripe.confirmPayment({
|
|
233
|
+
elements: this.elements,
|
|
234
|
+
clientSecret: params.clientSecret,
|
|
235
|
+
confirmParams: {
|
|
236
|
+
return_url: params.returnUrl ?? window.location.href
|
|
237
|
+
},
|
|
238
|
+
redirect: "if_required"
|
|
239
|
+
});
|
|
240
|
+
if (error) {
|
|
241
|
+
return {
|
|
242
|
+
status: "failed",
|
|
243
|
+
error: new import_shared.FloPayError(
|
|
244
|
+
error.message ?? "Payment failed",
|
|
245
|
+
"api_error",
|
|
246
|
+
{
|
|
247
|
+
code: error.code,
|
|
248
|
+
declineCode: error.decline_code
|
|
249
|
+
}
|
|
250
|
+
)
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
if (!paymentIntent) {
|
|
254
|
+
return { status: "failed", error: new import_shared.FloPayError("No payment intent returned", "api_error") };
|
|
255
|
+
}
|
|
256
|
+
const statusMap = {
|
|
257
|
+
succeeded: "succeeded",
|
|
258
|
+
processing: "processing",
|
|
259
|
+
requires_action: "requires_action",
|
|
260
|
+
requires_payment_method: "failed",
|
|
261
|
+
canceled: "failed"
|
|
262
|
+
};
|
|
263
|
+
return {
|
|
264
|
+
status: statusMap[paymentIntent.status] ?? "failed",
|
|
265
|
+
paymentIntentId: paymentIntent.id
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
async confirmPayPalPayment(params) {
|
|
269
|
+
if (!this.stripe) {
|
|
270
|
+
return { status: "failed", error: new import_shared.FloPayError("Stripe not initialized", "api_error") };
|
|
271
|
+
}
|
|
272
|
+
const baseUrl = params.billingApiUrl.replace(/\/+$/, "");
|
|
273
|
+
const createPM = this.stripe.createPaymentMethod;
|
|
274
|
+
const { error: pmError, paymentMethod } = await createPM({ type: "paypal" });
|
|
275
|
+
if (pmError) {
|
|
276
|
+
console.warn("[FloPay] Could not create PayPal PM upfront:", pmError.message);
|
|
277
|
+
}
|
|
278
|
+
const intentResponse = await fetch(`${baseUrl}/v1/checkouts/payments/intents`, {
|
|
279
|
+
method: "POST",
|
|
280
|
+
headers: { "Content-Type": "application/json" },
|
|
281
|
+
body: JSON.stringify({
|
|
282
|
+
sessionId: params.sessionId,
|
|
283
|
+
email: params.email,
|
|
284
|
+
paymentMethodType: paymentMethod?.id ?? "paypal",
|
|
285
|
+
isPaypal: "true"
|
|
286
|
+
})
|
|
287
|
+
});
|
|
288
|
+
if (!intentResponse.ok) {
|
|
289
|
+
return { status: "failed", error: new import_shared.FloPayError("Failed to create PayPal payment intent", "api_error") };
|
|
290
|
+
}
|
|
291
|
+
const intentJson = await intentResponse.json();
|
|
292
|
+
const intentClientSecret = intentJson.data?.id;
|
|
293
|
+
if (!intentClientSecret) {
|
|
294
|
+
return { status: "failed", error: new import_shared.FloPayError("No client_secret in response", "api_error") };
|
|
295
|
+
}
|
|
296
|
+
const confirmParams = {
|
|
297
|
+
return_url: params.returnUrl
|
|
298
|
+
};
|
|
299
|
+
if (paymentMethod?.id) {
|
|
300
|
+
confirmParams["payment_method"] = paymentMethod.id;
|
|
301
|
+
}
|
|
302
|
+
const { error, paymentIntent } = await this.stripe.confirmPayment({
|
|
303
|
+
clientSecret: intentClientSecret,
|
|
304
|
+
confirmParams,
|
|
305
|
+
redirect: "if_required"
|
|
306
|
+
});
|
|
307
|
+
if (error) {
|
|
308
|
+
return {
|
|
309
|
+
status: "failed",
|
|
310
|
+
error: new import_shared.FloPayError(error.message ?? "PayPal payment failed", "api_error", { code: error.code })
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
const pmId = typeof paymentIntent?.payment_method === "string" ? paymentIntent.payment_method : paymentIntent?.payment_method?.id;
|
|
314
|
+
return {
|
|
315
|
+
status: paymentIntent?.status ?? "failed",
|
|
316
|
+
paymentIntentId: paymentIntent?.id,
|
|
317
|
+
paymentMethodId: pmId
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
async resumePayPalPayment() {
|
|
321
|
+
if (!this.stripe || typeof window === "undefined") return null;
|
|
322
|
+
const params = new URLSearchParams(window.location.search);
|
|
323
|
+
const paymentIntentId = params.get("payment_intent");
|
|
324
|
+
const clientSecret = params.get("payment_intent_client_secret");
|
|
325
|
+
const redirectStatus = params.get("redirect_status");
|
|
326
|
+
if (!paymentIntentId || !clientSecret) return null;
|
|
327
|
+
if (redirectStatus === "failed") {
|
|
328
|
+
return {
|
|
329
|
+
status: "failed",
|
|
330
|
+
error: new import_shared.FloPayError("PayPal payment was declined. Please try again.", "api_error")
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
const { paymentIntent, error } = await this.stripe.retrievePaymentIntent(clientSecret);
|
|
334
|
+
if (error) {
|
|
335
|
+
return {
|
|
336
|
+
status: "failed",
|
|
337
|
+
error: new import_shared.FloPayError(error.message ?? "Failed to retrieve PayPal payment", "api_error")
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
if (paymentIntent && (paymentIntent.status === "requires_capture" || paymentIntent.status === "succeeded")) {
|
|
341
|
+
const pmId = typeof paymentIntent.payment_method === "string" ? paymentIntent.payment_method : paymentIntent.payment_method?.id;
|
|
342
|
+
const url = new URL(window.location.href);
|
|
343
|
+
url.searchParams.delete("payment_intent");
|
|
344
|
+
url.searchParams.delete("payment_intent_client_secret");
|
|
345
|
+
url.searchParams.delete("redirect_status");
|
|
346
|
+
window.history.replaceState({}, "", url.toString());
|
|
347
|
+
return {
|
|
348
|
+
status: paymentIntent.status,
|
|
349
|
+
paymentIntentId: paymentIntent.id,
|
|
350
|
+
paymentMethodId: pmId
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
return {
|
|
354
|
+
status: "failed",
|
|
355
|
+
error: new import_shared.FloPayError("PayPal payment was not completed. Please try again.", "api_error")
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
getRawProvider() {
|
|
359
|
+
return this.stripe;
|
|
360
|
+
}
|
|
361
|
+
createPayPalElements(options) {
|
|
362
|
+
if (!this.stripe) return null;
|
|
363
|
+
const elementsOptions = {
|
|
364
|
+
mode: "payment",
|
|
365
|
+
amount: options.amount ?? 0,
|
|
366
|
+
currency: (options.currency ?? "usd").toLowerCase(),
|
|
367
|
+
captureMethod: "manual"
|
|
368
|
+
};
|
|
369
|
+
if (options.appearance) {
|
|
370
|
+
elementsOptions["appearance"] = {
|
|
371
|
+
theme: options.appearance.theme ?? "stripe",
|
|
372
|
+
variables: options.appearance.variables,
|
|
373
|
+
rules: options.appearance.rules
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
return this.stripe.elements(elementsOptions);
|
|
377
|
+
}
|
|
378
|
+
destroy() {
|
|
379
|
+
this.elements = null;
|
|
380
|
+
this.stripe = null;
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
// src/flopay.ts
|
|
385
|
+
var import_shared4 = require("@flopay/shared");
|
|
386
|
+
|
|
387
|
+
// src/elements.ts
|
|
388
|
+
var import_shared2 = require("@flopay/shared");
|
|
389
|
+
var FloPayElements = class {
|
|
390
|
+
constructor(provider, options) {
|
|
391
|
+
this.elementMap = /* @__PURE__ */ new Map();
|
|
392
|
+
this.provider = provider;
|
|
393
|
+
this.baseOptions = options ?? {};
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Creates a new element of the given type.
|
|
397
|
+
* If an element of that type already exists, it is destroyed first.
|
|
398
|
+
*/
|
|
399
|
+
async create(type, options) {
|
|
400
|
+
const providerExisting = this.provider.getElement(type);
|
|
401
|
+
if (providerExisting) {
|
|
402
|
+
this.elementMap.set(type, providerExisting);
|
|
403
|
+
return providerExisting;
|
|
404
|
+
}
|
|
405
|
+
const existing = this.elementMap.get(type);
|
|
406
|
+
if (existing) {
|
|
407
|
+
existing.destroy();
|
|
408
|
+
}
|
|
409
|
+
const merged = { ...this.baseOptions, ...options };
|
|
410
|
+
const element = await this.provider.createElement(type, merged);
|
|
411
|
+
this.elementMap.set(type, element);
|
|
412
|
+
return element;
|
|
413
|
+
}
|
|
414
|
+
/** Returns a previously created element, or `null`. */
|
|
415
|
+
getElement(type) {
|
|
416
|
+
return this.elementMap.get(type) ?? null;
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Submits all mounted elements for validation.
|
|
420
|
+
*
|
|
421
|
+
* Returns an object with an optional error if validation fails.
|
|
422
|
+
* This does NOT confirm the payment — call `floPay.confirmPayment()` for that.
|
|
423
|
+
*/
|
|
424
|
+
async submit() {
|
|
425
|
+
return {};
|
|
426
|
+
}
|
|
427
|
+
/** Destroys all created elements and clears the internal map. */
|
|
428
|
+
destroy() {
|
|
429
|
+
for (const element of this.elementMap.values()) {
|
|
430
|
+
element.destroy();
|
|
431
|
+
}
|
|
432
|
+
this.elementMap.clear();
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
// src/payment-api.ts
|
|
437
|
+
var import_shared3 = require("@flopay/shared");
|
|
438
|
+
var PaymentAPI = class {
|
|
439
|
+
constructor(billingApiUrl) {
|
|
440
|
+
this.baseUrl = billingApiUrl.replace(/\/+$/, "");
|
|
441
|
+
}
|
|
442
|
+
/** Fetch a raw checkout session by ID. */
|
|
443
|
+
async getCheckoutSession(checkoutSessionId) {
|
|
444
|
+
const response = await fetch(
|
|
445
|
+
`${this.baseUrl}/v1/checkouts/sessions/${checkoutSessionId}`
|
|
446
|
+
);
|
|
447
|
+
if (!response.ok) {
|
|
448
|
+
throw new import_shared3.FloPayError(
|
|
449
|
+
"Failed to get checkout session",
|
|
450
|
+
"api_error",
|
|
451
|
+
{ code: `http_${response.status}` }
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
return response.json();
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Fetch and normalize a checkout session.
|
|
458
|
+
*
|
|
459
|
+
* Reads the backend's `gateway` field to determine the provider,
|
|
460
|
+
* then wraps the session in a `NormalizedCheckoutSession` for
|
|
461
|
+
* provider-agnostic consumption.
|
|
462
|
+
*/
|
|
463
|
+
async getUnifiedCheckoutSession(checkoutSessionId) {
|
|
464
|
+
const res = await this.getCheckoutSession(checkoutSessionId);
|
|
465
|
+
const session = res.data;
|
|
466
|
+
const gateway = session.gateway;
|
|
467
|
+
if (gateway === "chargebee") {
|
|
468
|
+
return {
|
|
469
|
+
provider: "chargebee",
|
|
470
|
+
mode: "tokenize",
|
|
471
|
+
data: { session: this.toCheckoutSession(session) },
|
|
472
|
+
raw: res
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
if (gateway === "stripe") {
|
|
476
|
+
return {
|
|
477
|
+
provider: "stripe",
|
|
478
|
+
mode: "tokenize",
|
|
479
|
+
data: {
|
|
480
|
+
session: this.toCheckoutSession(session),
|
|
481
|
+
stripe: {
|
|
482
|
+
clientSecret: session["stripeClientSecret"] ?? "",
|
|
483
|
+
publishableKey: session.gatewayData?.publishableKey ?? void 0
|
|
484
|
+
}
|
|
485
|
+
},
|
|
486
|
+
raw: res
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
return {
|
|
490
|
+
provider: "recurly",
|
|
491
|
+
mode: "tokenize",
|
|
492
|
+
data: { session: this.toCheckoutSession(session) },
|
|
493
|
+
raw: res
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* Submit a tokenized payment to the billing backend.
|
|
498
|
+
*
|
|
499
|
+
* The backend will either succeed, return `type: '3ds_required'`
|
|
500
|
+
* (with a `threeDSecureToken`), or return `type: 'paypal_redirect_required'`.
|
|
501
|
+
*/
|
|
502
|
+
async processPayment(userId, data) {
|
|
503
|
+
return fetch(
|
|
504
|
+
`${this.baseUrl}/v1/checkouts/sessions/process`,
|
|
505
|
+
{
|
|
506
|
+
method: "POST",
|
|
507
|
+
headers: {
|
|
508
|
+
"Content-Type": "application/json",
|
|
509
|
+
"x-user-id": userId
|
|
510
|
+
},
|
|
511
|
+
body: JSON.stringify(data)
|
|
512
|
+
}
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* Create a PaymentIntent on the backend.
|
|
517
|
+
*
|
|
518
|
+
* Used by the Stripe flow to create a server-side PaymentIntent
|
|
519
|
+
* with the client's payment method attached.
|
|
520
|
+
*/
|
|
521
|
+
async createPaymentIntent(sessionId, email, paymentMethodType, options) {
|
|
522
|
+
return fetch(
|
|
523
|
+
`${this.baseUrl}/v1/checkouts/payments/intents`,
|
|
524
|
+
{
|
|
525
|
+
method: "POST",
|
|
526
|
+
headers: { "Content-Type": "application/json" },
|
|
527
|
+
body: JSON.stringify({
|
|
528
|
+
sessionId,
|
|
529
|
+
email,
|
|
530
|
+
paymentMethodType,
|
|
531
|
+
isPaypal: options?.isPaypal ?? false
|
|
532
|
+
}),
|
|
533
|
+
signal: options?.signal
|
|
534
|
+
}
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* Create a SetupIntent for saving payment methods.
|
|
539
|
+
*/
|
|
540
|
+
async createSetupIntent(sessionId, email, paymentMethodType, options) {
|
|
541
|
+
return fetch(
|
|
542
|
+
`${this.baseUrl}/v1/checkouts/payments/setup-intents`,
|
|
543
|
+
{
|
|
544
|
+
method: "POST",
|
|
545
|
+
headers: { "Content-Type": "application/json" },
|
|
546
|
+
body: JSON.stringify({ sessionId, email, paymentMethodType }),
|
|
547
|
+
signal: options?.signal
|
|
548
|
+
}
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* Fetch user's prior payments by email.
|
|
553
|
+
* Used to determine if saved card UX should be shown.
|
|
554
|
+
*/
|
|
555
|
+
async getPaymentsByEmail(email, options) {
|
|
556
|
+
const page = options?.page ?? 1;
|
|
557
|
+
const limit = options?.limit ?? 1;
|
|
558
|
+
const params = new URLSearchParams({
|
|
559
|
+
email,
|
|
560
|
+
page: String(page),
|
|
561
|
+
limit: String(limit),
|
|
562
|
+
sortField: "occurredAt",
|
|
563
|
+
sortDirection: "DESC"
|
|
564
|
+
});
|
|
565
|
+
const response = await fetch(
|
|
566
|
+
`${this.baseUrl}/v1/payments?${params.toString()}`,
|
|
567
|
+
{
|
|
568
|
+
method: "GET",
|
|
569
|
+
signal: options?.signal,
|
|
570
|
+
keepalive: true
|
|
571
|
+
}
|
|
572
|
+
);
|
|
573
|
+
if (!response.ok) {
|
|
574
|
+
throw new import_shared3.FloPayError("Failed to fetch payments", "api_error");
|
|
575
|
+
}
|
|
576
|
+
return response.json();
|
|
577
|
+
}
|
|
578
|
+
/** Convert raw session to the SDK CheckoutSession shape. */
|
|
579
|
+
toCheckoutSession(raw) {
|
|
580
|
+
const totalAmount = [
|
|
581
|
+
...raw.subscriptions.map((s) => s.overrideAmount || s.totalAmount),
|
|
582
|
+
...raw.items.map((i) => i.overrideAmount || i.totalAmount)
|
|
583
|
+
].reduce((sum, val) => sum + val, 0);
|
|
584
|
+
const amountInCents = Math.round(totalAmount * 100);
|
|
585
|
+
return {
|
|
586
|
+
// Core fields (backward compat)
|
|
587
|
+
id: raw.uuid,
|
|
588
|
+
clientSecret: raw.nonce,
|
|
589
|
+
mode: raw.subscriptions.length > 0 ? "subscription" : "payment",
|
|
590
|
+
status: raw.status === "completed" ? "complete" : "open",
|
|
591
|
+
amount: amountInCents,
|
|
592
|
+
currency: raw.subscriptions[0]?.currency ?? raw.items[0]?.currency ?? "USD",
|
|
593
|
+
customer: {
|
|
594
|
+
id: raw.accountData.userId,
|
|
595
|
+
email: raw.accountData.email,
|
|
596
|
+
firstName: raw.accountData.firstName,
|
|
597
|
+
lastName: raw.accountData.lastName,
|
|
598
|
+
country: raw.accountData.country ?? void 0,
|
|
599
|
+
city: raw.accountData.city ?? void 0,
|
|
600
|
+
state: raw.accountData.state ?? void 0,
|
|
601
|
+
zip: raw.accountData.zip ?? void 0,
|
|
602
|
+
gender: raw.accountData.gender ?? void 0
|
|
603
|
+
},
|
|
604
|
+
metadata: {},
|
|
605
|
+
// Full session data from billing API
|
|
606
|
+
checkoutMode: raw.checkoutMode,
|
|
607
|
+
items: raw.items,
|
|
608
|
+
subscriptions: raw.subscriptions,
|
|
609
|
+
successUrl: raw.successUrl,
|
|
610
|
+
cancelUrl: raw.cancelUrl,
|
|
611
|
+
coupons: raw.coupons,
|
|
612
|
+
createdAt: raw.createdAt,
|
|
613
|
+
gateway: raw.gateway,
|
|
614
|
+
gatewayData: raw.gatewayData,
|
|
615
|
+
accountData: raw.accountData,
|
|
616
|
+
tagsData: raw.tagsData
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
};
|
|
620
|
+
|
|
621
|
+
// src/flopay.ts
|
|
622
|
+
var FloPay = class {
|
|
623
|
+
constructor(provider, config) {
|
|
624
|
+
this.currentElements = null;
|
|
625
|
+
this.provider = provider;
|
|
626
|
+
this.config = config;
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* Creates a new `FloPayElements` group for mounting payment fields.
|
|
630
|
+
*
|
|
631
|
+
* Only one elements group is active at a time. Creating a new one
|
|
632
|
+
* destroys the previous group.
|
|
633
|
+
*/
|
|
634
|
+
elements(options) {
|
|
635
|
+
if (this.currentElements) {
|
|
636
|
+
this.currentElements.destroy();
|
|
637
|
+
}
|
|
638
|
+
this.currentElements = new FloPayElements(this.provider, {
|
|
639
|
+
appearance: this.config.appearance,
|
|
640
|
+
...options
|
|
641
|
+
});
|
|
642
|
+
return this.currentElements;
|
|
643
|
+
}
|
|
644
|
+
/** Submit elements for validation. */
|
|
645
|
+
async submitElements() {
|
|
646
|
+
return this.provider.submitElements();
|
|
647
|
+
}
|
|
648
|
+
/** Create a payment method from the current elements (tokenize card). */
|
|
649
|
+
async createPaymentMethod() {
|
|
650
|
+
return this.provider.createPaymentMethod();
|
|
651
|
+
}
|
|
652
|
+
/** Confirm a card payment with a known client secret and payment method ID. */
|
|
653
|
+
async confirmCardPayment(params) {
|
|
654
|
+
return this.provider.confirmCardPayment(params);
|
|
655
|
+
}
|
|
656
|
+
/** Confirm a PayPal payment: create intent via billing API → confirm → redirect if needed. */
|
|
657
|
+
async confirmPayPalPayment(params) {
|
|
658
|
+
return this.provider.confirmPayPalPayment(params);
|
|
659
|
+
}
|
|
660
|
+
/** Resume a PayPal payment after redirect return. Returns null if no PayPal params in URL. */
|
|
661
|
+
async resumePayPalPayment() {
|
|
662
|
+
return this.provider.resumePayPalPayment();
|
|
663
|
+
}
|
|
664
|
+
/** Confirms a payment using the mounted elements. */
|
|
665
|
+
async confirmPayment(params) {
|
|
666
|
+
return this.provider.confirmPayment(params);
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* Retrieves a checkout session by ID via the billing API.
|
|
670
|
+
*
|
|
671
|
+
* Returns the normalized `CheckoutSession` with amount, currency,
|
|
672
|
+
* customer data, and status.
|
|
673
|
+
*
|
|
674
|
+
* Requires `billingApiUrl` to be set — either via `loadFloPay(key, { billingApiUrl })`
|
|
675
|
+
* or passed directly as the second argument.
|
|
676
|
+
*/
|
|
677
|
+
async retrieveSession(sessionId, billingApiUrl) {
|
|
678
|
+
if (!sessionId) {
|
|
679
|
+
throw new import_shared4.FloPayError(
|
|
680
|
+
"sessionId is required to retrieve a session.",
|
|
681
|
+
"validation_error",
|
|
682
|
+
{ param: "sessionId" }
|
|
683
|
+
);
|
|
684
|
+
}
|
|
685
|
+
const apiUrl = (0, import_shared4.resolveBillingApiUrl)(billingApiUrl ?? this.config.billingApiUrl);
|
|
686
|
+
const api = new PaymentAPI(apiUrl);
|
|
687
|
+
const unified = await api.getUnifiedCheckoutSession(sessionId);
|
|
688
|
+
if (!unified.data.session) {
|
|
689
|
+
throw new import_shared4.FloPayError("Session not found", "api_error");
|
|
690
|
+
}
|
|
691
|
+
return unified.data.session;
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* Retrieves and normalizes a checkout session, including provider-specific
|
|
695
|
+
* data (Stripe clientSecret/publishableKey, Chargebee site, etc.).
|
|
696
|
+
*
|
|
697
|
+
* The billing API URL is resolved from: explicit param → `loadFloPay()` config
|
|
698
|
+
* → `NEXT_PUBLIC_FLOPAY_ENV` env var → `configureFlopay()` → staging fallback.
|
|
699
|
+
*/
|
|
700
|
+
async retrieveUnifiedSession(sessionId, billingApiUrl) {
|
|
701
|
+
if (!sessionId) {
|
|
702
|
+
throw new import_shared4.FloPayError(
|
|
703
|
+
"sessionId is required.",
|
|
704
|
+
"validation_error",
|
|
705
|
+
{ param: "sessionId" }
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
const apiUrl = (0, import_shared4.resolveBillingApiUrl)(billingApiUrl ?? this.config.billingApiUrl);
|
|
709
|
+
const api = new PaymentAPI(apiUrl);
|
|
710
|
+
return api.getUnifiedCheckoutSession(sessionId);
|
|
711
|
+
}
|
|
712
|
+
/**
|
|
713
|
+
* Returns the raw underlying provider instance (e.g. Stripe object).
|
|
714
|
+
* Used internally by components that need direct provider access,
|
|
715
|
+
* such as PayPal which requires its own Elements instance.
|
|
716
|
+
*/
|
|
717
|
+
getRawProvider() {
|
|
718
|
+
return this.provider.getRawProvider();
|
|
719
|
+
}
|
|
720
|
+
/** Tears down the SDK instance and releases resources. */
|
|
721
|
+
destroy() {
|
|
722
|
+
this.currentElements?.destroy();
|
|
723
|
+
this.currentElements = null;
|
|
724
|
+
this.provider.destroy();
|
|
725
|
+
}
|
|
726
|
+
};
|
|
727
|
+
|
|
728
|
+
// src/load.ts
|
|
729
|
+
var cachedInstance = null;
|
|
730
|
+
var cachedKey = null;
|
|
731
|
+
async function loadFloPay(publishableKey, options) {
|
|
732
|
+
if (!publishableKey) {
|
|
733
|
+
throw new import_shared5.FloPayError(
|
|
734
|
+
"A publishable key is required to initialize FloPay.",
|
|
735
|
+
"validation_error",
|
|
736
|
+
{ param: "publishableKey" }
|
|
737
|
+
);
|
|
738
|
+
}
|
|
739
|
+
if (cachedInstance && cachedKey === publishableKey) {
|
|
740
|
+
return cachedInstance;
|
|
741
|
+
}
|
|
742
|
+
if (cachedInstance) {
|
|
743
|
+
cachedInstance.destroy();
|
|
744
|
+
}
|
|
745
|
+
const config = {
|
|
746
|
+
publishableKey,
|
|
747
|
+
...options
|
|
748
|
+
};
|
|
749
|
+
const adapter = new StripeAdapter();
|
|
750
|
+
await adapter.initialize(config);
|
|
751
|
+
const instance = new FloPay(adapter, config);
|
|
752
|
+
cachedKey = publishableKey;
|
|
753
|
+
cachedInstance = instance;
|
|
754
|
+
return instance;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// src/create-checkout-session.ts
|
|
758
|
+
async function createCheckoutSession(options) {
|
|
759
|
+
const {
|
|
760
|
+
billingApiUrl,
|
|
761
|
+
checkoutBaseUrl,
|
|
762
|
+
items = [],
|
|
763
|
+
subscriptions = [],
|
|
764
|
+
account,
|
|
765
|
+
successUrl,
|
|
766
|
+
cancelUrl,
|
|
767
|
+
checkoutMode = "confirm",
|
|
768
|
+
couponCodes = [],
|
|
769
|
+
tagsData,
|
|
770
|
+
redirectParams = {},
|
|
771
|
+
setCookie = true,
|
|
772
|
+
timeoutMs = 12e3,
|
|
773
|
+
clientId,
|
|
774
|
+
utmMetadata
|
|
775
|
+
} = options;
|
|
776
|
+
const payload = {
|
|
777
|
+
clientId,
|
|
778
|
+
successUrl,
|
|
779
|
+
cancelUrl,
|
|
780
|
+
checkoutMode,
|
|
781
|
+
items: items.map((item) => ({
|
|
782
|
+
providerItemId: item.providerItemId,
|
|
783
|
+
providerItemName: item.providerItemName ?? null,
|
|
784
|
+
quantity: item.quantity ?? 1,
|
|
785
|
+
totalAmount: item.totalAmount,
|
|
786
|
+
overrideAmount: item.overrideAmount ?? null,
|
|
787
|
+
currency: item.currency ?? "USD"
|
|
788
|
+
})),
|
|
789
|
+
subscriptions: subscriptions.map((sub) => ({
|
|
790
|
+
providerPlanId: sub.providerPlanId,
|
|
791
|
+
providerPlanName: sub.providerPlanName ?? null,
|
|
792
|
+
quantity: sub.quantity ?? 1,
|
|
793
|
+
totalAmount: sub.totalAmount,
|
|
794
|
+
overrideAmount: sub.overrideAmount ?? null,
|
|
795
|
+
currency: sub.currency ?? "USD"
|
|
796
|
+
})),
|
|
797
|
+
accountData: {
|
|
798
|
+
userId: account.userId,
|
|
799
|
+
firstName: account.firstName ?? null,
|
|
800
|
+
lastName: account.lastName ?? null,
|
|
801
|
+
email: account.email,
|
|
802
|
+
country: account.country ?? null,
|
|
803
|
+
gender: account.gender ?? null,
|
|
804
|
+
city: account.city ?? null,
|
|
805
|
+
state: account.state ?? null,
|
|
806
|
+
zip: account.zip ?? null
|
|
807
|
+
},
|
|
808
|
+
couponCodes
|
|
809
|
+
};
|
|
810
|
+
if (tagsData) {
|
|
811
|
+
payload["tagsData"] = tagsData;
|
|
812
|
+
}
|
|
813
|
+
if (utmMetadata?.length) {
|
|
814
|
+
payload["utmMetadata"] = utmMetadata;
|
|
815
|
+
}
|
|
816
|
+
const url = `${billingApiUrl.replace(/\/+$/, "")}/v1/checkouts/sessions`;
|
|
817
|
+
const controller = new AbortController();
|
|
818
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
819
|
+
let status;
|
|
820
|
+
let body;
|
|
821
|
+
try {
|
|
822
|
+
const response = await fetch(url, {
|
|
823
|
+
method: "POST",
|
|
824
|
+
headers: { "Content-Type": "application/json" },
|
|
825
|
+
body: JSON.stringify(payload),
|
|
826
|
+
signal: controller.signal
|
|
827
|
+
});
|
|
828
|
+
status = response.status;
|
|
829
|
+
try {
|
|
830
|
+
body = await response.json();
|
|
831
|
+
} catch {
|
|
832
|
+
}
|
|
833
|
+
} finally {
|
|
834
|
+
clearTimeout(timer);
|
|
835
|
+
}
|
|
836
|
+
if (status === 201) {
|
|
837
|
+
const uuid = body?.data?.uuid;
|
|
838
|
+
if (!uuid) {
|
|
839
|
+
throw new Error("Checkout session created but no UUID was returned by the billing API");
|
|
840
|
+
}
|
|
841
|
+
const redirectUrl = new URL(`${checkoutBaseUrl.replace(/\/+$/, "")}/secure`);
|
|
842
|
+
redirectUrl.searchParams.set("id", uuid);
|
|
843
|
+
for (const [key, value] of Object.entries(redirectParams)) {
|
|
844
|
+
redirectUrl.searchParams.set(key, value);
|
|
845
|
+
}
|
|
846
|
+
if (setCookie && typeof window !== "undefined" && typeof document !== "undefined") {
|
|
847
|
+
const checkoutData = JSON.stringify({ origin_url: cancelUrl });
|
|
848
|
+
const domain = window.location.hostname.split(".").slice(-2).join(".");
|
|
849
|
+
document.cookie = `checkout_data=${encodeURIComponent(checkoutData)}; domain=.${domain}; path=/; max-age=3600; SameSite=Lax; Secure;`;
|
|
850
|
+
}
|
|
851
|
+
if (typeof window !== "undefined") {
|
|
852
|
+
window.location.href = redirectUrl.toString();
|
|
853
|
+
}
|
|
854
|
+
return { status: 201, redirectUrl: redirectUrl.toString() };
|
|
855
|
+
}
|
|
856
|
+
if (status === 204) {
|
|
857
|
+
if (typeof window !== "undefined") {
|
|
858
|
+
window.location.href = successUrl;
|
|
859
|
+
}
|
|
860
|
+
return { status: 204 };
|
|
861
|
+
}
|
|
862
|
+
return { status };
|
|
863
|
+
}
|
|
864
|
+
async function createCheckoutSessionWithRetries(options) {
|
|
865
|
+
const { maxRetries = 3, ...sessionOptions } = options;
|
|
866
|
+
if (!Number.isFinite(maxRetries) || !Number.isInteger(maxRetries) || maxRetries <= 0) {
|
|
867
|
+
throw new Error("Number of retries must be greater than 0");
|
|
868
|
+
}
|
|
869
|
+
let lastErr;
|
|
870
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
871
|
+
try {
|
|
872
|
+
return await createCheckoutSession(sessionOptions);
|
|
873
|
+
} catch (err) {
|
|
874
|
+
lastErr = err;
|
|
875
|
+
if (err instanceof Error && err.name === "AbortError" && attempt < maxRetries) {
|
|
876
|
+
await new Promise((r) => setTimeout(r, 100 * Math.pow(2, attempt)));
|
|
877
|
+
continue;
|
|
878
|
+
}
|
|
879
|
+
throw err;
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
throw lastErr ?? new Error("Unknown error during checkout session creation");
|
|
883
|
+
}
|
|
884
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
885
|
+
0 && (module.exports = {
|
|
886
|
+
FloPay,
|
|
887
|
+
FloPayElements,
|
|
888
|
+
PaymentAPI,
|
|
889
|
+
StripeAdapter,
|
|
890
|
+
createCheckoutSession,
|
|
891
|
+
createCheckoutSessionWithRetries,
|
|
892
|
+
loadFloPay
|
|
893
|
+
});
|
|
894
|
+
//# sourceMappingURL=index.cjs.map
|