@funnelfox/billing 0.5.7 → 0.5.8
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/dist/chunk-index.cjs.js +1933 -154
- package/dist/chunk-index.cjs2.js +159 -54
- package/dist/chunk-index.cjs3.js +101 -0
- package/dist/chunk-index.es.js +1917 -154
- package/dist/chunk-index.es2.js +159 -54
- package/dist/chunk-index.es3.js +99 -0
- package/dist/funnelfox-billing.cjs.js +24 -1935
- package/dist/funnelfox-billing.esm.js +1 -1920
- package/dist/funnelfox-billing.js +62 -22
- package/dist/funnelfox-billing.min.js +1 -1
- package/package.json +2 -2
package/dist/chunk-index.cjs.js
CHANGED
|
@@ -7,212 +7,1991 @@
|
|
|
7
7
|
*/
|
|
8
8
|
'use strict';
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
11
|
+
/**
|
|
12
|
+
* @fileoverview Lightweight event emitter for Funnefox SDK
|
|
13
|
+
*/
|
|
14
|
+
class EventEmitter {
|
|
15
|
+
constructor() {
|
|
16
|
+
this._events = new Map();
|
|
17
|
+
}
|
|
18
|
+
on(eventName, handler) {
|
|
19
|
+
if (typeof handler !== 'function') {
|
|
20
|
+
throw new Error('Event handler must be a function');
|
|
21
|
+
}
|
|
22
|
+
if (!this._events.has(eventName)) {
|
|
23
|
+
this._events.set(eventName, []);
|
|
24
|
+
}
|
|
25
|
+
this._events.get(eventName).push(handler);
|
|
26
|
+
return this;
|
|
27
|
+
}
|
|
28
|
+
once(eventName, handler) {
|
|
29
|
+
if (typeof handler !== 'function') {
|
|
30
|
+
throw new Error('Event handler must be a function');
|
|
31
|
+
}
|
|
32
|
+
const onceWrapper = (...args) => {
|
|
33
|
+
this.off(eventName, onceWrapper);
|
|
34
|
+
handler.apply(this, args);
|
|
35
|
+
};
|
|
36
|
+
return this.on(eventName, onceWrapper);
|
|
37
|
+
}
|
|
38
|
+
off(eventName, handler = null) {
|
|
39
|
+
if (!this._events.has(eventName)) {
|
|
40
|
+
return this;
|
|
41
|
+
}
|
|
42
|
+
if (handler === null) {
|
|
43
|
+
this._events.delete(eventName);
|
|
44
|
+
return this;
|
|
45
|
+
}
|
|
46
|
+
const handlers = this._events.get(eventName);
|
|
47
|
+
const index = handlers.indexOf(handler);
|
|
48
|
+
if (index !== -1) {
|
|
49
|
+
handlers.splice(index, 1);
|
|
50
|
+
if (handlers.length === 0) {
|
|
51
|
+
this._events.delete(eventName);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return this;
|
|
55
|
+
}
|
|
56
|
+
emit(eventName, ...args) {
|
|
57
|
+
if (!this._events.has(eventName)) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
const handlers = this._events.get(eventName).slice();
|
|
61
|
+
for (const handler of handlers) {
|
|
62
|
+
try {
|
|
63
|
+
handler.apply(this, args);
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
// eslint-disable-next-line no-console
|
|
67
|
+
console.warn(`Error in event handler for "${String(eventName)}":`, error);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
listenerCount(eventName) {
|
|
73
|
+
return this._events.has(eventName)
|
|
74
|
+
? this._events.get(eventName).length
|
|
75
|
+
: 0;
|
|
76
|
+
}
|
|
77
|
+
eventNames() {
|
|
78
|
+
return Array.from(this._events.keys());
|
|
79
|
+
}
|
|
80
|
+
removeAllListeners() {
|
|
81
|
+
this._events.clear();
|
|
82
|
+
return this;
|
|
83
|
+
}
|
|
84
|
+
listeners(eventName) {
|
|
85
|
+
return this._events.has(eventName)
|
|
86
|
+
? this._events.get(eventName).slice()
|
|
87
|
+
: [];
|
|
88
|
+
}
|
|
89
|
+
}
|
|
14
90
|
|
|
15
|
-
|
|
91
|
+
/**
|
|
92
|
+
* @fileoverview Custom error classes for Funnefox SDK
|
|
93
|
+
*/
|
|
94
|
+
class FunnefoxSDKError extends Error {
|
|
95
|
+
constructor(message, code = ERROR_CODES.SDK_ERROR, details = null) {
|
|
96
|
+
super(message);
|
|
97
|
+
this.name = 'FunnefoxSDKError';
|
|
98
|
+
this.code = code;
|
|
99
|
+
this.details = details;
|
|
100
|
+
if (Error.captureStackTrace) {
|
|
101
|
+
Error.captureStackTrace(this, FunnefoxSDKError);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
class ValidationError extends FunnefoxSDKError {
|
|
106
|
+
constructor(field, message, value = null) {
|
|
107
|
+
super(`Invalid ${field}: ${message}`, ERROR_CODES.VALIDATION_ERROR);
|
|
108
|
+
this.name = 'ValidationError';
|
|
109
|
+
this.field = field;
|
|
110
|
+
this.value = value;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
class APIError extends FunnefoxSDKError {
|
|
114
|
+
constructor(message, statusCode = null, options = {}) {
|
|
115
|
+
super(message, options.errorCode || ERROR_CODES.API_ERROR);
|
|
116
|
+
this.name = 'APIError';
|
|
117
|
+
this.statusCode = statusCode;
|
|
118
|
+
this.errorCode = options.errorCode || null;
|
|
119
|
+
this.errorType = options.errorType || null;
|
|
120
|
+
this.requestId = options.requestId || null;
|
|
121
|
+
this.response = options.response || null;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
class PrimerError extends FunnefoxSDKError {
|
|
125
|
+
constructor(message, primerError = null) {
|
|
126
|
+
super(message, ERROR_CODES.PRIMER_ERROR);
|
|
127
|
+
this.name = 'PrimerError';
|
|
128
|
+
this.primerError = primerError;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
class CheckoutError extends FunnefoxSDKError {
|
|
132
|
+
constructor(message, phase = null) {
|
|
133
|
+
super(message, ERROR_CODES.CHECKOUT_ERROR);
|
|
134
|
+
this.name = 'CheckoutError';
|
|
135
|
+
this.phase = phase;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
class ConfigurationError extends FunnefoxSDKError {
|
|
139
|
+
constructor(message) {
|
|
140
|
+
super(message, ERROR_CODES.CONFIGURATION_ERROR);
|
|
141
|
+
this.name = 'ConfigurationError';
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
class NetworkError extends FunnefoxSDKError {
|
|
145
|
+
constructor(message, originalError = null) {
|
|
146
|
+
super(message, ERROR_CODES.NETWORK_ERROR);
|
|
147
|
+
this.name = 'NetworkError';
|
|
148
|
+
this.originalError = originalError;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
16
151
|
|
|
17
|
-
|
|
152
|
+
/**
|
|
153
|
+
* @fileoverview Dynamic loader for Primer SDK
|
|
154
|
+
* Loads Primer script and CSS from CDN independently of bundler
|
|
155
|
+
*/
|
|
156
|
+
const PRIMER_CDN_BASE = 'https://sdk.primer.io/web';
|
|
157
|
+
const DEFAULT_VERSION = '2.57.3';
|
|
158
|
+
// Integrity hashes for specific versions (for SRI security)
|
|
159
|
+
const INTEGRITY_HASHES = {
|
|
160
|
+
'2.57.3': {
|
|
161
|
+
js: 'sha384-xq2SWkYvTlKOMpuXQUXq1QI3eZN7JiqQ3Sc72U9wY1IE30MW3HkwQWg/1n6BTMz4',
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
let loadingPromise = null;
|
|
165
|
+
let isLoaded = false;
|
|
166
|
+
/**
|
|
167
|
+
* Injects a script tag into the document head
|
|
168
|
+
*/
|
|
169
|
+
function injectScript$1(src, integrity) {
|
|
170
|
+
return new Promise((resolve, reject) => {
|
|
171
|
+
// Check if script already exists
|
|
172
|
+
const existingScript = document.querySelector(`script[src="${src}"]`);
|
|
173
|
+
if (existingScript) {
|
|
174
|
+
resolve(existingScript);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const script = document.createElement('script');
|
|
178
|
+
script.src = src;
|
|
179
|
+
script.async = true;
|
|
180
|
+
script.crossOrigin = 'anonymous';
|
|
181
|
+
if (integrity) {
|
|
182
|
+
script.integrity = integrity;
|
|
183
|
+
}
|
|
184
|
+
script.onload = () => resolve(script);
|
|
185
|
+
script.onerror = () => reject(new Error(`Failed to load Primer SDK script from ${src}`));
|
|
186
|
+
document.head.appendChild(script);
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Injects a CSS link tag into the document head
|
|
191
|
+
*/
|
|
192
|
+
function injectCSS(href, integrity) {
|
|
193
|
+
return new Promise((resolve, reject) => {
|
|
194
|
+
// Check if stylesheet already exists
|
|
195
|
+
const existingLink = document.querySelector(`link[href="${href}"]`);
|
|
196
|
+
if (existingLink) {
|
|
197
|
+
resolve(existingLink);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
const link = document.createElement('link');
|
|
201
|
+
link.rel = 'stylesheet';
|
|
202
|
+
link.href = href;
|
|
203
|
+
link.crossOrigin = 'anonymous';
|
|
204
|
+
if (integrity) {
|
|
205
|
+
link.integrity = integrity;
|
|
206
|
+
}
|
|
207
|
+
link.onload = () => resolve(link);
|
|
208
|
+
link.onerror = () => reject(new Error(`Failed to load Primer SDK CSS from ${href}`));
|
|
209
|
+
document.head.appendChild(link);
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Waits for window.Primer to be available
|
|
214
|
+
*/
|
|
215
|
+
function waitForPrimer(timeout = 10000) {
|
|
216
|
+
return new Promise((resolve, reject) => {
|
|
217
|
+
const startTime = Date.now();
|
|
218
|
+
const check = () => {
|
|
219
|
+
if (typeof window !== 'undefined' &&
|
|
220
|
+
window.Primer &&
|
|
221
|
+
typeof window.Primer.createHeadless === 'function') {
|
|
222
|
+
resolve();
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
if (Date.now() - startTime > timeout) {
|
|
226
|
+
reject(new Error('Timeout waiting for Primer SDK to initialize on window'));
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
setTimeout(check, 50);
|
|
230
|
+
};
|
|
231
|
+
check();
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Loads the Primer SDK script and CSS from CDN
|
|
236
|
+
* @param version - The version of Primer SDK to load (default: 2.57.3)
|
|
237
|
+
* @returns Promise that resolves when SDK is loaded and ready
|
|
238
|
+
*/
|
|
239
|
+
async function loadPrimerSDK(version) {
|
|
240
|
+
// Already loaded
|
|
241
|
+
if (isLoaded) {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
// Already loading - return existing promise
|
|
245
|
+
if (loadingPromise) {
|
|
246
|
+
return loadingPromise;
|
|
247
|
+
}
|
|
248
|
+
// Check if Primer is already available (user may have loaded it manually)
|
|
249
|
+
if (typeof window !== 'undefined' &&
|
|
250
|
+
window.Primer &&
|
|
251
|
+
typeof window.Primer.createHeadless === 'function') {
|
|
252
|
+
isLoaded = true;
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
const ver = version || DEFAULT_VERSION;
|
|
256
|
+
const jsUrl = `${PRIMER_CDN_BASE}/v${ver}/Primer.min.js`;
|
|
257
|
+
const cssUrl = `${PRIMER_CDN_BASE}/v${ver}/Checkout.css`;
|
|
258
|
+
const hashes = INTEGRITY_HASHES[ver];
|
|
259
|
+
loadingPromise = (async () => {
|
|
260
|
+
try {
|
|
261
|
+
// Load CSS and JS in parallel
|
|
262
|
+
await Promise.all([
|
|
263
|
+
injectCSS(cssUrl, hashes?.css),
|
|
264
|
+
injectScript$1(jsUrl, hashes?.js),
|
|
265
|
+
]);
|
|
266
|
+
// Wait for Primer to be available on window
|
|
267
|
+
await waitForPrimer();
|
|
268
|
+
isLoaded = true;
|
|
269
|
+
}
|
|
270
|
+
catch (error) {
|
|
271
|
+
loadingPromise = null;
|
|
272
|
+
throw error;
|
|
273
|
+
}
|
|
274
|
+
})();
|
|
275
|
+
return loadingPromise;
|
|
276
|
+
}
|
|
18
277
|
|
|
19
|
-
|
|
278
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
279
|
+
/**
|
|
280
|
+
* @fileoverview Helper utilities for Funnefox SDK
|
|
281
|
+
*/
|
|
282
|
+
function merge(...objects) {
|
|
283
|
+
const result = {};
|
|
284
|
+
for (const obj of objects) {
|
|
285
|
+
if (obj && typeof obj === 'object') {
|
|
286
|
+
for (const key in obj) {
|
|
287
|
+
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
288
|
+
if (typeof obj[key] === 'object' &&
|
|
289
|
+
!Array.isArray(obj[key]) &&
|
|
290
|
+
obj[key] !== null) {
|
|
291
|
+
result[key] = merge(result[key] || {}, obj[key]);
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
result[key] = obj[key];
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return result;
|
|
301
|
+
}
|
|
302
|
+
function generateId(prefix = '') {
|
|
303
|
+
const timestamp = Date.now().toString(36);
|
|
304
|
+
const random = Math.random().toString(36).substr(2, 5);
|
|
305
|
+
return `${prefix}${timestamp}_${random}`;
|
|
306
|
+
}
|
|
307
|
+
function sleep(ms) {
|
|
308
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
309
|
+
}
|
|
310
|
+
async function retry(fn, maxAttempts = 3, baseDelay = 1000) {
|
|
311
|
+
let lastError;
|
|
312
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
313
|
+
try {
|
|
314
|
+
return await fn();
|
|
315
|
+
}
|
|
316
|
+
catch (error) {
|
|
317
|
+
lastError = error;
|
|
318
|
+
if (attempt === maxAttempts)
|
|
319
|
+
throw lastError;
|
|
320
|
+
const delay = baseDelay * Math.pow(2, attempt - 1);
|
|
321
|
+
await sleep(delay);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
throw lastError;
|
|
325
|
+
}
|
|
326
|
+
function withTimeout(promise, timeoutMs, message = 'Operation timed out') {
|
|
327
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
328
|
+
setTimeout(() => reject(new Error(message)), timeoutMs);
|
|
329
|
+
});
|
|
330
|
+
return Promise.race([promise, timeoutPromise]);
|
|
331
|
+
}
|
|
20
332
|
|
|
21
|
-
|
|
333
|
+
/**
|
|
334
|
+
* @fileoverview Headless checkout cache manager
|
|
335
|
+
*/
|
|
336
|
+
/**
|
|
337
|
+
* Manages caching and sequential creation of Primer headless checkout instances.
|
|
338
|
+
* Ensures that multiple checkouts with the same configuration reuse the same instance,
|
|
339
|
+
* and that creations happen sequentially to avoid race conditions.
|
|
340
|
+
*/
|
|
341
|
+
class HeadlessManager {
|
|
342
|
+
constructor() {
|
|
343
|
+
this.cache = new Map();
|
|
344
|
+
this.queue = Promise.resolve();
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Generates a cache key from clientToken and serializable options
|
|
348
|
+
*/
|
|
349
|
+
generateKey(clientToken, options) {
|
|
350
|
+
const serializableOptions = {
|
|
351
|
+
paymentHandling: options.paymentHandling,
|
|
352
|
+
apiVersion: options.apiVersion,
|
|
353
|
+
style: options.style,
|
|
354
|
+
card: options.card,
|
|
355
|
+
applePay: options.applePay,
|
|
356
|
+
paypal: options.paypal,
|
|
357
|
+
googlePay: options.googlePay,
|
|
358
|
+
};
|
|
359
|
+
return `${clientToken}:${JSON.stringify(serializableOptions)}`;
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Gets a cached headless instance or creates a new one.
|
|
363
|
+
* Ensures sequential creation order to avoid race conditions.
|
|
364
|
+
*/
|
|
365
|
+
getOrCreate(clientToken, options) {
|
|
366
|
+
const key = this.generateKey(clientToken, options);
|
|
367
|
+
// Return cached promise if exists
|
|
368
|
+
const cached = this.cache.get(key);
|
|
369
|
+
if (cached)
|
|
370
|
+
return cached;
|
|
371
|
+
// Create new headless in sequential order
|
|
372
|
+
const previousQueue = this.queue;
|
|
373
|
+
const promise = (async () => {
|
|
374
|
+
await previousQueue; // Wait for previous creation
|
|
375
|
+
const primerOptions = merge({
|
|
376
|
+
paymentHandling: 'MANUAL',
|
|
377
|
+
apiVersion: '2.4',
|
|
378
|
+
}, options);
|
|
379
|
+
try {
|
|
380
|
+
const headlessResult = await window.Primer.createHeadless(clientToken, primerOptions);
|
|
381
|
+
const headless = await headlessResult;
|
|
382
|
+
await headless.start();
|
|
383
|
+
return headless;
|
|
384
|
+
}
|
|
385
|
+
catch (error) {
|
|
386
|
+
// Remove from cache on failure
|
|
387
|
+
this.cache.delete(key);
|
|
388
|
+
throw new PrimerError('Failed to create Primer headless checkout', error);
|
|
389
|
+
}
|
|
390
|
+
})();
|
|
391
|
+
this.cache.set(key, promise);
|
|
392
|
+
this.queue = promise.catch(() => { }); // Update queue, ignore errors
|
|
393
|
+
return promise;
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Removes a headless instance from the cache
|
|
397
|
+
*/
|
|
398
|
+
remove(headlessPromise) {
|
|
399
|
+
for (const [key, value] of this.cache.entries()) {
|
|
400
|
+
if (value === headlessPromise) {
|
|
401
|
+
this.cache.delete(key);
|
|
402
|
+
break;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Clears all cached instances
|
|
408
|
+
*/
|
|
409
|
+
clear() {
|
|
410
|
+
this.cache.clear();
|
|
411
|
+
}
|
|
412
|
+
}
|
|
22
413
|
|
|
23
|
-
|
|
414
|
+
exports.PaymentMethod = void 0;
|
|
415
|
+
(function (PaymentMethod) {
|
|
416
|
+
PaymentMethod["GOOGLE_PAY"] = "GOOGLE_PAY";
|
|
417
|
+
PaymentMethod["APPLE_PAY"] = "APPLE_PAY";
|
|
418
|
+
PaymentMethod["PAYPAL"] = "PAYPAL";
|
|
419
|
+
PaymentMethod["PAYMENT_CARD"] = "PAYMENT_CARD";
|
|
420
|
+
})(exports.PaymentMethod || (exports.PaymentMethod = {}));
|
|
24
421
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
422
|
+
/**
|
|
423
|
+
* @fileoverview Constants for Funnefox SDK
|
|
424
|
+
*/
|
|
425
|
+
const SDK_VERSION = '0.5.8';
|
|
426
|
+
const DEFAULTS = {
|
|
427
|
+
BASE_URL: 'https://billing.funnelfox.com',
|
|
428
|
+
REGION: 'default',
|
|
429
|
+
SANDBOX: false,
|
|
430
|
+
REQUEST_TIMEOUT: 30000,
|
|
431
|
+
RETRY_ATTEMPTS: 3,
|
|
432
|
+
RETRY_BASE_DELAY: 1000,
|
|
30
433
|
};
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
434
|
+
const CHECKOUT_STATES = {
|
|
435
|
+
INITIALIZING: 'initializing',
|
|
436
|
+
READY: 'ready',
|
|
437
|
+
PROCESSING: 'processing',
|
|
438
|
+
ACTION_REQUIRED: 'action_required',
|
|
439
|
+
UPDATING: 'updating',
|
|
440
|
+
COMPLETED: 'completed',
|
|
441
|
+
ERROR: 'error',
|
|
442
|
+
DESTROYED: 'destroyed',
|
|
443
|
+
};
|
|
444
|
+
const EVENTS = {
|
|
445
|
+
SUCCESS: 'success',
|
|
446
|
+
ERROR: 'error',
|
|
447
|
+
STATUS_CHANGE: 'status-change',
|
|
448
|
+
DESTROY: 'destroy',
|
|
449
|
+
INPUT_ERROR: 'input-error',
|
|
450
|
+
LOADER_CHANGE: 'loader-change',
|
|
451
|
+
METHOD_RENDER: 'method-render',
|
|
452
|
+
METHOD_RENDER_ERROR: 'method-render-error',
|
|
453
|
+
START_PURCHASE: 'start-purchase',
|
|
454
|
+
PURCHASE_FAILURE: 'purchase-failure',
|
|
455
|
+
PURCHASE_COMPLETED: 'purchase-completed',
|
|
456
|
+
PURCHASE_CANCELLED: 'purchase-cancelled',
|
|
457
|
+
METHODS_AVAILABLE: 'methods-available',
|
|
458
|
+
};
|
|
459
|
+
const API_ENDPOINTS = {
|
|
460
|
+
CREATE_CLIENT_SESSION: '/v1/checkout/create_client_session',
|
|
461
|
+
UPDATE_CLIENT_SESSION: '/v1/checkout/update_client_session',
|
|
462
|
+
CREATE_PAYMENT: '/v1/checkout/create_payment',
|
|
463
|
+
RESUME_PAYMENT: '/v1/checkout/resume_payment',
|
|
464
|
+
};
|
|
465
|
+
const ERROR_CODES = {
|
|
466
|
+
SDK_ERROR: 'SDK_ERROR',
|
|
467
|
+
VALIDATION_ERROR: 'VALIDATION_ERROR',
|
|
468
|
+
API_ERROR: 'API_ERROR',
|
|
469
|
+
PRIMER_ERROR: 'PRIMER_ERROR',
|
|
470
|
+
CHECKOUT_ERROR: 'CHECKOUT_ERROR',
|
|
471
|
+
CONFIGURATION_ERROR: 'CONFIGURATION_ERROR',
|
|
472
|
+
NETWORK_ERROR: 'NETWORK_ERROR',
|
|
473
|
+
};
|
|
474
|
+
const ALLOWED_BUTTON_PAYMENT_METHODS = [
|
|
475
|
+
exports.PaymentMethod.GOOGLE_PAY,
|
|
476
|
+
exports.PaymentMethod.APPLE_PAY,
|
|
477
|
+
exports.PaymentMethod.PAYPAL,
|
|
478
|
+
];
|
|
479
|
+
const ALLOWED_CARD_PAYMENT_METHODS = [
|
|
480
|
+
exports.PaymentMethod.PAYMENT_CARD,
|
|
481
|
+
];
|
|
482
|
+
const ALLOWED_PAYMENT_METHODS = [
|
|
483
|
+
...ALLOWED_BUTTON_PAYMENT_METHODS,
|
|
484
|
+
...ALLOWED_CARD_PAYMENT_METHODS,
|
|
485
|
+
];
|
|
486
|
+
const inputStyle = {
|
|
487
|
+
input: {
|
|
488
|
+
error: {
|
|
489
|
+
borderColor: 'rgb(227, 47, 65)',
|
|
490
|
+
},
|
|
491
|
+
base: {
|
|
492
|
+
borderWidth: '1px',
|
|
493
|
+
borderStyle: 'solid',
|
|
494
|
+
borderColor: 'rgb(0 0 0 / 10%)',
|
|
495
|
+
height: '36px',
|
|
496
|
+
paddingHorizontal: 10,
|
|
497
|
+
borderRadius: '6px',
|
|
498
|
+
},
|
|
499
|
+
},
|
|
500
|
+
};
|
|
501
|
+
({
|
|
502
|
+
paddingLeft: inputStyle.input.base.paddingHorizontal + 'px',
|
|
503
|
+
paddingRight: inputStyle.input.base.paddingHorizontal + 'px'});
|
|
504
|
+
const DEFAULT_PAYMENT_METHOD_ORDER = [
|
|
505
|
+
exports.PaymentMethod.APPLE_PAY,
|
|
506
|
+
exports.PaymentMethod.GOOGLE_PAY,
|
|
507
|
+
exports.PaymentMethod.PAYPAL,
|
|
508
|
+
exports.PaymentMethod.PAYMENT_CARD,
|
|
509
|
+
];
|
|
510
|
+
const DEFAULT_BUTTONS_STYLES = {
|
|
511
|
+
[exports.PaymentMethod.APPLE_PAY]: {
|
|
512
|
+
buttonStyle: 'black',
|
|
513
|
+
},
|
|
514
|
+
[exports.PaymentMethod.PAYPAL]: {
|
|
515
|
+
buttonColor: 'gold',
|
|
516
|
+
buttonShape: 'pill',
|
|
517
|
+
buttonLabel: 'pay',
|
|
518
|
+
buttonSize: 'large',
|
|
519
|
+
buttonHeight: 54,
|
|
520
|
+
},
|
|
521
|
+
[exports.PaymentMethod.GOOGLE_PAY]: {
|
|
522
|
+
buttonColor: 'black',
|
|
523
|
+
buttonSizeMode: 'fill',
|
|
524
|
+
buttonType: 'pay',
|
|
525
|
+
},
|
|
526
|
+
};
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* @fileoverview Primer SDK integration wrapper
|
|
530
|
+
*/
|
|
531
|
+
class PrimerWrapper {
|
|
532
|
+
constructor() {
|
|
533
|
+
this.isInitialized = false;
|
|
534
|
+
this.destroyCallbacks = [];
|
|
535
|
+
this.currentHeadless = null;
|
|
536
|
+
this.availableMethods = [];
|
|
537
|
+
this.paymentMethodsInterfaces = [];
|
|
538
|
+
}
|
|
539
|
+
isPrimerAvailable() {
|
|
540
|
+
return (typeof window !== 'undefined' &&
|
|
541
|
+
window.Primer &&
|
|
542
|
+
typeof window.Primer?.createHeadless === 'function');
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Loads Primer SDK if not already available
|
|
546
|
+
* @param version - Optional version to load (uses default if not specified)
|
|
547
|
+
*/
|
|
548
|
+
async ensurePrimerLoaded(version) {
|
|
549
|
+
if (this.isPrimerAvailable()) {
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
try {
|
|
553
|
+
await loadPrimerSDK(version);
|
|
554
|
+
}
|
|
555
|
+
catch (error) {
|
|
556
|
+
throw new PrimerError('Failed to load Primer SDK', error);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
ensurePrimerAvailable() {
|
|
560
|
+
if (!this.isPrimerAvailable()) {
|
|
561
|
+
throw new PrimerError('Primer SDK not found. Please include the Primer SDK script before initializing FunnefoxSDK.');
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
async createHeadlessCheckout(clientToken, options) {
|
|
565
|
+
await this.ensurePrimerLoaded();
|
|
566
|
+
this.currentHeadless = PrimerWrapper.headlessManager.getOrCreate(clientToken, options);
|
|
567
|
+
return this.currentHeadless;
|
|
568
|
+
}
|
|
569
|
+
disableButtons(disabled) {
|
|
570
|
+
if (!this.paymentMethodsInterfaces)
|
|
571
|
+
return;
|
|
572
|
+
for (const paymentMethodInterface of this.paymentMethodsInterfaces) {
|
|
573
|
+
paymentMethodInterface.setDisabled(disabled);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
async renderButton(allowedPaymentMethod, { htmlNode, onMethodRenderError, onMethodRender, }) {
|
|
577
|
+
let button;
|
|
578
|
+
// Ensure Primer SDK is loaded
|
|
579
|
+
await this.ensurePrimerLoaded();
|
|
580
|
+
if (!this.currentHeadless) {
|
|
581
|
+
throw new PrimerError('Headless checkout not found');
|
|
582
|
+
}
|
|
583
|
+
try {
|
|
584
|
+
const headless = await this.currentHeadless;
|
|
585
|
+
const pmManager = await headless.createPaymentMethodManager(allowedPaymentMethod);
|
|
586
|
+
if (!pmManager) {
|
|
587
|
+
throw new Error('Payment method manager is not available');
|
|
588
|
+
}
|
|
589
|
+
/* hack for FFB-169 & FFB-242
|
|
590
|
+
* Primer SDK does not allow to set the height of the googlepay and applepay buttons, so we need to use a hack to set the height of the button.
|
|
591
|
+
*/
|
|
592
|
+
const wrapper = document.createElement('div');
|
|
593
|
+
wrapper.className = generateId('funnefox-primer-button-wrapper');
|
|
594
|
+
const styleEl = document.createElement('style');
|
|
595
|
+
document.head.appendChild(styleEl);
|
|
596
|
+
const sheet = styleEl.sheet;
|
|
597
|
+
if (sheet) {
|
|
598
|
+
sheet.insertRule(`
|
|
599
|
+
.${wrapper.className} {
|
|
600
|
+
width: 100% !important;
|
|
601
|
+
}
|
|
602
|
+
`);
|
|
603
|
+
sheet.insertRule(`
|
|
604
|
+
.${wrapper.className} button {
|
|
605
|
+
height: 54px !important;
|
|
606
|
+
border-radius: 28px !important;
|
|
607
|
+
}
|
|
608
|
+
`);
|
|
609
|
+
}
|
|
610
|
+
htmlNode.appendChild(wrapper);
|
|
611
|
+
/* end hack */
|
|
612
|
+
button = pmManager.createButton();
|
|
613
|
+
await button.render(wrapper, {});
|
|
614
|
+
this.destroyCallbacks.push(() => button.clean());
|
|
615
|
+
onMethodRender(allowedPaymentMethod);
|
|
616
|
+
return {
|
|
617
|
+
setDisabled: (disabled) => {
|
|
618
|
+
button.setDisabled(disabled);
|
|
619
|
+
},
|
|
620
|
+
destroy: () => {
|
|
621
|
+
styleEl.remove();
|
|
622
|
+
button.clean();
|
|
623
|
+
},
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
catch (error) {
|
|
627
|
+
onMethodRenderError(allowedPaymentMethod);
|
|
628
|
+
throw new PrimerError('Failed to initialize Primer checkout', error);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
async initMethod(method, htmlNode, options) {
|
|
632
|
+
try {
|
|
633
|
+
if (method === exports.PaymentMethod.PAYMENT_CARD) {
|
|
634
|
+
if (!options.cardElements ||
|
|
635
|
+
!options.onSubmit ||
|
|
636
|
+
!options.onInputChange) {
|
|
637
|
+
throw new PrimerError('Card elements, onSubmit, and onInputChange are required for PAYMENT_CARD method');
|
|
638
|
+
}
|
|
639
|
+
const cardInterface = await this.renderCardCheckoutWithElements(options.cardElements, {
|
|
640
|
+
onSubmit: options.onSubmit,
|
|
641
|
+
onInputChange: options.onInputChange,
|
|
642
|
+
onMethodRenderError: options.onMethodRenderError,
|
|
643
|
+
onMethodRender: options.onMethodRender,
|
|
644
|
+
});
|
|
645
|
+
this.paymentMethodsInterfaces.push(cardInterface);
|
|
646
|
+
return cardInterface;
|
|
647
|
+
}
|
|
648
|
+
else {
|
|
649
|
+
const buttonInterface = await this.renderButton(method, {
|
|
650
|
+
htmlNode,
|
|
651
|
+
onMethodRenderError: options.onMethodRenderError,
|
|
652
|
+
onMethodRender: options.onMethodRender,
|
|
653
|
+
});
|
|
654
|
+
this.paymentMethodsInterfaces.push(buttonInterface);
|
|
655
|
+
return buttonInterface;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
catch (error) {
|
|
659
|
+
throw new PrimerError('Failed to initialize Primer checkout', error);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
async renderCardCheckoutWithElements(elements, { onSubmit, onInputChange, onMethodRenderError, onMethodRender, }) {
|
|
663
|
+
try {
|
|
664
|
+
if (!this.currentHeadless) {
|
|
665
|
+
throw new PrimerError('Headless checkout not found');
|
|
666
|
+
}
|
|
667
|
+
const headless = await this.currentHeadless;
|
|
668
|
+
const pmManager = await headless.createPaymentMethodManager('PAYMENT_CARD');
|
|
669
|
+
if (!pmManager) {
|
|
670
|
+
throw new Error('Payment method manager is not available');
|
|
671
|
+
}
|
|
672
|
+
const { cardNumberInput, expiryInput, cvvInput } = pmManager.createHostedInputs();
|
|
673
|
+
const validateForm = async () => {
|
|
674
|
+
if (!pmManager)
|
|
675
|
+
return false;
|
|
676
|
+
const { valid, validationErrors } = await pmManager.validate();
|
|
677
|
+
const cardHolderError = validationErrors.find(v => v.name === 'cardholderName');
|
|
678
|
+
dispatchError('cardholderName', cardHolderError?.message || null);
|
|
679
|
+
return valid;
|
|
680
|
+
};
|
|
681
|
+
const dispatchError = (inputName, error) => {
|
|
682
|
+
onInputChange(inputName, error);
|
|
683
|
+
};
|
|
684
|
+
const onHostedInputChange = (name) => (event) => {
|
|
685
|
+
const input = event;
|
|
686
|
+
if (input.submitted) {
|
|
687
|
+
dispatchError(name, input.error);
|
|
688
|
+
}
|
|
689
|
+
};
|
|
690
|
+
const cardHolderOnChange = async (e) => {
|
|
691
|
+
pmManager.setCardholderName(e.target.value);
|
|
692
|
+
dispatchError('cardholderName', null);
|
|
693
|
+
};
|
|
694
|
+
elements.cardholderName?.addEventListener('input', cardHolderOnChange);
|
|
695
|
+
cardNumberInput.addEventListener('change', onHostedInputChange('cardNumber'));
|
|
696
|
+
expiryInput.addEventListener('change', onHostedInputChange('expiryDate'));
|
|
697
|
+
cvvInput.addEventListener('change', onHostedInputChange('cvv'));
|
|
698
|
+
const onSubmitHandler = async () => {
|
|
699
|
+
if (!(await validateForm())) {
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
try {
|
|
703
|
+
onSubmit(true);
|
|
704
|
+
await pmManager.submit();
|
|
705
|
+
}
|
|
706
|
+
catch (error) {
|
|
707
|
+
const primerError = new PrimerError('Failed to submit payment', error);
|
|
708
|
+
throw primerError;
|
|
709
|
+
}
|
|
710
|
+
finally {
|
|
711
|
+
onSubmit(false);
|
|
712
|
+
}
|
|
713
|
+
};
|
|
714
|
+
elements.button?.addEventListener('click', onSubmitHandler);
|
|
715
|
+
await Promise.all([
|
|
716
|
+
cardNumberInput.render(elements.cardNumber, {
|
|
717
|
+
placeholder: '1234 1234 1234 1234',
|
|
718
|
+
ariaLabel: 'Card number',
|
|
719
|
+
style: inputStyle,
|
|
720
|
+
}),
|
|
721
|
+
expiryInput.render(elements.expiryDate, {
|
|
722
|
+
placeholder: 'MM/YY',
|
|
723
|
+
ariaLabel: 'Expiry date',
|
|
724
|
+
style: inputStyle,
|
|
725
|
+
}),
|
|
726
|
+
cvvInput.render(elements.cvv, {
|
|
727
|
+
placeholder: '123',
|
|
728
|
+
ariaLabel: 'CVV',
|
|
729
|
+
style: inputStyle,
|
|
730
|
+
}),
|
|
731
|
+
]);
|
|
732
|
+
const onDestroy = () => {
|
|
733
|
+
pmManager.removeHostedInputs();
|
|
734
|
+
elements.cardholderName?.removeEventListener('change', cardHolderOnChange);
|
|
735
|
+
elements.button?.removeEventListener('click', onSubmitHandler);
|
|
736
|
+
};
|
|
737
|
+
this.destroyCallbacks.push(onDestroy);
|
|
738
|
+
onMethodRender(exports.PaymentMethod.PAYMENT_CARD);
|
|
739
|
+
return {
|
|
740
|
+
setDisabled: (disabled) => {
|
|
741
|
+
cardNumberInput.setDisabled(disabled);
|
|
742
|
+
expiryInput.setDisabled(disabled);
|
|
743
|
+
cvvInput.setDisabled(disabled);
|
|
744
|
+
if (elements.button) {
|
|
745
|
+
elements.button.disabled = disabled;
|
|
746
|
+
}
|
|
747
|
+
if (elements.cardholderName) {
|
|
748
|
+
elements.cardholderName.disabled = disabled;
|
|
749
|
+
}
|
|
750
|
+
},
|
|
751
|
+
submit: () => onSubmitHandler(),
|
|
752
|
+
destroy: () => {
|
|
753
|
+
this.destroyCallbacks = this.destroyCallbacks.filter(callback => callback !== onDestroy);
|
|
754
|
+
onDestroy();
|
|
755
|
+
},
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
catch (error) {
|
|
759
|
+
onMethodRenderError(exports.PaymentMethod.PAYMENT_CARD);
|
|
760
|
+
throw new PrimerError('Failed to initialize Primer checkout', error);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
async initializeHeadlessCheckout(clientToken, primerOptions) {
|
|
764
|
+
await this.createHeadlessCheckout(clientToken, {
|
|
765
|
+
...primerOptions,
|
|
766
|
+
onTokenizeSuccess: this.wrapTokenizeHandler(primerOptions.onTokenizeSuccess),
|
|
767
|
+
onResumeSuccess: this.wrapResumeHandler(primerOptions.onResumeSuccess),
|
|
768
|
+
onAvailablePaymentMethodsLoad: (items) => {
|
|
769
|
+
let isApplePayAvailable = false;
|
|
770
|
+
this.availableMethods = ALLOWED_PAYMENT_METHODS.filter(method => {
|
|
771
|
+
return items.some((item) => {
|
|
772
|
+
if (item.type === exports.PaymentMethod.APPLE_PAY) {
|
|
773
|
+
isApplePayAvailable = true;
|
|
774
|
+
}
|
|
775
|
+
return item.type === method;
|
|
776
|
+
});
|
|
777
|
+
});
|
|
778
|
+
if (isApplePayAvailable) {
|
|
779
|
+
this.availableMethods = this.availableMethods.filter(method => method !== exports.PaymentMethod.GOOGLE_PAY);
|
|
780
|
+
}
|
|
781
|
+
if (this.availableMethods.length === 0) {
|
|
782
|
+
throw new PrimerError('No allowed payment methods found');
|
|
783
|
+
}
|
|
784
|
+
},
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
async renderCheckout(clientToken, checkoutOptions, checkoutRenderOptions) {
|
|
788
|
+
const { cardElements, paymentButtonElements, container, onSubmit, onInputChange, onMethodRender, onMethodRenderError, onMethodsAvailable, } = checkoutRenderOptions;
|
|
789
|
+
await this.initializeHeadlessCheckout(clientToken, checkoutOptions);
|
|
790
|
+
onMethodsAvailable?.(this.availableMethods);
|
|
791
|
+
await Promise.all(this.availableMethods.map(method => {
|
|
792
|
+
if (method === exports.PaymentMethod.PAYMENT_CARD) {
|
|
793
|
+
// For card, use the main container
|
|
794
|
+
return this.initMethod(method, container, {
|
|
795
|
+
cardElements,
|
|
796
|
+
onSubmit,
|
|
797
|
+
onInputChange,
|
|
798
|
+
onMethodRender,
|
|
799
|
+
onMethodRenderError,
|
|
46
800
|
});
|
|
47
|
-
return;
|
|
48
801
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
802
|
+
else {
|
|
803
|
+
const buttonElementsMap = {
|
|
804
|
+
[exports.PaymentMethod.PAYPAL]: paymentButtonElements.paypal,
|
|
805
|
+
[exports.PaymentMethod.GOOGLE_PAY]: paymentButtonElements.googlePay,
|
|
806
|
+
[exports.PaymentMethod.APPLE_PAY]: paymentButtonElements.applePay,
|
|
807
|
+
};
|
|
808
|
+
// For buttons, use the specific button container element
|
|
809
|
+
const buttonElement = buttonElementsMap[method];
|
|
810
|
+
return this.initMethod(method, buttonElement, {
|
|
811
|
+
onMethodRender,
|
|
812
|
+
onMethodRenderError,
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
}));
|
|
816
|
+
this.isInitialized = true;
|
|
817
|
+
}
|
|
818
|
+
wrapTokenizeHandler(handler) {
|
|
819
|
+
return async (paymentMethodTokenData, primerHandler) => {
|
|
820
|
+
try {
|
|
821
|
+
await handler(paymentMethodTokenData, primerHandler);
|
|
53
822
|
}
|
|
54
|
-
|
|
55
|
-
|
|
823
|
+
catch (error) {
|
|
824
|
+
// eslint-disable-next-line no-console
|
|
825
|
+
console.error('Error in tokenize handler:', error);
|
|
826
|
+
primerHandler.handleFailure('Payment processing failed. Please try again.');
|
|
56
827
|
}
|
|
57
828
|
};
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
829
|
+
}
|
|
830
|
+
wrapResumeHandler(handler) {
|
|
831
|
+
return async (resumeTokenData, primerHandler) => {
|
|
832
|
+
try {
|
|
833
|
+
await handler(resumeTokenData, primerHandler);
|
|
62
834
|
}
|
|
63
|
-
|
|
64
|
-
|
|
835
|
+
catch (error) {
|
|
836
|
+
// eslint-disable-next-line no-console
|
|
837
|
+
console.error('Error in resume handler:', error);
|
|
838
|
+
primerHandler.handleFailure('Payment processing failed. Please try again.');
|
|
65
839
|
}
|
|
66
|
-
|
|
67
|
-
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
async destroy() {
|
|
843
|
+
if (this.currentHeadless) {
|
|
844
|
+
PrimerWrapper.headlessManager.remove(this.currentHeadless);
|
|
845
|
+
this.currentHeadless = null;
|
|
846
|
+
}
|
|
847
|
+
if (this.destroyCallbacks) {
|
|
848
|
+
try {
|
|
849
|
+
Promise.all(this.destroyCallbacks.map(destroy => destroy()));
|
|
850
|
+
}
|
|
851
|
+
catch (error) {
|
|
852
|
+
// eslint-disable-next-line no-console
|
|
853
|
+
console.warn('Error destroying Primer checkout:', error);
|
|
68
854
|
}
|
|
855
|
+
}
|
|
856
|
+
this.destroyCallbacks = [];
|
|
857
|
+
this.isInitialized = false;
|
|
858
|
+
}
|
|
859
|
+
createHandlers(handlers) {
|
|
860
|
+
return {
|
|
861
|
+
handleSuccess: () => {
|
|
862
|
+
if (handlers.onSuccess)
|
|
863
|
+
handlers.onSuccess();
|
|
864
|
+
},
|
|
865
|
+
handleFailure: (message) => {
|
|
866
|
+
if (handlers.onError)
|
|
867
|
+
handlers.onError(new Error(message));
|
|
868
|
+
},
|
|
869
|
+
continueWithNewClientToken: (newClientToken) => {
|
|
870
|
+
if (handlers.onActionRequired)
|
|
871
|
+
handlers.onActionRequired(newClientToken);
|
|
872
|
+
},
|
|
69
873
|
};
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
874
|
+
}
|
|
875
|
+
getCurrentCheckout() {
|
|
876
|
+
return this.destroyCallbacks;
|
|
877
|
+
}
|
|
878
|
+
isActive() {
|
|
879
|
+
return this.isInitialized && this.destroyCallbacks.length > 0;
|
|
880
|
+
}
|
|
881
|
+
validateContainer(selector) {
|
|
882
|
+
const element = document.querySelector(selector);
|
|
883
|
+
if (!element) {
|
|
884
|
+
throw new PrimerError(`Checkout container not found: ${selector}`);
|
|
885
|
+
}
|
|
886
|
+
const computedStyle = window.getComputedStyle(element);
|
|
887
|
+
if (computedStyle.display === 'none') {
|
|
888
|
+
// eslint-disable-next-line no-console
|
|
889
|
+
console.warn('Checkout container is hidden, this may cause display issues');
|
|
890
|
+
}
|
|
891
|
+
return element;
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
PrimerWrapper.headlessManager = new HeadlessManager();
|
|
895
|
+
|
|
896
|
+
/**
|
|
897
|
+
* @fileoverview Input validation utilities for Funnefox SDK
|
|
898
|
+
*/
|
|
899
|
+
function sanitizeString(input) {
|
|
900
|
+
return input?.trim() || '';
|
|
901
|
+
}
|
|
902
|
+
function requireString(value, fieldName) {
|
|
903
|
+
const sanitized = sanitizeString(value);
|
|
904
|
+
if (sanitized.length === 0) {
|
|
905
|
+
throw new ValidationError(fieldName, 'must be a non-empty string', value);
|
|
906
|
+
}
|
|
907
|
+
return true;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
/**
|
|
911
|
+
* @fileoverview API client for Funnefox backend integration
|
|
912
|
+
*/
|
|
913
|
+
class APIClient {
|
|
914
|
+
constructor(config) {
|
|
915
|
+
this.baseUrl = config.baseUrl.replace(/\/$/, '');
|
|
916
|
+
this.orgId = config.orgId;
|
|
917
|
+
this.timeout = config.timeout || 30000;
|
|
918
|
+
this.retryAttempts = config.retryAttempts || 3;
|
|
919
|
+
}
|
|
920
|
+
async request(endpoint, options = {}) {
|
|
921
|
+
const url = `${this.baseUrl}/${this.orgId}${endpoint}`;
|
|
922
|
+
const requestOptions = {
|
|
923
|
+
method: 'GET',
|
|
924
|
+
headers: {
|
|
925
|
+
'Content-Type': 'application/json',
|
|
926
|
+
'X-SDK-Version': SDK_VERSION,
|
|
927
|
+
...(options.headers || {}),
|
|
928
|
+
},
|
|
929
|
+
...options,
|
|
77
930
|
};
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
this.
|
|
931
|
+
try {
|
|
932
|
+
return await retry(async () => {
|
|
933
|
+
return await withTimeout(this._makeRequest(url, requestOptions), this.timeout, 'Request timed out');
|
|
934
|
+
}, this.retryAttempts);
|
|
935
|
+
}
|
|
936
|
+
catch (error) {
|
|
937
|
+
if (error instanceof Error && error.name === 'APIError') {
|
|
938
|
+
throw error;
|
|
939
|
+
}
|
|
940
|
+
throw new NetworkError('Network request failed', error);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
async _makeRequest(url, options) {
|
|
944
|
+
let response;
|
|
945
|
+
try {
|
|
946
|
+
response = await fetch(url, options);
|
|
947
|
+
}
|
|
948
|
+
catch (error) {
|
|
949
|
+
if (error instanceof Error && error.name === 'NetworkError') {
|
|
950
|
+
throw error;
|
|
81
951
|
}
|
|
952
|
+
throw new NetworkError('Network request failed', error);
|
|
953
|
+
}
|
|
954
|
+
let data;
|
|
955
|
+
try {
|
|
956
|
+
data = await response.json();
|
|
957
|
+
}
|
|
958
|
+
catch {
|
|
959
|
+
throw new APIError('Invalid JSON response', response.status, {});
|
|
960
|
+
}
|
|
961
|
+
if (!response.ok) {
|
|
962
|
+
const d = data;
|
|
963
|
+
const message = d.error?.[0]?.msg || 'Failed to create payment';
|
|
964
|
+
throw new APIError(message, response.status, {
|
|
965
|
+
response: data,
|
|
966
|
+
});
|
|
967
|
+
}
|
|
968
|
+
return data;
|
|
969
|
+
}
|
|
970
|
+
async createClientSession(params) {
|
|
971
|
+
const payload = {
|
|
972
|
+
region: params.region || 'default',
|
|
973
|
+
integration_type: 'primer',
|
|
974
|
+
pp_ident: params.priceId,
|
|
975
|
+
external_id: params.externalId,
|
|
976
|
+
email_address: params.email,
|
|
977
|
+
client_metadata: params.clientMetadata || {},
|
|
82
978
|
};
|
|
83
|
-
|
|
84
|
-
|
|
979
|
+
if (params.countryCode !== undefined) {
|
|
980
|
+
payload.country_code = params.countryCode;
|
|
981
|
+
}
|
|
982
|
+
return (await this.request(API_ENDPOINTS.CREATE_CLIENT_SESSION, {
|
|
983
|
+
method: 'POST',
|
|
984
|
+
body: JSON.stringify(payload),
|
|
985
|
+
}));
|
|
986
|
+
}
|
|
987
|
+
async updateClientSession(params) {
|
|
988
|
+
const payload = {
|
|
989
|
+
order_id: params.orderId,
|
|
990
|
+
client_token: params.clientToken,
|
|
991
|
+
pp_ident: params.priceId,
|
|
992
|
+
client_metadata: params.clientMetadata || {},
|
|
993
|
+
};
|
|
994
|
+
return await this.request(API_ENDPOINTS.UPDATE_CLIENT_SESSION, {
|
|
995
|
+
method: 'POST',
|
|
996
|
+
body: JSON.stringify(payload),
|
|
997
|
+
});
|
|
998
|
+
}
|
|
999
|
+
async createPayment(params) {
|
|
1000
|
+
const payload = {
|
|
1001
|
+
order_id: params.orderId,
|
|
1002
|
+
payment_method_token: params.paymentMethodToken,
|
|
1003
|
+
client_metadata: params.clientMetadata || {},
|
|
85
1004
|
};
|
|
86
|
-
this.
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
1005
|
+
return (await this.request(API_ENDPOINTS.CREATE_PAYMENT, {
|
|
1006
|
+
method: 'POST',
|
|
1007
|
+
body: JSON.stringify(payload),
|
|
1008
|
+
}));
|
|
1009
|
+
}
|
|
1010
|
+
async resumePayment(params) {
|
|
1011
|
+
const payload = {
|
|
1012
|
+
order_id: params.orderId,
|
|
1013
|
+
resume_token: params.resumeToken,
|
|
1014
|
+
};
|
|
1015
|
+
return (await this.request(API_ENDPOINTS.RESUME_PAYMENT, {
|
|
1016
|
+
method: 'POST',
|
|
1017
|
+
body: JSON.stringify(payload),
|
|
1018
|
+
}));
|
|
1019
|
+
}
|
|
1020
|
+
processSessionResponse(response) {
|
|
1021
|
+
if (response.status === 'error') {
|
|
1022
|
+
const firstError = response.error?.[0];
|
|
1023
|
+
const message = firstError?.msg || 'Session creation failed';
|
|
1024
|
+
throw new APIError(message, null, {
|
|
1025
|
+
errorCode: firstError?.code,
|
|
1026
|
+
errorType: firstError?.type,
|
|
1027
|
+
requestId: response.req_id,
|
|
1028
|
+
response,
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
const data = response.data;
|
|
1032
|
+
return {
|
|
1033
|
+
type: 'session_created',
|
|
1034
|
+
orderId: data.order_id,
|
|
1035
|
+
clientToken: data.client_token,
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
processPaymentResponse(response) {
|
|
1039
|
+
if (response.status === 'error') {
|
|
1040
|
+
const firstError = response.error?.[0];
|
|
1041
|
+
const message = firstError?.msg || 'Payment request failed';
|
|
1042
|
+
throw new APIError(message, null, {
|
|
1043
|
+
errorCode: firstError?.code,
|
|
1044
|
+
errorType: firstError?.type,
|
|
1045
|
+
response,
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
1048
|
+
const data = response.data;
|
|
1049
|
+
if (data.action_required_token) {
|
|
1050
|
+
return {
|
|
1051
|
+
type: 'action_required',
|
|
1052
|
+
orderId: data.order_id,
|
|
1053
|
+
clientToken: data.action_required_token,
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
if (data.checkout_status) {
|
|
1057
|
+
switch (data.checkout_status) {
|
|
1058
|
+
case 'succeeded':
|
|
1059
|
+
return {
|
|
1060
|
+
type: 'success',
|
|
1061
|
+
orderId: data.order_id,
|
|
1062
|
+
status: 'succeeded',
|
|
1063
|
+
};
|
|
1064
|
+
case 'failed':
|
|
1065
|
+
throw new APIError(data.failed_message_for_user || 'Payment failed', null, { response });
|
|
1066
|
+
case 'cancelled':
|
|
1067
|
+
throw new APIError('Payment was cancelled by user', null, {
|
|
1068
|
+
response,
|
|
1069
|
+
});
|
|
1070
|
+
case 'processing':
|
|
1071
|
+
return {
|
|
1072
|
+
type: 'processing',
|
|
1073
|
+
orderId: data.order_id,
|
|
1074
|
+
status: 'processing',
|
|
1075
|
+
};
|
|
1076
|
+
default:
|
|
1077
|
+
throw new APIError(`Unhandled checkout status: ${data.checkout_status}`, null, { response });
|
|
91
1078
|
}
|
|
92
|
-
|
|
93
|
-
|
|
1079
|
+
}
|
|
1080
|
+
throw new APIError('Invalid payment response format', null, { response });
|
|
1081
|
+
}
|
|
1082
|
+
async oneClick(payload) {
|
|
1083
|
+
return (await this.request(`/billing/${this.orgId}/v1/checkout/one_click`, {
|
|
1084
|
+
method: 'POST',
|
|
1085
|
+
body: JSON.stringify(payload),
|
|
1086
|
+
}));
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
var loaderHtml = "<div class=\"ff-sdk-loader-container\">\n <div class=\"ff-sdk-loader\"></div>\n</div>\n";
|
|
1091
|
+
|
|
1092
|
+
if(typeof document!=="undefined")document.head.appendChild(document.createElement("style")).textContent=".ff-sdk-loader-container {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n display: flex;\n justify-content: center;\n align-items: center;\n background-color: rgba(255, 255, 255);\n z-index: 2;\n}\n\n.ff-sdk-loader {\n width: 24px;\n height: 24px;\n border: 4px solid #e32f41;\n border-top: 4px solid transparent;\n border-radius: 50%;\n animation: spin 1s linear infinite;\n}\n\n@keyframes spin {\n 0% {\n transform: rotate(0deg);\n }\n 100% {\n transform: rotate(360deg);\n }\n }";
|
|
1093
|
+
|
|
1094
|
+
const loaderConfig = {
|
|
1095
|
+
html: loaderHtml,
|
|
1096
|
+
selectors: {
|
|
1097
|
+
loaderContainer: '.ff-sdk-loader-container'},
|
|
1098
|
+
};
|
|
1099
|
+
const renderLoader = (container) => {
|
|
1100
|
+
const loaderContainer = document.querySelector(container);
|
|
1101
|
+
if (loaderContainer) {
|
|
1102
|
+
loaderContainer.innerHTML = loaderConfig.html;
|
|
1103
|
+
}
|
|
1104
|
+
};
|
|
1105
|
+
const hideLoader = () => {
|
|
1106
|
+
const loaderContainer = document.querySelector(loaderConfig.selectors.loaderContainer);
|
|
1107
|
+
if (loaderContainer) {
|
|
1108
|
+
loaderContainer.remove();
|
|
1109
|
+
}
|
|
1110
|
+
};
|
|
1111
|
+
|
|
1112
|
+
var V3_URL = 'https://js.stripe.com/v3';
|
|
1113
|
+
var V3_URL_REGEX = /^https:\/\/js\.stripe\.com\/v3\/?(\?.*)?$/;
|
|
1114
|
+
var EXISTING_SCRIPT_MESSAGE = 'loadStripe.setLoadParameters was called but an existing Stripe.js script already exists in the document; existing script parameters will be used';
|
|
1115
|
+
var findScript = function findScript() {
|
|
1116
|
+
var scripts = document.querySelectorAll("script[src^=\"".concat(V3_URL, "\"]"));
|
|
1117
|
+
|
|
1118
|
+
for (var i = 0; i < scripts.length; i++) {
|
|
1119
|
+
var script = scripts[i];
|
|
1120
|
+
|
|
1121
|
+
if (!V3_URL_REGEX.test(script.src)) {
|
|
1122
|
+
continue;
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
return script;
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
return null;
|
|
1129
|
+
};
|
|
1130
|
+
|
|
1131
|
+
var injectScript = function injectScript(params) {
|
|
1132
|
+
var queryString = '';
|
|
1133
|
+
var script = document.createElement('script');
|
|
1134
|
+
script.src = "".concat(V3_URL).concat(queryString);
|
|
1135
|
+
var headOrBody = document.head || document.body;
|
|
1136
|
+
|
|
1137
|
+
if (!headOrBody) {
|
|
1138
|
+
throw new Error('Expected document.body not to be null. Stripe.js requires a <body> element.');
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
headOrBody.appendChild(script);
|
|
1142
|
+
return script;
|
|
1143
|
+
};
|
|
1144
|
+
|
|
1145
|
+
var registerWrapper = function registerWrapper(stripe, startTime) {
|
|
1146
|
+
if (!stripe || !stripe._registerWrapper) {
|
|
1147
|
+
return;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
stripe._registerWrapper({
|
|
1151
|
+
name: 'stripe-js',
|
|
1152
|
+
version: "4.6.0",
|
|
1153
|
+
startTime: startTime
|
|
1154
|
+
});
|
|
1155
|
+
};
|
|
1156
|
+
|
|
1157
|
+
var stripePromise = null;
|
|
1158
|
+
var onErrorListener = null;
|
|
1159
|
+
var onLoadListener = null;
|
|
1160
|
+
|
|
1161
|
+
var onError = function onError(reject) {
|
|
1162
|
+
return function () {
|
|
1163
|
+
reject(new Error('Failed to load Stripe.js'));
|
|
1164
|
+
};
|
|
1165
|
+
};
|
|
1166
|
+
|
|
1167
|
+
var onLoad = function onLoad(resolve, reject) {
|
|
1168
|
+
return function () {
|
|
1169
|
+
if (window.Stripe) {
|
|
1170
|
+
resolve(window.Stripe);
|
|
1171
|
+
} else {
|
|
1172
|
+
reject(new Error('Stripe.js not available'));
|
|
1173
|
+
}
|
|
1174
|
+
};
|
|
1175
|
+
};
|
|
1176
|
+
|
|
1177
|
+
var loadScript = function loadScript(params) {
|
|
1178
|
+
// Ensure that we only attempt to load Stripe.js at most once
|
|
1179
|
+
if (stripePromise !== null) {
|
|
1180
|
+
return stripePromise;
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
stripePromise = new Promise(function (resolve, reject) {
|
|
1184
|
+
if (typeof window === 'undefined' || typeof document === 'undefined') {
|
|
1185
|
+
// Resolve to null when imported server side. This makes the module
|
|
1186
|
+
// safe to import in an isomorphic code base.
|
|
1187
|
+
resolve(null);
|
|
1188
|
+
return;
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
if (window.Stripe) {
|
|
1192
|
+
resolve(window.Stripe);
|
|
1193
|
+
return;
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
try {
|
|
1197
|
+
var script = findScript();
|
|
1198
|
+
|
|
1199
|
+
if (script && params) ; else if (!script) {
|
|
1200
|
+
script = injectScript(params);
|
|
1201
|
+
} else if (script && onLoadListener !== null && onErrorListener !== null) {
|
|
1202
|
+
var _script$parentNode;
|
|
1203
|
+
|
|
1204
|
+
// remove event listeners
|
|
1205
|
+
script.removeEventListener('load', onLoadListener);
|
|
1206
|
+
script.removeEventListener('error', onErrorListener); // if script exists, but we are reloading due to an error,
|
|
1207
|
+
// reload script to trigger 'load' event
|
|
1208
|
+
|
|
1209
|
+
(_script$parentNode = script.parentNode) === null || _script$parentNode === void 0 ? void 0 : _script$parentNode.removeChild(script);
|
|
1210
|
+
script = injectScript(params);
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
onLoadListener = onLoad(resolve, reject);
|
|
1214
|
+
onErrorListener = onError(reject);
|
|
1215
|
+
script.addEventListener('load', onLoadListener);
|
|
1216
|
+
script.addEventListener('error', onErrorListener);
|
|
1217
|
+
} catch (error) {
|
|
1218
|
+
reject(error);
|
|
1219
|
+
return;
|
|
1220
|
+
}
|
|
1221
|
+
}); // Resets stripePromise on error
|
|
1222
|
+
|
|
1223
|
+
return stripePromise["catch"](function (error) {
|
|
1224
|
+
stripePromise = null;
|
|
1225
|
+
return Promise.reject(error);
|
|
1226
|
+
});
|
|
1227
|
+
};
|
|
1228
|
+
var initStripe = function initStripe(maybeStripe, args, startTime) {
|
|
1229
|
+
if (maybeStripe === null) {
|
|
1230
|
+
return null;
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
var stripe = maybeStripe.apply(undefined, args);
|
|
1234
|
+
registerWrapper(stripe, startTime);
|
|
1235
|
+
return stripe;
|
|
1236
|
+
}; // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
|
1237
|
+
|
|
1238
|
+
var stripePromise$1;
|
|
1239
|
+
var loadCalled = false;
|
|
1240
|
+
|
|
1241
|
+
var getStripePromise = function getStripePromise() {
|
|
1242
|
+
if (stripePromise$1) {
|
|
1243
|
+
return stripePromise$1;
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
stripePromise$1 = loadScript(null)["catch"](function (error) {
|
|
1247
|
+
// clear cache on error
|
|
1248
|
+
stripePromise$1 = null;
|
|
1249
|
+
return Promise.reject(error);
|
|
1250
|
+
});
|
|
1251
|
+
return stripePromise$1;
|
|
1252
|
+
}; // Execute our own script injection after a tick to give users time to do their
|
|
1253
|
+
// own script injection.
|
|
1254
|
+
|
|
1255
|
+
|
|
1256
|
+
Promise.resolve().then(function () {
|
|
1257
|
+
return getStripePromise();
|
|
1258
|
+
})["catch"](function (error) {
|
|
1259
|
+
if (!loadCalled) {
|
|
1260
|
+
console.warn(error);
|
|
1261
|
+
}
|
|
1262
|
+
});
|
|
1263
|
+
var loadStripe = function loadStripe() {
|
|
1264
|
+
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
|
|
1265
|
+
args[_key] = arguments[_key];
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
loadCalled = true;
|
|
1269
|
+
var startTime = Date.now(); // if previous attempts are unsuccessful, will re-load script
|
|
1270
|
+
|
|
1271
|
+
return getStripePromise().then(function (maybeStripe) {
|
|
1272
|
+
return initStripe(maybeStripe, args, startTime);
|
|
1273
|
+
});
|
|
1274
|
+
};
|
|
1275
|
+
|
|
1276
|
+
var errorHtml = "<div class=\"ff-sdk-error-container\">\n <p class=\"ff-sdk-error-message\">The payment form didn’t load correctly.</p>\n <p>Please refresh the page to try again.</p>\n <p>If the problem persists, please contact support.</p>\n <p class=\"ff-sdk-error-request-id\"></p>\n</div>\n";
|
|
1277
|
+
|
|
1278
|
+
if(typeof document!=="undefined")document.head.appendChild(document.createElement("style")).textContent=".ff-sdk-error-container {\n background-color: #d1000033;\n color: #d10000;\n font-size: 14px;\n padding: 16px 12px;\n border-radius: 8px;\n}";
|
|
1279
|
+
|
|
1280
|
+
const errorConfig = {
|
|
1281
|
+
html: errorHtml,
|
|
1282
|
+
selectors: {
|
|
1283
|
+
errorRequestId: '.ff-sdk-error-request-id',
|
|
1284
|
+
},
|
|
1285
|
+
};
|
|
1286
|
+
const renderError = (container, reqId) => {
|
|
1287
|
+
const errorContainer = document.querySelector(container);
|
|
1288
|
+
if (errorContainer) {
|
|
1289
|
+
errorContainer.innerHTML = errorConfig.html;
|
|
1290
|
+
if (reqId) {
|
|
1291
|
+
const errorRequestId = errorContainer.querySelector(errorConfig.selectors.errorRequestId);
|
|
1292
|
+
if (errorRequestId) {
|
|
1293
|
+
errorRequestId.textContent = `Request ID: ${reqId}`;
|
|
94
1294
|
}
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
};
|
|
1298
|
+
|
|
1299
|
+
/**
|
|
1300
|
+
* @fileoverview Checkout instance manager for Funnefox SDK
|
|
1301
|
+
*/
|
|
1302
|
+
class CheckoutInstance extends EventEmitter {
|
|
1303
|
+
constructor(config) {
|
|
1304
|
+
super();
|
|
1305
|
+
this.counter = 0;
|
|
1306
|
+
this.radarSessionId = null;
|
|
1307
|
+
this.handleInputChange = (inputName, error) => {
|
|
1308
|
+
this.emit(EVENTS.INPUT_ERROR, { name: inputName, error });
|
|
1309
|
+
};
|
|
1310
|
+
this.handleMethodRender = (method) => {
|
|
1311
|
+
this.emit(EVENTS.METHOD_RENDER, method);
|
|
95
1312
|
};
|
|
96
|
-
this.
|
|
97
|
-
this.
|
|
98
|
-
this.initAccordion();
|
|
99
|
-
methods.forEach(this.onMethodRender);
|
|
1313
|
+
this.handleMethodRenderError = (method) => {
|
|
1314
|
+
this.emit(EVENTS.METHOD_RENDER_ERROR, method);
|
|
100
1315
|
};
|
|
101
|
-
this.
|
|
102
|
-
this.
|
|
1316
|
+
this.handleSubmit = (isSubmitting) => {
|
|
1317
|
+
this.onLoaderChangeWithRace(isSubmitting);
|
|
1318
|
+
this._setState(isSubmitting ? 'processing' : 'ready');
|
|
1319
|
+
};
|
|
1320
|
+
this.handleTokenizeSuccess = async (paymentMethodTokenData, primerHandler) => {
|
|
1321
|
+
try {
|
|
1322
|
+
this.onLoaderChangeWithRace(true);
|
|
1323
|
+
this._setState('processing');
|
|
1324
|
+
const radarSessionId = await this.radarSessionId;
|
|
1325
|
+
const paymentResponse = await this.apiClient.createPayment({
|
|
1326
|
+
orderId: this.orderId,
|
|
1327
|
+
paymentMethodToken: paymentMethodTokenData.token,
|
|
1328
|
+
clientMetadata: {
|
|
1329
|
+
radarSessionId,
|
|
1330
|
+
},
|
|
1331
|
+
});
|
|
1332
|
+
const result = this.apiClient.processPaymentResponse(paymentResponse);
|
|
1333
|
+
await this._processPaymentResult(result, primerHandler);
|
|
1334
|
+
}
|
|
1335
|
+
catch (error) {
|
|
1336
|
+
this._setState('error');
|
|
1337
|
+
this.emit(EVENTS.PURCHASE_FAILURE, new Error(error.message || 'Payment processing failed'));
|
|
1338
|
+
primerHandler.handleFailure(error.message || 'Payment processing failed');
|
|
1339
|
+
}
|
|
1340
|
+
finally {
|
|
1341
|
+
this.onLoaderChangeWithRace(false);
|
|
1342
|
+
this._setState('ready');
|
|
1343
|
+
}
|
|
103
1344
|
};
|
|
104
|
-
this.
|
|
105
|
-
|
|
106
|
-
this.
|
|
1345
|
+
this.handleResumeSuccess = async (resumeTokenData, primerHandler) => {
|
|
1346
|
+
try {
|
|
1347
|
+
this.onLoaderChangeWithRace(true);
|
|
1348
|
+
this._setState('processing');
|
|
1349
|
+
const resumeResponse = await this.apiClient.resumePayment({
|
|
1350
|
+
orderId: this.orderId,
|
|
1351
|
+
resumeToken: resumeTokenData.resumeToken,
|
|
1352
|
+
});
|
|
1353
|
+
const result = this.apiClient.processPaymentResponse(resumeResponse);
|
|
1354
|
+
await this._processPaymentResult(result, primerHandler);
|
|
107
1355
|
}
|
|
108
|
-
|
|
1356
|
+
catch (error) {
|
|
1357
|
+
this._setState('error');
|
|
1358
|
+
this.emit(EVENTS.PURCHASE_FAILURE, new Error(error.message || 'Payment processing failed'));
|
|
1359
|
+
primerHandler.handleFailure(error.message || 'Payment processing failed');
|
|
1360
|
+
}
|
|
1361
|
+
finally {
|
|
1362
|
+
this.emit(EVENTS.PURCHASE_COMPLETED);
|
|
1363
|
+
this.onLoaderChangeWithRace(false);
|
|
1364
|
+
this._setState('ready');
|
|
1365
|
+
}
|
|
1366
|
+
};
|
|
1367
|
+
this.handleMethodsAvailable = (methods) => {
|
|
1368
|
+
this.emit(EVENTS.METHODS_AVAILABLE, methods);
|
|
109
1369
|
};
|
|
110
|
-
this.
|
|
111
|
-
this.
|
|
1370
|
+
this.onLoaderChangeWithRace = (state) => {
|
|
1371
|
+
const isLoading = !!(state ? ++this.counter : --this.counter);
|
|
1372
|
+
this.primerWrapper.disableButtons(isLoading);
|
|
1373
|
+
this.emit(EVENTS.LOADER_CHANGE, isLoading);
|
|
112
1374
|
};
|
|
113
|
-
this.
|
|
114
|
-
this.
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
1375
|
+
this.id = generateId('checkout_');
|
|
1376
|
+
this.orgId = config.orgId;
|
|
1377
|
+
this.baseUrl = config.baseUrl;
|
|
1378
|
+
this.region = config.region;
|
|
1379
|
+
this.checkoutConfig = { ...config.checkoutConfig };
|
|
1380
|
+
this.callbacks = {
|
|
1381
|
+
onSuccess: this.checkoutConfig.onSuccess,
|
|
1382
|
+
onError: this.checkoutConfig.onError,
|
|
1383
|
+
onStatusChange: this.checkoutConfig.onStatusChange,
|
|
1384
|
+
onDestroy: this.checkoutConfig.onDestroy,
|
|
1385
|
+
};
|
|
1386
|
+
delete this.checkoutConfig?.onSuccess;
|
|
1387
|
+
delete this.checkoutConfig?.onError;
|
|
1388
|
+
delete this.checkoutConfig?.onStatusChange;
|
|
1389
|
+
delete this.checkoutConfig?.onDestroy;
|
|
1390
|
+
this.state = 'initializing';
|
|
1391
|
+
this.orderId = null;
|
|
1392
|
+
this.clientToken = null;
|
|
1393
|
+
this.primerWrapper = new PrimerWrapper();
|
|
1394
|
+
this.isDestroyed = false;
|
|
1395
|
+
this._setupCallbackBridges();
|
|
1396
|
+
}
|
|
1397
|
+
_setupCallbackBridges() {
|
|
1398
|
+
if (this.callbacks.onSuccess) {
|
|
1399
|
+
this.on(EVENTS.SUCCESS, this.callbacks.onSuccess);
|
|
1400
|
+
}
|
|
1401
|
+
if (this.callbacks.onError) {
|
|
1402
|
+
this.on(EVENTS.ERROR, this.callbacks.onError);
|
|
1403
|
+
}
|
|
1404
|
+
if (this.callbacks.onStatusChange) {
|
|
1405
|
+
this.on(EVENTS.STATUS_CHANGE, this.callbacks.onStatusChange);
|
|
1406
|
+
}
|
|
1407
|
+
if (this.callbacks.onDestroy) {
|
|
1408
|
+
this.on(EVENTS.DESTROY, this.callbacks.onDestroy);
|
|
118
1409
|
}
|
|
119
|
-
this.containerEl = containerEl;
|
|
120
|
-
this.checkoutConfig = checkoutConfig;
|
|
121
1410
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
1411
|
+
removeAllListeners() {
|
|
1412
|
+
return super.removeAllListeners();
|
|
1413
|
+
}
|
|
1414
|
+
async initialize() {
|
|
1415
|
+
try {
|
|
1416
|
+
this.showInitializingLoader();
|
|
1417
|
+
this._setState('initializing');
|
|
1418
|
+
await this.createSession();
|
|
1419
|
+
await this._initializePrimerCheckout();
|
|
1420
|
+
this._setState('ready');
|
|
1421
|
+
this.checkoutConfig?.onInitialized?.();
|
|
1422
|
+
return this;
|
|
1423
|
+
}
|
|
1424
|
+
catch (error) {
|
|
1425
|
+
this._setState('error');
|
|
1426
|
+
renderError(this.checkoutConfig.container, error?.response?.req_id);
|
|
1427
|
+
this.emit(EVENTS.ERROR, error);
|
|
1428
|
+
throw error;
|
|
1429
|
+
}
|
|
1430
|
+
finally {
|
|
1431
|
+
this.hideInitializingLoader();
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
async createSession() {
|
|
1435
|
+
this.apiClient = new APIClient({
|
|
1436
|
+
baseUrl: this.baseUrl || DEFAULTS.BASE_URL,
|
|
1437
|
+
orgId: this.orgId,
|
|
1438
|
+
timeout: DEFAULTS.REQUEST_TIMEOUT,
|
|
1439
|
+
retryAttempts: DEFAULTS.RETRY_ATTEMPTS,
|
|
1440
|
+
});
|
|
1441
|
+
const sessionParams = {
|
|
1442
|
+
priceId: this.checkoutConfig.priceId,
|
|
1443
|
+
externalId: this.checkoutConfig.customer.externalId,
|
|
1444
|
+
email: this.checkoutConfig.customer.email,
|
|
1445
|
+
region: this.region || DEFAULTS.REGION,
|
|
1446
|
+
clientMetadata: this.checkoutConfig.clientMetadata,
|
|
1447
|
+
countryCode: this.checkoutConfig.customer.countryCode,
|
|
135
1448
|
};
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
1449
|
+
const cacheKey = [
|
|
1450
|
+
this.orgId,
|
|
1451
|
+
this.checkoutConfig.priceId,
|
|
1452
|
+
this.checkoutConfig.customer.externalId,
|
|
1453
|
+
this.checkoutConfig.customer.email,
|
|
1454
|
+
].join('-');
|
|
1455
|
+
let sessionResponse;
|
|
1456
|
+
// Return cached response if payload hasn't changed
|
|
1457
|
+
const cachedResponse = CheckoutInstance.sessionCache.get(cacheKey);
|
|
1458
|
+
if (cachedResponse) {
|
|
1459
|
+
sessionResponse = await cachedResponse;
|
|
1460
|
+
}
|
|
1461
|
+
else {
|
|
1462
|
+
const sessionRequest = this.apiClient
|
|
1463
|
+
.createClientSession(sessionParams)
|
|
1464
|
+
.then(response => {
|
|
1465
|
+
if (response.data?.stripe_public_key) {
|
|
1466
|
+
loadStripe(response.data?.stripe_public_key).then(stripe => {
|
|
1467
|
+
this.radarSessionId = stripe
|
|
1468
|
+
.createRadarSession()
|
|
1469
|
+
.then(session => session?.radarSession?.id)
|
|
1470
|
+
.catch(() => '');
|
|
1471
|
+
});
|
|
148
1472
|
}
|
|
1473
|
+
return response;
|
|
149
1474
|
});
|
|
150
|
-
|
|
1475
|
+
// Cache the successful response
|
|
1476
|
+
CheckoutInstance.sessionCache.set(cacheKey, sessionRequest);
|
|
1477
|
+
sessionResponse = await sessionRequest;
|
|
1478
|
+
}
|
|
1479
|
+
const sessionData = this.apiClient.processSessionResponse(sessionResponse);
|
|
1480
|
+
this.orderId = sessionData.orderId;
|
|
1481
|
+
this.clientToken = sessionData.clientToken;
|
|
151
1482
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
1483
|
+
convertCardSelectorsToElements(selectors, container) {
|
|
1484
|
+
const cardNumber = container.querySelector(selectors.cardNumber);
|
|
1485
|
+
const expiryDate = container.querySelector(selectors.expiryDate);
|
|
1486
|
+
const cvv = container.querySelector(selectors.cvv);
|
|
1487
|
+
const cardholderName = container.querySelector(selectors.cardholderName);
|
|
1488
|
+
const button = container.querySelector(selectors.button);
|
|
1489
|
+
if (!cardNumber || !expiryDate || !cvv || !button) {
|
|
1490
|
+
throw new CheckoutError('Required card input elements not found in container');
|
|
157
1491
|
}
|
|
158
|
-
|
|
159
|
-
|
|
1492
|
+
return {
|
|
1493
|
+
cardNumber,
|
|
1494
|
+
expiryDate,
|
|
1495
|
+
cvv,
|
|
1496
|
+
cardholderName,
|
|
160
1497
|
button,
|
|
161
1498
|
};
|
|
162
1499
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
1500
|
+
convertPaymentButtonSelectorsToElements(selectors) {
|
|
1501
|
+
const paypal = document.querySelector(selectors.paypal);
|
|
1502
|
+
const googlePay = document.querySelector(selectors.googlePay);
|
|
1503
|
+
const applePay = document.querySelector(selectors.applePay);
|
|
1504
|
+
if (!paypal || !googlePay || !applePay) {
|
|
1505
|
+
throw new CheckoutError('Required payment button elements not found in container');
|
|
1506
|
+
}
|
|
1507
|
+
return {
|
|
1508
|
+
paypal,
|
|
1509
|
+
googlePay,
|
|
1510
|
+
applePay,
|
|
1511
|
+
};
|
|
1512
|
+
}
|
|
1513
|
+
async _initializePrimerCheckout() {
|
|
1514
|
+
// Get container element
|
|
1515
|
+
const containerElement = this.getContainer();
|
|
1516
|
+
if (!containerElement) {
|
|
1517
|
+
throw new CheckoutError(`Checkout container not found: ${this.checkoutConfig.container}`);
|
|
1518
|
+
}
|
|
1519
|
+
// Get selectors (either from config or default skin)
|
|
1520
|
+
let cardElements;
|
|
1521
|
+
let paymentButtonElements;
|
|
1522
|
+
let checkoutOptions;
|
|
1523
|
+
if (!this.checkoutConfig.cardSelectors ||
|
|
1524
|
+
!this.checkoutConfig.paymentButtonSelectors) {
|
|
1525
|
+
this.checkoutConfig.paymentMethodOrder =
|
|
1526
|
+
this.checkoutConfig.paymentMethodOrder || DEFAULT_PAYMENT_METHOD_ORDER;
|
|
1527
|
+
const defaultSkinCheckoutOptions = await this.getDefaultSkinCheckoutOptions();
|
|
1528
|
+
if (!defaultSkinCheckoutOptions.cardElements ||
|
|
1529
|
+
!defaultSkinCheckoutOptions.paymentButtonElements) {
|
|
1530
|
+
throw new CheckoutError('Default skin must provide cardSelectors and paymentButtonSelectors');
|
|
1531
|
+
}
|
|
1532
|
+
cardElements =
|
|
1533
|
+
defaultSkinCheckoutOptions.cardElements;
|
|
1534
|
+
paymentButtonElements = defaultSkinCheckoutOptions.paymentButtonElements;
|
|
1535
|
+
checkoutOptions = this.getCheckoutOptions(defaultSkinCheckoutOptions);
|
|
1536
|
+
}
|
|
1537
|
+
else {
|
|
1538
|
+
if (this.checkoutConfig.paymentMethodOrder) {
|
|
1539
|
+
// eslint-disable-next-line no-console
|
|
1540
|
+
console.warn('paymentMethodOrder is using only for default skin and will be ignored if you are using custom checkout');
|
|
1541
|
+
}
|
|
1542
|
+
cardElements = this.convertCardSelectorsToElements(this.checkoutConfig.cardSelectors, containerElement);
|
|
1543
|
+
paymentButtonElements = this.convertPaymentButtonSelectorsToElements(this.checkoutConfig.paymentButtonSelectors);
|
|
1544
|
+
checkoutOptions = this.getCheckoutOptions({});
|
|
1545
|
+
}
|
|
1546
|
+
await this.primerWrapper.renderCheckout(this.clientToken, checkoutOptions, {
|
|
1547
|
+
container: containerElement,
|
|
1548
|
+
cardElements,
|
|
1549
|
+
paymentButtonElements,
|
|
1550
|
+
onSubmit: this.handleSubmit,
|
|
1551
|
+
onInputChange: this.handleInputChange,
|
|
1552
|
+
onMethodRender: this.handleMethodRender,
|
|
1553
|
+
onMethodsAvailable: this.handleMethodsAvailable,
|
|
1554
|
+
onMethodRenderError: this.handleMethodRenderError,
|
|
168
1555
|
});
|
|
169
|
-
this.cardInstance = new index$1.default(document.querySelector('#cardForm'), this.checkoutConfig);
|
|
170
|
-
this.cardInstance.init();
|
|
171
|
-
this.wireCardInputs();
|
|
172
1556
|
}
|
|
173
|
-
|
|
174
|
-
|
|
1557
|
+
async _processPaymentResult(result, primerHandler) {
|
|
1558
|
+
if (result.orderId) {
|
|
1559
|
+
this.orderId = result.orderId;
|
|
1560
|
+
}
|
|
1561
|
+
switch (result.type) {
|
|
1562
|
+
case 'success':
|
|
1563
|
+
this._setState('completed');
|
|
1564
|
+
this.emit(EVENTS.SUCCESS, {
|
|
1565
|
+
orderId: result.orderId,
|
|
1566
|
+
status: result.status,
|
|
1567
|
+
});
|
|
1568
|
+
primerHandler.handleSuccess();
|
|
1569
|
+
break;
|
|
1570
|
+
case 'action_required':
|
|
1571
|
+
this._setState('action_required');
|
|
1572
|
+
this.clientToken = result.clientToken;
|
|
1573
|
+
primerHandler.continueWithNewClientToken(result.clientToken);
|
|
1574
|
+
break;
|
|
1575
|
+
case 'processing':
|
|
1576
|
+
this._setState('processing');
|
|
1577
|
+
setTimeout(() => {
|
|
1578
|
+
primerHandler.handleFailure('Payment is still processing. Please check back later.');
|
|
1579
|
+
}, 30000);
|
|
1580
|
+
break;
|
|
1581
|
+
default:
|
|
1582
|
+
throw new CheckoutError(`Unknown payment result type: ${result.type}`);
|
|
1583
|
+
}
|
|
175
1584
|
}
|
|
176
|
-
|
|
1585
|
+
getCheckoutOptions(options) {
|
|
1586
|
+
let wasPaymentProcessedStarted = false;
|
|
177
1587
|
return {
|
|
178
|
-
...this.
|
|
179
|
-
|
|
1588
|
+
...this.checkoutConfig,
|
|
1589
|
+
...options,
|
|
1590
|
+
onTokenizeSuccess: this.handleTokenizeSuccess,
|
|
1591
|
+
onResumeSuccess: this.handleResumeSuccess,
|
|
1592
|
+
onResumeError: error => {
|
|
1593
|
+
if (error.stack?.includes('PROCESSOR_3DS') &&
|
|
1594
|
+
error.code === 'RESUME_ERROR' &&
|
|
1595
|
+
error.message?.includes('fetch resume key')) {
|
|
1596
|
+
// Ignore 3DS close error, because it is not understandable by user
|
|
1597
|
+
return;
|
|
1598
|
+
}
|
|
1599
|
+
this.emit(EVENTS.PURCHASE_FAILURE, error);
|
|
1600
|
+
},
|
|
1601
|
+
onCheckoutFail: error => {
|
|
1602
|
+
this.emit(EVENTS.PURCHASE_FAILURE, error);
|
|
1603
|
+
},
|
|
1604
|
+
onTokenizeError: error => {
|
|
1605
|
+
this.emit(EVENTS.PURCHASE_FAILURE, error);
|
|
1606
|
+
},
|
|
1607
|
+
onTokenizeShouldStart: data => {
|
|
1608
|
+
wasPaymentProcessedStarted = true;
|
|
1609
|
+
this.emit(EVENTS.ERROR, undefined);
|
|
1610
|
+
this.emit(EVENTS.START_PURCHASE, data.paymentMethodType);
|
|
1611
|
+
return true;
|
|
1612
|
+
},
|
|
1613
|
+
onPaymentMethodAction: action => {
|
|
1614
|
+
switch (action) {
|
|
1615
|
+
case 'PAYMENT_METHOD_SELECTED':
|
|
1616
|
+
this.emit(EVENTS.ERROR, undefined);
|
|
1617
|
+
break;
|
|
1618
|
+
case 'PAYMENT_METHOD_UNSELECTED':
|
|
1619
|
+
if (!wasPaymentProcessedStarted) {
|
|
1620
|
+
this.emit(EVENTS.PURCHASE_CANCELLED);
|
|
1621
|
+
}
|
|
1622
|
+
wasPaymentProcessedStarted = false;
|
|
1623
|
+
break;
|
|
1624
|
+
}
|
|
1625
|
+
},
|
|
180
1626
|
};
|
|
181
1627
|
}
|
|
182
|
-
|
|
1628
|
+
async updatePrice(newPriceId, clientMetadata) {
|
|
1629
|
+
this._ensureNotDestroyed();
|
|
1630
|
+
requireString(newPriceId, 'priceId');
|
|
1631
|
+
if (this.state === 'processing') {
|
|
1632
|
+
throw new CheckoutError('Cannot update price while payment is processing');
|
|
1633
|
+
}
|
|
1634
|
+
try {
|
|
1635
|
+
this._setState('updating');
|
|
1636
|
+
// Invalidate session cache
|
|
1637
|
+
CheckoutInstance.sessionCache.clear();
|
|
1638
|
+
await this.apiClient.updateClientSession({
|
|
1639
|
+
orderId: this.orderId,
|
|
1640
|
+
clientToken: this.clientToken,
|
|
1641
|
+
priceId: newPriceId,
|
|
1642
|
+
clientMetadata,
|
|
1643
|
+
});
|
|
1644
|
+
this.checkoutConfig.priceId = newPriceId;
|
|
1645
|
+
this._setState('ready');
|
|
1646
|
+
}
|
|
1647
|
+
catch (error) {
|
|
1648
|
+
this._setState('error');
|
|
1649
|
+
this.emit(EVENTS.ERROR, error);
|
|
1650
|
+
throw error;
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
getStatus() {
|
|
183
1654
|
return {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
1655
|
+
id: this.id,
|
|
1656
|
+
state: this.state,
|
|
1657
|
+
orderId: this.orderId,
|
|
1658
|
+
priceId: this.checkoutConfig.priceId,
|
|
1659
|
+
isDestroyed: this.isDestroyed,
|
|
187
1660
|
};
|
|
188
1661
|
}
|
|
189
|
-
|
|
1662
|
+
async destroy() {
|
|
1663
|
+
if (this.isDestroyed)
|
|
1664
|
+
return;
|
|
1665
|
+
try {
|
|
1666
|
+
CheckoutInstance.sessionCache.clear();
|
|
1667
|
+
await this.primerWrapper.destroy();
|
|
1668
|
+
this._setState('destroyed');
|
|
1669
|
+
this.orderId = null;
|
|
1670
|
+
this.clientToken = null;
|
|
1671
|
+
this.isDestroyed = true;
|
|
1672
|
+
this.emit(EVENTS.DESTROY);
|
|
1673
|
+
this.removeAllListeners();
|
|
1674
|
+
}
|
|
1675
|
+
catch (error) {
|
|
1676
|
+
// eslint-disable-next-line no-console
|
|
1677
|
+
console.warn('Error during checkout cleanup:', error);
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
_setState(newState) {
|
|
1681
|
+
if (this.state !== newState) {
|
|
1682
|
+
const oldState = this.state;
|
|
1683
|
+
this.state = newState;
|
|
1684
|
+
this.emit(EVENTS.STATUS_CHANGE, newState, oldState);
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
_ensureNotDestroyed() {
|
|
1688
|
+
if (this.isDestroyed) {
|
|
1689
|
+
throw new CheckoutError('Checkout instance has been destroyed');
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
getContainer() {
|
|
1693
|
+
return document.querySelector(this.checkoutConfig.container);
|
|
1694
|
+
}
|
|
1695
|
+
isInState(state) {
|
|
1696
|
+
return this.state === state;
|
|
1697
|
+
}
|
|
1698
|
+
isReady() {
|
|
1699
|
+
return this.state === 'ready' && !this.isDestroyed;
|
|
1700
|
+
}
|
|
1701
|
+
isProcessing() {
|
|
1702
|
+
return ['processing', 'action_required'].includes(this.state);
|
|
1703
|
+
}
|
|
1704
|
+
// Creates containers to render hosted inputs with labels and error messages,
|
|
1705
|
+
// a card holder input with label and error, and a submit button.
|
|
1706
|
+
async getDefaultSkinCheckoutOptions() {
|
|
1707
|
+
const skinFactory = (await Promise.resolve().then(function () { return require('./chunk-index.cjs2.js'); }))
|
|
1708
|
+
.default;
|
|
1709
|
+
const skin = await skinFactory(this.checkoutConfig);
|
|
1710
|
+
this.on(EVENTS.INPUT_ERROR, skin.onInputError);
|
|
1711
|
+
this.on(EVENTS.STATUS_CHANGE, skin.onStatusChange);
|
|
1712
|
+
this.on(EVENTS.ERROR, (error) => skin.onError(error));
|
|
1713
|
+
this.on(EVENTS.LOADER_CHANGE, skin.onLoaderChange);
|
|
1714
|
+
this.on(EVENTS.DESTROY, skin.onDestroy);
|
|
1715
|
+
this.on(EVENTS.SUCCESS, skin.onSuccess);
|
|
1716
|
+
this.on(EVENTS.START_PURCHASE, skin.onStartPurchase);
|
|
1717
|
+
this.on(EVENTS.PURCHASE_FAILURE, skin.onPurchaseFailure);
|
|
1718
|
+
this.on(EVENTS.PURCHASE_COMPLETED, skin.onPurchaseCompleted);
|
|
1719
|
+
this.on(EVENTS.METHODS_AVAILABLE, skin.onMethodsAvailable);
|
|
1720
|
+
this.on(EVENTS.METHODS_AVAILABLE, this.hideInitializingLoader);
|
|
1721
|
+
return skin.getCheckoutOptions();
|
|
1722
|
+
}
|
|
1723
|
+
async getCardDefaultSkinCheckoutOptions(node) {
|
|
1724
|
+
const CardSkin = (await Promise.resolve().then(function () { return require('./chunk-index.cjs3.js'); })).default;
|
|
1725
|
+
const skin = new CardSkin(node, this.checkoutConfig);
|
|
1726
|
+
skin.init();
|
|
1727
|
+
this.on(EVENTS.INPUT_ERROR, skin.onInputError);
|
|
1728
|
+
this.on(EVENTS.METHOD_RENDER, skin.onMethodRender);
|
|
1729
|
+
this.on(EVENTS.SUCCESS, skin.onDestroy);
|
|
1730
|
+
this.on(EVENTS.DESTROY, skin.onDestroy);
|
|
1731
|
+
return skin.getCheckoutOptions();
|
|
1732
|
+
}
|
|
1733
|
+
showInitializingLoader() {
|
|
1734
|
+
renderLoader(this.checkoutConfig.container);
|
|
1735
|
+
}
|
|
1736
|
+
hideInitializingLoader() {
|
|
1737
|
+
hideLoader();
|
|
1738
|
+
}
|
|
1739
|
+
async initMethod(method, element, callbacks) {
|
|
1740
|
+
this._ensureNotDestroyed();
|
|
1741
|
+
if (!this.isReady()) {
|
|
1742
|
+
await this.createSession();
|
|
1743
|
+
}
|
|
1744
|
+
if (callbacks.onRenderSuccess) {
|
|
1745
|
+
this.on(EVENTS.METHOD_RENDER, callbacks.onRenderSuccess);
|
|
1746
|
+
}
|
|
1747
|
+
if (callbacks.onRenderError) {
|
|
1748
|
+
this.on(EVENTS.METHOD_RENDER_ERROR, callbacks.onRenderError);
|
|
1749
|
+
}
|
|
1750
|
+
if (callbacks.onLoaderChange) {
|
|
1751
|
+
this.on(EVENTS.LOADER_CHANGE, callbacks.onLoaderChange);
|
|
1752
|
+
}
|
|
1753
|
+
if (callbacks.onPaymentSuccess) {
|
|
1754
|
+
this.on(EVENTS.SUCCESS, callbacks.onPaymentSuccess);
|
|
1755
|
+
}
|
|
1756
|
+
if (callbacks.onPaymentFail) {
|
|
1757
|
+
this.on(EVENTS.PURCHASE_FAILURE, callbacks.onPaymentFail);
|
|
1758
|
+
}
|
|
1759
|
+
if (callbacks.onPaymentCancel) {
|
|
1760
|
+
this.on(EVENTS.PURCHASE_CANCELLED, callbacks.onPaymentCancel);
|
|
1761
|
+
}
|
|
1762
|
+
if (callbacks.onErrorMessageChange) {
|
|
1763
|
+
this.on(EVENTS.ERROR, callbacks.onErrorMessageChange);
|
|
1764
|
+
}
|
|
1765
|
+
if (callbacks.onPaymentStarted) {
|
|
1766
|
+
this.on(EVENTS.START_PURCHASE, callbacks.onPaymentStarted);
|
|
1767
|
+
}
|
|
1768
|
+
if (callbacks.onMethodsAvailable) {
|
|
1769
|
+
this.on(EVENTS.METHODS_AVAILABLE, callbacks.onMethodsAvailable);
|
|
1770
|
+
}
|
|
1771
|
+
let checkoutOptions = this.getCheckoutOptions({});
|
|
1772
|
+
let methodOptions = {
|
|
1773
|
+
onMethodRender: this.handleMethodRender,
|
|
1774
|
+
onMethodRenderError: this.handleMethodRenderError,
|
|
1775
|
+
};
|
|
1776
|
+
if (method === exports.PaymentMethod.PAYMENT_CARD) {
|
|
1777
|
+
const cardDefaultOptions = await this.getCardDefaultSkinCheckoutOptions(element);
|
|
1778
|
+
checkoutOptions = this.getCheckoutOptions({
|
|
1779
|
+
...cardDefaultOptions,
|
|
1780
|
+
});
|
|
1781
|
+
methodOptions = {
|
|
1782
|
+
cardElements: cardDefaultOptions.cardElements,
|
|
1783
|
+
onSubmit: this.handleSubmit,
|
|
1784
|
+
onInputChange: this.handleInputChange,
|
|
1785
|
+
onMethodRender: this.handleMethodRender,
|
|
1786
|
+
onMethodRenderError: this.handleMethodRenderError,
|
|
1787
|
+
};
|
|
1788
|
+
}
|
|
1789
|
+
await this.primerWrapper.initializeHeadlessCheckout(this.clientToken, checkoutOptions);
|
|
1790
|
+
const methodInterface = await this.primerWrapper.initMethod(method, element, methodOptions);
|
|
190
1791
|
return {
|
|
191
|
-
...
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
buttonStyle: 'black',
|
|
1792
|
+
...methodInterface,
|
|
1793
|
+
destroy: async () => {
|
|
1794
|
+
await methodInterface.destroy();
|
|
1795
|
+
await this.destroy();
|
|
196
1796
|
},
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
1797
|
+
};
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
CheckoutInstance.sessionCache = new Map();
|
|
1801
|
+
|
|
1802
|
+
function getErrorImage(orgId, options) {
|
|
1803
|
+
const params = new URLSearchParams({
|
|
1804
|
+
message: options.message,
|
|
1805
|
+
code: options.code,
|
|
1806
|
+
timestamp: Date.now().toString(),
|
|
1807
|
+
sdk_version: SDK_VERSION,
|
|
1808
|
+
});
|
|
1809
|
+
if (options.req_id) {
|
|
1810
|
+
params.append('req_id', options.req_id);
|
|
1811
|
+
}
|
|
1812
|
+
const url = `https://billing.funnelfox.com/sdk_report/${encodeURIComponent(orgId)}/crash?${params.toString()}`;
|
|
1813
|
+
const img = new Image();
|
|
1814
|
+
img.src = url;
|
|
1815
|
+
img.style.display = 'none';
|
|
1816
|
+
document.body.appendChild(img);
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1819
|
+
/**
|
|
1820
|
+
* @fileoverview Public API with configuration and orchestration logic
|
|
1821
|
+
*/
|
|
1822
|
+
let defaultConfig = null;
|
|
1823
|
+
function configure(config) {
|
|
1824
|
+
defaultConfig = config;
|
|
1825
|
+
}
|
|
1826
|
+
function resolveConfig(options, functionName) {
|
|
1827
|
+
const { orgId, apiConfig } = options || {};
|
|
1828
|
+
const finalOrgId = orgId || defaultConfig?.orgId;
|
|
1829
|
+
if (!finalOrgId) {
|
|
1830
|
+
throw new Error(`orgId is required. Pass it to ${functionName}() or call configure() first.`);
|
|
1831
|
+
}
|
|
1832
|
+
const finalBaseUrl = apiConfig?.baseUrl || defaultConfig?.baseUrl || DEFAULTS.BASE_URL;
|
|
1833
|
+
const finalRegion = apiConfig?.region || defaultConfig?.region || DEFAULTS.REGION;
|
|
1834
|
+
return {
|
|
1835
|
+
orgId: finalOrgId,
|
|
1836
|
+
baseUrl: finalBaseUrl,
|
|
1837
|
+
region: finalRegion,
|
|
1838
|
+
};
|
|
1839
|
+
}
|
|
1840
|
+
async function createCheckout(options) {
|
|
1841
|
+
try {
|
|
1842
|
+
const { ...checkoutConfig } = options;
|
|
1843
|
+
// Ensure Primer SDK is loaded before creating checkout
|
|
1844
|
+
const primerWrapper = new PrimerWrapper();
|
|
1845
|
+
await primerWrapper.ensurePrimerLoaded();
|
|
1846
|
+
const config = resolveConfig(options, 'createCheckout');
|
|
1847
|
+
const checkout = new CheckoutInstance({
|
|
1848
|
+
...config,
|
|
1849
|
+
checkoutConfig: {
|
|
1850
|
+
...checkoutConfig,
|
|
203
1851
|
},
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
1852
|
+
});
|
|
1853
|
+
await checkout.initialize();
|
|
1854
|
+
return checkout;
|
|
1855
|
+
}
|
|
1856
|
+
catch (error) {
|
|
1857
|
+
getErrorImage(options.orgId, {
|
|
1858
|
+
message: error.message,
|
|
1859
|
+
code: error.code,
|
|
1860
|
+
req_id: error?.response?.req_id,
|
|
1861
|
+
});
|
|
1862
|
+
throw error;
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
async function createClientSession(params) {
|
|
1866
|
+
const { priceId, externalId, email, clientMetadata, countryCode } = params;
|
|
1867
|
+
const config = resolveConfig(params, 'createClientSession');
|
|
1868
|
+
const apiClient = new APIClient({
|
|
1869
|
+
baseUrl: config.baseUrl,
|
|
1870
|
+
orgId: config.orgId,
|
|
1871
|
+
timeout: DEFAULTS.REQUEST_TIMEOUT,
|
|
1872
|
+
retryAttempts: DEFAULTS.RETRY_ATTEMPTS,
|
|
1873
|
+
});
|
|
1874
|
+
const sessionResponse = await apiClient.createClientSession({
|
|
1875
|
+
priceId,
|
|
1876
|
+
externalId,
|
|
1877
|
+
email,
|
|
1878
|
+
region: config.region,
|
|
1879
|
+
clientMetadata,
|
|
1880
|
+
countryCode,
|
|
1881
|
+
});
|
|
1882
|
+
return apiClient.processSessionResponse(sessionResponse);
|
|
1883
|
+
}
|
|
1884
|
+
async function silentPurchase(options) {
|
|
1885
|
+
const { priceId, externalId, clientMetadata, orgId, baseUrl } = options;
|
|
1886
|
+
const apiClient = new APIClient({
|
|
1887
|
+
baseUrl: baseUrl,
|
|
1888
|
+
orgId: orgId,
|
|
1889
|
+
timeout: DEFAULTS.REQUEST_TIMEOUT,
|
|
1890
|
+
retryAttempts: DEFAULTS.RETRY_ATTEMPTS,
|
|
1891
|
+
});
|
|
1892
|
+
try {
|
|
1893
|
+
const response = await apiClient.oneClick({
|
|
1894
|
+
pp_ident: priceId,
|
|
1895
|
+
external_id: externalId,
|
|
1896
|
+
client_metadata: clientMetadata,
|
|
1897
|
+
});
|
|
1898
|
+
if (response.status !== 'success' &&
|
|
1899
|
+
response.error.some(({ code }) => code === 'double_purchase')) {
|
|
1900
|
+
throw new APIError('This product was already purchased');
|
|
1901
|
+
}
|
|
1902
|
+
else if (response.status !== 'success') {
|
|
1903
|
+
return false;
|
|
1904
|
+
}
|
|
1905
|
+
return true;
|
|
1906
|
+
}
|
|
1907
|
+
catch (error) {
|
|
1908
|
+
getErrorImage(orgId, {
|
|
1909
|
+
message: error.message,
|
|
1910
|
+
code: error.code,
|
|
1911
|
+
req_id: error?.response?.req_id,
|
|
1912
|
+
});
|
|
1913
|
+
throw error;
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
async function initMethod(method, element, options) {
|
|
1917
|
+
try {
|
|
1918
|
+
const checkoutInstance = new CheckoutInstance({
|
|
1919
|
+
orgId: options.orgId,
|
|
1920
|
+
baseUrl: options.baseUrl,
|
|
1921
|
+
checkoutConfig: {
|
|
1922
|
+
priceId: options.priceId,
|
|
1923
|
+
customer: {
|
|
1924
|
+
externalId: options.externalId,
|
|
1925
|
+
email: options.email,
|
|
1926
|
+
},
|
|
1927
|
+
container: '',
|
|
1928
|
+
clientMetadata: options.meta,
|
|
1929
|
+
card: options.card,
|
|
1930
|
+
style: options.style,
|
|
1931
|
+
applePay: {
|
|
1932
|
+
...DEFAULT_BUTTONS_STYLES[exports.PaymentMethod.APPLE_PAY],
|
|
1933
|
+
...(options.applePay || {}),
|
|
1934
|
+
},
|
|
1935
|
+
paypal: {
|
|
1936
|
+
...DEFAULT_BUTTONS_STYLES[exports.PaymentMethod.PAYPAL],
|
|
1937
|
+
...(options.paypal || {}),
|
|
1938
|
+
},
|
|
1939
|
+
googlePay: {
|
|
1940
|
+
...DEFAULT_BUTTONS_STYLES[exports.PaymentMethod.GOOGLE_PAY],
|
|
1941
|
+
...(options.googlePay || {}),
|
|
1942
|
+
},
|
|
208
1943
|
},
|
|
209
|
-
};
|
|
1944
|
+
});
|
|
1945
|
+
return checkoutInstance.initMethod(method, element, {
|
|
1946
|
+
onRenderSuccess: options.onRenderSuccess,
|
|
1947
|
+
onRenderError: options.onRenderError,
|
|
1948
|
+
onLoaderChange: options.onLoaderChange,
|
|
1949
|
+
onPaymentSuccess: options.onPaymentSuccess,
|
|
1950
|
+
onPaymentFail: options.onPaymentFail,
|
|
1951
|
+
onPaymentCancel: options.onPaymentCancel,
|
|
1952
|
+
onErrorMessageChange: options.onErrorMessageChange,
|
|
1953
|
+
onPaymentStarted: options.onPaymentStarted,
|
|
1954
|
+
onMethodsAvailable: options.onMethodsAvailable,
|
|
1955
|
+
});
|
|
1956
|
+
}
|
|
1957
|
+
catch (error) {
|
|
1958
|
+
getErrorImage(options.orgId, {
|
|
1959
|
+
message: error.message,
|
|
1960
|
+
code: error.code,
|
|
1961
|
+
req_id: error?.response?.req_id,
|
|
1962
|
+
});
|
|
1963
|
+
throw error;
|
|
210
1964
|
}
|
|
211
1965
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
1966
|
+
|
|
1967
|
+
/**
|
|
1968
|
+
* @fileoverview Main entry point for @funnelfox/billing
|
|
1969
|
+
*/
|
|
1970
|
+
const Billing = {
|
|
1971
|
+
configure: configure,
|
|
1972
|
+
createCheckout: createCheckout,
|
|
1973
|
+
createClientSession: createClientSession,
|
|
1974
|
+
initMethod: initMethod,
|
|
1975
|
+
silentPurchase: silentPurchase,
|
|
216
1976
|
};
|
|
1977
|
+
if (typeof window !== 'undefined') {
|
|
1978
|
+
window.Billing = Billing;
|
|
1979
|
+
}
|
|
217
1980
|
|
|
218
|
-
exports.
|
|
1981
|
+
exports.APIError = APIError;
|
|
1982
|
+
exports.Billing = Billing;
|
|
1983
|
+
exports.CHECKOUT_STATES = CHECKOUT_STATES;
|
|
1984
|
+
exports.CheckoutError = CheckoutError;
|
|
1985
|
+
exports.ConfigurationError = ConfigurationError;
|
|
1986
|
+
exports.DEFAULTS = DEFAULTS;
|
|
1987
|
+
exports.DEFAULT_BUTTONS_STYLES = DEFAULT_BUTTONS_STYLES;
|
|
1988
|
+
exports.ERROR_CODES = ERROR_CODES;
|
|
1989
|
+
exports.EVENTS = EVENTS;
|
|
1990
|
+
exports.FunnefoxSDKError = FunnefoxSDKError;
|
|
1991
|
+
exports.NetworkError = NetworkError;
|
|
1992
|
+
exports.PrimerError = PrimerError;
|
|
1993
|
+
exports.SDK_VERSION = SDK_VERSION;
|
|
1994
|
+
exports.ValidationError = ValidationError;
|
|
1995
|
+
exports.configure = configure;
|
|
1996
|
+
exports.createCheckout = createCheckout;
|
|
1997
|
+
exports.createClientSession = createClientSession;
|