@funnelfox/billing 0.6.7 → 0.7.1-beta.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.
@@ -148,46 +148,64 @@ class NetworkError extends FunnefoxSDKError {
148
148
  }
149
149
 
150
150
  /**
151
- * @fileoverview Dynamic loader for Primer SDK
152
- * Loads Primer script and CSS from CDN independently of bundler
151
+ * @fileoverview Generic script and stylesheet loader utility to reduce bundle size
153
152
  */
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;
164
153
  /**
165
- * Injects a script tag into the document head
154
+ * Dynamically loads an external script into the document.
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
166
159
  */
167
- function injectScript$1(src, integrity) {
160
+ function loadScript$1(options) {
161
+ const { id, src, async = true, type = 'text/javascript', attributes = {}, integrity, crossOrigin, appendTo = 'body', } = options;
168
162
  return new Promise((resolve, reject) => {
169
- // Check if script already exists
170
- const existingScript = document.querySelector(`script[src="${src}"]`);
163
+ // Check if script already exists (by ID or src)
164
+ let existingScript = null;
165
+ if (id) {
166
+ existingScript = document.getElementById(id);
167
+ }
168
+ if (!existingScript) {
169
+ existingScript = document.querySelector(`script[src="${src}"]`);
170
+ }
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;
176
180
  script.src = src;
177
- script.async = true;
178
- script.crossOrigin = 'anonymous';
181
+ if (async) {
182
+ script.async = true;
183
+ }
179
184
  if (integrity) {
180
185
  script.integrity = integrity;
181
186
  }
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
+ });
182
194
  script.onload = () => resolve(script);
183
- script.onerror = () => reject(new Error(`Failed to load Primer SDK script from ${src}`));
184
- document.head.appendChild(script);
195
+ script.onerror = () => reject(new Error(`Failed to load script: ${src}`));
196
+ const target = appendTo === 'head' ? document.head : document.body;
197
+ target.appendChild(script);
185
198
  });
186
199
  }
187
200
  /**
188
- * Injects a CSS link tag into the document head
201
+ * Dynamically loads an external stylesheet into the document head.
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
189
206
  */
