@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.
@@ -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-beta.3';
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
- this.ensurePrimerAvailable();
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
- this.ensurePrimerAvailable();
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
- return await this.renderCardCheckoutWithElements(options.cardElements, {
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
- return await this.renderButton(method, {
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.disabled = disabled;
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
- return Promise.all(this.availableMethods.map(method => {
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
- })).then((interfaces) => {
585
- this.paymentMethodsInterfaces = interfaces;
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
- sessionResponse = await this.apiClient.createClientSession(sessionParams);
1183
+ const sessionRequest = this.apiClient.createClientSession(sessionParams);
1032
1184
  // Cache the successful response
1033
- CheckoutInstance.sessionCache.set(cacheKey, sessionResponse);
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.primerWrapper, this.checkoutConfig);
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.ensurePrimerAvailable();
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
- let cardholderName = null;
1489
- if (this.checkoutConfig?.card?.cardholderName) {
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 || !expiryDate || !cvv) {
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(primerWrapper, checkoutConfig) {
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 (primerWrapper, checkoutConfig) => {
1735
- const skin = new DefaultSkin(primerWrapper, checkoutConfig);
1898
+ const createDefaultSkin = async (checkoutConfig) => {
1899
+ const skin = new DefaultSkin(checkoutConfig);
1736
1900
  await skin['init']();
1737
1901
  return skin;
1738
1902
  };