@funnelfox/billing 0.5.0-beta.4 → 0.5.1

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.4';
348
+ const SDK_VERSION = '0.5.1';
223
349
  const DEFAULTS = {
224
350
  BASE_URL: 'https://billing.funnelfox.com',
225
351
  REGION: 'default',
@@ -312,7 +438,6 @@
312
438
  constructor() {
313
439
  this.isInitialized = false;
314
440
  this.destroyCallbacks = [];
315
- this.headless = null;
316
441
  this.availableMethods = [];
317
442
  this.paymentMethodsInterfaces = [];
318
443
  }
@@ -321,24 +446,43 @@
321
446
  window.Primer &&
322
447
  typeof window.Primer?.createHeadless === 'function');
323
448
  }
449
+ /**
450
+ * Loads Primer SDK if not already available
451
+ * @param version - Optional version to load (uses default if not specified)
452
+ */
453
+ async ensurePrimerLoaded(version) {
454
+ if (this.isPrimerAvailable()) {
455
+ return;
456
+ }
457
+ try {
458
+ await loadPrimerSDK(version);
459
+ }
460
+ catch (error) {
461
+ throw new PrimerError('Failed to load Primer SDK', error);
462
+ }
463
+ }
324
464
  ensurePrimerAvailable() {
325
465
  if (!this.isPrimerAvailable()) {
326
466
  throw new PrimerError('Primer SDK not found. Please include the Primer SDK script before initializing FunnefoxSDK.');
327
467
  }
328
468
  }
