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