@funnelfox/billing 0.5.0-beta.3 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-index.cjs.js +4 -5
- package/dist/chunk-index.cjs2.js +10 -3
- package/dist/chunk-index.es.js +4 -5
- package/dist/chunk-index.es2.js +10 -3
- package/dist/funnelfox-billing.cjs.js +175 -17
- package/dist/funnelfox-billing.esm.js +175 -17
- package/dist/funnelfox-billing.js +189 -25
- package/dist/funnelfox-billing.min.js +1 -1
- package/package.json +4 -6
- package/src/types.d.ts +12 -15
|
@@ -208,6 +208,132 @@
|
|
|
208
208
|
return Promise.race([promise, timeoutPromise]);
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
+
/**
|
|
212
|
+
* @fileoverview Dynamic loader for Primer SDK
|
|
213
|
+
* Loads Primer script and CSS from CDN independently of bundler
|
|
214
|
+
*/
|
|
215
|
+
const PRIMER_CDN_BASE = 'https://sdk.primer.io/web';
|
|
216
|
+
const DEFAULT_VERSION = '2.57.3';
|
|
217
|
+
// Integrity hashes for specific versions (for SRI security)
|
|
218
|
+
const INTEGRITY_HASHES = {
|
|
219
|
+
'2.57.3': {
|
|
220
|
+
js: 'sha384-xq2SWkYvTlKOMpuXQUXq1QI3eZN7JiqQ3Sc72U9wY1IE30MW3HkwQWg/1n6BTMz4',
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
let loadingPromise = null;
|
|
224
|
+
let isLoaded = false;
|
|
225
|
+
/**
|
|
226
|
+
* Injects a script tag into the document head
|
|
227
|
+
*/
|
|
228
|
+
function injectScript(src, integrity) {
|
|
229
|
+
return new Promise((resolve, reject) => {
|
|
230
|
+
// Check if script already exists
|
|
231
|
+
const existingScript = document.querySelector(`script[src="${src}"]`);
|
|
232
|
+
if (existingScript) {
|
|
233
|
+
resolve(existingScript);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
const script = document.createElement('script');
|
|
237
|
+
script.src = src;
|
|
238
|
+
script.async = true;
|
|
239
|
+
script.crossOrigin = 'anonymous';
|
|
240
|
+
if (integrity) {
|
|
241
|
+
script.integrity = integrity;
|
|
242
|
+
}
|
|
243
|
+
script.onload = () => resolve(script);
|
|
244
|
+
script.onerror = () => reject(new Error(`Failed to load Primer SDK script from ${src}`));
|
|
245
|
+
document.head.appendChild(script);
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Injects a CSS link tag into the document head
|
|
250
|
+
*/
|
|
251
|
+
function injectCSS(href, integrity) {
|
|
252
|
+
return new Promise((resolve, reject) => {
|
|
253
|
+
// Check if stylesheet already exists
|
|
254
|
+
const existingLink = document.querySelector(`link[href="${href}"]`);
|
|
255
|
+
if (existingLink) {
|
|
256
|
+
resolve(existingLink);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
const link = document.createElement('link');
|
|
260
|
+
link.rel = 'stylesheet';
|
|
261
|
+
link.href = href;
|
|
262
|
+
link.crossOrigin = 'anonymous';
|
|
263
|
+
if (integrity) {
|
|
264
|
+
link.integrity = integrity;
|
|
265
|
+
}
|
|
266
|
+
link.onload = () => resolve(link);
|
|
267
|
+
link.onerror = () => reject(new Error(`Failed to load Primer SDK CSS from ${href}`));
|
|
268
|
+
document.head.appendChild(link);
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Waits for window.Primer to be available
|
|
273
|
+
*/
|
|
274
|
+
function waitForPrimer(timeout = 10000) {
|
|
275
|
+
return new Promise((resolve, reject) => {
|
|
276
|
+
const startTime = Date.now();
|
|
277
|
+
const check = () => {
|
|
278
|
+
if (typeof window !== 'undefined' &&
|
|
279
|
+
window.Primer &&
|
|
280
|
+
typeof window.Primer.createHeadless === 'function') {
|
|
281
|
+
resolve();
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
if (Date.now() - startTime > timeout) {
|
|
285
|
+
reject(new Error('Timeout waiting for Primer SDK to initialize on window'));
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
setTimeout(check, 50);
|
|
289
|
+
};
|
|
290
|
+
check();
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Loads the Primer SDK script and CSS from CDN
|
|
295
|
+
* @param version - The version of Primer SDK to load (default: 2.57.3)
|
|
296
|
+
* @returns Promise that resolves when SDK is loaded and ready
|
|
297
|
+
*/
|
|
298
|
+
async function loadPrimerSDK(version) {
|
|
299
|
+
// Already loaded
|
|
300
|
+
if (isLoaded) {
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
// Already loading - return existing promise
|
|
304
|
+
if (loadingPromise) {
|
|
305
|
+
return loadingPromise;
|
|
306
|
+
}
|
|
307
|
+
// Check if Primer is already available (user may have loaded it manually)
|
|
308
|
+
if (typeof window !== 'undefined' &&
|
|
309
|
+
window.Primer &&
|
|
310
|
+
typeof window.Primer.createHeadless === 'function') {
|
|
311
|
+
isLoaded = true;
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
const ver = version || DEFAULT_VERSION;
|
|
315
|
+
const jsUrl = `${PRIMER_CDN_BASE}/v${ver}/Primer.min.js`;
|
|
316
|
+
const cssUrl = `${PRIMER_CDN_BASE}/v${ver}/Checkout.css`;
|
|
317
|
+
const hashes = INTEGRITY_HASHES[ver];
|
|
318
|
+
loadingPromise = (async () => {
|
|
319
|
+
try {
|
|
320
|
+
// Load CSS and JS in parallel
|
|
321
|
+
await Promise.all([
|
|
322
|
+
injectCSS(cssUrl, hashes?.css),
|
|
323
|
+
injectScript(jsUrl, hashes?.js),
|
|
324
|
+
]);
|
|
325
|
+
// Wait for Primer to be available on window
|
|
326
|
+
await waitForPrimer();
|
|
327
|
+
isLoaded = true;
|
|
328
|
+
}
|
|
329
|
+
catch (error) {
|
|
330
|
+
loadingPromise = null;
|
|
331
|
+
throw error;
|
|
332
|
+
}
|
|
333
|
+
})();
|
|
334
|
+
return loadingPromise;
|
|
335
|
+
}
|
|
336
|
+
|
|
211
337
|
exports.PaymentMethod = void 0;
|
|
212
338
|
(function (PaymentMethod) {
|
|
213
339
|
PaymentMethod["GOOGLE_PAY"] = "GOOGLE_PAY";
|
|
@@ -219,7 +345,7 @@
|
|
|
219
345
|
/**
|
|
220
346
|
* @fileoverview Constants for Funnefox SDK
|
|
221
347
|
*/
|
|
222
|
-
const SDK_VERSION = '0.5.0
|
|
348
|
+
const SDK_VERSION = '0.5.0';
|
|
223
349
|
const DEFAULTS = {
|
|
224
350
|
BASE_URL: 'https://billing.funnelfox.com',
|
|
225
351
|
REGION: 'default',
|
|
@@ -314,12 +440,28 @@
|
|
|
314
440
|
this.destroyCallbacks = [];
|
|
315
441
|
this.headless = null;
|
|
316
442
|
this.availableMethods = [];
|
|
443
|
+
this.paymentMethodsInterfaces = [];
|
|
317
444
|
}
|
|
318
445
|
isPrimerAvailable() {
|
|
319
446
|
return (typeof window !== 'undefined' &&
|
|
320
447
|
window.Primer &&
|
|
321
448
|
typeof window.Primer?.createHeadless === 'function');
|
|
322
449
|
}
|
|
450
|
+
/**
|
|
451
|
+
* Loads Primer SDK if not already available
|
|
452
|
+
* @param version - Optional version to load (uses default if not specified)
|
|
453
|
+
*/
|
|
454
|
+
async ensurePrimerLoaded(version) {
|
|
455
|
+
if (this.isPrimerAvailable()) {
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
try {
|
|
459
|
+
await loadPrimerSDK(version);
|
|
460
|
+
}
|
|
461
|
+
catch (error) {
|
|
462
|
+
throw new PrimerError('Failed to load Primer SDK', error);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
323
465
|
ensurePrimerAvailable() {
|
|
324
466
|
if (!this.isPrimerAvailable()) {
|
|
325
467
|
throw new PrimerError('Primer SDK not found. Please include the Primer SDK script before initializing FunnefoxSDK.');
|
|
@@ -329,7 +471,8 @@
|
|
|
329
471
|
if (this.headless) {
|
|
330
472
|
return this.headless;
|
|
331
473
|
}
|
|
332
|
-
|
|
474
|
+
// Load Primer SDK if not already available
|
|
475
|
+
await this.ensurePrimerLoaded();
|
|
333
476
|
const primerOptions = merge({
|
|
334
477
|
paymentHandling: 'MANUAL',
|
|
335
478
|
apiVersion: '2.4',
|
|
@@ -382,7 +525,8 @@
|
|
|
382
525
|
}
|
|
383
526
|
async renderButton(allowedPaymentMethod, { htmlNode, onMethodRenderError, onMethodRender, }) {
|
|
384
527
|
let button;
|
|
385
|
-
|
|
528
|
+
// Ensure Primer SDK is loaded
|
|
529
|
+
await this.ensurePrimerLoaded();
|
|
386
530
|
if (!this.headless) {
|
|
387
531
|
throw new PrimerError('Headless checkout not found');
|
|
388
532
|
}
|
|
@@ -417,20 +561,24 @@
|
|
|
417
561
|
!options.onInputChange) {
|
|
418
562
|
throw new PrimerError('Card elements, onSubmit, and onInputChange are required for PAYMENT_CARD method');
|
|
419
563
|
}
|
|
420
|
-
|
|
564
|
+
const cardInterface = await this.renderCardCheckoutWithElements(options.cardElements, {
|
|
421
565
|
onSubmit: options.onSubmit,
|
|
422
566
|
onInputChange: options.onInputChange,
|
|
423
567
|
onMethodRenderError: options.onMethodRenderError,
|
|
424
568
|
onMethodRender: options.onMethodRender,
|
|
425
569
|
});
|
|
570
|
+
this.paymentMethodsInterfaces.push(cardInterface);
|
|
571
|
+
return cardInterface;
|
|
426
572
|
}
|
|
427
573
|
else {
|
|
428
574
|
try {
|
|
429
|
-
|
|
575
|
+
const buttonInterface = await this.renderButton(method, {
|
|
430
576
|
htmlNode,
|
|
431
577
|
onMethodRenderError: options.onMethodRenderError,
|
|
432
578
|
onMethodRender: options.onMethodRender,
|
|
433
579
|
});
|
|
580
|
+
this.paymentMethodsInterfaces.push(buttonInterface);
|
|
581
|
+
return buttonInterface;
|
|
434
582
|
}
|
|
435
583
|
catch (error) {
|
|
436
584
|
throw new PrimerError('Failed to initialize Primer checkout', error);
|
|
@@ -515,7 +663,12 @@
|
|
|
515
663
|
cardNumberInput.setDisabled(disabled);
|
|
516
664
|
expiryInput.setDisabled(disabled);
|
|
517
665
|
cvvInput.setDisabled(disabled);
|
|
518
|
-
elements.button
|
|
666
|
+
if (elements.button) {
|
|
667
|
+
elements.button.disabled = disabled;
|
|
668
|
+
}
|
|
669
|
+
if (elements.cardholderName) {
|
|
670
|
+
elements.cardholderName.disabled = disabled;
|
|
671
|
+
}
|
|
519
672
|
},
|
|
520
673
|
submit: () => onSubmitHandler(),
|
|
521
674
|
destroy: () => {
|
|
@@ -557,7 +710,7 @@
|
|
|
557
710
|
const { cardElements, paymentButtonElements, container, onSubmit, onInputChange, onMethodRender, onMethodRenderError, onMethodsAvailable, } = checkoutRenderOptions;
|
|
558
711
|
await this.initializeHeadlessCheckout(clientToken, checkoutOptions);
|
|
559
712
|
onMethodsAvailable?.(this.availableMethods);
|
|
560
|
-
|
|
713
|
+
await Promise.all(this.availableMethods.map(method => {
|
|
561
714
|
if (method === exports.PaymentMethod.PAYMENT_CARD) {
|
|
562
715
|
// For card, use the main container
|
|
563
716
|
return this.initMethod(method, container, {
|
|
@@ -581,10 +734,8 @@
|
|
|
581
734
|
onMethodRenderError,
|
|
582
735
|
});
|
|
583
736
|
}
|
|
584
|
-
}))
|
|
585
|
-
|
|
586
|
-
this.isInitialized = true;
|
|
587
|
-
});
|
|
737
|
+
}));
|
|
738
|
+
this.isInitialized = true;
|
|
588
739
|
}
|
|
589
740
|
wrapTokenizeHandler(handler) {
|
|
590
741
|
return async (paymentMethodTokenData, primerHandler) => {
|
|
@@ -940,6 +1091,7 @@
|
|
|
940
1091
|
};
|
|
941
1092
|
this.onLoaderChangeWithRace = (state) => {
|
|
942
1093
|
const isLoading = !!(state ? ++this.counter : --this.counter);
|
|
1094
|
+
this.primerWrapper.disableButtons(isLoading);
|
|
943
1095
|
this.emit(EVENTS.LOADER_CHANGE, isLoading);
|
|
944
1096
|
};
|
|
945
1097
|
this.id = generateId('checkout_');
|
|
@@ -1023,14 +1175,15 @@
|
|
|
1023
1175
|
].join('-');
|
|
1024
1176
|
let sessionResponse;
|
|
1025
1177
|
// Return cached response if payload hasn't changed
|
|
1026
|
-
const cachedResponse = CheckoutInstance.sessionCache.get(cacheKey);
|
|
1178
|
+
const cachedResponse = await CheckoutInstance.sessionCache.get(cacheKey);
|
|
1027
1179
|
if (cachedResponse) {
|
|
1028
1180
|
sessionResponse = cachedResponse;
|
|
1029
1181
|
}
|
|
1030
1182
|
else {
|
|
1031
|
-
|
|
1183
|
+
const sessionRequest = this.apiClient.createClientSession(sessionParams);
|
|
1032
1184
|
// Cache the successful response
|
|
1033
|
-
CheckoutInstance.sessionCache.set(cacheKey,
|
|
1185
|
+
CheckoutInstance.sessionCache.set(cacheKey, sessionRequest);
|
|
1186
|
+
sessionResponse = await sessionRequest;
|
|
1034
1187
|
}
|
|
1035
1188
|
const sessionData = this.apiClient.processSessionResponse(sessionResponse);
|
|
1036
1189
|
this.orderId = sessionData.orderId;
|
|
@@ -1259,18 +1412,18 @@
|
|
|
1259
1412
|
async getDefaultSkinCheckoutOptions() {
|
|
1260
1413
|
const skinFactory = (await Promise.resolve().then(function () { return index; }))
|
|
1261
1414
|
.default;
|
|
1262
|
-
const skin = await skinFactory(this.
|
|
1415
|
+
const skin = await skinFactory(this.checkoutConfig);
|
|
1263
1416
|
this.on(EVENTS.INPUT_ERROR, skin.onInputError);
|
|
1264
1417
|
this.on(EVENTS.STATUS_CHANGE, skin.onStatusChange);
|
|
1265
1418
|
this.on(EVENTS.ERROR, (error) => skin.onError(error));
|
|
1266
1419
|
this.on(EVENTS.LOADER_CHANGE, skin.onLoaderChange);
|
|
1267
1420
|
this.on(EVENTS.DESTROY, skin.onDestroy);
|
|
1268
|
-
this.on(EVENTS.METHOD_RENDER, skin.onMethodRender);
|
|
1269
1421
|
this.on(EVENTS.SUCCESS, skin.onSuccess);
|
|
1270
1422
|
this.on(EVENTS.START_PURCHASE, skin.onStartPurchase);
|
|
1271
1423
|
this.on(EVENTS.PURCHASE_FAILURE, skin.onPurchaseFailure);
|
|
1272
1424
|
this.on(EVENTS.PURCHASE_COMPLETED, skin.onPurchaseCompleted);
|
|
1273
1425
|
this.on(EVENTS.METHODS_AVAILABLE, skin.onMethodsAvailable);
|
|
1426
|
+
this.on(EVENTS.METHODS_AVAILABLE, this.hideInitializingLoader);
|
|
1274
1427
|
return skin.getCheckoutOptions();
|
|
1275
1428
|
}
|
|
1276
1429
|
async getCardDefaultSkinCheckoutOptions(node) {
|
|
@@ -1279,6 +1432,7 @@
|
|
|
1279
1432
|
skin.init();
|
|
1280
1433
|
this.on(EVENTS.INPUT_ERROR, skin.onInputError);
|
|
1281
1434
|
this.on(EVENTS.METHOD_RENDER, skin.onMethodRender);
|
|
1435
|
+
this.on(EVENTS.SUCCESS, skin.onDestroy);
|
|
1282
1436
|
return skin.getCheckoutOptions();
|
|
1283
1437
|
}
|
|
1284
1438
|
showInitializingLoader() {
|
|
@@ -1313,8 +1467,9 @@
|
|
|
1313
1467
|
}
|
|
1314
1468
|
async function createCheckout(options) {
|
|
1315
1469
|
const { ...checkoutConfig } = options;
|
|
1470
|
+
// Ensure Primer SDK is loaded before creating checkout
|
|
1316
1471
|
const primerWrapper = new PrimerWrapper();
|
|
1317
|
-
primerWrapper.
|
|
1472
|
+
await primerWrapper.ensurePrimerLoaded();
|
|
1318
1473
|
const config = resolveConfig(options, 'createCheckout');
|
|
1319
1474
|
const checkout = new CheckoutInstance({
|
|
1320
1475
|
...config,
|
|
@@ -1367,6 +1522,9 @@
|
|
|
1367
1522
|
return true;
|
|
1368
1523
|
}
|
|
1369
1524
|
async function initMethod(method, element, options) {
|
|
1525
|
+
// Ensure Primer SDK is loaded before initializing payment method
|
|
1526
|
+
const primerWrapper = new PrimerWrapper();
|
|
1527
|
+
await primerWrapper.ensurePrimerLoaded();
|
|
1370
1528
|
const checkoutInstance = new CheckoutInstance({
|
|
1371
1529
|
orgId: options.orgId,
|
|
1372
1530
|
baseUrl: options.baseUrl,
|
|
@@ -1474,6 +1632,9 @@
|
|
|
1474
1632
|
this.onMethodRender = () => {
|
|
1475
1633
|
this.containerEl.style.display = 'block';
|
|
1476
1634
|
};
|
|
1635
|
+
this.onDestroy = () => {
|
|
1636
|
+
this.containerEl.remove();
|
|
1637
|
+
};
|
|
1477
1638
|
if (!containerEl) {
|
|
1478
1639
|
throw new Error('Container element not found');
|
|
1479
1640
|
}
|
|
@@ -1485,15 +1646,19 @@
|
|
|
1485
1646
|
const cardNumber = this.containerEl.querySelector('#cardNumberInput');
|
|
1486
1647
|
const expiryDate = this.containerEl.querySelector('#expiryInput');
|
|
1487
1648
|
const cvv = this.containerEl.querySelector('#cvvInput');
|
|
1488
|
-
|
|
1489
|
-
|
|
1649
|
+
const hasCardholderInput = !!this.checkoutConfig?.card?.cardholderName;
|
|
1650
|
+
let cardholderName = undefined;
|
|
1651
|
+
if (hasCardholderInput) {
|
|
1490
1652
|
cardholderName =
|
|
1491
1653
|
this.containerEl.querySelector('#cardHolderInput');
|
|
1492
1654
|
}
|
|
1493
1655
|
else {
|
|
1494
1656
|
this.containerEl.querySelector('#cardHolderInput').parentElement.style.display = 'none';
|
|
1495
1657
|
}
|
|
1496
|
-
if (!cardNumber ||
|
|
1658
|
+
if (!cardNumber ||
|
|
1659
|
+
!expiryDate ||
|
|
1660
|
+
!cvv ||
|
|
1661
|
+
(hasCardholderInput && !cardholderName)) {
|
|
1497
1662
|
throw new Error('One or more card input elements are missing in the default skin');
|
|
1498
1663
|
}
|
|
1499
1664
|
this.cardInputElements = {
|
|
@@ -1546,9 +1711,8 @@
|
|
|
1546
1711
|
[exports.PaymentMethod.APPLE_PAY]: applePayTemplate,
|
|
1547
1712
|
};
|
|
1548
1713
|
class DefaultSkin {
|
|
1549
|
-
constructor(
|
|
1714
|
+
constructor(checkoutConfig) {
|
|
1550
1715
|
this.onLoaderChange = (isLoading) => {
|
|
1551
|
-
this.primerWrapper.disableButtons(isLoading);
|
|
1552
1716
|
document
|
|
1553
1717
|
.querySelectorAll(`${this.containerSelector} .loader-container`)
|
|
1554
1718
|
?.forEach(loaderEl => {
|
|
@@ -1612,6 +1776,7 @@
|
|
|
1612
1776
|
this.onMethodsAvailable = (methods) => {
|
|
1613
1777
|
this.availableMethods = methods;
|
|
1614
1778
|
this.initAccordion();
|
|
1779
|
+
methods.forEach(this.onMethodRender);
|
|
1615
1780
|
};
|
|
1616
1781
|
this.onStartPurchase = (paymentMethod) => {
|
|
1617
1782
|
this.currentPurchaseMethod = paymentMethod;
|
|
@@ -1632,7 +1797,6 @@
|
|
|
1632
1797
|
throw new Error(`Container element not found for selector: ${this.containerSelector}`);
|
|
1633
1798
|
}
|
|
1634
1799
|
this.containerEl = containerEl;
|
|
1635
|
-
this.primerWrapper = primerWrapper;
|
|
1636
1800
|
this.checkoutConfig = checkoutConfig;
|
|
1637
1801
|
}
|
|
1638
1802
|
initAccordion() {
|
|
@@ -1731,8 +1895,8 @@
|
|
|
1731
1895
|
};
|
|
1732
1896
|
}
|
|
1733
1897
|
}
|
|
1734
|
-
const createDefaultSkin = async (
|
|
1735
|
-
const skin = new DefaultSkin(
|
|
1898
|
+
const createDefaultSkin = async (checkoutConfig) => {
|
|
1899
|
+
const skin = new DefaultSkin(checkoutConfig);
|
|
1736
1900
|
await skin['init']();
|
|
1737
1901
|
return skin;
|
|
1738
1902
|
};
|