@trustchex/react-native-sdk 1.478.7 → 1.483.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.
Files changed (112) hide show
  1. package/android/src/main/java/com/trustchex/reactnativesdk/camera/TrustchexCameraView.kt +57 -3
  2. package/ios/Camera/TrustchexCameraView.swift +20 -15
  3. package/lib/module/Screens/Dynamic/IdentityDocumentEIDScanningScreen.js +0 -3
  4. package/lib/module/Screens/Dynamic/IdentityDocumentScanningScreen.js +21 -0
  5. package/lib/module/Screens/Dynamic/VideoCallScreen.js +1 -1
  6. package/lib/module/Screens/Static/ResultScreen.js +75 -16
  7. package/lib/module/Shared/Components/DiagnosticReportButton.js +64 -0
  8. package/lib/module/Shared/Components/EIDScanner.js +52 -19
  9. package/lib/module/Shared/Components/IdentityDocumentCamera.js +132 -8
  10. package/lib/module/Shared/Components/NavigationManager.js +7 -9
  11. package/lib/module/Shared/Contexts/AppContext.js +3 -0
  12. package/lib/module/Shared/EIDReader/eidReader.js +167 -30
  13. package/lib/module/Shared/EIDReader/lds/icao/dg11File.js +77 -0
  14. package/lib/module/Shared/Libs/MRZ_KNOWN_ISSUES.md +7 -0
  15. package/lib/module/Shared/Libs/diagnosticReport.js +133 -0
  16. package/lib/module/Shared/Libs/diagnostics.js +171 -0
  17. package/lib/module/Shared/Libs/documentDataNormalizer.js +104 -0
  18. package/lib/module/Shared/Libs/mrz.utils.js +0 -5
  19. package/lib/module/Shared/Libs/mrzFrameAggregator.js +32 -5
  20. package/lib/module/Shared/Libs/mrzTransliteration.js +80 -0
  21. package/lib/module/Shared/Libs/native-device-info.utils.js +67 -0
  22. package/lib/module/Shared/Libs/sendDiagnosticReport.js +130 -0
  23. package/lib/module/Shared/Services/DataUploadService.js +6 -3
  24. package/lib/module/Shared/Types/documentData.js +1 -0
  25. package/lib/module/Shared/Types/documentReadResult.js +3 -0
  26. package/lib/module/Translation/Resources/en.js +5 -0
  27. package/lib/module/Translation/Resources/tr.js +5 -0
  28. package/lib/module/Trustchex.js +31 -4
  29. package/lib/module/version.js +1 -1
  30. package/lib/typescript/src/Screens/Dynamic/IdentityDocumentEIDScanningScreen.d.ts.map +1 -1
  31. package/lib/typescript/src/Screens/Dynamic/IdentityDocumentScanningScreen.d.ts.map +1 -1
  32. package/lib/typescript/src/Screens/Dynamic/VideoCallScreen.d.ts.map +1 -1
  33. package/lib/typescript/src/Screens/Static/ResultScreen.d.ts.map +1 -1
  34. package/lib/typescript/src/Shared/Components/DiagnosticReportButton.d.ts +20 -0
  35. package/lib/typescript/src/Shared/Components/DiagnosticReportButton.d.ts.map +1 -0
  36. package/lib/typescript/src/Shared/Components/EIDScanner.d.ts.map +1 -1
  37. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.d.ts.map +1 -1
  38. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.types.d.ts +10 -0
  39. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.types.d.ts.map +1 -1
  40. package/lib/typescript/src/Shared/Components/NavigationManager.d.ts.map +1 -1
  41. package/lib/typescript/src/Shared/Components/TrustchexCamera.d.ts +10 -0
  42. package/lib/typescript/src/Shared/Components/TrustchexCamera.d.ts.map +1 -1
  43. package/lib/typescript/src/Shared/Contexts/AppContext.d.ts +5 -0
  44. package/lib/typescript/src/Shared/Contexts/AppContext.d.ts.map +1 -1
  45. package/lib/typescript/src/Shared/EIDReader/eidReader.d.ts +2 -0
  46. package/lib/typescript/src/Shared/EIDReader/eidReader.d.ts.map +1 -1
  47. package/lib/typescript/src/Shared/EIDReader/lds/icao/dg11File.d.ts +35 -0
  48. package/lib/typescript/src/Shared/EIDReader/lds/icao/dg11File.d.ts.map +1 -0
  49. package/lib/typescript/src/Shared/Libs/diagnosticReport.d.ts +48 -0
  50. package/lib/typescript/src/Shared/Libs/diagnosticReport.d.ts.map +1 -0
  51. package/lib/typescript/src/Shared/Libs/diagnostics.d.ts +145 -0
  52. package/lib/typescript/src/Shared/Libs/diagnostics.d.ts.map +1 -0
  53. package/lib/typescript/src/Shared/Libs/documentDataNormalizer.d.ts +12 -0
  54. package/lib/typescript/src/Shared/Libs/documentDataNormalizer.d.ts.map +1 -0
  55. package/lib/typescript/src/Shared/Libs/mrz.utils.d.ts.map +1 -1
  56. package/lib/typescript/src/Shared/Libs/mrzFrameAggregator.d.ts +33 -0
  57. package/lib/typescript/src/Shared/Libs/mrzFrameAggregator.d.ts.map +1 -1
  58. package/lib/typescript/src/Shared/Libs/mrzTransliteration.d.ts +3 -0
  59. package/lib/typescript/src/Shared/Libs/mrzTransliteration.d.ts.map +1 -0
  60. package/lib/typescript/src/Shared/Libs/native-device-info.utils.d.ts +33 -0
  61. package/lib/typescript/src/Shared/Libs/native-device-info.utils.d.ts.map +1 -1
  62. package/lib/typescript/src/Shared/Libs/sendDiagnosticReport.d.ts +30 -0
  63. package/lib/typescript/src/Shared/Libs/sendDiagnosticReport.d.ts.map +1 -0
  64. package/lib/typescript/src/Shared/Services/DataUploadService.d.ts +3 -2
  65. package/lib/typescript/src/Shared/Services/DataUploadService.d.ts.map +1 -1
  66. package/lib/typescript/src/Shared/Types/documentData.d.ts +44 -0
  67. package/lib/typescript/src/Shared/Types/documentData.d.ts.map +1 -0
  68. package/lib/typescript/src/Shared/Types/documentReadResult.d.ts +44 -0
  69. package/lib/typescript/src/Shared/Types/documentReadResult.d.ts.map +1 -0
  70. package/lib/typescript/src/Shared/Types/mrzFields.d.ts +1 -1
  71. package/lib/typescript/src/Shared/Types/mrzFields.d.ts.map +1 -1
  72. package/lib/typescript/src/Translation/Resources/en.d.ts +5 -0
  73. package/lib/typescript/src/Translation/Resources/en.d.ts.map +1 -1
  74. package/lib/typescript/src/Translation/Resources/tr.d.ts +5 -0
  75. package/lib/typescript/src/Translation/Resources/tr.d.ts.map +1 -1
  76. package/lib/typescript/src/Trustchex.d.ts +9 -0
  77. package/lib/typescript/src/Trustchex.d.ts.map +1 -1
  78. package/lib/typescript/src/index.d.ts +2 -0
  79. package/lib/typescript/src/index.d.ts.map +1 -1
  80. package/lib/typescript/src/version.d.ts +1 -1
  81. package/package.json +5 -1
  82. package/src/Screens/Dynamic/IdentityDocumentEIDScanningScreen.tsx +0 -4
  83. package/src/Screens/Dynamic/IdentityDocumentScanningScreen.tsx +23 -0
  84. package/src/Screens/Dynamic/VideoCallScreen.tsx +5 -1
  85. package/src/Screens/Static/ResultScreen.tsx +85 -16
  86. package/src/Shared/Components/DiagnosticReportButton.tsx +77 -0
  87. package/src/Shared/Components/EIDScanner.tsx +67 -18
  88. package/src/Shared/Components/IdentityDocumentCamera.tsx +145 -7
  89. package/src/Shared/Components/IdentityDocumentCamera.types.ts +6 -0
  90. package/src/Shared/Components/NavigationManager.tsx +7 -7
  91. package/src/Shared/Components/TrustchexCamera.tsx +6 -0
  92. package/src/Shared/Contexts/AppContext.ts +8 -0
  93. package/src/Shared/EIDReader/eidReader.ts +193 -47
  94. package/src/Shared/EIDReader/lds/icao/dg11File.ts +94 -0
  95. package/src/Shared/Libs/MRZ_KNOWN_ISSUES.md +7 -0
  96. package/src/Shared/Libs/diagnosticReport.ts +206 -0
  97. package/src/Shared/Libs/diagnostics.ts +251 -0
  98. package/src/Shared/Libs/documentDataNormalizer.ts +108 -0
  99. package/src/Shared/Libs/mrz.utils.ts +0 -5
  100. package/src/Shared/Libs/mrzFrameAggregator.ts +61 -3
  101. package/src/Shared/Libs/mrzTransliteration.ts +131 -0
  102. package/src/Shared/Libs/native-device-info.utils.ts +116 -0
  103. package/src/Shared/Libs/sendDiagnosticReport.ts +165 -0
  104. package/src/Shared/Services/DataUploadService.ts +9 -3
  105. package/src/Shared/Types/documentData.ts +52 -0
  106. package/src/Shared/Types/documentReadResult.ts +47 -0
  107. package/src/Shared/Types/mrzFields.ts +0 -5
  108. package/src/Translation/Resources/en.ts +6 -0
  109. package/src/Translation/Resources/tr.ts +6 -0
  110. package/src/Trustchex.tsx +48 -3
  111. package/src/index.tsx +7 -0
  112. package/src/version.ts +1 -1