190
- function injectCSS(href, integrity) {
207
+ function loadStylesheet(options) {
208
+ const { href, integrity, crossOrigin } = options;
191
209
  return new Promise((resolve, reject) => {
192
210
  // Check if stylesheet already exists
193
211
  const existingLink = document.querySelector(`link[href="${href}"]`);
@@ -198,15 +216,32 @@ function injectCSS(href, integrity) {
198
216
  const link = document.createElement('link');
199
217
  link.rel = 'stylesheet';
200
218
  link.href = href;
201
- link.crossOrigin = 'anonymous';
202
219
  if (integrity) {
203
220
  link.integrity = integrity;
204
221
  }
222
+ if (crossOrigin) {
223
+ link.crossOrigin = crossOrigin;
224
+ }
205
225
  link.onload = () => resolve(link);
206
- link.onerror = () => reject(new Error(`Failed to load Primer SDK CSS from ${href}`));
226
+ link.onerror = () => reject(new Error(`Failed to load stylesheet: ${href}`));
207
227
  document.head.appendChild(link);
208
228
  });
209
229
  }
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;
210
245
  /**
211
246
  * Waits for window.Primer to be available
212
247
  */
@@ -258,8 +293,17 @@ async function loadPrimerSDK(version) {
258
293
  try {
259
294
  // Load CSS and JS in parallel
260
295
  await Promise.all([
261
- injectCSS(cssUrl, hashes?.css),
262
- injectScript$1(jsUrl, hashes?.js),
296
+ loadStylesheet({
297
+ href: cssUrl,
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
+ }),
263
307
  ]);
264
308
  // Wait for Primer to be available on window
265
309
  await waitForPrimer();
@@ -302,6 +346,28 @@ function generateId(prefix = '') {
302
346
  const random = Math.random().toString(36).substr(2, 5);
303
347
  return `${prefix}${timestamp}_${random}`;
304
348
  }
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
+ }
305
371
  function sleep(ms) {
306
372
  return new Promise(resolve => setTimeout(resolve, ms));
307
373
  }
@@ -420,7 +486,7 @@ var PaymentMethod;
420
486
  /**
421
487
  * @fileoverview Constants for Funnefox SDK
422
488
  */
423
- const SDK_VERSION = '0.6.7';
489
+ const SDK_VERSION = '0.7.1-beta.0';
424
490
  const DEFAULTS = {
425
491
  BASE_URL: 'https://billing.funnelfox.com',
426
492
  REGION: 'default',
@@ -671,6 +737,8 @@ class PrimerWrapper {
671
737
  onMethodRenderError: options.onMethodRenderError,
672
738
  onMethodRender: options.onMethodRender,
673
739
  onCardInputValueChange: options.onCardInputValueChange,
740
+ isCardholderNameRequired: options.isCardholderNameRequired,
741
+ isPostalCodeRequired: options.isPostalCodeRequired,
674
742
  });
675
743
  this.paymentMethodsInterfaces.push(cardInterface);
676
744
  return cardInterface;
@@ -689,7 +757,7 @@ class PrimerWrapper {
689
757
  throw new PrimerError('Failed to initialize Primer checkout', error);
690
758
  }
691
759
  }
692
- async renderCardCheckoutWithElements(elements, { onSubmit, onInputChange, onCardInputValueChange, onMethodRenderError, onMethodRender, }) {
760
+ async renderCardCheckoutWithElements(elements, { onSubmit, onInputChange, onCardInputValueChange, isCardholderNameRequired, isPostalCodeRequired, onMethodRenderError, onMethodRender, }) {
693
761
  try {
694
762
  if (!this.currentHeadless) {
695
763
  throw new PrimerError('Headless checkout not found');
@@ -705,8 +773,10 @@ class PrimerWrapper {
705
773
  if (!pmManager)
706
774
  return false;
707
775
  const { valid, validationErrors } = await pmManager.validate();
708
- const cardHolderError = validationErrors.find(v => v.name === 'cardholderName');
709
- dispatchError('cardholderName', cardHolderError?.message || null);
776
+ const cardHolderError = isCardholderNameRequired?.()
777
+ ? validationErrors.find(v => v.name === 'cardholderName')?.message
778
+ : null;
779
+ dispatchError('cardholderName', cardHolderError);
710
780
  let emailError = null;
711
781
  if (hasEmail) {
712
782
  const emailAddress = elements.emailAddress?.value?.trim();
@@ -715,11 +785,19 @@ class PrimerWrapper {
715
785
  : null;
716
786
  dispatchError('emailAddress', emailError);
717
787
  }
718
- return valid && !emailError;
788
+ const postalCodeError = getPostalCodeError();
789
+ dispatchError('postalCode', postalCodeError);
790
+ return valid && !emailError && !cardHolderError && !postalCodeError;
719
791
  };
720
792
  const dispatchError = (inputName, error) => {
721
793
  onInputChange(inputName, error);
722
794
  };
795
+ const getPostalCodeError = () => {
796
+ const postalCode = elements.postalCode?.value?.trim();
797
+ return isPostalCodeRequired?.() && !postalCode
798
+ ? 'Please enter a postal code'
799
+ : null;
800
+ };
723
801
  const onHostedInputChange = (name) => (event) => {
724
802
  const input = event;
725
803
  if (input.submitted) {
@@ -742,7 +820,22 @@ class PrimerWrapper {
742
820
  };
743
821
  elements.emailAddress.addEventListener('input', emailAddressOnChange);
744
822
  }
823
+ const countrySelectorOnChange = (e) => {
824
+ const countryCode = e.target.value.trim();
825
+ onCardInputValueChange?.('countryCode', countryCode);
826
+ if (!isPostalCodeRequired?.()) {
827
+ dispatchError('postalCode', null);
828
+ }
829
+ };
830
+ const postalCodeOnChange = (e) => {
831
+ const postalCode = e.target.value.trim();
832
+ onCardInputValueChange?.('postalCode', postalCode);
833
+ dispatchError('postalCode', getPostalCodeError());
834
+ };
745
835
  elements.cardholderName?.addEventListener('input', cardHolderOnChange);
836
+ elements.emailAddress?.addEventListener('input', emailAddressOnChange);
837
+ elements.countrySelector?.addEventListener('change', countrySelectorOnChange);
838
+ elements.postalCode?.addEventListener('input', postalCodeOnChange);
746
839
  cardNumberInput.addEventListener('change', onHostedInputChange('cardNumber'));
747
840
  expiryInput.addEventListener('change', onHostedInputChange('expiryDate'));
748
841
  cvvInput.addEventListener('change', onHostedInputChange('cvv'));
@@ -785,6 +878,8 @@ class PrimerWrapper {
785
878
  pmManager.removeHostedInputs();
786
879
  elements.cardholderName?.removeEventListener('input', cardHolderOnChange);
787
880
  elements.emailAddress?.removeEventListener('input', emailAddressOnChange);
881
+ elements.countrySelector?.removeEventListener('change', countrySelectorOnChange);
882
+ elements.postalCode?.removeEventListener('input', postalCodeOnChange);
788
883
  elements.button?.removeEventListener('click', onSubmitHandler);
789
884
  };
790
885
  this.destroyCallbacks.push(onDestroy);
@@ -803,6 +898,12 @@ class PrimerWrapper {
803
898
  if (elements.emailAddress) {
804
899
  elements.emailAddress.disabled = disabled;
805
900
  }
901
+ if (elements.countrySelector) {
902
+ elements.countrySelector.disabled = disabled;
903
+ }
904
+ if (elements.postalCode) {
905
+ elements.postalCode.disabled = disabled;
906
+ }
806
907
  },
807
908
  submit: () => onSubmitHandler(),
808
909
  destroy: () => {
@@ -825,7 +926,7 @@ class PrimerWrapper {
825
926
  }, method);
826
927
  }
827
928
  async renderCheckout(clientToken, checkoutOptions, checkoutRenderOptions) {
828
- const { cardElements, paymentButtonElements, container, onSubmit, onInputChange, onMethodRender, onMethodRenderError, onMethodsAvailable, onCardInputValueChange, } = checkoutRenderOptions;
929
+ const { cardElements, paymentButtonElements, container, onSubmit, onInputChange, onMethodRender, onMethodRenderError, onMethodsAvailable, onCardInputValueChange, isCardholderNameRequired, isPostalCodeRequired, } = checkoutRenderOptions;
829
930
  await this.initializeHeadlessCheckout(clientToken, checkoutOptions);
830
931
  onMethodsAvailable?.(this.availableMethods);
831
932
  await Promise.all(this.availableMethods.map(method => {
@@ -838,6 +939,8 @@ class PrimerWrapper {
838
939
  onMethodRender,
839
940
  onMethodRenderError,
840
941
  onCardInputValueChange,
942
+ isCardholderNameRequired,
943
+ isPostalCodeRequired,
841
944
  });
842
945
  }
843
946
  else {
@@ -1052,6 +1155,12 @@ class APIClient {
1052
1155
  if (params.email !== undefined) {
1053
1156
  payload.email_address = params.email;
1054
1157
  }
1158
+ if (params.countryCode !== undefined) {
1159
+ payload.country_code = params.countryCode;
1160
+ }
1161
+ if (params.postalCode !== undefined) {
1162
+ payload.postal_code = params.postalCode;
1163
+ }
1055
1164
  return (await this.request(API_ENDPOINTS.CREATE_PAYMENT, {
1056
1165
  method: 'POST',
1057
1166
  body: JSON.stringify(payload),
@@ -1356,6 +1465,34 @@ const renderError = (container, reqId) => {
1356
1465
  }
1357
1466
  };
1358
1467
 
1468
+ /**
1469
+ * @fileoverview Airwallex device fingerprinting script loader
1470
+ */
1471
+ /**
1472
+ * Loads Airwallex device fingerprinting script for fraud prevention.
1473
+ * The script collects browser, screen, device, and interaction data.
1474
+ *
1475
+ * @param sessionId - Unique order session ID (UUID v4 format, max 128 chars)
1476
+ * @param isDemoMode - If true, uses demo environment URL for testing
1477
+ * @returns Promise that resolves when script is loaded
1478
+ *
1479
+ * @see https://www.airwallex.com/docs/payments/online-payments/native-api/device-fingerprinting
1480
+ */
1481
+ async function loadAirwallexDeviceFingerprint(sessionId, isLivemode = true) {
1482
+ const scriptId = 'airwallex-fraud-api';
1483
+ const src = isLivemode
1484
+ ? 'https://static.airwallex.com/webapp/fraud/device-fingerprint/index.js'
1485
+ : 'https://static-demo.airwallex.com/webapp/fraud/device-fingerprint/index.js';
1486
+ await loadScript$1({
1487
+ id: scriptId,
1488
+ src,
1489
+ async: true,
1490
+ attributes: {
1491
+ 'data-order-session-id': sessionId,
1492
+ },
1493
+ });
1494
+ }
1495
+
1359
1496
  /**
1360
1497
  * @fileoverview Checkout instance manager for Funnefox SDK
1361
1498
  */
@@ -1364,6 +1501,8 @@ class CheckoutInstance extends EventEmitter {
1364
1501
  super();
1365
1502
  this.counter = 0;
1366
1503
  this.radarSessionId = null;
1504
+ this.airwallexDeviceId = null;
1505
+ this.cardSessionFieldConfig = {};
1367
1506
  this.handleInputChange = (inputName, error) => {
1368
1507
  this.emit(EVENTS.INPUT_ERROR, { name: inputName, error });
1369
1508
  };
@@ -1381,6 +1520,17 @@ class CheckoutInstance extends EventEmitter {
1381
1520
  this.handleCardInputValueChange = (inputName, value) => {
1382
1521
  if (inputName === 'emailAddress') {
1383
1522
  this.cardEmailAddress = value?.trim() || undefined;
1523
+ return;
1524
+ }
1525
+ if (inputName === 'countryCode') {
1526
+ this.cardCountryCode = this.normalizeCountryCode(value);
1527
+ if (!this.isPostalCodeVisible()) {
1528
+ this.cardPostalCode = undefined;
1529
+ }
1530
+ return;
1531
+ }
1532
+ if (inputName === 'postalCode') {
1533
+ this.cardPostalCode = value?.trim() || undefined;
1384
1534
  }
1385
1535
  };
1386
1536
  this.handleMethodRender = (method) => {
@@ -1401,13 +1551,19 @@ class CheckoutInstance extends EventEmitter {
1401
1551
  try {
1402
1552
  this.onLoaderChangeWithRace(true);
1403
1553
  this._setState('processing');
1404
- const radarSessionId = await this.radarSessionId;
1554
+ const [radarSessionId, airwallexDeviceId] = await Promise.all([
1555
+ this.radarSessionId,
1556
+ this.airwallexDeviceId,
1557
+ ]);
1405
1558
  const paymentResponse = await this.apiClient.createPayment({
1406
1559
  orderId: this.orderId,
1407
1560
  paymentMethodToken: paymentMethodTokenData.token,
1408
1561
  email: this.getPaymentEmailAddress(),
1562
+ countryCode: this.getPaymentCountryCode(),
1563
+ postalCode: this.getPaymentPostalCode(),
1409
1564
  clientMetadata: {
1410
1565
  radarSessionId,
1566
+ airwallexDeviceId,
1411
1567
  },
1412
1568
  });
1413
1569
  const result = this.apiClient.processPaymentResponse(paymentResponse);
@@ -1478,6 +1634,8 @@ class CheckoutInstance extends EventEmitter {
1478
1634
  this.primerWrapper = new PrimerWrapper();
1479
1635
  this.isDestroyed = false;
1480
1636
  this.cardEmailAddress = this.checkoutConfig.customer.email;
1637
+ this.shouldApplySessionCardholderNameConfig =
1638
+ this.checkoutConfig.card?.cardholderName?.required === undefined;
1481
1639
  this._setupCallbackBridges();
1482
1640
  }
1483
1641
  _setupCallbackBridges() {
@@ -1517,7 +1675,7 @@ class CheckoutInstance extends EventEmitter {
1517
1675
  this.hideInitializingLoader();
1518
1676
  }
1519
1677
  }
1520
- async createSession(method) {
1678
+ async createSession() {
1521
1679
  this.apiClient = new APIClient({
1522
1680
  baseUrl: this.baseUrl || DEFAULTS.BASE_URL,
1523
1681
  orgId: this.orgId,
@@ -1532,14 +1690,11 @@ class CheckoutInstance extends EventEmitter {
1532
1690
  clientMetadata: this.checkoutConfig.clientMetadata,
1533
1691
  countryCode: this.checkoutConfig.customer.countryCode,
1534
1692
  };
1535
- this.sessionMethod = method;
1536
1693
  const cacheKey = [
1537
- //this.id,
1538
1694
  this.orgId,
1539
1695
  this.checkoutConfig.priceId,
1540
1696
  this.checkoutConfig.customer.externalId,
1541
1697
  this.checkoutConfig.customer.email,
1542
- //method || 'default',
1543
1698
  ].join('-');
1544
1699
  let sessionResponse;
1545
1700
  // Return cached response if payload hasn't changed
@@ -1559,6 +1714,17 @@ class CheckoutInstance extends EventEmitter {
1559
1714
  .catch(() => '');
1560
1715
  });
1561
1716
  }
1717
+ // Initialize Airwallex device fingerprinting if enabled by backend
1718
+ if (response.data?.airwallex_risk_enabled) {
1719
+ const isLivemode = response.data?.is_livemode;
1720
+ const deviceId = generateUUID();
1721
+ this.airwallexDeviceId = loadAirwallexDeviceFingerprint(deviceId, isLivemode)
1722
+ .then(() => deviceId)
1723
+ .catch(() => {
1724
+ // Silently fail - return deviceId anyway
1725
+ return deviceId;
1726
+ });
1727
+ }
1562
1728
  this.isCollectingApplePayEmail =
1563
1729
  !!response.data?.collect_apple_pay_email;
1564
1730
  this.applySessionCardFieldConfig(response);
@@ -1573,7 +1739,9 @@ class CheckoutInstance extends EventEmitter {
1573
1739
  this.clientToken = sessionData.clientToken;
1574
1740
  }
1575
1741
  applySessionCardFieldConfig(response) {
1576
- const cardConfig = this.checkoutConfig.card || {};
1742
+ const cardConfig = {
1743
+ ...(this.checkoutConfig.card || {}),
1744
+ };
1577
1745
  if (cardConfig.emailAddress?.visible === undefined &&
1578
1746
  response.data?.show_email_field !== undefined) {
1579
1747
  cardConfig.emailAddress = {
@@ -1581,19 +1749,41 @@ class CheckoutInstance extends EventEmitter {
1581
1749
  visible: response.data.show_email_field,
1582
1750
  };
1583
1751
  }
1584
- if (cardConfig.cardholderName?.required === undefined &&
1752
+ if (this.shouldApplySessionCardholderNameConfig &&
1585
1753
  response.data?.show_cardholder_name_field !== undefined) {
1586
1754
  cardConfig.cardholderName = {
1587
1755
  ...cardConfig.cardholderName,
1588
1756
  required: response.data.show_cardholder_name_field,
1589
1757
  };
1590
1758
  }
1759
+ const countryFieldOverrides = this.normalizeCountryFieldOverrides(response.data?.country_field_overrides);
1760
+ const detectedCountryCode = this.normalizeCountryCode(response.data?.detected_country_code) ||
1761
+ this.cardCountryCode;
1762
+ this.cardSessionFieldConfig = {
1763
+ ...this.cardSessionFieldConfig,
1764
+ showCountrySelector: response.data?.show_country_selector_field ??
1765
+ this.cardSessionFieldConfig.showCountrySelector,
1766
+ showPostalCode: response.data?.show_postal_code_field ??
1767
+ this.cardSessionFieldConfig.showPostalCode,
1768
+ detectedCountryCode: detectedCountryCode || this.cardSessionFieldConfig.detectedCountryCode,
1769
+ validCountries: response.data?.valid_countries ||
1770
+ this.cardSessionFieldConfig.validCountries,
1771
+ countryFieldOverrides: countryFieldOverrides ||
1772
+ this.cardSessionFieldConfig.countryFieldOverrides,
1773
+ };
1591
1774
  if (Object.keys(cardConfig).length > 0) {
1592
1775
  this.checkoutConfig.card = cardConfig;
1593
1776
  }
1777
+ this.cardCountryCode =
1778
+ this.cardSessionFieldConfig.detectedCountryCode || this.cardCountryCode;
1779
+ if (!this.isPostalCodeVisible()) {
1780
+ this.cardPostalCode = undefined;
1781
+ }
1594
1782
  }
1595
1783
  getPrimerCardConfig() {
1596
- const cardConfig = { ...(this.checkoutConfig.card || {}) };
1784
+ const cardConfig = {
1785
+ ...(this.checkoutConfig.card || {}),
1786
+ };
1597
1787
  delete cardConfig.emailAddress;
1598
1788
  return Object.keys(cardConfig).length
1599
1789
  ? cardConfig
@@ -1703,6 +1893,8 @@ class CheckoutInstance extends EventEmitter {
1703
1893
  onSubmit: this.handleSubmit,
1704
1894
  onInputChange: this.handleInputChange,
1705
1895
  onCardInputValueChange: this.handleCardInputValueChange,
1896
+ isCardholderNameRequired: () => this.isCardholderNameRequired(),
1897
+ isPostalCodeRequired: () => this.isPostalCodeVisible(),
1706
1898
  onMethodRender: this.handleMethodRender,
1707
1899
  onMethodsAvailable: this.handleMethodsAvailable,
1708
1900
  onMethodRenderError: this.handleMethodRenderError,
@@ -1861,12 +2053,58 @@ class CheckoutInstance extends EventEmitter {
1861
2053
  isProcessing() {
1862
2054
  return ['processing', 'action_required'].includes(this.state);
1863
2055
  }
2056
+ normalizeCountryCode(countryCode) {
2057
+ const normalized = countryCode?.trim().toUpperCase();
2058
+ return normalized || undefined;
2059
+ }
2060
+ normalizeCountryFieldOverrides(overrides) {
2061
+ if (!overrides) {
2062
+ return undefined;
2063
+ }
2064
+ return Object.entries(overrides).reduce((result, [countryCode, override]) => {
2065
+ const normalizedCountryCode = this.normalizeCountryCode(countryCode);
2066
+ if (normalizedCountryCode && override) {
2067
+ result[normalizedCountryCode] = override;
2068
+ }
2069
+ return result;
2070
+ }, {});
2071
+ }
2072
+ getSelectedCountryCode() {
2073
+ return (this.normalizeCountryCode(this.cardCountryCode) ||
2074
+ this.normalizeCountryCode(this.cardSessionFieldConfig.detectedCountryCode));
2075
+ }
2076
+ getCountryFieldOverride(countryCode = this.getSelectedCountryCode()) {
2077
+ if (!countryCode) {
2078
+ return undefined;
2079
+ }
2080
+ return this.cardSessionFieldConfig.countryFieldOverrides?.[countryCode];
2081
+ }
2082
+ isCardholderNameRequired() {
2083
+ return !!this.checkoutConfig.card?.cardholderName?.required;
2084
+ }
2085
+ isPostalCodeVisible(countryCode = this.getSelectedCountryCode()) {
2086
+ const defaultValue = !!this.cardSessionFieldConfig.showPostalCode;
2087
+ const overrideValue = this.getCountryFieldOverride(countryCode)?.show_postal_code;
2088
+ if (overrideValue === null || overrideValue === undefined) {
2089
+ return defaultValue;
2090
+ }
2091
+ return overrideValue;
2092
+ }
2093
+ getPaymentCountryCode() {
2094
+ return this.getSelectedCountryCode();
2095
+ }
2096
+ getPaymentPostalCode() {
2097
+ if (!this.isPostalCodeVisible()) {
2098
+ return undefined;
2099
+ }
2100
+ return this.cardPostalCode?.trim() || undefined;
2101
+ }
1864
2102
  // Creates containers to render hosted inputs with labels and error messages,
1865
2103
  // a card holder input with label and error, and a submit button.
1866
2104
  async getDefaultSkinCheckoutOptions() {
1867
2105
  const skinFactory = (await import('./chunk-index.es2.js'))
1868
2106
  .default;
1869
- const skin = await skinFactory(this.checkoutConfig);
2107
+ const skin = await skinFactory(this.checkoutConfig, this.cardSessionFieldConfig);
1870
2108
  this.on(EVENTS.INPUT_ERROR, skin.onInputError);
1871
2109
  this.on(EVENTS.STATUS_CHANGE, skin.onStatusChange);
1872
2110
  this.on(EVENTS.ERROR, (error) => skin.onError(error));
@@ -1882,7 +2120,7 @@ class CheckoutInstance extends EventEmitter {
1882
2120
  }
1883
2121
  async getCardDefaultSkinCheckoutOptions(node) {
1884
2122
  const CardSkin = (await import('./chunk-index.es3.js')).default;
1885
- const skin = new CardSkin(node, this.checkoutConfig);
2123
+ const skin = new CardSkin(node, this.checkoutConfig, this.cardSessionFieldConfig);
1886
2124
  skin.init();
1887
2125
  this.on(EVENTS.INPUT_ERROR, skin.onInputError);
1888
2126
  this.on(EVENTS.METHOD_RENDER, skin.onMethodRender);
@@ -1899,7 +2137,7 @@ class CheckoutInstance extends EventEmitter {
1899
2137
  async initMethod(method, element, callbacks) {
1900
2138
  this._ensureNotDestroyed();
1901
2139
  if (!this.isReady()) {
1902
- await this.createSession(method);
2140
+ await this.createSession();
1903
2141
  }
1904
2142
  if (callbacks.onRenderSuccess) {
1905
2143
  this.on(EVENTS.METHOD_RENDER, callbacks.onRenderSuccess);
@@ -1943,6 +2181,8 @@ class CheckoutInstance extends EventEmitter {
1943
2181
  onSubmit: this.handleSubmit,
1944
2182
  onInputChange: this.handleInputChange,
1945
2183
  onCardInputValueChange: this.handleCardInputValueChange,
2184
+ isCardholderNameRequired: () => this.isCardholderNameRequired(),
2185
+ isPostalCodeRequired: () => this.isPostalCodeVisible(),
1946
2186
  onMethodRender: this.handleMethodRender,
1947
2187
  onMethodRenderError: this.handleMethodRenderError,
1948
2188
  };
@@ -27,7 +27,7 @@ const paymentMethodTemplates = {
27
27
  [PaymentMethod.APPLE_PAY]: applePayTemplate,
28
28
  };
29
29
  class DefaultSkin {
30
- constructor(checkoutConfig) {
30
+ constructor(checkoutConfig, cardSessionFieldConfig) {
31
31
  this.onLoaderChange = (isLoading) => {
32
32
  document
33
33
  .querySelectorAll(`${this.containerSelector} .loader-container`)
@@ -116,6 +116,7 @@ class DefaultSkin {
116
116
  }
117
117
  this.containerEl = containerEl;
118
118
  this.checkoutConfig = checkoutConfig;
119
+ this.cardSessionFieldConfig = cardSessionFieldConfig;
119
120
  }
120
121
  initAccordion() {
121
122
  const paymentMethodCards = this.containerEl.querySelectorAll('.ff-payment-method-card');
@@ -164,7 +165,7 @@ class DefaultSkin {
164
165
  this.paymentMethodOrder.forEach(paymentMethod => {
165
166
  paymentMethodContainers.insertAdjacentHTML('beforeend', paymentMethodTemplates[paymentMethod]);
166
167
  });
167
- this.cardInstance = new CardSkin(document.querySelector('#cardForm'), this.checkoutConfig);
168
+ this.cardInstance = new CardSkin(document.querySelector('#cardForm'), this.checkoutConfig, this.cardSessionFieldConfig);
168
169
  this.cardInstance.init();
169
170
  this.wireCardInputs();
170
171
  }
@@ -195,8 +196,8 @@ class DefaultSkin {
195
196
  };
196
197
  }
197
198
  }
198
- const createDefaultSkin = async (checkoutConfig) => {
199
- const skin = new DefaultSkin(checkoutConfig);
199
+ const createDefaultSkin = async (checkoutConfig, cardSessionFieldConfig) => {
200
+ const skin = new DefaultSkin(checkoutConfig, cardSessionFieldConfig);
200
201
  await skin['init']();
201
202
  return skin;
202
203
  };