@funnelfox/billing 0.1.1 → 0.3.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.
@@ -5,1492 +5,1120 @@
5
5
  * @author Funnelfox
6
6
  * @license MIT
7
7
  */
8
+ /* eslint-disable @typescript-eslint/no-explicit-any */
8
9
  /**
9
- * @fileoverview Custom error classes for Funnefox SDK
10
+ * @fileoverview Lightweight event emitter for Funnefox SDK
10
11
  */
12
+ class EventEmitter {
13
+ constructor() {
14
+ this._events = new Map();
15
+ }
16
+ on(eventName, handler) {
17
+ if (typeof handler !== 'function') {
18
+ throw new Error('Event handler must be a function');
19
+ }
20
+ if (!this._events.has(eventName)) {
21
+ this._events.set(eventName, []);
22
+ }
23
+ this._events.get(eventName).push(handler);
24
+ return this;
25
+ }
26
+ once(eventName, handler) {
27
+ if (typeof handler !== 'function') {
28
+ throw new Error('Event handler must be a function');
29
+ }
30
+ const onceWrapper = (...args) => {
31
+ this.off(eventName, onceWrapper);
32
+ handler.apply(this, args);
33
+ };
34
+ return this.on(eventName, onceWrapper);
35
+ }
36
+ off(eventName, handler = null) {
37
+ if (!this._events.has(eventName)) {
38
+ return this;
39
+ }
40
+ if (handler === null) {
41
+ this._events.delete(eventName);
42
+ return this;
43
+ }
44
+ const handlers = this._events.get(eventName);
45
+ const index = handlers.indexOf(handler);
46
+ if (index !== -1) {
47
+ handlers.splice(index, 1);
48
+ if (handlers.length === 0) {
49
+ this._events.delete(eventName);
50
+ }
51
+ }
52
+ return this;
53
+ }
54
+ emit(eventName, ...args) {
55
+ if (!this._events.has(eventName)) {
56
+ return false;
57
+ }
58
+ const handlers = this._events.get(eventName).slice();
59
+ for (const handler of handlers) {
60
+ try {
61
+ handler.apply(this, args);
62
+ }
63
+ catch (error) {
64
+ // eslint-disable-next-line no-console
65
+ console.warn(`Error in event handler for "${String(eventName)}":`, error);
66
+ }
67
+ }
68
+ return true;
69
+ }
70
+ listenerCount(eventName) {
71
+ return this._events.has(eventName)
72
+ ? this._events.get(eventName).length
73
+ : 0;
74
+ }
75
+ eventNames() {
76
+ return Array.from(this._events.keys());
77
+ }
78
+ removeAllListeners() {
79
+ this._events.clear();
80
+ return this;
81
+ }
82
+ listeners(eventName) {
83
+ return this._events.has(eventName)
84
+ ? this._events.get(eventName).slice()
85
+ : [];
86
+ }
87
+ }
11
88
 
12
89
  /**
13
- * Base error class for all Funnefox SDK errors
90
+ * @fileoverview Custom error classes for Funnefox SDK
14
91
  */
