secure-ui-components 0.2.3 → 0.2.4

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.
@@ -19,769 +19,4 @@
19
19
  *
20
20
  * @module secure-card
21
21
  * @license MIT
22
- */
23
- import { SecureBaseComponent } from '../../core/base-component.js';
24
- // Ordered most-specific first to avoid false positives (e.g. Diners before Visa)
25
- const CARD_TYPES = Object.freeze([
26
- {
27
- type: 'amex',
28
- pattern: /^3[47]/,
29
- format: [4, 6, 5],
30
- cvcLength: 4,
31
- lengths: [15],
32
- label: 'Amex',
33
- },
34
- {
35
- type: 'diners',
36
- pattern: /^3(?:0[0-5]|[68])/,
37
- format: [4, 6, 4],
38
- cvcLength: 3,
39
- lengths: [14],
40
- label: 'Diners',
41
- },
42
- {
43
- type: 'discover',
44
- pattern: /^6(?:011|5[0-9]{2})/,
45
- format: [4, 4, 4, 4],
46
- cvcLength: 3,
47
- lengths: [16],
48
- label: 'Discover',
49
- },
50
- {
51
- type: 'jcb',
52
- pattern: /^(?:2131|1800|35\d{3})/,
53
- format: [4, 4, 4, 4],
54
- cvcLength: 3,
55
- lengths: [16],
56
- label: 'JCB',
57
- },
58
- {
59
- type: 'mastercard',
60
- pattern: /^(?:5[1-5]|2[2-7])/,
61
- format: [4, 4, 4, 4],
62
- cvcLength: 3,
63
- lengths: [16],
64
- label: 'Mastercard',
65
- },
66
- {
67
- type: 'visa',
68
- pattern: /^4/,
69
- format: [4, 4, 4, 4],
70
- cvcLength: 3,
71
- lengths: [13, 16, 19],
72
- label: 'Visa',
73
- },
74
- ]);
75
- // ── Utilities ──────────────────────────────────────────────────────────────
76
- function detectCardType(digits) {
77
- if (!digits)
78
- return null;
79
- return CARD_TYPES.find(ct => ct.pattern.test(digits)) ?? null;
80
- }
81
- function formatDigits(digits, format) {
82
- let result = '';
83
- let pos = 0;
84
- for (let i = 0; i < format.length; i++) {
85
- const chunk = digits.slice(pos, pos + format[i]);
86
- if (!chunk)
87
- break;
88
- if (i > 0)
89
- result += ' ';
90
- result += chunk;
91
- pos += format[i];
92
- }
93
- return result;
94
- }
95
- function maskCardForDisplay(digits, format) {
96
- const totalLen = format.reduce((a, b) => a + b, 0);
97
- if (!digits) {
98
- return formatDigits('•'.repeat(totalLen), format);
99
- }
100
- const padded = digits.padEnd(totalLen, '•').slice(0, totalLen);
101
- const last4Start = Math.max(0, padded.length - 4);
102
- const masked = '•'.repeat(last4Start) + padded.slice(last4Start);
103
- return formatDigits(masked, format);
104
- }
105
- function luhnValid(digits) {
106
- if (!digits || digits.length < 12)
107
- return false;
108
- let sum = 0;
109
- let alternate = false;
110
- for (let i = digits.length - 1; i >= 0; i--) {
111
- const ch = digits[i];
112
- if (!ch)
113
- return false;
114
- const n0 = parseInt(ch, 10);
115
- if (isNaN(n0))
116
- return false;
117
- let n = n0;
118
- if (alternate) {
119
- n *= 2;
120
- if (n > 9)
121
- n -= 9;
122
- }
123
- sum += n;
124
- alternate = !alternate;
125
- }
126
- return sum % 10 === 0;
127
- }
128
- function isExpiryValid(month, year) {
129
- const now = new Date();
130
- const currentYear = now.getFullYear() % 100;
131
- const currentMonth = now.getMonth() + 1;
132
- if (year < currentYear)
133
- return false;
134
- if (year === currentYear && month < currentMonth)
135
- return false;
136
- return true;
137
- }
138
- let instanceCounter = 0;
139
- // ── Component ──────────────────────────────────────────────────────────────
140
- export class SecureCard extends SecureBaseComponent {
141
- // Shadow DOM input elements
142
- #numberInput = null;
143
- #expiryInput = null;
144
- #cvcInput = null;
145
- #nameInput = null;
146
- // Card visual elements
147
- #cardEl = null;
148
- #cardNumberDisplay = null;
149
- #cardExpiryDisplay = null;
150
- #cardNameDisplay = null;
151
- #cardTypeLabel = null;
152
- #cardCvcDisplay = null;
153
- // Error containers
154
- #numberError = null;
155
- #expiryError = null;
156
- #cvcError = null;
157
- #nameError = null;
158
- // Light DOM hidden inputs for form participation
159
- #hiddenNumber = null;
160
- #hiddenExpiry = null;
161
- #hiddenName = null;
162
- // Sensitive state — cleared on disconnect
163
- #cardDigits = '';
164
- #cvcDigits = '';
165
- #expiryValue = '';
166
- #cardholderName = '';
167
- #cardTypeConfig = null;
168
- #instanceId;
169
- constructor() {
170
- super();
171
- this.#instanceId = `secure-card-${++instanceCounter}`;
172
- }
173
- static get observedAttributes() {
174
- return [...super.observedAttributes, 'name', 'label', 'show-name'];
175
- }
176
- // ── Render ─────────────────────────────────────────────────────────────────
177
- render() {
178
- const fragment = document.createDocumentFragment();
179
- const disabled = this.hasAttribute('disabled');
180
- const showName = this.hasAttribute('show-name');
181
- const label = this.getAttribute('label') ?? '';
182
- // ── Outer container ─────────────────────────────────────────────────────
183
- const container = document.createElement('div');
184
- container.className = 'card-container';
185
- container.setAttribute('part', 'container');
186
- // ── Card visual (aria-hidden — purely decorative) ────────────────────────
187
- const scene = document.createElement('div');
188
- scene.className = 'card-scene';
189
- scene.setAttribute('aria-hidden', 'true');
190
- this.#cardEl = document.createElement('div');
191
- this.#cardEl.className = 'card';
192
- // Front face
193
- const front = document.createElement('div');
194
- front.className = 'card-face card-front';
195
- const topRow = document.createElement('div');
196
- topRow.className = 'card-top-row';
197
- const chip = document.createElement('div');
198
- chip.className = 'card-chip';
199
- this.#cardTypeLabel = document.createElement('div');
200
- this.#cardTypeLabel.className = 'card-type-label';
201
- topRow.appendChild(chip);
202
- topRow.appendChild(this.#cardTypeLabel);
203
- front.appendChild(topRow);
204
- this.#cardNumberDisplay = document.createElement('div');
205
- this.#cardNumberDisplay.className = 'card-number-display';
206
- this.#cardNumberDisplay.textContent = '•••• •••• •••• ••••';
207
- front.appendChild(this.#cardNumberDisplay);
208
- const bottomRow = document.createElement('div');
209
- bottomRow.className = 'card-bottom-row';
210
- const nameSection = document.createElement('div');
211
- nameSection.className = 'card-name-section';
212
- const nameFieldLabel = document.createElement('div');
213
- nameFieldLabel.className = 'card-field-label';
214
- nameFieldLabel.textContent = 'Card holder';
215
- this.#cardNameDisplay = document.createElement('div');
216
- this.#cardNameDisplay.className = 'card-name-display';
217
- this.#cardNameDisplay.textContent = 'FULL NAME';
218
- nameSection.appendChild(nameFieldLabel);
219
- nameSection.appendChild(this.#cardNameDisplay);
220
- const expirySection = document.createElement('div');
221
- expirySection.className = 'card-expiry-section';
222
- const expiryFieldLabel = document.createElement('div');
223
- expiryFieldLabel.className = 'card-field-label';
224
- expiryFieldLabel.textContent = 'Expires';
225
- this.#cardExpiryDisplay = document.createElement('div');
226
- this.#cardExpiryDisplay.className = 'card-expiry-display';
227
- this.#cardExpiryDisplay.textContent = 'MM/YY';
228
- expirySection.appendChild(expiryFieldLabel);
229
- expirySection.appendChild(this.#cardExpiryDisplay);
230
- bottomRow.appendChild(nameSection);
231
- bottomRow.appendChild(expirySection);
232
- front.appendChild(bottomRow);
233
- // Back face
234
- const back = document.createElement('div');
235
- back.className = 'card-face card-back';
236
- const strip = document.createElement('div');
237
- strip.className = 'card-strip';
238
- const cvcArea = document.createElement('div');
239
- cvcArea.className = 'card-cvc-area';
240
- const cvcAreaLabel = document.createElement('div');
241
- cvcAreaLabel.className = 'card-cvc-label';
242
- cvcAreaLabel.textContent = 'CVC';
243
- this.#cardCvcDisplay = document.createElement('div');
244
- this.#cardCvcDisplay.className = 'card-cvc-display';
245
- this.#cardCvcDisplay.textContent = '•••';
246
- cvcArea.appendChild(cvcAreaLabel);
247
- cvcArea.appendChild(this.#cardCvcDisplay);
248
- back.appendChild(strip);
249
- back.appendChild(cvcArea);
250
- this.#cardEl.appendChild(front);
251
- this.#cardEl.appendChild(back);
252
- scene.appendChild(this.#cardEl);
253
- // ── Input fields ──────────────────────────────────────────────────────────
254
- const fields = document.createElement('div');
255
- fields.className = 'card-fields';
256
- if (label) {
257
- const legend = document.createElement('div');
258
- legend.className = 'card-legend';
259
- legend.setAttribute('part', 'label');
260
- legend.textContent = this.sanitizeValue(label);
261
- fields.appendChild(legend);
262
- }
263
- // Card number
264
- const numberGroup = this.#makeFieldGroup(`${this.#instanceId}-number`);
265
- const numberLabel = numberGroup.querySelector('label');
266
- numberLabel.textContent = 'Card number';
267
- this.#numberInput = document.createElement('input');
268
- this.#numberInput.id = `${this.#instanceId}-number`;
269
- this.#numberInput.className = 'card-input card-number-input';
270
- this.#numberInput.setAttribute('type', 'text');
271
- this.#numberInput.setAttribute('inputmode', 'numeric');
272
- this.#numberInput.setAttribute('autocomplete', 'cc-number');
273
- this.#numberInput.setAttribute('placeholder', '0000 0000 0000 0000');
274
- this.#numberInput.setAttribute('maxlength', '23');
275
- this.#numberInput.setAttribute('aria-required', 'true');
276
- this.#numberInput.setAttribute('aria-describedby', `${this.#instanceId}-number-error`);
277
- this.#numberInput.setAttribute('part', 'number-input');
278
- if (disabled)
279
- this.#numberInput.disabled = true;
280
- this.#numberError = numberGroup.querySelector('.error-container');
281
- this.#numberError.id = `${this.#instanceId}-number-error`;
282
- numberGroup.querySelector('.input-wrapper').appendChild(this.#numberInput);
283
- fields.appendChild(numberGroup);
284
- // Expiry + CVC side by side
285
- const fieldRow = document.createElement('div');
286
- fieldRow.className = 'field-row';
287
- const expiryGroup = this.#makeFieldGroup(`${this.#instanceId}-expiry`);
288
- expiryGroup.querySelector('label').textContent = 'Expiry date';
289
- this.#expiryInput = document.createElement('input');
290
- this.#expiryInput.id = `${this.#instanceId}-expiry`;
291
- this.#expiryInput.className = 'card-input';
292
- this.#expiryInput.setAttribute('type', 'text');
293
- this.#expiryInput.setAttribute('inputmode', 'numeric');
294
- this.#expiryInput.setAttribute('autocomplete', 'cc-exp');
295
- this.#expiryInput.setAttribute('placeholder', 'MM/YY');
296
- this.#expiryInput.setAttribute('maxlength', '5');
297
- this.#expiryInput.setAttribute('aria-required', 'true');
298
- this.#expiryInput.setAttribute('aria-describedby', `${this.#instanceId}-expiry-error`);
299
- this.#expiryInput.setAttribute('part', 'expiry-input');
300
- if (disabled)
301
- this.#expiryInput.disabled = true;
302
- this.#expiryError = expiryGroup.querySelector('.error-container');
303
- this.#expiryError.id = `${this.#instanceId}-expiry-error`;
304
- expiryGroup.querySelector('.input-wrapper').appendChild(this.#expiryInput);
305
- const cvcGroup = this.#makeFieldGroup(`${this.#instanceId}-cvc`);
306
- cvcGroup.querySelector('label').textContent = 'Security code';
307
- this.#cvcInput = document.createElement('input');
308
- this.#cvcInput.id = `${this.#instanceId}-cvc`;
309
- this.#cvcInput.className = 'card-input cvc-input';
310
- // type=password: browser masks natively, avoids screen capture of CVC
311
- this.#cvcInput.setAttribute('type', 'password');
312
- this.#cvcInput.setAttribute('inputmode', 'numeric');
313
- this.#cvcInput.setAttribute('autocomplete', 'cc-csc');
314
- this.#cvcInput.setAttribute('placeholder', '•••');
315
- this.#cvcInput.setAttribute('maxlength', '4');
316
- this.#cvcInput.setAttribute('aria-required', 'true');
317
- this.#cvcInput.setAttribute('aria-describedby', `${this.#instanceId}-cvc-error`);
318
- this.#cvcInput.setAttribute('part', 'cvc-input');
319
- if (disabled)
320
- this.#cvcInput.disabled = true;
321
- this.#cvcError = cvcGroup.querySelector('.error-container');
322
- this.#cvcError.id = `${this.#instanceId}-cvc-error`;
323
- cvcGroup.querySelector('.input-wrapper').appendChild(this.#cvcInput);
324
- fieldRow.appendChild(expiryGroup);
325
- fieldRow.appendChild(cvcGroup);
326
- fields.appendChild(fieldRow);
327
- // Cardholder name (optional)
328
- const nameGroup = this.#makeFieldGroup(`${this.#instanceId}-name`);
329
- nameGroup.id = `${this.#instanceId}-name-group`;
330
- nameGroup.querySelector('label').textContent = 'Cardholder name';
331
- if (!showName)
332
- nameGroup.hidden = true;
333
- this.#nameInput = document.createElement('input');
334
- this.#nameInput.id = `${this.#instanceId}-name`;
335
- this.#nameInput.className = 'card-input';
336
- this.#nameInput.setAttribute('type', 'text');
337
- this.#nameInput.setAttribute('autocomplete', 'cc-name');
338
- this.#nameInput.setAttribute('placeholder', 'Name as it appears on card');
339
- this.#nameInput.setAttribute('spellcheck', 'false');
340
- this.#nameInput.setAttribute('aria-describedby', `${this.#instanceId}-name-error`);
341
- this.#nameInput.setAttribute('part', 'name-input');
342
- if (disabled)
343
- this.#nameInput.disabled = true;
344
- this.#nameError = nameGroup.querySelector('.error-container');
345
- this.#nameError.id = `${this.#instanceId}-name-error`;
346
- nameGroup.querySelector('.input-wrapper').appendChild(this.#nameInput);
347
- fields.appendChild(nameGroup);
348
- container.appendChild(scene);
349
- container.appendChild(fields);
350
- // ── Light DOM hidden inputs ───────────────────────────────────────────────
351
- this.#createHiddenInputs();
352
- // ── Component styles ──────────────────────────────────────────────────────
353
- this.addComponentStyles(new URL('./secure-card.css', import.meta.url).href);
354
- // ── Event listeners ───────────────────────────────────────────────────────
355
- // Telemetry hooks aggregate signals across all card inputs into one
356
- // composite behavioral fingerprint for the overall card interaction.
357
- this.#numberInput.addEventListener('input', (e) => { this.recordTelemetryInput(e); this.#handleNumberInput(e); });
358
- this.#numberInput.addEventListener('focus', () => { this.recordTelemetryFocus(); this.#handleNumberFocus(); });
359
- this.#numberInput.addEventListener('blur', () => { this.recordTelemetryBlur(); this.#handleNumberBlur(); });
360
- this.#expiryInput.addEventListener('input', (e) => { this.recordTelemetryInput(e); this.#handleExpiryInput(e); });
361
- this.#expiryInput.addEventListener('focus', () => { this.recordTelemetryFocus(); this.#flipCard(false); });
362
- this.#expiryInput.addEventListener('blur', () => { this.recordTelemetryBlur(); this.#handleExpiryBlur(); });
363
- this.#cvcInput.addEventListener('input', (e) => { this.recordTelemetryInput(e); this.#handleCvcInput(e); });
364
- this.#cvcInput.addEventListener('focus', () => { this.recordTelemetryFocus(); this.#flipCard(true); });
365
- this.#cvcInput.addEventListener('blur', () => { this.recordTelemetryBlur(); this.#handleCvcBlur(); });
366
- this.#nameInput.addEventListener('input', (e) => { this.recordTelemetryInput(e); this.#handleNameInput(e); });
367
- this.#nameInput.addEventListener('focus', () => { this.recordTelemetryFocus(); this.#flipCard(false); });
368
- this.#nameInput.addEventListener('blur', () => { this.recordTelemetryBlur(); this.#handleNameBlur(); });
369
- fragment.appendChild(container);
370
- return fragment;
371
- }
372
- // ── Field group builder ────────────────────────────────────────────────────
373
- #makeFieldGroup(id) {
374
- const group = document.createElement('div');
375
- group.className = 'field-group';
376
- const lbl = document.createElement('label');
377
- lbl.htmlFor = id;
378
- lbl.className = 'field-label';
379
- const wrapper = document.createElement('div');
380
- wrapper.className = 'input-wrapper';
381
- wrapper.setAttribute('part', 'wrapper');
382
- const err = document.createElement('div');
383
- err.className = 'error-container hidden';
384
- err.setAttribute('role', 'alert');
385
- err.setAttribute('part', 'error');
386
- group.appendChild(lbl);
387
- group.appendChild(wrapper);
388
- group.appendChild(err);
389
- return group;
390
- }
391
- // ── Attribute change handler ───────────────────────────────────────────────
392
- handleAttributeChange(name, _oldValue, newValue) {
393
- if (!this.shadowRoot)
394
- return;
395
- switch (name) {
396
- case 'disabled': {
397
- const d = newValue !== null;
398
- if (this.#numberInput)
399
- this.#numberInput.disabled = d;
400
- if (this.#expiryInput)
401
- this.#expiryInput.disabled = d;
402
- if (this.#cvcInput)
403
- this.#cvcInput.disabled = d;
404
- if (this.#nameInput)
405
- this.#nameInput.disabled = d;
406
- break;
407
- }
408
- case 'show-name': {
409
- const group = this.shadowRoot.querySelector(`#${this.#instanceId}-name-group`);
410
- if (group)
411
- group.hidden = newValue === null;
412
- break;
413
- }
414
- }
415
- }
416
- // ── Card number handlers ───────────────────────────────────────────────────
417
- #handleNumberInput(e) {
418
- const input = e.target;
419
- const digits = input.value.replace(/\D/g, '');
420
- const prevType = this.#cardTypeConfig?.type;
421
- this.#cardTypeConfig = detectCardType(digits);
422
- const format = this.#cardTypeConfig?.format ?? [4, 4, 4, 4];
423
- const maxLen = format.reduce((a, b) => a + b, 0);
424
- this.#cardDigits = digits.slice(0, maxLen);
425
- input.value = formatDigits(this.#cardDigits, format);
426
- // Update CVC length when card type changes
427
- if (this.#cardTypeConfig?.type !== prevType) {
428
- const maxWithSpaces = maxLen + format.length - 1;
429
- input.setAttribute('maxlength', String(maxWithSpaces));
430
- const cvcLen = this.#cardTypeConfig?.cvcLength ?? 3;
431
- if (this.#cvcInput) {
432
- this.#cvcInput.setAttribute('maxlength', String(cvcLen));
433
- }
434
- this.#updateCardType();
435
- }
436
- // Update card visual front face
437
- if (this.#cardNumberDisplay) {
438
- this.#cardNumberDisplay.textContent =
439
- input.value || formatDigits('•'.repeat(maxLen), format);
440
- }
441
- this.#clearError(this.#numberError);
442
- this.#syncHiddenInputs();
443
- this.#dispatchChangeEvent();
444
- }
445
- #handleNumberFocus() {
446
- // Restore formatted digits on focus (undo blur masking)
447
- if (this.#numberInput && this.#cardDigits) {
448
- const format = this.#cardTypeConfig?.format ?? [4, 4, 4, 4];
449
- const formatted = formatDigits(this.#cardDigits, format);
450
- this.#numberInput.value = formatted;
451
- if (this.#cardNumberDisplay)
452
- this.#cardNumberDisplay.textContent = formatted;
453
- }
454
- this.#flipCard(false);
455
- this.#clearError(this.#numberError);
456
- }
457
- #handleNumberBlur() {
458
- const rl = this.checkRateLimit();
459
- if (!rl.allowed) {
460
- this.#showError(this.#numberError, `Too many attempts. Try again in ${Math.ceil(rl.retryAfter / 1000)}s.`);
461
- return;
462
- }
463
- if (!this.#cardDigits) {
464
- this.#showError(this.#numberError, 'Card number is required');
465
- this.#numberInput?.setAttribute('aria-invalid', 'true');
466
- }
467
- else if (!luhnValid(this.#cardDigits)) {
468
- this.#showError(this.#numberError, 'Invalid card number');
469
- this.#numberInput?.setAttribute('aria-invalid', 'true');
470
- }
471
- else {
472
- this.#clearError(this.#numberError);
473
- this.#numberInput?.removeAttribute('aria-invalid');
474
- }
475
- // Mask middle digits on blur — last 4 remain visible
476
- this.#applyNumberMask();
477
- this.audit('card-number-blur', { cardType: this.#cardTypeConfig?.type ?? 'unknown' });
478
- }
479
- #applyNumberMask() {
480
- const format = this.#cardTypeConfig?.format ?? [4, 4, 4, 4];
481
- const masked = maskCardForDisplay(this.#cardDigits, format);
482
- if (this.#numberInput)
483
- this.#numberInput.value = masked;
484
- if (this.#cardNumberDisplay)
485
- this.#cardNumberDisplay.textContent = masked;
486
- }
487
- // ── Expiry handlers ───────────────────────────────────────────────────────
488
- #handleExpiryInput(e) {
489
- const input = e.target;
490
- const digits = input.value.replace(/\D/g, '').slice(0, 4);
491
- const formatted = digits.length > 2 ? `${digits.slice(0, 2)}/${digits.slice(2)}` : digits;
492
- input.value = formatted;
493
- this.#expiryValue = formatted;
494
- if (this.#cardExpiryDisplay) {
495
- this.#cardExpiryDisplay.textContent = formatted || 'MM/YY';
496
- }
497
- this.#clearError(this.#expiryError);
498
- this.#syncHiddenInputs();
499
- this.#dispatchChangeEvent();
500
- }
501
- #handleExpiryBlur() {
502
- const parts = this.#expiryValue.split('/');
503
- if (!this.#expiryValue) {
504
- this.#showError(this.#expiryError, 'Expiry date is required');
505
- this.#expiryInput?.setAttribute('aria-invalid', 'true');
506
- return;
507
- }
508
- const mm = parseInt(parts[0] ?? '', 10);
509
- const yy = parseInt(parts[1] ?? '', 10);
510
- if (isNaN(mm) || isNaN(yy) || mm < 1 || mm > 12 || (parts[1] ?? '').length < 2) {
511
- this.#showError(this.#expiryError, 'Enter a valid expiry date (MM/YY)');
512
- this.#expiryInput?.setAttribute('aria-invalid', 'true');
513
- }
514
- else if (!isExpiryValid(mm, yy)) {
515
- this.#showError(this.#expiryError, 'This card has expired');
516
- this.#expiryInput?.setAttribute('aria-invalid', 'true');
517
- }
518
- else {
519
- this.#clearError(this.#expiryError);
520
- this.#expiryInput?.removeAttribute('aria-invalid');
521
- }
522
- this.audit('card-expiry-blur', {});
523
- }
524
- // ── CVC handlers ──────────────────────────────────────────────────────────
525
- #handleCvcInput(e) {
526
- const input = e.target;
527
- const digits = input.value.replace(/\D/g, '');
528
- const maxLen = this.#cardTypeConfig?.cvcLength ?? 3;
529
- this.#cvcDigits = digits.slice(0, maxLen);
530
- input.value = this.#cvcDigits;
531
- // Card back shows bullet count matching typed length — value never shown
532
- if (this.#cardCvcDisplay) {
533
- this.#cardCvcDisplay.textContent =
534
- this.#cvcDigits.length > 0 ? '•'.repeat(this.#cvcDigits.length) : '•••';
535
- }
536
- this.#clearError(this.#cvcError);
537
- this.#dispatchChangeEvent();
538
- }
539
- #handleCvcBlur() {
540
- const maxLen = this.#cardTypeConfig?.cvcLength ?? 3;
541
- if (!this.#cvcDigits) {
542
- this.#showError(this.#cvcError, 'Security code is required');
543
- this.#cvcInput?.setAttribute('aria-invalid', 'true');
544
- }
545
- else if (this.#cvcDigits.length < maxLen) {
546
- this.#showError(this.#cvcError, `Security code must be ${maxLen} digits`);
547
- this.#cvcInput?.setAttribute('aria-invalid', 'true');
548
- }
549
- else {
550
- this.#clearError(this.#cvcError);
551
- this.#cvcInput?.removeAttribute('aria-invalid');
552
- }
553
- this.#flipCard(false);
554
- // CVC value is never audited
555
- this.audit('card-cvc-blur', {});
556
- }
557
- // ── Name handlers ─────────────────────────────────────────────────────────
558
- #handleNameInput(e) {
559
- const input = e.target;
560
- // Letters, spaces, hyphens, apostrophes only
561
- const sanitized = input.value.replace(/[^a-zA-Z\s\-']/g, '').slice(0, 50);
562
- input.value = sanitized;
563
- this.#cardholderName = sanitized;
564
- if (this.#cardNameDisplay) {
565
- this.#cardNameDisplay.textContent = sanitized.toUpperCase() || 'FULL NAME';
566
- }
567
- this.#clearError(this.#nameError);
568
- this.#syncHiddenInputs();
569
- this.#dispatchChangeEvent();
570
- }
571
- #handleNameBlur() {
572
- if (!this.hasAttribute('show-name'))
573
- return;
574
- if (!this.#cardholderName.trim()) {
575
- this.#showError(this.#nameError, 'Cardholder name is required');
576
- this.#nameInput?.setAttribute('aria-invalid', 'true');
577
- }
578
- else {
579
- this.#clearError(this.#nameError);
580
- this.#nameInput?.removeAttribute('aria-invalid');
581
- }
582
- }
583
- // ── Card visual ───────────────────────────────────────────────────────────
584
- #flipCard(toBack) {
585
- this.#cardEl?.classList.toggle('is-flipped', toBack);
586
- }
587
- #updateCardType() {
588
- if (!this.#cardEl)
589
- return;
590
- for (const ct of CARD_TYPES) {
591
- this.#cardEl.classList.remove(`card--${ct.type}`);
592
- }
593
- const type = this.#cardTypeConfig?.type;
594
- if (type)
595
- this.#cardEl.classList.add(`card--${type}`);
596
- if (this.#cardTypeLabel) {
597
- this.#cardTypeLabel.textContent = this.#cardTypeConfig?.label ?? '';
598
- }
599
- }
600
- // ── Error helpers ─────────────────────────────────────────────────────────
601
- #showError(container, message) {
602
- if (!container)
603
- return;
604
- container.textContent = '';
605
- const span = document.createElement('span');
606
- span.textContent = message;
607
- container.appendChild(span);
608
- container.classList.remove('hidden');
609
- }
610
- #clearError(container) {
611
- if (!container)
612
- return;
613
- container.textContent = '';
614
- container.classList.add('hidden');
615
- }
616
- // ── Hidden inputs for light DOM form participation ─────────────────────────
617
- #createHiddenInputs() {
618
- const fieldName = this.getAttribute('name');
619
- if (!fieldName)
620
- return;
621
- // Card number: stores last4 only — full PAN must never reach your server
622
- this.#hiddenNumber = document.createElement('input');
623
- this.#hiddenNumber.type = 'hidden';
624
- this.#hiddenNumber.name = fieldName;
625
- this.appendChild(this.#hiddenNumber);
626
- this.#hiddenExpiry = document.createElement('input');
627
- this.#hiddenExpiry.type = 'hidden';
628
- this.#hiddenExpiry.name = `${fieldName}-expiry`;
629
- this.appendChild(this.#hiddenExpiry);
630
- this.#hiddenName = document.createElement('input');
631
- this.#hiddenName.type = 'hidden';
632
- this.#hiddenName.name = `${fieldName}-holder`;
633
- this.appendChild(this.#hiddenName);
634
- // No hidden input for CVC — never submit CVC to your own server
635
- }
636
- #syncHiddenInputs() {
637
- if (this.#hiddenNumber)
638
- this.#hiddenNumber.value = this.#cardDigits.slice(-4);
639
- if (this.#hiddenExpiry)
640
- this.#hiddenExpiry.value = this.#expiryValue;
641
- if (this.#hiddenName)
642
- this.#hiddenName.value = this.#cardholderName;
643
- }
644
- // ── Event dispatch ────────────────────────────────────────────────────────
645
- #dispatchChangeEvent() {
646
- const [rawMonth, rawYear] = this.#expiryValue.split('/');
647
- this.dispatchEvent(new CustomEvent('secure-card', {
648
- bubbles: true,
649
- composed: true,
650
- detail: {
651
- name: this.getAttribute('name') ?? '',
652
- cardType: this.#cardTypeConfig?.type ?? 'unknown',
653
- last4: this.#cardDigits.slice(-4),
654
- expiryMonth: parseInt(rawMonth ?? '0', 10) || 0,
655
- expiryYear: parseInt(rawYear ?? '0', 10) || 0,
656
- cardholderName: this.#cardholderName,
657
- valid: this.valid,
658
- tier: this.securityTier,
659
- // Full PAN and CVC are intentionally absent — use getCardData() for SDK calls
660
- },
661
- }));
662
- }
663
- // ── Lifecycle ─────────────────────────────────────────────────────────────
664
- disconnectedCallback() {
665
- // Wipe all sensitive state from memory
666
- this.#cardDigits = '';
667
- this.#cvcDigits = '';
668
- this.#expiryValue = '';
669
- this.#cardholderName = '';
670
- if (this.#numberInput)
671
- this.#numberInput.value = '';
672
- if (this.#expiryInput)
673
- this.#expiryInput.value = '';
674
- if (this.#cvcInput)
675
- this.#cvcInput.value = '';
676
- if (this.#nameInput)
677
- this.#nameInput.value = '';
678
- if (this.#hiddenNumber)
679
- this.#hiddenNumber.value = '';
680
- if (this.#hiddenExpiry)
681
- this.#hiddenExpiry.value = '';
682
- if (this.#hiddenName)
683
- this.#hiddenName.value = '';
684
- super.disconnectedCallback();
685
- }
686
- // ── Public API ────────────────────────────────────────────────────────────
687
- /**
688
- * True when all visible, required fields pass validation.
689
- */
690
- get valid() {
691
- const numberOk = this.#cardDigits.length >= 12 && luhnValid(this.#cardDigits);
692
- const [mm, yy] = this.#expiryValue.split('/');
693
- const month = parseInt(mm ?? '', 10);
694
- const year = parseInt(yy ?? '', 10);
695
- const expiryOk = !isNaN(month) &&
696
- !isNaN(year) &&
697
- month >= 1 &&
698
- month <= 12 &&
699
- (yy ?? '').length >= 2 &&
700
- isExpiryValid(month, year);
701
- const cvcLen = this.#cardTypeConfig?.cvcLength ?? 3;
702
- const cvcOk = this.#cvcDigits.length === cvcLen;
703
- const nameOk = !this.hasAttribute('show-name') || this.#cardholderName.trim().length > 0;
704
- return numberOk && expiryOk && cvcOk && nameOk;
705
- }
706
- /**
707
- * Card type detected from the entered number prefix.
708
- */
709
- get cardType() {
710
- return this.#cardTypeConfig?.type ?? 'unknown';
711
- }
712
- /**
713
- * Last 4 digits of the entered card number. Safe to display and log.
714
- */
715
- get last4() {
716
- return this.#cardDigits.slice(-4);
717
- }
718
- /**
719
- * The name attribute value.
720
- */
721
- get name() {
722
- return this.getAttribute('name') ?? '';
723
- }
724
- /**
725
- * Returns raw card data for immediate handoff to a payment SDK tokeniser.
726
- *
727
- * SECURITY: Pass this data only to a PCI-compliant processor's client SDK
728
- * (e.g. Stripe.js createToken, Braintree tokenizeCard). Never send raw card
729
- * numbers or CVCs to your own server.
730
- *
731
- * Returns null if the form is not yet valid.
732
- */
733
- getCardData() {
734
- if (!this.valid)
735
- return null;
736
- return {
737
- number: this.#cardDigits,
738
- expiry: this.#expiryValue,
739
- cvc: this.#cvcDigits,
740
- name: this.#cardholderName,
741
- };
742
- }
743
- /**
744
- * Clears all fields and resets component state.
745
- */
746
- reset() {
747
- this.#cardDigits = '';
748
- this.#cvcDigits = '';
749
- this.#expiryValue = '';
750
- this.#cardholderName = '';
751
- this.#cardTypeConfig = null;
752
- if (this.#numberInput)
753
- this.#numberInput.value = '';
754
- if (this.#expiryInput)
755
- this.#expiryInput.value = '';
756
- if (this.#cvcInput)
757
- this.#cvcInput.value = '';
758
- if (this.#nameInput)
759
- this.#nameInput.value = '';
760
- this.#syncHiddenInputs();
761
- this.#flipCard(false);
762
- this.#updateCardType();
763
- const defaultFormat = [4, 4, 4, 4];
764
- if (this.#cardNumberDisplay) {
765
- this.#cardNumberDisplay.textContent = formatDigits('•'.repeat(16), defaultFormat);
766
- }
767
- if (this.#cardExpiryDisplay)
768
- this.#cardExpiryDisplay.textContent = 'MM/YY';
769
- if (this.#cardNameDisplay)
770
- this.#cardNameDisplay.textContent = 'FULL NAME';
771
- if (this.#cardCvcDisplay)
772
- this.#cardCvcDisplay.textContent = '•••';
773
- this.#clearError(this.#numberError);
774
- this.#clearError(this.#expiryError);
775
- this.#clearError(this.#cvcError);
776
- this.#clearError(this.#nameError);
777
- this.audit('card-reset', {});
778
- }
779
- /**
780
- * Focuses the card number input.
781
- */
782
- focus() {
783
- this.#numberInput?.focus();
784
- }
785
- }
786
- customElements.define('secure-card', SecureCard);
787
- //# sourceMappingURL=secure-card.js.map
22
+ */import{SecureBaseComponent as k}from"../../core/base-component.js";const $=Object.freeze([{type:"amex",pattern:/^3[47]/,format:[4,6,5],cvcLength:4,lengths:[15],label:"Amex"},{type:"diners",pattern:/^3(?:0[0-5]|[68])/,format:[4,6,4],cvcLength:3,lengths:[14],label:"Diners"},{type:"discover",pattern:/^6(?:011|5[0-9]{2})/,format:[4,4,4,4],cvcLength:3,lengths:[16],label:"Discover"},{type:"jcb",pattern:/^(?:2131|1800|35\d{3})/,format:[4,4,4,4],cvcLength:3,lengths:[16],label:"JCB"},{type:"mastercard",pattern:/^(?:5[1-5]|2[2-7])/,format:[4,4,4,4],cvcLength:3,lengths:[16],label:"Mastercard"},{type:"visa",pattern:/^4/,format:[4,4,4,4],cvcLength:3,lengths:[13,16,19],label:"Visa"}]);function M(h){return h?$.find(t=>t.pattern.test(h))??null:null}function o(h,t){let e="",i=0;for(let s=0;s<t.length;s++){const r=h.slice(i,i+t[s]);if(!r)break;s>0&&(e+=" "),e+=r,i+=t[s]}return e}function q(h,t){const e=t.reduce((n,l)=>n+l,0);if(!h)return o("\u2022".repeat(e),t);const i=h.padEnd(e,"\u2022").slice(0,e),s=Math.max(0,i.length-4),r="\u2022".repeat(s)+i.slice(s);return o(r,t)}function S(h){if(!h||h.length<12)return!1;let t=0,e=!1;for(let i=h.length-1;i>=0;i--){const s=h[i];if(!s)return!1;const r=parseInt(s,10);if(isNaN(r))return!1;let n=r;e&&(n*=2,n>9&&(n-=9)),t+=n,e=!e}return t%10===0}function T(h,t){const e=new Date,i=e.getFullYear()%100,s=e.getMonth()+1;return!(t<i||t===i&&h<s)}let D=0;class F extends k{#t=null;#i=null;#e=null;#s=null;#d=null;#l=null;#m=null;#b=null;#L=null;#f=null;#u=null;#v=null;#E=null;#A=null;#y=null;#C=null;#g=null;#a="";#c="";#o="";#x="";#n=null;#r;constructor(){super(),this.#r=`secure-card-${++D}`}static get observedAttributes(){return[...super.observedAttributes,"name","label","show-name"]}render(){const t=document.createDocumentFragment(),e=this.hasAttribute("disabled"),i=this.hasAttribute("show-name"),s=this.getAttribute("label")??"",r=document.createElement("div");r.className="card-container",r.setAttribute("part","container");const n=document.createElement("div");n.className="card-scene",n.setAttribute("aria-hidden","true"),this.#d=document.createElement("div"),this.#d.className="card";const l=document.createElement("div");l.className="card-face card-front";const c=document.createElement("div");c.className="card-top-row";const p=document.createElement("div");p.className="card-chip",this.#L=document.createElement("div"),this.#L.className="card-type-label",c.appendChild(p),c.appendChild(this.#L),l.appendChild(c),this.#l=document.createElement("div"),this.#l.className="card-number-display",this.#l.textContent="\u2022\u2022\u2022\u2022 \u2022\u2022\u2022\u2022 \u2022\u2022\u2022\u2022 \u2022\u2022\u2022\u2022",l.appendChild(this.#l);const m=document.createElement("div");m.className="card-bottom-row";const b=document.createElement("div");b.className="card-name-section";const A=document.createElement("div");A.className="card-field-label",A.textContent="Card holder",this.#b=document.createElement("div"),this.#b.className="card-name-display",this.#b.textContent="FULL NAME",b.appendChild(A),b.appendChild(this.#b);const f=document.createElement("div");f.className="card-expiry-section";const N=document.createElement("div");N.className="card-field-label",N.textContent="Expires",this.#m=document.createElement("div"),this.#m.className="card-expiry-display",this.#m.textContent="MM/YY",f.appendChild(N),f.appendChild(this.#m),m.appendChild(b),m.appendChild(f),l.appendChild(m);const v=document.createElement("div");v.className="card-face card-back";const w=document.createElement("div");w.className="card-strip";const y=document.createElement("div");y.className="card-cvc-area";const L=document.createElement("div");L.className="card-cvc-label",L.textContent="CVC",this.#f=document.createElement("div"),this.#f.className="card-cvc-display",this.#f.textContent="\u2022\u2022\u2022",y.appendChild(L),y.appendChild(this.#f),v.appendChild(w),v.appendChild(y),this.#d.appendChild(l),this.#d.appendChild(v),n.appendChild(this.#d);const d=document.createElement("div");if(d.className="card-fields",s){const a=document.createElement("div");a.className="card-legend",a.setAttribute("part","label"),a.textContent=this.sanitizeValue(s),d.appendChild(a)}const C=this.#w(`${this.#r}-number`),I=C.querySelector("label");I.textContent="Card number",this.#t=document.createElement("input"),this.#t.id=`${this.#r}-number`,this.#t.className="card-input card-number-input",this.#t.setAttribute("type","text"),this.#t.setAttribute("inputmode","numeric"),this.#t.setAttribute("autocomplete","cc-number"),this.#t.setAttribute("placeholder","0000 0000 0000 0000"),this.#t.setAttribute("maxlength","23"),this.#t.setAttribute("aria-required","true"),this.#t.setAttribute("aria-describedby",`${this.#r}-number-error`),this.#t.setAttribute("part","number-input"),e&&(this.#t.disabled=!0),this.#u=C.querySelector(".error-container"),this.#u.id=`${this.#r}-number-error`,C.querySelector(".input-wrapper").appendChild(this.#t),d.appendChild(C);const g=document.createElement("div");g.className="field-row";const x=this.#w(`${this.#r}-expiry`);x.querySelector("label").textContent="Expiry date",this.#i=document.createElement("input"),this.#i.id=`${this.#r}-expiry`,this.#i.className="card-input",this.#i.setAttribute("type","text"),this.#i.setAttribute("inputmode","numeric"),this.#i.setAttribute("autocomplete","cc-exp"),this.#i.setAttribute("placeholder","MM/YY"),this.#i.setAttribute("maxlength","5"),this.#i.setAttribute("aria-required","true"),this.#i.setAttribute("aria-describedby",`${this.#r}-expiry-error`),this.#i.setAttribute("part","expiry-input"),e&&(this.#i.disabled=!0),this.#v=x.querySelector(".error-container"),this.#v.id=`${this.#r}-expiry-error`,x.querySelector(".input-wrapper").appendChild(this.#i);const E=this.#w(`${this.#r}-cvc`);E.querySelector("label").textContent="Security code",this.#e=document.createElement("input"),this.#e.id=`${this.#r}-cvc`,this.#e.className="card-input cvc-input",this.#e.setAttribute("type","password"),this.#e.setAttribute("inputmode","numeric"),this.#e.setAttribute("autocomplete","cc-csc"),this.#e.setAttribute("placeholder","\u2022\u2022\u2022"),this.#e.setAttribute("maxlength","4"),this.#e.setAttribute("aria-required","true"),this.#e.setAttribute("aria-describedby",`${this.#r}-cvc-error`),this.#e.setAttribute("part","cvc-input"),e&&(this.#e.disabled=!0),this.#E=E.querySelector(".error-container"),this.#E.id=`${this.#r}-cvc-error`,E.querySelector(".input-wrapper").appendChild(this.#e),g.appendChild(x),g.appendChild(E),d.appendChild(g);const u=this.#w(`${this.#r}-name`);return u.id=`${this.#r}-name-group`,u.querySelector("label").textContent="Cardholder name",i||(u.hidden=!0),this.#s=document.createElement("input"),this.#s.id=`${this.#r}-name`,this.#s.className="card-input",this.#s.setAttribute("type","text"),this.#s.setAttribute("autocomplete","cc-name"),this.#s.setAttribute("placeholder","Name as it appears on card"),this.#s.setAttribute("spellcheck","false"),this.#s.setAttribute("aria-describedby",`${this.#r}-name-error`),this.#s.setAttribute("part","name-input"),e&&(this.#s.disabled=!0),this.#A=u.querySelector(".error-container"),this.#A.id=`${this.#r}-name-error`,u.querySelector(".input-wrapper").appendChild(this.#s),d.appendChild(u),r.appendChild(n),r.appendChild(d),this.#G(),this.addComponentStyles(new URL("./secure-card.css",import.meta.url).href),this.#t.addEventListener("input",a=>{this.recordTelemetryInput(a),this.#I(a)}),this.#t.addEventListener("focus",()=>{this.recordTelemetryFocus(),this.#k()}),this.#t.addEventListener("blur",()=>{this.recordTelemetryBlur(),this.#M()}),this.#i.addEventListener("input",a=>{this.recordTelemetryInput(a),this.#D(a)}),this.#i.addEventListener("focus",()=>{this.recordTelemetryFocus(),this.#N(!1)}),this.#i.addEventListener("blur",()=>{this.recordTelemetryBlur(),this.#F()}),this.#e.addEventListener("input",a=>{this.recordTelemetryInput(a),this.#Y(a)}),this.#e.addEventListener("focus",()=>{this.recordTelemetryFocus(),this.#N(!0)}),this.#e.addEventListener("blur",()=>{this.recordTelemetryBlur(),this.#B()}),this.#s.addEventListener("input",a=>{this.recordTelemetryInput(a),this.#R(a)}),this.#s.addEventListener("focus",()=>{this.recordTelemetryFocus(),this.#N(!1)}),this.#s.addEventListener("blur",()=>{this.recordTelemetryBlur(),this.#V()}),t.appendChild(r),t}#w(t){const e=document.createElement("div");e.className="field-group";const i=document.createElement("label");i.htmlFor=t,i.className="field-label";const s=document.createElement("div");s.className="input-wrapper",s.setAttribute("part","wrapper");const r=document.createElement("div");return r.className="error-container hidden",r.setAttribute("role","alert"),r.setAttribute("part","error"),e.appendChild(i),e.appendChild(s),e.appendChild(r),e}handleAttributeChange(t,e,i){if(this.shadowRoot)switch(t){case"disabled":{const s=i!==null;this.#t&&(this.#t.disabled=s),this.#i&&(this.#i.disabled=s),this.#e&&(this.#e.disabled=s),this.#s&&(this.#s.disabled=s);break}case"show-name":{const s=this.shadowRoot.querySelector(`#${this.#r}-name-group`);s&&(s.hidden=i===null);break}}}#I(t){const e=t.target,i=e.value.replace(/\D/g,""),s=this.#n?.type;this.#n=M(i);const r=this.#n?.format??[4,4,4,4],n=r.reduce((l,c)=>l+c,0);if(this.#a=i.slice(0,n),e.value=o(this.#a,r),this.#n?.type!==s){const l=n+r.length-1;e.setAttribute("maxlength",String(l));const c=this.#n?.cvcLength??3;this.#e&&this.#e.setAttribute("maxlength",String(c)),this.#T()}this.#l&&(this.#l.textContent=e.value||o("\u2022".repeat(n),r)),this.#h(this.#u),this.#$(),this.#S()}#k(){if(this.#t&&this.#a){const t=this.#n?.format??[4,4,4,4],e=o(this.#a,t);this.#t.value=e,this.#l&&(this.#l.textContent=e)}this.#N(!1),this.#h(this.#u)}#M(){const t=this.checkRateLimit();if(!t.allowed){this.#p(this.#u,`Too many attempts. Try again in ${Math.ceil(t.retryAfter/1e3)}s.`);return}this.#a?S(this.#a)?(this.#h(this.#u),this.#t?.removeAttribute("aria-invalid")):(this.#p(this.#u,"Invalid card number"),this.#t?.setAttribute("aria-invalid","true")):(this.#p(this.#u,"Card number is required"),this.#t?.setAttribute("aria-invalid","true")),this.#q(),this.audit("card-number-blur",{cardType:this.#n?.type??"unknown"})}#q(){const t=this.#n?.format??[4,4,4,4],e=q(this.#a,t);this.#t&&(this.#t.value=e),this.#l&&(this.#l.textContent=e)}#D(t){const e=t.target,i=e.value.replace(/\D/g,"").slice(0,4),s=i.length>2?`${i.slice(0,2)}/${i.slice(2)}`:i;e.value=s,this.#o=s,this.#m&&(this.#m.textContent=s||"MM/YY"),this.#h(this.#v),this.#$(),this.#S()}#F(){const t=this.#o.split("/");if(!this.#o){this.#p(this.#v,"Expiry date is required"),this.#i?.setAttribute("aria-invalid","true");return}const e=parseInt(t[0]??"",10),i=parseInt(t[1]??"",10);isNaN(e)||isNaN(i)||e<1||e>12||(t[1]??"").length<2?(this.#p(this.#v,"Enter a valid expiry date (MM/YY)"),this.#i?.setAttribute("aria-invalid","true")):T(e,i)?(this.#h(this.#v),this.#i?.removeAttribute("aria-invalid")):(this.#p(this.#v,"This card has expired"),this.#i?.setAttribute("aria-invalid","true")),this.audit("card-expiry-blur",{})}#Y(t){const e=t.target,i=e.value.replace(/\D/g,""),s=this.#n?.cvcLength??3;this.#c=i.slice(0,s),e.value=this.#c,this.#f&&(this.#f.textContent=this.#c.length>0?"\u2022".repeat(this.#c.length):"\u2022\u2022\u2022"),this.#h(this.#E),this.#S()}#B(){const t=this.#n?.cvcLength??3;this.#c?this.#c.length<t?(this.#p(this.#E,`Security code must be ${t} digits`),this.#e?.setAttribute("aria-invalid","true")):(this.#h(this.#E),this.#e?.removeAttribute("aria-invalid")):(this.#p(this.#E,"Security code is required"),this.#e?.setAttribute("aria-invalid","true")),this.#N(!1),this.audit("card-cvc-blur",{})}#R(t){const e=t.target,i=e.value.replace(/[^a-zA-Z\s\-']/g,"").slice(0,50);e.value=i,this.#x=i,this.#b&&(this.#b.textContent=i.toUpperCase()||"FULL NAME"),this.#h(this.#A),this.#$(),this.#S()}#V(){this.hasAttribute("show-name")&&(this.#x.trim()?(this.#h(this.#A),this.#s?.removeAttribute("aria-invalid")):(this.#p(this.#A,"Cardholder name is required"),this.#s?.setAttribute("aria-invalid","true")))}#N(t){this.#d?.classList.toggle("is-flipped",t)}#T(){if(!this.#d)return;for(const e of $)this.#d.classList.remove(`card--${e.type}`);const t=this.#n?.type;t&&this.#d.classList.add(`card--${t}`),this.#L&&(this.#L.textContent=this.#n?.label??"")}#p(t,e){if(!t)return;t.textContent="";const i=document.createElement("span");i.textContent=e,t.appendChild(i),t.classList.remove("hidden")}#h(t){t&&(t.textContent="",t.classList.add("hidden"))}#G(){const t=this.getAttribute("name");t&&(this.#y=document.createElement("input"),this.#y.type="hidden",this.#y.name=t,this.appendChild(this.#y),this.#C=document.createElement("input"),this.#C.type="hidden",this.#C.name=`${t}-expiry`,this.appendChild(this.#C),this.#g=document.createElement("input"),this.#g.type="hidden",this.#g.name=`${t}-holder`,this.appendChild(this.#g))}#$(){this.#y&&(this.#y.value=this.#a.slice(-4)),this.#C&&(this.#C.value=this.#o),this.#g&&(this.#g.value=this.#x)}#S(){const[t,e]=this.#o.split("/");this.dispatchEvent(new CustomEvent("secure-card",{bubbles:!0,composed:!0,detail:{name:this.getAttribute("name")??"",cardType:this.#n?.type??"unknown",last4:this.#a.slice(-4),expiryMonth:parseInt(t??"0",10)||0,expiryYear:parseInt(e??"0",10)||0,cardholderName:this.#x,valid:this.valid,tier:this.securityTier}}))}disconnectedCallback(){this.#a="",this.#c="",this.#o="",this.#x="",this.#t&&(this.#t.value=""),this.#i&&(this.#i.value=""),this.#e&&(this.#e.value=""),this.#s&&(this.#s.value=""),this.#y&&(this.#y.value=""),this.#C&&(this.#C.value=""),this.#g&&(this.#g.value=""),super.disconnectedCallback()}get valid(){const t=this.#a.length>=12&&S(this.#a),[e,i]=this.#o.split("/"),s=parseInt(e??"",10),r=parseInt(i??"",10),n=!isNaN(s)&&!isNaN(r)&&s>=1&&s<=12&&(i??"").length>=2&&T(s,r),l=this.#n?.cvcLength??3,c=this.#c.length===l,p=!this.hasAttribute("show-name")||this.#x.trim().length>0;return t&&n&&c&&p}get cardType(){return this.#n?.type??"unknown"}get last4(){return this.#a.slice(-4)}get name(){return this.getAttribute("name")??""}getCardData(){return this.valid?{number:this.#a,expiry:this.#o,cvc:this.#c,name:this.#x}:null}reset(){this.#a="",this.#c="",this.#o="",this.#x="",this.#n=null,this.#t&&(this.#t.value=""),this.#i&&(this.#i.value=""),this.#e&&(this.#e.value=""),this.#s&&(this.#s.value=""),this.#$(),this.#N(!1),this.#T();const t=[4,4,4,4];this.#l&&(this.#l.textContent=o("\u2022".repeat(16),t)),this.#m&&(this.#m.textContent="MM/YY"),this.#b&&(this.#b.textContent="FULL NAME"),this.#f&&(this.#f.textContent="\u2022\u2022\u2022"),this.#h(this.#u),this.#h(this.#v),this.#h(this.#E),this.#h(this.#A),this.audit("card-reset",{})}focus(){this.#t?.focus()}}customElements.define("secure-card",F);export{F as SecureCard};