@@ -4,11 +4,39 @@ import { NFCManagerCardService } from './nfcManagerCardService';
4
4
  import { EIDService } from './eidService';
5
5
  import { DG1File } from './lds/icao/dg1File';
6
6
  import { DG2File } from './lds/icao/dg2File';
7
+ import { DG11File } from './lds/icao/dg11File';
7
8
  import { FaceImageInfo } from './lds/iso19794/faceImageInfo';
8
9
  import { Buffer } from 'buffer';
9
10
  import { MRZInfo } from './lds/icao/mrzInfo';
10
11
  import { InputStream } from './java/inputStream';
11
12
  import { debugLog } from '../Libs/debug.utils';
13
+ import { diagnostics } from '../Libs/diagnostics';
14
+
15
+ /**
16
+ * Extract an APDU status word (hex, e.g. "6982") from an error message if the
17
+ * protocol layer included one. Returns undefined when none is present. Kept
18
+ * deliberately loose — the protocol senders embed "SW=...." / "(6982)" forms.
19
+ */
20
+ const extractStatusWord = (msg: string): string | undefined => {
21
+ const m = msg.match(/\b(?:SW[:= ]?)?([0-9a-fA-F]{4})\b/);
22
+ return m ? m[1].toUpperCase() : undefined;
23
+ };
24
+
25
+ /** Categorise an NFC error message into a coarse, non-PII bucket. */
26
+ const categorizeNfcError = (msg: string): string => {
27
+ const m = msg.toLowerCase();
28
+ if (m.includes('cancel')) return 'user_cancelled';
29
+ if (m.includes('lost') || m.includes('steady')) return 'tag_lost';
30
+ if (m.includes('timeout')) return 'timeout';
31
+ if (m.includes('not supported') || m.includes('unsupported')) {
32
+ return 'unsupported';
33
+ }
34
+ if (m.includes('not enabled') || m.includes('disabled')) return 'not_enabled';
35
+ if (m.includes('auth') || m.includes('mutual') || m.includes('security')) {
36
+ return 'auth_failed';
37
+ }
38
+ return 'reading_error';
39
+ };
12
40
 
