@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
@@ -13,6 +13,7 @@ import { useKeepAwake } from "../Libs/native-keep-awake.utils.js";
13
13
  import { useIsFocused } from '@react-navigation/native';
14
14
  import { useTranslation } from 'react-i18next';
15
15
  import { debugLog, logError, isDebugEnabled } from "../Libs/debug.utils.js";
16
+ import { diagnostics } from "../Libs/diagnostics.js";
16
17
  import LottieView from 'lottie-react-native';
17
18
  import StyledButton from "./StyledButton.js";
18
19
  import { SafeAreaView } from 'react-native-safe-area-context';
@@ -52,8 +53,21 @@ const IdentityDocumentCamera = ({
52
53
  }, []);
53
54
  const isCameraInitialized = useRef(false);
54
55
  const [currentFaceImage, setCurrentFaceImage] = useState(undefined);
56
+ // Synchronous mirror of the locked face image (see currentHologramImageRef).
57
+ // Used as a fallback when completing SCAN_HOLOGRAM: the torch is on during
58
+ // hologram capture so the live frame's face is often rejected, leaving the
59
+ // state-derived faceImageToUse null within the stale closure — without this
60
+ // the completion gate's `&& !!faceImageToUse` would never pass.
61
+ const currentFaceImageRef = useRef(undefined);
55
62
  const [_currentHologramMaskImage, setCurrentHologramMaskImage] = useState(undefined);
56
63
  const [currentHologramImage, setCurrentHologramImage] = useState(undefined);
64
+ // Mirror of currentHologramImage for synchronous reads inside the per-frame
65
+ // callback. The setState above is async and currentHologramImage is a
66
+ // useCallback dependency, so frames processed before React re-renders the
67
+ // closure would otherwise still see the old (undefined) value — causing the
68
+ // SCAN_HOLOGRAM step to re-collect faces forever and never complete after a
69
+ // successful detection. Always keep this ref in lockstep with the state.
70
+ const currentHologramImageRef = useRef(undefined);
57
71
  const [currentSecondaryFaceImage, setCurrentSecondaryFaceImage] = useState(undefined);
58
72
  const [isBrightnessLow, setIsBrightnessLow] = useState(false);
59
73
  const [isFrameBlurry, setIsFrameBlurry] = useState(false);
