@funnelfox/billing 0.6.4-beta.0 → 0.6.4-beta.1

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