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