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