@@ -110,10 +124,28 @@ const IdentityDocumentCamera = ({
110
124
  const isHologramDetectionInProgress = useRef(false); // Prevent concurrent hologram detection calls
111
125
  const isCompletionCallbackInvoked = useRef(false); // Prevent multiple callback invocations when COMPLETED
112
126
  const isStepTransitionInProgress = useRef(false); // Prevent duplicate step transitions from rapid frames
127
+ // The step a transition is currently in progress FROM. The latch above is
128
+ // reset asynchronously (via a nextStep effect), so in fast flows (e.g. demo
129
+ // mode auto-advancing) a later transition from a DIFFERENT step could be
130
+ // wrongly blocked while the latch is still set for the previous step. Gating
131
+ // on the originating step lets a genuine next-step transition through while
132
+ // still de-duping rapid repeat frames of the SAME transition.
133
+ const stepTransitionFrom = useRef(null);
113
134
  const isCompletionAnnouncementSpoken = useRef(false); // Prevent duplicate "scan completed" voice guidance
114
135
 
115
136
  const faceDetectionErrorCount = useRef(0);
116
137
  const brightnessHistory = useRef([]);
138
+ // Running camera-quality stats for the support diagnostic report.
139
+ const diagStats = useRef({
140
+ frames: 0,
141
+ brightnessSum: 0,
142
+ minBrightness: 255,
143
+ maxBrightness: 0,
144
+ frameW: 0,
145
+ frameH: 0,
146
+ hadLowBrightness: false,
147
+ hadBlurryFrames: false
148
+ });
117
149
  const [faceDetectionEnabled, setFaceDetectionEnabled] = useState(true);
118
150
  const faceImages = useRef([]);
119
151
  const hologramImageCountRef = useRef(0);
@@ -151,6 +183,7 @@ const IdentityDocumentCamera = ({
151
183
  setIsActive(true);
152
184
  isCompletionCallbackInvoked.current = false; // Reset callback flag when starting new scan
153
185
  isStepTransitionInProgress.current = false;
186
+ stepTransitionFrom.current = null;
154
187
  isCompletionAnnouncementSpoken.current = false;
155
188
  } else {
156
189
  setIsActive(false);
@@ -166,12 +199,27 @@ const IdentityDocumentCamera = ({
166
199
  validMRZConsecutiveCount.current = 0;
167
200
  mrzAggregator.current.reset();
168
201
  setMrzReliable(false);
202
+ // Start a fresh diagnostics session for the support report.
203
+ diagnostics.reset(Date.now());
204
+ diagStats.current = {
205
+ frames: 0,
206
+ brightnessSum: 0,
207
+ minBrightness: 255,
208
+ maxBrightness: 0,
209
+ frameW: 0,
210
+ frameH: 0,
211
+ hadLowBrightness: false,
212
+ hadBlurryFrames: false
213
+ };
169
214
  cachedBarcode.current = null;
170
215
  isCompletionCallbackInvoked.current = false;
171
216
  isStepTransitionInProgress.current = false;
217
+ stepTransitionFrom.current = null;
172
218
  isCompletionAnnouncementSpoken.current = false;
173
219
  // Clear all captured image states from previous scan
220
+ currentFaceImageRef.current = undefined;
174
221
  setCurrentFaceImage(undefined);
222
+ currentHologramImageRef.current = undefined;
175
223
  setCurrentHologramImage(undefined);
176
224
  setCurrentSecondaryFaceImage(undefined);
177
225
  resetLastMessage();
@@ -184,9 +232,12 @@ const IdentityDocumentCamera = ({
184
232
  setLatestHologramFaceImage(undefined);
185
233
  isCompletionCallbackInvoked.current = false; // Reset callback flag on unmount
186
234
  isStepTransitionInProgress.current = false;
235
+ stepTransitionFrom.current = null;
187
236
  isCompletionAnnouncementSpoken.current = false;
188
237
  // Clear all captured image states on unmount
238
+ currentFaceImageRef.current = undefined;
189
239
  setCurrentFaceImage(undefined);
240
+ currentHologramImageRef.current = undefined;
190
241
  setCurrentHologramImage(undefined);
191
242
  setCurrentSecondaryFaceImage(undefined);
192
243
  resetLastMessage();
@@ -232,6 +283,7 @@ const IdentityDocumentCamera = ({
232
283
  isCompletionCallbackInvoked.current = false;
233
284
  }
234
285
  isStepTransitionInProgress.current = false;
286
+ stepTransitionFrom.current = null;
235
287
  }, [nextStep]);
236
288
 
237
289
  // Update status bar based on guide visibility
@@ -365,6 +417,7 @@ const IdentityDocumentCamera = ({
365
417
  setHologramImageCount(0);
366
418
  setLatestHologramFaceImage(undefined);
367
419
  // Clear previous hologram state to prevent premature completion
420
+ currentHologramImageRef.current = undefined;
368
421
  setCurrentHologramImage(undefined);
369
422
  setCurrentHologramMaskImage(undefined);
370
423
  }
@@ -408,10 +461,14 @@ const IdentityDocumentCamera = ({
408
461
  }
409
462
  }, [setIsTorchOn]);
410
463
  const transitionStepWithCallback = useCallback((nextStepType, fromStep, scannedData) => {
411
- if (isStepTransitionInProgress.current) {
464
+ // De-dupe only rapid repeat frames of the SAME originating step. A
465
+ // transition from a different step is a genuine advance (the async latch
466
+ // reset may not have run yet in fast flows like demo mode), so allow it.
467
+ if (isStepTransitionInProgress.current && stepTransitionFrom.current === fromStep) {
412
468
  return;
413
469
  }
414
470
  isStepTransitionInProgress.current = true;
471
+ stepTransitionFrom.current = fromStep;
415
472
 
416
473
  // Torch only needed during SCAN_HOLOGRAM - turn off for all other transitions
417
474
  if (nextStepType !== 'SCAN_HOLOGRAM') {
@@ -567,9 +624,30 @@ const IdentityDocumentCamera = ({
567
624
  // of letting one noisy frame reset progress.
568
625
  const mrzBandHeight = scannedText?.blocks?.length ? Math.max(0, ...scannedText.blocks.map(b => b.blockFrame?.height ?? 0)) : 0;
569
626
  const frameWeight = mrzBandHeight > 0 ? mrzBandHeight : 1;
627
+ // ML Kit per-symbol confidence over MRZ characters (Android bundled library
628
+ // only; undefined on iOS). When present it down-weights frames the
629
+ // recognizer was unsure about, so a confident frame outvotes a shaky one of
630
+ // the same band height on look-alike ties (A↔4, S↔5, B↔8).
631
+ const frameConfidence = scannedText?.textConfidence?.mean;
570
632
  const consensus = mrzAggregator.current.addFrame({
571
633
  text: textForValidation,
572
- weight: frameWeight
634
+ weight: frameWeight,
635
+ confidence: frameConfidence
636
+ });
637
+
638
+ // Record MRZ consensus metrics for the support diagnostic report (counts
639
+ // and pass/fail only — never the MRZ values themselves). `reachedStable`
640
+ // is sticky in the collector, so a later flicker can't unset it.
641
+ // `stabilityFrames`/`minContestedMargin` and the MRZ band pixel height are
642
+ // device-variance signals (convergence speed, look-alike resolution
643
+ // confidence, and whether the band cleared the ~16px/char OCR floor).
644
+ diagnostics.setMrz({
645
+ framesProcessed: consensus.frames,
646
+ reachedStable: consensus.stable,
647
+ format: consensus.validation?.format ?? undefined,
648
+ stabilityFrames: consensus.stableStreak,
649
+ minContestedMargin: Number.isFinite(consensus.minContestedMargin) ? consensus.minContestedMargin : 0,
650
+ maxMrzBandHeightPx: mrzBandHeight
573
651
  });
574
652
 
575
653
  // Use the fused consensus when it is valid; otherwise fall back to a
@@ -593,6 +671,9 @@ const IdentityDocumentCamera = ({
593
671
  // be wrong (ID_FRONT locked before passport MRZ became readable)
594
672
  const documentType = nextStep === 'SCAN_ID_FRONT_OR_PASSPORT' ? detectDocumentType(primaryFaces, text, parsedMRZData?.fields, frameWidth, mrzText) : parsedMRZData?.fields?.documentCode === 'P' ? 'PASSPORT' : detectedDocumentType;
595
673
 
674
+ // Record the detected document type for the diagnostic report.
675
+ if (documentType) diagnostics.setDocument(documentType);
676
+
596
677
  // Crop faces once document type is confirmed or we're past the initial step
597
678
  const shouldCropFaces = documentType === 'ID_FRONT' || documentType === 'PASSPORT' || nextStep !== 'SCAN_ID_FRONT_OR_PASSPORT';
598
679
  const croppedFaces = shouldCropFaces ? await getFaceImages(primaryFaces, image ?? '', frameWidth, frameHeight) : [];
@@ -646,6 +727,7 @@ const IdentityDocumentCamera = ({
646
727
  // Continue scanning without locking this face
647
728
  } else {
648
729
  faceImageToUse = croppedFaces[0];
730
+ currentFaceImageRef.current = croppedFaces[0];
649
731
  setCurrentFaceImage(croppedFaces[0]);
650
732
  }
651
733
  }
@@ -860,8 +942,8 @@ const IdentityDocumentCamera = ({
860
942
  // Skip face position validation for hologram — flash toggling causes position jitter
861
943
  if (primaryFaceOnly) {
862
944
  hologramFramesWithoutFace.current = 0;
863
- if (currentHologramImage) {
864
- scannedData.hologramImage = currentHologramImage;
945
+ if (currentHologramImageRef.current) {
946
+ scannedData.hologramImage = currentHologramImageRef.current;
865
947
  } else if (faceImages.current.length < HOLOGRAM_IMAGE_COUNT) {
866
948
  // Space out captures for better variation
867
949
  const timeSinceLastCapture = Date.now() - lastHologramCaptureTime.current;
@@ -921,6 +1003,7 @@ const IdentityDocumentCamera = ({
921
1003
  if (hologram) {
922
1004
  setCurrentHologramMaskImage(hologramMask);
923
1005
  scannedData.hologramImage = hologram;
1006
+ currentHologramImageRef.current = hologram;
924
1007
  setCurrentHologramImage(hologram);
925
1008
  if (isDebugEnabled()) {
926
1009
  debugLog('IdentityDocumentCamera', '[Hologram] ✓ Saved hologram image');
@@ -946,8 +1029,8 @@ const IdentityDocumentCamera = ({
946
1029
  } else {
947
1030
  hologramFramesWithoutFace.current++;
948
1031
  }
949
- } else if (currentHologramImage) {
950
- scannedData.hologramImage = currentHologramImage;
1032
+ } else if (currentHologramImageRef.current) {
1033
+ scannedData.hologramImage = currentHologramImageRef.current;
951
1034
  }
952
1035
 
953
1036
  // Secondary face capture (continuous during initial scan and hologram detection)
@@ -1081,10 +1164,20 @@ const IdentityDocumentCamera = ({
1081
1164
  return;
1082
1165
  }
1083
1166
 
1084
- // Complete when hologram captured OR retries exhausted (not mid-collection)
1085
- const stepComplete = (!!scannedData.hologramImage || maxRetriesReached && !isCollecting) && !!faceImageToUse; // Require face before completing
1167
+ // Complete when hologram captured OR retries exhausted (not mid-collection).
1168
+ // Fall back to the previously locked face (ref) when the live frame's face
1169
+ // was rejected — the torch is on during hologram capture, so faceImageToUse
1170
+ // is frequently null even though a valid face was locked during the front
1171
+ // scan. Without this the gate's face requirement could never be satisfied.
1172
+ const faceForCompletion = faceImageToUse ?? currentFaceImageRef.current;
1173
+ const stepComplete = (!!scannedData.hologramImage || maxRetriesReached && !isCollecting) && !!faceForCompletion; // Require face before completing
1086
1174
 
1087
1175
  if (stepComplete) {
1176
+ // Make sure the locked face rides along even if this frame's face was
1177
+ // rejected.
1178
+ if (!scannedData.faceImage && faceForCompletion) {
1179
+ scannedData.faceImage = faceForCompletion;
1180
+ }
1088
1181
  // Ensure preserved MRZ data is included (current frame may not have
1089
1182
  // readable MRZ due to flash/tilting during hologram capture)
1090
1183
  if (!scannedData.mrzText && lastValidMRZText.current) {
@@ -1196,6 +1289,15 @@ const IdentityDocumentCamera = ({
1196
1289
  brightnessHistory.current.shift();
1197
1290
  }
1198
1291
  const avgBrightness = brightnessHistory.current.reduce((a, b) => a + b, 0) / brightnessHistory.current.length;
1292
+
1293
+ // Accumulate camera diagnostics (frame count, brightness range, dims) for
1294
+ // the support diagnostic report. Cheap, side-effect-only.
1295
+ diagStats.current.frames += 1;
1296
+ diagStats.current.brightnessSum += frameBrightness;
1297
+ diagStats.current.minBrightness = Math.min(diagStats.current.minBrightness, frameBrightness);
1298
+ diagStats.current.maxBrightness = Math.max(diagStats.current.maxBrightness, frameBrightness);
1299
+ diagStats.current.frameW = frame.width ?? diagStats.current.frameW;
1300
+ diagStats.current.frameH = frame.height ?? diagStats.current.frameH;
1199
1301
  const isOverallBright = avgBrightness >= MIN_BRIGHTNESS_THRESHOLD;
1200
1302
 
1201
1303
  // Check brightness in center region (area of interest)
@@ -1206,6 +1308,7 @@ const IdentityDocumentCamera = ({
1206
1308
  isRegionBright = isOverallBright;
1207
1309
  }
1208
1310
  setIsBrightnessLow(!isRegionBright);
1311
+ if (!isRegionBright) diagStats.current.hadLowBrightness = true;
1209
1312
 
1210
1313
  // Check blur only in center region (area of interest) to avoid false positives
1211
1314
  // from iOS depth-of-field background blur
@@ -1226,10 +1329,30 @@ const IdentityDocumentCamera = ({
1226
1329
  );
1227
1330
  isNotBlurry = !isBlurry;
1228
1331
  setIsFrameBlurry(isBlurry);
1332
+ if (isBlurry) diagStats.current.hadBlurryFrames = true;
1229
1333
  } catch (error) {
1230
1334
  setIsFrameBlurry(false);
1231
1335
  }
1232
1336
 
1337
+ // Flush accumulated camera stats into the diagnostics collector.
1338
+ const ds = diagStats.current;
1339
+ diagnostics.setCamera({
1340
+ frames: ds.frames,
1341
+ frameW: ds.frameW,
1342
+ frameH: ds.frameH,
1343
+ avgBrightness: ds.frames > 0 ? Math.round(ds.brightnessSum / ds.frames) : 0,
1344
+ minBrightness: ds.minBrightness === 255 && ds.frames === 0 ? 0 : ds.minBrightness,
1345
+ maxBrightness: ds.maxBrightness,
1346
+ hadLowBrightness: ds.hadLowBrightness,
1347
+ hadBlurryFrames: ds.hadBlurryFrames,
1348
+ // The actual gates the measured values are judged against — lets support
1349
+ // read "measured 45 vs floor 45" instead of just a pass/fail flag.
1350
+ thresholds: {
1351
+ minBrightness: MIN_BRIGHTNESS_THRESHOLD,
1352
+ blur: 50
1353
+ }
1354
+ });
1355
+
1233
1356
  // Only proceed if image quality is acceptable
1234
1357
  const hasAcceptableQuality = isRegionBright && isNotBlurry;
1235
1358
 
@@ -1274,6 +1397,7 @@ const IdentityDocumentCamera = ({
1274
1397
  resultText,
1275
1398
  mrzOnlyText: undefined,
1276
1399
  // Will be set below if MRZ blocks detected
1400
+ textConfidence: frame.textConfidence,
1277
1401
  blocks: textBlocks.map(block => ({
1278
1402
  blockText: block.text || '',
1279
1403
  blockFrame: block.blockFrame ?? {
@@ -5,7 +5,6 @@ import AppContext from "../Contexts/AppContext.js";
5
5
  import { CommonActions, useNavigation, usePreventRemove } from '@react-navigation/native';
6
6
  import { View, StyleSheet, Alert } from 'react-native';
7
7
  import { useTranslation } from 'react-i18next';
8
- import { useSafeAreaInsets } from 'react-native-safe-area-context';
9
8
  import i18n from "../../Translation/index.js";
10
9
  import StyledButton from "./StyledButton.js";
11
10
  import { analyticsService } from "../Services/AnalyticsService.js";
@@ -26,7 +25,6 @@ const NavigationManager = /*#__PURE__*/forwardRef(({
26
25
  const {
27
26
  t
28
27
  } = useTranslation();
29
- const insets = useSafeAreaInsets();
30
28
  const routes = {
31
29
  VERIFICATION_SESSION_CHECK: 'VerificationSessionCheckScreen',
32
30
  DYNAMIC_ROUTES: {
@@ -185,9 +183,7 @@ const NavigationManager = /*#__PURE__*/forwardRef(({
185
183
  allowSkipStep: appContext.currentWorkflowStep?.required
186
184
  }));
187
185
  return appContext.currentWorkflowStep && (!appContext.currentWorkflowStep?.required || canSkipStep) && /*#__PURE__*/_jsx(View, {
188
- style: [styles.container, {
189
- paddingBottom: insets.bottom
190
- }],
186
+ style: styles.container,
191
187
  children: /*#__PURE__*/_jsx(StyledButton, {
192
188
  mode: "text",
193
189
  onPress: goToNextRouteWithAlert,
@@ -196,11 +192,13 @@ const NavigationManager = /*#__PURE__*/forwardRef(({
196
192
  });
197
193
  });
198
194
  const styles = StyleSheet.create({
195
+ // Hug the text button: centered, minimal padding, no full-width opaque bar so
196
+ // it occupies the least area and never overlays sibling content. The screen's
197
+ // footer wrapper already applies the bottom safe-area inset.
199
198
  container: {
200
- justifyContent: 'center',
201
- alignItems: 'center',
202
- padding: 20,
203
- backgroundColor: 'white'
199
+ alignSelf: 'center',
200
+ paddingHorizontal: 16,
201
+ paddingVertical: 8
204
202
  }
205
203
  });
206
204
  export default NavigationManager;
@@ -24,8 +24,11 @@ export default /*#__PURE__*/createContext({
24
24
  currentWorkflowStep: undefined,
25
25
  isDebugNavigated: false,
26
26
  isTestVideoSession: false,
27
+ lastDocumentRead: undefined,
27
28
  onCompleted: undefined,
28
29
  onError: undefined,
30
+ onDocumentRead: undefined,
31
+ setLastDocumentRead: undefined,
29
32
  setSessionId: undefined,
30
33
  setBaseUrl: undefined
31
34
  });
@@ -6,9 +6,37 @@ import { NFCManagerCardService } from "./nfcManagerCardService.js";
6
6
  import { EIDService } from "./eidService.js";
7
7
  import { DG1File } from "./lds/icao/dg1File.js";
8
8
  import { DG2File } from "./lds/icao/dg2File.js";
9
+ import { DG11File } from "./lds/icao/dg11File.js";
9
10
  import { Buffer } from 'buffer';
10
11
  import { InputStream } from "./java/inputStream.js";
11
12
  import { debugLog } from "../Libs/debug.utils.js";
13
+ import { diagnostics } from "../Libs/diagnostics.js";
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 => {
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 => {
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.
@@ -275,8 +303,40 @@ const eidReader = async (documentNumber, dateOfBirth, dateOfExpiry, progressCall
275
303
  let progress = 0;
276
304
  const nfcManagerCardService = new NFCManagerCardService();
277
305
  const passportService = new EIDService(nfcManagerCardService, EIDService.NORMAL_MAX_TRANSCEIVE_LENGTH, EIDService.DEFAULT_MAX_BLOCKSIZE, false, true);
306
+
307
+ // Structured NFC diagnostics: record each step's outcome + timing + APDU SW
308
+ // into the session collector so the result screen can surface device-specific
309
+ // read failures. Best-effort and side-effect-only — never alters the flow.
310
+ diagnostics.nfcAttemptStarted();
311
+ const stepTimer = () => {
312
+ const t0 = Date.now();
313
+ return (step, ok, error) => {
314
+ const msg = error instanceof Error ? error.message : error ? String(error) : undefined;
315
+ // Store only the APDU status word and a COARSE category — never the raw
316
+ // error text, which could in theory embed field/PII values. The SW + step
317
+ // name + category carry all the actionable signal.
318
+ diagnostics.nfcStep({
319
+ step,
320
+ ok,
321
+ ms: Date.now() - t0,
322
+ sw: msg ? extractStatusWord(msg) : undefined,
323
+ error: msg ? categorizeNfcError(msg) : undefined
324
+ });
325
+ };
326
+ };
278
327
  try {
279
- await passportService.open();
328
+ {
329
+ const done = stepTimer();
330
+ try {
331
+ await passportService.open();
332
+ // Reaching open() without throwing means the chip powered on / was seen.
333
+ diagnostics.nfcChipDetected();
334
+ done('open', true);
335
+ } catch (e) {
336
+ done('open', false, e);
337
+ throw e;
338
+ }
339
+ }
280
340
  progress = 1;
281
341
  if (progressCallback) {
282
342
  progressCallback(progress);
@@ -286,12 +346,17 @@ const eidReader = async (documentNumber, dateOfBirth, dateOfExpiry, progressCall
286
346
  // Explicitly SELECT MF first so the SFI=0x1C context is correct,
287
347
  // regardless of which application the chip activates on power-on.
288
348
  let hasPACESucceeded = false;
289
- try {
290
- await passportService.sendSelectMF();
291
- debugLog('EID', '[EID] SELECT MF OK');
292
- } catch (mfErr) {
293
- const mfMsg = mfErr instanceof Error ? mfErr.message : String(mfErr);
294
- debugLog('EID', `[EID] SELECT MF failed (${mfMsg}), continuing with SFI read anyway`);
349
+ {
350
+ const done = stepTimer();
351
+ try {
352
+ await passportService.sendSelectMF();
353
+ done('selectMF', true);
354
+ debugLog('EID', '[EID] SELECT MF OK');
355
+ } catch (mfErr) {
356
+ done('selectMF', false, mfErr);
357
+ const mfMsg = mfErr instanceof Error ? mfErr.message : String(mfErr);
358
+ debugLog('EID', `[EID] SELECT MF failed (${mfMsg}), continuing with SFI read anyway`);
359
+ }
295
360
  }
296
361
  progress = 2;
297
362
  if (progressCallback) {
@@ -301,19 +366,24 @@ const eidReader = async (documentNumber, dateOfBirth, dateOfExpiry, progressCall
301
366
  // Read EF.CardAccess — kept in a separate try/catch so we can distinguish
302
367
  // "file read failure" from "PACE protocol failure" in the logs.
303
368
  let cardAccessBuf = null;
304
- try {
305
- const cardAccessStream = passportService.getInputStream(EIDService.EF_CARD_ACCESS);
306
- await cardAccessStream.init();
307
- const cardAccessLen = cardAccessStream.getLength();
308
- const buf = Buffer.alloc(cardAccessLen);
309
- for (let i = 0; i < cardAccessLen; i++) {
310
- buf[i] = await cardAccessStream.read();
369
+ {
370
+ const done = stepTimer();
371
+ try {
372
+ const cardAccessStream = passportService.getInputStream(EIDService.EF_CARD_ACCESS);
373
+ await cardAccessStream.init();
374
+ const cardAccessLen = cardAccessStream.getLength();
375
+ const buf = Buffer.alloc(cardAccessLen);
376
+ for (let i = 0; i < cardAccessLen; i++) {
377
+ buf[i] = await cardAccessStream.read();
378
+ }
379
+ cardAccessBuf = buf;
380
+ done('readCardAccess', true);
381
+ debugLog('EID', `[EID] EF.CardAccess read OK: ${cardAccessLen} bytes = ${cardAccessBuf.toString('hex')}`);
382
+ } catch (readErr) {
383
+ done('readCardAccess', false, readErr);
384
+ const readMsg = readErr instanceof Error ? readErr.message : String(readErr);
385
+ debugLog('EID', `[EID] EF.CardAccess read FAILED: ${readMsg} — falling back to BAC`);
311
386
  }
312
- cardAccessBuf = buf;
313
- debugLog('EID', `[EID] EF.CardAccess read OK: ${cardAccessLen} bytes = ${cardAccessBuf.toString('hex')}`);
314
- } catch (readErr) {
315
- const readMsg = readErr instanceof Error ? readErr.message : String(readErr);
316
- debugLog('EID', `[EID] EF.CardAccess read FAILED: ${readMsg} — falling back to BAC`);
317
387
  }
318
388
  progress = 3;
319
389
  if (progressCallback) {
@@ -321,11 +391,17 @@ const eidReader = async (documentNumber, dateOfBirth, dateOfExpiry, progressCall
321
391
  }
322
392
 
323
393
  // If EF.CardAccess was read successfully, attempt PACE.
394
+ if (cardAccessBuf === null) {
395
+ // EF.CardAccess unreadable (the readCardAccess step already recorded the
396
+ // failure/SW) → PACE can't be attempted; BAC will be used.
397
+ diagnostics.nfcPaceFellBackToBac('no_card_access');
398
+ }
324
399
  if (cardAccessBuf !== null) {
325
400
  try {
326
401
  const paceInfo = parsePACEInfoFromCardAccess(cardAccessBuf);
327
402
  if (paceInfo) {
328
403
  debugLog('EID', `[EID] PACE available: oid=${paceInfo.oid} parameterId=${paceInfo.parameterId}`);
404
+ diagnostics.nfcSetAuth('PACE', paceInfo.oid);
329
405
  // PACE is available - derive key from MRZ and perform PACE
330
406
  const bacKey = new BACKey(documentNumber, dateOfBirth, dateOfExpiry);
331
407
  const paceKey = PACEKeySpec.createMRZKey(bacKey.getKeySeedForPACE());
@@ -333,14 +409,23 @@ const eidReader = async (documentNumber, dateOfBirth, dateOfExpiry, progressCall
333
409
  if (progressCallback) {
334
410
  progressCallback(progress);
335
411
  }
336
- await passportService.doPACE(paceKey, paceInfo.oid, paceInfo.parameterId, progressCallback);
337
- hasPACESucceeded = true;
338
- debugLog('EID', '[EID] PACE succeeded');
412
+ const done = stepTimer();
413
+ try {
414
+ await passportService.doPACE(paceKey, paceInfo.oid, paceInfo.parameterId, progressCallback);
415
+ hasPACESucceeded = true;
416
+ done('paceAuth', true);
417
+ debugLog('EID', '[EID] PACE succeeded');
418
+ } catch (paceErr) {
419
+ done('paceAuth', false, paceErr);
420
+ throw paceErr;
421
+ }
339
422
  } else {
423
+ diagnostics.nfcPaceFellBackToBac('no_pace_info');
340
424
  debugLog('EID', '[EID] EF.CardAccess read OK but no PACE SecurityInfo found — document does not support PACE, falling back to BAC');
341
425
  }
342
426
  } catch (paceError) {
343
427
  // PACE protocol exchange failed — fall back to BAC
428
+ diagnostics.nfcPaceFellBackToBac('pace_failed');
344
429
  const msg = paceError instanceof Error ? paceError.message : String(paceError);
345
430
  debugLog('EID', `[EID] PACE protocol failed (${msg}), falling back to BAC`);
346
431
  if (paceError instanceof Error && paceError.stack) {
@@ -354,7 +439,16 @@ const eidReader = async (documentNumber, dateOfBirth, dateOfExpiry, progressCall
354
439
  }
355
440
 
356
441
  // Select Applet for MRTD
357
- await passportService.sendSelectApplet(hasPACESucceeded);
442
+ {
443
+ const done = stepTimer();
444
+ try {
445
+ await passportService.sendSelectApplet(hasPACESucceeded);
446
+ done('selectApplet', true);
447
+ } catch (e) {
448
+ done('selectApplet', false, e);
449
+ throw e;
450
+ }
451
+ }
358
452
  progress = 10;
359
453
  if (progressCallback) {
360
454
  progressCallback(progress);
@@ -367,8 +461,16 @@ const eidReader = async (documentNumber, dateOfBirth, dateOfExpiry, progressCall
367
461
  await efComStream.read();
368
462
  } catch (error) {
369
463
  // EF_COM not available -> try to do BAC
464
+ diagnostics.nfcSetAuth('BAC');
370
465
  const bacKey = new BACKey(documentNumber, dateOfBirth, dateOfExpiry);
371
- await passportService.doBAC(bacKey);
466
+ const done = stepTimer();
467
+ try {
468
+ await passportService.doBAC(bacKey);
469
+ done('bacAuth', true);
470
+ } catch (e) {
471
+ done('bacAuth', false, e);
472
+ throw e;
473
+ }
372
474
  }
373
475
  }
374
476
  progress = 20;
@@ -377,16 +479,39 @@ const eidReader = async (documentNumber, dateOfBirth, dateOfExpiry, progressCall
377
479
  }
378
480
 
379
481
  // Read DG1 (MRZ Info)
380
- const dg1InputStream = passportService.getInputStream(EIDService.EF_DG1);
381
- await dg1InputStream.init();
382
- const dg1File = new DG1File(undefined, dg1InputStream);
383
- const mrzInfo = await dg1File.getMRZInfo();
482
+ const dg1Done = stepTimer();
483
+ let mrzInfo;
484
+ try {
485
+ const dg1InputStream = passportService.getInputStream(EIDService.EF_DG1);
486
+ await dg1InputStream.init();
487
+ const dg1File = new DG1File(undefined, dg1InputStream);
488
+ mrzInfo = await dg1File.getMRZInfo();
489
+ dg1Done('readDG1', true);
490
+ } catch (e) {
491
+ dg1Done('readDG1', false, e);
492
+ throw e;
493
+ }
384
494
  progress = 30;
385
495
  if (progressCallback) {
386
496
  progressCallback(progress);
387
497
  }
388
498
 
499
+ // Read DG11 (Additional Personal Data — printed name in UTF-8, optional)
500
+ let dg11FirstName = null;
501
+ let dg11LastName = null;
502
+ try {
503
+ const dg11InputStream = passportService.getInputStream(EIDService.EF_DG11);
504
+ await dg11InputStream.init();
505
+ const dg11File = new DG11File(dg11InputStream);
506
+ await dg11File.read();
507
+ dg11FirstName = dg11File.getFirstName();
508
+ dg11LastName = dg11File.getLastName();
509
+ } catch {
510
+ // DG11 is optional — not all chips have it, and access may be denied
511
+ }
512
+
389
513
  // Read DG2 (Face Image)
514
+ const dg2Done = stepTimer();
390
515
  const dg2InputStream = passportService.getInputStream(EIDService.EF_DG2);
391
516
  await dg2InputStream.init();
392
517
  InputStream.onRead((totalBytesRead, fileLength) => {
@@ -399,7 +524,14 @@ const eidReader = async (documentNumber, dateOfBirth, dateOfExpiry, progressCall
399
524
  let imageAsBase64 = '';
400
525
  let mimeType = '';
401
526
  const allFaceImageInfos = [];
402
- const faceInfos = await dg2File.getFaceInfos();
527
+ let faceInfos;
528
+ try {
529
+ faceInfos = await dg2File.getFaceInfos();
530
+ dg2Done('readDG2', true);
531
+ } catch (e) {
532
+ dg2Done('readDG2', false, e);
533
+ throw e;
534
+ }
403
535
  for (let faceInfo of faceInfos) {
404
536
  allFaceImageInfos.push(...faceInfo.getFaceImageInfos());
405
537
  }
@@ -418,12 +550,17 @@ const eidReader = async (documentNumber, dateOfBirth, dateOfExpiry, progressCall
418
550
  if (progressCallback) {
419
551
  progressCallback(progress);
420
552
  }
553
+ diagnostics.nfcResult(true);
421
554
  return {
422
555
  mrz: mrzInfo,
423
556
  faceImage: imageAsBase64,
424
- mimeType: mimeType
557
+ mimeType: mimeType,
558
+ dg11FirstName,
559
+ dg11LastName
425
560
  };
426
561
  } catch (error) {
562
+ const msg = error instanceof Error ? error.message : String(error);
563
+ diagnostics.nfcResult(false, categorizeNfcError(msg));
427
564
  debugLog('EID', 'Error reading passport data', error);
428
565
  } finally {
429
566
  await passportService.close();