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