@funnelfox/billing 0.1.1 → 0.2.1

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