@funnelfox/billing 0.2.1 → 0.3.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.
@@ -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.1';
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',
@@ -386,7 +391,8 @@ 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
+ throw primerError;
390
396
  }
391
397
  finally {
392
398
  onSubmit(false);
@@ -459,14 +465,6 @@ class PrimerWrapper {
459
465
  throw new PrimerError('No allowed payment methods found');
460
466
  }
461
467
  },
462
- onCheckoutFail: error => {
463
- // eslint-disable-next-line no-console
464
- console.error(error);
465
- },
466
- onTokenizeError: error => {
467
- // eslint-disable-next-line no-console
468
- console.error(error);
469
- },
470
468
  });
471
469
  const methodOptions = {
472
470
  cardSelectors,
@@ -548,7 +546,7 @@ class PrimerWrapper {
548
546
  return this.destroyCallbacks;
549
547
  }
550
548
  isActive() {
551
- return this.isInitialized && this.destroyCallbacks.length;
549
+ return this.isInitialized && this.destroyCallbacks.length > 0;
552
550
  }
553
551
  validateContainer(selector) {
554
552
  const element = document.querySelector(selector);
@@ -568,10 +566,7 @@ class PrimerWrapper {
568
566
  * @fileoverview Input validation utilities for Funnefox SDK
569
567
  */
570
568
  function sanitizeString(input) {
571
- if (input === null || input === undefined) {
572
- return '';
573
- }
574
- return String(input).trim();
569
+ return input?.trim() || '';
575
570
  }
576
571
  function requireString(value, fieldName) {
577
572
  const sanitized = sanitizeString(value);
@@ -607,7 +602,7 @@ class APIClient {
607
602
  }, this.retryAttempts);
608
603
  }
609
604
  catch (error) {
610
- if (error.name === 'APIError') {
605
+ if (error instanceof Error && error.name === 'APIError') {
611
606
  throw error;
612
607
  }
613
608
  throw new NetworkError('Network request failed', error);
@@ -619,6 +614,9 @@ class APIClient {
619
614
  response = await fetch(url, options);
620
615
  }
621
616
  catch (error) {
617
+ if (error instanceof Error && error.name === 'NetworkError') {
618
+ throw error;
619
+ }
622
620
  throw new NetworkError('Network request failed', error);
623
621
  }
624
622
  let data;
@@ -629,8 +627,9 @@ class APIClient {
629
627
  throw new APIError('Invalid JSON response', response.status, {});
630
628
  }
631
629
  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, {
630
+ const d = data;
631
+ const message = d.error?.[0]?.msg || 'Failed to create payment';
632
+ throw new APIError(message, response.status, {
634
633
  response: data,
635
634
  });
636
635
  }
@@ -648,10 +647,10 @@ class APIClient {
648
647
  if (params.countryCode !== undefined) {
649
648
  payload.country_code = params.countryCode;
650
649
  }
651
- return await this.request(API_ENDPOINTS.CREATE_CLIENT_SESSION, {
650
+ return (await this.request(API_ENDPOINTS.CREATE_CLIENT_SESSION, {
652
651
  method: 'POST',
653
652
  body: JSON.stringify(payload),
654
- });
653
+ }));
655
654
  }
656
655
  async updateClientSession(params) {
657
656
  const payload = {
@@ -669,20 +668,20 @@ class APIClient {
669
668
  order_id: params.orderId,
670
669
  payment_method_token: params.paymentMethodToken,
671
670
  };
672
- return await this.request(API_ENDPOINTS.CREATE_PAYMENT, {
671
+ return (await this.request(API_ENDPOINTS.CREATE_PAYMENT, {
673
672
  method: 'POST',
674
673
  body: JSON.stringify(payload),
675
- });
674
+ }));
676
675
  }
677
676
  async resumePayment(params) {
678
677
  const payload = {
679
678
  order_id: params.orderId,
680
679
  resume_token: params.resumeToken,
681
680
  };
682
- return await this.request(API_ENDPOINTS.RESUME_PAYMENT, {
681
+ return (await this.request(API_ENDPOINTS.RESUME_PAYMENT, {
683
682
  method: 'POST',
684
683
  body: JSON.stringify(payload),
685
- });
684
+ }));
686
685
  }
687
686
  processSessionResponse(response) {
688
687
  if (response.status === 'error') {
@@ -695,7 +694,7 @@ class APIClient {
695
694
  response,
696
695
  });
697
696
  }
698
- const data = response.data || response;
697
+ const data = response.data;
699
698
  return {
700
699
  type: 'session_created',
701
700
  orderId: data.order_id,
@@ -709,11 +708,10 @@ class APIClient {
709
708
  throw new APIError(message, null, {
710
709
  errorCode: firstError?.code,
711
710
  errorType: firstError?.type,
712
- requestId: response.req_id,
713
711
  response,
714
712
  });
715
713
  }
716
- const data = response.data || response;
714
+ const data = response.data;
717
715
  if (data.action_required_token) {
718
716
  return {
719
717
  type: 'action_required',
@@ -728,12 +726,13 @@ class APIClient {
728
726
  type: 'success',
729
727
  orderId: data.order_id,
730
728
  status: 'succeeded',
731
- transactionId: data.transaction_id,
732
729
  };
733
730
  case 'failed':
734
- throw new APIError(data.failed_message_for_user || 'Payment failed', null, data);
731
+ throw new APIError(data.failed_message_for_user || 'Payment failed', null, { response });
735
732
  case 'cancelled':
736
- throw new APIError('Payment was cancelled by user', null, data);
733
+ throw new APIError('Payment was cancelled by user', null, {
734
+ response,
735
+ });
737
736
  case 'processing':
738
737
  return {
739
738
  type: 'processing',
@@ -741,20 +740,10 @@ class APIClient {
741
740
  status: 'processing',
742
741
  };
743
742
  default:
744
- throw new APIError(`Unhandled checkout status: ${data.checkout_status}`, null, data);
743
+ throw new APIError(`Unhandled checkout status: ${data.checkout_status}`, null, { response });
745
744
  }
746
745
  }
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);
746
+ throw new APIError('Invalid payment response format', null, { response });
758
747
  }
759
748
  }
760
749
 
@@ -773,8 +762,6 @@ class CheckoutInstance extends EventEmitter {
773
762
  };
774
763
  this.handleSubmit = (isSubmitting) => {
775
764
  this.onLoaderChangeWithRace(isSubmitting);
776
- // Clear any previous errors
777
- this.emit(EVENTS.ERROR);
778
765
  this._setState(isSubmitting ? 'processing' : 'ready');
779
766
  };
780
767
  this.handleTokenizeSuccess = async (paymentMethodTokenData, primerHandler) => {
@@ -790,7 +777,7 @@ class CheckoutInstance extends EventEmitter {
790
777
  }
791
778
  catch (error) {
792
779
  this._setState('error');
793
- this.emit(EVENTS.ERROR, error);
780
+ this.emit(EVENTS.PURCHASE_FAILURE, new Error(error.message || 'Payment processing failed'));
794
781
  primerHandler.handleFailure(error.message || 'Payment processing failed');
795
782
  }
796
783
  finally {
@@ -811,10 +798,11 @@ class CheckoutInstance extends EventEmitter {
811
798
  }
812
799
  catch (error) {
813
800
  this._setState('error');
814
- this.emit(EVENTS.ERROR, error);
801
+ this.emit(EVENTS.PURCHASE_FAILURE, new Error(error.message || 'Payment processing failed'));
815
802
  primerHandler.handleFailure(error.message || 'Payment processing failed');
816
803
  }
817
804
  finally {
805
+ this.emit(EVENTS.PURCHASE_COMPLETED);
818
806
  this.onLoaderChangeWithRace(false);
819
807
  this._setState('ready');
820
808
  }
@@ -859,18 +847,6 @@ class CheckoutInstance extends EventEmitter {
859
847
  this.on(EVENTS.DESTROY, this.callbacks.onDestroy);
860
848
  }
861
849
  }
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
850
  removeAllListeners() {
875
851
  return super.removeAllListeners();
876
852
  }
@@ -912,10 +888,30 @@ class CheckoutInstance extends EventEmitter {
912
888
  onSubmit: this.handleSubmit,
913
889
  onInputChange: this.handleInputChange,
914
890
  onMethodRender: this.handleMethodRender,
891
+ onResumeError: error => {
892
+ if (error.stack?.includes('PROCESSOR_3DS') &&
893
+ error.code === 'RESUME_ERROR' &&
894
+ error.message?.includes('fetch resume key')) {
895
+ // Ignore 3DS close error, because it is not understandable by user
896
+ return;
897
+ }
898
+ this.emit(EVENTS.PURCHASE_FAILURE, error);
899
+ },
900
+ onCheckoutFail: error => {
901
+ this.emit(EVENTS.PURCHASE_FAILURE, error);
902
+ },
903
+ onTokenizeError: error => {
904
+ this.emit(EVENTS.PURCHASE_FAILURE, error);
905
+ },
906
+ onTokenizeShouldStart: data => {
907
+ this.emit(EVENTS.ERROR, undefined);
908
+ this.emit(EVENTS.START_PURCHASE, data.paymentMethodType);
909
+ return true;
910
+ },
915
911
  };
916
912
  if (!this.checkoutConfig.cardSelectors ||
917
913
  !this.checkoutConfig.paymentButtonSelectors) {
918
- const cardSelectors = await this.createCardElements(this.checkoutConfig.container);
914
+ const cardSelectors = await this.createCardElements();
919
915
  const paymentButtonSelectors = {
920
916
  paypal: '#paypalButton',
921
917
  googlePay: '#googlePayButton',
@@ -951,8 +947,6 @@ class CheckoutInstance extends EventEmitter {
951
947
  this.emit(EVENTS.SUCCESS, {
952
948
  orderId: result.orderId,
953
949
  status: result.status,
954
- transactionId: result.transactionId,
955
- metadata: result.metadata,
956
950
  });
957
951
  primerHandler.handleSuccess();
958
952
  break;
@@ -986,7 +980,6 @@ class CheckoutInstance extends EventEmitter {
986
980
  });
987
981
  this.checkoutConfig.priceId = newPriceId;
988
982
  this._setState('ready');
989
- this.emit(EVENTS.STATUS_CHANGE, 'price-updated');
990
983
  }
991
984
  catch (error) {
992
985
  this._setState('error');
@@ -1046,71 +1039,21 @@ class CheckoutInstance extends EventEmitter {
1046
1039
  }
1047
1040
  // Creates containers to render hosted inputs with labels and error messages,
1048
1041
  // 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
- };
1042
+ async createCardElements() {
1043
+ const skinFactory = (await import('./chunk-index.es.js'))
1044
+ .default;
1045
+ const skin = await skinFactory(this.primerWrapper, this.checkoutConfig.container);
1046
+ this.on(EVENTS.INPUT_ERROR, skin.onInputError);
1047
+ this.on(EVENTS.STATUS_CHANGE, skin.onStatusChange);
1048
+ this.on(EVENTS.ERROR, (error) => skin.onError(error));
1049
+ this.on(EVENTS.LOADER_CHANGE, skin.onLoaderChange);
1050
+ this.on(EVENTS.DESTROY, skin.onDestroy);
1051
+ this.on(EVENTS.METHOD_RENDER, skin.onMethodRender);
1052
+ this.on(EVENTS.SUCCESS, skin.onSuccess);
1053
+ this.on(EVENTS.START_PURCHASE, skin.onStartPurchase);
1054
+ this.on(EVENTS.PURCHASE_FAILURE, skin.onPurchaseFailure);
1055
+ this.on(EVENTS.PURCHASE_COMPLETED, skin.onPurchaseCompleted);
1056
+ return skin.getCardInputSelectors();
1114
1057
  }
1115
1058
  }
1116
1059