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

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 +23 -1
  12. package/dist/esm/tokenize/SpreedlyTokenizer.js +330 -151
  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 +23 -1
  38. package/dist/tokenize/SpreedlyTokenizer.js +327 -148
  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,33 @@ 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.numberBaseCss = '';
30
+ this.cvvBaseCss = '';
31
+ this.numberErrorCss = '';
32
+ this.cvvErrorCss = '';
33
+ this.numberHasError = false;
34
+ this.cvvHasError = false;
35
+ this.effectiveLayout = 'single-line';
36
+ this.responsiveBreakpoint = RESPONSIVE_BREAKPOINT;
28
37
  this.mode = mode;
29
38
  this.bankCountry = bankCountry;
30
39
  this.enableTestMode = enableTestMode;
40
+ this.responsiveBreakpoint = responsiveBreakpoint;
31
41
  if (mode === 'credit_card') {
32
42
  const SpreedlyFrame = window.SpreedlyPaymentFrame;
33
43
  if (SpreedlyFrame) {
@@ -63,40 +73,33 @@ export class SpreedlyTokenizer extends Tokenizer {
63
73
  }
64
74
  static create(gateway, container, config) {
65
75
  return __awaiter(this, void 0, void 0, function* () {
66
- var _a;
76
+ var _a, _b;
67
77
  if (container.mode !== 'bank_account') {
68
78
  yield SpreedlyTokenizer.ensureSpreedlyLoaded();
69
79
  }
70
- let environmentKey = (_a = gateway.config) === null || _a === void 0 ? void 0 : _a.environment_key;
80
+ let environmentKey = (_a = gateway.config) === null || _a === void 0 ? void 0 : _a.environmentKey;
71
81
  if (!environmentKey && config.clientConfig.spreedlyEnvironmentKey) {
72
82
  environmentKey = config.clientConfig.spreedlyEnvironmentKey;
73
83
  }
74
84
  if (!environmentKey) {
75
- environmentKey = '';
85
+ throw new Error('Spreedly environment key is required. Provide it via gateway.config.environmentKey or client config.');
76
86
  }
77
- const tokenizer = new SpreedlyTokenizer(environmentKey, container.containerId, container.styling, container.mode || 'credit_card', container.bankCountry || 'US', container.enableTestMode || false, container.layout || 'single-line');
87
+ 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
88
  tokenizer.organizationId = config.organizationId;
79
89
  tokenizer.embedId = config.embedId;
80
90
  tokenizer.clientConfig = config.clientConfig;
81
91
  tokenizer.createInternalElements();
82
- let securityArgs;
83
92
  if (container.mode === 'credit_card' &&
84
93
  config.clientConfig.enableSpreedlySecureTokenization) {
85
94
  if (!config.organizationId || !config.embedId) {
86
95
  throw new Error('Secure tokenization is enabled but organizationId and embedId are required');
87
96
  }
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
97
  }
95
- yield tokenizer.init(securityArgs);
98
+ yield tokenizer.init();
96
99
  return tokenizer;
97
100
  });
98
101
  }
99
- init(securityArgs) {
102
+ init() {
100
103
  return __awaiter(this, void 0, void 0, function* () {
101
104
  this.containerEl = document.getElementById(this.containerId);
102
105
  if (this.mode === 'bank_account') {
@@ -140,11 +143,12 @@ export class SpreedlyTokenizer extends Tokenizer {
140
143
  reject(new Error('Spreedly initialization timeout'));
141
144
  }, INIT_TIMEOUT);
142
145
  this.spreedly.on('ready', () => {
146
+ var _a, _b;
143
147
  clearTimeout(timeout);
144
- this.spreedly.setPlaceholder('number', PLACEHOLDERS.cardNumber);
148
+ this.spreedly.setPlaceholder('number', ((_a = this.customPlaceholders) === null || _a === void 0 ? void 0 : _a.cardNumber) || PLACEHOLDERS.cardNumber);
145
149
  this.spreedly.setFieldType('number', 'text');
146
150
  this.spreedly.setNumberFormat('prettyFormat');
147
- this.spreedly.setPlaceholder('cvv', PLACEHOLDERS.cvv);
151
+ this.spreedly.setPlaceholder('cvv', ((_b = this.customPlaceholders) === null || _b === void 0 ? void 0 : _b.cvv) || PLACEHOLDERS.cvv);
148
152
  this.spreedly.setFieldType('cvv', 'text');
149
153
  this.applyUnifiedStyles();
150
154
  if (this.enableTestMode &&
@@ -162,23 +166,37 @@ export class SpreedlyTokenizer extends Tokenizer {
162
166
  this.emit('ready');
163
167
  resolve();
164
168
  });
169
+ this.spreedly.on('paymentMethod', (token, pmData) => {
170
+ const paymentToken = {
171
+ token,
172
+ lastFour: pmData.last_four_digits,
173
+ cardType: this.normalizeCardType(pmData.card_type),
174
+ paymentMethodType: 'credit_card',
175
+ provider: 'spreedly',
176
+ };
177
+ if (this.tokenizationResolve) {
178
+ const resolveTokenization = this.tokenizationResolve;
179
+ this.clearTokenizationState();
180
+ this.emit('tokenReady', paymentToken);
181
+ resolveTokenization(paymentToken);
182
+ }
183
+ });
165
184
  this.spreedly.on('errors', (errors) => {
166
- this.emit('error', new TokenizationError(errors));
185
+ const error = new TokenizationError(errors);
186
+ this.emit('error', error);
187
+ if (this.tokenizationReject) {
188
+ const rejectTokenization = this.tokenizationReject;
189
+ this.clearTokenizationState();
190
+ rejectTokenization(error);
191
+ }
167
192
  });
168
193
  this.spreedly.on('fieldEvent', (fieldName, eventType, activeEl, inputProperties) => {
169
194
  this.handleFieldEvent(fieldName, eventType, inputProperties);
170
195
  });
171
- const initOptions = {
196
+ this.spreedly.init(this.environmentKey, {
172
197
  numberEl: this.numberEl,
173
198
  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);
199
+ });
182
200
  });
183
201
  });
184
202
  }
@@ -196,58 +214,45 @@ export class SpreedlyTokenizer extends Tokenizer {
196
214
  });
197
215
  }
198
216
  tokenizeCardInternal(cardData) {
217
+ if (this.tokenizationPromise) {
218
+ return this.tokenizationPromise;
219
+ }
220
+ const expiry = this.parseExpiry(this.expiryEl);
221
+ if (!expiry) {
222
+ return Promise.reject(new TokenizationError('Expiration date is required', 'VALIDATION_ERROR'));
223
+ }
224
+ this.tokenizationPromise = this.executeCardTokenization(cardData, expiry).finally(() => {
225
+ this.tokenizationPromise = undefined;
226
+ });
227
+ return this.tokenizationPromise;
228
+ }
229
+ executeCardTokenization(cardData, expiry) {
199
230
  return __awaiter(this, void 0, void 0, function* () {
231
+ var _a;
232
+ let securityFields = {};
233
+ if ((_a = this.clientConfig) === null || _a === void 0 ? void 0 : _a.enableSpreedlySecureTokenization) {
234
+ try {
235
+ const args = yield fetchSpreedlySecurityArgs(this.clientConfig, this.organizationId, this.embedId);
236
+ securityFields = {
237
+ nonce: args.nonce,
238
+ timestamp: args.timestamp,
239
+ certificateToken: args.certificate_token,
240
+ signature: args.signature,
241
+ };
242
+ }
243
+ catch (error) {
244
+ throw new TokenizationError(`Secure tokenization failed: ${error instanceof Error ? error.message : 'Unknown error'}`, 'SECURE_TOKENIZATION_ERROR');
245
+ }
246
+ }
200
247
  return new Promise((resolve, reject) => {
201
248
  var _a, _b, _c, _d, _e, _f;
202
- const timeout = setTimeout(() => {
203
- if (this.spreedly && this.spreedly.removeHandlers) {
204
- this.spreedly.removeHandlers();
205
- }
249
+ this.tokenizationResolve = resolve;
250
+ this.tokenizationReject = reject;
251
+ this.tokenizationTimeout = setTimeout(() => {
252
+ this.clearTokenizationState();
206
253
  reject(new TokenizationError('Tokenization timeout', 'TIMEOUT'));
207
254
  }, 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
- });
255
+ 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
256
  });
252
257
  });
253
258
  }
@@ -259,7 +264,7 @@ export class SpreedlyTokenizer extends Tokenizer {
259
264
  minWidth: CANADIAN_BANK_FIELD_FLEX.accountType.minWidth,
260
265
  });
261
266
  this.applyInputStyles(this.accountTypeEl, this.mergedStyles, 'left');
262
- container.appendChild(this.accountTypeEl);
267
+ this.appendField(container, this.accountTypeEl, 'Account Type');
263
268
  this.institutionNumberEl = createInputElement(`${this.containerId}-institution`, 'text', 'Inst *', 3);
264
269
  Object.assign(this.institutionNumberEl.style, {
265
270
  flex: '1',
@@ -276,7 +281,7 @@ export class SpreedlyTokenizer extends Tokenizer {
276
281
  }
277
282
  this.updateBankAccountValidation();
278
283
  });
279
- container.appendChild(this.institutionNumberEl);
284
+ this.appendField(container, this.institutionNumberEl, 'Institution');
280
285
  this.transitNumberEl = createInputElement(`${this.containerId}-transit`, 'text', 'Transit *', 5);
281
286
  Object.assign(this.transitNumberEl.style, {
282
287
  flex: '2',
@@ -293,7 +298,7 @@ export class SpreedlyTokenizer extends Tokenizer {
293
298
  }
294
299
  this.updateBankAccountValidation();
295
300
  });
296
- container.appendChild(this.transitNumberEl);
301
+ this.appendField(container, this.transitNumberEl, 'Transit');
297
302
  this.accountNumberEl = createInputElement(`${this.containerId}-account`, 'text', 'Account Number *');
298
303
  Object.assign(this.accountNumberEl.style, {
299
304
  flex: '1',
@@ -311,7 +316,7 @@ export class SpreedlyTokenizer extends Tokenizer {
311
316
  }
312
317
  this.updateBankAccountValidation();
313
318
  });
314
- container.appendChild(this.accountNumberEl);
319
+ this.appendField(container, this.accountNumberEl, 'Account Number');
315
320
  }
