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