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