316
321
  else {
317
322
  this.institutionNumberEl = createInputElement(`${this.containerId}-institution`, 'text', 'Inst *', 3);
@@ -330,7 +335,7 @@ export class SpreedlyTokenizer extends Tokenizer {
330
335
  }
331
336
  this.updateBankAccountValidation();
332
337
  });
333
- container.appendChild(this.institutionNumberEl);
338
+ this.appendField(container, this.institutionNumberEl, 'Institution');
334
339
  this.transitNumberEl = createInputElement(`${this.containerId}-transit`, 'text', 'Transit *', 5);
335
340
  Object.assign(this.transitNumberEl.style, {
336
341
  flex: CANADIAN_BANK_FIELD_FLEX.transitNumber.flex,
@@ -347,7 +352,7 @@ export class SpreedlyTokenizer extends Tokenizer {
347
352
  }
348
353
  this.updateBankAccountValidation();
349
354
  });
350
- container.appendChild(this.transitNumberEl);
355
+ this.appendField(container, this.transitNumberEl, 'Transit');
351
356
  this.accountNumberEl = createInputElement(`${this.containerId}-account`, 'text', 'Account Number *');
352
357
  Object.assign(this.accountNumberEl.style, {
353
358
  flex: CANADIAN_BANK_FIELD_FLEX.accountNumber.flex,
@@ -364,14 +369,14 @@ export class SpreedlyTokenizer extends Tokenizer {
364
369
  }
365
370
  this.updateBankAccountValidation();
366
371
  });
