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