@funnelfox/billing 0.6.4-beta.0 → 0.6.4-beta.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.
- package/README.md +14 -4
- package/dist/chunk-index.cjs.js +143 -163
- package/dist/chunk-index.cjs3.js +24 -6
- package/dist/chunk-index.es.js +143 -163
- package/dist/chunk-index.es3.js +24 -6
- package/dist/funnelfox-billing.js +167 -169
- package/dist/funnelfox-billing.min.js +1 -1
- package/package.json +2 -2
- package/src/types.d.ts +28 -11
package/README.md
CHANGED
|
@@ -94,17 +94,24 @@ const checkout = await createCheckout({
|
|
|
94
94
|
priceId: 'price_123',
|
|
95
95
|
customer: {
|
|
96
96
|
externalId: 'user_456',
|
|
97
|
-
email: 'user@example.com',
|
|
97
|
+
email: 'user@example.com', // Optional if you collect it in the card form
|
|
98
98
|
countryCode: 'US', // Optional
|
|
99
99
|
},
|
|
100
100
|
container: '#checkout-container',
|
|
101
101
|
clientMetadata: { source: 'web' },
|
|
102
|
+
card: {
|
|
103
|
+
emailAddress: {
|
|
104
|
+
visible: true,
|
|
105
|
+
template: '{{email}}',
|
|
106
|
+
},
|
|
107
|
+
},
|
|
102
108
|
cardSelectors: {
|
|
103
109
|
// Custom card input selectors (optional, defaults to auto-generated)
|
|
104
110
|
cardNumber: '#cardNumberInput',
|
|
105
111
|
expiryDate: '#expiryInput',
|
|
106
112
|
cvv: '#cvvInput',
|
|
107
113
|
cardholderName: '#cardHolderInput',
|
|
114
|
+
emailAddress: '#emailAddressInput',
|
|
108
115
|
button: '#submitButton',
|
|
109
116
|
},
|
|
110
117
|
paypalButtonContainer: '#paypalButton', // Optional
|
|
@@ -130,9 +137,11 @@ const checkout = await createCheckout({
|
|
|
130
137
|
- `options.priceId` (string, required) - Price identifier
|
|
131
138
|
- `options.customer` (object, required)
|
|
132
139
|
- `customer.externalId` (string, required) - Your user identifier
|
|
133
|
-
- `customer.email` (string,
|
|
140
|
+
- `customer.email` (string, optional) - Customer email
|
|
134
141
|
- `customer.countryCode` (string, optional) - ISO country code
|
|
135
142
|
- `options.container` (string, required) - CSS selector for checkout container
|
|
143
|
+
- `options.card.emailAddress.visible` (boolean, optional) - Shows an email field in the card form. Disabled by default.
|
|
144
|
+
- `options.card.emailAddress.template` (string, optional) - Wraps the entered email before it is sent with payment, for example `{{email}}`.
|
|
136
145
|
|
|
137
146
|
**Container Styling Requirements (Default Skin):**
|
|
138
147
|
|
|
@@ -176,7 +185,7 @@ import { createClientSession } from '@funnelfox/billing';
|
|
|
176
185
|
const session = await createClientSession({
|
|
177
186
|
priceId: 'price_123',
|
|
178
187
|
externalId: 'user_456',
|
|
179
|
-
email: 'user@example.com',
|
|
188
|
+
email: 'user@example.com', // Optional
|
|
180
189
|
orgId: 'your-org-id', // Optional if configured
|
|
181
190
|
});
|
|
182
191
|
|
|
@@ -528,6 +537,7 @@ const checkout = await createCheckout({
|
|
|
528
537
|
expiryDate: '#my-expiry',
|
|
529
538
|
cvv: '#my-cvv',
|
|
530
539
|
cardholderName: '#my-cardholder',
|
|
540
|
+
emailAddress: '#my-email',
|
|
531
541
|
button: '#my-submit-button',
|
|
532
542
|
},
|
|
533
543
|
|
|
@@ -657,7 +667,7 @@ await paymentMethod.destroy();
|
|
|
657
667
|
- `orgId` (string, required) - Your organization identifier
|
|
658
668
|
- `priceId` (string, required) - Price identifier
|
|
659
669
|
- `externalId` (string, required) - Your user identifier
|
|
660
|
-
- `email` (string,
|
|
670
|
+
- `email` (string, optional) - Customer email
|
|
661
671
|
- `baseUrl` (string, optional) - Custom API URL
|
|
662
672
|
- `meta` (object, optional) - Custom metadata
|
|
663
673
|
- `style`, `card`, `applePay`, `paypal`, `googlePay` (optional) - Primer SDK configuration options
|
package/dist/chunk-index.cjs.js
CHANGED
|
@@ -150,64 +150,46 @@ class NetworkError extends FunnefoxSDKError {
|
|
|
150
150
|
}
|
|
151
151
|
|
|
152
152
|
/**
|
|
153
|
-
* @fileoverview
|
|
153
|
+
* @fileoverview Dynamic loader for Primer SDK
|
|
154
|
+
* Loads Primer script and CSS from CDN independently of bundler
|
|
154
155
|
*/
|
|
156
|
+
const PRIMER_CDN_BASE = 'https://sdk.primer.io/web';
|
|
157
|
+
const DEFAULT_VERSION = '2.57.3';
|
|
158
|
+
// Integrity hashes for specific versions (for SRI security)
|
|
159
|
+
const INTEGRITY_HASHES = {
|
|
160
|
+
'2.57.3': {
|
|
161
|
+
js: 'sha384-xq2SWkYvTlKOMpuXQUXq1QI3eZN7JiqQ3Sc72U9wY1IE30MW3HkwQWg/1n6BTMz4',
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
let loadingPromise = null;
|
|
165
|
+
let isLoaded = false;
|
|
155
166
|
/**
|
|
156
|
-
*
|
|
157
|
-
* Checks if script already exists before loading to prevent duplicates.
|
|
158
|
-
*
|
|
159
|
-
* @param options - Script configuration options
|
|
160
|
-
* @returns Promise that resolves when script is loaded or rejects on error
|
|
167
|
+
* Injects a script tag into the document head
|
|
161
168
|
*/
|
|
162
|
-
function
|
|
163
|
-
const { id, src, async = true, type = 'text/javascript', attributes = {}, integrity, crossOrigin, appendTo = 'body', } = options;
|
|
169
|
+
function injectScript$1(src, integrity) {
|
|
164
170
|
return new Promise((resolve, reject) => {
|
|
165
|
-
// Check if script already exists
|
|
166
|
-
|
|
167
|
-
if (id) {
|
|
168
|
-
existingScript = document.getElementById(id);
|
|
169
|
-
}
|
|
170
|
-
if (!existingScript) {
|
|
171
|
-
existingScript = document.querySelector(`script[src="${src}"]`);
|
|
172
|
-
}
|
|
171
|
+
// Check if script already exists
|
|
172
|
+
const existingScript = document.querySelector(`script[src="${src}"]`);
|
|
173
173
|
if (existingScript) {
|
|
174
174
|
resolve(existingScript);
|
|
175
175
|
return;
|
|
176
176
|
}
|
|
177
177
|
const script = document.createElement('script');
|
|
178
|
-
if (id) {
|
|
179
|
-
script.id = id;
|
|
180
|
-
}
|
|
181
|
-
script.type = type;
|
|
182
178
|
script.src = src;
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
}
|
|
179
|
+
script.async = true;
|
|
180
|
+
script.crossOrigin = 'anonymous';
|
|
186
181
|
if (integrity) {
|
|
187
182
|
script.integrity = integrity;
|
|
188
183
|
}
|
|
189
|
-
if (crossOrigin) {
|
|
190
|
-
script.crossOrigin = crossOrigin;
|
|
191
|
-
}
|
|
192
|
-
// Set additional attributes
|
|
193
|
-
Object.entries(attributes).forEach(([key, value]) => {
|
|
194
|
-
script.setAttribute(key, value);
|
|
195
|
-
});
|
|
196
184
|
script.onload = () => resolve(script);
|
|
197
|
-
script.onerror = () => reject(new Error(`Failed to load script
|
|
198
|
-
|
|
199
|
-
target.appendChild(script);
|
|
185
|
+
script.onerror = () => reject(new Error(`Failed to load Primer SDK script from ${src}`));
|
|
186
|
+
document.head.appendChild(script);
|
|
200
187
|
});
|
|
201
188
|
}
|
|
202
189
|
/**
|
|
203
|
-
*
|
|
204
|
-
* Checks if stylesheet already exists before loading to prevent duplicates.
|
|
205
|
-
*
|
|
206
|
-
* @param options - Stylesheet configuration options
|
|
207
|
-
* @returns Promise that resolves when stylesheet is loaded or rejects on error
|
|
190
|
+
* Injects a CSS link tag into the document head
|
|
208
191
|
*/
|
|
209
|
-
function
|
|
210
|
-
const { href, integrity, crossOrigin } = options;
|
|
192
|
+
function injectCSS(href, integrity) {
|
|
211
193
|
return new Promise((resolve, reject) => {
|
|
212
194
|
// Check if stylesheet already exists
|
|
213
195
|
const existingLink = document.querySelector(`link[href="${href}"]`);
|
|
@@ -218,32 +200,15 @@ function loadStylesheet(options) {
|
|
|
218
200
|
const link = document.createElement('link');
|
|
219
201
|
link.rel = 'stylesheet';
|
|
220
202
|
link.href = href;
|
|
203
|
+
link.crossOrigin = 'anonymous';
|
|
221
204
|
if (integrity) {
|
|
222
205
|
link.integrity = integrity;
|
|
223
206
|
}
|
|
224
|
-
if (crossOrigin) {
|
|
225
|
-
link.crossOrigin = crossOrigin;
|
|
226
|
-
}
|
|
227
207
|
link.onload = () => resolve(link);
|
|
228
|
-
link.onerror = () => reject(new Error(`Failed to load
|
|
208
|
+
link.onerror = () => reject(new Error(`Failed to load Primer SDK CSS from ${href}`));
|
|
229
209
|
document.head.appendChild(link);
|
|
230
210
|
});
|
|
231
211
|
}
|
|
232
|
-
|
|
233
|
-
/**
|
|
234
|
-
* @fileoverview Dynamic loader for Primer SDK
|
|
235
|
-
* Loads Primer script and CSS from CDN independently of bundler
|
|
236
|
-
*/
|
|
237
|
-
const PRIMER_CDN_BASE = 'https://sdk.primer.io/web';
|
|
238
|
-
const DEFAULT_VERSION = '2.57.3';
|
|
239
|
-
// Integrity hashes for specific versions (for SRI security)
|
|
240
|
-
const INTEGRITY_HASHES = {
|
|
241
|
-
'2.57.3': {
|
|
242
|
-
js: 'sha384-xq2SWkYvTlKOMpuXQUXq1QI3eZN7JiqQ3Sc72U9wY1IE30MW3HkwQWg/1n6BTMz4',
|
|
243
|
-
},
|
|
244
|
-
};
|
|
245
|
-
let loadingPromise = null;
|
|
246
|
-
let isLoaded = false;
|
|
247
212
|
/**
|
|
248
213
|
* Waits for window.Primer to be available
|
|
249
214
|
*/
|
|
@@ -295,17 +260,8 @@ async function loadPrimerSDK(version) {
|
|
|
295
260
|
try {
|
|
296
261
|
// Load CSS and JS in parallel
|
|
297
262
|
await Promise.all([
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
integrity: hashes?.css,
|
|
301
|
-
crossOrigin: 'anonymous',
|
|
302
|
-
}),
|
|
303
|
-
loadScript$1({
|
|
304
|
-
src: jsUrl,
|
|
305
|
-
integrity: hashes?.js,
|
|
306
|
-
crossOrigin: 'anonymous',
|
|
307
|
-
appendTo: 'head',
|
|
308
|
-
}),
|
|
263
|
+
injectCSS(cssUrl, hashes?.css),
|
|
264
|
+
injectScript$1(jsUrl, hashes?.js),
|
|
309
265
|
]);
|
|
310
266
|
// Wait for Primer to be available on window
|
|
311
267
|
await waitForPrimer();
|
|
@@ -348,28 +304,6 @@ function generateId(prefix = '') {
|
|
|
348
304
|
const random = Math.random().toString(36).substr(2, 5);
|
|
349
305
|
return `${prefix}${timestamp}_${random}`;
|
|
350
306
|
}
|
|
351
|
-
/**
|
|
352
|
-
* Generates a UUID v4 compliant string (RFC 4122).
|
|
353
|
-
* Meets Airwallex requirements:
|
|
354
|
-
* - Maximum 128 characters (UUID is 36 chars)
|
|
355
|
-
* - Only contains: a-z, A-Z, 0-9, underscore, hyphen
|
|
356
|
-
* - No prefix + timestamp pattern
|
|
357
|
-
* - Not a short series of numbers
|
|
358
|
-
*
|
|
359
|
-
* @returns UUID v4 string (e.g., "a3bb189e-8bf9-3888-9912-ace4e6543002")
|
|
360
|
-
*/
|
|
361
|
-
function generateUUID() {
|
|
362
|
-
// Use crypto.randomUUID if available (modern browsers)
|
|
363
|
-
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
|
364
|
-
return crypto.randomUUID();
|
|
365
|
-
}
|
|
366
|
-
// Fallback: manual UUID v4 generation
|
|
367
|
-
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
|
|
368
|
-
const r = (Math.random() * 16) | 0;
|
|
369
|
-
const v = c === 'x' ? r : (r & 0x3) | 0x8;
|
|
370
|
-
return v.toString(16);
|
|
371
|
-
});
|
|
372
|
-
}
|
|
373
307
|
function sleep(ms) {
|
|
374
308
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
375
309
|
}
|
|
@@ -488,7 +422,7 @@ exports.PaymentMethod = void 0;
|
|
|
488
422
|
/**
|
|
489
423
|
* @fileoverview Constants for Funnefox SDK
|
|
490
424
|
*/
|
|
491
|
-
const SDK_VERSION = '0.6.4-beta.
|
|
425
|
+
const SDK_VERSION = '0.6.4-beta.1';
|
|
492
426
|
const DEFAULTS = {
|
|
493
427
|
BASE_URL: 'https://billing.funnelfox.com',
|
|
494
428
|
REGION: 'default',
|
|
@@ -602,6 +536,26 @@ const APPLE_PAY_COLLECTING_EMAIL_OPTIONS = {
|
|
|
602
536
|
},
|
|
603
537
|
};
|
|
604
538
|
|
|
539
|
+
/**
|
|
540
|
+
* @fileoverview Input validation utilities for Funnefox SDK
|
|
541
|
+
*/
|
|
542
|
+
function isValidEmail(email) {
|
|
543
|
+
if (typeof email !== 'string')
|
|
544
|
+
return false;
|
|
545
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
546
|
+
return emailRegex.test(email);
|
|
547
|
+
}
|
|
548
|
+
function sanitizeString(input) {
|
|
549
|
+
return input?.trim() || '';
|
|
550
|
+
}
|
|
551
|
+
function requireString(value, fieldName) {
|
|
552
|
+
const sanitized = sanitizeString(value);
|
|
553
|
+
if (sanitized.length === 0) {
|
|
554
|
+
throw new ValidationError(fieldName, 'must be a non-empty string', value);
|
|
555
|
+
}
|
|
556
|
+
return true;
|
|
557
|
+
}
|
|
558
|
+
|
|
605
559
|
/**
|
|
606
560
|
* @fileoverview Primer SDK integration wrapper
|
|
607
561
|
*/
|
|
@@ -718,6 +672,7 @@ class PrimerWrapper {
|
|
|
718
672
|
onInputChange: options.onInputChange,
|
|
719
673
|
onMethodRenderError: options.onMethodRenderError,
|
|
720
674
|
onMethodRender: options.onMethodRender,
|
|
675
|
+
onCardInputValueChange: options.onCardInputValueChange,
|
|
721
676
|
});
|
|
722
677
|
this.paymentMethodsInterfaces.push(cardInterface);
|
|
723
678
|
return cardInterface;
|
|
@@ -736,7 +691,7 @@ class PrimerWrapper {
|
|
|
736
691
|
throw new PrimerError('Failed to initialize Primer checkout', error);
|
|
737
692
|
}
|
|
738
693
|
}
|
|
739
|
-
async renderCardCheckoutWithElements(elements, { onSubmit, onInputChange, onMethodRenderError, onMethodRender, }) {
|
|
694
|
+
async renderCardCheckoutWithElements(elements, { onSubmit, onInputChange, onCardInputValueChange, onMethodRenderError, onMethodRender, }) {
|
|
740
695
|
try {
|
|
741
696
|
if (!this.currentHeadless) {
|
|
742
697
|
throw new PrimerError('Headless checkout not found');
|
|
@@ -753,7 +708,12 @@ class PrimerWrapper {
|
|
|
753
708
|
const { valid, validationErrors } = await pmManager.validate();
|
|
754
709
|
const cardHolderError = validationErrors.find(v => v.name === 'cardholderName');
|
|
755
710
|
dispatchError('cardholderName', cardHolderError?.message || null);
|
|
756
|
-
|
|
711
|
+
const emailAddress = elements.emailAddress?.value?.trim();
|
|
712
|
+
const emailError = emailAddress && !isValidEmail(emailAddress)
|
|
713
|
+
? 'Please enter a valid email address'
|
|
714
|
+
: null;
|
|
715
|
+
dispatchError('emailAddress', emailError);
|
|
716
|
+
return valid && !emailError;
|
|
757
717
|
};
|
|
758
718
|
const dispatchError = (inputName, error) => {
|
|
759
719
|
onInputChange(inputName, error);
|
|
@@ -768,7 +728,16 @@ class PrimerWrapper {
|
|
|
768
728
|
pmManager.setCardholderName(e.target.value);
|
|
769
729
|
dispatchError('cardholderName', null);
|
|
770
730
|
};
|
|
731
|
+
const emailAddressOnChange = (e) => {
|
|
732
|
+
const value = e.target.value;
|
|
733
|
+
const email = value.trim();
|
|
734
|
+
onCardInputValueChange?.('emailAddress', email);
|
|
735
|
+
dispatchError('emailAddress', email && !isValidEmail(email)
|
|
736
|
+
? 'Please enter a valid email address'
|
|
737
|
+
: null);
|
|
738
|
+
};
|
|
771
739
|
elements.cardholderName?.addEventListener('input', cardHolderOnChange);
|
|
740
|
+
elements.emailAddress?.addEventListener('input', emailAddressOnChange);
|
|
772
741
|
cardNumberInput.addEventListener('change', onHostedInputChange('cardNumber'));
|
|
773
742
|
expiryInput.addEventListener('change', onHostedInputChange('expiryDate'));
|
|
774
743
|
cvvInput.addEventListener('change', onHostedInputChange('cvv'));
|
|
@@ -808,7 +777,8 @@ class PrimerWrapper {
|
|
|
808
777
|
]);
|
|
809
778
|
const onDestroy = () => {
|
|
810
779
|
pmManager.removeHostedInputs();
|
|
811
|
-
elements.cardholderName?.removeEventListener('
|
|
780
|
+
elements.cardholderName?.removeEventListener('input', cardHolderOnChange);
|
|
781
|
+
elements.emailAddress?.removeEventListener('input', emailAddressOnChange);
|
|
812
782
|
elements.button?.removeEventListener('click', onSubmitHandler);
|
|
813
783
|
};
|
|
814
784
|
this.destroyCallbacks.push(onDestroy);
|
|
@@ -824,6 +794,9 @@ class PrimerWrapper {
|
|
|
824
794
|
if (elements.cardholderName) {
|
|
825
795
|
elements.cardholderName.disabled = disabled;
|
|
826
796
|
}
|
|
797
|
+
if (elements.emailAddress) {
|
|
798
|
+
elements.emailAddress.disabled = disabled;
|
|
799
|
+
}
|
|
827
800
|
},
|
|
828
801
|
submit: () => onSubmitHandler(),
|
|
829
802
|
destroy: () => {
|
|
@@ -846,7 +819,7 @@ class PrimerWrapper {
|
|
|
846
819
|
});
|
|
847
820
|
}
|
|
848
821
|
async renderCheckout(clientToken, checkoutOptions, checkoutRenderOptions) {
|
|
849
|
-
const { cardElements, paymentButtonElements, container, onSubmit, onInputChange, onMethodRender, onMethodRenderError, onMethodsAvailable, } = checkoutRenderOptions;
|
|
822
|
+
const { cardElements, paymentButtonElements, container, onSubmit, onInputChange, onMethodRender, onMethodRenderError, onMethodsAvailable, onCardInputValueChange, } = checkoutRenderOptions;
|
|
850
823
|
await this.initializeHeadlessCheckout(clientToken, checkoutOptions);
|
|
851
824
|
onMethodsAvailable?.(this.availableMethods);
|
|
852
825
|
await Promise.all(this.availableMethods.map(method => {
|
|
@@ -858,6 +831,7 @@ class PrimerWrapper {
|
|
|
858
831
|
onInputChange,
|
|
859
832
|
onMethodRender,
|
|
860
833
|
onMethodRenderError,
|
|
834
|
+
onCardInputValueChange,
|
|
861
835
|
});
|
|
862
836
|
}
|
|
863
837
|
else {
|
|
@@ -974,20 +948,6 @@ class PrimerWrapper {
|
|
|
974
948
|
}
|
|
975
949
|
PrimerWrapper.headlessManager = new HeadlessManager();
|
|
976
950
|
|
|
977
|
-
/**
|
|
978
|
-
* @fileoverview Input validation utilities for Funnefox SDK
|
|
979
|
-
*/
|
|
980
|
-
function sanitizeString(input) {
|
|
981
|
-
return input?.trim() || '';
|
|
982
|
-
}
|
|
983
|
-
function requireString(value, fieldName) {
|
|
984
|
-
const sanitized = sanitizeString(value);
|
|
985
|
-
if (sanitized.length === 0) {
|
|
986
|
-
throw new ValidationError(fieldName, 'must be a non-empty string', value);
|
|
987
|
-
}
|
|
988
|
-
return true;
|
|
989
|
-
}
|
|
990
|
-
|
|
991
951
|
/**
|
|
992
952
|
* @fileoverview API client for Funnefox backend integration
|
|
993
953
|
*/
|
|
@@ -1083,6 +1043,9 @@ class APIClient {
|
|
|
1083
1043
|
payment_method_token: params.paymentMethodToken,
|
|
1084
1044
|
client_metadata: params.clientMetadata || {},
|
|
1085
1045
|
};
|
|
1046
|
+
if (params.email !== undefined) {
|
|
1047
|
+
payload.email_address = params.email;
|
|
1048
|
+
}
|
|
1086
1049
|
return (await this.request(API_ENDPOINTS.CREATE_PAYMENT, {
|
|
1087
1050
|
method: 'POST',
|
|
1088
1051
|
body: JSON.stringify(payload),
|
|
@@ -1387,34 +1350,6 @@ const renderError = (container, reqId) => {
|
|
|
1387
1350
|
}
|
|
1388
1351
|
};
|
|
1389
1352
|
|
|
1390
|
-
/**
|
|
1391
|
-
* @fileoverview Airwallex device fingerprinting script loader
|
|
1392
|
-
*/
|
|
1393
|
-
/**
|
|
1394
|
-
* Loads Airwallex device fingerprinting script for fraud prevention.
|
|
1395
|
-
* The script collects browser, screen, device, and interaction data.
|
|
1396
|
-
*
|
|
1397
|
-
* @param sessionId - Unique order session ID (UUID v4 format, max 128 chars)
|
|
1398
|
-
* @param isDemoMode - If true, uses demo environment URL for testing
|
|
1399
|
-
* @returns Promise that resolves when script is loaded
|
|
1400
|
-
*
|
|
1401
|
-
* @see https://www.airwallex.com/docs/payments/online-payments/native-api/device-fingerprinting
|
|
1402
|
-
*/
|
|
1403
|
-
async function loadAirwallexDeviceFingerprint(sessionId, isDemoMode = false) {
|
|
1404
|
-
const scriptId = 'airwallex-fraud-api';
|
|
1405
|
-
const src = isDemoMode
|
|
1406
|
-
? 'https://static-demo.airwallex.com/webapp/fraud/device-fingerprint/index.js'
|
|
1407
|
-
: 'https://static.airwallex.com/webapp/fraud/device-fingerprint/index.js';
|
|
1408
|
-
await loadScript$1({
|
|
1409
|
-
id: scriptId,
|
|
1410
|
-
src,
|
|
1411
|
-
async: true,
|
|
1412
|
-
attributes: {
|
|
1413
|
-
'data-order-session-id': sessionId,
|
|
1414
|
-
},
|
|
1415
|
-
});
|
|
1416
|
-
}
|
|
1417
|
-
|
|
1418
1353
|
/**
|
|
1419
1354
|
* @fileoverview Checkout instance manager for Funnefox SDK
|
|
1420
1355
|
*/
|
|
@@ -1423,10 +1358,14 @@ class CheckoutInstance extends EventEmitter {
|
|
|
1423
1358
|
super();
|
|
1424
1359
|
this.counter = 0;
|
|
1425
1360
|
this.radarSessionId = null;
|
|
1426
|
-
this.airwallexDeviceId = null;
|
|
1427
1361
|
this.handleInputChange = (inputName, error) => {
|
|
1428
1362
|
this.emit(EVENTS.INPUT_ERROR, { name: inputName, error });
|
|
1429
1363
|
};
|
|
1364
|
+
this.handleCardInputValueChange = (inputName, value) => {
|
|
1365
|
+
if (inputName === 'emailAddress') {
|
|
1366
|
+
this.cardEmailAddress = value?.trim() || undefined;
|
|
1367
|
+
}
|
|
1368
|
+
};
|
|
1430
1369
|
this.handleMethodRender = (method) => {
|
|
1431
1370
|
this.emit(EVENTS.METHOD_RENDER, method);
|
|
1432
1371
|
};
|
|
@@ -1445,16 +1384,13 @@ class CheckoutInstance extends EventEmitter {
|
|
|
1445
1384
|
try {
|
|
1446
1385
|
this.onLoaderChangeWithRace(true);
|
|
1447
1386
|
this._setState('processing');
|
|
1448
|
-
const
|
|
1449
|
-
this.radarSessionId,
|
|
1450
|
-
this.airwallexDeviceId,
|
|
1451
|
-
]);
|
|
1387
|
+
const radarSessionId = await this.radarSessionId;
|
|
1452
1388
|
const paymentResponse = await this.apiClient.createPayment({
|
|
1453
1389
|
orderId: this.orderId,
|
|
1454
1390
|
paymentMethodToken: paymentMethodTokenData.token,
|
|
1391
|
+
email: this.getPaymentEmailAddress(),
|
|
1455
1392
|
clientMetadata: {
|
|
1456
1393
|
radarSessionId,
|
|
1457
|
-
airwallexDeviceId,
|
|
1458
1394
|
},
|
|
1459
1395
|
});
|
|
1460
1396
|
const result = this.apiClient.processPaymentResponse(paymentResponse);
|
|
@@ -1524,6 +1460,7 @@ class CheckoutInstance extends EventEmitter {
|
|
|
1524
1460
|
this.clientToken = null;
|
|
1525
1461
|
this.primerWrapper = new PrimerWrapper();
|
|
1526
1462
|
this.isDestroyed = false;
|
|
1463
|
+
this.cardEmailAddress = this.checkoutConfig.customer.email;
|
|
1527
1464
|
this._setupCallbackBridges();
|
|
1528
1465
|
}
|
|
1529
1466
|
_setupCallbackBridges() {
|
|
@@ -1602,16 +1539,6 @@ class CheckoutInstance extends EventEmitter {
|
|
|
1602
1539
|
.catch(() => '');
|
|
1603
1540
|
});
|
|
1604
1541
|
}
|
|
1605
|
-
// Initialize Airwallex device fingerprinting if enabled by backend
|
|
1606
|
-
if (response.data?.airwallex_risk_enabled) {
|
|
1607
|
-
const deviceId = generateUUID();
|
|
1608
|
-
this.airwallexDeviceId = loadAirwallexDeviceFingerprint(deviceId, true)
|
|
1609
|
-
.then(() => deviceId)
|
|
1610
|
-
.catch(() => {
|
|
1611
|
-
// Silently fail - return deviceId anyway
|
|
1612
|
-
return deviceId;
|
|
1613
|
-
});
|
|
1614
|
-
}
|
|
1615
1542
|
this.isCollectingApplePayEmail =
|
|
1616
1543
|
!!response.data?.collect_apple_pay_email;
|
|
1617
1544
|
return response;
|
|
@@ -1624,11 +1551,61 @@ class CheckoutInstance extends EventEmitter {
|
|
|
1624
1551
|
this.orderId = sessionData.orderId;
|
|
1625
1552
|
this.clientToken = sessionData.clientToken;
|
|
1626
1553
|
}
|
|
1554
|
+
getPrimerCardConfig() {
|
|
1555
|
+
const cardConfig = { ...(this.checkoutConfig.card || {}) };
|
|
1556
|
+
delete cardConfig.emailAddress;
|
|
1557
|
+
return Object.keys(cardConfig).length
|
|
1558
|
+
? cardConfig
|
|
1559
|
+
: undefined;
|
|
1560
|
+
}
|
|
1561
|
+
getPaymentEmailAddress() {
|
|
1562
|
+
const email = this.cardEmailAddress?.trim() || this.checkoutConfig.customer.email;
|
|
1563
|
+
if (!email || !isValidEmail(email)) {
|
|
1564
|
+
return undefined;
|
|
1565
|
+
}
|
|
1566
|
+
const template = this.checkoutConfig.card?.emailAddress?.template;
|
|
1567
|
+
if (template?.includes('{{email}}')) {
|
|
1568
|
+
return template.replace(/\{\{email\}\}/g, email);
|
|
1569
|
+
}
|
|
1570
|
+
return email;
|
|
1571
|
+
}
|
|
1572
|
+
mergeApplePayCollectingEmailOptions(checkoutOptions) {
|
|
1573
|
+
if (!this.isCollectingApplePayEmail) {
|
|
1574
|
+
return checkoutOptions;
|
|
1575
|
+
}
|
|
1576
|
+
const billingFields = Array.from(new Set([
|
|
1577
|
+
...(checkoutOptions.applePay?.billingOptions
|
|
1578
|
+
?.requiredBillingContactFields || []),
|
|
1579
|
+
...(APPLE_PAY_COLLECTING_EMAIL_OPTIONS.billingOptions
|
|
1580
|
+
?.requiredBillingContactFields || []),
|
|
1581
|
+
]));
|
|
1582
|
+
const shippingFields = Array.from(new Set([
|
|
1583
|
+
...(checkoutOptions.applePay?.shippingOptions
|
|
1584
|
+
?.requiredShippingContactFields || []),
|
|
1585
|
+
...(APPLE_PAY_COLLECTING_EMAIL_OPTIONS.shippingOptions
|
|
1586
|
+
?.requiredShippingContactFields || []),
|
|
1587
|
+
]));
|
|
1588
|
+
return merge(checkoutOptions, {
|
|
1589
|
+
applePay: {
|
|
1590
|
+
billingOptions: {
|
|
1591
|
+
requiredBillingContactFields: billingFields,
|
|
1592
|
+
},
|
|
1593
|
+
shippingOptions: {
|
|
1594
|
+
requiredShippingContactFields: shippingFields,
|
|
1595
|
+
},
|
|
1596
|
+
},
|
|
1597
|
+
});
|
|
1598
|
+
}
|
|
1627
1599
|
convertCardSelectorsToElements(selectors, container) {
|
|
1628
1600
|
const cardNumber = container.querySelector(selectors.cardNumber);
|
|
1629
1601
|
const expiryDate = container.querySelector(selectors.expiryDate);
|
|
1630
1602
|
const cvv = container.querySelector(selectors.cvv);
|
|
1631
|
-
const cardholderName =
|
|
1603
|
+
const cardholderName = selectors.cardholderName
|
|
1604
|
+
? container.querySelector(selectors.cardholderName)
|
|
1605
|
+
: undefined;
|
|
1606
|
+
const emailAddress = selectors.emailAddress
|
|
1607
|
+
? container.querySelector(selectors.emailAddress)
|
|
1608
|
+
: undefined;
|
|
1632
1609
|
const button = container.querySelector(selectors.button);
|
|
1633
1610
|
if (!cardNumber || !expiryDate || !cvv || !button) {
|
|
1634
1611
|
throw new CheckoutError('Required card input elements not found in container');
|
|
@@ -1638,6 +1615,7 @@ class CheckoutInstance extends EventEmitter {
|
|
|
1638
1615
|
expiryDate,
|
|
1639
1616
|
cvv,
|
|
1640
1617
|
cardholderName,
|
|
1618
|
+
emailAddress,
|
|
1641
1619
|
button,
|
|
1642
1620
|
};
|
|
1643
1621
|
}
|
|
@@ -1687,15 +1665,14 @@ class CheckoutInstance extends EventEmitter {
|
|
|
1687
1665
|
paymentButtonElements = this.convertPaymentButtonSelectorsToElements(this.checkoutConfig.paymentButtonSelectors);
|
|
1688
1666
|
checkoutOptions = this.getCheckoutOptions({});
|
|
1689
1667
|
}
|
|
1690
|
-
checkoutOptions =
|
|
1691
|
-
? { applePay: APPLE_PAY_COLLECTING_EMAIL_OPTIONS }
|
|
1692
|
-
: {});
|
|
1668
|
+
checkoutOptions = this.mergeApplePayCollectingEmailOptions(checkoutOptions);
|
|
1693
1669
|
await this.primerWrapper.renderCheckout(this.clientToken, checkoutOptions, {
|
|
1694
1670
|
container: containerElement,
|
|
1695
1671
|
cardElements,
|
|
1696
1672
|
paymentButtonElements,
|
|
1697
1673
|
onSubmit: this.handleSubmit,
|
|
1698
1674
|
onInputChange: this.handleInputChange,
|
|
1675
|
+
onCardInputValueChange: this.handleCardInputValueChange,
|
|
1699
1676
|
onMethodRender: this.handleMethodRender,
|
|
1700
1677
|
onMethodsAvailable: this.handleMethodsAvailable,
|
|
1701
1678
|
onMethodRenderError: this.handleMethodRenderError,
|
|
@@ -1731,9 +1708,13 @@ class CheckoutInstance extends EventEmitter {
|
|
|
1731
1708
|
}
|
|
1732
1709
|
getCheckoutOptions(options) {
|
|
1733
1710
|
let wasPaymentProcessedStarted = false;
|
|
1711
|
+
const checkoutConfig = { ...this.checkoutConfig };
|
|
1712
|
+
delete checkoutConfig.card;
|
|
1734
1713
|
return {
|
|
1735
|
-
...
|
|
1714
|
+
...checkoutConfig,
|
|
1736
1715
|
...options,
|
|
1716
|
+
card: merge(this.getPrimerCardConfig() || {}, options.card || {}),
|
|
1717
|
+
applePay: merge(this.checkoutConfig.applePay || {}, options.applePay || {}),
|
|
1737
1718
|
onTokenizeSuccess: this.handleTokenizeSuccess,
|
|
1738
1719
|
onResumeSuccess: this.handleResumeSuccess,
|
|
1739
1720
|
onResumeError: error => {
|
|
@@ -1917,9 +1898,7 @@ class CheckoutInstance extends EventEmitter {
|
|
|
1917
1898
|
if (callbacks.onMethodsAvailable) {
|
|
1918
1899
|
this.on(EVENTS.METHODS_AVAILABLE, callbacks.onMethodsAvailable);
|
|
1919
1900
|
}
|
|
1920
|
-
let checkoutOptions = this.
|
|
1921
|
-
? { applePay: APPLE_PAY_COLLECTING_EMAIL_OPTIONS }
|
|
1922
|
-
: {});
|
|
1901
|
+
let checkoutOptions = this.mergeApplePayCollectingEmailOptions(this.getCheckoutOptions({}));
|
|
1923
1902
|
let methodOptions = {
|
|
1924
1903
|
onMethodRender: this.handleMethodRender,
|
|
1925
1904
|
onMethodRenderError: this.handleMethodRenderError,
|
|
@@ -1933,6 +1912,7 @@ class CheckoutInstance extends EventEmitter {
|
|
|
1933
1912
|
cardElements: cardDefaultOptions.cardElements,
|
|
1934
1913
|
onSubmit: this.handleSubmit,
|
|
1935
1914
|
onInputChange: this.handleInputChange,
|
|
1915
|
+
onCardInputValueChange: this.handleCardInputValueChange,
|
|
1936
1916
|
onMethodRender: this.handleMethodRender,
|
|
1937
1917
|
onMethodRenderError: this.handleMethodRenderError,
|
|
1938
1918
|
};
|