367
- container.appendChild(this.accountNumberEl);
372
+ this.appendField(container, this.accountNumberEl, 'Account Number');
368
373
  this.accountTypeEl = createAccountTypeSelect(`${this.containerId}-account-type`);
369
374
  Object.assign(this.accountTypeEl.style, {
370
375
  flex: CANADIAN_BANK_FIELD_FLEX.accountType.flex,
371
376
  minWidth: CANADIAN_BANK_FIELD_FLEX.accountType.minWidth,
372
377
  });
373
378
  this.applyInputStyles(this.accountTypeEl, this.mergedStyles, 'right');
374
- container.appendChild(this.accountTypeEl);
379
+ this.appendField(container, this.accountTypeEl, 'Account Type');
375
380
  }
376
381
  }
377
382
  tokenizeBankAccountInternal(bankData) {
@@ -439,18 +444,23 @@ export class SpreedlyTokenizer extends Tokenizer {
439
444
  accountType: accountType,
440
445
  accountHolderType: accountHolderType,
441
446
  },
442
- }, this.clientConfig);
443
- return {
447
+ }, this.clientConfig, this.environmentKey);
448
+ const paymentToken = {
444
449
  token: result,
445
450
  lastFour: accountNumber.slice(-4),
446
451
  accountType: accountType,
447
452
  paymentMethodType: 'bank_account',
448
453
  provider: 'spreedly',
449
454
  };
455
+ this.emit('tokenReady', paymentToken);
456
+ return paymentToken;
450
457
  });
451
458
  }
