@idonatedev/idonate-sdk 1.2.0-dev2 → 1.2.0-dev20

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.
Files changed (56) hide show
  1. package/dist/constants.js +11 -2
  2. package/dist/esm/constants.js +11 -2
  3. package/dist/esm/idonate-client.js +18 -8
  4. package/dist/esm/recaptcha.d.ts +1 -0
  5. package/dist/esm/recaptcha.js +26 -2
  6. package/dist/esm/shared.js +9 -1
  7. package/dist/esm/tokenize/CardConnectTokenizer.d.ts +8 -1
  8. package/dist/esm/tokenize/CardConnectTokenizer.js +353 -104
  9. package/dist/esm/tokenize/PayPalTokenizer.d.ts +6 -0
  10. package/dist/esm/tokenize/PayPalTokenizer.js +66 -16
  11. package/dist/esm/tokenize/SpreedlyTokenizer.d.ts +17 -1
  12. package/dist/esm/tokenize/SpreedlyTokenizer.js +270 -138
  13. package/dist/esm/tokenize/Tokenizer.d.ts +6 -2
  14. package/dist/esm/tokenize/Tokenizer.js +24 -8
  15. package/dist/esm/tokenize/gateway-utils.d.ts +2 -0
  16. package/dist/esm/tokenize/gateway-utils.js +27 -0
  17. package/dist/esm/tokenize/index.d.ts +3 -2
  18. package/dist/esm/tokenize/index.js +1 -0
  19. package/dist/esm/tokenize/spreedly-secure.js +6 -2
  20. package/dist/esm/tokenize/styles.d.ts +1 -1
  21. package/dist/esm/tokenize/styles.js +20 -1
  22. package/dist/esm/tokenize/tokenizer-constants.d.ts +1 -0
  23. package/dist/esm/tokenize/tokenizer-constants.js +1 -0
  24. package/dist/esm/tokenize/tokenizer-utils.d.ts +4 -1
  25. package/dist/esm/tokenize/tokenizer-utils.js +77 -4
  26. package/dist/esm/tokenize/types.d.ts +33 -8
  27. package/dist/esm/typeAdapters.js +6 -4
  28. package/dist/esm/types.d.ts +4 -10
  29. package/dist/idonate-client.js +18 -8
  30. package/dist/recaptcha.d.ts +1 -0
  31. package/dist/recaptcha.js +27 -2
  32. package/dist/shared.js +9 -1
  33. package/dist/tokenize/CardConnectTokenizer.d.ts +8 -1
  34. package/dist/tokenize/CardConnectTokenizer.js +351 -105
  35. package/dist/tokenize/PayPalTokenizer.d.ts +6 -0
  36. package/dist/tokenize/PayPalTokenizer.js +66 -16
  37. package/dist/tokenize/SpreedlyTokenizer.d.ts +17 -1
  38. package/dist/tokenize/SpreedlyTokenizer.js +267 -135
  39. package/dist/tokenize/Tokenizer.d.ts +6 -2
  40. package/dist/tokenize/Tokenizer.js +24 -8
  41. package/dist/tokenize/gateway-utils.d.ts +2 -0
  42. package/dist/tokenize/gateway-utils.js +30 -0
  43. package/dist/tokenize/index.d.ts +3 -2
  44. package/dist/tokenize/index.js +3 -1
  45. package/dist/tokenize/spreedly-secure.js +6 -2
  46. package/dist/tokenize/styles.d.ts +1 -1
  47. package/dist/tokenize/styles.js +20 -1
  48. package/dist/tokenize/tokenizer-constants.d.ts +1 -0
  49. package/dist/tokenize/tokenizer-constants.js +2 -1
  50. package/dist/tokenize/tokenizer-utils.d.ts +4 -1
  51. package/dist/tokenize/tokenizer-utils.js +80 -4
  52. package/dist/tokenize/types.d.ts +33 -8
  53. package/dist/typeAdapters.js +6 -4
  54. package/dist/types.d.ts +4 -10
  55. package/package.json +33 -2
  56. package/umd/idonate-sdk.js +1 -1
@@ -11,23 +11,27 @@ import { Tokenizer } from './Tokenizer';
11
11
  import { TokenizationError, } from './types';
12
12
  import { SPREEDLY_TOKENIZER_URL } from '../constants';
13
13
  import { extractSpreedlyToken, unpackSpreedlyResponse } from '../typeAdapters';
14
- import { fetchSpreedlySecurityArgs, } from './spreedly-secure';
14
+ import { fetchSpreedlySecurityArgs } from './spreedly-secure';
15
15
  import { DEFAULT_UNIFIED_STYLES, mergeStyles, getContainerStylesForLayout, } from './styles';
16
- import { INIT_TIMEOUT, TOKENIZE_TIMEOUT, FIELD_FLEX, BANK_FIELD_FLEX, CANADIAN_BANK_FIELD_FLEX, PLACEHOLDERS, FIELD_CONSTRAINTS, } from './tokenizer-constants';
17
- import { createFieldContainer, createInputElement, validateRoutingNumber, validateAccountNumber, createAccountTypeSelect, validateInstitutionNumber, validateTransitNumber, validateCanadianAccountNumber, formatCanadianRoutingNumber, } from './tokenizer-utils';
16
+ import { INIT_TIMEOUT, TOKENIZE_TIMEOUT, FIELD_FLEX, BANK_FIELD_FLEX, CANADIAN_BANK_FIELD_FLEX, PLACEHOLDERS, FIELD_CONSTRAINTS, RESPONSIVE_BREAKPOINT, } from './tokenizer-constants';
17
+ import { createFieldContainer, createInputElement, validateRoutingNumber, validateAccountNumber, createAccountTypeSelect, validateInstitutionNumber, validateTransitNumber, validateCanadianAccountNumber, formatCanadianRoutingNumber, wrapFieldWithLabel, } from './tokenizer-utils';
18
18
  const SPREEDLY_SCRIPT_URL = 'https://core.spreedly.com/iframe/iframe-v1.min.js';
