@trustchex/react-native-sdk 1.478.7 → 1.481.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.
- package/android/src/main/java/com/trustchex/reactnativesdk/camera/TrustchexCameraView.kt +57 -3
- package/ios/Camera/TrustchexCameraView.swift +20 -15
- package/lib/module/Screens/Static/ResultScreen.js +42 -6
- package/lib/module/Shared/Components/DiagnosticReportButton.js +64 -0
- package/lib/module/Shared/Components/IdentityDocumentCamera.js +132 -8
- package/lib/module/Shared/Components/NavigationManager.js +7 -9
- package/lib/module/Shared/EIDReader/eidReader.js +149 -29
- package/lib/module/Shared/Libs/MRZ_KNOWN_ISSUES.md +7 -0
- package/lib/module/Shared/Libs/diagnosticReport.js +133 -0
- package/lib/module/Shared/Libs/diagnostics.js +171 -0
- package/lib/module/Shared/Libs/mrzFrameAggregator.js +32 -5
- package/lib/module/Shared/Libs/native-device-info.utils.js +67 -0
- package/lib/module/Shared/Libs/sendDiagnosticReport.js +130 -0
- package/lib/module/Translation/Resources/en.js +5 -0
- package/lib/module/Translation/Resources/tr.js +5 -0
- package/lib/module/Trustchex.js +20 -4
- package/lib/module/version.js +1 -1
- package/lib/typescript/src/Screens/Static/ResultScreen.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/DiagnosticReportButton.d.ts +20 -0
- package/lib/typescript/src/Shared/Components/DiagnosticReportButton.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.types.d.ts +10 -0
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.types.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/NavigationManager.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/TrustchexCamera.d.ts +10 -0
- package/lib/typescript/src/Shared/Components/TrustchexCamera.d.ts.map +1 -1
- package/lib/typescript/src/Shared/EIDReader/eidReader.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/diagnosticReport.d.ts +48 -0
- package/lib/typescript/src/Shared/Libs/diagnosticReport.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Libs/diagnostics.d.ts +145 -0
- package/lib/typescript/src/Shared/Libs/diagnostics.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Libs/mrzFrameAggregator.d.ts +33 -0
- package/lib/typescript/src/Shared/Libs/mrzFrameAggregator.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/native-device-info.utils.d.ts +33 -0
- package/lib/typescript/src/Shared/Libs/native-device-info.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/sendDiagnosticReport.d.ts +30 -0
- package/lib/typescript/src/Shared/Libs/sendDiagnosticReport.d.ts.map +1 -0
- package/lib/typescript/src/Translation/Resources/en.d.ts +5 -0
- package/lib/typescript/src/Translation/Resources/en.d.ts.map +1 -1
- package/lib/typescript/src/Translation/Resources/tr.d.ts +5 -0
- package/lib/typescript/src/Translation/Resources/tr.d.ts.map +1 -1
- package/lib/typescript/src/Trustchex.d.ts.map +1 -1
- package/lib/typescript/src/version.d.ts +1 -1
- package/package.json +5 -1
- package/src/Screens/Static/ResultScreen.tsx +46 -1
- package/src/Shared/Components/DiagnosticReportButton.tsx +77 -0
- package/src/Shared/Components/IdentityDocumentCamera.tsx +145 -7
- package/src/Shared/Components/IdentityDocumentCamera.types.ts +6 -0
- package/src/Shared/Components/NavigationManager.tsx +7 -7
- package/src/Shared/Components/TrustchexCamera.tsx +6 -0
- package/src/Shared/EIDReader/eidReader.ts +166 -46
- package/src/Shared/Libs/MRZ_KNOWN_ISSUES.md +7 -0
- package/src/Shared/Libs/diagnosticReport.ts +206 -0
- package/src/Shared/Libs/diagnostics.ts +251 -0
- package/src/Shared/Libs/mrzFrameAggregator.ts +61 -3
- package/src/Shared/Libs/native-device-info.utils.ts +116 -0
- package/src/Shared/Libs/sendDiagnosticReport.ts +165 -0
- package/src/Translation/Resources/en.ts +6 -0
- package/src/Translation/Resources/tr.ts +6 -0
- package/src/Trustchex.tsx +26 -3
- package/src/version.ts +1 -1
|
@@ -9,6 +9,33 @@ import { Buffer } from 'buffer';
|
|
|
9
9
|
import { MRZInfo } from './lds/icao/mrzInfo';
|
|
10
10
|
import { InputStream } from './java/inputStream';
|
|
11
11
|
import { debugLog } from '../Libs/debug.utils';
|
|
12
|
+
import { diagnostics } from '../Libs/diagnostics';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Extract an APDU status word (hex, e.g. "6982") from an error message if the
|
|
16
|
+
* protocol layer included one. Returns undefined when none is present. Kept
|
|
17
|
+
* deliberately loose — the protocol senders embed "SW=...." / "(6982)" forms.
|
|
18
|
+
*/
|
|
19
|
+
const extractStatusWord = (msg: string): string | undefined => {
|
|
20
|
+
const m = msg.match(/\b(?:SW[:= ]?)?([0-9a-fA-F]{4})\b/);
|
|
21
|
+
return m ? m[1].toUpperCase() : undefined;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/** Categorise an NFC error message into a coarse, non-PII bucket. */
|
|
25
|
+
const categorizeNfcError = (msg: string): string => {
|
|
26
|
+
const m = msg.toLowerCase();
|
|
27
|
+
if (m.includes('cancel')) return 'user_cancelled';
|
|
28
|
+
if (m.includes('lost') || m.includes('steady')) return 'tag_lost';
|
|
29
|
+
if (m.includes('timeout')) return 'timeout';
|
|
30
|
+
if (m.includes('not supported') || m.includes('unsupported')) {
|
|
31
|
+
return 'unsupported';
|
|
32
|
+
}
|
|
33
|
+
if (m.includes('not enabled') || m.includes('disabled')) return 'not_enabled';
|
|
34
|
+
if (m.includes('auth') || m.includes('mutual') || m.includes('security')) {
|
|
35
|
+
return 'auth_failed';
|
|
36
|
+
}
|
|
37
|
+
return 'reading_error';
|
|
38
|
+
};
|
|
12
39
|
|
|
13
40
|
// --- Minimal PNG encoder (pure JS, no native deps) ---
|
|
14
41
|
// Uses deflate stored (uncompressed) blocks for O(n) encoding speed.
|
|
@@ -307,8 +334,40 @@ const eidReader = async (
|
|
|
307
334
|
true
|
|
308
335
|
);
|
|
309
336
|
|
|
337
|
+
// Structured NFC diagnostics: record each step's outcome + timing + APDU SW
|
|
338
|
+
// into the session collector so the result screen can surface device-specific
|
|
339
|
+
// read failures. Best-effort and side-effect-only — never alters the flow.
|
|
340
|
+
diagnostics.nfcAttemptStarted();
|
|
341
|
+
const stepTimer = () => {
|
|
342
|
+
const t0 = Date.now();
|
|
343
|
+
return (step: string, ok: boolean, error?: unknown) => {
|
|
344
|
+
const msg = error instanceof Error ? error.message : error ? String(error) : undefined;
|
|
345
|
+
// Store only the APDU status word and a COARSE category — never the raw
|
|
346
|
+
// error text, which could in theory embed field/PII values. The SW + step
|
|
347
|
+
// name + category carry all the actionable signal.
|
|
348
|
+
diagnostics.nfcStep({
|
|
349
|
+
step,
|
|
350
|
+
ok,
|
|
351
|
+
ms: Date.now() - t0,
|
|
352
|
+
sw: msg ? extractStatusWord(msg) : undefined,
|
|
353
|
+
error: msg ? categorizeNfcError(msg) : undefined,
|
|
354
|
+
});
|
|
355
|
+
};
|
|
356
|
+
};
|
|
357
|
+
|
|
310
358
|
try {
|
|
311
|
-
|
|
359
|
+
{
|
|
360
|
+
const done = stepTimer();
|
|
361
|
+
try {
|
|
362
|
+
await passportService.open();
|
|
363
|
+
// Reaching open() without throwing means the chip powered on / was seen.
|
|
364
|
+
diagnostics.nfcChipDetected();
|
|
365
|
+
done('open', true);
|
|
366
|
+
} catch (e) {
|
|
367
|
+
done('open', false, e);
|
|
368
|
+
throw e;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
312
371
|
|
|
313
372
|
progress = 1;
|
|
314
373
|
if (progressCallback) {
|
|
@@ -320,15 +379,20 @@ const eidReader = async (
|
|
|
320
379
|
// regardless of which application the chip activates on power-on.
|
|
321
380
|
let hasPACESucceeded = false;
|
|
322
381
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
382
|
+
{
|
|
383
|
+
const done = stepTimer();
|
|
384
|
+
try {
|
|
385
|
+
await passportService.sendSelectMF();
|
|
386
|
+
done('selectMF', true);
|
|
387
|
+
debugLog('EID', '[EID] SELECT MF OK');
|
|
388
|
+
} catch (mfErr) {
|
|
389
|
+
done('selectMF', false, mfErr);
|
|
390
|
+
const mfMsg = mfErr instanceof Error ? mfErr.message : String(mfErr);
|
|
391
|
+
debugLog(
|
|
392
|
+
'EID',
|
|
393
|
+
`[EID] SELECT MF failed (${mfMsg}), continuing with SFI read anyway`
|
|
394
|
+
);
|
|
395
|
+
}
|
|
332
396
|
}
|
|
333
397
|
|
|
334
398
|
progress = 2;
|
|
@@ -339,28 +403,33 @@ const eidReader = async (
|
|
|
339
403
|
// Read EF.CardAccess — kept in a separate try/catch so we can distinguish
|
|
340
404
|
// "file read failure" from "PACE protocol failure" in the logs.
|
|
341
405
|
let cardAccessBuf: Buffer | null = null;
|
|
342
|
-
|
|
343
|
-
const
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
buf
|
|
406
|
+
{
|
|
407
|
+
const done = stepTimer();
|
|
408
|
+
try {
|
|
409
|
+
const cardAccessStream = passportService.getInputStream(
|
|
410
|
+
EIDService.EF_CARD_ACCESS
|
|
411
|
+
);
|
|
412
|
+
await cardAccessStream.init();
|
|
413
|
+
const cardAccessLen = cardAccessStream.getLength();
|
|
414
|
+
const buf = Buffer.alloc(cardAccessLen);
|
|
415
|
+
for (let i = 0; i < cardAccessLen; i++) {
|
|
416
|
+
buf[i] = await cardAccessStream.read();
|
|
417
|
+
}
|
|
418
|
+
cardAccessBuf = buf;
|
|
419
|
+
done('readCardAccess', true);
|
|
420
|
+
debugLog(
|
|
421
|
+
'EID',
|
|
422
|
+
`[EID] EF.CardAccess read OK: ${cardAccessLen} bytes = ${cardAccessBuf.toString('hex')}`
|
|
423
|
+
);
|
|
424
|
+
} catch (readErr) {
|
|
425
|
+
done('readCardAccess', false, readErr);
|
|
426
|
+
const readMsg =
|
|
427
|
+
readErr instanceof Error ? readErr.message : String(readErr);
|
|
428
|
+
debugLog(
|
|
429
|
+
'EID',
|
|
430
|
+
`[EID] EF.CardAccess read FAILED: ${readMsg} — falling back to BAC`
|
|
431
|
+
);
|
|
351
432
|
}
|
|
352
|
-
cardAccessBuf = buf;
|
|
353
|
-
debugLog(
|
|
354
|
-
'EID',
|
|
355
|
-
`[EID] EF.CardAccess read OK: ${cardAccessLen} bytes = ${cardAccessBuf.toString('hex')}`
|
|
356
|
-
);
|
|
357
|
-
} catch (readErr) {
|
|
358
|
-
const readMsg =
|
|
359
|
-
readErr instanceof Error ? readErr.message : String(readErr);
|
|
360
|
-
debugLog(
|
|
361
|
-
'EID',
|
|
362
|
-
`[EID] EF.CardAccess read FAILED: ${readMsg} — falling back to BAC`
|
|
363
|
-
);
|
|
364
433
|
}
|
|
365
434
|
|
|
366
435
|
progress = 3;
|
|
@@ -369,6 +438,11 @@ const eidReader = async (
|
|
|
369
438
|
}
|
|
370
439
|
|
|
371
440
|
// If EF.CardAccess was read successfully, attempt PACE.
|
|
441
|
+
if (cardAccessBuf === null) {
|
|
442
|
+
// EF.CardAccess unreadable (the readCardAccess step already recorded the
|
|
443
|
+
// failure/SW) → PACE can't be attempted; BAC will be used.
|
|
444
|
+
diagnostics.nfcPaceFellBackToBac('no_card_access');
|
|
445
|
+
}
|
|
372
446
|
if (cardAccessBuf !== null) {
|
|
373
447
|
try {
|
|
374
448
|
const paceInfo = parsePACEInfoFromCardAccess(cardAccessBuf);
|
|
@@ -378,6 +452,7 @@ const eidReader = async (
|
|
|
378
452
|
'EID',
|
|
379
453
|
`[EID] PACE available: oid=${paceInfo.oid} parameterId=${paceInfo.parameterId}`
|
|
380
454
|
);
|
|
455
|
+
diagnostics.nfcSetAuth('PACE', paceInfo.oid);
|
|
381
456
|
// PACE is available - derive key from MRZ and perform PACE
|
|
382
457
|
const bacKey = new BACKey(documentNumber, dateOfBirth, dateOfExpiry);
|
|
383
458
|
const paceKey = PACEKeySpec.createMRZKey(bacKey.getKeySeedForPACE());
|
|
@@ -387,15 +462,23 @@ const eidReader = async (
|
|
|
387
462
|
progressCallback(progress);
|
|
388
463
|
}
|
|
389
464
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
465
|
+
const done = stepTimer();
|
|
466
|
+
try {
|
|
467
|
+
await passportService.doPACE(
|
|
468
|
+
paceKey,
|
|
469
|
+
paceInfo.oid,
|
|
470
|
+
paceInfo.parameterId,
|
|
471
|
+
progressCallback
|
|
472
|
+
);
|
|
473
|
+
hasPACESucceeded = true;
|
|
474
|
+
done('paceAuth', true);
|
|
475
|
+
debugLog('EID', '[EID] PACE succeeded');
|
|
476
|
+
} catch (paceErr) {
|
|
477
|
+
done('paceAuth', false, paceErr);
|
|
478
|
+
throw paceErr;
|
|
479
|
+
}
|
|
398
480
|
} else {
|
|
481
|
+
diagnostics.nfcPaceFellBackToBac('no_pace_info');
|
|
399
482
|
debugLog(
|
|
400
483
|
'EID',
|
|
401
484
|
'[EID] EF.CardAccess read OK but no PACE SecurityInfo found — document does not support PACE, falling back to BAC'
|
|
@@ -403,6 +486,7 @@ const eidReader = async (
|
|
|
403
486
|
}
|
|
404
487
|
} catch (paceError) {
|
|
405
488
|
// PACE protocol exchange failed — fall back to BAC
|
|
489
|
+
diagnostics.nfcPaceFellBackToBac('pace_failed');
|
|
406
490
|
const msg =
|
|
407
491
|
paceError instanceof Error ? paceError.message : String(paceError);
|
|
408
492
|
debugLog(
|
|
@@ -421,7 +505,16 @@ const eidReader = async (
|
|
|
421
505
|
}
|
|
422
506
|
|
|
423
507
|
// Select Applet for MRTD
|
|
424
|
-
|
|
508
|
+
{
|
|
509
|
+
const done = stepTimer();
|
|
510
|
+
try {
|
|
511
|
+
await passportService.sendSelectApplet(hasPACESucceeded);
|
|
512
|
+
done('selectApplet', true);
|
|
513
|
+
} catch (e) {
|
|
514
|
+
done('selectApplet', false, e);
|
|
515
|
+
throw e;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
425
518
|
|
|
426
519
|
progress = 10;
|
|
427
520
|
if (progressCallback) {
|
|
@@ -436,8 +529,16 @@ const eidReader = async (
|
|
|
436
529
|
await efComStream.read();
|
|
437
530
|
} catch (error) {
|
|
438
531
|
// EF_COM not available -> try to do BAC
|
|
532
|
+
diagnostics.nfcSetAuth('BAC');
|
|
439
533
|
const bacKey = new BACKey(documentNumber, dateOfBirth, dateOfExpiry);
|
|
440
|
-
|
|
534
|
+
const done = stepTimer();
|
|
535
|
+
try {
|
|
536
|
+
await passportService.doBAC(bacKey);
|
|
537
|
+
done('bacAuth', true);
|
|
538
|
+
} catch (e) {
|
|
539
|
+
done('bacAuth', false, e);
|
|
540
|
+
throw e;
|
|
541
|
+
}
|
|
441
542
|
}
|
|
442
543
|
}
|
|
443
544
|
|
|
@@ -447,10 +548,18 @@ const eidReader = async (
|
|
|
447
548
|
}
|
|
448
549
|
|
|
449
550
|
// Read DG1 (MRZ Info)
|
|
450
|
-
const
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
551
|
+
const dg1Done = stepTimer();
|
|
552
|
+
let mrzInfo: MRZInfo;
|
|
553
|
+
try {
|
|
554
|
+
const dg1InputStream = passportService.getInputStream(EIDService.EF_DG1);
|
|
555
|
+
await dg1InputStream.init();
|
|
556
|
+
const dg1File = new DG1File(undefined, dg1InputStream);
|
|
557
|
+
mrzInfo = await dg1File.getMRZInfo();
|
|
558
|
+
dg1Done('readDG1', true);
|
|
559
|
+
} catch (e) {
|
|
560
|
+
dg1Done('readDG1', false, e);
|
|
561
|
+
throw e;
|
|
562
|
+
}
|
|
454
563
|
|
|
455
564
|
progress = 30;
|
|
456
565
|
if (progressCallback) {
|
|
@@ -458,6 +567,7 @@ const eidReader = async (
|
|
|
458
567
|
}
|
|
459
568
|
|
|
460
569
|
// Read DG2 (Face Image)
|
|
570
|
+
const dg2Done = stepTimer();
|
|
461
571
|
const dg2InputStream = passportService.getInputStream(EIDService.EF_DG2);
|
|
462
572
|
await dg2InputStream.init();
|
|
463
573
|
InputStream.onRead((totalBytesRead, fileLength) => {
|
|
@@ -471,7 +581,14 @@ const eidReader = async (
|
|
|
471
581
|
let mimeType = '';
|
|
472
582
|
|
|
473
583
|
const allFaceImageInfos: FaceImageInfo[] = [];
|
|
474
|
-
|
|
584
|
+
let faceInfos;
|
|
585
|
+
try {
|
|
586
|
+
faceInfos = await dg2File.getFaceInfos();
|
|
587
|
+
dg2Done('readDG2', true);
|
|
588
|
+
} catch (e) {
|
|
589
|
+
dg2Done('readDG2', false, e);
|
|
590
|
+
throw e;
|
|
591
|
+
}
|
|
475
592
|
for (let faceInfo of faceInfos) {
|
|
476
593
|
allFaceImageInfos.push(...faceInfo.getFaceImageInfos());
|
|
477
594
|
}
|
|
@@ -493,12 +610,15 @@ const eidReader = async (
|
|
|
493
610
|
progressCallback(progress);
|
|
494
611
|
}
|
|
495
612
|
|
|
613
|
+
diagnostics.nfcResult(true);
|
|
496
614
|
return {
|
|
497
615
|
mrz: mrzInfo,
|
|
498
616
|
faceImage: imageAsBase64,
|
|
499
617
|
mimeType: mimeType,
|
|
500
618
|
};
|
|
501
619
|
} catch (error) {
|
|
620
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
621
|
+
diagnostics.nfcResult(false, categorizeNfcError(msg));
|
|
502
622
|
debugLog('EID', 'Error reading passport data', error);
|
|
503
623
|
} finally {
|
|
504
624
|
await passportService.close();
|
|
@@ -118,6 +118,13 @@ Covered by the "real letters in names are not eaten" tests in `mrz.utils.test.ts
|
|
|
118
118
|
30–44-char MRZ line below that floor, so the native pipeline **crops to the
|
|
119
119
|
on-screen scan frame and upscales + grayscale/contrast-enhances** before OCR.
|
|
120
120
|
The MRZ band must be reasonably filled within the on-screen guide.
|
|
121
|
+
- Two distinct regions exist in the native code (don't confuse them):
|
|
122
|
+
the **MRZ-OCR crop** (`buildScanFrameRoiImage`: top 25% / bottom 12% / sides
|
|
123
|
+
4%, upscaled ≤2.5× to ≤2200px, grayscale, contrast 1.6×) and the
|
|
124
|
+
**brightness-sampling region** (the on-screen guide: 36% / 36% / 5%). Both are
|
|
125
|
+
the same on iOS and Android. The diagnostic report records the measured MRZ
|
|
126
|
+
band pixel height so you can tell when a device leaves the band below the
|
|
127
|
+
~16 px/char floor.
|
|
121
128
|
- **Glare / lamination.** Heavy glare on a glossy laminated card still degrades
|
|
122
129
|
OCR; the enhancement helps but does not eliminate it. Steadying the card and
|
|
123
130
|
reducing direct glare improves convergence.
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds the support diagnostic report from a scan session.
|
|
3
|
+
*
|
|
4
|
+
* Produces three things the result screen turns into a reviewable email:
|
|
5
|
+
* - `diagnostics.json` — technical signals (device, camera, MRZ, NFC steps).
|
|
6
|
+
* - `scan-data.json` — the scanned identity data + raw MRZ (PII; the user
|
|
7
|
+
* reviews it in the draft and chooses to send).
|
|
8
|
+
* - a short, size-bounded plain-text BODY summary that always fits an email.
|
|
9
|
+
*
|
|
10
|
+
* The full detail lives in the JSON attachments; the email body is intentionally
|
|
11
|
+
* compact so the draft opens reliably on every mail client.
|
|
12
|
+
*/
|
|
13
|
+
import type { DiagnosticsSnapshot } from './diagnostics';
|
|
14
|
+
import type { DeviceDetails } from './native-device-info.utils';
|
|
15
|
+
import type { MRZFields } from '../Types/mrzFields';
|
|
16
|
+
|
|
17
|
+
/** ICAO 9303 7-3-1 weighted check digit (local copy; mirrors mrz.utils). */
|
|
18
|
+
const icaoCheckDigit = (str: string): string => {
|
|
19
|
+
const weights = [7, 3, 1];
|
|
20
|
+
let sum = 0;
|
|
21
|
+
for (let i = 0; i < str.length; i++) {
|
|
22
|
+
const c = str[i];
|
|
23
|
+
const v =
|
|
24
|
+
c === '<'
|
|
25
|
+
? 0
|
|
26
|
+
: c >= '0' && c <= '9'
|
|
27
|
+
? c.charCodeAt(0) - 48
|
|
28
|
+
: c.charCodeAt(0) - 55;
|
|
29
|
+
sum = (sum + weights[i % 3] * v) % 10;
|
|
30
|
+
}
|
|
31
|
+
return String(sum);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export interface ScanDataForReport {
|
|
35
|
+
documentType?: string;
|
|
36
|
+
dataSource?: string;
|
|
37
|
+
mrzText?: string | null;
|
|
38
|
+
mrzFields?: MRZFields | null;
|
|
39
|
+
barcodeValue?: string | null;
|
|
40
|
+
/** Which images were captured (presence only, used for the body summary). */
|
|
41
|
+
images?: {
|
|
42
|
+
mrzSide?: boolean;
|
|
43
|
+
front?: boolean;
|
|
44
|
+
face?: boolean;
|
|
45
|
+
nfcFace?: boolean;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface Td1CheckResult {
|
|
50
|
+
checkDigits: Record<string, 'pass' | 'fail'>;
|
|
51
|
+
/** True when the Turkish leading-letter (digits-only) convention was applied. */
|
|
52
|
+
turkishLetterPrefix: boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Compute per-field check-digit pass/fail for a TD1 MRZ (best-effort). */
|
|
56
|
+
const computeTd1CheckDigits = (
|
|
57
|
+
mrzText?: string | null
|
|
58
|
+
): Td1CheckResult | undefined => {
|
|
59
|
+
if (!mrzText) return undefined;
|
|
60
|
+
const lines = mrzText
|
|
61
|
+
.split('\n')
|
|
62
|
+
.map((l) => l.trim())
|
|
63
|
+
.filter(Boolean);
|
|
64
|
+
if (lines.length < 2 || lines[0].length < 30 || lines[1].length < 30) {
|
|
65
|
+
return undefined; // only TD1 handled here; other formats omitted from summary
|
|
66
|
+
}
|
|
67
|
+
const l1 = lines[0];
|
|
68
|
+
const l2 = lines[1];
|
|
69
|
+
const result: Record<string, 'pass' | 'fail'> = {};
|
|
70
|
+
const check = (data: string, printed: string) =>
|
|
71
|
+
icaoCheckDigit(data) === printed ? 'pass' : 'fail';
|
|
72
|
+
|
|
73
|
+
// Turkish convention: a doc number may carry a leading serial LETTER, and the
|
|
74
|
+
// doc-number check digit is computed over the DIGITS ONLY (letter dropped).
|
|
75
|
+
// The TD1 composite then also diverges from strict ICAO for such cards (no
|
|
76
|
+
// interpretation of the letter reproduces it). To avoid reporting a misleading
|
|
77
|
+
// "fail" on a card the SDK legitimately accepted, detect the letter-prefix and
|
|
78
|
+
// treat both the doc-number and composite as validated under that convention.
|
|
79
|
+
const docData = l1.slice(5, 14);
|
|
80
|
+
const docPrinted = l1[14];
|
|
81
|
+
const hasLetterPrefix = /^[A-Z]/.test(docData);
|
|
82
|
+
const strictDocOk = icaoCheckDigit(docData) === docPrinted;
|
|
83
|
+
const turkishDocOk =
|
|
84
|
+
hasLetterPrefix && icaoCheckDigit(docData.slice(1)) === docPrinted;
|
|
85
|
+
const docOk = strictDocOk || turkishDocOk;
|
|
86
|
+
result.documentNumber = docOk ? 'pass' : 'fail';
|
|
87
|
+
result.birthDate = check(l2.slice(0, 6), l2[6]);
|
|
88
|
+
result.expiry = check(l2.slice(8, 14), l2[14]);
|
|
89
|
+
|
|
90
|
+
const composite =
|
|
91
|
+
l1.slice(5, 30) + l2.slice(0, 7) + l2.slice(8, 15) + l2.slice(18, 29);
|
|
92
|
+
const compStrict = check(composite, l2[29]);
|
|
93
|
+
// If the strict composite fails ONLY because of the Turkish letter-prefix
|
|
94
|
+
// doc number (which itself passed under the digits-only rule), don't surface a
|
|
95
|
+
// misleading failure — the SDK accepts these cards.
|
|
96
|
+
result.composite =
|
|
97
|
+
compStrict === 'fail' && hasLetterPrefix && docOk ? 'pass' : compStrict;
|
|
98
|
+
// The Turkish convention was the deciding factor only when strict ICAO would
|
|
99
|
+
// have failed the doc number but the digits-only rule passes it.
|
|
100
|
+
const turkishLetterPrefix = !strictDocOk && turkishDocOk;
|
|
101
|
+
return { checkDigits: result, turkishLetterPrefix };
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export interface DiagnosticReportBundle {
|
|
105
|
+
/** Pretty-printed technical report. */
|
|
106
|
+
diagnosticsJson: string;
|
|
107
|
+
/** Pretty-printed scanned identity data (PII). */
|
|
108
|
+
scanDataJson: string;
|
|
109
|
+
/** Compact, size-bounded plain-text email body. */
|
|
110
|
+
body: string;
|
|
111
|
+
/** Suggested email subject. */
|
|
112
|
+
subject: string;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const MAX_BODY_BYTES = 4096;
|
|
116
|
+
|
|
117
|
+
const yesNo = (b: boolean | undefined) => (b ? 'yes' : 'no');
|
|
118
|
+
|
|
119
|
+
/** Assemble the full report bundle. Pure — no I/O, no native calls. */
|
|
120
|
+
export const buildDiagnosticReport = (params: {
|
|
121
|
+
diag: DiagnosticsSnapshot;
|
|
122
|
+
device: DeviceDetails;
|
|
123
|
+
scan: ScanDataForReport;
|
|
124
|
+
sdkVersion: string;
|
|
125
|
+
sessionId?: string;
|
|
126
|
+
}): DiagnosticReportBundle => {
|
|
127
|
+
const { diag, device, scan, sdkVersion, sessionId } = params;
|
|
128
|
+
|
|
129
|
+
const computed = computeTd1CheckDigits(scan.mrzText);
|
|
130
|
+
const checkDigits = diag.mrz.checkDigits ?? computed?.checkDigits;
|
|
131
|
+
// Set the Turkish-letter-prefix flag from the final accepted MRZ (the collector
|
|
132
|
+
// can't know it during scanning; the report does, from the MRZ text).
|
|
133
|
+
const turkishLetterPrefixApplied =
|
|
134
|
+
diag.mrz.turkishLetterPrefixApplied || computed?.turkishLetterPrefix === true;
|
|
135
|
+
|
|
136
|
+
// --- diagnostics.json (technical, low-PII) ---
|
|
137
|
+
const diagnosticsObj = {
|
|
138
|
+
schemaVersion: diag.schemaVersion,
|
|
139
|
+
generatedAt: diag.generatedAt,
|
|
140
|
+
sessionId: sessionId ?? null,
|
|
141
|
+
sdk: { platform: device.platform, sdkVersion, buildNumber: device.buildNumber },
|
|
142
|
+
device,
|
|
143
|
+
scan: {
|
|
144
|
+
documentType: scan.documentType ?? diag.documentType ?? null,
|
|
145
|
+
dataSource: scan.dataSource ?? diag.dataSource ?? null,
|
|
146
|
+
totalDurationMs: diag.totalDurationMs,
|
|
147
|
+
camera: diag.camera,
|
|
148
|
+
},
|
|
149
|
+
mrz: {
|
|
150
|
+
...diag.mrz,
|
|
151
|
+
checkDigits: checkDigits ?? null,
|
|
152
|
+
turkishLetterPrefixApplied,
|
|
153
|
+
},
|
|
154
|
+
nfc: diag.nfc,
|
|
155
|
+
errors: diag.errors,
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
// --- scan-data.json (PII; user reviews before sending) ---
|
|
159
|
+
const scanDataObj = {
|
|
160
|
+
documentType: scan.documentType ?? diag.documentType ?? null,
|
|
161
|
+
dataSource: scan.dataSource ?? diag.dataSource ?? null,
|
|
162
|
+
mrzText: scan.mrzText ?? null,
|
|
163
|
+
fields: scan.mrzFields ?? null,
|
|
164
|
+
barcodeValue: scan.barcodeValue ?? null,
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
// --- compact email body (always under MAX_BODY_BYTES) ---
|
|
168
|
+
const nfc = diag.nfc;
|
|
169
|
+
const nfcLine = nfc.attempted
|
|
170
|
+
? `${nfc.authProtocol}${nfc.paceFellBackToBac ? '→BAC' : ''} · ${
|
|
171
|
+
nfc.succeeded ? 'success' : `failed at ${nfc.failureStep ?? '?'} (${nfc.errorCategory ?? 'error'})`
|
|
172
|
+
}`
|
|
173
|
+
: 'not attempted';
|
|
174
|
+
const checksLine = checkDigits
|
|
175
|
+
? Object.entries(checkDigits)
|
|
176
|
+
.map(([k, v]) => `${k}:${v}`)
|
|
177
|
+
.join(' ')
|
|
178
|
+
: 'n/a';
|
|
179
|
+
|
|
180
|
+
const body = [
|
|
181
|
+
'TRUSTCHEX SCAN REPORT',
|
|
182
|
+
`Device: ${device.brand} ${device.model} · ${device.systemName} ${device.systemVersion}${device.apiLevel ? ` (API ${device.apiLevel})` : ''}`,
|
|
183
|
+
`SDK: ${sdkVersion} (build ${device.buildNumber}) · ${device.manufacturer} · ${device.deviceId}`,
|
|
184
|
+
`Document: ${scanDataObj.documentType ?? 'unknown'} · source: ${scanDataObj.dataSource ?? 'unknown'}`,
|
|
185
|
+
`MRZ: ${diag.mrz.reachedStable ? 'stable' : 'NOT stable'} after ${diag.mrz.framesProcessed} frames · checks: ${checksLine}`,
|
|
186
|
+
`Camera: ${diag.camera.frames} frames · brightness ${diag.camera.avgBrightness} (low:${yesNo(diag.camera.hadLowBrightness)} blur:${yesNo(diag.camera.hadBlurryFrames)})`,
|
|
187
|
+
`NFC: ${nfcLine}`,
|
|
188
|
+
sessionId ? `Session: ${sessionId}` : '',
|
|
189
|
+
'',
|
|
190
|
+
'Describe the issue here:',
|
|
191
|
+
'',
|
|
192
|
+
'',
|
|
193
|
+
'— Full diagnostics and document images are attached. Please review before sending; do not edit the attachments. —',
|
|
194
|
+
]
|
|
195
|
+
.filter(Boolean)
|
|
196
|
+
.join('\n');
|
|
197
|
+
|
|
198
|
+
const subject = `Trustchex scan report — ${scanDataObj.documentType ?? 'doc'} — ${device.brand} ${device.model} — v${sdkVersion}${sessionId ? ` — ${sessionId}` : ''}`;
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
diagnosticsJson: JSON.stringify(diagnosticsObj, null, 2),
|
|
202
|
+
scanDataJson: JSON.stringify(scanDataObj, null, 2),
|
|
203
|
+
body: body.length > MAX_BODY_BYTES ? body.slice(0, MAX_BODY_BYTES - 1) + '…' : body,
|
|
204
|
+
subject,
|
|
205
|
+
};
|
|
206
|
+
};
|