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