@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.
@@ -202,6 +202,132 @@ function withTimeout(promise, timeoutMs, message = 'Operation timed out') {
202
202
  return Promise.race([promise, timeoutPromise]);
203
203
  }
204
204
 
205
+ /**
206
+ * @fileoverview Dynamic loader for Primer SDK
207
+ * Loads Primer script and CSS from CDN independently of bundler
208
+ */
209
+ const PRIMER_CDN_BASE = 'https://sdk.primer.io/web';
210
+ const DEFAULT_VERSION = '2.57.3';
211
+ // Integrity hashes for specific versions (for SRI security)
212
+ const INTEGRITY_HASHES = {
213
+ '2.57.3': {
214
+ js: 'sha384-xq2SWkYvTlKOMpuXQUXq1QI3eZN7JiqQ3Sc72U9wY1IE30MW3HkwQWg/1n6BTMz4',
215
+ },
216
+ };
217
+ let loadingPromise = null;
218
+ let isLoaded = false;
219
+ /**
220
+ * Injects a script tag into the document head
221
+ */
222
+ function injectScript(src, integrity) {
223
+ return new Promise((resolve, reject) => {
224
+ // Check if script already exists
225
+ const existingScript = document.querySelector(`script[src="${src}"]`);
226
+ if (existingScript) {
227
+ resolve(existingScript);
228
+ return;
229
+ }
230
+ const script = document.createElement('script');
231
+ script.src = src;
232
+ script.async = true;
233
+ script.crossOrigin = 'anonymous';
234
+ if (integrity) {
235
+ script.integrity = integrity;
236
+ }
237
+ script.onload = () => resolve(script);
238
+ script.onerror = () => reject(new Error(`Failed to load Primer SDK script from ${src}`));
239
+ document.head.appendChild(script);
240
+ });
241
+ }
242
+ /**
243
+ * Injects a CSS link tag into the document head
244
+ */
245
+ function injectCSS(href, integrity) {
246
+ return new Promise((resolve, reject) => {
247
+ // Check if stylesheet already exists
248
+ const existingLink = document.querySelector(`link[href="${href}"]`);
249
+ if (existingLink) {
250
+ resolve(existingLink);
251
+ return;
252
+ }
253
+ const link = document.createElement('link');
254
+ link.rel = 'stylesheet';
255
+ link.href = href;
256
+ link.crossOrigin = 'anonymous';
257
+ if (integrity) {
258
+ link.integrity = integrity;
259
+ }
260
+ link.onload = () => resolve(link);
261
+ link.onerror = () => reject(new Error(`Failed to load Primer SDK CSS from ${href}`));
262
+ document.head.appendChild(link);
263
+ });
264
+ }
265
+ /**
266
+ * Waits for window.Primer to be available
267
+ */
268
+ function waitForPrimer(timeout = 10000) {
269
+ return new Promise((resolve, reject) => {
270
+ const startTime = Date.now();
271
+ const check = () => {
272
+ if (typeof window !== 'undefined' &&
273
+ window.Primer &&
274
+ typeof window.Primer.createHeadless === 'function') {
275
+ resolve();
276
+ return;
277
+ }
278
+ if (Date.now() - startTime > timeout) {
279
+ reject(new Error('Timeout waiting for Primer SDK to initialize on window'));
280
+ return;
281
+ }
282
+ setTimeout(check, 50);
283
+ };
284
+ check();
285
+ });
286
+ }
287
+ /**
288
+ * Loads the Primer SDK script and CSS from CDN
289
+ * @param version - The version of Primer SDK to load (default: 2.57.3)
290
+ * @returns Promise that resolves when SDK is loaded and ready
291
+ */
292
+ async function loadPrimerSDK(version) {
293
+ // Already loaded
294
+ if (isLoaded) {
295
+ return;
296
+ }
297
+ // Already loading - return existing promise
298
+ if (loadingPromise) {
299
+ return loadingPromise;
300
+ }
301
+ // Check if Primer is already available (user may have loaded it manually)
302
+ if (typeof window !== 'undefined' &&
303
+ window.Primer &&
304
+ typeof window.Primer.createHeadless === 'function') {
305
+ isLoaded = true;
306
+ return;
307
+ }
308
+ const ver = version || DEFAULT_VERSION;
309
+ const jsUrl = `${PRIMER_CDN_BASE}/v${ver}/Primer.min.js`;
310
+ const cssUrl = `${PRIMER_CDN_BASE}/v${ver}/Checkout.css`;
311
+ const hashes = INTEGRITY_HASHES[ver];
312
+ loadingPromise = (async () => {
313
+ try {
314
+ // Load CSS and JS in parallel
315
+ await Promise.all([
316
+ injectCSS(cssUrl, hashes?.css),
317
+ injectScript(jsUrl, hashes?.js),
318
+ ]);
319
+ // Wait for Primer to be available on window
320
+ await waitForPrimer();
321
+ isLoaded = true;
322
+ }
323
+ catch (error) {
324
+ loadingPromise = null;
325
+ throw error;
326
+ }
327
+ })();
328
+ return loadingPromise;
329
+ }
330
+
205
331
  var PaymentMethod;
