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