452
459
  validate() {
453
460
  return __awaiter(this, void 0, void 0, function* () {
461
+ if (!this.isReady) {
462
+ throw new Error('Tokenizer not initialized');
463
+ }
454
464
  if (this.mode === 'bank_account') {
455
465
  return this.validateBankAccountInternal();
456
466
  }
@@ -585,19 +595,21 @@ export class SpreedlyTokenizer extends Tokenizer {
585
595
  const isConnected = ((_a = this.mergedStyles.container) === null || _a === void 0 ? void 0 : _a.gap) === '0';
586
596
  if (this.mergedStyles.input) {
587
597
  if (isConnected) {
588
- const numberStyles = Object.assign(Object.assign({}, this.mergedStyles.input), { borderRight: 'none', borderRadius: this.mergedStyles.input.borderRadius
598
+ const numberStyles = Object.assign(Object.assign({}, this.mergedStyles.input), { borderRight: '1px solid transparent', borderRadius: this.mergedStyles.input.borderRadius
589
599
  ? `${this.mergedStyles.input.borderRadius} 0 0 ${this.mergedStyles.input.borderRadius}`
590
600
  : '0' });
591
- const numberCss = this.stylesToCssString(numberStyles);
592
- this.spreedly.setStyle('number', numberCss);
601
+ this.numberBaseCss = this.stylesToCssString(numberStyles);
602
+ this.spreedly.setStyle('number', this.numberBaseCss);
593
603
  const cvvStyles = Object.assign(Object.assign({}, this.mergedStyles.input), { borderRadius: this.mergedStyles.input.borderRadius
594
604
  ? `0 ${this.mergedStyles.input.borderRadius} ${this.mergedStyles.input.borderRadius} 0`
595
605
  : '0' });
596
- const cvvCss = this.stylesToCssString(cvvStyles);
597
- this.spreedly.setStyle('cvv', cvvCss);
606
+ this.cvvBaseCss = this.stylesToCssString(cvvStyles);
607
+ this.spreedly.setStyle('cvv', this.cvvBaseCss);
598
608
  }
599
609
  else {
600
610
  const baseCss = this.stylesToCssString(this.mergedStyles.input);
611
+ this.numberBaseCss = baseCss;
612
+ this.cvvBaseCss = baseCss;
601
613
  this.spreedly.setStyle('number', baseCss);
602
614
  this.spreedly.setStyle('cvv', baseCss);
603
615
  }
@@ -608,9 +620,20 @@ export class SpreedlyTokenizer extends Tokenizer {
608
620
  this.spreedly.setStyle('cvv:focus', focusCss);
609
621
  }
610
622
  if (this.mergedStyles.error) {
611
- const errorCss = this.stylesToCssString(this.mergedStyles.error);
612
- this.spreedly.setStyle('number.invalid', errorCss);
613
- this.spreedly.setStyle('cvv.invalid', errorCss);
623
+ const errorOverrides = {};
624
+ if (this.mergedStyles.error.borderColor) {
625
+ errorOverrides.border = `1px solid ${this.mergedStyles.error.borderColor}`;
626
+ }
627
+ if (this.mergedStyles.error.backgroundColor) {
628
+ errorOverrides.backgroundColor =
629
+ this.mergedStyles.error.backgroundColor;
630
+ }
631
+ if (this.mergedStyles.error.color) {
632
+ errorOverrides.color = this.mergedStyles.error.color;
633
+ }
634
+ const errorCss = this.stylesToCssString(errorOverrides);
635
+ this.numberErrorCss = this.numberBaseCss + '; ' + errorCss;
636
+ this.cvvErrorCss = this.cvvBaseCss + '; ' + errorCss;
614
637
  }
615
638
  }
616
639
  stylesToCssString(styles) {
@@ -638,13 +661,41 @@ export class SpreedlyTokenizer extends Tokenizer {
638
661
  this.accountNumberEl.value = '';
639
662
  if (this.accountTypeEl)
640
663
  this.accountTypeEl.selectedIndex = 0;
664
+ this.currentValidationState = {
665
+ isValid: false,
666
+ routingNumber: { isValid: false, isEmpty: true },
667
+ accountNumber: { isValid: false, isEmpty: true },
668
+ };
641
669
  }
642
670
  else {
643
671
  this.spreedly.reload();
644
672
  if (this.expiryEl) {
645
673
  this.expiryEl.value = '';
646
674
  }
675
+ if (this.numberHasError && this.numberBaseCss) {
676
+ this.spreedly.setStyle('number', this.numberBaseCss);
677
+ this.numberHasError = false;
678
+ }
679
+ if (this.cvvHasError && this.cvvBaseCss) {
680
+ this.spreedly.setStyle('cvv', this.cvvBaseCss);
681
+ this.cvvHasError = false;
682
+ }
683
+ this.currentValidationState = {
684
+ isValid: false,
685
+ cardNumber: { isValid: false, isEmpty: true },
686
+ cvv: { isValid: false, isEmpty: true },
687
+ expiry: { isValid: false, isEmpty: true },
688
+ };
689
+ }
690
+ if (this.tokenizationReject) {
691
+ const reject = this.tokenizationReject;
692
+ this.clearTokenizationState();
693
+ reject(new TokenizationError('Tokenizer cleared', 'CLEARED'));
647
694
  }
695
+ else {
696
+ this.clearTokenizationState();
697
+ }
698
+ this.emit('validation', this.currentValidationState);
648
699
  }
649
700
  focus(field) {
650
701
  if (this.mode === 'bank_account') {
@@ -680,12 +731,84 @@ export class SpreedlyTokenizer extends Tokenizer {
680
731
  element === null || element === void 0 ? void 0 : element.focus();
681
732
  }
682
733
  }
734
+ determineLayout(width) {
735
+ return width < this.responsiveBreakpoint ? 'two-line' : 'single-line';
736
+ }
737
+ setupResizeObserver() {
738
+ if (this.layout !== 'responsive')
739
+ return;
740
+ const container = document.getElementById(this.containerId);
741
+ if (!container)
742
+ return;
743
+ let debounceTimer;
744
+ this.resizeObserver = new ResizeObserver((entries) => {
745
+ clearTimeout(debounceTimer);
746
+ debounceTimer = setTimeout(() => {
747
+ const newWidth = entries[0].contentRect.width;
748
+ const newLayout = this.determineLayout(newWidth);
749
+ if (newLayout !== this.effectiveLayout) {
750
+ this.applyLayoutStyles(newLayout);
751
+ }
752
+ }, 100);
753
+ });
754
+ this.resizeObserver.observe(container);
755
+ }
756
+ applyLayoutStyles(newLayout) {
757
+ this.effectiveLayout = newLayout;
758
+ const container = document.getElementById(this.containerId);
759
+ if (!container)
760
+ return;
761
+ if (newLayout === 'two-line') {
762
+ container.style.flexWrap = 'wrap';
763
+ if (this.cardNumberDiv) {
764
+ this.cardNumberDiv.style.flex = '1';
765
+ this.cardNumberDiv.style.flexBasis = '100%';
766
+ }
767
+ if (this.expiryEl) {
768
+ this.expiryEl.style.flex = '1';
769
+ this.expiryEl.style.flexBasis = 'auto';
770
+ }
771
+ if (this.cvvDiv) {
772
+ this.cvvDiv.style.flex = '1';
773
+ this.cvvDiv.style.flexBasis = 'auto';
774
+ }
775
+ }
776
+ else {
777
+ container.style.flexWrap = 'nowrap';
778
+ if (this.cardNumberDiv) {
779
+ this.cardNumberDiv.style.flex = FIELD_FLEX.cardNumber.flex;
780
+ }
781
+ if (this.expiryEl) {
782
+ this.expiryEl.style.flex = FIELD_FLEX.expiry.flex;
783
+ }
784
+ if (this.cvvDiv) {
785
+ this.cvvDiv.style.flex = FIELD_FLEX.cvv.flex;
786
+ }
787
+ }
788
+ }
789
+ clearTokenizationState() {
790
+ if (this.tokenizationTimeout) {
791
+ clearTimeout(this.tokenizationTimeout);
792
+ this.tokenizationTimeout = undefined;
793
+ }
794
+ this.tokenizationResolve = undefined;
795
+ this.tokenizationReject = undefined;
796
+ }
683
797
  destroy() {
798
+ var _a;
799
+ this.markDestroyed();
800
+ (_a = this.resizeObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
801
+ if (this.tokenizationReject) {
802
+ const reject = this.tokenizationReject;
803
+ this.clearTokenizationState();
804
+ reject(new TokenizationError('Tokenizer destroyed', 'DESTROYED'));
805
+ }
684
806
  if (this.spreedly && this.spreedly.removeHandlers) {
685
807
  this.spreedly.removeHandlers();
686
808
  }
687
809
  this.eventHandlers.clear();
688
- if (this.expiryEl) {
810
+ if (this.containerEl) {
811
+ this.containerEl.innerHTML = '';
689
812
  }
690
813
  }
691
814
  hasToken() {
@@ -713,10 +836,9 @@ export class SpreedlyTokenizer extends Tokenizer {
713
836
  handleFieldEvent(fieldName, eventType, inputProperties) {
714
837
  if (eventType === 'input') {
715
838
  if (fieldName === 'number') {
716
- this.currentValidationState.cardNumber = {
717
- isValid: inputProperties.validNumber || false,
718
- isEmpty: inputProperties.numberLength === 0,
719
- };
839
+ const isEmpty = inputProperties.numberLength === 0;
840
+ const isValid = inputProperties.validNumber || false;
841
+ this.currentValidationState.cardNumber = { isValid, isEmpty };
720
842
  if (inputProperties.cardType &&
721
843
  inputProperties.cardType !== 'unknown') {
722
844
  this.emit('cardTypeChange', {
@@ -725,21 +847,37 @@ export class SpreedlyTokenizer extends Tokenizer {
725
847
  }
726
848
  }
727
849
  else if (fieldName === 'cvv') {
728
- this.currentValidationState.cvv = {
729
- isValid: inputProperties.validCvv || false,
730
- isEmpty: inputProperties.cvvLength === 0,
731
- };
850
+ const isEmpty = inputProperties.cvvLength === 0;
851
+ const isValid = inputProperties.validCvv || false;
852
+ this.currentValidationState.cvv = { isValid, isEmpty };
732
853
  }
733
854
  this.updateOverallValidation();
734
855
  }
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 });
856
+ if (eventType === 'blur') {
857
+ if (fieldName === 'number') {
858
+ const state = this.currentValidationState.cardNumber;
859
+ const shouldShowError = state
860
+ ? !state.isEmpty && !state.isValid
861
+ : false;
862
+ if (shouldShowError !== this.numberHasError) {
863
+ this.numberHasError = shouldShowError;
864
+ if (this.numberErrorCss) {
865
+ this.spreedly.setStyle('number', shouldShowError ? this.numberErrorCss : this.numberBaseCss);
866
+ }
867
+ }
868
+ }
869
+ else if (fieldName === 'cvv') {
870
+ const state = this.currentValidationState.cvv;
871
+ const shouldShowError = state
872
+ ? !state.isEmpty && !state.isValid
873
+ : false;
874
+ if (shouldShowError !== this.cvvHasError) {
875
+ this.cvvHasError = shouldShowError;
876
+ if (this.cvvErrorCss) {
877
+ this.spreedly.setStyle('cvv', shouldShowError ? this.cvvErrorCss : this.cvvBaseCss);
878
+ }
879
+ }
880
+ }
743
881
  }
744
882
  }
745
883
  createInternalElements() {
@@ -756,13 +894,25 @@ export class SpreedlyTokenizer extends Tokenizer {
756
894
  this.createCreditCardFields(container);
757
895
  }
758
896
  }
897
+ appendField(container, field, labelText) {
898
+ container.appendChild(wrapFieldWithLabel(field, labelText, this.mergedStyles));
899
+ }
759
900
  createCreditCardFields(container) {
760
- if (this.layout === 'two-line') {
901
+ var _a, _b;
902
+ if (this.layout === 'responsive') {
903
+ this.effectiveLayout = this.determineLayout(container.offsetWidth);
904
+ }
905
+ else {
906
+ this.effectiveLayout =
907
+ this.layout === 'two-line' ? 'two-line' : 'single-line';
908
+ }
909
+ if (this.effectiveLayout === 'two-line' && this.layout !== 'responsive') {
761
910
  const cardNumberDiv = createFieldContainer(this.numberEl, '1', FIELD_FLEX.cardNumber.minWidth);
762
911
  cardNumberDiv.style.height = this.mergedStyles.input.height;
763
912
  cardNumberDiv.style.flexBasis = '100%';
764
- container.appendChild(cardNumberDiv);
765
- this.expiryEl = createInputElement(this.expiryId, 'text', PLACEHOLDERS.expiry, FIELD_CONSTRAINTS.expiry.maxLength);
913
+ this.cardNumberDiv = cardNumberDiv;
914
+ this.appendField(container, cardNumberDiv, 'Card Number');
915
+ this.expiryEl = createInputElement(this.expiryId, 'text', ((_a = this.customPlaceholders) === null || _a === void 0 ? void 0 : _a.expiry) || PLACEHOLDERS.expiry, FIELD_CONSTRAINTS.expiry.maxLength);
766
916
  Object.assign(this.expiryEl.style, {
767
917
  flex: '1',
768
918
  minWidth: FIELD_FLEX.expiry.minWidth,
@@ -778,16 +928,22 @@ export class SpreedlyTokenizer extends Tokenizer {
778
928
  };
779
929
  this.updateOverallValidation();
780
930
  });
781
- container.appendChild(this.expiryEl);
931
+ this.expiryEl.addEventListener('blur', () => {
932
+ this.validateExpiry();
933
+ this.updateOverallValidation();
934
+ });
935
+ this.appendField(container, this.expiryEl, 'Expiration');
782
936
  const cvvDiv = createFieldContainer(this.cvvEl, '1', FIELD_FLEX.cvv.minWidth);
783
937
  cvvDiv.style.height = this.mergedStyles.input.height;
784
- container.appendChild(cvvDiv);
938
+ this.cvvDiv = cvvDiv;
939
+ this.appendField(container, cvvDiv, 'CVV');
785
940
  }
786
941
  else {
787
942
  const cardNumberDiv = createFieldContainer(this.numberEl, FIELD_FLEX.cardNumber.flex, FIELD_FLEX.cardNumber.minWidth);
788
943
  cardNumberDiv.style.height = this.mergedStyles.input.height;
789
- container.appendChild(cardNumberDiv);
790
- this.expiryEl = createInputElement(this.expiryId, 'text', PLACEHOLDERS.expiry, FIELD_CONSTRAINTS.expiry.maxLength);
944
+ this.cardNumberDiv = cardNumberDiv;
945
+ this.appendField(container, cardNumberDiv, 'Card Number');
946
+ this.expiryEl = createInputElement(this.expiryId, 'text', ((_b = this.customPlaceholders) === null || _b === void 0 ? void 0 : _b.expiry) || PLACEHOLDERS.expiry, FIELD_CONSTRAINTS.expiry.maxLength);
791
947
  Object.assign(this.expiryEl.style, {
792
948
  flex: FIELD_FLEX.expiry.flex,
793
949
  minWidth: FIELD_FLEX.expiry.minWidth,
@@ -803,10 +959,19 @@ export class SpreedlyTokenizer extends Tokenizer {
803
959
  };
804
960
  this.updateOverallValidation();
805
961
  });
806
- container.appendChild(this.expiryEl);
962
+ this.expiryEl.addEventListener('blur', () => {
963
+ this.validateExpiry();
964
+ this.updateOverallValidation();
965
+ });
966
+ this.appendField(container, this.expiryEl, 'Expiration');
807
967
  const cvvDiv = createFieldContainer(this.cvvEl, FIELD_FLEX.cvv.flex, FIELD_FLEX.cvv.minWidth);
808
968
  cvvDiv.style.height = this.mergedStyles.input.height;
809
- container.appendChild(cvvDiv);
969
+ this.cvvDiv = cvvDiv;
970
+ this.appendField(container, cvvDiv, 'CVV');
971
+ if (this.layout === 'responsive') {
972
+ this.applyLayoutStyles(this.effectiveLayout);
973
+ this.setupResizeObserver();
974
+ }
810
975
  }
811
976
  }
812
977
  createBankAccountFields(container) {
@@ -818,6 +983,7 @@ export class SpreedlyTokenizer extends Tokenizer {
818
983
  }
819
984
  }
820
985
  createUSBankAccountFields(container) {
986
+ var _a, _b, _c, _d;
821
987
  if (this.layout === 'two-line') {
822
988
  this.accountTypeEl = createAccountTypeSelect(`${this.containerId}-account-type`);
823
989
  Object.assign(this.accountTypeEl.style, {
@@ -825,8 +991,8 @@ export class SpreedlyTokenizer extends Tokenizer {
825
991
  minWidth: BANK_FIELD_FLEX.accountType.minWidth,
826
992
  });
827
993
  this.applyInputStyles(this.accountTypeEl, this.mergedStyles, 'left');
828
- container.appendChild(this.accountTypeEl);
829
- this.routingNumberEl = createInputElement(`${this.containerId}-routing`, 'text', 'Routing *', 9);
994
+ this.appendField(container, this.accountTypeEl, 'Account Type');
995
+ this.routingNumberEl = createInputElement(`${this.containerId}-routing`, 'text', ((_a = this.customPlaceholders) === null || _a === void 0 ? void 0 : _a.routingNumber) || 'Routing *', 9);
830
996
  Object.assign(this.routingNumberEl.style, {
831
997
  flex: '1',
832
998
  minWidth: BANK_FIELD_FLEX.routingNumber.minWidth,
@@ -842,8 +1008,8 @@ export class SpreedlyTokenizer extends Tokenizer {
842
1008
  }
843
1009
  this.updateBankAccountValidation();
844
1010
  });
845
- container.appendChild(this.routingNumberEl);
846
- this.accountNumberEl = createInputElement(`${this.containerId}-account`, 'text', 'Account Number *', 17);
1011
+ this.appendField(container, this.routingNumberEl, 'Routing Number');
1012
+ this.accountNumberEl = createInputElement(`${this.containerId}-account`, 'text', ((_b = this.customPlaceholders) === null || _b === void 0 ? void 0 : _b.accountNumber) || 'Account Number *', 17);
847
1013
  Object.assign(this.accountNumberEl.style, {
848
1014
  flex: '1',
849
1015
  flexBasis: '100%',
@@ -860,10 +1026,10 @@ export class SpreedlyTokenizer extends Tokenizer {
860
1026
  }
861
1027
  this.updateBankAccountValidation();
862
1028
  });
863
- container.appendChild(this.accountNumberEl);
1029
+ this.appendField(container, this.accountNumberEl, 'Account Number');
864
1030
  }
865
1031
  else {
866
- this.routingNumberEl = createInputElement(`${this.containerId}-routing`, 'text', 'Routing *', 9);
1032
+ this.routingNumberEl = createInputElement(`${this.containerId}-routing`, 'text', ((_c = this.customPlaceholders) === null || _c === void 0 ? void 0 : _c.routingNumber) || 'Routing *', 9);
867
1033
  Object.assign(this.routingNumberEl.style, {
868
1034
  flex: BANK_FIELD_FLEX.routingNumber.flex,
869
1035
  minWidth: BANK_FIELD_FLEX.routingNumber.minWidth,
@@ -879,8 +1045,8 @@ export class SpreedlyTokenizer extends Tokenizer {
879
1045
  }
880
1046
  this.updateBankAccountValidation();
881
1047
  });
882
- container.appendChild(this.routingNumberEl);
883
- this.accountNumberEl = createInputElement(`${this.containerId}-account`, 'text', 'Account Number *', 17);
1048
+ this.appendField(container, this.routingNumberEl, 'Routing Number');
1049
+ this.accountNumberEl = createInputElement(`${this.containerId}-account`, 'text', ((_d = this.customPlaceholders) === null || _d === void 0 ? void 0 : _d.accountNumber) || 'Account Number *', 17);
884
1050
  Object.assign(this.accountNumberEl.style, {
885
1051
  flex: BANK_FIELD_FLEX.accountNumber.flex,
886
1052
  minWidth: BANK_FIELD_FLEX.accountNumber.minWidth,
@@ -896,14 +1062,14 @@ export class SpreedlyTokenizer extends Tokenizer {
896
1062
  }
897
1063
  this.updateBankAccountValidation();
898
1064
  });
899
- container.appendChild(this.accountNumberEl);
1065
+ this.appendField(container, this.accountNumberEl, 'Account Number');
900
1066
  this.accountTypeEl = createAccountTypeSelect(`${this.containerId}-account-type`);
901
1067
  Object.assign(this.accountTypeEl.style, {
902
1068
  flex: BANK_FIELD_FLEX.accountType.flex,
903
1069
  minWidth: BANK_FIELD_FLEX.accountType.minWidth,
904
1070
  });
905
1071
  this.applyInputStyles(this.accountTypeEl, this.mergedStyles, 'right');
906
- container.appendChild(this.accountTypeEl);
1072
+ this.appendField(container, this.accountTypeEl, 'Account Type');
907
1073
  }
908
1074
  }
909
1075
  updateBankAccountValidation() {
@@ -960,30 +1126,42 @@ export class SpreedlyTokenizer extends Tokenizer {
960
1126
  if (!this.expiryEl)
961
1127
  return;
962
1128
  const expiry = this.parseExpiry(this.expiryEl);
1129
+ const isEmpty = !this.expiryEl.value;
1130
+ let isValid = false;
963
1131
  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;
1132
+ if (!isEmpty)
1133
+ this.applyErrorStyles(this.expiryEl);
972
1134
  }
973
- const now = new Date();
974
- const expiryDate = new Date(year, month - 1);
975
- if (expiryDate < now) {
976
- this.applyErrorStyles(this.expiryEl);
977
- return;
1135
+ else {
1136
+ const month = parseInt(expiry.month, 10);
1137
+ const year = parseInt(expiry.year, 10);
1138
+ if (month < 1 || month > 12) {
1139
+ this.applyErrorStyles(this.expiryEl);
1140
+ }
1141
+ else {
1142
+ const now = new Date();
1143
+ const expiryEndDate = new Date(year, month);
1144
+ if (expiryEndDate <= now) {
1145
+ this.applyErrorStyles(this.expiryEl);
1146
+ }
1147
+ else {
1148
+ isValid = true;
1149
+ const expiryPosition = this.effectiveLayout === 'two-line' ? 'left' : 'middle';
1150
+ this.applyInputStyles(this.expiryEl, this.mergedStyles, expiryPosition);
1151
+ }
1152
+ }
978
1153
  }
979
- this.applyInputStyles(this.expiryEl, this.mergedStyles, 'middle');
1154
+ this.currentValidationState.expiry = { isValid, isEmpty };
980
1155
  }
981
1156
  applyErrorStyles(element) {
982
- if (this.mergedStyles.error) {
983
- Object.assign(element.style, this.mergedStyles.error);
1157
+ var _a, _b, _c;
1158
+ const errorColor = ((_a = this.mergedStyles.error) === null || _a === void 0 ? void 0 : _a.borderColor) || '#dc3545';
1159
+ element.style.border = `1px solid ${errorColor}`;
1160
+ if ((_b = this.mergedStyles.error) === null || _b === void 0 ? void 0 : _b.backgroundColor) {
1161
+ element.style.backgroundColor = this.mergedStyles.error.backgroundColor;
984
1162
  }
985
- else {
986
- element.style.borderColor = '#dc3545';
1163
+ if ((_c = this.mergedStyles.error) === null || _c === void 0 ? void 0 : _c.color) {
1164
+ element.style.color = this.mergedStyles.error.color;
987
1165
  }
988
1166
  }
989
1167
  static ensureSpreedlyLoaded() {
@@ -1030,13 +1208,14 @@ export class SpreedlyTokenizer extends Tokenizer {
1030
1208
  throw new Error(`Timeout waiting for global: ${globalName}`);
1031
1209
  });
1032
1210
  }
1033
- static tokenizeBankAccount(data, config) {
1034
- if (config.spreedlyEnvironmentKey === undefined) {
1211
+ static tokenizeBankAccount(data, config, environmentKeyOverride) {
1212
+ const environmentKey = environmentKeyOverride || config.spreedlyEnvironmentKey;
1213
+ if (!environmentKey) {
1035
1214
  return Promise.reject(new Error('Spreedly is not configured.'));
1036
1215
  }
1037
1216
  return fetch(SPREEDLY_TOKENIZER_URL +
1038
1217
  '?' +
1039
- new URLSearchParams({ environment_key: config.spreedlyEnvironmentKey }), {
1218
+ new URLSearchParams({ environment_key: environmentKey }), {
1040
1219
  method: 'POST',
1041
1220
  headers: {
1042
1221
  'Content-Type': 'application/json',