@funnelfox/billing 0.2.1 → 0.3.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.
@@ -9,6 +9,7 @@
9
9
 
10
10
  Object.defineProperty(exports, '__esModule', { value: true });
11
11
 
12
+ /* eslint-disable @typescript-eslint/no-explicit-any */
12
13
  /**
13
14
  * @fileoverview Lightweight event emitter for Funnefox SDK
14
15
  */
@@ -65,7 +66,7 @@ class EventEmitter {
65
66
  }
66
67
  catch (error) {
67
68
  // eslint-disable-next-line no-console
68
- console.warn(`Error in event handler for "${eventName}":`, error);
69
+ console.warn(`Error in event handler for "${String(eventName)}":`, error);
69
70
  }
70
71
  }
71
72
  return true;
@@ -150,6 +151,7 @@ class NetworkError extends FunnefoxSDKError {
150
151
  }
151
152
  }
152
153
 
154
+ /* eslint-disable @typescript-eslint/no-explicit-any */
153
155
  /**
154
156
  * @fileoverview Helper utilities for Funnefox SDK
155
157
  */
@@ -215,7 +217,7 @@ var PaymentMethod;
215
217
  /**
216
218
  * @fileoverview Constants for Funnefox SDK
217
219
  */
218
- const SDK_VERSION = '0.2.1';
220
+ const SDK_VERSION = '0.3.0';
219
221
  const DEFAULTS = {
220
222
  BASE_URL: 'https://billing.funnelfox.com',
221
223
  REGION: 'default',
@@ -242,6 +244,9 @@ const EVENTS = {
242
244
  INPUT_ERROR: 'input-error',
243
245
  LOADER_CHANGE: 'loader-change',
244
246
  METHOD_RENDER: 'method-render',
247
+ START_PURCHASE: 'start-purchase',
248
+ PURCHASE_FAILURE: 'purchase-failure',
249
+ PURCHASE_COMPLETED: 'purchase-completed',
245
250
  };
246
251
  const API_ENDPOINTS = {
247
252
  CREATE_CLIENT_SESSION: '/v1/checkout/create_client_session',
@@ -348,7 +353,7 @@ class PrimerWrapper {
348
353
  this.paymentMethodsInterfaces[method].setDisabled(disabled);
349
354
  }
350
355
  }
351
- async renderCardCheckout({ onSubmit, cardSelectors, onInputChange, }) {
356
+ async renderCardCheckout({ onSubmitError, onSubmit, cardSelectors, onInputChange, }) {
352
357
  try {
353
358
  const elements = this.initializeCardElements(cardSelectors);
354
359
  const pmManager = await this.headless.createPaymentMethodManager('PAYMENT_CARD');
@@ -390,7 +395,9 @@ class PrimerWrapper {
390
395
  await pmManager.submit();
391
396
  }
392
397
  catch (error) {
393
- throw new PrimerError('Failed to submit payment', error);
398
+ const primerError = new PrimerError('Failed to submit payment', error);
399
+ onSubmitError(primerError);
400
+ throw primerError;
394
401
  }
395
402
  finally {
396
403
  onSubmit(false);
@@ -452,7 +459,7 @@ class PrimerWrapper {
452
459
  }
453
460
  }
454
461
  async renderCheckout(clientToken, options) {
455
- const { cardSelectors, paymentButtonSelectors, container, onTokenizeSuccess, onResumeSuccess, onSubmit, onInputChange, onMethodRender, ...restPrimerOptions } = options;
462
+ const { cardSelectors, paymentButtonSelectors, container, onTokenizeSuccess, onResumeSuccess, onSubmit, onInputChange, onMethodRender, onSubmitError, ...restPrimerOptions } = options;
456
463
  await this.createHeadlessCheckout(clientToken, {
457
464
  ...restPrimerOptions,
458
465
  onTokenizeSuccess: this.wrapTokenizeHandler(onTokenizeSuccess),
@@ -477,6 +484,7 @@ class PrimerWrapper {
477
484
  container,
478
485
  onSubmit,
479
486
  onInputChange,
487
+ onSubmitError,
480
488
  };
481
489
  this.availableMethods.forEach(async (method) => {
482
490
  if (method === PaymentMethod.PAYMENT_CARD) {
@@ -552,7 +560,7 @@ class PrimerWrapper {
552
560
  return this.destroyCallbacks;
553
561
  }
554
562
  isActive() {
555
- return this.isInitialized && this.destroyCallbacks.length;
563
+ return this.isInitialized && this.destroyCallbacks.length > 0;
556
564
  }
557
565
  validateContainer(selector) {
558
566
  const element = document.querySelector(selector);
@@ -572,10 +580,7 @@ class PrimerWrapper {
572
580
  * @fileoverview Input validation utilities for Funnefox SDK
573
581
  */
574
582
  function sanitizeString(input) {
575
- if (input === null || input === undefined) {
576
- return '';
577
- }
578
- return String(input).trim();
583
+ return input?.trim() || '';
579
584
  }
580
585
  function requireString(value, fieldName) {
581
586
  const sanitized = sanitizeString(value);
@@ -611,7 +616,7 @@ class APIClient {
611
616
  }, this.retryAttempts);
612
617
  }
613
618
  catch (error) {
614
- if (error.name === 'APIError') {
619
+ if (error instanceof Error && error.name === 'APIError') {
615
620
  throw error;
616
621
  }
617
622
  throw new NetworkError('Network request failed', error);
@@ -623,6 +628,9 @@ class APIClient {
623
628
  response = await fetch(url, options);
624
629
  }
625
630
  catch (error) {
631
+ if (error instanceof Error && error.name === 'NetworkError') {
632
+ throw error;
633
+ }
626
634
  throw new NetworkError('Network request failed', error);
627
635
  }
628
636
  let data;
@@ -633,8 +641,11 @@ class APIClient {
633
641
  throw new APIError('Invalid JSON response', response.status, {});
634
642
  }
635
643
  if (!response.ok) {
636
- const message = data.message || data.message || data.error || `HTTP ${response.status}`;
637
- throw new APIError(message?.[0]?.msg || message, response.status, {
644
+ const d = data;
645
+ const message = d.message instanceof Array
646
+ ? d.message[0].msg
647
+ : d.message || d.error || `HTTP ${response.status}`;
648
+ throw new APIError(message, response.status, {
638
649
  response: data,
639
650
  });
640
651
  }
@@ -652,10 +663,10 @@ class APIClient {
652
663
  if (params.countryCode !== undefined) {
653
664
  payload.country_code = params.countryCode;
654
665
  }
655
- return await this.request(API_ENDPOINTS.CREATE_CLIENT_SESSION, {
666
+ return (await this.request(API_ENDPOINTS.CREATE_CLIENT_SESSION, {
656
667
  method: 'POST',
657
668
  body: JSON.stringify(payload),
658
- });
669
+ }));
659
670
  }
660
671
  async updateClientSession(params) {
661
672
  const payload = {
@@ -673,20 +684,20 @@ class APIClient {
673
684
  order_id: params.orderId,
674
685
  payment_method_token: params.paymentMethodToken,
675
686
  };
676
- return await this.request(API_ENDPOINTS.CREATE_PAYMENT, {
687
+ return (await this.request(API_ENDPOINTS.CREATE_PAYMENT, {
677
688
  method: 'POST',
678
689
  body: JSON.stringify(payload),
679
- });
690
+ }));
680
691
  }
681
692
  async resumePayment(params) {
682
693
  const payload = {
683
694
  order_id: params.orderId,
684
695
  resume_token: params.resumeToken,
685
696
  };
686
- return await this.request(API_ENDPOINTS.RESUME_PAYMENT, {
697
+ return (await this.request(API_ENDPOINTS.RESUME_PAYMENT, {
687
698
  method: 'POST',
688
699
  body: JSON.stringify(payload),
689
- });
700
+ }));
690
701
  }
691
702
  processSessionResponse(response) {
692
703
  if (response.status === 'error') {
@@ -699,7 +710,7 @@ class APIClient {
699
710
  response,
700
711
  });
701
712
  }
702
- const data = response.data || response;
713
+ const data = response.data;
703
714
  return {
704
715
  type: 'session_created',
705
716
  orderId: data.order_id,
@@ -713,11 +724,10 @@ class APIClient {
713
724
  throw new APIError(message, null, {
714
725
  errorCode: firstError?.code,
715
726
  errorType: firstError?.type,
716
- requestId: response.req_id,
717
727
  response,
718
728
  });
719
729
  }
720
- const data = response.data || response;
730
+ const data = response.data;
721
731
  if (data.action_required_token) {
722
732
  return {
723
733
  type: 'action_required',
@@ -732,12 +742,13 @@ class APIClient {
732
742
  type: 'success',
733
743
  orderId: data.order_id,
734
744
  status: 'succeeded',
735
- transactionId: data.transaction_id,
736
745
  };
737
746
  case 'failed':
738
- throw new APIError(data.failed_message_for_user || 'Payment failed', null, data);
747
+ throw new APIError(data.failed_message_for_user || 'Payment failed', null, { response });
739
748
  case 'cancelled':
740
- throw new APIError('Payment was cancelled by user', null, data);
749
+ throw new APIError('Payment was cancelled by user', null, {
750
+ response,
751
+ });
741
752
  case 'processing':
742
753
  return {
743
754
  type: 'processing',
@@ -745,20 +756,10 @@ class APIClient {
745
756
  status: 'processing',
746
757
  };
747
758
  default:
748
- throw new APIError(`Unhandled checkout status: ${data.checkout_status}`, null, data);
759
+ throw new APIError(`Unhandled checkout status: ${data.checkout_status}`, null, { response });
749
760
  }
750
761
  }
751
- throw new APIError('Invalid payment response format', null, data);
752
- }
753
- processResponse(response) {
754
- const data = response.data || response;
755
- if (data.client_token && data.order_id && !data.checkout_status) {
756
- return this.processSessionResponse(response);
757
- }
758
- if (data.checkout_status || data.action_required_token) {
759
- return this.processPaymentResponse(response);
760
- }
761
- throw new APIError('Unknown response format', null, response);
762
+ throw new APIError('Invalid payment response format', null, { response });
762
763
  }
763
764
  }
764
765
 
@@ -776,13 +777,17 @@ class CheckoutInstance extends EventEmitter {
776
777
  this.emit(EVENTS.METHOD_RENDER, method);
777
778
  };
778
779
  this.handleSubmit = (isSubmitting) => {
780
+ if (isSubmitting) {
781
+ // Clear any previous errors
782
+ this.emit(EVENTS.ERROR, undefined);
783
+ this.emit(EVENTS.START_PURCHASE, PaymentMethod.PAYMENT_CARD);
784
+ }
779
785
  this.onLoaderChangeWithRace(isSubmitting);
780
- // Clear any previous errors
781
- this.emit(EVENTS.ERROR);
782
786
  this._setState(isSubmitting ? 'processing' : 'ready');
783
787
  };
784
788
  this.handleTokenizeSuccess = async (paymentMethodTokenData, primerHandler) => {
785
789
  try {
790
+ this.emit(EVENTS.START_PURCHASE, paymentMethodTokenData.paymentInstrumentType);
786
791
  this.onLoaderChangeWithRace(true);
787
792
  this._setState('processing');
788
793
  const paymentResponse = await this.apiClient.createPayment({
@@ -794,7 +799,7 @@ class CheckoutInstance extends EventEmitter {
794
799
  }
795
800
  catch (error) {
796
801
  this._setState('error');
797
- this.emit(EVENTS.ERROR, error);
802
+ this.emit(EVENTS.PURCHASE_FAILURE, error);
798
803
  primerHandler.handleFailure(error.message || 'Payment processing failed');
799
804
  }
800
805
  finally {
@@ -815,10 +820,11 @@ class CheckoutInstance extends EventEmitter {
815
820
  }
816
821
  catch (error) {
817
822
  this._setState('error');
818
- this.emit(EVENTS.ERROR, error);
823
+ this.emit(EVENTS.PURCHASE_FAILURE, error);
819
824
  primerHandler.handleFailure(error.message || 'Payment processing failed');
820
825
  }
821
826
  finally {
827
+ this.emit(EVENTS.PURCHASE_COMPLETED);
822
828
  this.onLoaderChangeWithRace(false);
823
829
  this._setState('ready');
824
830
  }
@@ -863,18 +869,6 @@ class CheckoutInstance extends EventEmitter {
863
869
  this.on(EVENTS.DESTROY, this.callbacks.onDestroy);
864
870
  }
865
871
  }
866
- on(eventName, handler) {
867
- return super.on(eventName, handler);
868
- }
869
- once(eventName, handler) {
870
- return super.once(eventName, handler);
871
- }
872
- off(eventName, handler = null) {
873
- return super.off(eventName, handler);
874
- }
875
- emit(eventName, ...args) {
876
- return super.emit(eventName, ...args);
877
- }
878
872
  removeAllListeners() {
879
873
  return super.removeAllListeners();
880
874
  }
@@ -916,10 +910,11 @@ class CheckoutInstance extends EventEmitter {
916
910
  onSubmit: this.handleSubmit,
917
911
  onInputChange: this.handleInputChange,
918
912
  onMethodRender: this.handleMethodRender,
913
+ onSubmitError: (error) => this.emit(EVENTS.PURCHASE_FAILURE, error),
919
914
  };
920
915
  if (!this.checkoutConfig.cardSelectors ||
921
916
  !this.checkoutConfig.paymentButtonSelectors) {
922
- const cardSelectors = await this.createCardElements(this.checkoutConfig.container);
917
+ const cardSelectors = await this.createCardElements();
923
918
  const paymentButtonSelectors = {
924
919
  paypal: '#paypalButton',
925
920
  googlePay: '#googlePayButton',
@@ -955,8 +950,6 @@ class CheckoutInstance extends EventEmitter {
955
950
  this.emit(EVENTS.SUCCESS, {
956
951
  orderId: result.orderId,
957
952
  status: result.status,
958
- transactionId: result.transactionId,
959
- metadata: result.metadata,
960
953
  });
961
954
  primerHandler.handleSuccess();
962
955
  break;
@@ -990,7 +983,6 @@ class CheckoutInstance extends EventEmitter {
990
983
  });
991
984
  this.checkoutConfig.priceId = newPriceId;
992
985
  this._setState('ready');
993
- this.emit(EVENTS.STATUS_CHANGE, 'price-updated');
994
986
  }
995
987
  catch (error) {
996
988
  this._setState('error');
@@ -1050,71 +1042,21 @@ class CheckoutInstance extends EventEmitter {
1050
1042
  }
1051
1043
  // Creates containers to render hosted inputs with labels and error messages,
1052
1044
  // a card holder input with label and error, and a submit button.
1053
- async createCardElements(container) {
1054
- await Promise.resolve().then(function () { return require('./chunk-index.cjs.js'); })
1055
- .then(module => module.default)
1056
- .then(init => init(this.checkoutConfig.container));
1057
- const cardNumberContainer = document.querySelector(`${container} #cardNumberInput`);
1058
- const cardExpiryContainer = document.querySelector(`${container} #expiryInput`);
1059
- const cardCvvContainer = document.querySelector(`${container} #cvvInput`);
1060
- const elementsMap = {
1061
- cardNumber: cardNumberContainer.parentElement,
1062
- expiryDate: cardExpiryContainer.parentElement,
1063
- cvv: cardCvvContainer.parentElement,
1064
- };
1065
- const onLoaderChange = (isLoading) => {
1066
- this.primerWrapper.disableButtons(isLoading);
1067
- document
1068
- .querySelectorAll(`${container} .loader-container`)
1069
- ?.forEach(loaderEl => {
1070
- loaderEl.style.display = isLoading ? 'flex' : 'none';
1071
- });
1072
- };
1073
- this.on(EVENTS.INPUT_ERROR, event => {
1074
- const { name, error } = event;
1075
- const errorContainer = elementsMap[name]?.querySelector('.errorContainer');
1076
- if (errorContainer) {
1077
- errorContainer.textContent = error || '';
1078
- }
1079
- });
1080
- this.on(EVENTS.STATUS_CHANGE, (state, oldState) => {
1081
- const isLoading = ['initializing'].includes(state);
1082
- if (!isLoading && oldState === 'initializing') {
1083
- onLoaderChange(false);
1084
- }
1085
- });
1086
- function setError(error) {
1087
- const errorContainer = document.querySelector('.payment-errors-container');
1088
- if (errorContainer) {
1089
- errorContainer.textContent = error?.message || '';
1090
- }
1091
- }
1092
- this.on(EVENTS.ERROR, (error) => {
1093
- setError(error);
1094
- });
1095
- this.on(EVENTS.LOADER_CHANGE, onLoaderChange);
1096
- this.on(EVENTS.DESTROY, () => {
1097
- this.primerWrapper.validateContainer(container)?.remove();
1098
- });
1099
- this.on(EVENTS.METHOD_RENDER, (method) => {
1100
- const methodContainer = document.querySelector(`.ff-payment-method-${method.replace('_', '-').toLowerCase()}`);
1101
- methodContainer.classList.add('visible');
1102
- });
1103
- this.on(EVENTS.SUCCESS, () => {
1104
- const successScreenString = document.querySelector('#success-screen')?.innerHTML;
1105
- const containers = document.querySelectorAll('.ff-payment-container');
1106
- containers.forEach(container => {
1107
- container.innerHTML = successScreenString;
1108
- });
1109
- onLoaderChange(false);
1110
- });
1111
- return {
1112
- cardNumber: '#cardNumberInput',
1113
- expiryDate: '#expiryInput',
1114
- cvv: '#cvvInput',
1115
- cardholderName: '#cardHolderInput',
1116
- button: '#submitButton',
1117
- };
1045
+ async createCardElements() {
1046
+ const skinFactory = (await Promise.resolve().then(function () { return require('./chunk-index.cjs.js'); }))
1047
+ .default;
1048
+ const skin = await skinFactory(this.primerWrapper, this.checkoutConfig.container);
1049
+ this.on(EVENTS.INPUT_ERROR, skin.onInputError);
1050
+ this.on(EVENTS.STATUS_CHANGE, skin.onStatusChange);
1051
+ this.on(EVENTS.ERROR, (error) => skin.onError(error));
1052
+ this.on(EVENTS.LOADER_CHANGE, skin.onLoaderChange);
1053
+ this.on(EVENTS.DESTROY, skin.onDestroy);
1054
+ this.on(EVENTS.METHOD_RENDER, skin.onMethodRender);
1055
+ this.on(EVENTS.SUCCESS, skin.onSuccess);
1056
+ this.on(EVENTS.START_PURCHASE, skin.onStartPurchase);
1057
+ this.on(EVENTS.PURCHASE_FAILURE, skin.onPurchaseFailure);
1058
+ this.on(EVENTS.PURCHASE_COMPLETED, skin.onPurchaseCompleted);
1059
+ return skin.getCardInputSelectors();
1118
1060
  }
1119
1061
  }
1120
1062