@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.
@@ -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.4';
342
+ const SDK_VERSION = '0.5.1';
217
343
  const DEFAULTS = {
218
344
  BASE_URL: 'https://billing.funnelfox.com',
219
345
  REGION: 'default',
@@ -306,7 +432,6 @@ class PrimerWrapper {
306
432
  constructor() {
307
433
  this.isInitialized = false;
308
434
  this.destroyCallbacks = [];
309
- this.headless = null;
310
435
  this.availableMethods = [];
311
436
  this.paymentMethodsInterfaces = [];
312
437
  }
@@ -315,24 +440,43 @@ class PrimerWrapper {
315
440
  window.Primer &&
316
441
  typeof window.Primer?.createHeadless === 'function');
317
442
  }
443
+ /**
444
+ * Loads Primer SDK if not already available
445
+ * @param version - Optional version to load (uses default if not specified)
446
+ */
447
+ async ensurePrimerLoaded(version) {
448
+ if (this.isPrimerAvailable()) {
449
+ return;
450
+ }
451
+ try {
452
+ await loadPrimerSDK(version);
453
+ }
454
+ catch (error) {
455
+ throw new PrimerError('Failed to load Primer SDK', error);
456
+ }
457
+ }
318
458
  ensurePrimerAvailable() {
319
459
  if (!this.isPrimerAvailable()) {
320
460
  throw new PrimerError('Primer SDK not found. Please include the Primer SDK script before initializing FunnefoxSDK.');
321
461
  }
322
462
  }
323
463
  async createHeadlessCheckout(clientToken, options) {
324
- if (this.headless) {
325
- return this.headless;
464
+ if (PrimerWrapper.headless) {
465
+ return PrimerWrapper.headless;
326
466
  }
327
- this.ensurePrimerAvailable();
467
+ // Load Primer SDK if not already available
468
+ await this.ensurePrimerLoaded();
328
469
  const primerOptions = merge({
329
470
  paymentHandling: 'MANUAL',
330
471
  apiVersion: '2.4',
331
472
  }, options);
332
473
  try {
333
- const headless = await window.Primer.createHeadless(clientToken, primerOptions);
334
- await headless.start();
335
- this.headless = headless;
474
+ PrimerWrapper.headless = window.Primer.createHeadless(clientToken, primerOptions).then(async (headlessPromise) => {
475
+ const headless = await headlessPromise;
476
+ await headless.start();
477
+ return headless;
478
+ });
479
+ return await PrimerWrapper.headless;
336
480
  }
337
481
  catch (error) {
338
482
  throw new PrimerError('Failed to create Primer headless checkout', error);
@@ -345,52 +489,21 @@ class PrimerWrapper {
345
489
  paymentMethodInterface.setDisabled(disabled);
346
490
  }
347
491
  }
348
- waitForPayPalReady() {
349
- return new Promise((resolve, reject) => {
350
- let counter = 0;
351
- const checkPayPalEnabler = async () => {
352
- /**
353
- * Wait 1000 seconds for PayPal SDK to initialize
354
- */
355
- await new Promise(resolve => {
356
- setTimeout(() => {
357
- resolve();
358
- }, 1000);
359
- });
360
- /**
361
- * @link https://github.com/krakenjs/zoid/issues/334
362
- */
363
- // @ts-expect-error paymentMethod is private property
364
- const isPayPalReady = !!window?.paypalPrimer?.Buttons?.instances?.[0];
365
- if (++counter < 20 && !isPayPalReady) {
366
- setTimeout(checkPayPalEnabler, 0);
367
- }
368
- else if (!isPayPalReady) {
369
- reject(new PrimerError('PayPal paypal_js_sdk_v5_unhandled_exception was detected', PaymentMethod.PAYPAL));
370
- }
371
- else {
372
- resolve();
373
- }
374
- };
375
- checkPayPalEnabler();
376
- });
377
- }
378
492
  async renderButton(allowedPaymentMethod, { htmlNode, onMethodRenderError, onMethodRender, }) {
379
493
  let button;
380
- this.ensurePrimerAvailable();
381
- if (!this.headless) {
494
+ // Ensure Primer SDK is loaded
495
+ await this.ensurePrimerLoaded();
496
+ if (!PrimerWrapper.headless) {
382
497
  throw new PrimerError('Headless checkout not found');
383
498
  }
384
499
  try {
385
- const pmManager = await this.headless.createPaymentMethodManager(allowedPaymentMethod);
500
+ const headless = await PrimerWrapper.headless;
501
+ const pmManager = await headless.createPaymentMethodManager(allowedPaymentMethod);
386
502
  if (!pmManager) {
387
503
  throw new Error('Payment method manager is not available');
388
504
  }
389
505
  button = pmManager.createButton();
390
506
  await button.render(htmlNode, {});
391
- if (allowedPaymentMethod === PaymentMethod.PAYPAL) {
392
- await this.waitForPayPalReady();
393
- }
394
507
  this.destroyCallbacks.push(() => button.clean());
395
508
  onMethodRender(allowedPaymentMethod);
396
509
  return {
@@ -406,23 +519,23 @@ class PrimerWrapper {
406
519
  }
407
520
  }
408
521
  async initMethod(method, htmlNode, options) {
409
- if (method === PaymentMethod.PAYMENT_CARD) {
410
- if (!options.cardElements ||
411
- !options.onSubmit ||
412
- !options.onInputChange) {
413
- throw new PrimerError('Card elements, onSubmit, and onInputChange are required for PAYMENT_CARD method');
522
+ try {
523
+ if (method === PaymentMethod.PAYMENT_CARD) {
524
+ if (!options.cardElements ||
525
+ !options.onSubmit ||
526
+ !options.onInputChange) {
527
+ throw new PrimerError('Card elements, onSubmit, and onInputChange are required for PAYMENT_CARD method');
528
+ }
529
+ const cardInterface = await this.renderCardCheckoutWithElements(options.cardElements, {
530
+ onSubmit: options.onSubmit,
531
+ onInputChange: options.onInputChange,
532
+ onMethodRenderError: options.onMethodRenderError,
533
+ onMethodRender: options.onMethodRender,
534
+ });
535
+ this.paymentMethodsInterfaces.push(cardInterface);
536
+ return cardInterface;
414
537
  }
415
- const cardInterface = await this.renderCardCheckoutWithElements(options.cardElements, {
416
- onSubmit: options.onSubmit,
417
- onInputChange: options.onInputChange,
418
- onMethodRenderError: options.onMethodRenderError,
419
- onMethodRender: options.onMethodRender,
420
- });
421
- this.paymentMethodsInterfaces.push(cardInterface);
422
- return cardInterface;
423
- }
424
- else {
425
- try {
538
+ else {
426
539
  const buttonInterface = await this.renderButton(method, {
427
540
  htmlNode,
428
541
  onMethodRenderError: options.onMethodRenderError,
@@ -431,14 +544,15 @@ class PrimerWrapper {
431
544
  this.paymentMethodsInterfaces.push(buttonInterface);
432
545
  return buttonInterface;
433
546
  }
434
- catch (error) {
435
- throw new PrimerError('Failed to initialize Primer checkout', error);
436
- }
547
+ }
548
+ catch (error) {
549
+ throw new PrimerError('Failed to initialize Primer checkout', error);
437
550
  }
438
551
  }
439
552
  async renderCardCheckoutWithElements(elements, { onSubmit, onInputChange, onMethodRenderError, onMethodRender, }) {
440
553
  try {
441
- const pmManager = await this.headless.createPaymentMethodManager('PAYMENT_CARD');
554
+ const headless = await PrimerWrapper.headless;
555
+ const pmManager = await headless.createPaymentMethodManager('PAYMENT_CARD');
442
556
  if (!pmManager) {
443
557
  throw new Error('Payment method manager is not available');
444
558
  }
@@ -660,6 +774,7 @@ class PrimerWrapper {
660
774
  return element;
661
775
  }
662
776
  }
777
+ PrimerWrapper.headless = null;
663
778
 
664
779
  /**
665
780
  * @fileoverview Input validation utilities for Funnefox SDK
@@ -1269,12 +1384,12 @@ class CheckoutInstance extends EventEmitter {
1269
1384
  this.on(EVENTS.ERROR, (error) => skin.onError(error));
1270
1385
  this.on(EVENTS.LOADER_CHANGE, skin.onLoaderChange);
1271
1386
  this.on(EVENTS.DESTROY, skin.onDestroy);
1272
- this.on(EVENTS.METHOD_RENDER, skin.onMethodRender);
1273
1387
  this.on(EVENTS.SUCCESS, skin.onSuccess);
1274
1388
  this.on(EVENTS.START_PURCHASE, skin.onStartPurchase);
1275
1389
  this.on(EVENTS.PURCHASE_FAILURE, skin.onPurchaseFailure);
1276
1390
  this.on(EVENTS.PURCHASE_COMPLETED, skin.onPurchaseCompleted);
1277
1391
  this.on(EVENTS.METHODS_AVAILABLE, skin.onMethodsAvailable);
1392
+ this.on(EVENTS.METHODS_AVAILABLE, this.hideInitializingLoader);
1278
1393
  return skin.getCheckoutOptions();
1279
1394
  }
1280
1395
  async getCardDefaultSkinCheckoutOptions(node) {
@@ -1318,8 +1433,9 @@ function resolveConfig(options, functionName) {
1318
1433
  }
1319
1434
  async function createCheckout(options) {
1320
1435
  const { ...checkoutConfig } = options;
1436
+ // Ensure Primer SDK is loaded before creating checkout
1321
1437
  const primerWrapper = new PrimerWrapper();
1322
- primerWrapper.ensurePrimerAvailable();
1438
+ await primerWrapper.ensurePrimerLoaded();
1323
1439
  const config = resolveConfig(options, 'createCheckout');
1324
1440
  const checkout = new CheckoutInstance({
1325
1441
  ...config,
@@ -1372,6 +1488,9 @@ async function silentPurchase(options) {
1372
1488
  return true;
1373
1489
  }
1374
1490
  async function initMethod(method, element, options) {
1491
+ // Ensure Primer SDK is loaded before initializing payment method
1492
+ const primerWrapper = new PrimerWrapper();
1493
+ await primerWrapper.ensurePrimerLoaded();
1375
1494
  const checkoutInstance = new CheckoutInstance({
1376
1495
  orgId: options.orgId,
1377
1496
  baseUrl: options.baseUrl,