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