19
19
  const SPREEDLY_SCRIPT_ID = 'spreedly-iframe-script';
20
20
  export class SpreedlyTokenizer extends Tokenizer {
21
- constructor(environmentKey, containerId, styles, mode = 'credit_card', bankCountry = 'US', enableTestMode = false, layout = 'single-line') {
21
+ constructor(environmentKey, containerId, styles, mode = 'credit_card', bankCountry = 'US', enableTestMode = false, layout = 'single-line', responsiveBreakpoint = RESPONSIVE_BREAKPOINT, customPlaceholders) {
22
22
  super();
23
23
  this.environmentKey = environmentKey;
24
24
  this.containerId = containerId;
25
25
  this.layout = layout;
26
+ this.customPlaceholders = customPlaceholders;
26
27
  this.bankCountry = 'US';
27
28
  this.enableTestMode = false;
29
+ this.effectiveLayout = 'single-line';
30
+ this.responsiveBreakpoint = RESPONSIVE_BREAKPOINT;
28
31
  this.mode = mode;
29
32
  this.bankCountry = bankCountry;
30
33
  this.enableTestMode = enableTestMode;
34
+ this.responsiveBreakpoint = responsiveBreakpoint;
31
35
  if (mode === 'credit_card') {
32
36
  const SpreedlyFrame = window.SpreedlyPaymentFrame;
33
37
  if (SpreedlyFrame) {
@@ -63,40 +67,33 @@ export class SpreedlyTokenizer extends Tokenizer {
63
67
  }
64
68
  static create(gateway, container, config) {
65
69
  return __awaiter(this, void 0, void 0, function* () {
66
- var _a;
70
+ var _a, _b;
67
71
  if (container.mode !== 'bank_account') {
68
72
  yield SpreedlyTokenizer.ensureSpreedlyLoaded();
69
73
  }
70
- let environmentKey = (_a = gateway.config) === null || _a === void 0 ? void 0 : _a.environment_key;
74
+ let environmentKey = (_a = gateway.config) === null || _a === void 0 ? void 0 : _a.environmentKey;
71
75
  if (!environmentKey && config.clientConfig.spreedlyEnvironmentKey) {
72
76
  environmentKey = config.clientConfig.spreedlyEnvironmentKey;
73
77
  }
74
78
  if (!environmentKey) {
75
- environmentKey = '';
79
+ throw new Error('Spreedly environment key is required. Provide it via gateway.config.environmentKey or client config.');
76
80
  }
77
- const tokenizer = new SpreedlyTokenizer(environmentKey, container.containerId, container.styling, container.mode || 'credit_card', container.bankCountry || 'US', container.enableTestMode || false, container.layout || 'single-line');
81
+ const tokenizer = new SpreedlyTokenizer(environmentKey, container.containerId, container.styling, container.mode || 'credit_card', container.bankCountry || 'US', container.enableTestMode || false, container.layout || 'single-line', (_b = container.responsiveBreakpoint) !== null && _b !== void 0 ? _b : RESPONSIVE_BREAKPOINT, container.placeholders);
78
82
  tokenizer.organizationId = config.organizationId;
79
83
  tokenizer.embedId = config.embedId;
80
84
  tokenizer.clientConfig = config.clientConfig;
81
85
  tokenizer.createInternalElements();
82
- let securityArgs;
83
86
  if (container.mode === 'credit_card' &&
84
87
  config.clientConfig.enableSpreedlySecureTokenization) {
85
88
  if (!config.organizationId || !config.embedId) {
86
89
  throw new Error('Secure tokenization is enabled but organizationId and embedId are required');
87
90
  }
88
- try {
89
- securityArgs = yield fetchSpreedlySecurityArgs(config.clientConfig, config.organizationId, config.embedId);
90
- }
91
- catch (error) {
92
- throw new Error(`Secure tokenization is enabled but failed to initialize: ${error instanceof Error ? error.message : 'Unknown error'}`);
93
- }
94
91
  }
95
- yield tokenizer.init(securityArgs);
92
+ yield tokenizer.init();
96
93
  return tokenizer;
97
94
  });
98
95
  }
99
- init(securityArgs) {
96
+ init() {
100
97
  return __awaiter(this, void 0, void 0, function* () {
101
98
  this.containerEl = document.getElementById(this.containerId);
102
99
  if (this.mode === 'bank_account') {
@@ -140,11 +137,12 @@ export class SpreedlyTokenizer extends Tokenizer {
140
137
  reject(new Error('Spreedly initialization timeout'));
141
138
  }, INIT_TIMEOUT);
142
139
  this.spreedly.on('ready', () => {
140
+ var _a, _b;
143
141
  clearTimeout(timeout);
144
- this.spreedly.setPlaceholder('number', PLACEHOLDERS.cardNumber);
142
+ this.spreedly.setPlaceholder('number', ((_a = this.customPlaceholders) === null || _a === void 0 ? void 0 : _a.cardNumber) || PLACEHOLDERS.cardNumber);
145
143
  this.spreedly.setFieldType('number', 'text');
146
144
  this.spreedly.setNumberFormat('prettyFormat');
147
- this.spreedly.setPlaceholder('cvv', PLACEHOLDERS.cvv);
145
+ this.spreedly.setPlaceholder('cvv', ((_b = this.customPlaceholders) === null || _b === void 0 ? void 0 : _b.cvv) || PLACEHOLDERS.cvv);
148
146
  this.spreedly.setFieldType('cvv', 'text');
149
147
  this.applyUnifiedStyles();
150
148
  if (this.enableTestMode &&
@@ -162,23 +160,37 @@ export class SpreedlyTokenizer extends Tokenizer {
162
160
  this.emit('ready');
163
161
  resolve();
164
162
  });
163
+ this.spreedly.on('paymentMethod', (token, pmData) => {
164
+ const paymentToken = {
165
+ token,
166
+ lastFour: pmData.last_four_digits,
167
+ cardType: this.normalizeCardType(pmData.card_type),
168
+ paymentMethodType: 'credit_card',
169
+ provider: 'spreedly',
170
+ };
171
+ if (this.tokenizationResolve) {
172
+ const resolveTokenization = this.tokenizationResolve;
173
+ this.clearTokenizationState();
174
+ this.emit('tokenReady', paymentToken);
175
+ resolveTokenization(paymentToken);
176
+ }
177
+ });
165
178
  this.spreedly.on('errors', (errors) => {
166
- this.emit('error', new TokenizationError(errors));
179
+ const error = new TokenizationError(errors);
180
+ this.emit('error', error);
181
+ if (this.tokenizationReject) {
182
+ const rejectTokenization = this.tokenizationReject;
183
+ this.clearTokenizationState();
184
+ rejectTokenization(error);
185
+ }
167
186
  });
168
187
  this.spreedly.on('fieldEvent', (fieldName, eventType, activeEl, inputProperties) => {
169
188
  this.handleFieldEvent(fieldName, eventType, inputProperties);
170
189
  });
171
- const initOptions = {
190
+ this.spreedly.init(this.environmentKey, {
172
191
  numberEl: this.numberEl,
173
192
  cvvEl: this.cvvEl,
174
- };
175
- if (securityArgs) {
176
- initOptions.certificateToken = securityArgs.certificate_token;
177
- initOptions.signature = securityArgs.signature;
178
- initOptions.timestamp = securityArgs.timestamp;
179
- initOptions.nonce = securityArgs.nonce;
180
- }
181
- this.spreedly.init(this.environmentKey, initOptions);
193
+ });
182
194
  });
183
195
  });
184
196
  }
@@ -196,58 +208,45 @@ export class SpreedlyTokenizer extends Tokenizer {
196
208
  });
197
209
  }
198
210
  tokenizeCardInternal(cardData) {
211
+ if (this.tokenizationPromise) {
212
+ return this.tokenizationPromise;
213
+ }
214
+ const expiry = this.parseExpiry(this.expiryEl);
215
+ if (!expiry) {
216
+ return Promise.reject(new TokenizationError('Expiration date is required', 'VALIDATION_ERROR'));
217
+ }
218
+ this.tokenizationPromise = this.executeCardTokenization(cardData, expiry).finally(() => {
219
+ this.tokenizationPromise = undefined;
220
+ });
221
+ return this.tokenizationPromise;
222
+ }
223
+ executeCardTokenization(cardData, expiry) {
199
224
  return __awaiter(this, void 0, void 0, function* () {
225
+ var _a;
226
+ let securityFields = {};
227
+ if ((_a = this.clientConfig) === null || _a === void 0 ? void 0 : _a.enableSpreedlySecureTokenization) {
228
+ try {
229
+ const args = yield fetchSpreedlySecurityArgs(this.clientConfig, this.organizationId, this.embedId);
230
+ securityFields = {
231
+ nonce: args.nonce,
232
+ timestamp: args.timestamp,
233
+ certificateToken: args.certificate_token,
234
+ signature: args.signature,
235
+ };
236
+ }
237
+ catch (error) {
238
+ throw new TokenizationError(`Secure tokenization failed: ${error instanceof Error ? error.message : 'Unknown error'}`, 'SECURE_TOKENIZATION_ERROR');
239
+ }
240
+ }
200
241
  return new Promise((resolve, reject) => {
201
242
  var _a, _b, _c, _d, _e, _f;
202
- const timeout = setTimeout(() => {
203
- if (this.spreedly && this.spreedly.removeHandlers) {
204
- this.spreedly.removeHandlers();
205
- }
243
+ this.tokenizationResolve = resolve;
244
+ this.tokenizationReject = reject;
245
+ this.tokenizationTimeout = setTimeout(() => {
246
+ this.clearTokenizationState();
206
247
  reject(new TokenizationError('Tokenization timeout', 'TIMEOUT'));
207
248
  }, TOKENIZE_TIMEOUT);
208
- const cleanup = () => {
209
- clearTimeout(timeout);
210
- if (this.spreedly && this.spreedly.removeHandlers) {
211
- this.spreedly.removeHandlers();
212
- }
213
- };
214
- this.spreedly.on('paymentMethod', (token, pmData) => {
215
- cleanup();
216
- const paymentToken = {
217
- token,
218
- lastFour: pmData.last_four_digits,
219
- cardType: this.normalizeCardType(pmData.card_type),
220
- provider: 'spreedly',
221
- };
222
- this.emit('tokenReady', paymentToken);
223
- resolve(paymentToken);
224
- });
225
- this.spreedly.on('errors', (errors) => {
226
- cleanup();
227
- reject(new TokenizationError(errors));
228
- });
229
- const expiry = this.parseExpiry(this.expiryEl);
230
- if (!expiry) {
231
- cleanup();
232
- reject(new TokenizationError('Expiration date is required', 'VALIDATION_ERROR'));
233
- return;
234
- }
235
- const expiryMonth = expiry.month;
236
- const expiryYear = expiry.year;
237
- this.spreedly.tokenizeCreditCard({
238
- first_name: cardData.firstName,
239
- last_name: cardData.lastName,
240
- month: expiryMonth,
241
- year: expiryYear,
242
- email: cardData.email,
243
- phone_number: cardData.phone,
244
- address1: (_a = cardData.address) === null || _a === void 0 ? void 0 : _a.address1,
245
- address2: (_b = cardData.address) === null || _b === void 0 ? void 0 : _b.address2,
246
- city: (_c = cardData.address) === null || _c === void 0 ? void 0 : _c.city,
247
- state: (_d = cardData.address) === null || _d === void 0 ? void 0 : _d.state,
248
- zip: (_e = cardData.address) === null || _e === void 0 ? void 0 : _e.zip,
249
- country: (_f = cardData.address) === null || _f === void 0 ? void 0 : _f.country,
250
- });
249
+ this.spreedly.tokenizeCreditCard(Object.assign({ first_name: cardData.firstName, last_name: cardData.lastName, month: expiry.month, year: expiry.year, email: cardData.email, phone_number: cardData.phone, address1: (_a = cardData.address) === null || _a === void 0 ? void 0 : _a.address1, address2: (_b = cardData.address) === null || _b === void 0 ? void 0 : _b.address2, city: (_c = cardData.address) === null || _c === void 0 ? void 0 : _c.city, state: (_d = cardData.address) === null || _d === void 0 ? void 0 : _d.state, zip: (_e = cardData.address) === null || _e === void 0 ? void 0 : _e.zip, country: (_f = cardData.address) === null || _f === void 0 ? void 0 : _f.country }, securityFields));
251
250
  });
252
251
  });
253
252
  }
@@ -259,7 +258,7 @@ export class SpreedlyTokenizer extends Tokenizer {
259
258
  minWidth: CANADIAN_BANK_FIELD_FLEX.accountType.minWidth,
260
259
  });
261
260
  this.applyInputStyles(this.accountTypeEl, this.mergedStyles, 'left');
262
- container.appendChild(this.accountTypeEl);
261
+ this.appendField(container, this.accountTypeEl, 'Account Type');
263
262
  this.institutionNumberEl = createInputElement(`${this.containerId}-institution`, 'text', 'Inst *', 3);
264
263
  Object.assign(this.institutionNumberEl.style, {
265
264
  flex: '1',
@@ -276,7 +275,7 @@ export class SpreedlyTokenizer extends Tokenizer {
276
275
  }
277
276
  this.updateBankAccountValidation();
278
277
  });
279
- container.appendChild(this.institutionNumberEl);
278
+ this.appendField(container, this.institutionNumberEl, 'Institution');
280
279
  this.transitNumberEl = createInputElement(`${this.containerId}-transit`, 'text', 'Transit *', 5);
281
280
  Object.assign(this.transitNumberEl.style, {
282
281
  flex: '2',
@@ -293,7 +292,7 @@ export class SpreedlyTokenizer extends Tokenizer {
293
292
  }
294
293
  this.updateBankAccountValidation();
295
294
  });
296
- container.appendChild(this.transitNumberEl);
295
+ this.appendField(container, this.transitNumberEl, 'Transit');
297
296
  this.accountNumberEl = createInputElement(`${this.containerId}-account`, 'text', 'Account Number *');
298
297
  Object.assign(this.accountNumberEl.style, {
299
298
  flex: '1',
@@ -311,7 +310,7 @@ export class SpreedlyTokenizer extends Tokenizer {
311
310
  }
312
311
  this.updateBankAccountValidation();
313
312
  });
314
- container.appendChild(this.accountNumberEl);
313
+ this.appendField(container, this.accountNumberEl, 'Account Number');
315
314
  }
316
315
  else {
317
316
  this.institutionNumberEl = createInputElement(`${this.containerId}-institution`, 'text', 'Inst *', 3);
@@ -330,7 +329,7 @@ export class SpreedlyTokenizer extends Tokenizer {
330
329
  }
331
330
  this.updateBankAccountValidation();
332
331
  });
333
- container.appendChild(this.institutionNumberEl);
332
+ this.appendField(container, this.institutionNumberEl, 'Institution');
334
333
  this.transitNumberEl = createInputElement(`${this.containerId}-transit`, 'text', 'Transit *', 5);
335
334
  Object.assign(this.transitNumberEl.style, {
336
335
  flex: CANADIAN_BANK_FIELD_FLEX.transitNumber.flex,
@@ -347,7 +346,7 @@ export class SpreedlyTokenizer extends Tokenizer {
347
346
  }
348
347
  this.updateBankAccountValidation();
349
348
  });
350
- container.appendChild(this.transitNumberEl);
349
+ this.appendField(container, this.transitNumberEl, 'Transit');
351
350
  this.accountNumberEl = createInputElement(`${this.containerId}-account`, 'text', 'Account Number *');
352
351
  Object.assign(this.accountNumberEl.style, {
353
352
  flex: CANADIAN_BANK_FIELD_FLEX.accountNumber.flex,
@@ -364,14 +363,14 @@ export class SpreedlyTokenizer extends Tokenizer {
364
363
  }
365
364
  this.updateBankAccountValidation();
366
365
  });
367
- container.appendChild(this.accountNumberEl);
366
+ this.appendField(container, this.accountNumberEl, 'Account Number');
368
367
  this.accountTypeEl = createAccountTypeSelect(`${this.containerId}-account-type`);
369
368
  Object.assign(this.accountTypeEl.style, {
370
369
  flex: CANADIAN_BANK_FIELD_FLEX.accountType.flex,
371
370
  minWidth: CANADIAN_BANK_FIELD_FLEX.accountType.minWidth,
372
371
  });
373
372
  this.applyInputStyles(this.accountTypeEl, this.mergedStyles, 'right');
374
- container.appendChild(this.accountTypeEl);
373
+ this.appendField(container, this.accountTypeEl, 'Account Type');
375
374
  }
376
375
  }
377
376
  tokenizeBankAccountInternal(bankData) {
@@ -439,18 +438,23 @@ export class SpreedlyTokenizer extends Tokenizer {
439
438
  accountType: accountType,
440
439
  accountHolderType: accountHolderType,
441
440
  },
442
- }, this.clientConfig);
443
- return {
441
+ }, this.clientConfig, this.environmentKey);
442
+ const paymentToken = {
444
443
  token: result,
445
444
  lastFour: accountNumber.slice(-4),
446
445
  accountType: accountType,
447
446
  paymentMethodType: 'bank_account',
448
447
  provider: 'spreedly',
449
448
  };
