@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.
@@ -29,9 +29,8 @@ const paymentMethodTemplates = {
29
29
  [index.PaymentMethod.APPLE_PAY]: applePayTemplate,
30
30
  };
31
31
  class DefaultSkin {
32
- constructor(primerWrapper, checkoutConfig) {
32
+ constructor(checkoutConfig) {
33
33
  this.onLoaderChange = (isLoading) => {
34
- this.primerWrapper.disableButtons(isLoading);
35
34
  document
36
35
  .querySelectorAll(`${this.containerSelector} .loader-container`)
37
36
  ?.forEach(loaderEl => {
@@ -95,6 +94,7 @@ class DefaultSkin {
95
94
  this.onMethodsAvailable = (methods) => {
96
95
  this.availableMethods = methods;
97
96
  this.initAccordion();
97
+ methods.forEach(this.onMethodRender);
98
98
  };
99
99
  this.onStartPurchase = (paymentMethod) => {
100
100
  this.currentPurchaseMethod = paymentMethod;
@@ -115,7 +115,6 @@ class DefaultSkin {
115
115
  throw new Error(`Container element not found for selector: ${this.containerSelector}`);
116
116
  }
117
117
  this.containerEl = containerEl;
118
- this.primerWrapper = primerWrapper;
119
118
  this.checkoutConfig = checkoutConfig;
120
119
  }
121
120
  initAccordion() {
@@ -214,8 +213,8 @@ class DefaultSkin {
214
213
  };
215
214
  }
216
215
  }
217
- const createDefaultSkin = async (primerWrapper, checkoutConfig) => {
218
- const skin = new DefaultSkin(primerWrapper, checkoutConfig);
216
+ const createDefaultSkin = async (checkoutConfig) => {
217
+ const skin = new DefaultSkin(checkoutConfig);
219
218
  await skin['init']();
220
219
  return skin;
221
220
  };
@@ -38,6 +38,9 @@ class CardSkin {
38
38
  this.onMethodRender = () => {
39
39
  this.containerEl.style.display = 'block';
40
40
  };
41
+ this.onDestroy = () => {
42
+ this.containerEl.remove();
43
+ };
41
44
  if (!containerEl) {
42
45
  throw new Error('Container element not found');
43
46
  }
@@ -49,15 +52,19 @@ class CardSkin {
49
52
  const cardNumber = this.containerEl.querySelector('#cardNumberInput');
50
53
  const expiryDate = this.containerEl.querySelector('#expiryInput');
51
54
  const cvv = this.containerEl.querySelector('#cvvInput');
52
- let cardholderName = null;
53
- if (this.checkoutConfig?.card?.cardholderName) {
55
+ const hasCardholderInput = !!this.checkoutConfig?.card?.cardholderName;
56
+ let cardholderName = undefined;
57
+ if (hasCardholderInput) {
54
58
  cardholderName =
55
59
  this.containerEl.querySelector('#cardHolderInput');
56
60
  }
57
61
  else {
58
62
  this.containerEl.querySelector('#cardHolderInput').parentElement.style.display = 'none';
59
63
  }
60
- if (!cardNumber || !expiryDate || !cvv) {
64
+ if (!cardNumber ||
65
+ !expiryDate ||
66
+ !cvv ||
67
+ (hasCardholderInput && !cardholderName)) {
61
68
  throw new Error('One or more card input elements are missing in the default skin');
62
69
  }
63
70
  this.cardInputElements = {
@@ -27,9 +27,8 @@ const paymentMethodTemplates = {
27
27
  [PaymentMethod.APPLE_PAY]: applePayTemplate,
28
28
  };
29
29
  class DefaultSkin {
30
- constructor(primerWrapper, checkoutConfig) {
30
+ constructor(checkoutConfig) {
31
31
  this.onLoaderChange = (isLoading) => {
32
- this.primerWrapper.disableButtons(isLoading);
33
32
  document
34
33
  .querySelectorAll(`${this.containerSelector} .loader-container`)
35
34
  ?.forEach(loaderEl => {
@@ -93,6 +92,7 @@ class DefaultSkin {
93
92
  this.onMethodsAvailable = (methods) => {
94
93
  this.availableMethods = methods;
95
94
  this.initAccordion();
95
+ methods.forEach(this.onMethodRender);
96
96
  };
97
97
  this.onStartPurchase = (paymentMethod) => {
98
98
  this.currentPurchaseMethod = paymentMethod;
@@ -113,7 +113,6 @@ class DefaultSkin {
113
113
  throw new Error(`Container element not found for selector: ${this.containerSelector}`);
114
114
  }
115
115
  this.containerEl = containerEl;
116
- this.primerWrapper = primerWrapper;
117
116
  this.checkoutConfig = checkoutConfig;
118
117
  }
119
118
  initAccordion() {
@@ -212,8 +211,8 @@ class DefaultSkin {
212
211
  };
213
212
  }
214
213
  }
215
- const createDefaultSkin = async (primerWrapper, checkoutConfig) => {
216
- const skin = new DefaultSkin(primerWrapper, checkoutConfig);
214
+ const createDefaultSkin = async (checkoutConfig) => {
215
+ const skin = new DefaultSkin(checkoutConfig);
217
216
  await skin['init']();
218
217
  return skin;
219
218
  };
@@ -36,6 +36,9 @@ class CardSkin {
36
36
  this.onMethodRender = () => {
37
37
  this.containerEl.style.display = 'block';
38
38
  };
39
+ this.onDestroy = () => {
40
+ this.containerEl.remove();
41
+ };
39
42
  if (!containerEl) {
40
43
  throw new Error('Container element not found');
41
44
  }
@@ -47,15 +50,19 @@ class CardSkin {
47
50
  const cardNumber = this.containerEl.querySelector('#cardNumberInput');
48
51
  const expiryDate = this.containerEl.querySelector('#expiryInput');
49
52
  const cvv = this.containerEl.querySelector('#cvvInput');
50
- let cardholderName = null;
51
- if (this.checkoutConfig?.card?.cardholderName) {
53
+ const hasCardholderInput = !!this.checkoutConfig?.card?.cardholderName;
54
+ let cardholderName = undefined;
55
+ if (hasCardholderInput) {
52
56
  cardholderName =
53
57
  this.containerEl.querySelector('#cardHolderInput');
54
58
  }
55
59
  else {
56
60
  this.containerEl.querySelector('#cardHolderInput').parentElement.style.display = 'none';
57
61
  }
58
- if (!cardNumber || !expiryDate || !cvv) {
62
+ if (!cardNumber ||
63
+ !expiryDate ||
64
+ !cvv ||
65
+ (hasCardholderInput && !cardholderName)) {
59
66
  throw new Error('One or more card input elements are missing in the default skin');
60
67
  }
61
68
  this.cardInputElements = {
@@ -206,6 +206,132 @@ function withTimeout(promise, timeoutMs, message = 'Operation timed out') {
206
206
  return Promise.race([promise, timeoutPromise]);
207
207
  }
208
208
 
209
+ /**
210
+ * @fileoverview Dynamic loader for Primer SDK
211
+ * Loads Primer script and CSS from CDN independently of bundler
212
+ */
213
+ const PRIMER_CDN_BASE = 'https://sdk.primer.io/web';
214
+ const DEFAULT_VERSION = '2.57.3';
215
+ // Integrity hashes for specific versions (for SRI security)
216
+ const INTEGRITY_HASHES = {
217
+ '2.57.3': {
218
+ js: 'sha384-xq2SWkYvTlKOMpuXQUXq1QI3eZN7JiqQ3Sc72U9wY1IE30MW3HkwQWg/1n6BTMz4',
219
+ },
220
+ };
221
+ let loadingPromise = null;
222
+ let isLoaded = false;
223
+ /**
224
+ * Injects a script tag into the document head
225
+ */
226
+ function injectScript(src, integrity) {
227
+ return new Promise((resolve, reject) => {
228
+ // Check if script already exists
229
+ const existingScript = document.querySelector(`script[src="${src}"]`);
230
+ if (existingScript) {
231
+ resolve(existingScript);
232
+ return;
233
+ }
234
+ const script = document.createElement('script');
235
+ script.src = src;
236
+ script.async = true;
237
+ script.crossOrigin = 'anonymous';
238
+ if (integrity) {
239
+ script.integrity = integrity;
240
+ }
241
+ script.onload = () => resolve(script);
242
+ script.onerror = () => reject(new Error(`Failed to load Primer SDK script from ${src}`));
243
+ document.head.appendChild(script);
244
+ });
245
+ }
246
+ /**
247
+ * Injects a CSS link tag into the document head
248
+ */
249
+ function injectCSS(href, integrity) {
250
+ return new Promise((resolve, reject) => {
251
+ // Check if stylesheet already exists
252
+ const existingLink = document.querySelector(`link[href="${href}"]`);
253
+ if (existingLink) {
254
+ resolve(existingLink);
255
+ return;
256
+ }
257
+ const link = document.createElement('link');
258
+ link.rel = 'stylesheet';
259
+ link.href = href;
260
+ link.crossOrigin = 'anonymous';
261
+ if (integrity) {
262
+ link.integrity = integrity;
263
+ }
264
+ link.onload = () => resolve(link);
265
+ link.onerror = () => reject(new Error(`Failed to load Primer SDK CSS from ${href}`));
266
+ document.head.appendChild(link);
267
+ });
268
+ }
269
+ /**
270
+ * Waits for window.Primer to be available
271
+ */
272
+ function waitForPrimer(timeout = 10000) {
273
+ return new Promise((resolve, reject) => {
274
+ const startTime = Date.now();
275
+ const check = () => {
276
+ if (typeof window !== 'undefined' &&
277
+ window.Primer &&
278
+ typeof window.Primer.createHeadless === 'function') {
279
+ resolve();
280
+ return;
281
+ }
282
+ if (Date.now() - startTime > timeout) {
283
+ reject(new Error('Timeout waiting for Primer SDK to initialize on window'));
284
+ return;
285
+ }
286
+ setTimeout(check, 50);
287
+ };
288
+ check();
289
+ });
290
+ }
291
+ /**
292
+ * Loads the Primer SDK script and CSS from CDN
293
+ * @param version - The version of Primer SDK to load (default: 2.57.3)
294
+ * @returns Promise that resolves when SDK is loaded and ready
295
+ */
296
+ async function loadPrimerSDK(version) {
297
+ // Already loaded
298
+ if (isLoaded) {
299
+ return;
300
+ }
301
+ // Already loading - return existing promise
302
+ if (loadingPromise) {
303
+ return loadingPromise;
304
+ }
305
+ // Check if Primer is already available (user may have loaded it manually)
306
+ if (typeof window !== 'undefined' &&
307
+ window.Primer &&
308
+ typeof window.Primer.createHeadless === 'function') {
309
+ isLoaded = true;
310
+ return;
311
+ }
312
+ const ver = version || DEFAULT_VERSION;
313
+ const jsUrl = `${PRIMER_CDN_BASE}/v${ver}/Primer.min.js`;
314
+ const cssUrl = `${PRIMER_CDN_BASE}/v${ver}/Checkout.css`;
315
+ const hashes = INTEGRITY_HASHES[ver];
316
+ loadingPromise = (async () => {
317
+ try {
318
+ // Load CSS and JS in parallel
319
+ await Promise.all([
320
+ injectCSS(cssUrl, hashes?.css),
321
+ injectScript(jsUrl, hashes?.js),
322
+ ]);
323
+ // Wait for Primer to be available on window
324
+ await waitForPrimer();
325
+ isLoaded = true;
326
+ }
327
+ catch (error) {
328
+ loadingPromise = null;
329
+ throw error;
330
+ }
331
+ })();
332
+ return loadingPromise;
333
+ }
334
+
209
335
  exports.PaymentMethod = void 0;
210
336
  (function (PaymentMethod) {
211
337
  PaymentMethod["GOOGLE_PAY"] = "GOOGLE_PAY";
@@ -217,7 +343,7 @@ exports.PaymentMethod = void 0;
217
343
  /**
218
344
  * @fileoverview Constants for Funnefox SDK
219
345
  */
220
- const SDK_VERSION = '0.5.0-beta.3';
346
+ const SDK_VERSION = '0.5.0';
221
347
  const DEFAULTS = {
222
348
  BASE_URL: 'https://billing.funnelfox.com',
223
349
  REGION: 'default',
@@ -312,12 +438,28 @@ class PrimerWrapper {
312
438
  this.destroyCallbacks = [];
313
439
  this.headless = null;
314
440
  this.availableMethods = [];
441
+ this.paymentMethodsInterfaces = [];
315
442
  }
316
443
  isPrimerAvailable() {
317
444
  return (typeof window !== 'undefined' &&
318
445
  window.Primer &&
319
446
  typeof window.Primer?.createHeadless === 'function');
320
447
  }
448
+ /**
449
+ * Loads Primer SDK if not already available
450
+ * @param version - Optional version to load (uses default if not specified)
451
+ */
452
+ async ensurePrimerLoaded(version) {
453
+ if (this.isPrimerAvailable()) {
454
+ return;
455
+ }
456
+ try {
457
+ await loadPrimerSDK(version);
458
+ }
459
+ catch (error) {
460
+ throw new PrimerError('Failed to load Primer SDK', error);
461
+ }
462
+ }
321
463
  ensurePrimerAvailable() {
322
464
  if (!this.isPrimerAvailable()) {
323
465
  throw new PrimerError('Primer SDK not found. Please include the Primer SDK script before initializing FunnefoxSDK.');
@@ -327,7 +469,8 @@ class PrimerWrapper {
327
469
  if (this.headless) {
328
470
  return this.headless;
329
471
  }
330
- this.ensurePrimerAvailable();
472
+ // Load Primer SDK if not already available
473
+ await this.ensurePrimerLoaded();
331
474
  const primerOptions = merge({
332
475
  paymentHandling: 'MANUAL',
333
476
  apiVersion: '2.4',
@@ -380,7 +523,8 @@ class PrimerWrapper {
380
523
  }
381
524
  async renderButton(allowedPaymentMethod, { htmlNode, onMethodRenderError, onMethodRender, }) {
382
525
  let button;
383
- this.ensurePrimerAvailable();
526
+ // Ensure Primer SDK is loaded
527
+ await this.ensurePrimerLoaded();
384
528
  if (!this.headless) {
385
529
  throw new PrimerError('Headless checkout not found');
386
530
  }
@@ -415,20 +559,24 @@ class PrimerWrapper {
415
559
  !options.onInputChange) {
416
560
  throw new PrimerError('Card elements, onSubmit, and onInputChange are required for PAYMENT_CARD method');
417
561
  }
418
- return await this.renderCardCheckoutWithElements(options.cardElements, {
562
+ const cardInterface = await this.renderCardCheckoutWithElements(options.cardElements, {
419
563
  onSubmit: options.onSubmit,
420
564
  onInputChange: options.onInputChange,
421
565
  onMethodRenderError: options.onMethodRenderError,
422
566
  onMethodRender: options.onMethodRender,
423
567
  });
568
+ this.paymentMethodsInterfaces.push(cardInterface);
569
+ return cardInterface;
424
570
  }
425
571
  else {
426
572
  try {
427
- return await this.renderButton(method, {
573
+ const buttonInterface = await this.renderButton(method, {
428
574
  htmlNode,
429
575
  onMethodRenderError: options.onMethodRenderError,
430
576
  onMethodRender: options.onMethodRender,
431
577
  });
578
+ this.paymentMethodsInterfaces.push(buttonInterface);
579
+ return buttonInterface;
432
580
  }
433
581
  catch (error) {
434
582
  throw new PrimerError('Failed to initialize Primer checkout', error);
@@ -513,7 +661,12 @@ class PrimerWrapper {
513
661
  cardNumberInput.setDisabled(disabled);
514
662
  expiryInput.setDisabled(disabled);
515
663
  cvvInput.setDisabled(disabled);
516
- elements.button.disabled = disabled;
664
+ if (elements.button) {
665
+ elements.button.disabled = disabled;
666
+ }
667
+ if (elements.cardholderName) {
668
+ elements.cardholderName.disabled = disabled;
669
+ }
517
670
  },
518
671
  submit: () => onSubmitHandler(),
519
672
  destroy: () => {
@@ -555,7 +708,7 @@ class PrimerWrapper {
555
708
  const { cardElements, paymentButtonElements, container, onSubmit, onInputChange, onMethodRender, onMethodRenderError, onMethodsAvailable, } = checkoutRenderOptions;
556
709
  await this.initializeHeadlessCheckout(clientToken, checkoutOptions);
557
710
  onMethodsAvailable?.(this.availableMethods);
558
- return Promise.all(this.availableMethods.map(method => {
711
+ await Promise.all(this.availableMethods.map(method => {
559
712
  if (method === exports.PaymentMethod.PAYMENT_CARD) {
560
713
  // For card, use the main container
561
714
  return this.initMethod(method, container, {
@@ -579,10 +732,8 @@ class PrimerWrapper {
579
732
  onMethodRenderError,
580
733
  });
581
734
  }
582
- })).then((interfaces) => {
583
- this.paymentMethodsInterfaces = interfaces;
584
- this.isInitialized = true;
585
- });
735
+ }));
736
+ this.isInitialized = true;
586
737
  }
587
738
  wrapTokenizeHandler(handler) {
588
739
  return async (paymentMethodTokenData, primerHandler) => {
@@ -938,6 +1089,7 @@ class CheckoutInstance extends EventEmitter {
938
1089
  };
939
1090
  this.onLoaderChangeWithRace = (state) => {
940
1091
  const isLoading = !!(state ? ++this.counter : --this.counter);
1092
+ this.primerWrapper.disableButtons(isLoading);
941
1093
  this.emit(EVENTS.LOADER_CHANGE, isLoading);
942
1094
  };
943
1095
  this.id = generateId('checkout_');
@@ -1021,14 +1173,15 @@ class CheckoutInstance extends EventEmitter {
1021
1173
  ].join('-');
1022
1174
  let sessionResponse;
1023
1175
  // Return cached response if payload hasn't changed
1024
- const cachedResponse = CheckoutInstance.sessionCache.get(cacheKey);
1176
+ const cachedResponse = await CheckoutInstance.sessionCache.get(cacheKey);
1025
1177
  if (cachedResponse) {
1026
1178
  sessionResponse = cachedResponse;
1027
1179
  }
1028
1180
  else {
1029
- sessionResponse = await this.apiClient.createClientSession(sessionParams);
1181
+ const sessionRequest = this.apiClient.createClientSession(sessionParams);
1030
1182
  // Cache the successful response
1031
- CheckoutInstance.sessionCache.set(cacheKey, sessionResponse);
1183
+ CheckoutInstance.sessionCache.set(cacheKey, sessionRequest);
1184
+ sessionResponse = await sessionRequest;
1032
1185
  }
1033
1186
  const sessionData = this.apiClient.processSessionResponse(sessionResponse);
1034
1187
  this.orderId = sessionData.orderId;
@@ -1257,18 +1410,18 @@ class CheckoutInstance extends EventEmitter {
1257
1410
  async getDefaultSkinCheckoutOptions() {
1258
1411
  const skinFactory = (await Promise.resolve().then(function () { return require('./chunk-index.cjs.js'); }))
1259
1412
  .default;
1260
- const skin = await skinFactory(this.primerWrapper, this.checkoutConfig);
1413
+ const skin = await skinFactory(this.checkoutConfig);
1261
1414
  this.on(EVENTS.INPUT_ERROR, skin.onInputError);
1262
1415
  this.on(EVENTS.STATUS_CHANGE, skin.onStatusChange);
1263
1416
  this.on(EVENTS.ERROR, (error) => skin.onError(error));
1264
1417
  this.on(EVENTS.LOADER_CHANGE, skin.onLoaderChange);
1265
1418
  this.on(EVENTS.DESTROY, skin.onDestroy);
1266
- this.on(EVENTS.METHOD_RENDER, skin.onMethodRender);
1267
1419
  this.on(EVENTS.SUCCESS, skin.onSuccess);
1268
1420
  this.on(EVENTS.START_PURCHASE, skin.onStartPurchase);
1269
1421
  this.on(EVENTS.PURCHASE_FAILURE, skin.onPurchaseFailure);
1270
1422
  this.on(EVENTS.PURCHASE_COMPLETED, skin.onPurchaseCompleted);
1271
1423
  this.on(EVENTS.METHODS_AVAILABLE, skin.onMethodsAvailable);
1424
+ this.on(EVENTS.METHODS_AVAILABLE, this.hideInitializingLoader);
1272
1425
  return skin.getCheckoutOptions();
1273
1426
  }
1274
1427
  async getCardDefaultSkinCheckoutOptions(node) {
@@ -1277,6 +1430,7 @@ class CheckoutInstance extends EventEmitter {
1277
1430
  skin.init();
1278
1431
  this.on(EVENTS.INPUT_ERROR, skin.onInputError);
1279
1432
  this.on(EVENTS.METHOD_RENDER, skin.onMethodRender);
1433
+ this.on(EVENTS.SUCCESS, skin.onDestroy);
1280
1434
  return skin.getCheckoutOptions();
1281
1435
  }
1282
1436
  showInitializingLoader() {
@@ -1311,8 +1465,9 @@ function resolveConfig(options, functionName) {
1311
1465
  }
1312
1466
  async function createCheckout(options) {
1313
1467
  const { ...checkoutConfig } = options;
1468
+ // Ensure Primer SDK is loaded before creating checkout
1314
1469
  const primerWrapper = new PrimerWrapper();
1315
- primerWrapper.ensurePrimerAvailable();
1470
+ await primerWrapper.ensurePrimerLoaded();
1316
1471
  const config = resolveConfig(options, 'createCheckout');
1317
1472
  const checkout = new CheckoutInstance({
1318
1473
  ...config,
@@ -1365,6 +1520,9 @@ async function silentPurchase(options) {
1365
1520
  return true;
1366
1521
  }
1367
1522
  async function initMethod(method, element, options) {
1523
+ // Ensure Primer SDK is loaded before initializing payment method
1524
+ const primerWrapper = new PrimerWrapper();
1525
+ await primerWrapper.ensurePrimerLoaded();
1368
1526
  const checkoutInstance = new CheckoutInstance({
1369
1527
  orgId: options.orgId,
1370
1528
  baseUrl: options.baseUrl,