206
332
  (function (PaymentMethod) {
207
333
  PaymentMethod["GOOGLE_PAY"] = "GOOGLE_PAY";
@@ -213,7 +339,7 @@ var PaymentMethod;
213
339
  /**
214
340
  * @fileoverview Constants for Funnefox SDK
215
341
  */
216
- const SDK_VERSION = '0.5.0-beta.3';
342
+ const SDK_VERSION = '0.5.0';
217
343
  const DEFAULTS = {
218
344
  BASE_URL: 'https://billing.funnelfox.com',
219
345
  REGION: 'default',
@@ -308,12 +434,28 @@ class PrimerWrapper {
308
434
  this.destroyCallbacks = [];
309
435
  this.headless = null;
310
436
  this.availableMethods = [];
437
+ this.paymentMethodsInterfaces = [];
311
438
  }
312
439
  isPrimerAvailable() {
313
440
  return (typeof window !== 'undefined' &&
314
441
  window.Primer &&
315
442
  typeof window.Primer?.createHeadless === 'function');
316
443
  }
444
+ /**
445
+ * Loads Primer SDK if not already available
446
+ * @param version - Optional version to load (uses default if not specified)
447
+ */
448
+ async ensurePrimerLoaded(version) {
449
+ if (this.isPrimerAvailable()) {
450
+ return;
451
+ }
452
+ try {
453
+ await loadPrimerSDK(version);
454
+ }
455
+ catch (error) {
456
+ throw new PrimerError('Failed to load Primer SDK', error);
457
+ }
458
+ }
317
459
  ensurePrimerAvailable() {
318
460
  if (!this.isPrimerAvailable()) {
319
461
  throw new PrimerError('Primer SDK not found. Please include the Primer SDK script before initializing FunnefoxSDK.');
@@ -323,7 +465,8 @@ class PrimerWrapper {
323
465
  if (this.headless) {
324
466
  return this.headless;
325
467
  }
326
- this.ensurePrimerAvailable();
468
+ // Load Primer SDK if not already available
469
+ await this.ensurePrimerLoaded();
327
470
  const primerOptions = merge({
328
471
  paymentHandling: 'MANUAL',
329
472
  apiVersion: '2.4',
@@ -376,7 +519,8 @@ class PrimerWrapper {
376
519
  }
377
520
  async renderButton(allowedPaymentMethod, { htmlNode, onMethodRenderError, onMethodRender, }) {
378
521
  let button;
379
- this.ensurePrimerAvailable();
522
+ // Ensure Primer SDK is loaded
523
+ await this.ensurePrimerLoaded();
380
524
  if (!this.headless) {
381
525
  throw new PrimerError('Headless checkout not found');
382
526
  }
@@ -411,20 +555,24 @@ class PrimerWrapper {
411
555
  !options.onInputChange) {
412
556
  throw new PrimerError('Card elements, onSubmit, and onInputChange are required for PAYMENT_CARD method');
413
557
  }
414
- return await this.renderCardCheckoutWithElements(options.cardElements, {
558
+ const cardInterface = await this.renderCardCheckoutWithElements(options.cardElements, {
415
559
  onSubmit: options.onSubmit,
416
560
  onInputChange: options.onInputChange,
417
561
  onMethodRenderError: options.onMethodRenderError,
418
562
  onMethodRender: options.onMethodRender,
419
563
  });
564
+ this.paymentMethodsInterfaces.push(cardInterface);
565
+ return cardInterface;
420
566
  }
421
567
  else {
422
568
  try {
423
- return await this.renderButton(method, {
569
+ const buttonInterface = await this.renderButton(method, {
424
570
  htmlNode,
425
571
  onMethodRenderError: options.onMethodRenderError,
426
572
  onMethodRender: options.onMethodRender,
427
573
  });
574
+ this.paymentMethodsInterfaces.push(buttonInterface);
575
+ return buttonInterface;
428
576
  }
429
577
  catch (error) {
430
578
  throw new PrimerError('Failed to initialize Primer checkout', error);
@@ -509,7 +657,12 @@ class PrimerWrapper {
509
657
  cardNumberInput.setDisabled(disabled);
510
658
  expiryInput.setDisabled(disabled);
511
659
  cvvInput.setDisabled(disabled);
512
- elements.button.disabled = disabled;
660
+ if (elements.button) {
661
+ elements.button.disabled = disabled;
662
+ }
663
+ if (elements.cardholderName) {
664
+ elements.cardholderName.disabled = disabled;
665
+ }
513
666
  },
514
667
  submit: () => onSubmitHandler(),
515
668
  destroy: () => {
@@ -551,7 +704,7 @@ class PrimerWrapper {
551
704
  const { cardElements, paymentButtonElements, container, onSubmit, onInputChange, onMethodRender, onMethodRenderError, onMethodsAvailable, } = checkoutRenderOptions;
552
705
  await this.initializeHeadlessCheckout(clientToken, checkoutOptions);
553
706
  onMethodsAvailable?.(this.availableMethods);
554
- return Promise.all(this.availableMethods.map(method => {
707
+ await Promise.all(this.availableMethods.map(method => {
555
708
  if (method === PaymentMethod.PAYMENT_CARD) {
556
709
  // For card, use the main container
557
710
  return this.initMethod(method, container, {
@@ -575,10 +728,8 @@ class PrimerWrapper {
575
728
  onMethodRenderError,
576
729
  });
577
730
  }
578
- })).then((interfaces) => {
579
- this.paymentMethodsInterfaces = interfaces;
580
- this.isInitialized = true;
581
- });
731
+ }));
732
+ this.isInitialized = true;
582
733
  }
583
734
  wrapTokenizeHandler(handler) {
584
735
  return async (paymentMethodTokenData, primerHandler) => {
@@ -934,6 +1085,7 @@ class CheckoutInstance extends EventEmitter {
934
1085
  };
935
1086
  this.onLoaderChangeWithRace = (state) => {
936
1087
  const isLoading = !!(state ? ++this.counter : --this.counter);
1088
+ this.primerWrapper.disableButtons(isLoading);
937
1089
  this.emit(EVENTS.LOADER_CHANGE, isLoading);
938
1090
  };
939
1091
  this.id = generateId('checkout_');
@@ -1017,14 +1169,15 @@ class CheckoutInstance extends EventEmitter {
1017
1169
  ].join('-');
1018
1170
  let sessionResponse;
1019
1171
  // Return cached response if payload hasn't changed
1020
- const cachedResponse = CheckoutInstance.sessionCache.get(cacheKey);
1172
+ const cachedResponse = await CheckoutInstance.sessionCache.get(cacheKey);
1021
1173
  if (cachedResponse) {
1022
1174
  sessionResponse = cachedResponse;
1023
1175
  }
1024
1176
  else {
1025
- sessionResponse = await this.apiClient.createClientSession(sessionParams);
1177
+ const sessionRequest = this.apiClient.createClientSession(sessionParams);
1026
1178
  // Cache the successful response
1027
- CheckoutInstance.sessionCache.set(cacheKey, sessionResponse);
1179
+ CheckoutInstance.sessionCache.set(cacheKey, sessionRequest);
1180
+ sessionResponse = await sessionRequest;
1028
1181
  }
1029
1182
  const sessionData = this.apiClient.processSessionResponse(sessionResponse);
1030
1183
  this.orderId = sessionData.orderId;
@@ -1253,18 +1406,18 @@ class CheckoutInstance extends EventEmitter {
1253
1406
  async getDefaultSkinCheckoutOptions() {
1254
1407
  const skinFactory = (await import('./chunk-index.es.js'))
1255
1408
  .default;
1256
- const skin = await skinFactory(this.primerWrapper, this.checkoutConfig);
1409
+ const skin = await skinFactory(this.checkoutConfig);
1257
1410
  this.on(EVENTS.INPUT_ERROR, skin.onInputError);
1258
1411
  this.on(EVENTS.STATUS_CHANGE, skin.onStatusChange);
1259
1412
  this.on(EVENTS.ERROR, (error) => skin.onError(error));
1260
1413
  this.on(EVENTS.LOADER_CHANGE, skin.onLoaderChange);
1261
1414
  this.on(EVENTS.DESTROY, skin.onDestroy);
1262
- this.on(EVENTS.METHOD_RENDER, skin.onMethodRender);
1263
1415
  this.on(EVENTS.SUCCESS, skin.onSuccess);
1264
1416
  this.on(EVENTS.START_PURCHASE, skin.onStartPurchase);
1265
1417
  this.on(EVENTS.PURCHASE_FAILURE, skin.onPurchaseFailure);
1266
1418
  this.on(EVENTS.PURCHASE_COMPLETED, skin.onPurchaseCompleted);
1267
1419
  this.on(EVENTS.METHODS_AVAILABLE, skin.onMethodsAvailable);
1420
+ this.on(EVENTS.METHODS_AVAILABLE, this.hideInitializingLoader);
1268
1421
  return skin.getCheckoutOptions();
1269
1422
  }
1270
1423
  async getCardDefaultSkinCheckoutOptions(node) {
@@ -1273,6 +1426,7 @@ class CheckoutInstance extends EventEmitter {
1273
1426
  skin.init();
1274
1427
  this.on(EVENTS.INPUT_ERROR, skin.onInputError);
1275
1428
  this.on(EVENTS.METHOD_RENDER, skin.onMethodRender);
1429
+ this.on(EVENTS.SUCCESS, skin.onDestroy);
1276
1430
  return skin.getCheckoutOptions();
1277
1431
  }
1278
1432
  showInitializingLoader() {
@@ -1307,8 +1461,9 @@ function resolveConfig(options, functionName) {
1307
1461
  }
1308
1462
  async function createCheckout(options) {
1309
1463
  const { ...checkoutConfig } = options;
1464
+ // Ensure Primer SDK is loaded before creating checkout
1310
1465
  const primerWrapper = new PrimerWrapper();
1311
- primerWrapper.ensurePrimerAvailable();
1466
+ await primerWrapper.ensurePrimerLoaded();
1312
1467
  const config = resolveConfig(options, 'createCheckout');
1313
1468
  const checkout = new CheckoutInstance({
1314
1469
  ...config,
@@ -1361,6 +1516,9 @@ async function silentPurchase(options) {
1361
1516
  return true;
1362
1517
  }
1363
1518
  async function initMethod(method, element, options) {
1519
+ // Ensure Primer SDK is loaded before initializing payment method
1520
+ const primerWrapper = new PrimerWrapper();
1521
+ await primerWrapper.ensurePrimerLoaded();
1364
1522
  const checkoutInstance = new CheckoutInstance({
1365
1523
  orgId: options.orgId,
1366
1524
  baseUrl: options.baseUrl,