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