secure-ui-components 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +368 -38
- package/dist/components/secure-card/secure-card.css +427 -0
- package/dist/components/secure-card/secure-card.d.ts +73 -0
- package/dist/components/secure-card/secure-card.d.ts.map +1 -0
- package/dist/components/secure-card/secure-card.js +787 -0
- package/dist/components/secure-card/secure-card.js.map +1 -0
- package/dist/components/secure-datetime/secure-datetime.d.ts.map +1 -1
- package/dist/components/secure-datetime/secure-datetime.js +6 -19
- package/dist/components/secure-datetime/secure-datetime.js.map +1 -1
- package/dist/components/secure-file-upload/secure-file-upload.d.ts.map +1 -1
- package/dist/components/secure-file-upload/secure-file-upload.js +0 -16
- package/dist/components/secure-file-upload/secure-file-upload.js.map +1 -1
- package/dist/components/secure-form/secure-form.d.ts.map +1 -1
- package/dist/components/secure-form/secure-form.js +126 -6
- package/dist/components/secure-form/secure-form.js.map +1 -1
- package/dist/components/secure-input/secure-input.d.ts.map +1 -1
- package/dist/components/secure-input/secure-input.js +6 -21
- package/dist/components/secure-input/secure-input.js.map +1 -1
- package/dist/components/secure-select/secure-select.d.ts.map +1 -1
- package/dist/components/secure-select/secure-select.js +6 -19
- package/dist/components/secure-select/secure-select.js.map +1 -1
- package/dist/components/secure-telemetry-provider/secure-telemetry-provider.d.ts +63 -0
- package/dist/components/secure-telemetry-provider/secure-telemetry-provider.d.ts.map +1 -0
- package/dist/components/secure-telemetry-provider/secure-telemetry-provider.js +198 -0
- package/dist/components/secure-telemetry-provider/secure-telemetry-provider.js.map +1 -0
- package/dist/components/secure-textarea/secure-textarea.d.ts.map +1 -1
- package/dist/components/secure-textarea/secure-textarea.js +6 -19
- package/dist/components/secure-textarea/secure-textarea.js.map +1 -1
- package/dist/core/base-component.d.ts +23 -1
- package/dist/core/base-component.d.ts.map +1 -1
- package/dist/core/base-component.js +111 -0
- package/dist/core/base-component.js.map +1 -1
- package/dist/core/security-config.d.ts.map +1 -1
- package/dist/core/security-config.js +5 -21
- package/dist/core/security-config.js.map +1 -1
- package/dist/core/types.d.ts +129 -8
- package/dist/core/types.d.ts.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/package.json +4 -2
- package/package.json +13 -1
|
@@ -0,0 +1,787 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview SecureCard — composite credit card input Web Component
|
|
3
|
+
*
|
|
4
|
+
* Renders card number, expiry date, CVC, and optional cardholder name
|
|
5
|
+
* fields inside a closed Shadow DOM with a live 3D card preview.
|
|
6
|
+
*
|
|
7
|
+
* Security model:
|
|
8
|
+
* - Full PAN is never included in events, audit logs, or hidden inputs
|
|
9
|
+
* - CVC is never in events, hidden inputs, or audit logs
|
|
10
|
+
* - Card number masked to last-4 on blur
|
|
11
|
+
* - Security tier is locked to CRITICAL (immutable, fail-secure default)
|
|
12
|
+
* - Luhn validation on card number
|
|
13
|
+
* - Rate limiting and audit logging via SecureBaseComponent
|
|
14
|
+
* - All sensitive fields cleared on disconnectedCallback
|
|
15
|
+
*
|
|
16
|
+
* PCI note: These fields provide a secure UI layer only. Raw card data
|
|
17
|
+
* must be passed to a PCI-compliant payment processor's SDK — never to
|
|
18
|
+
* your own server. Use getCardData() exclusively for SDK tokenisation.
|
|
19
|
+
*
|
|
20
|
+
* @module secure-card
|
|
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
|