@funnelfox/billing 0.6.4-beta.0 → 0.6.4-beta.2
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 +164 -163
- package/dist/chunk-index.cjs3.js +24 -6
- package/dist/chunk-index.es.js +164 -163
- package/dist/chunk-index.es3.js +24 -6
- package/dist/funnelfox-billing.js +188 -169
- package/dist/funnelfox-billing.min.js +1 -1
- package/package.json +2 -2
- package/src/types.d.ts +30 -11
package/dist/chunk-index.es.js
CHANGED
|
@@ -148,64 +148,46 @@ class NetworkError extends FunnefoxSDKError {
|
|
|
148
148
|
}
|
|
149
149
|
|
|
150
150
|
/**
|
|
151
|
-
* @fileoverview
|
|
151
|
+
* @fileoverview Dynamic loader for Primer SDK
|
|
152
|
+
* Loads Primer script and CSS from CDN independently of bundler
|
|
152
153
|
*/
|
|
154
|
+
const PRIMER_CDN_BASE = 'https://sdk.primer.io/web';
|
|
155
|
+
const DEFAULT_VERSION = '2.57.3';
|
|
156
|
+
// Integrity hashes for specific versions (for SRI security)
|
|
157
|
+
const INTEGRITY_HASHES = {
|
|
158
|
+
'2.57.3': {
|
|
159
|
+
js: 'sha384-xq2SWkYvTlKOMpuXQUXq1QI3eZN7JiqQ3Sc72U9wY1IE30MW3HkwQWg/1n6BTMz4',
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
let loadingPromise = null;
|
|
163
|
+
let isLoaded = false;
|
|
153
164
|
/**
|
|
154
|
-
*
|
|
155
|
-
* Checks if script already exists before loading to prevent duplicates.
|
|
156
|
-
*
|
|
157
|
-
* @param options - Script configuration options
|
|
158
|
-
* @returns Promise that resolves when script is loaded or rejects on error
|
|
165
|
+
* Injects a script tag into the document head
|
|
159
166
|
*/
|
|
160
|
-
function
|
|
161
|
-
const { id, src, async = true, type = 'text/javascript', attributes = {}, integrity, crossOrigin, appendTo = 'body', } = options;
|
|
167
|
+
function injectScript$1(src, integrity) {
|
|
162
168
|
return new Promise((resolve, reject) => {
|
|
163
|
-
// Check if script already exists
|
|
164
|
-
|
|
165
|
-
if (id) {
|
|
166
|
-
existingScript = document.getElementById(id);
|
|
167
|
-
}
|
|
168
|
-
if (!existingScript) {
|
|
169
|
-
existingScript = document.querySelector(`script[src="${src}"]`);
|
|
170
|
-
}
|
|
169
|
+
// Check if script already exists
|
|
170
|
+
const existingScript = document.querySelector(`script[src="${src}"]`);
|
|
171
171
|
if (existingScript) {
|
|
172
172
|
resolve(existingScript);
|
|
173
173
|
return;
|
|
174
174
|
}
|
|
175
175
|
const script = document.createElement('script');
|
|
176
|
-
if (id) {
|
|
177
|
-
script.id = id;
|
|
178
|
-
}
|
|
179
|
-
script.type = type;
|
|
180
176
|
script.src = src;
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
}
|
|
177
|
+
script.async = true;
|
|
178
|
+
script.crossOrigin = 'anonymous';
|
|
184
179
|
if (integrity) {
|
|
185
180
|
script.integrity = integrity;
|
|
186
181
|
}
|
|
187
|
-
if (crossOrigin) {
|
|
188
|
-
script.crossOrigin = crossOrigin;
|
|
189
|
-
}
|
|
190
|
-
// Set additional attributes
|
|
191
|
-
Object.entries(attributes).forEach(([key, value]) => {
|
|
192
|
-
script.setAttribute(key, value);
|
|
193
|
-
});
|
|
194
182
|
script.onload = () => resolve(script);
|
|
195
|
-
script.onerror = () => reject(new Error(`Failed to load script
|
|
196
|
-
|
|
197
|
-
target.appendChild(script);
|
|
183
|
+
script.onerror = () => reject(new Error(`Failed to load Primer SDK script from ${src}`));
|
|
184
|
+
document.head.appendChild(script);
|
|
198
185
|
});
|
|
199
186
|
}
|
|
200
187
|
/**
|
|
201
|
-
*
|
|
202
|
-
* Checks if stylesheet already exists before loading to prevent duplicates.
|
|
203
|
-
*
|
|
204
|
-
* @param options - Stylesheet configuration options
|
|
205
|
-
* @returns Promise that resolves when stylesheet is loaded or rejects on error
|
|
188
|
+
* Injects a CSS link tag into the document head
|
|
206
189
|
*/
|
|
207
|
-
function
|
|
208
|
-
const { href, integrity, crossOrigin } = options;
|
|
190
|
+
function injectCSS(href, integrity) {
|
|
209
191
|
return new Promise((resolve, reject) => {
|
|
210
192
|
// Check if stylesheet already exists
|
|
211
193
|
const existingLink = document.querySelector(`link[href="${href}"]`);
|
|
@@ -216,32 +198,15 @@ function loadStylesheet(options) {
|
|
|
216
198
|
const link = document.createElement('link');
|
|
217
199
|
link.rel = 'stylesheet';
|
|
218
200
|
link.href = href;
|
|
201
|
+
link.crossOrigin = 'anonymous';
|
|
219
202
|
if (integrity) {
|
|
220
203
|
link.integrity = integrity;
|
|
221
204
|
}
|
|
222
|
-
if (crossOrigin) {
|
|
223
|
-
link.crossOrigin = crossOrigin;
|
|
224
|
-
}
|
|
225
205
|
link.onload = () => resolve(link);
|
|
226
|
-
link.onerror = () => reject(new Error(`Failed to load
|
|
206
|
+
link.onerror = () => reject(new Error(`Failed to load Primer SDK CSS from ${href}`));
|
|
227
207
|
document.head.appendChild(link);
|
|
228
208
|
});
|
|
229
209
|
}
|
|
230
|
-
|
|
231
|
-
/**
|
|
232
|
-
* @fileoverview Dynamic loader for Primer SDK
|
|
233
|
-
* Loads Primer script and CSS from CDN independently of bundler
|
|
234
|
-
*/
|
|
235
|
-
const PRIMER_CDN_BASE = 'https://sdk.primer.io/web';
|
|
236
|
-
const DEFAULT_VERSION = '2.57.3';
|
|
237
|
-
// Integrity hashes for specific versions (for SRI security)
|
|
238
|
-
const INTEGRITY_HASHES = {
|
|
239
|
-
'2.57.3': {
|
|
240
|
-
js: 'sha384-xq2SWkYvTlKOMpuXQUXq1QI3eZN7JiqQ3Sc72U9wY1IE30MW3HkwQWg/1n6BTMz4',
|
|
241
|
-
},
|
|
242
|
-
};
|
|
243
|
-
let loadingPromise = null;
|
|
244
|
-
let isLoaded = false;
|
|
245
210
|
/**
|
|
246
211
|
* Waits for window.Primer to be available
|
|
247
212
|
*/
|
|
@@ -293,17 +258,8 @@ async function loadPrimerSDK(version) {
|
|
|
293
258
|
try {
|
|
294
259
|
// Load CSS and JS in parallel
|
|
295
260
|
await Promise.all([
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
integrity: hashes?.css,
|
|
299
|
-
crossOrigin: 'anonymous',
|
|
300
|
-
}),
|
|
301
|
-
loadScript$1({
|
|
302
|
-
src: jsUrl,
|
|
303
|
-
integrity: hashes?.js,
|
|
304
|
-
crossOrigin: 'anonymous',
|
|
305
|
-
appendTo: 'head',
|
|
306
|
-
}),
|
|
261
|
+
injectCSS(cssUrl, hashes?.css),
|
|
262
|
+
injectScript$1(jsUrl, hashes?.js),
|
|
307
263
|
]);
|
|
308
264
|
// Wait for Primer to be available on window
|
|
309
265
|
await waitForPrimer();
|
|
@@ -346,28 +302,6 @@ function generateId(prefix = '') {
|
|
|
346
302
|
const random = Math.random().toString(36).substr(2, 5);
|
|
347
303
|
return `${prefix}${timestamp}_${random}`;
|
|
348
304
|
}
|
|
349
|
-
/**
|
|
350
|
-
* Generates a UUID v4 compliant string (RFC 4122).
|
|
351
|
-
* Meets Airwallex requirements:
|
|
352
|
-
* - Maximum 128 characters (UUID is 36 chars)
|
|
353
|
-
* - Only contains: a-z, A-Z, 0-9, underscore, hyphen
|
|
354
|
-
* - No prefix + timestamp pattern
|
|
355
|
-
* - Not a short series of numbers
|
|
356
|
-
*
|
|
357
|
-
* @returns UUID v4 string (e.g., "a3bb189e-8bf9-3888-9912-ace4e6543002")
|
|
358
|
-
*/
|
|
359
|
-
function generateUUID() {
|
|
360
|
-
// Use crypto.randomUUID if available (modern browsers)
|
|
361
|
-
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
|
362
|
-
return crypto.randomUUID();
|
|
363
|
-
}
|
|
364
|
-
// Fallback: manual UUID v4 generation
|
|
365
|
-
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
|
|
366
|
-
const r = (Math.random() * 16) | 0;
|
|
367
|
-
const v = c === 'x' ? r : (r & 0x3) | 0x8;
|
|
368
|
-
return v.toString(16);
|
|
369
|
-
});
|
|
370
|
-
}
|
|
371
305
|
function sleep(ms) {
|
|
372
306
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
373
307
|
}
|
|
@@ -486,7 +420,7 @@ var PaymentMethod;
|
|
|
486
420
|
/**
|
|
487
421
|
* @fileoverview Constants for Funnefox SDK
|
|
488
422
|
*/
|
|
489
|
-
const SDK_VERSION = '0.6.4-beta.
|
|
423
|
+
const SDK_VERSION = '0.6.4-beta.2';
|
|
490
424
|
const DEFAULTS = {
|
|
491
425
|
BASE_URL: 'https://billing.funnelfox.com',
|
|
492
426
|
REGION: 'default',
|
|
@@ -600,6 +534,26 @@ const APPLE_PAY_COLLECTING_EMAIL_OPTIONS = {
|
|
|
600
534
|
},
|
|
601
535
|
};
|
|
602
536
|
|
|
537
|
+
/**
|
|
538
|
+
* @fileoverview Input validation utilities for Funnefox SDK
|
|
539
|
+
*/
|
|
540
|
+
function isValidEmail(email) {
|
|
541
|
+
if (typeof email !== 'string')
|
|
542
|
+
return false;
|
|
543
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
544
|
+
return emailRegex.test(email);
|
|
545
|
+
}
|
|
546
|
+
function sanitizeString(input) {
|
|
547
|
+
return input?.trim() || '';
|
|
548
|
+
}
|
|
549
|
+
function requireString(value, fieldName) {
|
|
550
|
+
const sanitized = sanitizeString(value);
|
|
551
|
+
if (sanitized.length === 0) {
|
|
552
|
+
throw new ValidationError(fieldName, 'must be a non-empty string', value);
|
|
553
|
+
}
|
|
554
|
+
return true;
|
|
555
|
+
}
|
|
556
|
+
|
|
603
557
|
/**
|
|
604
558
|
* @fileoverview Primer SDK integration wrapper
|
|
605
559
|
*/
|
|
@@ -716,6 +670,7 @@ class PrimerWrapper {
|
|
|
716
670
|
onInputChange: options.onInputChange,
|
|
717
671
|
onMethodRenderError: options.onMethodRenderError,
|
|
718
672
|
onMethodRender: options.onMethodRender,
|
|
673
|
+
onCardInputValueChange: options.onCardInputValueChange,
|
|
719
674
|
});
|
|
720
675
|
this.paymentMethodsInterfaces.push(cardInterface);
|
|
721
676
|
return cardInterface;
|
|
@@ -734,7 +689,7 @@ class PrimerWrapper {
|
|
|
734
689
|
throw new PrimerError('Failed to initialize Primer checkout', error);
|
|
735
690
|
}
|
|
736
691
|
}
|
|
737
|
-
async renderCardCheckoutWithElements(elements, { onSubmit, onInputChange, onMethodRenderError, onMethodRender, }) {
|
|
692
|
+
async renderCardCheckoutWithElements(elements, { onSubmit, onInputChange, onCardInputValueChange, onMethodRenderError, onMethodRender, }) {
|
|
738
693
|
try {
|
|
739
694
|
if (!this.currentHeadless) {
|
|
740
695
|
throw new PrimerError('Headless checkout not found');
|
|
@@ -751,7 +706,12 @@ class PrimerWrapper {
|
|
|
751
706
|
const { valid, validationErrors } = await pmManager.validate();
|
|
752
707
|
const cardHolderError = validationErrors.find(v => v.name === 'cardholderName');
|
|
753
708
|
dispatchError('cardholderName', cardHolderError?.message || null);
|
|
754
|
-
|
|
709
|
+
const emailAddress = elements.emailAddress?.value?.trim();
|
|
710
|
+
const emailError = emailAddress && !isValidEmail(emailAddress)
|
|
711
|
+
? 'Please enter a valid email address'
|
|
712
|
+
: null;
|
|
713
|
+
dispatchError('emailAddress', emailError);
|
|
714
|
+
return valid && !emailError;
|
|
755
715
|
};
|
|
756
716
|
const dispatchError = (inputName, error) => {
|
|
757
717
|
onInputChange(inputName, error);
|
|
@@ -766,7 +726,16 @@ class PrimerWrapper {
|
|
|
766
726
|
pmManager.setCardholderName(e.target.value);
|
|
767
727
|
dispatchError('cardholderName', null);
|
|
768
728
|
};
|
|
729
|
+
const emailAddressOnChange = (e) => {
|
|
730
|
+
const value = e.target.value;
|
|
731
|
+
const email = value.trim();
|
|
732
|
+
onCardInputValueChange?.('emailAddress', email);
|
|
733
|
+
dispatchError('emailAddress', email && !isValidEmail(email)
|
|
734
|
+
? 'Please enter a valid email address'
|
|
735
|
+
: null);
|
|
736
|
+
};
|
|
769
737
|
elements.cardholderName?.addEventListener('input', cardHolderOnChange);
|
|
738
|
+
elements.emailAddress?.addEventListener('input', emailAddressOnChange);
|
|
770
739
|
cardNumberInput.addEventListener('change', onHostedInputChange('cardNumber'));
|
|
771
740
|
expiryInput.addEventListener('change', onHostedInputChange('expiryDate'));
|
|
772
741
|
cvvInput.addEventListener('change', onHostedInputChange('cvv'));
|
|
@@ -806,7 +775,8 @@ class PrimerWrapper {
|
|
|
806
775
|
]);
|
|
807
776
|
const onDestroy = () => {
|
|
808
777
|
pmManager.removeHostedInputs();
|
|
809
|
-
elements.cardholderName?.removeEventListener('
|
|
778
|
+
elements.cardholderName?.removeEventListener('input', cardHolderOnChange);
|
|
779
|
+
elements.emailAddress?.removeEventListener('input', emailAddressOnChange);
|
|
810
780
|
elements.button?.removeEventListener('click', onSubmitHandler);
|
|
811
781
|
};
|
|
812
782
|
this.destroyCallbacks.push(onDestroy);
|
|
@@ -822,6 +792,9 @@ class PrimerWrapper {
|
|
|
822
792
|
if (elements.cardholderName) {
|
|
823
793
|
elements.cardholderName.disabled = disabled;
|
|
824
794
|
}
|
|
795
|
+
if (elements.emailAddress) {
|
|
796
|
+
elements.emailAddress.disabled = disabled;
|
|
797
|
+
}
|
|
825
798
|
},
|
|
826
799
|
submit: () => onSubmitHandler(),
|
|
827
800
|
destroy: () => {
|
|
@@ -844,7 +817,7 @@ class PrimerWrapper {
|
|
|
844
817
|
});
|
|
845
818
|
}
|
|
846
819
|
async renderCheckout(clientToken, checkoutOptions, checkoutRenderOptions) {
|
|
847
|
-
const { cardElements, paymentButtonElements, container, onSubmit, onInputChange, onMethodRender, onMethodRenderError, onMethodsAvailable, } = checkoutRenderOptions;
|
|
820
|
+
const { cardElements, paymentButtonElements, container, onSubmit, onInputChange, onMethodRender, onMethodRenderError, onMethodsAvailable, onCardInputValueChange, } = checkoutRenderOptions;
|
|
848
821
|
await this.initializeHeadlessCheckout(clientToken, checkoutOptions);
|
|
849
822
|
onMethodsAvailable?.(this.availableMethods);
|
|
850
823
|
await Promise.all(this.availableMethods.map(method => {
|
|
@@ -856,6 +829,7 @@ class PrimerWrapper {
|
|
|
856
829
|
onInputChange,
|
|
857
830
|
onMethodRender,
|
|
858
831
|
onMethodRenderError,
|
|
832
|
+
onCardInputValueChange,
|
|
859
833
|
});
|
|
860
834
|
}
|
|
861
835
|
else {
|
|
@@ -972,20 +946,6 @@ class PrimerWrapper {
|
|
|
972
946
|
}
|
|
973
947
|
PrimerWrapper.headlessManager = new HeadlessManager();
|
|
974
948
|
|
|
975
|
-
/**
|
|
976
|
-
* @fileoverview Input validation utilities for Funnefox SDK
|
|
977
|
-
*/
|
|
978
|
-
function sanitizeString(input) {
|
|
979
|
-
return input?.trim() || '';
|
|
980
|
-
}
|
|
981
|
-
function requireString(value, fieldName) {
|
|
982
|
-
const sanitized = sanitizeString(value);
|
|
983
|
-
if (sanitized.length === 0) {
|
|
984
|
-
throw new ValidationError(fieldName, 'must be a non-empty string', value);
|
|
985
|
-
}
|
|
986
|
-
return true;
|
|
987
|
-
}
|
|
988
|
-
|
|
989
949
|
/**
|
|
990
950
|
* @fileoverview API client for Funnefox backend integration
|
|
991
951
|
*/
|
|
@@ -1081,6 +1041,9 @@ class APIClient {
|
|
|
1081
1041
|
payment_method_token: params.paymentMethodToken,
|
|
1082
1042
|
client_metadata: params.clientMetadata || {},
|
|
1083
1043
|
};
|
|
1044
|
+
if (params.email !== undefined) {
|
|
1045
|
+
payload.email_address = params.email;
|
|
1046
|
+
}
|
|
1084
1047
|
return (await this.request(API_ENDPOINTS.CREATE_PAYMENT, {
|
|
1085
1048
|
method: 'POST',
|
|
1086
1049
|
body: JSON.stringify(payload),
|
|
@@ -1385,34 +1348,6 @@ const renderError = (container, reqId) => {
|
|
|
1385
1348
|
}
|
|
1386
1349
|
};
|
|
1387
1350
|
|
|
1388
|
-
/**
|
|
1389
|
-
* @fileoverview Airwallex device fingerprinting script loader
|
|
1390
|
-
*/
|
|
1391
|
-
/**
|
|
1392
|
-
* Loads Airwallex device fingerprinting script for fraud prevention.
|
|
1393
|
-
* The script collects browser, screen, device, and interaction data.
|
|
1394
|
-
*
|
|
1395
|
-
* @param sessionId - Unique order session ID (UUID v4 format, max 128 chars)
|
|
1396
|
-
* @param isDemoMode - If true, uses demo environment URL for testing
|
|
1397
|
-
* @returns Promise that resolves when script is loaded
|
|
1398
|
-
*
|
|
1399
|
-
* @see https://www.airwallex.com/docs/payments/online-payments/native-api/device-fingerprinting
|
|
1400
|
-
*/
|
|
1401
|
-
async function loadAirwallexDeviceFingerprint(sessionId, isDemoMode = false) {
|
|
1402
|
-
const scriptId = 'airwallex-fraud-api';
|
|
1403
|
-
const src = isDemoMode
|
|
1404
|
-
? 'https://static-demo.airwallex.com/webapp/fraud/device-fingerprint/index.js'
|
|
1405
|
-
: 'https://static.airwallex.com/webapp/fraud/device-fingerprint/index.js';
|
|
1406
|
-
await loadScript$1({
|
|
1407
|
-
id: scriptId,
|
|
1408
|
-
src,
|
|
1409
|
-
async: true,
|
|
1410
|
-
attributes: {
|
|
1411
|
-
'data-order-session-id': sessionId,
|
|
1412
|
-
},
|
|
1413
|
-
});
|
|
1414
|
-
}
|
|
1415
|
-
|
|
1416
1351
|
/**
|
|
1417
1352
|
* @fileoverview Checkout instance manager for Funnefox SDK
|
|
1418
1353
|
*/
|
|
@@ -1421,10 +1356,14 @@ class CheckoutInstance extends EventEmitter {
|
|
|
1421
1356
|
super();
|
|
1422
1357
|
this.counter = 0;
|
|
1423
1358
|
this.radarSessionId = null;
|
|
1424
|
-
this.airwallexDeviceId = null;
|
|
1425
1359
|
this.handleInputChange = (inputName, error) => {
|
|
1426
1360
|
this.emit(EVENTS.INPUT_ERROR, { name: inputName, error });
|
|
1427
1361
|
};
|
|
1362
|
+
this.handleCardInputValueChange = (inputName, value) => {
|
|
1363
|
+
if (inputName === 'emailAddress') {
|
|
1364
|
+
this.cardEmailAddress = value?.trim() || undefined;
|
|
1365
|
+
}
|
|
1366
|
+
};
|
|
1428
1367
|
this.handleMethodRender = (method) => {
|
|
1429
1368
|
this.emit(EVENTS.METHOD_RENDER, method);
|
|
1430
1369
|
};
|
|
@@ -1443,16 +1382,13 @@ class CheckoutInstance extends EventEmitter {
|
|
|
1443
1382
|
try {
|
|
1444
1383
|
this.onLoaderChangeWithRace(true);
|
|
1445
1384
|
this._setState('processing');
|
|
1446
|
-
const
|
|
1447
|
-
this.radarSessionId,
|
|
1448
|
-
this.airwallexDeviceId,
|
|
1449
|
-
]);
|
|
1385
|
+
const radarSessionId = await this.radarSessionId;
|
|
1450
1386
|
const paymentResponse = await this.apiClient.createPayment({
|
|
1451
1387
|
orderId: this.orderId,
|
|
1452
1388
|
paymentMethodToken: paymentMethodTokenData.token,
|
|
1389
|
+
email: this.getPaymentEmailAddress(),
|
|
1453
1390
|
clientMetadata: {
|
|
1454
1391
|
radarSessionId,
|
|
1455
|
-
airwallexDeviceId,
|
|
1456
1392
|
},
|
|
1457
1393
|
});
|
|
1458
1394
|
const result = this.apiClient.processPaymentResponse(paymentResponse);
|
|
@@ -1522,6 +1458,7 @@ class CheckoutInstance extends EventEmitter {
|
|
|
1522
1458
|
this.clientToken = null;
|
|
1523
1459
|
this.primerWrapper = new PrimerWrapper();
|
|
1524
1460
|
this.isDestroyed = false;
|
|
1461
|
+
this.cardEmailAddress = this.checkoutConfig.customer.email;
|
|
1525
1462
|
this._setupCallbackBridges();
|
|
1526
1463
|
}
|
|
1527
1464
|
_setupCallbackBridges() {
|
|
@@ -1600,18 +1537,9 @@ class CheckoutInstance extends EventEmitter {
|
|
|
1600
1537
|
.catch(() => '');
|
|
1601
1538
|
});
|
|
1602
1539
|
}
|
|
1603
|
-
// Initialize Airwallex device fingerprinting if enabled by backend
|
|
1604
|
-
if (response.data?.airwallex_risk_enabled) {
|
|
1605
|
-
const deviceId = generateUUID();
|
|
1606
|
-
this.airwallexDeviceId = loadAirwallexDeviceFingerprint(deviceId, true)
|
|
1607
|
-
.then(() => deviceId)
|
|
1608
|
-
.catch(() => {
|
|
1609
|
-
// Silently fail - return deviceId anyway
|
|
1610
|
-
return deviceId;
|
|
1611
|
-
});
|
|
1612
|
-
}
|
|
1613
1540
|
this.isCollectingApplePayEmail =
|
|
1614
1541
|
!!response.data?.collect_apple_pay_email;
|
|
1542
|
+
this.applySessionCardFieldConfig(response);
|
|
1615
1543
|
return response;
|
|
1616
1544
|
});
|
|
1617
1545
|
// Cache the successful response
|
|
@@ -1622,11 +1550,81 @@ class CheckoutInstance extends EventEmitter {
|
|
|
1622
1550
|
this.orderId = sessionData.orderId;
|
|
1623
1551
|
this.clientToken = sessionData.clientToken;
|
|
1624
1552
|
}
|
|
1553
|
+
applySessionCardFieldConfig(response) {
|
|
1554
|
+
const cardConfig = this.checkoutConfig.card || {};
|
|
1555
|
+
if (cardConfig.emailAddress?.visible === undefined &&
|
|
1556
|
+
response.data?.show_email_field !== undefined) {
|
|
1557
|
+
cardConfig.emailAddress = {
|
|
1558
|
+
...cardConfig.emailAddress,
|
|
1559
|
+
visible: response.data.show_email_field,
|
|
1560
|
+
};
|
|
1561
|
+
}
|
|
1562
|
+
if (cardConfig.cardholderName?.required === undefined &&
|
|
1563
|
+
response.data?.show_cardholder_name_field !== undefined) {
|
|
1564
|
+
cardConfig.cardholderName = {
|
|
1565
|
+
...cardConfig.cardholderName,
|
|
1566
|
+
required: response.data.show_cardholder_name_field,
|
|
1567
|
+
};
|
|
1568
|
+
}
|
|
1569
|
+
if (Object.keys(cardConfig).length > 0) {
|
|
1570
|
+
this.checkoutConfig.card = cardConfig;
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
getPrimerCardConfig() {
|
|
1574
|
+
const cardConfig = { ...(this.checkoutConfig.card || {}) };
|
|
1575
|
+
delete cardConfig.emailAddress;
|
|
1576
|
+
return Object.keys(cardConfig).length
|
|
1577
|
+
? cardConfig
|
|
1578
|
+
: undefined;
|
|
1579
|
+
}
|
|
1580
|
+
getPaymentEmailAddress() {
|
|
1581
|
+
const email = this.cardEmailAddress?.trim() || this.checkoutConfig.customer.email;
|
|
1582
|
+
if (!email || !isValidEmail(email)) {
|
|
1583
|
+
return undefined;
|
|
1584
|
+
}
|
|
1585
|
+
const template = this.checkoutConfig.card?.emailAddress?.template;
|
|
1586
|
+
if (template?.includes('{{email}}')) {
|
|
1587
|
+
return template.replace(/\{\{email\}\}/g, email);
|
|
1588
|
+
}
|
|
1589
|
+
return email;
|
|
1590
|
+
}
|
|
1591
|
+
mergeApplePayCollectingEmailOptions(checkoutOptions) {
|
|
1592
|
+
if (!this.isCollectingApplePayEmail) {
|
|
1593
|
+
return checkoutOptions;
|
|
1594
|
+
}
|
|
1595
|
+
const billingFields = Array.from(new Set([
|
|
1596
|
+
...(checkoutOptions.applePay?.billingOptions
|
|
1597
|
+
?.requiredBillingContactFields || []),
|
|
1598
|
+
...(APPLE_PAY_COLLECTING_EMAIL_OPTIONS.billingOptions
|
|
1599
|
+
?.requiredBillingContactFields || []),
|
|
1600
|
+
]));
|
|
1601
|
+
const shippingFields = Array.from(new Set([
|
|
1602
|
+
...(checkoutOptions.applePay?.shippingOptions
|
|
1603
|
+
?.requiredShippingContactFields || []),
|
|
1604
|
+
...(APPLE_PAY_COLLECTING_EMAIL_OPTIONS.shippingOptions
|
|
1605
|
+
?.requiredShippingContactFields || []),
|
|
1606
|
+
]));
|
|
1607
|
+
return merge(checkoutOptions, {
|
|
1608
|
+
applePay: {
|
|
1609
|
+
billingOptions: {
|
|
1610
|
+
requiredBillingContactFields: billingFields,
|
|
1611
|
+
},
|
|
1612
|
+
shippingOptions: {
|
|
1613
|
+
requiredShippingContactFields: shippingFields,
|
|
1614
|
+
},
|
|
1615
|
+
},
|
|
1616
|
+
});
|
|
1617
|
+
}
|
|
1625
1618
|
convertCardSelectorsToElements(selectors, container) {
|
|
1626
1619
|
const cardNumber = container.querySelector(selectors.cardNumber);
|
|
1627
1620
|
const expiryDate = container.querySelector(selectors.expiryDate);
|
|
1628
1621
|
const cvv = container.querySelector(selectors.cvv);
|
|
1629
|
-
const cardholderName =
|
|
1622
|
+
const cardholderName = selectors.cardholderName
|
|
1623
|
+
? container.querySelector(selectors.cardholderName)
|
|
1624
|
+
: undefined;
|
|
1625
|
+
const emailAddress = selectors.emailAddress
|
|
1626
|
+
? container.querySelector(selectors.emailAddress)
|
|
1627
|
+
: undefined;
|
|
1630
1628
|
const button = container.querySelector(selectors.button);
|
|
1631
1629
|
if (!cardNumber || !expiryDate || !cvv || !button) {
|
|
1632
1630
|
throw new CheckoutError('Required card input elements not found in container');
|
|
@@ -1636,6 +1634,7 @@ class CheckoutInstance extends EventEmitter {
|
|
|
1636
1634
|
expiryDate,
|
|
1637
1635
|
cvv,
|
|
1638
1636
|
cardholderName,
|
|
1637
|
+
emailAddress,
|
|
1639
1638
|
button,
|
|
1640
1639
|
};
|
|
1641
1640
|
}
|
|
@@ -1685,15 +1684,14 @@ class CheckoutInstance extends EventEmitter {
|
|
|
1685
1684
|
paymentButtonElements = this.convertPaymentButtonSelectorsToElements(this.checkoutConfig.paymentButtonSelectors);
|
|
1686
1685
|
checkoutOptions = this.getCheckoutOptions({});
|
|
1687
1686
|
}
|
|
1688
|
-
checkoutOptions =
|
|
1689
|
-
? { applePay: APPLE_PAY_COLLECTING_EMAIL_OPTIONS }
|
|
1690
|
-
: {});
|
|
1687
|
+
checkoutOptions = this.mergeApplePayCollectingEmailOptions(checkoutOptions);
|
|
1691
1688
|
await this.primerWrapper.renderCheckout(this.clientToken, checkoutOptions, {
|
|
1692
1689
|
container: containerElement,
|
|
1693
1690
|
cardElements,
|
|
1694
1691
|
paymentButtonElements,
|
|
1695
1692
|
onSubmit: this.handleSubmit,
|
|
1696
1693
|
onInputChange: this.handleInputChange,
|
|
1694
|
+
onCardInputValueChange: this.handleCardInputValueChange,
|
|
1697
1695
|
onMethodRender: this.handleMethodRender,
|
|
1698
1696
|
onMethodsAvailable: this.handleMethodsAvailable,
|
|
1699
1697
|
onMethodRenderError: this.handleMethodRenderError,
|
|
@@ -1729,9 +1727,13 @@ class CheckoutInstance extends EventEmitter {
|
|
|
1729
1727
|
}
|
|
1730
1728
|
getCheckoutOptions(options) {
|
|
1731
1729
|
let wasPaymentProcessedStarted = false;
|
|
1730
|
+
const checkoutConfig = { ...this.checkoutConfig };
|
|
1731
|
+
delete checkoutConfig.card;
|
|
1732
1732
|
return {
|
|
1733
|
-
...
|
|
1733
|
+
...checkoutConfig,
|
|
1734
1734
|
...options,
|
|
1735
|
+
card: merge(this.getPrimerCardConfig() || {}, options.card || {}),
|
|
1736
|
+
applePay: merge(this.checkoutConfig.applePay || {}, options.applePay || {}),
|
|
1735
1737
|
onTokenizeSuccess: this.handleTokenizeSuccess,
|
|
1736
1738
|
onResumeSuccess: this.handleResumeSuccess,
|
|
1737
1739
|
onResumeError: error => {
|
|
@@ -1915,9 +1917,7 @@ class CheckoutInstance extends EventEmitter {
|
|
|
1915
1917
|
if (callbacks.onMethodsAvailable) {
|
|
1916
1918
|
this.on(EVENTS.METHODS_AVAILABLE, callbacks.onMethodsAvailable);
|
|
1917
1919
|
}
|
|
1918
|
-
let checkoutOptions = this.
|
|
1919
|
-
? { applePay: APPLE_PAY_COLLECTING_EMAIL_OPTIONS }
|
|
1920
|
-
: {});
|
|
1920
|
+
let checkoutOptions = this.mergeApplePayCollectingEmailOptions(this.getCheckoutOptions({}));
|
|
1921
1921
|
let methodOptions = {
|
|
1922
1922
|
onMethodRender: this.handleMethodRender,
|
|
1923
1923
|
onMethodRenderError: this.handleMethodRenderError,
|
|
@@ -1931,6 +1931,7 @@ class CheckoutInstance extends EventEmitter {
|
|
|
1931
1931
|
cardElements: cardDefaultOptions.cardElements,
|
|
1932
1932
|
onSubmit: this.handleSubmit,
|
|
1933
1933
|
onInputChange: this.handleInputChange,
|
|
1934
|
+
onCardInputValueChange: this.handleCardInputValueChange,
|
|
1934
1935
|
onMethodRender: this.handleMethodRender,
|
|
1935
1936
|
onMethodRenderError: this.handleMethodRenderError,
|
|
1936
1937
|
};
|