@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.
@@ -148,64 +148,46 @@ class NetworkError extends FunnefoxSDKError {
148
148
  }
149
149
 
150
150
  /**
151
- * @fileoverview Generic script and stylesheet loader utility to reduce bundle size
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
- * 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
165
+ * Injects a script tag into the document head
159
166
  */
160
- function loadScript$1(options) {
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 (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
- }
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
- if (async) {
182
- script.async = true;
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: ${src}`));
196
- const target = appendTo === 'head' ? document.head : document.body;
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
- * 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
188
+ * Injects a CSS link tag into the document head
206
189
  */
207
- function loadStylesheet(options) {
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 stylesheet: ${href}`));
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
- 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
- }),
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.0';
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
- return valid;
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('change', cardHolderOnChange);
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 [radarSessionId, airwallexDeviceId] = await Promise.all([
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 = container.querySelector(selectors.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 = merge(checkoutOptions, this.isCollectingApplePayEmail
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
- ...this.checkoutConfig,
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.getCheckoutOptions(this.isCollectingApplePayEmail
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
  };