@funnelfox/billing 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.
@@ -0,0 +1,1519 @@
1
+ /**
2
+ * @funnelfox/billing v0.1.0
3
+ * JavaScript SDK for Funnelfox billing with Primer integration
4
+ *
5
+ * @author Funnelfox
6
+ * @license MIT
7
+ */
8
+ (function (global, factory) {
9
+ typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
10
+ typeof define === 'function' && define.amd ? define(['exports'], factory) :
11
+ (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.FunnelfoxSDK = {}));
12
+ })(this, (function (exports) { 'use strict';
13
+
14
+ /**
15
+ * @fileoverview Custom error classes for Funnefox SDK
16
+ */
17
+
18
+ /**
19
+ * Base error class for all Funnefox SDK errors
20
+ */
21
+ class FunnefoxSDKError extends Error {
22
+ /**
23
+ * @param {string} message - Error message
24
+ * @param {string} [code='SDK_ERROR'] - Error code
25
+ * @param {*} [details=null] - Additional error details
26
+ */
27
+ constructor(message, code = 'SDK_ERROR', details = null) {
28
+ super(message);
29
+ this.name = 'FunnefoxSDKError';
30
+ this.code = code;
31
+ this.details = details;
32
+ if (Error.captureStackTrace) {
33
+ Error.captureStackTrace(this, FunnefoxSDKError);
34
+ }
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Error thrown when input validation fails
40
+ */
41
+ class ValidationError extends FunnefoxSDKError {
42
+ /**
43
+ * @param {string} field - Field that failed validation
44
+ * @param {string} message - Validation error message
45
+ * @param {*} [value] - Invalid value that caused the error
46
+ */
47
+ constructor(field, message, value = null) {
48
+ super(`Invalid ${field}: ${message}`, 'VALIDATION_ERROR');
49
+ this.name = 'ValidationError';
50
+ this.field = field;
51
+ this.value = value;
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Error thrown when API calls fail
57
+ */
58
+ class APIError extends FunnefoxSDKError {
59
+ /**
60
+ * @param {string} message - API error message
61
+ * @param {number} [statusCode] - HTTP status code
62
+ * @param {Object} [options] - Additional error details
63
+ * @param {string} [options.errorCode] - API error code (e.g., 'double_purchase')
64
+ * @param {string} [options.errorType] - API error type (e.g., 'api_exception')
65
+ * @param {string} [options.requestId] - Request ID for tracking
66
+ * @param {*} [options.response] - Full API response data
67
+ */
68
+ constructor(message, statusCode = null, options = {}) {
69
+ super(message, options.errorCode || 'API_ERROR');
70
+ this.name = 'APIError';
71
+ this.statusCode = statusCode;
72
+ this.errorCode = options.errorCode || null;
73
+ this.errorType = options.errorType || null;
74
+ this.requestId = options.requestId || null;
75
+ this.response = options.response || null;
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Error thrown when Primer SDK integration fails
81
+ */
82
+ class PrimerError extends FunnefoxSDKError {
83
+ /**
84
+ * @param {string} message - Primer error message
85
+ * @param {*} [primerError] - Original Primer error object
86
+ */
87
+ constructor(message, primerError = null) {
88
+ super(message, 'PRIMER_ERROR');
89
+ this.name = 'PrimerError';
90
+ this.primerError = primerError;
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Error thrown when checkout operations fail
96
+ */
97
+ class CheckoutError extends FunnefoxSDKError {
98
+ /**
99
+ * @param {string} message - Checkout error message
100
+ * @param {string} [phase] - Checkout phase where error occurred
101
+ */
102
+ constructor(message, phase = null) {
103
+ super(message, 'CHECKOUT_ERROR');
104
+ this.name = 'CheckoutError';
105
+ this.phase = phase;
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Error thrown when SDK configuration is invalid
111
+ */
112
+ class ConfigurationError extends FunnefoxSDKError {
113
+ /**
114
+ * @param {string} message - Configuration error message
115
+ */
116
+ constructor(message) {
117
+ super(message, 'CONFIGURATION_ERROR');
118
+ this.name = 'ConfigurationError';
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Error thrown when network requests fail
124
+ */
125
+ class NetworkError extends FunnefoxSDKError {
126
+ /**
127
+ * @param {string} message - Network error message
128
+ * @param {*} [originalError] - Original network error
129
+ */
130
+ constructor(message, originalError = null) {
131
+ super(message, 'NETWORK_ERROR');
132
+ this.name = 'NetworkError';
133
+ this.originalError = originalError;
134
+ }
135
+ }
136
+
137
+ /**
138
+ * @fileoverview Constants for Funnefox SDK
139
+ */
140
+
141
+ /**
142
+ * SDK version
143
+ */
144
+ const SDK_VERSION = '0.1.0';
145
+
146
+ /**
147
+ * Default configuration values
148
+ */
149
+ const DEFAULTS = {
150
+ BASE_URL: 'https://billing.funnelfox.com',
151
+ REGION: 'default',
152
+ SANDBOX: false,
153
+ REQUEST_TIMEOUT: 30000,
154
+ RETRY_ATTEMPTS: 3,
155
+ RETRY_BASE_DELAY: 1000
156
+ };
157
+
158
+ /**
159
+ * Checkout states
160
+ */
161
+ const CHECKOUT_STATES = {
162
+ INITIALIZING: 'initializing',
163
+ READY: 'ready',
164
+ PROCESSING: 'processing',
165
+ ACTION_REQUIRED: 'action_required',
166
+ UPDATING: 'updating',
167
+ COMPLETED: 'completed',
168
+ ERROR: 'error',
169
+ DESTROYED: 'destroyed'
170
+ };
171
+
172
+ /**
173
+ * Event names
174
+ */
175
+ const EVENTS = {
176
+ SUCCESS: 'success',
177
+ ERROR: 'error',
178
+ STATUS_CHANGE: 'status-change',
179
+ DESTROY: 'destroy'
180
+ };
181
+
182
+ /**
183
+ * Error codes
184
+ */
185
+ const ERROR_CODES = {
186
+ SDK_ERROR: 'SDK_ERROR',
187
+ VALIDATION_ERROR: 'VALIDATION_ERROR',
188
+ API_ERROR: 'API_ERROR',
189
+ PRIMER_ERROR: 'PRIMER_ERROR',
190
+ CHECKOUT_ERROR: 'CHECKOUT_ERROR',
191
+ CONFIGURATION_ERROR: 'CONFIGURATION_ERROR',
192
+ NETWORK_ERROR: 'NETWORK_ERROR'
193
+ };
194
+
195
+ /**
196
+ * @fileoverview Lightweight event emitter for Funnefox SDK
197
+ */
198
+
199
+ /**
200
+ * Simple event emitter for checkout instances
201
+ */
202
+ class EventEmitter {
203
+ constructor() {
204
+ this._events = new Map();
205
+ }
206
+
207
+ /**
208
+ * Add an event listener
209
+ * @param {string} eventName - Name of the event
210
+ * @param {function} handler - Event handler function
211
+ * @returns {EventEmitter} Returns this for chaining
212
+ */
213
+ on(eventName, handler) {
214
+ if (typeof handler !== 'function') {
215
+ throw new Error('Event handler must be a function');
216
+ }
217
+ if (!this._events.has(eventName)) {
218
+ this._events.set(eventName, []);
219
+ }
220
+ this._events.get(eventName).push(handler);
221
+ return this;
222
+ }
223
+
224
+ /**
225
+ * Add a one-time event listener
226
+ * @param {string} eventName - Name of the event
227
+ * @param {function} handler - Event handler function
228
+ * @returns {EventEmitter} Returns this for chaining
229
+ */
230
+ once(eventName, handler) {
231
+ if (typeof handler !== 'function') {
232
+ throw new Error('Event handler must be a function');
233
+ }
234
+ const onceWrapper = (...args) => {
235
+ this.off(eventName, onceWrapper);
236
+ handler.apply(this, args);
237
+ };
238
+ return this.on(eventName, onceWrapper);
239
+ }
240
+
241
+ /**
242
+ * Remove an event listener
243
+ * @param {string} eventName - Name of the event
244
+ * @param {function} [handler] - Specific handler to remove. If not provided, removes all handlers for the event
245
+ * @returns {EventEmitter} Returns this for chaining
246
+ */
247
+ off(eventName, handler = null) {
248
+ if (!this._events.has(eventName)) {
249
+ return this;
250
+ }
251
+ if (handler === null) {
252
+ this._events.delete(eventName);
253
+ return this;
254
+ }
255
+ const handlers = this._events.get(eventName);
256
+ const index = handlers.indexOf(handler);
257
+ if (index !== -1) {
258
+ handlers.splice(index, 1);
259
+ if (handlers.length === 0) {
260
+ this._events.delete(eventName);
261
+ }
262
+ }
263
+ return this;
264
+ }
265
+
266
+ /**
267
+ * Emit an event to all registered handlers
268
+ * @param {string} eventName - Name of the event to emit
269
+ * @param {...*} args - Arguments to pass to event handlers
270
+ * @returns {boolean} Returns true if event had listeners, false otherwise
271
+ */
272
+ emit(eventName, ...args) {
273
+ if (!this._events.has(eventName)) {
274
+ return false;
275
+ }
276
+ const handlers = this._events.get(eventName).slice();
277
+ for (const handler of handlers) {
278
+ try {
279
+ handler.apply(this, args);
280
+ } catch (error) {
281
+ // Don't let handler errors break the emission chain
282
+ // eslint-disable-next-line no-console
283
+ console.warn(`Error in event handler for "${eventName}":`, error);
284
+ }
285
+ }
286
+ return true;
287
+ }
288
+
289
+ /**
290
+ * Get the number of listeners for an event
291
+ * @param {string} eventName - Name of the event
292
+ * @returns {number} Number of listeners
293
+ */
294
+ listenerCount(eventName) {
295
+ return this._events.has(eventName) ? this._events.get(eventName).length : 0;
296
+ }
297
+
298
+ /**
299
+ * Get all event names that have listeners
300
+ * @returns {string[]} Array of event names
301
+ */
302
+ eventNames() {
303
+ return Array.from(this._events.keys());
304
+ }
305
+
306
+ /**
307
+ * Remove all listeners for all events
308
+ * @returns {EventEmitter} Returns this for chaining
309
+ */
310
+ removeAllListeners() {
311
+ this._events.clear();
312
+ return this;
313
+ }
314
+
315
+ /**
316
+ * Get all listeners for an event
317
+ * @param {string} eventName - Name of the event
318
+ * @returns {function[]} Array of handler functions
319
+ */
320
+ listeners(eventName) {
321
+ return this._events.has(eventName) ? this._events.get(eventName).slice() : [];
322
+ }
323
+ }
324
+
325
+ /**
326
+ * @fileoverview Helper utilities for Funnefox SDK
327
+ */
328
+
329
+
330
+ /**
331
+ * Merges multiple objects into a new object
332
+ * @param {...Object} objects - Objects to merge
333
+ * @returns {Object} Merged object
334
+ */
335
+ function merge(...objects) {
336
+ const result = {};
337
+ for (const obj of objects) {
338
+ if (obj && typeof obj === 'object') {
339
+ for (const key in obj) {
340
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
341
+ if (typeof obj[key] === 'object' && !Array.isArray(obj[key]) && obj[key] !== null) {
342
+ result[key] = merge(result[key] || {}, obj[key]);
343
+ } else {
344
+ result[key] = obj[key];
345
+ }
346
+ }
347
+ }
348
+ }
349
+ }
350
+ return result;
351
+ }
352
+
353
+ /**
354
+ * Generates a unique identifier
355
+ * @param {string} [prefix=''] - Optional prefix for the ID
356
+ * @returns {string} Unique identifier
357
+ */
358
+ function generateId(prefix = '') {
359
+ const timestamp = Date.now().toString(36);
360
+ const random = Math.random().toString(36).substr(2, 5);
361
+ return `${prefix}${timestamp}_${random}`;
362
+ }
363
+
364
+ /**
365
+ * Waits for a specified amount of time
366
+ * @param {number} ms - Milliseconds to wait
367
+ * @returns {Promise} Promise that resolves after the specified time
368
+ */
369
+ function sleep(ms) {
370
+ return new Promise(resolve => setTimeout(resolve, ms));
371
+ }
372
+
373
+ /**
374
+ * Retries a function with exponential backoff
375
+ * @param {function} fn - Function to retry
376
+ * @param {number} [maxAttempts=3] - Maximum number of attempts
377
+ * @param {number} [baseDelay=1000] - Base delay in milliseconds
378
+ * @returns {Promise} Promise that resolves with the function result
379
+ */
380
+ async function retry(fn, maxAttempts = 3, baseDelay = 1000) {
381
+ let lastError;
382
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
383
+ try {
384
+ return await fn();
385
+ } catch (error) {
386
+ lastError = error;
387
+ if (attempt === maxAttempts) {
388
+ throw lastError;
389
+ }
390
+ const delay = baseDelay * Math.pow(2, attempt - 1);
391
+ await sleep(delay);
392
+ }
393
+ }
394
+ throw lastError;
395
+ }
396
+
397
+ /**
398
+ * Creates a promise that rejects after a timeout
399
+ * @param {Promise} promise - Promise to wrap
400
+ * @param {number} timeoutMs - Timeout in milliseconds
401
+ * @param {string} [message='Operation timed out'] - Timeout error message
402
+ * @returns {Promise} Promise that rejects if timeout is reached
403
+ */
404
+ function withTimeout(promise, timeoutMs, message = 'Operation timed out') {
405
+ const timeoutPromise = new Promise((_, reject) => {
406
+ setTimeout(() => reject(new Error(message)), timeoutMs);
407
+ });
408
+ return Promise.race([promise, timeoutPromise]);
409
+ }
410
+
411
+ /**
412
+ * @fileoverview Primer SDK integration wrapper
413
+ *
414
+ * @typedef {import('@primer-io/checkout-web').IPrimerHeadlessUniversalCheckout} PrimerCheckout
415
+ * @typedef {import('@primer-io/checkout-web').ITokenizationHandler} PrimerTokenizationHandler
416
+ * @typedef {import('@primer-io/checkout-web').IResumeHandler} PrimerResumeHandler
417
+ * @typedef {import('@primer-io/checkout-web').PaymentMethodTokenData} PaymentMethodTokenData
418
+ * @typedef {import('@primer-io/checkout-web').ResumeTokenData} ResumeTokenData
419
+ * @typedef {import('@primer-io/checkout-web').UniversalCheckoutOptions} UniversalCheckoutOptions
420
+ */
421
+
422
+
423
+ /**
424
+ * Wrapper class for Primer SDK integration
425
+ */
426
+ class PrimerWrapper {
427
+ constructor() {
428
+ this.currentCheckout = null;
429
+ this.isInitialized = false;
430
+ }
431
+
432
+ /**
433
+ * Checks if Primer SDK is available
434
+ * @returns {boolean} True if Primer is available
435
+ */
436
+ isPrimerAvailable() {
437
+ return typeof window !== 'undefined' && window.Primer && typeof window.Primer.showUniversalCheckout === 'function';
438
+ }
439
+
440
+ /**
441
+ * Ensures Primer SDK is available
442
+ * @throws {PrimerError} If Primer SDK is not available
443
+ */
444
+ ensurePrimerAvailable() {
445
+ if (!this.isPrimerAvailable()) {
446
+ throw new PrimerError('Primer SDK not found. Please include the Primer SDK script before initializing FunnefoxSDK.');
447
+ }
448
+ }
449
+
450
+ /**
451
+ * Creates a universal checkout instance
452
+ * @param {string} clientToken - Client token from your backend
453
+ * @param {Partial<UniversalCheckoutOptions> & {onTokenizeSuccess: function, onResumeSuccess: function}} options - Checkout options
454
+ * @returns {Promise<PrimerCheckout>} Primer checkout instance
455
+ */
456
+ async showUniversalCheckout(clientToken, options) {
457
+ this.ensurePrimerAvailable();
458
+
459
+ // Extract SDK-managed handlers
460
+ const {
461
+ onTokenizeSuccess,
462
+ onResumeSuccess,
463
+ container,
464
+ ...userPrimerOptions
465
+ } = options;
466
+
467
+ // Merge defaults with user's Primer options
468
+ /** @type {UniversalCheckoutOptions} */
469
+ const primerOptions = merge({
470
+ clientToken,
471
+ container,
472
+ paymentHandling: 'MANUAL',
473
+ apiVersion: '2.4',
474
+ paypal: {
475
+ buttonColor: 'blue',
476
+ paymentFlow: 'PREFER_VAULT'
477
+ }
478
+ }, userPrimerOptions);
479
+
480
+ // Add the required event handlers (must override any user-provided ones)
481
+ primerOptions.onTokenizeSuccess = this._wrapTokenizeHandler(onTokenizeSuccess);
482
+ primerOptions.onResumeSuccess = this._wrapResumeHandler(onResumeSuccess);
483
+ try {
484
+ const checkout = await window.Primer.showUniversalCheckout(clientToken, primerOptions);
485
+ this.currentCheckout = checkout;
486
+ this.isInitialized = true;
487
+ return checkout;
488
+ } catch (error) {
489
+ throw new PrimerError('Failed to initialize Primer checkout', error);
490
+ }
491
+ }
492
+
493
+ /**
494
+ * Wraps the tokenize success handler with error handling
495
+ * @private
496
+ * @param {function(PaymentMethodTokenData, PrimerTokenizationHandler): Promise<void>} handler
497
+ * @returns {function(PaymentMethodTokenData, PrimerTokenizationHandler): Promise<void>}
498
+ */
499
+ _wrapTokenizeHandler(handler) {
500
+ return async (paymentMethodTokenData, primerHandler) => {
501
+ try {
502
+ await handler(paymentMethodTokenData, primerHandler);
503
+ } catch (error) {
504
+ console.error('Error in tokenize handler:', error);
505
+ primerHandler.handleFailure('Payment processing failed. Please try again.');
506
+ }
507
+ };
508
+ }
509
+
510
+ /**
511
+ * Wraps the resume success handler with error handling
512
+ * @private
513
+ * @param {function(ResumeTokenData, PrimerResumeHandler): Promise<void>} handler
514
+ * @returns {function(ResumeTokenData, PrimerResumeHandler): Promise<void>}
515
+ */
516
+ _wrapResumeHandler(handler) {
517
+ return async (resumeTokenData, primerHandler) => {
518
+ try {
519
+ await handler(resumeTokenData, primerHandler);
520
+ } catch (error) {
521
+ console.error('Error in resume handler:', error);
522
+ primerHandler.handleFailure('Payment processing failed. Please try again.');
523
+ }
524
+ };
525
+ }
526
+
527
+ /**
528
+ * Updates the client token for an existing checkout
529
+ * @param {string} newClientToken - New client token
530
+ * @returns {Promise<void>}
531
+ */
532
+ async updateClientToken(newClientToken) {
533
+ if (!this.currentCheckout) {
534
+ throw new PrimerError('No active checkout to update');
535
+ }
536
+ try {
537
+ // Primer SDK doesn't have a direct update method, so we need to destroy and recreate
538
+ // This is handled at the checkout level
539
+ throw new PrimerError('Client token updates require checkout recreation');
540
+ } catch (error) {
541
+ throw new PrimerError('Failed to update client token', error);
542
+ }
543
+ }
544
+
545
+ /**
546
+ * Destroys the current checkout instance
547
+ * @returns {Promise<void>}
548
+ */
549
+ async destroy() {
550
+ if (this.currentCheckout) {
551
+ try {
552
+ // Primer checkout cleanup - this depends on Primer SDK version
553
+ if (typeof this.currentCheckout.destroy === 'function') {
554
+ await this.currentCheckout.destroy();
555
+ } else {
556
+ // Fallback - clear the container
557
+ const container = document.querySelector(this.currentCheckout.container);
558
+ if (container) {
559
+ container.innerHTML = '';
560
+ }
561
+ }
562
+ } catch (error) {
563
+ console.warn('Error destroying Primer checkout:', error);
564
+ } finally {
565
+ this.currentCheckout = null;
566
+ this.isInitialized = false;
567
+ }
568
+ }
569
+ }
570
+
571
+ /**
572
+ * Creates standardized Primer handler helpers
573
+ * @param {Object} handlers - Handler functions
574
+ * @param {function} handlers.onSuccess - Success callback
575
+ * @param {function} handlers.onError - Error callback
576
+ * @param {function} handlers.onActionRequired - Action required callback
577
+ * @returns {Object} Primer-compatible handler functions
578
+ */
579
+ createHandlers(handlers) {
580
+ return {
581
+ /**
582
+ * Handle successful tokenization
583
+ */
584
+ handleSuccess: () => {
585
+ if (handlers.onSuccess) {
586
+ handlers.onSuccess();
587
+ }
588
+ },
589
+ /**
590
+ * Handle payment failure
591
+ */
592
+ handleFailure: message => {
593
+ if (handlers.onError) {
594
+ handlers.onError(new Error(message));
595
+ }
596
+ },
597
+ /**
598
+ * Continue with new client token (for 3DS flows)
599
+ */
600
+ continueWithNewClientToken: newClientToken => {
601
+ if (handlers.onActionRequired) {
602
+ handlers.onActionRequired(newClientToken);
603
+ }
604
+ }
605
+ };
606
+ }
607
+
608
+ /**
609
+ * Gets the current checkout state
610
+ * @returns {PrimerCheckout|null} Current checkout instance or null
611
+ */
612
+ getCurrentCheckout() {
613
+ return this.currentCheckout;
614
+ }
615
+
616
+ /**
617
+ * Checks if checkout is currently active
618
+ * @returns {boolean} True if checkout is active
619
+ */
620
+ isActive() {
621
+ return this.isInitialized && this.currentCheckout !== null;
622
+ }
623
+
624
+ /**
625
+ * Validates that a container element exists and is suitable for Primer
626
+ * @param {string} selector - DOM selector
627
+ * @returns {Element} The container element
628
+ * @throws {PrimerError} If container is invalid
629
+ */
630
+ validateContainer(selector) {
631
+ const element = document.querySelector(selector);
632
+ if (!element) {
633
+ throw new PrimerError(`Checkout container not found: ${selector}`);
634
+ }
635
+
636
+ // Check if container is suitable (visible, not already in use, etc.)
637
+ const computedStyle = window.getComputedStyle(element);
638
+ if (computedStyle.display === 'none') {
639
+ console.warn('Checkout container is hidden, this may cause display issues');
640
+ }
641
+ return element;
642
+ }
643
+ }
644
+
645
+ var primerWrapper = /*#__PURE__*/Object.freeze({
646
+ __proto__: null,
647
+ default: PrimerWrapper
648
+ });
649
+
650
+ /**
651
+ * @fileoverview Input validation utilities for Funnefox SDK
652
+ */
653
+
654
+
655
+ /**
656
+ * Sanitizes a string input
657
+ * @param {*} input - Input to sanitize
658
+ * @returns {string} Sanitized string
659
+ */
660
+ function sanitizeString(input) {
661
+ if (input === null || input === undefined) {
662
+ return '';
663
+ }
664
+ return String(input).trim();
665
+ }
666
+
667
+ /**
668
+ * Validates that a value is a non-empty string
669
+ * @param {*} value - Value to validate
670
+ * @param {string} fieldName - Name of the field for error reporting
671
+ * @returns {boolean} True if valid
672
+ * @throws {ValidationError} If validation fails
673
+ */
674
+ function requireString(value, fieldName) {
675
+ const sanitized = sanitizeString(value);
676
+ if (sanitized.length === 0) {
677
+ throw new ValidationError(fieldName, 'must be a non-empty string', value);
678
+ }
679
+ return true;
680
+ }
681
+
682
+ /**
683
+ * Validates that a container element exists in the DOM
684
+ * @param {string} selector - DOM selector
685
+ * @returns {Element} The found element
686
+ * @throws {ValidationError} If element not found
687
+ */
688
+ function validateContainer(selector) {
689
+ requireString(selector, 'container');
690
+ const element = document.querySelector(selector);
691
+ if (!element) {
692
+ throw new ValidationError('container', `element not found: ${selector}`, selector);
693
+ }
694
+ return element;
695
+ }
696
+
697
+ /**
698
+ * @fileoverview API client for Funnefox backend integration
699
+ */
700
+
701
+
702
+ /**
703
+ * HTTP client for Funnefox API requests
704
+ */
705
+ class APIClient {
706
+ /**
707
+ * @param {Object} config - API client configuration
708
+ * @param {string} config.baseUrl - Base URL for API calls
709
+ * @param {string} config.orgId - Organization identifier
710
+ * @param {number} [config.timeout=30000] - Request timeout in milliseconds
711
+ * @param {number} [config.retryAttempts=3] - Number of retry attempts
712
+ */
713
+ constructor(config) {
714
+ this.baseUrl = config.baseUrl.replace(/\/$/, '');
715
+ this.orgId = config.orgId;
716
+ this.timeout = config.timeout || 30000;
717
+ this.retryAttempts = config.retryAttempts || 3;
718
+ }
719
+
720
+ /**
721
+ * Makes an HTTP request with error handling and retries
722
+ * @param {string} endpoint - API endpoint
723
+ * @param {Object} [options={}] - Request options
724
+ * @returns {Promise<Object>} API response data
725
+ * @throws {APIError|NetworkError} If request fails
726
+ */
727
+ async request(endpoint, options = {}) {
728
+ const url = `${this.baseUrl}/${this.orgId}${endpoint}`;
729
+ const requestOptions = {
730
+ method: 'GET',
731
+ headers: {
732
+ 'Content-Type': 'application/json',
733
+ ...options.headers
734
+ },
735
+ ...options
736
+ };
737
+ try {
738
+ return await retry(async () => {
739
+ return await withTimeout(this._makeRequest(url, requestOptions), this.timeout, 'Request timed out');
740
+ }, this.retryAttempts);
741
+ } catch (error) {
742
+ if (error.name === 'APIError') {
743
+ throw error;
744
+ }
745
+ throw new NetworkError('Network request failed', error);
746
+ }
747
+ }
748
+
749
+ /**
750
+ * Internal method to make the actual HTTP request
751
+ * @private
752
+ */
753
+ async _makeRequest(url, options) {
754
+ let response;
755
+ try {
756
+ response = await fetch(url, options);
757
+ } catch (error) {
758
+ throw new NetworkError('Network request failed', error);
759
+ }
760
+ let data;
761
+ try {
762
+ data = await response.json();
763
+ } catch (error) {
764
+ throw new APIError('Invalid JSON response', response.status, {});
765
+ }
766
+ if (!response.ok) {
767
+ const message = data.message || data.error || `HTTP ${response.status}`;
768
+ throw new APIError(message, response.status, {
769
+ response: data
770
+ });
771
+ }
772
+ return data;
773
+ }
774
+
775
+ /**
776
+ * Creates a client session for checkout
777
+ * @param {Object} params - Session parameters
778
+ * @param {string} params.priceId - Price point identifier
779
+ * @param {string} params.externalId - External user identifier
780
+ * @param {string} params.email - Customer email
781
+ * @param {Object} [params.clientMetadata] - Additional metadata
782
+ * @param {string} [params.region='default'] - Checkout region
783
+ * @param {string} [params.countryCode] - Optional ISO country code
784
+ * @returns {Promise<Object>} Client session response
785
+ */
786
+ async createClientSession(params) {
787
+ const payload = {
788
+ region: params.region || 'default',
789
+ integration_type: 'primer',
790
+ pp_ident: params.priceId,
791
+ external_id: params.externalId,
792
+ email_address: params.email,
793
+ client_metadata: params.clientMetadata || {}
794
+ };
795
+
796
+ // Add country_code if provided (nullable field per Swagger spec)
797
+ if (params.countryCode !== undefined) {
798
+ payload.country_code = params.countryCode;
799
+ }
800
+ return await this.request('/v1/checkout/create_client_session', {
801
+ method: 'POST',
802
+ body: JSON.stringify(payload)
803
+ });
804
+ }
805
+
806
+ /**
807
+ * Updates an existing client session
808
+ * @param {Object} params - Update parameters
809
+ * @param {string} params.orderId - Order identifier
810
+ * @param {string} params.clientToken - Current client token
811
+ * @param {string} params.priceId - New price identifier
812
+ * @returns {Promise<Object>} Update response
813
+ */
814
+ async updateClientSession(params) {
815
+ const payload = {
816
+ order_id: params.orderId,
817
+ client_token: params.clientToken,
818
+ pp_ident: params.priceId
819
+ };
820
+ return await this.request('/v1/checkout/update_client_session', {
821
+ method: 'POST',
822
+ body: JSON.stringify(payload)
823
+ });
824
+ }
825
+
826
+ /**
827
+ * Creates a payment with tokenized payment method
828
+ * @param {Object} params - Payment parameters
829
+ * @param {string} params.orderId - Order identifier
830
+ * @param {string} params.paymentMethodToken - Payment method token from Primer
831
+ * @returns {Promise<Object>} Payment response
832
+ */
833
+ async createPayment(params) {
834
+ const payload = {
835
+ order_id: params.orderId,
836
+ payment_method_token: params.paymentMethodToken
837
+ };
838
+ return await this.request('/v1/checkout/create_payment', {
839
+ method: 'POST',
840
+ body: JSON.stringify(payload)
841
+ });
842
+ }
843
+
844
+ /**
845
+ * Resumes a payment with 3DS or similar flows
846
+ * @param {Object} params - Resume parameters
847
+ * @param {string} params.orderId - Order identifier
848
+ * @param {string} params.resumeToken - Resume token from Primer
849
+ * @returns {Promise<Object>} Resume response
850
+ */
851
+ async resumePayment(params) {
852
+ const payload = {
853
+ order_id: params.orderId,
854
+ resume_token: params.resumeToken
855
+ };
856
+ return await this.request('/v1/checkout/resume_payment', {
857
+ method: 'POST',
858
+ body: JSON.stringify(payload)
859
+ });
860
+ }
861
+
862
+ /**
863
+ * One-click payment for returning customers with saved payment methods
864
+ * @param {Object} params - One-click payment parameters
865
+ * @param {string} params.externalId - External user identifier
866
+ * @param {string} params.priceId - Price point identifier
867
+ * @param {Object} [params.clientMetadata] - Additional metadata
868
+ * @returns {Promise<Object>} Payment response
869
+ */
870
+ async oneClickPayment(params) {
871
+ const payload = {
872
+ external_id: params.externalId,
873
+ pp_ident: params.priceId,
874
+ client_metadata: params.clientMetadata || {}
875
+ };
876
+ return await this.request('/v1/checkout/one_click', {
877
+ method: 'POST',
878
+ body: JSON.stringify(payload)
879
+ });
880
+ }
881
+
882
+ /**
883
+ * Processes session creation response (create_client_session)
884
+ * @param {Object} response - Raw API response
885
+ * @returns {Object} Processed session data
886
+ * @throws {APIError} If response indicates an error
887
+ */
888
+ processSessionResponse(response) {
889
+ if (response.status === 'error') {
890
+ // Extract error details from error array format
891
+ const firstError = response.error?.[0];
892
+ const message = firstError?.msg || 'Session creation failed';
893
+ throw new APIError(message, null, {
894
+ errorCode: firstError?.code,
895
+ errorType: firstError?.type,
896
+ requestId: response.req_id,
897
+ response
898
+ });
899
+ }
900
+ const data = response.data || response;
901
+ return {
902
+ type: 'session_created',
903
+ orderId: data.order_id,
904
+ clientToken: data.client_token
905
+ };
906
+ }
907
+
908
+ /**
909
+ * Processes payment/resume response (create_payment, resume_payment, one_click)
910
+ * @param {Object} response - Raw API response
911
+ * @returns {Object} Processed payment data
912
+ * @throws {APIError} If response indicates an error
913
+ */
914
+ processPaymentResponse(response) {
915
+ if (response.status === 'error') {
916
+ // Extract error details from error array format
917
+ const firstError = response.error?.[0];
918
+ const message = firstError?.msg || 'Payment request failed';
919
+ throw new APIError(message, null, {
920
+ errorCode: firstError?.code,
921
+ errorType: firstError?.type,
922
+ requestId: response.req_id,
923
+ response
924
+ });
925
+ }
926
+ const data = response.data || response;
927
+
928
+ // Check for action required (3DS, etc.)
929
+ if (data.action_required_token) {
930
+ return {
931
+ type: 'action_required',
932
+ orderId: data.order_id,
933
+ clientToken: data.action_required_token
934
+ };
935
+ }
936
+
937
+ // Handle checkout status
938
+ if (data.checkout_status) {
939
+ switch (data.checkout_status) {
940
+ case 'succeeded':
941
+ return {
942
+ type: 'success',
943
+ orderId: data.order_id,
944
+ status: 'succeeded',
945
+ transactionId: data.transaction_id
946
+ };
947
+ case 'failed':
948
+ throw new APIError(data.failed_message_for_user || 'Payment failed', null, data);
949
+ case 'cancelled':
950
+ throw new APIError('Payment was cancelled by user', null, data);
951
+ case 'processing':
952
+ return {
953
+ type: 'processing',
954
+ orderId: data.order_id,
955
+ status: 'processing'
956
+ };
957
+ default:
958
+ throw new APIError(`Unhandled checkout status: ${data.checkout_status}`, null, data);
959
+ }
960
+ }
961
+ throw new APIError('Invalid payment response format', null, data);
962
+ }
963
+
964
+ /**
965
+ * Processes API response and extracts relevant data
966
+ * @deprecated Use processSessionResponse() or processPaymentResponse() instead
967
+ * @param {Object} response - Raw API response
968
+ * @returns {Object} Processed response data
969
+ * @throws {APIError} If response indicates an error
970
+ */
971
+ processResponse(response) {
972
+ // Try to detect response type and delegate to specialized methods
973
+ const data = response.data || response;
974
+ if (data.client_token && data.order_id && !data.checkout_status) {
975
+ return this.processSessionResponse(response);
976
+ }
977
+ if (data.checkout_status || data.action_required_token) {
978
+ return this.processPaymentResponse(response);
979
+ }
980
+
981
+ // Fallback for unknown response types
982
+ throw new APIError('Unknown response format', null, response);
983
+ }
984
+ }
985
+
986
+ /**
987
+ * @fileoverview Checkout instance manager for Funnefox SDK
988
+ */
989
+
990
+
991
+ /**
992
+ * Checkout instance that manages the complete checkout lifecycle
993
+ * @typedef {import('./types').CheckoutInstance} CheckoutInstance
994
+ * @typedef {import('./types').CheckoutConfig} CheckoutConfig
995
+ * @typedef {import('./types').CheckoutConfigWithCallbacks} CheckoutConfigWithCallbacks
996
+ * @typedef {import('./types').PaymentResult} PaymentResult
997
+ * @typedef {import('./types').CheckoutState} CheckoutState
998
+ * @typedef {import('@primer-io/checkout-web').PaymentMethodTokenData} PaymentMethodTokenData
999
+ * @typedef {import('@primer-io/checkout-web').ResumeTokenData} ResumeTokenData
1000
+ * @typedef {import('@primer-io/checkout-web').ITokenizationHandler} PrimerHandler
1001
+ * @typedef {import('@primer-io/checkout-web').UniversalCheckoutOptions} UniversalCheckoutOptions
1002
+ */
1003
+ class CheckoutInstance extends EventEmitter {
1004
+ /**
1005
+ * @param {Object} config - Checkout configuration
1006
+ * @param {string} config.orgId - Organization ID
1007
+ * @param {string} [config.baseUrl] - API base URL
1008
+ * @param {string} [config.region] - Region
1009
+ * @param {CheckoutConfig | CheckoutConfigWithCallbacks} config.checkoutConfig - Checkout configuration
1010
+ */
1011
+ constructor(config) {
1012
+ super();
1013
+ this.id = generateId('checkout_');
1014
+ this.orgId = config.orgId;
1015
+ this.baseUrl = config.baseUrl;
1016
+ this.region = config.region;
1017
+ this.checkoutConfig = {
1018
+ ...config.checkoutConfig
1019
+ };
1020
+
1021
+ // Extract callbacks from config
1022
+ this.callbacks = {
1023
+ onSuccess: this.checkoutConfig.onSuccess,
1024
+ onError: this.checkoutConfig.onError,
1025
+ onStatusChange: this.checkoutConfig.onStatusChange,
1026
+ onDestroy: this.checkoutConfig.onDestroy
1027
+ };
1028
+
1029
+ // Clean callbacks from config to avoid sending to API
1030
+ delete this.checkoutConfig.onSuccess;
1031
+ delete this.checkoutConfig.onError;
1032
+ delete this.checkoutConfig.onStatusChange;
1033
+ delete this.checkoutConfig.onDestroy;
1034
+
1035
+ // Internal state
1036
+ this.state = 'initializing';
1037
+ this.orderId = null;
1038
+ this.clientToken = null;
1039
+ this.primerWrapper = new PrimerWrapper();
1040
+ this.isDestroyed = false;
1041
+
1042
+ // Set up callback bridges if provided
1043
+ this._setupCallbackBridges();
1044
+
1045
+ // Bind methods to preserve context
1046
+ this._handleTokenizeSuccess = this._handleTokenizeSuccess.bind(this);
1047
+ this._handleResumeSuccess = this._handleResumeSuccess.bind(this);
1048
+ }
1049
+
1050
+ /**
1051
+ * Set up bridges between events and callbacks
1052
+ * @private
1053
+ */
1054
+ _setupCallbackBridges() {
1055
+ if (this.callbacks.onSuccess) {
1056
+ this.on('success', this.callbacks.onSuccess);
1057
+ }
1058
+ if (this.callbacks.onError) {
1059
+ this.on('error', this.callbacks.onError);
1060
+ }
1061
+ if (this.callbacks.onStatusChange) {
1062
+ this.on('status-change', this.callbacks.onStatusChange);
1063
+ }
1064
+ if (this.callbacks.onDestroy) {
1065
+ this.on('destroy', this.callbacks.onDestroy);
1066
+ }
1067
+ }
1068
+
1069
+ /**
1070
+ * Initialize the checkout instance
1071
+ * @returns {Promise<CheckoutInstance>} Returns this instance for chaining
1072
+ */
1073
+ async initialize() {
1074
+ try {
1075
+ this._setState('initializing');
1076
+
1077
+ // Validate container exists
1078
+ validateContainer(this.checkoutConfig.container);
1079
+
1080
+ // Create API client and session
1081
+ this.apiClient = new APIClient({
1082
+ baseUrl: this.baseUrl || DEFAULTS.BASE_URL,
1083
+ orgId: this.orgId,
1084
+ timeout: DEFAULTS.REQUEST_TIMEOUT,
1085
+ retryAttempts: DEFAULTS.RETRY_ATTEMPTS
1086
+ });
1087
+ const sessionResponse = await this.apiClient.createClientSession({
1088
+ priceId: this.checkoutConfig.priceId,
1089
+ externalId: this.checkoutConfig.customer.externalId,
1090
+ email: this.checkoutConfig.customer.email,
1091
+ region: this.region || DEFAULTS.REGION,
1092
+ clientMetadata: this.checkoutConfig.clientMetadata,
1093
+ countryCode: this.checkoutConfig.customer.countryCode
1094
+ });
1095
+ const sessionData = this.apiClient.processSessionResponse(sessionResponse);
1096
+ this.orderId = sessionData.orderId;
1097
+ this.clientToken = sessionData.clientToken;
1098
+
1099
+ // Initialize Primer checkout
1100
+ await this._initializePrimerCheckout();
1101
+ this._setState('ready');
1102
+ return this;
1103
+ } catch (error) {
1104
+ this._setState('error');
1105
+ this.emit('error', error);
1106
+ throw error;
1107
+ }
1108
+ }
1109
+
1110
+ /**
1111
+ * Initialize Primer checkout with current client token
1112
+ * @private
1113
+ */
1114
+ async _initializePrimerCheckout() {
1115
+ /** @type {Partial<UniversalCheckoutOptions> & {onTokenizeSuccess: function, onResumeSuccess: function}} */
1116
+ const checkoutOptions = {
1117
+ container: this.checkoutConfig.container,
1118
+ onTokenizeSuccess: this._handleTokenizeSuccess,
1119
+ onResumeSuccess: this._handleResumeSuccess,
1120
+ ...(this.checkoutConfig.universalCheckoutOptions || {})
1121
+ };
1122
+ await this.primerWrapper.showUniversalCheckout(this.clientToken, checkoutOptions);
1123
+ }
1124
+
1125
+ /**
1126
+ * Handle successful payment method tokenization
1127
+ * @private
1128
+ * @param {PaymentMethodTokenData} paymentMethodTokenData - Payment method token from Primer
1129
+ * @param {PrimerHandler} primerHandler - Primer handler for success/failure
1130
+ */
1131
+ async _handleTokenizeSuccess(paymentMethodTokenData, primerHandler) {
1132
+ try {
1133
+ this._setState('processing');
1134
+ const paymentResponse = await this.apiClient.createPayment({
1135
+ orderId: this.orderId,
1136
+ paymentMethodToken: paymentMethodTokenData.token
1137
+ });
1138
+ const result = this.apiClient.processPaymentResponse(paymentResponse);
1139
+ await this._processPaymentResult(result, primerHandler);
1140
+ } catch (error) {
1141
+ this._setState('error');
1142
+ this.emit('error', error);
1143
+ primerHandler.handleFailure(error.message || 'Payment processing failed');
1144
+ }
1145
+ }
1146
+
1147
+ /**
1148
+ * Handle successful payment resume (3DS flows)
1149
+ * @private
1150
+ * @param {ResumeTokenData} resumeTokenData - Resume token from Primer
1151
+ * @param {PrimerHandler} primerHandler - Primer handler for success/failure
1152
+ */
1153
+ async _handleResumeSuccess(resumeTokenData, primerHandler) {
1154
+ try {
1155
+ this._setState('processing');
1156
+ const resumeResponse = await this.apiClient.resumePayment({
1157
+ orderId: this.orderId,
1158
+ resumeToken: resumeTokenData.resumeToken
1159
+ });
1160
+ const result = this.apiClient.processPaymentResponse(resumeResponse);
1161
+ await this._processPaymentResult(result, primerHandler);
1162
+ } catch (error) {
1163
+ this._setState('error');
1164
+ this.emit('error', error);
1165
+ primerHandler.handleFailure(error.message || 'Payment processing failed');
1166
+ }
1167
+ }
1168
+
1169
+ /**
1170
+ * Process payment result and handle different scenarios
1171
+ * @private
1172
+ */
1173
+ async _processPaymentResult(result, primerHandler) {
1174
+ switch (result.type) {
1175
+ case 'success':
1176
+ this._setState('completed');
1177
+ this.emit('success', {
1178
+ orderId: result.orderId,
1179
+ status: result.status,
1180
+ transactionId: result.transactionId,
1181
+ metadata: result.metadata
1182
+ });
1183
+ primerHandler.handleSuccess();
1184
+ break;
1185
+ case 'action_required':
1186
+ this._setState('action_required');
1187
+ // Update client token and continue
1188
+ this.clientToken = result.clientToken;
1189
+ primerHandler.continueWithNewClientToken(result.clientToken);
1190
+ break;
1191
+ case 'processing':
1192
+ this._setState('processing');
1193
+ // Let the payment process - usually requires polling or webhook
1194
+ setTimeout(() => {
1195
+ primerHandler.handleFailure('Payment is still processing. Please check back later.');
1196
+ }, 30000);
1197
+ break;
1198
+ default:
1199
+ throw new CheckoutError(`Unknown payment result type: ${result.type}`);
1200
+ }
1201
+ }
1202
+
1203
+ /**
1204
+ * Update the checkout to use a different price
1205
+ * @param {string} newPriceId - New price identifier
1206
+ * @returns {Promise<void>}
1207
+ */
1208
+ async updatePrice(newPriceId) {
1209
+ this._ensureNotDestroyed();
1210
+ requireString(newPriceId, 'priceId');
1211
+ if (this.state === 'processing') {
1212
+ throw new CheckoutError('Cannot update price while payment is processing');
1213
+ }
1214
+ try {
1215
+ this._setState('updating');
1216
+
1217
+ // Update client session with new price
1218
+ await this.apiClient.updateClientSession({
1219
+ orderId: this.orderId,
1220
+ clientToken: this.clientToken,
1221
+ priceId: newPriceId
1222
+ });
1223
+ this.checkoutConfig.priceId = newPriceId;
1224
+ this._setState('ready');
1225
+ this.emit('status-change', 'price-updated');
1226
+ } catch (error) {
1227
+ this._setState('error');
1228
+ this.emit('error', error);
1229
+ throw error;
1230
+ }
1231
+ }
1232
+
1233
+ /**
1234
+ * Get current checkout status
1235
+ * @returns {Object} Status information
1236
+ */
1237
+ getStatus() {
1238
+ return {
1239
+ id: this.id,
1240
+ state: this.state,
1241
+ orderId: this.orderId,
1242
+ priceId: this.checkoutConfig.priceId,
1243
+ isDestroyed: this.isDestroyed
1244
+ };
1245
+ }
1246
+
1247
+ /**
1248
+ * Destroy the checkout instance and clean up resources
1249
+ * @returns {Promise<void>}
1250
+ */
1251
+ async destroy() {
1252
+ if (this.isDestroyed) {
1253
+ return;
1254
+ }
1255
+ try {
1256
+ // Clean up Primer checkout
1257
+ await this.primerWrapper.destroy();
1258
+
1259
+ // Clear state
1260
+ this._setState('destroyed');
1261
+ this.orderId = null;
1262
+ this.clientToken = null;
1263
+ this.isDestroyed = true;
1264
+
1265
+ // Emit destroy event
1266
+ this.emit('destroy');
1267
+
1268
+ // Remove all listeners
1269
+ this.removeAllListeners();
1270
+ } catch (error) {
1271
+ console.warn('Error during checkout cleanup:', error);
1272
+ }
1273
+ }
1274
+
1275
+ /**
1276
+ * Set internal state and emit change events
1277
+ * @private
1278
+ */
1279
+ _setState(newState) {
1280
+ if (this.state !== newState) {
1281
+ const oldState = this.state;
1282
+ this.state = newState;
1283
+ this.emit('status-change', newState, oldState);
1284
+ }
1285
+ }
1286
+
1287
+ /**
1288
+ * Ensure checkout is not destroyed
1289
+ * @private
1290
+ */
1291
+ _ensureNotDestroyed() {
1292
+ if (this.isDestroyed) {
1293
+ throw new CheckoutError('Checkout instance has been destroyed');
1294
+ }
1295
+ }
1296
+
1297
+ /**
1298
+ * Get the container element
1299
+ * @returns {Element} Container element
1300
+ */
1301
+ getContainer() {
1302
+ return document.querySelector(this.checkoutConfig.container);
1303
+ }
1304
+
1305
+ /**
1306
+ * Check if checkout is in a given state
1307
+ * @param {string} state - State to check
1308
+ * @returns {boolean} True if in the specified state
1309
+ */
1310
+ isInState(state) {
1311
+ return this.state === state;
1312
+ }
1313
+
1314
+ /**
1315
+ * Check if checkout is ready for user interaction
1316
+ * @returns {boolean} True if ready
1317
+ */
1318
+ isReady() {
1319
+ return this.state === 'ready' && !this.isDestroyed;
1320
+ }
1321
+
1322
+ /**
1323
+ * Check if checkout is currently processing a payment
1324
+ * @returns {boolean} True if processing
1325
+ */
1326
+ isProcessing() {
1327
+ return ['processing', 'action_required'].includes(this.state);
1328
+ }
1329
+ }
1330
+
1331
+ /**
1332
+ * @fileoverview Public API with configuration and orchestration logic
1333
+ */
1334
+
1335
+ let defaultConfig = null;
1336
+
1337
+ /**
1338
+ * Configure global SDK settings
1339
+ * @param {import('./types').SDKConfig} config - SDK configuration
1340
+ */
1341
+ function configure(config) {
1342
+ defaultConfig = config;
1343
+ }
1344
+
1345
+ /**
1346
+ * Resolve configuration with fallback chain
1347
+ * @private
1348
+ * @param {Object} options - Options with optional orgId and apiConfig
1349
+ * @param {string} functionName - Name of calling function for error messages
1350
+ * @returns {{orgId: string, baseUrl: string, region: string}} Resolved configuration
1351
+ */
1352
+ function resolveConfig(options, functionName) {
1353
+ const {
1354
+ orgId,
1355
+ apiConfig
1356
+ } = options;
1357
+
1358
+ // Fallback chain: params > configure() > error
1359
+ const finalOrgId = orgId || defaultConfig?.orgId;
1360
+ if (!finalOrgId) {
1361
+ throw new Error(`orgId is required. Pass it to ${functionName}() or call configure() first.`);
1362
+ }
1363
+ const finalBaseUrl = apiConfig?.baseUrl || defaultConfig?.baseUrl || DEFAULTS.BASE_URL;
1364
+ const finalRegion = apiConfig?.region || defaultConfig?.region || DEFAULTS.REGION;
1365
+ return {
1366
+ orgId: finalOrgId,
1367
+ baseUrl: finalBaseUrl,
1368
+ region: finalRegion
1369
+ };
1370
+ }
1371
+
1372
+ /**
1373
+ * Create a checkout instance - supports both events and callbacks
1374
+ * @param {import('./types').CreateCheckoutOptions} options - Checkout options with optional SDK config
1375
+ * @returns {Promise<import('./types').CheckoutInstance>} Checkout instance
1376
+ */
1377
+ async function createCheckout(options) {
1378
+ const {
1379
+ ...checkoutConfig
1380
+ } = options;
1381
+
1382
+ // Verify Primer SDK is available
1383
+ const primerWrapper = new PrimerWrapper();
1384
+ primerWrapper.ensurePrimerAvailable();
1385
+
1386
+ // Resolve configuration with fallback chain
1387
+ const config = resolveConfig(options, 'createCheckout');
1388
+ const checkout = new CheckoutInstance({
1389
+ ...config,
1390
+ checkoutConfig
1391
+ });
1392
+ await checkout.initialize();
1393
+ return checkout;
1394
+ }
1395
+
1396
+ /**
1397
+ * Direct checkout creation with Primer (legacy compatible)
1398
+ * @param {string} clientToken - Pre-created client token
1399
+ * @param {Object} options - Primer options
1400
+ * @returns {Promise<Object>} Primer checkout instance
1401
+ */
1402
+ async function showUniversalCheckout(clientToken, options) {
1403
+ // Import dynamically to avoid circular dependencies
1404
+ const {
1405
+ default: PrimerWrapper
1406
+ } = await Promise.resolve().then(function () { return primerWrapper; });
1407
+ const primerWrapper$1 = new PrimerWrapper();
1408
+ return await primerWrapper$1.showUniversalCheckout(clientToken, options);
1409
+ }
1410
+
1411
+ /**
1412
+ * Create client session for manual checkout integration
1413
+ * @param {Object} params - Session creation parameters
1414
+ * @param {string} params.priceId - Price identifier
1415
+ * @param {string} params.externalId - Customer external ID
1416
+ * @param {string} params.email - Customer email
1417
+ * @param {string} [params.orgId] - Organization ID (optional if configured)
1418
+ * @param {import('./types').APIConfig} [params.apiConfig] - Optional API config override
1419
+ * @param {Object} [params.clientMetadata] - Optional client metadata
1420
+ * @param {string} [params.countryCode] - Optional country code
1421
+ * @returns {Promise<{clientToken: string, orderId: string, type: string}>} Session data
1422
+ */
1423
+ async function createClientSession(params) {
1424
+ const {
1425
+ priceId,
1426
+ externalId,
1427
+ email,
1428
+ clientMetadata,
1429
+ countryCode
1430
+ } = params;
1431
+
1432
+ // Resolve configuration with fallback chain
1433
+ const config = resolveConfig(params, 'createClientSession');
1434
+
1435
+ // Create API client and session
1436
+ const apiClient = new APIClient({
1437
+ baseUrl: config.baseUrl,
1438
+ orgId: config.orgId,
1439
+ timeout: DEFAULTS.REQUEST_TIMEOUT,
1440
+ retryAttempts: DEFAULTS.RETRY_ATTEMPTS
1441
+ });
1442
+ const sessionResponse = await apiClient.createClientSession({
1443
+ priceId,
1444
+ externalId,
1445
+ email,
1446
+ region: config.region,
1447
+ clientMetadata,
1448
+ countryCode
1449
+ });
1450
+ return apiClient.processSessionResponse(sessionResponse);
1451
+ }
1452
+
1453
+ /**
1454
+ * @fileoverview Main entry point for @funnelfox/billing
1455
+ *
1456
+ * Modern functional SDK for subscription payments with Primer integration
1457
+ *
1458
+ * @example
1459
+ * // Functional style (simple)
1460
+ * import { configure, createCheckout } from '@funnelfox/billing';
1461
+ *
1462
+ * configure({
1463
+ * orgId: 'your-org-id'
1464
+ * });
1465
+ *
1466
+ * const checkout = await createCheckout({
1467
+ * priceId: 'price_123',
1468
+ * customer: {
1469
+ * externalId: 'user_456',
1470
+ * email: 'user@example.com'
1471
+ * },
1472
+ * container: '#checkout-container'
1473
+ * });
1474
+ *
1475
+ * @example
1476
+ * // Namespace style
1477
+ * import { Billing } from '@funnelfox/billing';
1478
+ *
1479
+ * Billing.configure({ orgId: 'your-org-id' });
1480
+ * const checkout = await Billing.createCheckout({ ... });
1481
+ *
1482
+ * // Handle events
1483
+ * checkout.on('success', (result) => {
1484
+ * console.log('Payment completed:', result.orderId);
1485
+ * });
1486
+ */
1487
+
1488
+ const Billing = {
1489
+ configure: configure,
1490
+ createCheckout: createCheckout,
1491
+ showUniversalCheckout: showUniversalCheckout,
1492
+ createClientSession: createClientSession
1493
+ };
1494
+ if (typeof window !== 'undefined') {
1495
+ window.Billing = Billing;
1496
+ }
1497
+
1498
+ exports.APIError = APIError;
1499
+ exports.Billing = Billing;
1500
+ exports.CHECKOUT_STATES = CHECKOUT_STATES;
1501
+ exports.CheckoutError = CheckoutError;
1502
+ exports.ConfigurationError = ConfigurationError;
1503
+ exports.DEFAULTS = DEFAULTS;
1504
+ exports.ERROR_CODES = ERROR_CODES;
1505
+ exports.EVENTS = EVENTS;
1506
+ exports.FunnefoxSDKError = FunnefoxSDKError;
1507
+ exports.NetworkError = NetworkError;
1508
+ exports.PrimerError = PrimerError;
1509
+ exports.SDK_VERSION = SDK_VERSION;
1510
+ exports.ValidationError = ValidationError;
1511
+ exports.configure = configure;
1512
+ exports.createCheckout = createCheckout;
1513
+ exports.createClientSession = createClientSession;
1514
+ exports.default = Billing;
1515
+ exports.showUniversalCheckout = showUniversalCheckout;
1516
+
1517
+ Object.defineProperty(exports, '__esModule', { value: true });
1518
+
1519
+ }));