329
469
  async createHeadlessCheckout(clientToken, options) {
330
- if (this.headless) {
331
- return this.headless;
470
+ if (PrimerWrapper.headless) {
471
+ return PrimerWrapper.headless;
332
472
  }
333
- this.ensurePrimerAvailable();
473
+ // Load Primer SDK if not already available
474
+ await this.ensurePrimerLoaded();
334
475
  const primerOptions = merge({
335
476
  paymentHandling: 'MANUAL',
336
477
  apiVersion: '2.4',
337
478
  }, options);
338
479
  try {
339
- const headless = await window.Primer.createHeadless(clientToken, primerOptions);
340
- await headless.start();
341
- this.headless = headless;
480
+ PrimerWrapper.headless = window.Primer.createHeadless(clientToken, primerOptions).then(async (headlessPromise) => {
481
+ const headless = await headlessPromise;
482
+ await headless.start();
483
+ return headless;
484
+ });
485
+ return await PrimerWrapper.headless;
342
486
  }
343
487
  catch (error) {
344
488
  throw new PrimerError('Failed to create Primer headless checkout', error);
@@ -351,52 +495,21 @@
351
495
  paymentMethodInterface.setDisabled(disabled);
352
496
  }
353
497
  }
354
- waitForPayPalReady() {
355
- return new Promise((resolve, reject) => {
356
- let counter = 0;
357
- const checkPayPalEnabler = async () => {
358
- /**
359
- * Wait 1000 seconds for PayPal SDK to initialize
360
- */
361
- await new Promise(resolve => {
362
- setTimeout(() => {
363
- resolve();
364
- }, 1000);
365
- });
366
- /**
367
- * @link https://github.com/krakenjs/zoid/issues/334
368
- */
369
- // @ts-expect-error paymentMethod is private property
370
- const isPayPalReady = !!window?.paypalPrimer?.Buttons?.instances?.[0];
371
- if (++counter < 20 && !isPayPalReady) {
372
- setTimeout(checkPayPalEnabler, 0);
373
- }
374
- else if (!isPayPalReady) {
375
- reject(new PrimerError('PayPal paypal_js_sdk_v5_unhandled_exception was detected', exports.PaymentMethod.PAYPAL));
376
- }
377
- else {
378
- resolve();
379
- }
380
- };
381
- checkPayPalEnabler();
382
- });
383
- }
384
498
  async renderButton(allowedPaymentMethod, { htmlNode, onMethodRenderError, onMethodRender, }) {
385
499
  let button;
386
- this.ensurePrimerAvailable();
387
- if (!this.headless) {
500
+ // Ensure Primer SDK is loaded
501
+ await this.ensurePrimerLoaded();
502
+ if (!PrimerWrapper.headless) {
388
503
  throw new PrimerError('Headless checkout not found');
389
504
  }
390
505
  try {
391
- const pmManager = await this.headless.createPaymentMethodManager(allowedPaymentMethod);
506
+ const headless = await PrimerWrapper.headless;
507
+ const pmManager = await headless.createPaymentMethodManager(allowedPaymentMethod);
392
508
  if (!pmManager) {
393
509
  throw new Error('Payment method manager is not available');
394
510
  }
395
511
  button = pmManager.createButton();
396
512
  await button.render(htmlNode, {});
397
- if (allowedPaymentMethod === exports.PaymentMethod.PAYPAL) {
398
- await this.waitForPayPalReady();
399
- }
400
513
  this.destroyCallbacks.push(() => button.clean());
401
514
  onMethodRender(allowedPaymentMethod);
402
515
  return {
@@ -412,23 +525,23 @@
412
525
  }
413
526
  }
414
527
  async initMethod(method, htmlNode, options) {
415
- if (method === exports.PaymentMethod.PAYMENT_CARD) {
416
- if (!options.cardElements ||
417
- !options.onSubmit ||
418
- !options.onInputChange) {
419
- throw new PrimerError('Card elements, onSubmit, and onInputChange are required for PAYMENT_CARD method');
528
+ try {
529
+ if (method === exports.PaymentMethod.PAYMENT_CARD) {
530
+ if (!options.cardElements ||
531
+ !options.onSubmit ||
532
+ !options.onInputChange) {
533
+ throw new PrimerError('Card elements, onSubmit, and onInputChange are required for PAYMENT_CARD method');
534
+ }
535
+ const cardInterface = await this.renderCardCheckoutWithElements(options.cardElements, {
536
+ onSubmit: options.onSubmit,
537
+ onInputChange: options.onInputChange,
538
+ onMethodRenderError: options.onMethodRenderError,
539
+ onMethodRender: options.onMethodRender,
540
+ });
541
+ this.paymentMethodsInterfaces.push(cardInterface);
542
+ return cardInterface;
420
543
  }
421
- const cardInterface = await this.renderCardCheckoutWithElements(options.cardElements, {
422
- onSubmit: options.onSubmit,
423
- onInputChange: options.onInputChange,
424
- onMethodRenderError: options.onMethodRenderError,
425
- onMethodRender: options.onMethodRender,
426
- });
427
- this.paymentMethodsInterfaces.push(cardInterface);
428
- return cardInterface;
429
- }
430
- else {
431
- try {
544
+ else {
432
545
  const buttonInterface = await this.renderButton(method, {
433
546
  htmlNode,
434
547
  onMethodRenderError: options.onMethodRenderError,
@@ -437,14 +550,15 @@
437
550
  this.paymentMethodsInterfaces.push(buttonInterface);
438
551
  return buttonInterface;
439
552
  }
440
- catch (error) {
441
- throw new PrimerError('Failed to initialize Primer checkout', error);
442
- }
553
+ }
554
+ catch (error) {
555
+ throw new PrimerError('Failed to initialize Primer checkout', error);
443
556
  }
444
557
  }
445
558
  async renderCardCheckoutWithElements(elements, { onSubmit, onInputChange, onMethodRenderError, onMethodRender, }) {
446
559
  try {
447
- const pmManager = await this.headless.createPaymentMethodManager('PAYMENT_CARD');
560
+ const headless = await PrimerWrapper.headless;
561
+ const pmManager = await headless.createPaymentMethodManager('PAYMENT_CARD');
448
562
  if (!pmManager) {
449
563
  throw new Error('Payment method manager is not available');
450
564
  }
@@ -666,6 +780,7 @@
666
780
  return element;
667
781
  }
668
782
  }
783
+ PrimerWrapper.headless = null;
669
784
 
670
785
  /**
671
786
  * @fileoverview Input validation utilities for Funnefox SDK
@@ -1275,12 +1390,12 @@
1275
1390
  this.on(EVENTS.ERROR, (error) => skin.onError(error));
1276
1391
  this.on(EVENTS.LOADER_CHANGE, skin.onLoaderChange);
1277
1392
  this.on(EVENTS.DESTROY, skin.onDestroy);
1278
- this.on(EVENTS.METHOD_RENDER, skin.onMethodRender);
1279
1393
  this.on(EVENTS.SUCCESS, skin.onSuccess);
1280
1394
  this.on(EVENTS.START_PURCHASE, skin.onStartPurchase);
1281
1395
  this.on(EVENTS.PURCHASE_FAILURE, skin.onPurchaseFailure);
1282
1396
  this.on(EVENTS.PURCHASE_COMPLETED, skin.onPurchaseCompleted);
1283
1397
  this.on(EVENTS.METHODS_AVAILABLE, skin.onMethodsAvailable);
1398
+ this.on(EVENTS.METHODS_AVAILABLE, this.hideInitializingLoader);
1284
1399
  return skin.getCheckoutOptions();
1285
1400
  }
1286
1401
  async getCardDefaultSkinCheckoutOptions(node) {
@@ -1324,8 +1439,9 @@
1324
1439
  }
1325
1440
  async function createCheckout(options) {
1326
1441
  const { ...checkoutConfig } = options;
1442
+ // Ensure Primer SDK is loaded before creating checkout
1327
1443
  const primerWrapper = new PrimerWrapper();
1328
- primerWrapper.ensurePrimerAvailable();
1444
+ await primerWrapper.ensurePrimerLoaded();
1329
1445
  const config = resolveConfig(options, 'createCheckout');
1330
1446
  const checkout = new CheckoutInstance({
1331
1447
  ...config,
@@ -1378,6 +1494,9 @@
1378
1494
  return true;
1379
1495
  }
1380
1496
  async function initMethod(method, element, options) {
1497
+ // Ensure Primer SDK is loaded before initializing payment method
1498
+ const primerWrapper = new PrimerWrapper();
1499
+ await primerWrapper.ensurePrimerLoaded();
1381
1500
  const checkoutInstance = new CheckoutInstance({
1382
1501
  orgId: options.orgId,
1383
1502
  baseUrl: options.baseUrl,
@@ -1629,6 +1748,7 @@
1629
1748
  this.onMethodsAvailable = (methods) => {
1630
1749
  this.availableMethods = methods;
1631
1750
  this.initAccordion();
1751
+ methods.forEach(this.onMethodRender);
1632
1752
  };
1633
1753
  this.onStartPurchase = (paymentMethod) => {
1634
1754
  this.currentPurchaseMethod = paymentMethod;