449
+ this.emit('tokenReady', paymentToken);
450
+ return paymentToken;
450
451
  });
451
452
  }
452
453
  validate() {
453
454
  return __awaiter(this, void 0, void 0, function* () {
455
+ if (!this.isReady) {
456
+ throw new Error('Tokenizer not initialized');
457
+ }
454
458
  if (this.mode === 'bank_account') {
455
459
  return this.validateBankAccountInternal();
456
460
  }
@@ -585,7 +589,7 @@ export class SpreedlyTokenizer extends Tokenizer {
585
589
  const isConnected = ((_a = this.mergedStyles.container) === null || _a === void 0 ? void 0 : _a.gap) === '0';
586
590
  if (this.mergedStyles.input) {
587
591
  if (isConnected) {
588
- const numberStyles = Object.assign(Object.assign({}, this.mergedStyles.input), { borderRight: 'none', borderRadius: this.mergedStyles.input.borderRadius
592
+ const numberStyles = Object.assign(Object.assign({}, this.mergedStyles.input), { borderRight: '1px solid transparent', borderRadius: this.mergedStyles.input.borderRadius
589
593
  ? `${this.mergedStyles.input.borderRadius} 0 0 ${this.mergedStyles.input.borderRadius}`
590
594
  : '0' });
591
595
  const numberCss = this.stylesToCssString(numberStyles);
@@ -608,7 +612,11 @@ export class SpreedlyTokenizer extends Tokenizer {
608
612
  this.spreedly.setStyle('cvv:focus', focusCss);
609
613
  }
610
614
  if (this.mergedStyles.error) {
611
- const errorCss = this.stylesToCssString(this.mergedStyles.error);
615
+ const errorStyles = Object.assign({}, this.mergedStyles.error);
616
+ if (errorStyles.borderColor) {
617
+ errorStyles.borderRightColor = errorStyles.borderColor;
618
+ }
619
+ const errorCss = this.stylesToCssString(errorStyles);
612
620
  this.spreedly.setStyle('number.invalid', errorCss);
613
621
  this.spreedly.setStyle('cvv.invalid', errorCss);
614
622
  }
@@ -638,13 +646,33 @@ export class SpreedlyTokenizer extends Tokenizer {
638
646
  this.accountNumberEl.value = '';
639
647
  if (this.accountTypeEl)
640
648
  this.accountTypeEl.selectedIndex = 0;
649
+ this.currentValidationState = {
650
+ isValid: false,
651
+ routingNumber: { isValid: false, isEmpty: true },
652
+ accountNumber: { isValid: false, isEmpty: true },
653
+ };
641
654
  }
642
655
  else {
643
656
  this.spreedly.reload();
644
657
  if (this.expiryEl) {
645
658
  this.expiryEl.value = '';
646
659
  }
660
+ this.currentValidationState = {
661
+ isValid: false,
662
+ cardNumber: { isValid: false, isEmpty: true },
663
+ cvv: { isValid: false, isEmpty: true },
664
+ expiry: { isValid: false, isEmpty: true },
665
+ };
647
666
  }
667
+ if (this.tokenizationReject) {
668
+ const reject = this.tokenizationReject;
669
+ this.clearTokenizationState();
670
+ reject(new TokenizationError('Tokenizer cleared', 'CLEARED'));
671
+ }
672
+ else {
673
+ this.clearTokenizationState();
674
+ }
675
+ this.emit('validation', this.currentValidationState);
648
676
  }
649
677
  focus(field) {
650
678
  if (this.mode === 'bank_account') {
@@ -680,12 +708,84 @@ export class SpreedlyTokenizer extends Tokenizer {
680
708
  element === null || element === void 0 ? void 0 : element.focus();
681
709
  }
682
710
  }
711
+ determineLayout(width) {
712
+ return width < this.responsiveBreakpoint ? 'two-line' : 'single-line';
713
+ }
714
+ setupResizeObserver() {
715
+ if (this.layout !== 'responsive')
716
+ return;
717
+ const container = document.getElementById(this.containerId);
718
+ if (!container)
719
+ return;
720
+ let debounceTimer;
721
+ this.resizeObserver = new ResizeObserver((entries) => {
722
+ clearTimeout(debounceTimer);
723
+ debounceTimer = setTimeout(() => {
724
+ const newWidth = entries[0].contentRect.width;
725
+ const newLayout = this.determineLayout(newWidth);
726
+ if (newLayout !== this.effectiveLayout) {
727
+ this.applyLayoutStyles(newLayout);
728
+ }
729
+ }, 100);
730
+ });
731
+ this.resizeObserver.observe(container);
732
+ }
733
+ applyLayoutStyles(newLayout) {
734
+ this.effectiveLayout = newLayout;
735
+ const container = document.getElementById(this.containerId);
736
+ if (!container)
737
+ return;
738
+ if (newLayout === 'two-line') {
739
+ container.style.flexWrap = 'wrap';
740
+ if (this.cardNumberDiv) {
741
+ this.cardNumberDiv.style.flex = '1';
742
+ this.cardNumberDiv.style.flexBasis = '100%';
743
+ }
744
+ if (this.expiryEl) {
745
+ this.expiryEl.style.flex = '1';
746
+ this.expiryEl.style.flexBasis = 'auto';
747
+ }
748
+ if (this.cvvDiv) {
749
+ this.cvvDiv.style.flex = '1';
750
+ this.cvvDiv.style.flexBasis = 'auto';
751
+ }
752
+ }
753
+ else {
754
+ container.style.flexWrap = 'nowrap';
755
+ if (this.cardNumberDiv) {
756
+ this.cardNumberDiv.style.flex = FIELD_FLEX.cardNumber.flex;
757
+ }
758
+ if (this.expiryEl) {
759
+ this.expiryEl.style.flex = FIELD_FLEX.expiry.flex;
760
+ }
761
+ if (this.cvvDiv) {
762
+ this.cvvDiv.style.flex = FIELD_FLEX.cvv.flex;
763
+ }
764
+ }
765
+ }
766
+ clearTokenizationState() {
767
+ if (this.tokenizationTimeout) {
768
+ clearTimeout(this.tokenizationTimeout);
769
+ this.tokenizationTimeout = undefined;
770
+ }
771
+ this.tokenizationResolve = undefined;
772
+ this.tokenizationReject = undefined;
773
+ }
683
774
  destroy() {
775
+ var _a;
776
+ this.markDestroyed();
777
+ (_a = this.resizeObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
778
+ if (this.tokenizationReject) {
779
+ const reject = this.tokenizationReject;
780
+ this.clearTokenizationState();
781
+ reject(new TokenizationError('Tokenizer destroyed', 'DESTROYED'));
782
+ }
684
783
  if (this.spreedly && this.spreedly.removeHandlers) {
685
784
  this.spreedly.removeHandlers();
686
785
  }
687
786
  this.eventHandlers.clear();
688
- if (this.expiryEl) {
787
+ if (this.containerEl) {
788
+ this.containerEl.innerHTML = '';
689
789
  }
690
790
  }
691
791
  hasToken() {
@@ -732,15 +832,6 @@ export class SpreedlyTokenizer extends Tokenizer {
732
832
  }
733
833
  this.updateOverallValidation();
734
834
  }
735
- if (eventType === 'focus') {
736
- this.emit('focus', { field: fieldName });
737
- }
738
- else if (eventType === 'blur') {
739
- this.emit('blur', { field: fieldName });
740
- }
741
- else if (eventType === 'input') {
742
- this.emit('change', { field: fieldName });
743
- }
744
835
  }
745
836
  createInternalElements() {
746
837
  const container = document.getElementById(this.containerId);
@@ -756,13 +847,25 @@ export class SpreedlyTokenizer extends Tokenizer {
756
847
  this.createCreditCardFields(container);
757
848
  }
758
849
  }
850
+ appendField(container, field, labelText) {
851
+ container.appendChild(wrapFieldWithLabel(field, labelText, this.mergedStyles));
852
+ }
759
853
  createCreditCardFields(container) {
760
- if (this.layout === 'two-line') {
854
+ var _a, _b;
855
+ if (this.layout === 'responsive') {
856
+ this.effectiveLayout = this.determineLayout(container.offsetWidth);
857
+ }
858
+ else {
859
+ this.effectiveLayout =
860
+ this.layout === 'two-line' ? 'two-line' : 'single-line';
861
+ }
862
+ if (this.effectiveLayout === 'two-line' && this.layout !== 'responsive') {
761
863
  const cardNumberDiv = createFieldContainer(this.numberEl, '1', FIELD_FLEX.cardNumber.minWidth);
762
864
  cardNumberDiv.style.height = this.mergedStyles.input.height;
763
865
  cardNumberDiv.style.flexBasis = '100%';
764
- container.appendChild(cardNumberDiv);
765
- this.expiryEl = createInputElement(this.expiryId, 'text', PLACEHOLDERS.expiry, FIELD_CONSTRAINTS.expiry.maxLength);
866
+ this.cardNumberDiv = cardNumberDiv;
867
+ this.appendField(container, cardNumberDiv, 'Card Number');
868
+ this.expiryEl = createInputElement(this.expiryId, 'text', ((_a = this.customPlaceholders) === null || _a === void 0 ? void 0 : _a.expiry) || PLACEHOLDERS.expiry, FIELD_CONSTRAINTS.expiry.maxLength);
766
869
  Object.assign(this.expiryEl.style, {
767
870
  flex: '1',
768
871
  minWidth: FIELD_FLEX.expiry.minWidth,
@@ -778,16 +881,22 @@ export class SpreedlyTokenizer extends Tokenizer {
778
881
  };
779
882
  this.updateOverallValidation();
780
883
  });
781
- container.appendChild(this.expiryEl);
884
+ this.expiryEl.addEventListener('blur', () => {
885
+ this.validateExpiry();
886
+ this.updateOverallValidation();
887
+ });
888
+ this.appendField(container, this.expiryEl, 'Expiration');
782
889
  const cvvDiv = createFieldContainer(this.cvvEl, '1', FIELD_FLEX.cvv.minWidth);
783
890
  cvvDiv.style.height = this.mergedStyles.input.height;
784
- container.appendChild(cvvDiv);
891
+ this.cvvDiv = cvvDiv;
892
+ this.appendField(container, cvvDiv, 'CVV');
785
893
  }
786
894
  else {
787
895
  const cardNumberDiv = createFieldContainer(this.numberEl, FIELD_FLEX.cardNumber.flex, FIELD_FLEX.cardNumber.minWidth);
788
896
  cardNumberDiv.style.height = this.mergedStyles.input.height;
789
- container.appendChild(cardNumberDiv);
790
- this.expiryEl = createInputElement(this.expiryId, 'text', PLACEHOLDERS.expiry, FIELD_CONSTRAINTS.expiry.maxLength);
897
+ this.cardNumberDiv = cardNumberDiv;
898
+ this.appendField(container, cardNumberDiv, 'Card Number');
899
+ this.expiryEl = createInputElement(this.expiryId, 'text', ((_b = this.customPlaceholders) === null || _b === void 0 ? void 0 : _b.expiry) || PLACEHOLDERS.expiry, FIELD_CONSTRAINTS.expiry.maxLength);
791
900
  Object.assign(this.expiryEl.style, {
792
901
  flex: FIELD_FLEX.expiry.flex,
793
902
  minWidth: FIELD_FLEX.expiry.minWidth,
@@ -803,10 +912,19 @@ export class SpreedlyTokenizer extends Tokenizer {
803
912
  };
804
913
  this.updateOverallValidation();
805
914
  });
806
- container.appendChild(this.expiryEl);
915
+ this.expiryEl.addEventListener('blur', () => {
916
+ this.validateExpiry();
917
+ this.updateOverallValidation();
918
+ });
919
+ this.appendField(container, this.expiryEl, 'Expiration');
807
920
  const cvvDiv = createFieldContainer(this.cvvEl, FIELD_FLEX.cvv.flex, FIELD_FLEX.cvv.minWidth);
808
921
  cvvDiv.style.height = this.mergedStyles.input.height;
809
- container.appendChild(cvvDiv);
922
+ this.cvvDiv = cvvDiv;
923
+ this.appendField(container, cvvDiv, 'CVV');
924
+ if (this.layout === 'responsive') {
925
+ this.applyLayoutStyles(this.effectiveLayout);
926
+ this.setupResizeObserver();
927
+ }
810
928
  }
811
929
  }
812
930
  createBankAccountFields(container) {
@@ -818,6 +936,7 @@ export class SpreedlyTokenizer extends Tokenizer {
818
936
  }
819
937
  }
820
938
  createUSBankAccountFields(container) {
939
+ var _a, _b, _c, _d;
821
940
  if (this.layout === 'two-line') {
822
941
  this.accountTypeEl = createAccountTypeSelect(`${this.containerId}-account-type`);
823
942
  Object.assign(this.accountTypeEl.style, {
@@ -825,8 +944,8 @@ export class SpreedlyTokenizer extends Tokenizer {
825
944
  minWidth: BANK_FIELD_FLEX.accountType.minWidth,
826
945
  });
827
946
  this.applyInputStyles(this.accountTypeEl, this.mergedStyles, 'left');
828
- container.appendChild(this.accountTypeEl);
829
- this.routingNumberEl = createInputElement(`${this.containerId}-routing`, 'text', 'Routing *', 9);
947
+ this.appendField(container, this.accountTypeEl, 'Account Type');
948
+ this.routingNumberEl = createInputElement(`${this.containerId}-routing`, 'text', ((_a = this.customPlaceholders) === null || _a === void 0 ? void 0 : _a.routingNumber) || 'Routing *', 9);
830
949
  Object.assign(this.routingNumberEl.style, {
831
950
  flex: '1',
832
951
  minWidth: BANK_FIELD_FLEX.routingNumber.minWidth,
@@ -842,8 +961,8 @@ export class SpreedlyTokenizer extends Tokenizer {
842
961
  }
843
962
  this.updateBankAccountValidation();
844
963
  });
845
- container.appendChild(this.routingNumberEl);
846
- this.accountNumberEl = createInputElement(`${this.containerId}-account`, 'text', 'Account Number *', 17);
964
+ this.appendField(container, this.routingNumberEl, 'Routing Number');
965
+ this.accountNumberEl = createInputElement(`${this.containerId}-account`, 'text', ((_b = this.customPlaceholders) === null || _b === void 0 ? void 0 : _b.accountNumber) || 'Account Number *', 17);
847
966
  Object.assign(this.accountNumberEl.style, {
848
967
  flex: '1',
849
968
  flexBasis: '100%',
@@ -860,10 +979,10 @@ export class SpreedlyTokenizer extends Tokenizer {
860
979
  }
861
980
  this.updateBankAccountValidation();
862
981
  });
863
- container.appendChild(this.accountNumberEl);
982
+ this.appendField(container, this.accountNumberEl, 'Account Number');
864
983
  }
865
984
  else {
866
- this.routingNumberEl = createInputElement(`${this.containerId}-routing`, 'text', 'Routing *', 9);
985
+ this.routingNumberEl = createInputElement(`${this.containerId}-routing`, 'text', ((_c = this.customPlaceholders) === null || _c === void 0 ? void 0 : _c.routingNumber) || 'Routing *', 9);
867
986
  Object.assign(this.routingNumberEl.style, {
868
987
  flex: BANK_FIELD_FLEX.routingNumber.flex,
869
988
  minWidth: BANK_FIELD_FLEX.routingNumber.minWidth,
@@ -879,8 +998,8 @@ export class SpreedlyTokenizer extends Tokenizer {
879
998
  }
880
999
  this.updateBankAccountValidation();
881
1000
  });
882
- container.appendChild(this.routingNumberEl);
883
- this.accountNumberEl = createInputElement(`${this.containerId}-account`, 'text', 'Account Number *', 17);
1001
+ this.appendField(container, this.routingNumberEl, 'Routing Number');
1002
+ this.accountNumberEl = createInputElement(`${this.containerId}-account`, 'text', ((_d = this.customPlaceholders) === null || _d === void 0 ? void 0 : _d.accountNumber) || 'Account Number *', 17);
884
1003
  Object.assign(this.accountNumberEl.style, {
885
1004
  flex: BANK_FIELD_FLEX.accountNumber.flex,
886
1005
  minWidth: BANK_FIELD_FLEX.accountNumber.minWidth,
@@ -896,14 +1015,14 @@ export class SpreedlyTokenizer extends Tokenizer {
896
1015
  }
897
1016
  this.updateBankAccountValidation();
898
1017
  });
899
- container.appendChild(this.accountNumberEl);
1018
+ this.appendField(container, this.accountNumberEl, 'Account Number');
900
1019
  this.accountTypeEl = createAccountTypeSelect(`${this.containerId}-account-type`);
901
1020
  Object.assign(this.accountTypeEl.style, {
902
1021
  flex: BANK_FIELD_FLEX.accountType.flex,
903
1022
  minWidth: BANK_FIELD_FLEX.accountType.minWidth,
904
1023
  });
905
1024
  this.applyInputStyles(this.accountTypeEl, this.mergedStyles, 'right');
906
- container.appendChild(this.accountTypeEl);
1025
+ this.appendField(container, this.accountTypeEl, 'Account Type');
907
1026
  }
908
1027
  }
909
1028
  updateBankAccountValidation() {
@@ -960,30 +1079,42 @@ export class SpreedlyTokenizer extends Tokenizer {
960
1079
  if (!this.expiryEl)
961
1080
  return;
962
1081
  const expiry = this.parseExpiry(this.expiryEl);
1082
+ const isEmpty = !this.expiryEl.value;
1083
+ let isValid = false;
963
1084
  if (!expiry) {
964
- this.applyErrorStyles(this.expiryEl);
965
- return;
966
- }
967
- const month = parseInt(expiry.month, 10);
968
- const year = parseInt(expiry.year, 10);
969
- if (month < 1 || month > 12) {
970
- this.applyErrorStyles(this.expiryEl);
971
- return;
1085
+ if (!isEmpty)
1086
+ this.applyErrorStyles(this.expiryEl);
972
1087
  }
973
- const now = new Date();
974
- const expiryDate = new Date(year, month - 1);
975
- if (expiryDate < now) {
976
- this.applyErrorStyles(this.expiryEl);
977
- return;
1088
+ else {
1089
+ const month = parseInt(expiry.month, 10);
1090
+ const year = parseInt(expiry.year, 10);
1091
+ if (month < 1 || month > 12) {
1092
+ this.applyErrorStyles(this.expiryEl);
1093
+ }
1094
+ else {
1095
+ const now = new Date();
1096
+ const expiryEndDate = new Date(year, month);
1097
+ if (expiryEndDate <= now) {
1098
+ this.applyErrorStyles(this.expiryEl);
1099
+ }
1100
+ else {
1101
+ isValid = true;
1102
+ const expiryPosition = this.effectiveLayout === 'two-line' ? 'left' : 'middle';
1103
+ this.applyInputStyles(this.expiryEl, this.mergedStyles, expiryPosition);
1104
+ }
1105
+ }
978
1106
  }
979
- this.applyInputStyles(this.expiryEl, this.mergedStyles, 'middle');
1107
+ this.currentValidationState.expiry = { isValid, isEmpty };
980
1108
  }
981
1109
  applyErrorStyles(element) {
982
- if (this.mergedStyles.error) {
983
- Object.assign(element.style, this.mergedStyles.error);
1110
+ var _a, _b, _c;
1111
+ const errorColor = ((_a = this.mergedStyles.error) === null || _a === void 0 ? void 0 : _a.borderColor) || '#dc3545';
1112
+ element.style.border = `1px solid ${errorColor}`;
1113
+ if ((_b = this.mergedStyles.error) === null || _b === void 0 ? void 0 : _b.backgroundColor) {
1114
+ element.style.backgroundColor = this.mergedStyles.error.backgroundColor;
984
1115
  }
985
- else {
986
- element.style.borderColor = '#dc3545';
1116
+ if ((_c = this.mergedStyles.error) === null || _c === void 0 ? void 0 : _c.color) {
1117
+ element.style.color = this.mergedStyles.error.color;
987
1118
  }
988
1119
  }
989
1120
  static ensureSpreedlyLoaded() {
@@ -1030,13 +1161,14 @@ export class SpreedlyTokenizer extends Tokenizer {
1030
1161
  throw new Error(`Timeout waiting for global: ${globalName}`);
1031
1162
  });
1032
1163
  }
1033
- static tokenizeBankAccount(data, config) {
1034
- if (config.spreedlyEnvironmentKey === undefined) {
1164
+ static tokenizeBankAccount(data, config, environmentKeyOverride) {
1165
+ const environmentKey = environmentKeyOverride || config.spreedlyEnvironmentKey;
1166
+ if (!environmentKey) {
1035
1167
  return Promise.reject(new Error('Spreedly is not configured.'));
1036
1168
  }
1037
1169
  return fetch(SPREEDLY_TOKENIZER_URL +
1038
1170
  '?' +
1039
- new URLSearchParams({ environment_key: config.spreedlyEnvironmentKey }), {
1171
+ new URLSearchParams({ environment_key: environmentKey }), {
1040
1172
  method: 'POST',
1041
1173
  headers: {
1042
1174
  'Content-Type': 'application/json',