@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.
- package/dist/chunk-index.cjs.js +159 -37
- package/dist/chunk-index.es.js +159 -37
- package/dist/funnelfox-billing.cjs.js +64 -122
- package/dist/funnelfox-billing.esm.js +64 -122
- package/dist/funnelfox-billing.js +224 -160
- package/dist/funnelfox-billing.min.js +1 -1
- package/package.json +2 -2
- package/src/types.d.ts +105 -11
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
633
|
-
|
|
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
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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.
|
|
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.
|
|
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(
|
|
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(
|
|
1050
|
-
await import('./chunk-index.es.js')
|
|
1051
|
-
.
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
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
|
|