13
41
  // --- Minimal PNG encoder (pure JS, no native deps) ---
14
42
  // Uses deflate stored (uncompressed) blocks for O(n) encoding speed.
@@ -295,7 +323,14 @@ const eidReader = async (
295
323
  dateOfExpiry: string,
296
324
  progressCallback?: (progress: number) => void
297
325
  ): Promise<
298
- { mrz?: MRZInfo; faceImage: string; mimeType: string } | undefined
326
+ | {
327
+ mrz?: MRZInfo;
328
+ faceImage: string;
329
+ mimeType: string;
330
+ dg11FirstName: string | null;
331
+ dg11LastName: string | null;
332
+ }
333
+ | undefined
299
334
  > => {
300
335
  let progress = 0;
301
336
  const nfcManagerCardService = new NFCManagerCardService();
@@ -307,8 +342,40 @@ const eidReader = async (
307
342
  true
308
343
  );
309
344
 
345
+ // Structured NFC diagnostics: record each step's outcome + timing + APDU SW
346
+ // into the session collector so the result screen can surface device-specific
347
+ // read failures. Best-effort and side-effect-only — never alters the flow.
348
+ diagnostics.nfcAttemptStarted();
349
+ const stepTimer = () => {
350
+ const t0 = Date.now();
351
+ return (step: string, ok: boolean, error?: unknown) => {
352
+ const msg = error instanceof Error ? error.message : error ? String(error) : undefined;
353
+ // Store only the APDU status word and a COARSE category — never the raw
354
+ // error text, which could in theory embed field/PII values. The SW + step
355
+ // name + category carry all the actionable signal.
356
+ diagnostics.nfcStep({
357
+ step,
358
+ ok,
359
+ ms: Date.now() - t0,
360
+ sw: msg ? extractStatusWord(msg) : undefined,
361
+ error: msg ? categorizeNfcError(msg) : undefined,
362
+ });
363
+ };
364
+ };
365
+
310
366
  try {
311
- await passportService.open();
367
+ {
368
+ const done = stepTimer();
369
+ try {
370
+ await passportService.open();
371
+ // Reaching open() without throwing means the chip powered on / was seen.
372
+ diagnostics.nfcChipDetected();
373
+ done('open', true);
374
+ } catch (e) {
375
+ done('open', false, e);
376
+ throw e;
377
+ }
378
+ }
312
379
 
313
380
  progress = 1;
314
381
  if (progressCallback) {
@@ -320,15 +387,20 @@ const eidReader = async (
320
387
  // regardless of which application the chip activates on power-on.
321
388
  let hasPACESucceeded = false;
322
389
 
323
- try {
324
- await passportService.sendSelectMF();
325
- debugLog('EID', '[EID] SELECT MF OK');
326
- } catch (mfErr) {
327
- const mfMsg = mfErr instanceof Error ? mfErr.message : String(mfErr);
328
- debugLog(
329
- 'EID',
330
- `[EID] SELECT MF failed (${mfMsg}), continuing with SFI read anyway`
331
- );
390
+ {
391
+ const done = stepTimer();
392
+ try {
393
+ await passportService.sendSelectMF();
394
+ done('selectMF', true);
395
+ debugLog('EID', '[EID] SELECT MF OK');
396
+ } catch (mfErr) {
397
+ done('selectMF', false, mfErr);
398
+ const mfMsg = mfErr instanceof Error ? mfErr.message : String(mfErr);
399
+ debugLog(
400
+ 'EID',
401
+ `[EID] SELECT MF failed (${mfMsg}), continuing with SFI read anyway`
402
+ );
403
+ }
332
404
  }
333
405
 
334
406
  progress = 2;
@@ -339,28 +411,33 @@ const eidReader = async (
339
411
  // Read EF.CardAccess — kept in a separate try/catch so we can distinguish
340
412
  // "file read failure" from "PACE protocol failure" in the logs.
341
413
  let cardAccessBuf: Buffer | null = null;
342
- try {
343
- const cardAccessStream = passportService.getInputStream(
344
- EIDService.EF_CARD_ACCESS
345
- );
346
- await cardAccessStream.init();
347
- const cardAccessLen = cardAccessStream.getLength();
348
- const buf = Buffer.alloc(cardAccessLen);
349
- for (let i = 0; i < cardAccessLen; i++) {
350
- buf[i] = await cardAccessStream.read();
414
+ {
415
+ const done = stepTimer();
416
+ try {
417
+ const cardAccessStream = passportService.getInputStream(
418
+ EIDService.EF_CARD_ACCESS
419
+ );
420
+ await cardAccessStream.init();
421
+ const cardAccessLen = cardAccessStream.getLength();
422
+ const buf = Buffer.alloc(cardAccessLen);
423
+ for (let i = 0; i < cardAccessLen; i++) {
424
+ buf[i] = await cardAccessStream.read();
425
+ }
426
+ cardAccessBuf = buf;
427
+ done('readCardAccess', true);
428
+ debugLog(
429
+ 'EID',
430
+ `[EID] EF.CardAccess read OK: ${cardAccessLen} bytes = ${cardAccessBuf.toString('hex')}`
431
+ );
432
+ } catch (readErr) {
433
+ done('readCardAccess', false, readErr);
434
+ const readMsg =
435
+ readErr instanceof Error ? readErr.message : String(readErr);
436
+ debugLog(
437
+ 'EID',
438
+ `[EID] EF.CardAccess read FAILED: ${readMsg} — falling back to BAC`
439
+ );
351
440
  }
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
441
  }
365
442
 
366
443
  progress = 3;
@@ -369,6 +446,11 @@ const eidReader = async (
369
446
  }
370
447
 
371
448
  // If EF.CardAccess was read successfully, attempt PACE.
449
+ if (cardAccessBuf === null) {
450
+ // EF.CardAccess unreadable (the readCardAccess step already recorded the
451
+ // failure/SW) → PACE can't be attempted; BAC will be used.
452
+ diagnostics.nfcPaceFellBackToBac('no_card_access');
453
+ }
372
454
  if (cardAccessBuf !== null) {
373
455
  try {
374
456
  const paceInfo = parsePACEInfoFromCardAccess(cardAccessBuf);
@@ -378,6 +460,7 @@ const eidReader = async (
378
460
  'EID',
379
461
  `[EID] PACE available: oid=${paceInfo.oid} parameterId=${paceInfo.parameterId}`
380
462
  );
463
+ diagnostics.nfcSetAuth('PACE', paceInfo.oid);
381
464
  // PACE is available - derive key from MRZ and perform PACE
382
465
  const bacKey = new BACKey(documentNumber, dateOfBirth, dateOfExpiry);
383
466
  const paceKey = PACEKeySpec.createMRZKey(bacKey.getKeySeedForPACE());
@@ -387,15 +470,23 @@ const eidReader = async (
387
470
  progressCallback(progress);
388
471
  }
389
472
 
390
- await passportService.doPACE(
391
- paceKey,
392
- paceInfo.oid,
393
- paceInfo.parameterId,
394
- progressCallback
395
- );
396
- hasPACESucceeded = true;
397
- debugLog('EID', '[EID] PACE succeeded');
473
+ const done = stepTimer();
474
+ try {
475
+ await passportService.doPACE(
476
+ paceKey,
477
+ paceInfo.oid,
478
+ paceInfo.parameterId,
479
+ progressCallback
480
+ );
481
+ hasPACESucceeded = true;
482
+ done('paceAuth', true);
483
+ debugLog('EID', '[EID] PACE succeeded');
484
+ } catch (paceErr) {
485
+ done('paceAuth', false, paceErr);
486
+ throw paceErr;
487
+ }
398
488
  } else {
489
+ diagnostics.nfcPaceFellBackToBac('no_pace_info');
399
490
  debugLog(
400
491
  'EID',
401
492
  '[EID] EF.CardAccess read OK but no PACE SecurityInfo found — document does not support PACE, falling back to BAC'
@@ -403,6 +494,7 @@ const eidReader = async (
403
494
  }
404
495
  } catch (paceError) {
405
496
  // PACE protocol exchange failed — fall back to BAC
497
+ diagnostics.nfcPaceFellBackToBac('pace_failed');
406
498
  const msg =
407
499
  paceError instanceof Error ? paceError.message : String(paceError);
408
500
  debugLog(
@@ -421,7 +513,16 @@ const eidReader = async (
421
513
  }
422
514
 
423
515
  // Select Applet for MRTD
424
- await passportService.sendSelectApplet(hasPACESucceeded);
516
+ {
517
+ const done = stepTimer();
518
+ try {
519
+ await passportService.sendSelectApplet(hasPACESucceeded);
520
+ done('selectApplet', true);
521
+ } catch (e) {
522
+ done('selectApplet', false, e);
523
+ throw e;
524
+ }
525
+ }
425
526
 
426
527
  progress = 10;
427
528
  if (progressCallback) {
@@ -436,8 +537,16 @@ const eidReader = async (
436
537
  await efComStream.read();
437
538
  } catch (error) {
438
539
  // EF_COM not available -> try to do BAC
540
+ diagnostics.nfcSetAuth('BAC');
439
541
  const bacKey = new BACKey(documentNumber, dateOfBirth, dateOfExpiry);
440
- await passportService.doBAC(bacKey);
542
+ const done = stepTimer();
543
+ try {
544
+ await passportService.doBAC(bacKey);
545
+ done('bacAuth', true);
546
+ } catch (e) {
547
+ done('bacAuth', false, e);
548
+ throw e;
549
+ }
441
550
  }
442
551
  }
443
552
 
@@ -447,17 +556,42 @@ const eidReader = async (
447
556
  }
448
557
 
449
558
  // Read DG1 (MRZ Info)
450
- const dg1InputStream = passportService.getInputStream(EIDService.EF_DG1);
451
- await dg1InputStream.init();
452
- const dg1File = new DG1File(undefined, dg1InputStream);
453
- const mrzInfo = await dg1File.getMRZInfo();
559
+ const dg1Done = stepTimer();
560
+ let mrzInfo: MRZInfo;
561
+ try {
562
+ const dg1InputStream = passportService.getInputStream(EIDService.EF_DG1);
563
+ await dg1InputStream.init();
564
+ const dg1File = new DG1File(undefined, dg1InputStream);
565
+ mrzInfo = await dg1File.getMRZInfo();
566
+ dg1Done('readDG1', true);
567
+ } catch (e) {
568
+ dg1Done('readDG1', false, e);
569
+ throw e;
570
+ }
454
571
 
455
572
  progress = 30;
456
573
  if (progressCallback) {
457
574
  progressCallback(progress);
458
575
  }
459
576
 
577
+ // Read DG11 (Additional Personal Data — printed name in UTF-8, optional)
578
+ let dg11FirstName: string | null = null;
579
+ let dg11LastName: string | null = null;
580
+ try {
581
+ const dg11InputStream = passportService.getInputStream(
582
+ EIDService.EF_DG11
583
+ );
584
+ await dg11InputStream.init();
585
+ const dg11File = new DG11File(dg11InputStream);
586
+ await dg11File.read();
587
+ dg11FirstName = dg11File.getFirstName();
588
+ dg11LastName = dg11File.getLastName();
589
+ } catch {
590
+ // DG11 is optional — not all chips have it, and access may be denied
591
+ }
592
+
460
593
  // Read DG2 (Face Image)
594
+ const dg2Done = stepTimer();
461
595
  const dg2InputStream = passportService.getInputStream(EIDService.EF_DG2);
462
596
  await dg2InputStream.init();
463
597
  InputStream.onRead((totalBytesRead, fileLength) => {
@@ -471,7 +605,14 @@ const eidReader = async (
471
605
  let mimeType = '';
472
606
 
473
607
  const allFaceImageInfos: FaceImageInfo[] = [];
474
- const faceInfos = await dg2File.getFaceInfos();
608
+ let faceInfos;
609
+ try {
610
+ faceInfos = await dg2File.getFaceInfos();
611
+ dg2Done('readDG2', true);
612
+ } catch (e) {
613
+ dg2Done('readDG2', false, e);
614
+ throw e;
615
+ }
475
616
  for (let faceInfo of faceInfos) {
476
617
  allFaceImageInfos.push(...faceInfo.getFaceImageInfos());
477
618
  }
@@ -493,12 +634,17 @@ const eidReader = async (
493
634
  progressCallback(progress);
494
635
  }
495
636
 
637
+ diagnostics.nfcResult(true);
496
638
  return {
497
639
  mrz: mrzInfo,
498
640
  faceImage: imageAsBase64,
499
641
  mimeType: mimeType,
642
+ dg11FirstName,
643
+ dg11LastName,
500
644
  };
501
645
  } catch (error) {
646
+ const msg = error instanceof Error ? error.message : String(error);
647
+ diagnostics.nfcResult(false, categorizeNfcError(msg));
502
648
  debugLog('EID', 'Error reading passport data', error);
503
649
  } finally {
504
650
  await passportService.close();
@@ -0,0 +1,94 @@
1
+ import { Buffer } from 'buffer';
2
+ import { InputStream } from '../../java/inputStream';
3
+ import { TLVInputStream } from '../../tlv/tlvInputStream';
4
+ import { DataGroup } from '../dataGroup';
5
+
6
+ /**
7
+ * DG11 — Additional Personal Data (ICAO 9303-10 §4.6.6).
8
+ *
9
+ * We only extract tag 0x5F0E (Name of Holder), which contains the full name
10
+ * exactly as printed on the document, in UTF-8, using the same `<<` primary/
11
+ * secondary delimiter as the MRZ (but with native Unicode characters).
12
+ *
13
+ * Example: "ÖZTÜRK<<GÖKHAN MEHMET" → lastName="ÖZTÜRK", firstName="GÖKHAN MEHMET"
14
+ *
15
+ * The file is optional on the chip — if absent or unreadable we return null
16
+ * so the caller can fall back to reverse transliteration.
17
+ */
18
+ export class DG11File extends DataGroup {
19
+ private static readonly DG11_TAG = 0x6b;
20
+ private static readonly TAG_LIST_TAG = 0x5c;
21
+ private static readonly NAME_OF_HOLDER_TAG = 0x5f0e;
22
+
23
+ private firstName: string | null = null;
24
+ private lastName: string | null = null;
25
+
26
+ constructor(is?: InputStream) {
27
+ super(DG11File.DG11_TAG, is);
28
+ }
29
+
30
+ /** Returns the first name from DG11, or null if not present. */
31
+ getFirstName(): string | null {
32
+ return this.firstName;
33
+ }
34
+
35
+ /** Returns the last name from DG11, or null if not present. */
36
+ getLastName(): string | null {
37
+ return this.lastName;
38
+ }
39
+
40
+ async read(): Promise<void> {
41
+ await super.readObject();
42
+ }
43
+
44
+ protected async readContent(inputStream: InputStream): Promise<void> {
45
+ const tlvIn =
46
+ inputStream instanceof TLVInputStream
47
+ ? inputStream
48
+ : new TLVInputStream(inputStream);
49
+
50
+ // Skip the tag list (0x5C) — we only care about 0x5F0E
51
+ // The tag list tells us which optional tags are present; we probe directly.
52
+ try {
53
+ await tlvIn.skipToTag(DG11File.NAME_OF_HOLDER_TAG);
54
+ } catch {
55
+ // Tag not present in this chip's DG11
56
+ return;
57
+ }
58
+
59
+ const length = await tlvIn.readLength();
60
+ if (length <= 0) return;
61
+
62
+ const bytes = new Uint8Array(length);
63
+ await inputStream.readBytesWithOffset(bytes, 0, length);
64
+
65
+ const nameString = Buffer.from(bytes).toString('utf8').trim();
66
+ this.parseName(nameString);
67
+ }
68
+
69
+ protected writeContent(): void {
70
+ // Write not needed for reading
71
+ }
72
+
73
+ /**
74
+ * Split "PRIMARY<<SECONDARY" into last/first name components.
75
+ * Strips trailing filler `<` within each segment.
76
+ */
77
+ private parseName(nameString: string): void {
78
+ const delimIndex = nameString.indexOf('<<');
79
+ if (delimIndex < 0) {
80
+ this.lastName = nameString.replace(/<+$/, '').trim();
81
+ this.firstName = '';
82
+ return;
83
+ }
84
+ this.lastName = nameString
85
+ .substring(0, delimIndex)
86
+ .replace(/<+$/, '')
87
+ .trim();
88
+ this.firstName = nameString
89
+ .substring(delimIndex + 2)
90
+ .replace(/<$/, ' ')
91
+ .replace(/<+$/, '')
92
+ .trim();
93
+ }
94
+ }
@@ -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
+ };