15
92
  class FunnefoxSDKError extends Error {
16
- /**
17
- * @param {string} message - Error message
18
- * @param {string} [code='SDK_ERROR'] - Error code
19
- * @param {*} [details=null] - Additional error details
20
- */
21
- constructor(message, code = 'SDK_ERROR', details = null) {
22
- super(message);
23
- this.name = 'FunnefoxSDKError';
24
- this.code = code;
25
- this.details = details;
26
- if (Error.captureStackTrace) {
27
- Error.captureStackTrace(this, FunnefoxSDKError);
93
+ constructor(message, code = ERROR_CODES.SDK_ERROR, details = null) {
94
+ super(message);
95
+ this.name = 'FunnefoxSDKError';
96
+ this.code = code;
97
+ this.details = details;
98
+ if (Error.captureStackTrace) {
99
+ Error.captureStackTrace(this, FunnefoxSDKError);
100
+ }
28
101
  }
29
- }
30
102
  }
31
-
32
- /**
33
- * Error thrown when input validation fails
34
- */
35
103
  class ValidationError extends FunnefoxSDKError {
36
- /**
37
- * @param {string} field - Field that failed validation
38
- * @param {string} message - Validation error message
39
- * @param {*} [value] - Invalid value that caused the error
40
- */
41
- constructor(field, message, value = null) {
42
- super(`Invalid ${field}: ${message}`, 'VALIDATION_ERROR');
43
- this.name = 'ValidationError';
44
- this.field = field;
45
- this.value = value;
46
- }
104
+ constructor(field, message, value = null) {
105
+ super(`Invalid ${field}: ${message}`, ERROR_CODES.VALIDATION_ERROR);
106
+ this.name = 'ValidationError';
107
+ this.field = field;
108
+ this.value = value;
109
+ }
47
110
  }
48
-
49
- /**
50
- * Error thrown when API calls fail
51
- */
52
111
  class APIError extends FunnefoxSDKError {
53
- /**
54
- * @param {string} message - API error message
55
- * @param {number} [statusCode] - HTTP status code
56
- * @param {Object} [options] - Additional error details
57
- * @param {string} [options.errorCode] - API error code (e.g., 'double_purchase')
58
- * @param {string} [options.errorType] - API error type (e.g., 'api_exception')
59
- * @param {string} [options.requestId] - Request ID for tracking
60
- * @param {*} [options.response] - Full API response data
61
- */
62
- constructor(message, statusCode = null, options = {}) {
63
- super(message, options.errorCode || 'API_ERROR');
64
- this.name = 'APIError';
65
- this.statusCode = statusCode;
66
- this.errorCode = options.errorCode || null;
67
- this.errorType = options.errorType || null;
68
- this.requestId = options.requestId || null;
69
- this.response = options.response || null;
70
- }
112
+ constructor(message, statusCode = null, options = {}) {
113
+ super(message, options.errorCode || ERROR_CODES.API_ERROR);
114
+ this.name = 'APIError';
115
+ this.statusCode = statusCode;
116
+ this.errorCode = options.errorCode || null;
117
+ this.errorType = options.errorType || null;
118
+ this.requestId = options.requestId || null;
119
+ this.response = options.response || null;
120
+ }
71
121
  }
72
-
73
- /**
74
- * Error thrown when Primer SDK integration fails
75
- */
76
122
  class PrimerError extends FunnefoxSDKError {
77
- /**
78
- * @param {string} message - Primer error message
79
- * @param {*} [primerError] - Original Primer error object
80
- */
81
- constructor(message, primerError = null) {
82
- super(message, 'PRIMER_ERROR');
83
- this.name = 'PrimerError';
84
- this.primerError = primerError;
85
- }
123
+ constructor(message, primerError = null) {
124
+ super(message, ERROR_CODES.PRIMER_ERROR);
125
+ this.name = 'PrimerError';
126
+ this.primerError = primerError;
127
+ }
86
128
  }
87
-
88
- /**
89
- * Error thrown when checkout operations fail
90
- */
91
129
  class CheckoutError extends FunnefoxSDKError {
92
- /**
93
- * @param {string} message - Checkout error message
94
- * @param {string} [phase] - Checkout phase where error occurred
95
- */
96
- constructor(message, phase = null) {
97
- super(message, 'CHECKOUT_ERROR');
98
- this.name = 'CheckoutError';
99
- this.phase = phase;
100
- }
130
+ constructor(message, phase = null) {
131
+ super(message, ERROR_CODES.CHECKOUT_ERROR);
132
+ this.name = 'CheckoutError';
133
+ this.phase = phase;
134
+ }
101
135
  }
102
-
103
- /**
104
- * Error thrown when SDK configuration is invalid
105
- */
106
136
  class ConfigurationError extends FunnefoxSDKError {
107
- /**
108
- * @param {string} message - Configuration error message
109
- */
110
- constructor(message) {
111
- super(message, 'CONFIGURATION_ERROR');
112
- this.name = 'ConfigurationError';
113
- }
137
+ constructor(message) {
138
+ super(message, ERROR_CODES.CONFIGURATION_ERROR);
139
+ this.name = 'ConfigurationError';
140
+ }
114
141
  }
115
-
116
- /**
117
- * Error thrown when network requests fail
118
- */
119
142
  class NetworkError extends FunnefoxSDKError {
120
- /**
121
- * @param {string} message - Network error message
122
- * @param {*} [originalError] - Original network error
123
- */
124
- constructor(message, originalError = null) {
125
- super(message, 'NETWORK_ERROR');
126
- this.name = 'NetworkError';
127
- this.originalError = originalError;
128
- }
129
- }
130
-
131
- /**
132
- * @fileoverview Constants for Funnefox SDK
133
- */
134
-
135
- /**
136
- * SDK version
137
- */
138
- const SDK_VERSION = '0.1.1';
139
-
140
- /**
141
- * Default configuration values
142
- */
143
- const DEFAULTS = {
144
- BASE_URL: 'https://billing.funnelfox.com',
145
- REGION: 'default',
146
- SANDBOX: false,
147
- REQUEST_TIMEOUT: 30000,
148
- RETRY_ATTEMPTS: 3,
149
- RETRY_BASE_DELAY: 1000
150
- };
151
-
152
- /**
153
- * Checkout states
154
- */
155
- const CHECKOUT_STATES = {
156
- INITIALIZING: 'initializing',
157
- READY: 'ready',
158
- PROCESSING: 'processing',
159
- ACTION_REQUIRED: 'action_required',
160
- UPDATING: 'updating',
161
- COMPLETED: 'completed',
162
- ERROR: 'error',
163
- DESTROYED: 'destroyed'
164
- };
165
-
166
- /**
167
- * Event names
168
- */
169
- const EVENTS = {
170
- SUCCESS: 'success',
171
- ERROR: 'error',
172
- STATUS_CHANGE: 'status-change',
173
- DESTROY: 'destroy'
174
- };
175
-
176
- /**
177
- * Error codes
178
- */
179
- const ERROR_CODES = {
180
- SDK_ERROR: 'SDK_ERROR',
181
- VALIDATION_ERROR: 'VALIDATION_ERROR',
182
- API_ERROR: 'API_ERROR',
183
- PRIMER_ERROR: 'PRIMER_ERROR',
184
- CHECKOUT_ERROR: 'CHECKOUT_ERROR',
185
- CONFIGURATION_ERROR: 'CONFIGURATION_ERROR',
186
- NETWORK_ERROR: 'NETWORK_ERROR'
187
- };
188
-
189
- /**
190
- * @fileoverview Lightweight event emitter for Funnefox SDK
191
- */
192
-
193
- /**
194
- * Simple event emitter for checkout instances
195
- */
196
- class EventEmitter {
197
- constructor() {
198
- this._events = new Map();
199
- }
200
-
201
- /**
202
- * Add an event listener
203
- * @param {string} eventName - Name of the event
204
- * @param {function} handler - Event handler function
205
- * @returns {EventEmitter} Returns this for chaining
206
- */
207
- on(eventName, handler) {
208
- if (typeof handler !== 'function') {
209
- throw new Error('Event handler must be a function');
210
- }
211
- if (!this._events.has(eventName)) {
212
- this._events.set(eventName, []);
213
- }
214
- this._events.get(eventName).push(handler);
215
- return this;
216
- }
217
-
218
- /**
219
- * Add a one-time event listener
220
- * @param {string} eventName - Name of the event
221
- * @param {function} handler - Event handler function
222
- * @returns {EventEmitter} Returns this for chaining
223
- */
224
- once(eventName, handler) {
225
- if (typeof handler !== 'function') {
226
- throw new Error('Event handler must be a function');
227
- }
228
- const onceWrapper = (...args) => {
229
- this.off(eventName, onceWrapper);
230
- handler.apply(this, args);
231
- };
232
- return this.on(eventName, onceWrapper);
233
- }
234
-
235
- /**
236
- * Remove an event listener
237
- * @param {string} eventName - Name of the event
238
- * @param {function} [handler] - Specific handler to remove. If not provided, removes all handlers for the event
239
- * @returns {EventEmitter} Returns this for chaining
240
- */
241
- off(eventName, handler = null) {
242
- if (!this._events.has(eventName)) {
243
- return this;
244
- }
245
- if (handler === null) {
246
- this._events.delete(eventName);
247
- return this;
248
- }
249
- const handlers = this._events.get(eventName);
250
- const index = handlers.indexOf(handler);
251
- if (index !== -1) {
252
- handlers.splice(index, 1);
253
- if (handlers.length === 0) {
254
- this._events.delete(eventName);
255
- }
143
+ constructor(message, originalError = null) {
144
+ super(message, ERROR_CODES.NETWORK_ERROR);
145
+ this.name = 'NetworkError';
146
+ this.originalError = originalError;
256
147
  }
257
- return this;
258
- }
259
-
260
- /**
261
- * Emit an event to all registered handlers
262
- * @param {string} eventName - Name of the event to emit
263
- * @param {...*} args - Arguments to pass to event handlers
264
- * @returns {boolean} Returns true if event had listeners, false otherwise
265
- */
266
- emit(eventName, ...args) {
267
- if (!this._events.has(eventName)) {
268
- return false;
269
- }
270
- const handlers = this._events.get(eventName).slice();
271
- for (const handler of handlers) {
272
- try {
273
- handler.apply(this, args);
274
- } catch (error) {
275
- // Don't let handler errors break the emission chain
276
- // eslint-disable-next-line no-console
277
- console.warn(`Error in event handler for "${eventName}":`, error);
278
- }
279
- }
280
- return true;
281
- }
282
-
283
- /**
284
- * Get the number of listeners for an event
285
- * @param {string} eventName - Name of the event
286
- * @returns {number} Number of listeners
287
- */
288
- listenerCount(eventName) {
289
- return this._events.has(eventName) ? this._events.get(eventName).length : 0;
290
- }
291
-
292
- /**
293
- * Get all event names that have listeners
294
- * @returns {string[]} Array of event names
295
- */
296
- eventNames() {
297
- return Array.from(this._events.keys());
298
- }
299
-
300
- /**
301
- * Remove all listeners for all events
302
- * @returns {EventEmitter} Returns this for chaining
303
- */
304
- removeAllListeners() {
305
- this._events.clear();
306
- return this;
307
- }
308
-
309
- /**
310
- * Get all listeners for an event
311
- * @param {string} eventName - Name of the event
312
- * @returns {function[]} Array of handler functions
313
- */
314
- listeners(eventName) {
315
- return this._events.has(eventName) ? this._events.get(eventName).slice() : [];
316
- }
317
148
  }
318
149
 
150
+ /* eslint-disable @typescript-eslint/no-explicit-any */
319
151
  /**
320
152
  * @fileoverview Helper utilities for Funnefox SDK
321
153
  */
322
-
323
-
324
- /**
325
- * Merges multiple objects into a new object
326
- * @param {...Object} objects - Objects to merge
327
- * @returns {Object} Merged object
328
- */
329
154
  function merge(...objects) {
330
- const result = {};
331
- for (const obj of objects) {
332
- if (obj && typeof obj === 'object') {
333
- for (const key in obj) {
334
- if (Object.prototype.hasOwnProperty.call(obj, key)) {
335
- if (typeof obj[key] === 'object' && !Array.isArray(obj[key]) && obj[key] !== null) {
336
- result[key] = merge(result[key] || {}, obj[key]);
337
- } else {
338
- result[key] = obj[key];
339
- }
155
+ const result = {};
156
+ for (const obj of objects) {
157
+ if (obj && typeof obj === 'object') {
158
+ for (const key in obj) {
159
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
160
+ if (typeof obj[key] === 'object' &&
161
+ !Array.isArray(obj[key]) &&
162
+ obj[key] !== null) {
163
+ result[key] = merge(result[key] || {}, obj[key]);
164
+ }
165
+ else {
166
+ result[key] = obj[key];
167
+ }
168
+ }
169
+ }
340
170
  }
341
- }
342
171
  }
343
- }
344
- return result;
172
+ return result;
345
173
  }
346
-
347
- /**
348
- * Generates a unique identifier
349
- * @param {string} [prefix=''] - Optional prefix for the ID
350
- * @returns {string} Unique identifier
351
- */
352
174
  function generateId(prefix = '') {
353
- const timestamp = Date.now().toString(36);
354
- const random = Math.random().toString(36).substr(2, 5);
355
- return `${prefix}${timestamp}_${random}`;
175
+ const timestamp = Date.now().toString(36);
176
+ const random = Math.random().toString(36).substr(2, 5);
177
+ return `${prefix}${timestamp}_${random}`;
356
178
  }
357
-
358
- /**
359
- * Waits for a specified amount of time
360
- * @param {number} ms - Milliseconds to wait
361
- * @returns {Promise} Promise that resolves after the specified time
362
- */
363
179
  function sleep(ms) {
364
- return new Promise(resolve => setTimeout(resolve, ms));
180
+ return new Promise(resolve => setTimeout(resolve, ms));
365
181
  }
366
-
367
- /**
368
- * Retries a function with exponential backoff
369
- * @param {function} fn - Function to retry
370
- * @param {number} [maxAttempts=3] - Maximum number of attempts
371
- * @param {number} [baseDelay=1000] - Base delay in milliseconds
372
- * @returns {Promise} Promise that resolves with the function result
373
- */
374
182
  async function retry(fn, maxAttempts = 3, baseDelay = 1000) {
375
- let lastError;
376
- for (let attempt = 1; attempt <= maxAttempts; attempt++) {
377
- try {
378
- return await fn();
379
- } catch (error) {
380
- lastError = error;
381
- if (attempt === maxAttempts) {
382
- throw lastError;
383
- }
384
- const delay = baseDelay * Math.pow(2, attempt - 1);
385
- await sleep(delay);
183
+ let lastError;
184
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
185
+ try {
186
+ return await fn();
187
+ }
188
+ catch (error) {
189
+ lastError = error;
190
+ if (attempt === maxAttempts)
191
+ throw lastError;
192
+ const delay = baseDelay * Math.pow(2, attempt - 1);
193
+ await sleep(delay);
194
+ }
386
195
  }
387
- }
388
- throw lastError;
196
+ throw lastError;
389
197
  }
390
-
391
- /**
392
- * Creates a promise that rejects after a timeout
393
- * @param {Promise} promise - Promise to wrap
394
- * @param {number} timeoutMs - Timeout in milliseconds
395
- * @param {string} [message='Operation timed out'] - Timeout error message
396
- * @returns {Promise} Promise that rejects if timeout is reached
397
- */
398
198
  function withTimeout(promise, timeoutMs, message = 'Operation timed out') {
399
- const timeoutPromise = new Promise((_, reject) => {
400
- setTimeout(() => reject(new Error(message)), timeoutMs);
401
- });
402
- return Promise.race([promise, timeoutPromise]);
199
+ const timeoutPromise = new Promise((_, reject) => {
200
+ setTimeout(() => reject(new Error(message)), timeoutMs);
201
+ });
202
+ return Promise.race([promise, timeoutPromise]);
403
203
  }
404
204
 
205
+ var PaymentMethod;
206
+ (function (PaymentMethod) {
207
+ PaymentMethod["GOOGLE_PAY"] = "GOOGLE_PAY";
208
+ PaymentMethod["APPLE_PAY"] = "APPLE_PAY";
209
+ PaymentMethod["PAYPAL"] = "PAYPAL";
210
+ PaymentMethod["PAYMENT_CARD"] = "PAYMENT_CARD";
211
+ })(PaymentMethod || (PaymentMethod = {}));
212
+
405
213
  /**
406
- * @fileoverview Primer SDK integration wrapper
407
- *
408
- * @typedef {import('@primer-io/checkout-web').IPrimerHeadlessUniversalCheckout} PrimerCheckout
409
- * @typedef {import('@primer-io/checkout-web').ITokenizationHandler} PrimerTokenizationHandler
410
- * @typedef {import('@primer-io/checkout-web').IResumeHandler} PrimerResumeHandler
411
- * @typedef {import('@primer-io/checkout-web').PaymentMethodTokenData} PaymentMethodTokenData
412
- * @typedef {import('@primer-io/checkout-web').ResumeTokenData} ResumeTokenData
413
- * @typedef {import('@primer-io/checkout-web').UniversalCheckoutOptions} UniversalCheckoutOptions
214
+ * @fileoverview Constants for Funnefox SDK
414
215
  */
415
-
216
+ const SDK_VERSION = '0.3.0';
217
+ const DEFAULTS = {
218
+ BASE_URL: 'https://billing.funnelfox.com',
219
+ REGION: 'default',
220
+ SANDBOX: false,
221
+ REQUEST_TIMEOUT: 30000,
222
+ RETRY_ATTEMPTS: 3,
223
+ RETRY_BASE_DELAY: 1000,
224
+ };
225
+ const CHECKOUT_STATES = {
226
+ INITIALIZING: 'initializing',
227
+ READY: 'ready',
228
+ PROCESSING: 'processing',
229
+ ACTION_REQUIRED: 'action_required',
230
+ UPDATING: 'updating',
231
+ COMPLETED: 'completed',
232
+ ERROR: 'error',
233
+ DESTROYED: 'destroyed',
234
+ };
235
+ const EVENTS = {
236
+ SUCCESS: 'success',
237
+ ERROR: 'error',
238
+ STATUS_CHANGE: 'status-change',
239
+ DESTROY: 'destroy',
240
+ INPUT_ERROR: 'input-error',
241
+ LOADER_CHANGE: 'loader-change',
242
+ METHOD_RENDER: 'method-render',
243
+ START_PURCHASE: 'start-purchase',
244
+ PURCHASE_FAILURE: 'purchase-failure',
245
+ PURCHASE_COMPLETED: 'purchase-completed',
246
+ };
247
+ const API_ENDPOINTS = {
248
+ CREATE_CLIENT_SESSION: '/v1/checkout/create_client_session',
249
+ UPDATE_CLIENT_SESSION: '/v1/checkout/update_client_session',
250
+ CREATE_PAYMENT: '/v1/checkout/create_payment',
251
+ RESUME_PAYMENT: '/v1/checkout/resume_payment',
252
+ };
253
+ const ERROR_CODES = {
254
+ SDK_ERROR: 'SDK_ERROR',
255
+ VALIDATION_ERROR: 'VALIDATION_ERROR',
256
+ API_ERROR: 'API_ERROR',
257
+ PRIMER_ERROR: 'PRIMER_ERROR',
258
+ CHECKOUT_ERROR: 'CHECKOUT_ERROR',
259
+ CONFIGURATION_ERROR: 'CONFIGURATION_ERROR',
260
+ NETWORK_ERROR: 'NETWORK_ERROR',
261
+ };
262
+ const ALLOWED_BUTTON_PAYMENT_METHODS = [
263
+ PaymentMethod.GOOGLE_PAY,
264
+ PaymentMethod.APPLE_PAY,
265
+ PaymentMethod.PAYPAL,
266
+ ];
267
+ const ALLOWED_CARD_PAYMENT_METHODS = [
268
+ PaymentMethod.PAYMENT_CARD,
269
+ ];
270
+ const ALLOWED_PAYMENT_METHODS = [
271
+ ...ALLOWED_BUTTON_PAYMENT_METHODS,
272
+ ...ALLOWED_CARD_PAYMENT_METHODS,
273
+ ];
274
+ const inputStyle = {
275
+ input: {
276
+ error: {
277
+ borderColor: 'rgb(227, 47, 65)',
278
+ },
279
+ base: {
280
+ borderWidth: '1px',
281
+ borderStyle: 'solid',
282
+ borderColor: 'rgb(0 0 0 / 10%)',
283
+ height: '36px',
284
+ paddingHorizontal: 10,
285
+ borderRadius: '6px',
286
+ },
287
+ },
288
+ };
289
+ ({
290
+ paddingLeft: inputStyle.input.base.paddingHorizontal + 'px',
291
+ paddingRight: inputStyle.input.base.paddingHorizontal + 'px'});
416
292
 
417
293
  /**
418
- * Wrapper class for Primer SDK integration
294
+ * @fileoverview Primer SDK integration wrapper
419
295
  */
420
296
  class PrimerWrapper {
421
- constructor() {
422
- this.currentCheckout = null;
423
- this.isInitialized = false;
424
- }
425
-
426
- /**
427
- * Checks if Primer SDK is available
428
- * @returns {boolean} True if Primer is available
429
- */
430
- isPrimerAvailable() {
431
- return typeof window !== 'undefined' && window.Primer && typeof window.Primer.showUniversalCheckout === 'function';
432
- }
433
-
434
- /**
435
- * Ensures Primer SDK is available
436
- * @throws {PrimerError} If Primer SDK is not available
437
- */
438
- ensurePrimerAvailable() {
439
- if (!this.isPrimerAvailable()) {
440
- throw new PrimerError('Primer SDK not found. Please include the Primer SDK script before initializing FunnefoxSDK.');
297
+ constructor() {
298
+ this.isInitialized = false;
299
+ this.destroyCallbacks = [];
300
+ this.headless = null;
301
+ this.availableMethods = [];
441
302
  }
442
- }
443
-
444
- /**
445
- * Creates a universal checkout instance
446
- * @param {string} clientToken - Client token from your backend
447
- * @param {Partial<UniversalCheckoutOptions> & {onTokenizeSuccess: function, onResumeSuccess: function}} options - Checkout options
448
- * @returns {Promise<PrimerCheckout>} Primer checkout instance
449
- */
450
- async showUniversalCheckout(clientToken, options) {
451
- this.ensurePrimerAvailable();
452
-
453
- // Extract SDK-managed handlers
454
- const {
455
- onTokenizeSuccess,
456
- onResumeSuccess,
457
- container,
458
- ...userPrimerOptions
459
- } = options;
460
-
461
- // Merge defaults with user's Primer options
462
- /** @type {UniversalCheckoutOptions} */
463
- const primerOptions = merge({
464
- clientToken,
465
- container,
466
- paymentHandling: 'MANUAL',
467
- apiVersion: '2.4',
468
- paypal: {
469
- buttonColor: 'blue',
470
- paymentFlow: 'PREFER_VAULT'
471
- }
472
- }, userPrimerOptions);
473
-
474
- // Add the required event handlers (must override any user-provided ones)
475
- primerOptions.onTokenizeSuccess = this._wrapTokenizeHandler(onTokenizeSuccess);
476
- primerOptions.onResumeSuccess = this._wrapResumeHandler(onResumeSuccess);
477
- try {
478
- const checkout = await window.Primer.showUniversalCheckout(clientToken, primerOptions);
479
- this.currentCheckout = checkout;
480
- this.isInitialized = true;
481
- return checkout;
482
- } catch (error) {
483
- throw new PrimerError('Failed to initialize Primer checkout', error);
303
+ isPrimerAvailable() {
304
+ return (typeof window !== 'undefined' &&
305
+ window.Primer &&
306
+ typeof window.Primer?.createHeadless === 'function');
484
307
  }
485
- }
486
-
487
- /**
488
- * Wraps the tokenize success handler with error handling
489
- * @private
490
- * @param {function(PaymentMethodTokenData, PrimerTokenizationHandler): Promise<void>} handler
491
- * @returns {function(PaymentMethodTokenData, PrimerTokenizationHandler): Promise<void>}
492
- */
493
- _wrapTokenizeHandler(handler) {
494
- return async (paymentMethodTokenData, primerHandler) => {
495
- try {
496
- await handler(paymentMethodTokenData, primerHandler);
497
- } catch (error) {
498
- console.error('Error in tokenize handler:', error);
499
- primerHandler.handleFailure('Payment processing failed. Please try again.');
500
- }
501
- };
502
- }
503
-
504
- /**
505
- * Wraps the resume success handler with error handling
506
- * @private
507
- * @param {function(ResumeTokenData, PrimerResumeHandler): Promise<void>} handler
508
- * @returns {function(ResumeTokenData, PrimerResumeHandler): Promise<void>}
509
- */
510
- _wrapResumeHandler(handler) {
511
- return async (resumeTokenData, primerHandler) => {
512
- try {
513
- await handler(resumeTokenData, primerHandler);
514
- } catch (error) {
515
- console.error('Error in resume handler:', error);
516
- primerHandler.handleFailure('Payment processing failed. Please try again.');
517
- }
518
- };
519
- }
520
-
521
- /**
522
- * Updates the client token for an existing checkout
523
- * @param {string} newClientToken - New client token
524
- * @returns {Promise<void>}
525
- */
526
- async updateClientToken(newClientToken) {
527
- if (!this.currentCheckout) {
528
- throw new PrimerError('No active checkout to update');
308
+ ensurePrimerAvailable() {
309
+ if (!this.isPrimerAvailable()) {
310
+ throw new PrimerError('Primer SDK not found. Please include the Primer SDK script before initializing FunnefoxSDK.');
311
+ }
529
312
  }
530
- try {
531
- // Primer SDK doesn't have a direct update method, so we need to destroy and recreate
532
- // This is handled at the checkout level
533
- throw new PrimerError('Client token updates require checkout recreation');
534
- } catch (error) {
535
- throw new PrimerError('Failed to update client token', error);
313
+ async createHeadlessCheckout(clientToken, options) {
314
+ if (this.headless) {
315
+ return this.headless;
316
+ }
317
+ this.ensurePrimerAvailable();
318
+ const primerOptions = merge({
319
+ paymentHandling: 'MANUAL',
320
+ apiVersion: '2.4',
321
+ paypal: {
322
+ buttonColor: 'blue',
323
+ paymentFlow: 'PREFER_VAULT',
324
+ },
325
+ }, options);
326
+ try {
327
+ const headless = await window.Primer.createHeadless(clientToken, primerOptions);
328
+ await headless.start();
329
+ this.headless = headless;
330
+ }
331
+ catch (error) {
332
+ throw new PrimerError('Failed to create Primer headless checkout', error);
333
+ }
536
334
  }
537
- }
538
-
539
- /**
540
- * Destroys the current checkout instance
541
- * @returns {Promise<void>}
542
- */
543
- async destroy() {
544
- if (this.currentCheckout) {
545
- try {
546
- // Primer checkout cleanup - this depends on Primer SDK version
547
- if (typeof this.currentCheckout.destroy === 'function') {
548
- await this.currentCheckout.destroy();
549
- } else {
550
- // Fallback - clear the container
551
- const container = document.querySelector(this.currentCheckout.container);
552
- if (container) {
553
- container.innerHTML = '';
554
- }
335
+ initializeCardElements(selectors) {
336
+ const { cardNumber, expiryDate, cvv, cardholderName, button } = selectors;
337
+ return {
338
+ cardNumber: document.querySelector(cardNumber),
339
+ expiryDate: document.querySelector(expiryDate),
340
+ cvv: document.querySelector(cvv),
341
+ cardholderName: document.querySelector(cardholderName),
342
+ button: document.querySelector(button),
343
+ };
344
+ }
345
+ disableButtons(disabled) {
346
+ if (!this.paymentMethodsInterfaces)
347
+ return;
348
+ for (const method in this.paymentMethodsInterfaces) {
349
+ this.paymentMethodsInterfaces[method].setDisabled(disabled);
555
350
  }
556
- } catch (error) {
557
- console.warn('Error destroying Primer checkout:', error);
558
- } finally {
559
- this.currentCheckout = null;
560
- this.isInitialized = false;
561
- }
562
351
  }
563
- }
564
-
565
- /**
566
- * Creates standardized Primer handler helpers
567
- * @param {Object} handlers - Handler functions
568
- * @param {function} handlers.onSuccess - Success callback
569
- * @param {function} handlers.onError - Error callback
570
- * @param {function} handlers.onActionRequired - Action required callback
571
- * @returns {Object} Primer-compatible handler functions
572
- */
573
- createHandlers(handlers) {
574
- return {
575
- /**
576
- * Handle successful tokenization
577
- */
578
- handleSuccess: () => {
579
- if (handlers.onSuccess) {
580
- handlers.onSuccess();
352
+ async renderCardCheckout({ onSubmitError, onSubmit, cardSelectors, onInputChange, }) {
353
+ try {
354
+ const elements = this.initializeCardElements(cardSelectors);
355
+ const pmManager = await this.headless.createPaymentMethodManager('PAYMENT_CARD');
356
+ if (!pmManager) {
357
+ throw new Error('Payment method manager is not available');
358
+ }
359
+ const { cardNumberInput, expiryInput, cvvInput } = pmManager.createHostedInputs();
360
+ const validateForm = async () => {
361
+ if (!pmManager)
362
+ return false;
363
+ const { valid, validationErrors } = await pmManager.validate();
364
+ const cardHolderError = validationErrors.find(v => v.name === 'cardholderName');
365
+ dispatchError('cardholderName', cardHolderError?.message || null);
366
+ return valid;
367
+ };
368
+ const dispatchError = (inputName, error) => {
369
+ onInputChange(inputName, error);
370
+ };
371
+ const onHostedInputChange = (name) => (event) => {
372
+ const input = event;
373
+ if (input.submitted) {
374
+ dispatchError(name, input.error);
375
+ }
376
+ };
377
+ const cardHolderOnChange = async (e) => {
378
+ pmManager.setCardholderName(e.target.value);
379
+ dispatchError('cardholderName', null);
380
+ };
381
+ elements.cardholderName?.addEventListener('input', cardHolderOnChange);
382
+ cardNumberInput.addEventListener('change', onHostedInputChange('cardNumber'));
383
+ expiryInput.addEventListener('change', onHostedInputChange('expiryDate'));
384
+ cvvInput.addEventListener('change', onHostedInputChange('cvv'));
385
+ const onSubmitHandler = async () => {
386
+ if (!(await validateForm())) {
387
+ return;
388
+ }
389
+ try {
390
+ onSubmit(true);
391
+ await pmManager.submit();
392
+ }
393
+ catch (error) {
394
+ const primerError = new PrimerError('Failed to submit payment', error);
395
+ onSubmitError(primerError);
396
+ throw primerError;
397
+ }
398
+ finally {
399
+ onSubmit(false);
400
+ }
401
+ };
402
+ elements.button.addEventListener('click', onSubmitHandler);
403
+ await Promise.all([
404
+ cardNumberInput.render(elements.cardNumber, {
405
+ placeholder: '1234 1234 1234 1234',
406
+ ariaLabel: 'Card number',
407
+ style: inputStyle,
408
+ }),
409
+ expiryInput.render(elements.expiryDate, {
410
+ placeholder: 'MM/YY',
411
+ ariaLabel: 'Expiry date',
412
+ style: inputStyle,
413
+ }),
414
+ cvvInput.render(elements.cvv, {
415
+ placeholder: '123',
416
+ ariaLabel: 'CVV',
417
+ style: inputStyle,
418
+ }),
419
+ ]);
420
+ this.destroyCallbacks.push(() => {
421
+ pmManager.removeHostedInputs();
422
+ elements.cardholderName.removeEventListener('change', cardHolderOnChange);
423
+ });
424
+ return {
425
+ setDisabled: (disabled) => {
426
+ cardNumberInput.setDisabled(disabled);
427
+ expiryInput.setDisabled(disabled);
428
+ cvvInput.setDisabled(disabled);
429
+ elements.button.disabled = disabled;
430
+ },
431
+ };
581
432
  }
582
- },
583
- /**
584
- * Handle payment failure
585
- */
586
- handleFailure: message => {
587
- if (handlers.onError) {
588
- handlers.onError(new Error(message));
433
+ catch (error) {
434
+ throw new PrimerError('Failed to initialize Primer checkout', error);
589
435
  }
590
- },
591
- /**
592
- * Continue with new client token (for 3DS flows)
593
- */
594
- continueWithNewClientToken: newClientToken => {
595
- if (handlers.onActionRequired) {
596
- handlers.onActionRequired(newClientToken);
436
+ }
437
+ async renderButton(allowedPaymentMethod, { container, }) {
438
+ const containerEl = this.validateContainer(container);
439
+ let button;
440
+ this.ensurePrimerAvailable();
441
+ if (!this.headless) {
442
+ throw new PrimerError('Headless checkout not found');
443
+ }
444
+ try {
445
+ const pmManager = await this.headless.createPaymentMethodManager(allowedPaymentMethod);
446
+ if (!pmManager) {
447
+ throw new Error('Payment method manager is not available');
448
+ }
449
+ button = pmManager.createButton();
450
+ await button.render(containerEl, {});
451
+ this.destroyCallbacks.push(() => button.clean());
452
+ }
453
+ catch (error) {
454
+ throw new PrimerError('Failed to initialize Primer checkout', error);
597
455
  }
598
- }
599
- };
600
- }
601
-
602
- /**
603
- * Gets the current checkout state
604
- * @returns {PrimerCheckout|null} Current checkout instance or null
605
- */
606
- getCurrentCheckout() {
607
- return this.currentCheckout;
608
- }
609
-
610
- /**
611
- * Checks if checkout is currently active
612
- * @returns {boolean} True if checkout is active
613
- */
614
- isActive() {
615
- return this.isInitialized && this.currentCheckout !== null;
616
- }
617
-
618
- /**
619
- * Validates that a container element exists and is suitable for Primer
620
- * @param {string} selector - DOM selector
621
- * @returns {Element} The container element
622
- * @throws {PrimerError} If container is invalid
623
- */
624
- validateContainer(selector) {
625
- const element = document.querySelector(selector);
626
- if (!element) {
627
- throw new PrimerError(`Checkout container not found: ${selector}`);
628
456
  }
629
-
630
- // Check if container is suitable (visible, not already in use, etc.)
631
- const computedStyle = window.getComputedStyle(element);
632
- if (computedStyle.display === 'none') {
633
- console.warn('Checkout container is hidden, this may cause display issues');
457
+ async renderCheckout(clientToken, options) {
458
+ const { cardSelectors, paymentButtonSelectors, container, onTokenizeSuccess, onResumeSuccess, onSubmit, onInputChange, onMethodRender, onSubmitError, ...restPrimerOptions } = options;
459
+ await this.createHeadlessCheckout(clientToken, {
460
+ ...restPrimerOptions,
461
+ onTokenizeSuccess: this.wrapTokenizeHandler(onTokenizeSuccess),
462
+ onResumeSuccess: this.wrapResumeHandler(onResumeSuccess),
463
+ onAvailablePaymentMethodsLoad: (items) => {
464
+ this.availableMethods = ALLOWED_PAYMENT_METHODS.filter(method => items.some((item) => item.type === method));
465
+ if (this.availableMethods.length === 0) {
466
+ throw new PrimerError('No allowed payment methods found');
467
+ }
468
+ },
469
+ onCheckoutFail: error => {
470
+ // eslint-disable-next-line no-console
471
+ console.error(error);
472
+ },
473
+ onTokenizeError: error => {
474
+ // eslint-disable-next-line no-console
475
+ console.error(error);
476
+ },
477
+ });
478
+ const methodOptions = {
479
+ cardSelectors,
480
+ container,
481
+ onSubmit,
482
+ onInputChange,
483
+ onSubmitError,
484
+ };
485
+ this.availableMethods.forEach(async (method) => {
486
+ if (method === PaymentMethod.PAYMENT_CARD) {
487
+ await this.renderCardCheckout(methodOptions);
488
+ onMethodRender(PaymentMethod.PAYMENT_CARD);
489
+ }
490
+ else {
491
+ const container = method === PaymentMethod.PAYPAL
492
+ ? paymentButtonSelectors.paypal
493
+ : method === PaymentMethod.GOOGLE_PAY
494
+ ? paymentButtonSelectors.googlePay
495
+ : paymentButtonSelectors.applePay;
496
+ await this.renderButton(method, { container });
497
+ onMethodRender(method);
498
+ }
499
+ });
500
+ this.isInitialized = true;
501
+ }
502
+ wrapTokenizeHandler(handler) {
503
+ return async (paymentMethodTokenData, primerHandler) => {
504
+ try {
505
+ await handler(paymentMethodTokenData, primerHandler);
506
+ }
507
+ catch (error) {
508
+ // eslint-disable-next-line no-console
509
+ console.error('Error in tokenize handler:', error);
510
+ primerHandler.handleFailure('Payment processing failed. Please try again.');
511
+ }
512
+ };
513
+ }
514
+ wrapResumeHandler(handler) {
515
+ return async (resumeTokenData, primerHandler) => {
516
+ try {
517
+ await handler(resumeTokenData, primerHandler);
518
+ }
519
+ catch (error) {
520
+ // eslint-disable-next-line no-console
521
+ console.error('Error in resume handler:', error);
522
+ primerHandler.handleFailure('Payment processing failed. Please try again.');
523
+ }
524
+ };
525
+ }
526
+ async destroy() {
527
+ if (this.destroyCallbacks) {
528
+ try {
529
+ Promise.all(this.destroyCallbacks.map(destroy => destroy()));
530
+ }
531
+ catch (error) {
532
+ // eslint-disable-next-line no-console
533
+ console.warn('Error destroying Primer checkout:', error);
534
+ }
535
+ }
536
+ this.destroyCallbacks = [];
537
+ this.isInitialized = false;
538
+ }
539
+ createHandlers(handlers) {
540
+ return {
541
+ handleSuccess: () => {
542
+ if (handlers.onSuccess)
543
+ handlers.onSuccess();
544
+ },
545
+ handleFailure: (message) => {
546
+ if (handlers.onError)
547
+ handlers.onError(new Error(message));
548
+ },
549
+ continueWithNewClientToken: (newClientToken) => {
550
+ if (handlers.onActionRequired)
551
+ handlers.onActionRequired(newClientToken);
552
+ },
553
+ };
554
+ }
555
+ getCurrentCheckout() {
556
+ return this.destroyCallbacks;
557
+ }
558
+ isActive() {
559
+ return this.isInitialized && this.destroyCallbacks.length > 0;
560
+ }
561
+ validateContainer(selector) {
562
+ const element = document.querySelector(selector);
563
+ if (!element) {
564
+ throw new PrimerError(`Checkout container not found: ${selector}`);
565
+ }
566
+ const computedStyle = window.getComputedStyle(element);
567
+ if (computedStyle.display === 'none') {
568
+ // eslint-disable-next-line no-console
569
+ console.warn('Checkout container is hidden, this may cause display issues');
570
+ }
571
+ return element;
634
572
  }
635
- return element;
636
- }
637
573
  }
638
574
 
639
- var primerWrapper = /*#__PURE__*/Object.freeze({
640
- __proto__: null,
641
- default: PrimerWrapper
642
- });
643
-
644
575
  /**
645
576
  * @fileoverview Input validation utilities for Funnefox SDK
646
577
  */
647
-
648
-
649
- /**
650
- * Sanitizes a string input
651
- * @param {*} input - Input to sanitize
652
- * @returns {string} Sanitized string
653
- */
654
578
  function sanitizeString(input) {
655
- if (input === null || input === undefined) {
656
- return '';
657
- }
658
- return String(input).trim();
579
+ return input?.trim() || '';
659
580
  }
660
-
661
- /**
662
- * Validates that a value is a non-empty string
663
- * @param {*} value - Value to validate
664
- * @param {string} fieldName - Name of the field for error reporting
665
- * @returns {boolean} True if valid
666
- * @throws {ValidationError} If validation fails
667
- */
668
581
  function requireString(value, fieldName) {
669
- const sanitized = sanitizeString(value);
670
- if (sanitized.length === 0) {
671
- throw new ValidationError(fieldName, 'must be a non-empty string', value);
672
- }
673
- return true;
674
- }
675
-
676
- /**
677
- * Validates that a container element exists in the DOM
678
- * @param {string} selector - DOM selector
679
- * @returns {Element} The found element
680
- * @throws {ValidationError} If element not found
681
- */
682
- function validateContainer(selector) {
683
- requireString(selector, 'container');
684
- const element = document.querySelector(selector);
685
- if (!element) {
686
- throw new ValidationError('container', `element not found: ${selector}`, selector);
687
- }
688
- return element;
582
+ const sanitized = sanitizeString(value);
583
+ if (sanitized.length === 0) {
584
+ throw new ValidationError(fieldName, 'must be a non-empty string', value);
585
+ }
586
+ return true;
689
587
  }
690
588
 
691
589
  /**
692
590
  * @fileoverview API client for Funnefox backend integration
693
591
  */
694
-
695
-
696
- /**
697
- * HTTP client for Funnefox API requests
698
- */
699
592
  class APIClient {
700
- /**
701
- * @param {Object} config - API client configuration
702
- * @param {string} config.baseUrl - Base URL for API calls
703
- * @param {string} config.orgId - Organization identifier
704
- * @param {number} [config.timeout=30000] - Request timeout in milliseconds
705
- * @param {number} [config.retryAttempts=3] - Number of retry attempts
706
- */
707
- constructor(config) {
708
- this.baseUrl = config.baseUrl.replace(/\/$/, '');
709
- this.orgId = config.orgId;
710
- this.timeout = config.timeout || 30000;
711
- this.retryAttempts = config.retryAttempts || 3;
712
- }
713
-
714
- /**
715
- * Makes an HTTP request with error handling and retries
716
- * @param {string} endpoint - API endpoint
717
- * @param {Object} [options={}] - Request options
718
- * @returns {Promise<Object>} API response data
719
- * @throws {APIError|NetworkError} If request fails
720
- */
721
- async request(endpoint, options = {}) {
722
- const url = `${this.baseUrl}/${this.orgId}${endpoint}`;
723
- const requestOptions = {
724
- method: 'GET',
725
- headers: {
726
- 'Content-Type': 'application/json',
727
- ...options.headers
728
- },
729
- ...options
730
- };
731
- try {
732
- return await retry(async () => {
733
- return await withTimeout(this._makeRequest(url, requestOptions), this.timeout, 'Request timed out');
734
- }, this.retryAttempts);
735
- } catch (error) {
736
- if (error.name === 'APIError') {
737
- throw error;
738
- }
739
- throw new NetworkError('Network request failed', error);
740
- }
741
- }
742
-
743
- /**
744
- * Internal method to make the actual HTTP request
745
- * @private
746
- */
747
- async _makeRequest(url, options) {
748
- let response;
749
- try {
750
- response = await fetch(url, options);
751
- } catch (error) {
752
- throw new NetworkError('Network request failed', error);
593
+ constructor(config) {
594
+ this.baseUrl = config.baseUrl.replace(/\/$/, '');
595
+ this.orgId = config.orgId;
596
+ this.timeout = config.timeout || 30000;
597
+ this.retryAttempts = config.retryAttempts || 3;
753
598
  }
754
- let data;
755
- try {
756
- data = await response.json();
757
- } catch (error) {
758
- throw new APIError('Invalid JSON response', response.status, {});
599
+ async request(endpoint, options = {}) {
600
+ const url = `${this.baseUrl}/${this.orgId}${endpoint}`;
601
+ const requestOptions = {
602
+ method: 'GET',
603
+ headers: {
604
+ 'Content-Type': 'application/json',
605
+ ...(options.headers || {}),
606
+ },
607
+ ...options,
608
+ };
609
+ try {
610
+ return await retry(async () => {
611
+ return await withTimeout(this._makeRequest(url, requestOptions), this.timeout, 'Request timed out');
612
+ }, this.retryAttempts);
613
+ }
614
+ catch (error) {
615
+ if (error instanceof Error && error.name === 'APIError') {
616
+ throw error;
617
+ }
618
+ throw new NetworkError('Network request failed', error);
619
+ }
759
620
  }
760
- if (!response.ok) {
761
- const message = data.message || data.error || `HTTP ${response.status}`;
762
- throw new APIError(message, response.status, {
763
- response: data
764
- });
621
+ async _makeRequest(url, options) {
622
+ let response;
623
+ try {
624
+ response = await fetch(url, options);
625
+ }
626
+ catch (error) {
627
+ if (error instanceof Error && error.name === 'NetworkError') {
628
+ throw error;
629
+ }
630
+ throw new NetworkError('Network request failed', error);
631
+ }
632
+ let data;
633
+ try {
634
+ data = await response.json();
635
+ }
636
+ catch {
637
+ throw new APIError('Invalid JSON response', response.status, {});
638
+ }
639
+ if (!response.ok) {
640
+ const d = data;
641
+ const message = d.message instanceof Array
642
+ ? d.message[0].msg
643
+ : d.message || d.error || `HTTP ${response.status}`;
644
+ throw new APIError(message, response.status, {
645
+ response: data,
646
+ });
647
+ }
648
+ return data;
765
649
  }
766
- return data;
767
- }
768
-
769
- /**
770
- * Creates a client session for checkout
771
- * @param {Object} params - Session parameters
772
- * @param {string} params.priceId - Price point identifier
773
- * @param {string} params.externalId - External user identifier
774
- * @param {string} params.email - Customer email
775
- * @param {Object} [params.clientMetadata] - Additional metadata
776
- * @param {string} [params.region='default'] - Checkout region
777
- * @param {string} [params.countryCode] - Optional ISO country code
778
- * @returns {Promise<Object>} Client session response
779
- */
780
- async createClientSession(params) {
781
- const payload = {
782
- region: params.region || 'default',
783
- integration_type: 'primer',
784
- pp_ident: params.priceId,
785
- external_id: params.externalId,
786
- email_address: params.email,
787
- client_metadata: params.clientMetadata || {}
788
- };
789
-
790
- // Add country_code if provided (nullable field per Swagger spec)
791
- if (params.countryCode !== undefined) {
792
- payload.country_code = params.countryCode;
650
+ async createClientSession(params) {
651
+ const payload = {
652
+ region: params.region || 'default',
653
+ integration_type: 'primer',
654
+ pp_ident: params.priceId,
655
+ external_id: params.externalId,
656
+ email_address: params.email,
657
+ client_metadata: params.clientMetadata || {},
658
+ };
659
+ if (params.countryCode !== undefined) {
660
+ payload.country_code = params.countryCode;
661
+ }
662
+ return (await this.request(API_ENDPOINTS.CREATE_CLIENT_SESSION, {
663
+ method: 'POST',
664
+ body: JSON.stringify(payload),
665
+ }));
793
666
  }
794
- return await this.request('/v1/checkout/create_client_session', {
795
- method: 'POST',
796
- body: JSON.stringify(payload)
797
- });
798
- }
799
-
800
- /**
801
- * Updates an existing client session
802
- * @param {Object} params - Update parameters
803
- * @param {string} params.orderId - Order identifier
804
- * @param {string} params.clientToken - Current client token
805
- * @param {string} params.priceId - New price identifier
806
- * @returns {Promise<Object>} Update response
807
- */
808
- async updateClientSession(params) {
809
- const payload = {
810
- order_id: params.orderId,
811
- client_token: params.clientToken,
812
- pp_ident: params.priceId
813
- };
814
- return await this.request('/v1/checkout/update_client_session', {
815
- method: 'POST',
816
- body: JSON.stringify(payload)
817
- });
818
- }
819
-
820
- /**
821
- * Creates a payment with tokenized payment method
822
- * @param {Object} params - Payment parameters
823
- * @param {string} params.orderId - Order identifier
824
- * @param {string} params.paymentMethodToken - Payment method token from Primer
825
- * @returns {Promise<Object>} Payment response
826
- */
827
- async createPayment(params) {
828
- const payload = {
829
- order_id: params.orderId,
830
- payment_method_token: params.paymentMethodToken
831
- };
832
- return await this.request('/v1/checkout/create_payment', {
833
- method: 'POST',
834
- body: JSON.stringify(payload)
835
- });
836
- }
837
-
838
- /**
839
- * Resumes a payment with 3DS or similar flows
840
- * @param {Object} params - Resume parameters
841
- * @param {string} params.orderId - Order identifier
842
- * @param {string} params.resumeToken - Resume token from Primer
843
- * @returns {Promise<Object>} Resume response
844
- */
845
- async resumePayment(params) {
846
- const payload = {
847
- order_id: params.orderId,
848
- resume_token: params.resumeToken
849
- };
850
- return await this.request('/v1/checkout/resume_payment', {
851
- method: 'POST',
852
- body: JSON.stringify(payload)
853
- });
854
- }
855
-
856
- /**
857
- * One-click payment for returning customers with saved payment methods
858
- * @param {Object} params - One-click payment parameters
859
- * @param {string} params.externalId - External user identifier
860
- * @param {string} params.priceId - Price point identifier
861
- * @param {Object} [params.clientMetadata] - Additional metadata
862
- * @returns {Promise<Object>} Payment response
863
- */
864
- async oneClickPayment(params) {
865
- const payload = {
866
- external_id: params.externalId,
867
- pp_ident: params.priceId,
868
- client_metadata: params.clientMetadata || {}
869
- };
870
- return await this.request('/v1/checkout/one_click', {
871
- method: 'POST',
872
- body: JSON.stringify(payload)
873
- });
874
- }
875
-
876
- /**
877
- * Processes session creation response (create_client_session)
878
- * @param {Object} response - Raw API response
879
- * @returns {Object} Processed session data
880
- * @throws {APIError} If response indicates an error
881
- */
882
- processSessionResponse(response) {
883
- if (response.status === 'error') {
884
- // Extract error details from error array format
885
- const firstError = response.error?.[0];
886
- const message = firstError?.msg || 'Session creation failed';
887
- throw new APIError(message, null, {
888
- errorCode: firstError?.code,
889
- errorType: firstError?.type,
890
- requestId: response.req_id,
891
- response
892
- });
667
+ async updateClientSession(params) {
668
+ const payload = {
669
+ order_id: params.orderId,
670
+ client_token: params.clientToken,
671
+ pp_ident: params.priceId,
672
+ };
673
+ return await this.request(API_ENDPOINTS.UPDATE_CLIENT_SESSION, {
674
+ method: 'POST',
675
+ body: JSON.stringify(payload),
676
+ });
893
677
  }
894
- const data = response.data || response;
895
- return {
896
- type: 'session_created',
897
- orderId: data.order_id,
898
- clientToken: data.client_token
899
- };
900
- }
901
-
902
- /**
903
- * Processes payment/resume response (create_payment, resume_payment, one_click)
904
- * @param {Object} response - Raw API response
905
- * @returns {Object} Processed payment data
906
- * @throws {APIError} If response indicates an error
907
- */
908
- processPaymentResponse(response) {
909
- if (response.status === 'error') {
910
- // Extract error details from error array format
911
- const firstError = response.error?.[0];
912
- const message = firstError?.msg || 'Payment request failed';
913
- throw new APIError(message, null, {
914
- errorCode: firstError?.code,
915
- errorType: firstError?.type,
916
- requestId: response.req_id,
917
- response
918
- });
678
+ async createPayment(params) {
679
+ const payload = {
680
+ order_id: params.orderId,
681
+ payment_method_token: params.paymentMethodToken,
682
+ };
683
+ return (await this.request(API_ENDPOINTS.CREATE_PAYMENT, {
684
+ method: 'POST',
685
+ body: JSON.stringify(payload),
686
+ }));
919
687
  }
920
- const data = response.data || response;
921
-
922
- // Check for action required (3DS, etc.)
923
- if (data.action_required_token) {
924
- return {
925
- type: 'action_required',
926
- orderId: data.order_id,
927
- clientToken: data.action_required_token
928
- };
688
+ async resumePayment(params) {
689
+ const payload = {
690
+ order_id: params.orderId,
691
+ resume_token: params.resumeToken,
692
+ };
693
+ return (await this.request(API_ENDPOINTS.RESUME_PAYMENT, {
694
+ method: 'POST',
695
+ body: JSON.stringify(payload),
696
+ }));
929
697
  }
930
-
931
- // Handle checkout status
932
- if (data.checkout_status) {
933
- switch (data.checkout_status) {
934
- case 'succeeded':
935
- return {
936
- type: 'success',
937
- orderId: data.order_id,
938
- status: 'succeeded',
939
- transactionId: data.transaction_id
940
- };
941
- case 'failed':
942
- throw new APIError(data.failed_message_for_user || 'Payment failed', null, data);
943
- case 'cancelled':
944
- throw new APIError('Payment was cancelled by user', null, data);
945
- case 'processing':
946
- return {
947
- type: 'processing',
698
+ processSessionResponse(response) {
699
+ if (response.status === 'error') {
700
+ const firstError = response.error?.[0];
701
+ const message = firstError?.msg || 'Session creation failed';
702
+ throw new APIError(message, null, {
703
+ errorCode: firstError?.code,
704
+ errorType: firstError?.type,
705
+ requestId: response.req_id,
706
+ response,
707
+ });
708
+ }
709
+ const data = response.data;
710
+ return {
711
+ type: 'session_created',
948
712
  orderId: data.order_id,
949
- status: 'processing'
950
- };
951
- default:
952
- throw new APIError(`Unhandled checkout status: ${data.checkout_status}`, null, data);
953
- }
954
- }
955
- throw new APIError('Invalid payment response format', null, data);
956
- }
957
-
958
- /**
959
- * Processes API response and extracts relevant data
960
- * @deprecated Use processSessionResponse() or processPaymentResponse() instead
961
- * @param {Object} response - Raw API response
962
- * @returns {Object} Processed response data
963
- * @throws {APIError} If response indicates an error
964
- */
965
- processResponse(response) {
966
- // Try to detect response type and delegate to specialized methods
967
- const data = response.data || response;
968
- if (data.client_token && data.order_id && !data.checkout_status) {
969
- return this.processSessionResponse(response);
713
+ clientToken: data.client_token,
714
+ };
970
715
  }
971
- if (data.checkout_status || data.action_required_token) {
972
- return this.processPaymentResponse(response);
716
+ processPaymentResponse(response) {
717
+ if (response.status === 'error') {
718
+ const firstError = response.error?.[0];
719
+ const message = firstError?.msg || 'Payment request failed';
720
+ throw new APIError(message, null, {
721
+ errorCode: firstError?.code,
722
+ errorType: firstError?.type,
723
+ response,
724
+ });
725
+ }
726
+ const data = response.data;
727
+ if (data.action_required_token) {
728
+ return {
729
+ type: 'action_required',
730
+ orderId: data.order_id,
731
+ clientToken: data.action_required_token,
732
+ };
733
+ }
734
+ if (data.checkout_status) {
735
+ switch (data.checkout_status) {
736
+ case 'succeeded':
737
+ return {
738
+ type: 'success',
739
+ orderId: data.order_id,
740
+ status: 'succeeded',
741
+ };
742
+ case 'failed':
743
+ throw new APIError(data.failed_message_for_user || 'Payment failed', null, { response });
744
+ case 'cancelled':
745
+ throw new APIError('Payment was cancelled by user', null, {
746
+ response,
747
+ });
748
+ case 'processing':
749
+ return {
750
+ type: 'processing',
751
+ orderId: data.order_id,
752
+ status: 'processing',
753
+ };
754
+ default:
755
+ throw new APIError(`Unhandled checkout status: ${data.checkout_status}`, null, { response });
756
+ }
757
+ }
758
+ throw new APIError('Invalid payment response format', null, { response });
973
759
  }
974
-
975
- // Fallback for unknown response types
976
- throw new APIError('Unknown response format', null, response);
977
- }
978
760
  }
979
761
 
980
762
  /**
981
763
  * @fileoverview Checkout instance manager for Funnefox SDK
982
764
  */
983
-
984
-
985
- /**
986
- * Checkout instance that manages the complete checkout lifecycle
987
- * @typedef {import('./types').CheckoutInstance} CheckoutInstance
988
- * @typedef {import('./types').CheckoutConfig} CheckoutConfig
989
- * @typedef {import('./types').CheckoutConfigWithCallbacks} CheckoutConfigWithCallbacks
990
- * @typedef {import('./types').PaymentResult} PaymentResult
991
- * @typedef {import('./types').CheckoutState} CheckoutState
992
- * @typedef {import('@primer-io/checkout-web').PaymentMethodTokenData} PaymentMethodTokenData
993
- * @typedef {import('@primer-io/checkout-web').ResumeTokenData} ResumeTokenData
994
- * @typedef {import('@primer-io/checkout-web').ITokenizationHandler} PrimerHandler
995
- * @typedef {import('@primer-io/checkout-web').UniversalCheckoutOptions} UniversalCheckoutOptions
996
- */
997
765
  class CheckoutInstance extends EventEmitter {
998
- /**
999
- * @param {Object} config - Checkout configuration
1000
- * @param {string} config.orgId - Organization ID
1001
- * @param {string} [config.baseUrl] - API base URL
1002
- * @param {string} [config.region] - Region
1003
- * @param {CheckoutConfig | CheckoutConfigWithCallbacks} config.checkoutConfig - Checkout configuration
1004
- */
1005
- constructor(config) {
1006
- super();
1007
- this.id = generateId('checkout_');
1008
- this.orgId = config.orgId;
1009
- this.baseUrl = config.baseUrl;
1010
- this.region = config.region;
1011
- this.checkoutConfig = {
1012
- ...config.checkoutConfig
1013
- };
1014
-
1015
- // Extract callbacks from config
1016
- this.callbacks = {
1017
- onSuccess: this.checkoutConfig.onSuccess,
1018
- onError: this.checkoutConfig.onError,
1019
- onStatusChange: this.checkoutConfig.onStatusChange,
1020
- onDestroy: this.checkoutConfig.onDestroy
1021
- };
1022
-
1023
- // Clean callbacks from config to avoid sending to API
1024
- delete this.checkoutConfig.onSuccess;
1025
- delete this.checkoutConfig.onError;
1026
- delete this.checkoutConfig.onStatusChange;
1027
- delete this.checkoutConfig.onDestroy;
1028
-
1029
- // Internal state
1030
- this.state = 'initializing';
1031
- this.orderId = null;
1032
- this.clientToken = null;
1033
- this.primerWrapper = new PrimerWrapper();
1034
- this.isDestroyed = false;
1035
-
1036
- // Set up callback bridges if provided
1037
- this._setupCallbackBridges();
1038
-
1039
- // Bind methods to preserve context
1040
- this._handleTokenizeSuccess = this._handleTokenizeSuccess.bind(this);
1041
- this._handleResumeSuccess = this._handleResumeSuccess.bind(this);
1042
- }
1043
-
1044
- /**
1045
- * Set up bridges between events and callbacks
1046
- * @private
1047
- */
1048
- _setupCallbackBridges() {
1049
- if (this.callbacks.onSuccess) {
1050
- this.on('success', this.callbacks.onSuccess);
766
+ constructor(config) {
767
+ super();
768
+ this.counter = 0;
769
+ this.handleInputChange = (inputName, error) => {
770
+ this.emit(EVENTS.INPUT_ERROR, { name: inputName, error });
771
+ };
772
+ this.handleMethodRender = (method) => {
773
+ this.emit(EVENTS.METHOD_RENDER, method);
774
+ };
775
+ this.handleSubmit = (isSubmitting) => {
776
+ if (isSubmitting) {
777
+ // Clear any previous errors
778
+ this.emit(EVENTS.ERROR, undefined);
779
+ this.emit(EVENTS.START_PURCHASE, PaymentMethod.PAYMENT_CARD);
780
+ }
781
+ this.onLoaderChangeWithRace(isSubmitting);
782
+ this._setState(isSubmitting ? 'processing' : 'ready');
783
+ };
784
+ this.handleTokenizeSuccess = async (paymentMethodTokenData, primerHandler) => {
785
+ try {
786
+ this.emit(EVENTS.START_PURCHASE, paymentMethodTokenData.paymentInstrumentType);
787
+ this.onLoaderChangeWithRace(true);
788
+ this._setState('processing');
789
+ const paymentResponse = await this.apiClient.createPayment({
790
+ orderId: this.orderId,
791
+ paymentMethodToken: paymentMethodTokenData.token,
792
+ });
793
+ const result = this.apiClient.processPaymentResponse(paymentResponse);
794
+ await this._processPaymentResult(result, primerHandler);
795
+ }
796
+ catch (error) {
797
+ this._setState('error');
798
+ this.emit(EVENTS.PURCHASE_FAILURE, error);
799
+ primerHandler.handleFailure(error.message || 'Payment processing failed');
800
+ }
801
+ finally {
802
+ this.onLoaderChangeWithRace(false);
803
+ this._setState('ready');
804
+ }
805
+ };
806
+ this.handleResumeSuccess = async (resumeTokenData, primerHandler) => {
807
+ try {
808
+ this.onLoaderChangeWithRace(true);
809
+ this._setState('processing');
810
+ const resumeResponse = await this.apiClient.resumePayment({
811
+ orderId: this.orderId,
812
+ resumeToken: resumeTokenData.resumeToken,
813
+ });
814
+ const result = this.apiClient.processPaymentResponse(resumeResponse);
815
+ await this._processPaymentResult(result, primerHandler);
816
+ }
817
+ catch (error) {
818
+ this._setState('error');
819
+ this.emit(EVENTS.PURCHASE_FAILURE, error);
820
+ primerHandler.handleFailure(error.message || 'Payment processing failed');
821
+ }
822
+ finally {
823
+ this.emit(EVENTS.PURCHASE_COMPLETED);
824
+ this.onLoaderChangeWithRace(false);
825
+ this._setState('ready');
826
+ }
827
+ };
828
+ this.onLoaderChangeWithRace = (state) => {
829
+ const isLoading = !!(state ? ++this.counter : --this.counter);
830
+ this.emit(EVENTS.LOADER_CHANGE, isLoading);
831
+ };
832
+ this.id = generateId('checkout_');
833
+ this.orgId = config.orgId;
834
+ this.baseUrl = config.baseUrl;
835
+ this.region = config.region;
836
+ this.checkoutConfig = { ...config.checkoutConfig };
837
+ this.callbacks = {
838
+ onSuccess: this.checkoutConfig.onSuccess,
839
+ onError: this.checkoutConfig.onError,
840
+ onStatusChange: this.checkoutConfig.onStatusChange,
841
+ onDestroy: this.checkoutConfig.onDestroy,
842
+ };
843
+ delete this.checkoutConfig?.onSuccess;
844
+ delete this.checkoutConfig?.onError;
845
+ delete this.checkoutConfig?.onStatusChange;
846
+ delete this.checkoutConfig?.onDestroy;
847
+ this.state = 'initializing';
848
+ this.orderId = null;
849
+ this.clientToken = null;
850
+ this.primerWrapper = new PrimerWrapper();
851
+ this.isDestroyed = false;
852
+ this._setupCallbackBridges();
1051
853
  }
1052
- if (this.callbacks.onError) {
1053
- this.on('error', this.callbacks.onError);
854
+ _setupCallbackBridges() {
855
+ if (this.callbacks.onSuccess) {
856
+ this.on(EVENTS.SUCCESS, this.callbacks.onSuccess);
857
+ }
858
+ if (this.callbacks.onError) {
859
+ this.on(EVENTS.ERROR, this.callbacks.onError);
860
+ }
861
+ if (this.callbacks.onStatusChange) {
862
+ this.on(EVENTS.STATUS_CHANGE, this.callbacks.onStatusChange);
863
+ }
864
+ if (this.callbacks.onDestroy) {
865
+ this.on(EVENTS.DESTROY, this.callbacks.onDestroy);
866
+ }
1054
867
  }
1055
- if (this.callbacks.onStatusChange) {
1056
- this.on('status-change', this.callbacks.onStatusChange);
868
+ removeAllListeners() {
869
+ return super.removeAllListeners();
1057
870
  }
1058
- if (this.callbacks.onDestroy) {
1059
- this.on('destroy', this.callbacks.onDestroy);
871
+ async initialize() {
872
+ try {
873
+ this._setState('initializing');
874
+ this.apiClient = new APIClient({
875
+ baseUrl: this.baseUrl || DEFAULTS.BASE_URL,
876
+ orgId: this.orgId,
877
+ timeout: DEFAULTS.REQUEST_TIMEOUT,
878
+ retryAttempts: DEFAULTS.RETRY_ATTEMPTS,
879
+ });
880
+ const sessionResponse = await this.apiClient.createClientSession({
881
+ priceId: this.checkoutConfig.priceId,
882
+ externalId: this.checkoutConfig.customer.externalId,
883
+ email: this.checkoutConfig.customer.email,
884
+ region: this.region || DEFAULTS.REGION,
885
+ clientMetadata: this.checkoutConfig.clientMetadata,
886
+ countryCode: this.checkoutConfig.customer.countryCode,
887
+ });
888
+ const sessionData = this.apiClient.processSessionResponse(sessionResponse);
889
+ this.orderId = sessionData.orderId;
890
+ this.clientToken = sessionData.clientToken;
891
+ await this._initializePrimerCheckout();
892
+ this._setState('ready');
893
+ return this;
894
+ }
895
+ catch (error) {
896
+ this._setState('error');
897
+ this.emit(EVENTS.ERROR, error);
898
+ throw error;
899
+ }
1060
900
  }
1061
- }
1062
-
1063
- /**
1064
- * Initialize the checkout instance
1065
- * @returns {Promise<CheckoutInstance>} Returns this instance for chaining
1066
- */
1067
- async initialize() {
1068
- try {
1069
- this._setState('initializing');
1070
-
1071
- // Validate container exists
1072
- validateContainer(this.checkoutConfig.container);
1073
-
1074
- // Create API client and session
1075
- this.apiClient = new APIClient({
1076
- baseUrl: this.baseUrl || DEFAULTS.BASE_URL,
1077
- orgId: this.orgId,
1078
- timeout: DEFAULTS.REQUEST_TIMEOUT,
1079
- retryAttempts: DEFAULTS.RETRY_ATTEMPTS
1080
- });
1081
- const sessionResponse = await this.apiClient.createClientSession({
1082
- priceId: this.checkoutConfig.priceId,
1083
- externalId: this.checkoutConfig.customer.externalId,
1084
- email: this.checkoutConfig.customer.email,
1085
- region: this.region || DEFAULTS.REGION,
1086
- clientMetadata: this.checkoutConfig.clientMetadata,
1087
- countryCode: this.checkoutConfig.customer.countryCode
1088
- });
1089
- const sessionData = this.apiClient.processSessionResponse(sessionResponse);
1090
- this.orderId = sessionData.orderId;
1091
- this.clientToken = sessionData.clientToken;
1092
-
1093
- // Initialize Primer checkout
1094
- await this._initializePrimerCheckout();
1095
- this._setState('ready');
1096
- return this;
1097
- } catch (error) {
1098
- this._setState('error');
1099
- this.emit('error', error);
1100
- throw error;
901
+ async _initializePrimerCheckout() {
902
+ const checkoutOptions = {
903
+ ...this.checkoutConfig,
904
+ onTokenizeSuccess: this.handleTokenizeSuccess,
905
+ onResumeSuccess: this.handleResumeSuccess,
906
+ onSubmit: this.handleSubmit,
907
+ onInputChange: this.handleInputChange,
908
+ onMethodRender: this.handleMethodRender,
909
+ onSubmitError: (error) => this.emit(EVENTS.PURCHASE_FAILURE, error),
910
+ };
911
+ if (!this.checkoutConfig.cardSelectors ||
912
+ !this.checkoutConfig.paymentButtonSelectors) {
913
+ const cardSelectors = await this.createCardElements();
914
+ const paymentButtonSelectors = {
915
+ paypal: '#paypalButton',
916
+ googlePay: '#googlePayButton',
917
+ applePay: '#applePayButton',
918
+ };
919
+ checkoutOptions.cardSelectors = cardSelectors;
920
+ checkoutOptions.paymentButtonSelectors = paymentButtonSelectors;
921
+ checkoutOptions.card = {
922
+ cardholderName: {
923
+ required: false,
924
+ },
925
+ };
926
+ checkoutOptions.applePay = {
927
+ buttonStyle: 'black',
928
+ };
929
+ checkoutOptions.paypal = {
930
+ buttonColor: 'gold',
931
+ buttonShape: 'pill',
932
+ };
933
+ checkoutOptions.googlePay = {
934
+ buttonColor: 'black',
935
+ };
936
+ }
937
+ await this.primerWrapper.renderCheckout(this.clientToken, checkoutOptions);
1101
938
  }
1102
- }
1103
-
1104
- /**
1105
- * Initialize Primer checkout with current client token
1106
- * @private
1107
- */
1108
- async _initializePrimerCheckout() {
1109
- /** @type {Partial<UniversalCheckoutOptions> & {onTokenizeSuccess: function, onResumeSuccess: function}} */
1110
- const checkoutOptions = {
1111
- container: this.checkoutConfig.container,
1112
- onTokenizeSuccess: this._handleTokenizeSuccess,
1113
- onResumeSuccess: this._handleResumeSuccess,
1114
- ...(this.checkoutConfig.universalCheckoutOptions || {})
1115
- };
1116
- await this.primerWrapper.showUniversalCheckout(this.clientToken, checkoutOptions);
1117
- }
1118
-
1119
- /**
1120
- * Handle successful payment method tokenization
1121
- * @private
1122
- * @param {PaymentMethodTokenData} paymentMethodTokenData - Payment method token from Primer
1123
- * @param {PrimerHandler} primerHandler - Primer handler for success/failure
1124
- */
1125
- async _handleTokenizeSuccess(paymentMethodTokenData, primerHandler) {
1126
- try {
1127
- this._setState('processing');
1128
- const paymentResponse = await this.apiClient.createPayment({
1129
- orderId: this.orderId,
1130
- paymentMethodToken: paymentMethodTokenData.token
1131
- });
1132
- const result = this.apiClient.processPaymentResponse(paymentResponse);
1133
- await this._processPaymentResult(result, primerHandler);
1134
- } catch (error) {
1135
- this._setState('error');
1136
- this.emit('error', error);
1137
- primerHandler.handleFailure(error.message || 'Payment processing failed');
939
+ async _processPaymentResult(result, primerHandler) {
940
+ if (result.orderId) {
941
+ this.orderId = result.orderId;
942
+ }
943
+ switch (result.type) {
944
+ case 'success':
945
+ this._setState('completed');
946
+ this.emit(EVENTS.SUCCESS, {
947
+ orderId: result.orderId,
948
+ status: result.status,
949
+ });
950
+ primerHandler.handleSuccess();
951
+ break;
952
+ case 'action_required':
953
+ this._setState('action_required');
954
+ this.clientToken = result.clientToken;
955
+ primerHandler.continueWithNewClientToken(result.clientToken);
956
+ break;
957
+ case 'processing':
958
+ this._setState('processing');
959
+ setTimeout(() => {
960
+ primerHandler.handleFailure('Payment is still processing. Please check back later.');
961
+ }, 30000);
962
+ break;
963
+ default:
964
+ throw new CheckoutError(`Unknown payment result type: ${result.type}`);
965
+ }
1138
966
  }
1139
- }
1140
-
1141
- /**
1142
- * Handle successful payment resume (3DS flows)
1143
- * @private
1144
- * @param {ResumeTokenData} resumeTokenData - Resume token from Primer
1145
- * @param {PrimerHandler} primerHandler - Primer handler for success/failure
1146
- */
1147
- async _handleResumeSuccess(resumeTokenData, primerHandler) {
1148
- try {
1149
- this._setState('processing');
1150
- const resumeResponse = await this.apiClient.resumePayment({
1151
- orderId: this.orderId,
1152
- resumeToken: resumeTokenData.resumeToken
1153
- });
1154
- const result = this.apiClient.processPaymentResponse(resumeResponse);
1155
- await this._processPaymentResult(result, primerHandler);
1156
- } catch (error) {
1157
- this._setState('error');
1158
- this.emit('error', error);
1159
- primerHandler.handleFailure(error.message || 'Payment processing failed');
967
+ async updatePrice(newPriceId) {
968
+ this._ensureNotDestroyed();
969
+ requireString(newPriceId, 'priceId');
970
+ if (this.state === 'processing') {
971
+ throw new CheckoutError('Cannot update price while payment is processing');
972
+ }
973
+ try {
974
+ this._setState('updating');
975
+ await this.apiClient.updateClientSession({
976
+ orderId: this.orderId,
977
+ clientToken: this.clientToken,
978
+ priceId: newPriceId,
979
+ });
980
+ this.checkoutConfig.priceId = newPriceId;
981
+ this._setState('ready');
982
+ }
983
+ catch (error) {
984
+ this._setState('error');
985
+ this.emit(EVENTS.ERROR, error);
986
+ throw error;
987
+ }
1160
988
  }
1161
- }
1162
-
1163
- /**
1164
- * Process payment result and handle different scenarios
1165
- * @private
1166
- */
1167
- async _processPaymentResult(result, primerHandler) {
1168
- // Update order ID if it changed
1169
- if (result.orderId) {
1170
- this.orderId = result.orderId;
989
+ getStatus() {
990
+ return {
991
+ id: this.id,
992
+ state: this.state,
993
+ orderId: this.orderId,
994
+ priceId: this.checkoutConfig.priceId,
995
+ isDestroyed: this.isDestroyed,
996
+ };
1171
997
  }
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}`);
998
+ async destroy() {
999
+ if (this.isDestroyed)
1000
+ return;
1001
+ try {
1002
+ await this.primerWrapper.destroy();
1003
+ this._setState('destroyed');
1004
+ this.orderId = null;
1005
+ this.clientToken = null;
1006
+ this.isDestroyed = true;
1007
+ this.emit(EVENTS.DESTROY);
1008
+ this.removeAllListeners();
1009
+ }
1010
+ catch (error) {
1011
+ // eslint-disable-next-line no-console
1012
+ console.warn('Error during checkout cleanup:', error);
1013
+ }
1198
1014
  }
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');
1015
+ _setState(newState) {
1016
+ if (this.state !== newState) {
1017
+ const oldState = this.state;
1018
+ this.state = newState;
1019
+ this.emit(EVENTS.STATUS_CHANGE, newState, oldState);
1020
+ }
1211
1021
  }
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;
1022
+ _ensureNotDestroyed() {
1023
+ if (this.isDestroyed) {
1024
+ throw new CheckoutError('Checkout instance has been destroyed');
1025
+ }
1228
1026
  }
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;
1027
+ getContainer() {
1028
+ return document.querySelector(this.checkoutConfig.container);
1252
1029
  }
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);
1030
+ isInState(state) {
1031
+ return this.state === state;
1270
1032
  }
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);
1033
+ isReady() {
1034
+ return this.state === 'ready' && !this.isDestroyed;
1282
1035
  }
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');
1036
+ isProcessing() {
1037
+ return ['processing', 'action_required'].includes(this.state);
1038
+ }
1039
+ // Creates containers to render hosted inputs with labels and error messages,
1040
+ // a card holder input with label and error, and a submit button.
1041
+ async createCardElements() {
1042
+ const skinFactory = (await import('./chunk-index.es.js'))
1043
+ .default;
1044
+ const skin = await skinFactory(this.primerWrapper, this.checkoutConfig.container);
1045
+ this.on(EVENTS.INPUT_ERROR, skin.onInputError);
1046
+ this.on(EVENTS.STATUS_CHANGE, skin.onStatusChange);
1047
+ this.on(EVENTS.ERROR, (error) => skin.onError(error));
1048
+ this.on(EVENTS.LOADER_CHANGE, skin.onLoaderChange);
1049
+ this.on(EVENTS.DESTROY, skin.onDestroy);
1050
+ this.on(EVENTS.METHOD_RENDER, skin.onMethodRender);
1051
+ this.on(EVENTS.SUCCESS, skin.onSuccess);
1052
+ this.on(EVENTS.START_PURCHASE, skin.onStartPurchase);
1053
+ this.on(EVENTS.PURCHASE_FAILURE, skin.onPurchaseFailure);
1054
+ this.on(EVENTS.PURCHASE_COMPLETED, skin.onPurchaseCompleted);
1055
+ return skin.getCardInputSelectors();
1292
1056
  }
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
1057
  }
1328
1058
 
1329
1059
  /**
1330
1060
  * @fileoverview Public API with configuration and orchestration logic
1331
1061
  */
1332
-
1333
1062
  let defaultConfig = null;
1334
-
1335
- /**
1336
- * Configure global SDK settings
1337
- * @param {import('./types').SDKConfig} config - SDK configuration
1338
- */
1339
1063
  function configure(config) {
1340
- defaultConfig = config;
1064
+ defaultConfig = config;
1341
1065
  }
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
1066
  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
- };
1067
+ const { orgId, apiConfig } = options || {};
1068
+ const finalOrgId = orgId || defaultConfig?.orgId;
1069
+ if (!finalOrgId) {
1070
+ throw new Error(`orgId is required. Pass it to ${functionName}() or call configure() first.`);
1071
+ }
1072
+ const finalBaseUrl = apiConfig?.baseUrl || defaultConfig?.baseUrl || DEFAULTS.BASE_URL;
1073
+ const finalRegion = apiConfig?.region || defaultConfig?.region || DEFAULTS.REGION;
1074
+ return {
1075
+ orgId: finalOrgId,
1076
+ baseUrl: finalBaseUrl,
1077
+ region: finalRegion,
1078
+ };
1368
1079
  }
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
1080
  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);
1081
+ const { ...checkoutConfig } = options;
1082
+ const primerWrapper = new PrimerWrapper();
1083
+ primerWrapper.ensurePrimerAvailable();
1084
+ const config = resolveConfig(options, 'createCheckout');
1085
+ const checkout = new CheckoutInstance({
1086
+ ...config,
1087
+ checkoutConfig,
1088
+ });
1089
+ await checkout.initialize();
1090
+ return checkout;
1407
1091
  }
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
1092
  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);
1093
+ const { priceId, externalId, email, clientMetadata, countryCode } = params;
1094
+ const config = resolveConfig(params, 'createClientSession');
1095
+ const apiClient = new APIClient({
1096
+ baseUrl: config.baseUrl,
1097
+ orgId: config.orgId,
1098
+ timeout: DEFAULTS.REQUEST_TIMEOUT,
1099
+ retryAttempts: DEFAULTS.RETRY_ATTEMPTS,
1100
+ });
1101
+ const sessionResponse = await apiClient.createClientSession({
1102
+ priceId,
1103
+ externalId,
1104
+ email,
1105
+ region: config.region,
1106
+ clientMetadata,
1107
+ countryCode,
1108
+ });
1109
+ return apiClient.processSessionResponse(sessionResponse);
1449
1110
  }
1450
1111
 
1451
1112
  /**
1452
1113
  * @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
1114
  */
1485
-
1486
1115
  const Billing = {
1487
- configure: configure,
1488
- createCheckout: createCheckout,
1489
- showUniversalCheckout: showUniversalCheckout,
1490
- createClientSession: createClientSession
1116
+ configure: configure,
1117
+ createCheckout: createCheckout,
1118
+ createClientSession: createClientSession,
1491
1119
  };
1492
1120
  if (typeof window !== 'undefined') {
1493
- window.Billing = Billing;
1121
+ window.Billing = Billing;
1494
1122
  }
1495
1123
 
1496
- export { APIError, Billing, CHECKOUT_STATES, CheckoutError, ConfigurationError, DEFAULTS, ERROR_CODES, EVENTS, FunnefoxSDKError, NetworkError, PrimerError, SDK_VERSION, ValidationError, configure, createCheckout, createClientSession, Billing as default, showUniversalCheckout };
1124
+ export { APIError, Billing, CHECKOUT_STATES, CheckoutError, ConfigurationError, DEFAULTS, ERROR_CODES, EVENTS, FunnefoxSDKError, NetworkError, PrimerError, SDK_VERSION, ValidationError, configure, createCheckout, createClientSession, Billing as default };