verant_id_cloud_scan 1.4.4-beta.0 → 1.4.4-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,499 @@
1
+ import { dmvCheck } from './Api.js';
2
+ import { FIELD_MAPPING, getFieldValue as getFieldValueFromMapping, FIELD_LABELS } from './FieldMapping.js';
3
+ import { formatDateToDisplay } from './DateUtils.js';
4
+
5
+ /**
6
+ * VerificationModal - DMV Verification Modal Controller
7
+ *
8
+ * Creates and manages a modal dialog that prompts users to verify their ID
9
+ * against DMV records. Handles confirmation, loading, results, and error states.
10
+ *
11
+ * @class VerificationModal
12
+ */
13
+ export class VerificationModal {
14
+ /**
15
+ * Creates a new VerificationModal instance
16
+ * @param {string} clientId - Client ID for DMV verification
17
+ * @param {string} scannerType - Type of scanner used (e.g., 'DigitalCheck', 'VerantId6S')
18
+ * @param {Object} licenseData - Barcode data from scanned license
19
+ * @param {Object} options - Configuration options
20
+ * @param {string} options.primaryColor - Theme color (default: '#0074CB')
21
+ * @param {HTMLElement} options.container - Mount point for modal (default: document.body)
22
+ */
23
+ constructor(clientId, scannerType, licenseData, options = {}) {
24
+ this.clientId = clientId;
25
+ this.scannerType = scannerType;
26
+ this.licenseData = licenseData;
27
+ this.primaryColor = options.primaryColor || '#0074CB';
28
+ this.container = options.container || document.body;
29
+ this.modalRoot = null;
30
+ this.resolve = null;
31
+ this.reject = null;
32
+
33
+ // Automatically inject CSS if not already present
34
+ this.injectCSS();
35
+ }
36
+
37
+ /**
38
+ * Injects the modal CSS into the document if not already present
39
+ * @private
40
+ */
41
+ injectCSS() {
42
+ // Check if CSS is already injected
43
+ if (document.getElementById('verant-dmv-modal-styles')) {
44
+ return;
45
+ }
46
+
47
+ // Try to load external CSS file first
48
+ const cssLink = document.createElement('link');
49
+ cssLink.id = 'verant-dmv-modal-styles';
50
+ cssLink.rel = 'stylesheet';
51
+
52
+ // Try to determine the correct path to the CSS file
53
+ // First, check if it's in the same directory as this script
54
+ const scriptPath = new URL(import.meta.url).pathname;
55
+ const scriptDir = scriptPath.substring(0, scriptPath.lastIndexOf('/'));
56
+ cssLink.href = scriptDir + '/dmv-modal.css';
57
+
58
+ document.head.appendChild(cssLink);
59
+ }
60
+
61
+ /**
62
+ * Opens the modal and returns a Promise that resolves when user completes/cancels
63
+ * @returns {Promise<Object>} Result object with status and optional DMV data
64
+ */
65
+ async open() {
66
+ return new Promise((resolve, reject) => {
67
+ this.resolve = resolve;
68
+ this.reject = reject;
69
+ this.render();
70
+ this.showConfirmation();
71
+ });
72
+ }
73
+
74
+ /**
75
+ * Renders the modal structure and attaches event listeners
76
+ * @private
77
+ */
78
+ render() {
79
+ // Create overlay + modal structure
80
+ this.modalRoot = document.createElement('div');
81
+ this.modalRoot.className = 'verant-dmv-overlay';
82
+ this.modalRoot.innerHTML = `
83
+ <div class="verant-dmv-modal" role="dialog" aria-modal="true" aria-labelledby="dmv-modal-title">
84
+ <div class="verant-dmv-header" style="background-color: ${this.primaryColor}">
85
+ <h3 id="dmv-modal-title">DMV Verification</h3>
86
+ <button class="verant-dmv-close" aria-label="Close">&times;</button>
87
+ </div>
88
+ <div class="verant-dmv-body">
89
+ </div>
90
+ </div>
91
+ `;
92
+
93
+ // Close on X button
94
+ this.modalRoot.querySelector('.verant-dmv-close').addEventListener('click', () => this.cancel());
95
+
96
+ // Close on overlay click
97
+ this.modalRoot.addEventListener('click', (e) => {
98
+ if (e.target === this.modalRoot) this.cancel();
99
+ });
100
+
101
+ // Keyboard: ESC to close
102
+ this.handleKeydown = (e) => {
103
+ if (e.key === 'Escape') this.cancel();
104
+ };
105
+ document.addEventListener('keydown', this.handleKeydown);
106
+
107
+ this.container.appendChild(this.modalRoot);
108
+
109
+ // Focus trap - focus first button
110
+ setTimeout(() => {
111
+ const firstButton = this.modalRoot.querySelector('button:not(.verant-dmv-close)');
112
+ if (firstButton) firstButton.focus();
113
+ }, 100);
114
+ }
115
+
116
+ /**
117
+ * Shows the confirmation screen asking user if they want to verify
118
+ * @private
119
+ */
120
+ showConfirmation() {
121
+ const body = this.modalRoot.querySelector('.verant-dmv-body');
122
+
123
+ // Extract all driver data fields using shared field mapping
124
+ const firstName = getFieldValueFromMapping(this.licenseData, 'first_name') || '';
125
+ const lastName = getFieldValueFromMapping(this.licenseData, 'last_name') || '';
126
+ const licenseNumber = getFieldValueFromMapping(this.licenseData, 'license_number') || '';
127
+ const state = getFieldValueFromMapping(this.licenseData, 'state') || '';
128
+ const birthDate = formatDateToDisplay(getFieldValueFromMapping(this.licenseData, 'birth_date') || '');
129
+ const issueDate = formatDateToDisplay(getFieldValueFromMapping(this.licenseData, 'issue_date') || '');
130
+ const expirationDate = formatDateToDisplay(getFieldValueFromMapping(this.licenseData, 'expiration_date') || '');
131
+ const address = getFieldValueFromMapping(this.licenseData, 'address_line_1') || '';
132
+ const city = getFieldValueFromMapping(this.licenseData, 'city') || '';
133
+ const postalCode = getFieldValueFromMapping(this.licenseData, 'postal_code') || '';
134
+ const sex = getFieldValueFromMapping(this.licenseData, 'sex_code') || '';
135
+ const eyeColor = getFieldValueFromMapping(this.licenseData, 'eye_color') || '';
136
+ const height = getFieldValueFromMapping(this.licenseData, 'height') || '';
137
+ const weight = getFieldValueFromMapping(this.licenseData, 'weight') || '';
138
+ const documentCategory = getFieldValueFromMapping(this.licenseData, 'document_category_code') || '';
139
+
140
+ // Build the data display in two columns with new styling
141
+ let dataHTML = '<div class="verant-dmv-results-container">';
142
+ dataHTML += '<div class="verant-dmv-results-grid">';
143
+
144
+ // Left column
145
+ dataHTML += '<ul class="verant-dmv-fields-column">';
146
+ if (firstName) {
147
+ dataHTML += `
148
+ <li class="verant-dmv-field-item">
149
+ <div class="verant-dmv-field-label">First Name</div>
150
+ <div class="verant-dmv-field-value">${firstName}</div>
151
+ </li>`;
152
+ }
153
+ if (lastName) {
154
+ dataHTML += `
155
+ <li class="verant-dmv-field-item">
156
+ <div class="verant-dmv-field-label">Last Name</div>
157
+ <div class="verant-dmv-field-value">${lastName}</div>
158
+ </li>`;
159
+ }
160
+ if (licenseNumber) {
161
+ dataHTML += `
162
+ <li class="verant-dmv-field-item">
163
+ <div class="verant-dmv-field-label">License Number</div>
164
+ <div class="verant-dmv-field-value">${licenseNumber}</div>
165
+ </li>`;
166
+ }
167
+ if (state) {
168
+ dataHTML += `
169
+ <li class="verant-dmv-field-item">
170
+ <div class="verant-dmv-field-label">State</div>
171
+ <div class="verant-dmv-field-value">${state}</div>
172
+ </li>`;
173
+ }
174
+ if (birthDate) {
175
+ dataHTML += `
176
+ <li class="verant-dmv-field-item">
177
+ <div class="verant-dmv-field-label">Date of Birth</div>
178
+ <div class="verant-dmv-field-value">${birthDate}</div>
179
+ </li>`;
180
+ }
181
+ if (sex) {
182
+ dataHTML += `
183
+ <li class="verant-dmv-field-item">
184
+ <div class="verant-dmv-field-label">Sex</div>
185
+ <div class="verant-dmv-field-value">${sex}</div>
186
+ </li>`;
187
+ }
188
+ if (eyeColor) {
189
+ dataHTML += `
190
+ <li class="verant-dmv-field-item">
191
+ <div class="verant-dmv-field-label">Eye Color</div>
192
+ <div class="verant-dmv-field-value">${eyeColor}</div>
193
+ </li>`;
194
+ }
195
+ dataHTML += '</ul>';
196
+
197
+ // Right column
198
+ dataHTML += '<ul class="verant-dmv-fields-column">';
199
+ if (weight) {
200
+ dataHTML += `
201
+ <li class="verant-dmv-field-item">
202
+ <div class="verant-dmv-field-label">Weight</div>
203
+ <div class="verant-dmv-field-value">${weight}</div>
204
+ </li>`;
205
+ }
206
+ if (issueDate) {
207
+ dataHTML += `
208
+ <li class="verant-dmv-field-item">
209
+ <div class="verant-dmv-field-label">Issue Date</div>
210
+ <div class="verant-dmv-field-value">${issueDate}</div>
211
+ </li>`;
212
+ }
213
+ if (expirationDate) {
214
+ dataHTML += `
215
+ <li class="verant-dmv-field-item">
216
+ <div class="verant-dmv-field-label">Expiration Date</div>
217
+ <div class="verant-dmv-field-value">${expirationDate}</div>
218
+ </li>`;
219
+ }
220
+ if (address) {
221
+ dataHTML += `
222
+ <li class="verant-dmv-field-item">
223
+ <div class="verant-dmv-field-label">Address</div>
224
+ <div class="verant-dmv-field-value">${address}</div>
225
+ </li>`;
226
+ }
227
+ if (city) {
228
+ dataHTML += `
229
+ <li class="verant-dmv-field-item">
230
+ <div class="verant-dmv-field-label">City</div>
231
+ <div class="verant-dmv-field-value">${city}</div>
232
+ </li>`;
233
+ }
234
+ if (postalCode) {
235
+ dataHTML += `
236
+ <li class="verant-dmv-field-item">
237
+ <div class="verant-dmv-field-label">Postal Code</div>
238
+ <div class="verant-dmv-field-value">${postalCode}</div>
239
+ </li>`;
240
+ }
241
+ if (documentCategory) {
242
+ dataHTML += `
243
+ <li class="verant-dmv-field-item">
244
+ <div class="verant-dmv-field-label">Document Type</div>
245
+ <div class="verant-dmv-field-value">${documentCategory}</div>
246
+ </li>`;
247
+ }
248
+ if (height) {
249
+ dataHTML += `
250
+ <li class="verant-dmv-field-item">
251
+ <div class="verant-dmv-field-label">Height</div>
252
+ <div class="verant-dmv-field-value">${height}</div>
253
+ </li>`;
254
+ }
255
+ dataHTML += '</ul>';
256
+
257
+ dataHTML += '</div>'; // close grid
258
+ dataHTML += '</div>'; // close container
259
+
260
+ body.innerHTML = `
261
+ <p class="verant-dmv-question">Would you like to perform a DMV Verification for this customer?</p>
262
+ <div class="verant-dmv-info-box">
263
+ ${dataHTML}
264
+ </div>
265
+ <div class="verant-dmv-actions">
266
+ <button class="verant-dmv-btn-secondary" id="cancelBtn">Skip</button>
267
+ <button class="verant-dmv-btn-primary" id="verifyBtn" style="background-color: ${this.primaryColor}">Yes, Verify</button>
268
+ </div>
269
+ `;
270
+
271
+ body.querySelector('#cancelBtn').addEventListener('click', () => this.cancel());
272
+ body.querySelector('#verifyBtn').addEventListener('click', () => this.verify());
273
+ }
274
+
275
+ /**
276
+ * Shows mock DMV results for preview/testing purposes
277
+ * @private
278
+ */
279
+ showMockResults() {
280
+ // Create mock DMV data with mixed match/mismatch results
281
+ const mockDmvData = {
282
+ match_status: true,
283
+ field_matches: [
284
+ { first_name: 'true' },
285
+ { last_name: 'true' },
286
+ { license_number: 'true' },
287
+ { birth_date: 'true' },
288
+ { address_line_1: 'false' },
289
+ { expiration_date: 'true' },
290
+ { issue_date: 'true' },
291
+ { eye_color: 'true' },
292
+ { sex_code: 'true' },
293
+ { height: 'false' },
294
+ { weight: 'true' },
295
+ { city: 'true' },
296
+ { state: 'true' },
297
+ { postal_code: 'false' }
298
+ ]
299
+ };
300
+
301
+ this.showResults(mockDmvData);
302
+ }
303
+
304
+ /**
305
+ * Initiates DMV verification process
306
+ * @private
307
+ */
308
+ async verify() {
309
+ this.showLoading();
310
+
311
+ try {
312
+ const response = await dmvCheck(this.clientId, this.scannerType, this.licenseData);
313
+
314
+ if (response && response.data) {
315
+ this.showResults(response.data);
316
+ } else if (response && response.status === 'error') {
317
+ this.showError(response.message || 'DMV verification service returned an error.');
318
+ } else {
319
+ this.showError('DMV verification service returned an invalid response.');
320
+ }
321
+ } catch (error) {
322
+ console.error('DMV check error:', error);
323
+ this.showError(error.message || 'Unable to connect to DMV verification service.');
324
+ }
325
+ }
326
+
327
+ /**
328
+ * Shows loading spinner while DMV check is in progress
329
+ * @private
330
+ */
331
+ showLoading() {
332
+ const body = this.modalRoot.querySelector('.verant-dmv-body');
333
+ body.innerHTML = `
334
+ <div class="verant-dmv-loading">
335
+ <div class="verant-dmv-spinner" style="border-top-color: ${this.primaryColor}"></div>
336
+ <p>Verification in progress...</p>
337
+ </div>
338
+ `;
339
+ }
340
+
341
+ /**
342
+ * Shows DMV verification results
343
+ * @private
344
+ * @param {Object} dmvData - DMV API response data
345
+ */
346
+ showResults(dmvData) {
347
+ const body = this.modalRoot.querySelector('.verant-dmv-body');
348
+
349
+ const matchStatus = dmvData.match_status === true || dmvData.match_status === 'true';
350
+
351
+ // Build field list in two columns
352
+ let fieldsHTML = '';
353
+ if (dmvData.field_matches && Array.isArray(dmvData.field_matches)) {
354
+ fieldsHTML += '<div class="verant-dmv-results-container">';
355
+ fieldsHTML += '<div class="verant-dmv-results-grid">';
356
+
357
+ // Split fields into two columns
358
+ const halfLength = Math.ceil(dmvData.field_matches.length / 2);
359
+
360
+ // Left column
361
+ fieldsHTML += '<ul class="verant-dmv-fields-column">';
362
+ dmvData.field_matches.slice(0, halfLength).forEach(field => {
363
+ const fieldName = Object.keys(field)[0];
364
+ const matches = field[fieldName] === 'true' || field[fieldName] === true;
365
+ const icon = matches ? '✓' : '✗';
366
+ const label = this.formatFieldLabel(fieldName);
367
+ const value = this.getFieldValue(fieldName);
368
+ const className = matches ? 'match' : 'mismatch';
369
+ fieldsHTML += `
370
+ <li class="verant-dmv-field-item">
371
+ <div class="verant-dmv-field-header">
372
+ <span class="verant-dmv-field-label">${label}</span>
373
+ <span class="verant-dmv-field-icon ${className}">${icon}</span>
374
+ </div>
375
+ <div class="verant-dmv-field-value">${value}</div>
376
+ </li>`;
377
+ });
378
+ fieldsHTML += '</ul>';
379
+
380
+ // Right column
381
+ fieldsHTML += '<ul class="verant-dmv-fields-column">';
382
+ dmvData.field_matches.slice(halfLength).forEach(field => {
383
+ const fieldName = Object.keys(field)[0];
384
+ const matches = field[fieldName] === 'true' || field[fieldName] === true;
385
+ const icon = matches ? '✓' : '✗';
386
+ const label = this.formatFieldLabel(fieldName);
387
+ const value = this.getFieldValue(fieldName);
388
+ const className = matches ? 'match' : 'mismatch';
389
+ fieldsHTML += `
390
+ <li class="verant-dmv-field-item">
391
+ <div class="verant-dmv-field-header">
392
+ <span class="verant-dmv-field-label">${label}</span>
393
+ <span class="verant-dmv-field-icon ${className}">${icon}</span>
394
+ </div>
395
+ <div class="verant-dmv-field-value">${value}</div>
396
+ </li>`;
397
+ });
398
+ fieldsHTML += '</ul>';
399
+
400
+ fieldsHTML += '</div>'; // close grid
401
+ fieldsHTML += '</div>'; // close container
402
+ }
403
+
404
+ body.innerHTML = `
405
+ <div class="verant-dmv-info-box">
406
+ ${fieldsHTML}
407
+ </div>
408
+ <div class="verant-dmv-actions">
409
+ <button class="verant-dmv-btn-primary" id="doneBtn" style="background-color: ${this.primaryColor}">Done</button>
410
+ </div>
411
+ `;
412
+
413
+ body.querySelector('#doneBtn').addEventListener('click', () => {
414
+ this.close({
415
+ status: 'verified',
416
+ matchStatus: matchStatus,
417
+ fieldMatches: dmvData.field_matches,
418
+ rawResponse: dmvData
419
+ });
420
+ });
421
+ }
422
+
423
+ /**
424
+ * Shows error message
425
+ * @private
426
+ * @param {string} message - Error message to display
427
+ */
428
+ showError(message) {
429
+ const body = this.modalRoot.querySelector('.verant-dmv-body');
430
+ body.innerHTML = `
431
+ <div class="verant-dmv-result error">
432
+ <h3>✗ Verification Failed</h3>
433
+ <p>${message}</p>
434
+ </div>
435
+ <div class="verant-dmv-actions">
436
+ <button class="verant-dmv-btn-primary" id="closeBtn" style="background-color: ${this.primaryColor}">Close</button>
437
+ </div>
438
+ `;
439
+
440
+ body.querySelector('#closeBtn').addEventListener('click', () => {
441
+ this.close({ status: 'error', error: message });
442
+ });
443
+ }
444
+
445
+ /**
446
+ * Cancels verification (user clicked Skip or closed modal)
447
+ * @private
448
+ */
449
+ cancel() {
450
+ this.close({ status: 'skipped' });
451
+ }
452
+
453
+ /**
454
+ * Closes the modal and resolves the promise
455
+ * @private
456
+ * @param {Object} result - Result object to return to caller
457
+ */
458
+ close(result) {
459
+ document.removeEventListener('keydown', this.handleKeydown);
460
+ if (this.modalRoot) {
461
+ this.modalRoot.remove();
462
+ this.modalRoot = null;
463
+ }
464
+ this.resolve(result);
465
+ }
466
+
467
+ /**
468
+ * Gets the value of a field from license data
469
+ * @private
470
+ * @param {string} fieldName - Field name from API response (e.g., 'first_name', 'last_name')
471
+ * @returns {string} Field value from license data
472
+ */
473
+ getFieldValue(fieldName) {
474
+ const value = getFieldValueFromMapping(this.licenseData, fieldName);
475
+
476
+ if (value === undefined || value === null || value === '') {
477
+ return 'N/A';
478
+ }
479
+
480
+ let displayValue = String(value).trim();
481
+
482
+ // Format date fields to MM/DD/YYYY
483
+ if (fieldName === 'birth_date' || fieldName === 'expiration_date' || fieldName === 'issue_date') {
484
+ displayValue = formatDateToDisplay(displayValue);
485
+ }
486
+
487
+ return displayValue;
488
+ }
489
+
490
+ /**
491
+ * Formats field names for display
492
+ * @private
493
+ * @param {string} fieldName - Field name from API response
494
+ * @returns {string} Formatted label
495
+ */
496
+ formatFieldLabel(fieldName) {
497
+ return FIELD_LABELS[fieldName] || fieldName.replace(/_/g, ' ');
498
+ }
499
+ }