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