@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
@@ -28,6 +28,7 @@ import { useKeepAwake } from '../Libs/native-keep-awake.utils';
28
28
  import { useIsFocused } from '@react-navigation/native';
29
29
  import { useTranslation } from 'react-i18next';
30
30
  import { debugLog, logError, isDebugEnabled } from '../Libs/debug.utils';
31
+ import { diagnostics } from '../Libs/diagnostics';
31
32
  import LottieView from 'lottie-react-native';
32
33
  import StyledButton from './StyledButton';
33
34
  import { SafeAreaView } from 'react-native-safe-area-context';
@@ -112,12 +113,25 @@ const IdentityDocumentCamera = ({
112
113
  const [currentFaceImage, setCurrentFaceImage] = useState<string | undefined>(
113
114
  undefined
114
115
  );
116
+ // Synchronous mirror of the locked face image (see currentHologramImageRef).
117
+ // Used as a fallback when completing SCAN_HOLOGRAM: the torch is on during
118
+ // hologram capture so the live frame's face is often rejected, leaving the
119
+ // state-derived faceImageToUse null within the stale closure — without this
120
+ // the completion gate's `&& !!faceImageToUse` would never pass.
121
+ const currentFaceImageRef = useRef<string | undefined>(undefined);
115
122
  const [_currentHologramMaskImage, setCurrentHologramMaskImage] = useState<
116
123
  string | undefined
117
124
  >(undefined);
118
125
  const [currentHologramImage, setCurrentHologramImage] = useState<
119
126
  string | undefined
120
127
  >(undefined);
128
+ // Mirror of currentHologramImage for synchronous reads inside the per-frame
129
+ // callback. The setState above is async and currentHologramImage is a
130
+ // useCallback dependency, so frames processed before React re-renders the
131
+ // closure would otherwise still see the old (undefined) value — causing the
132
+ // SCAN_HOLOGRAM step to re-collect faces forever and never complete after a
133
+ // successful detection. Always keep this ref in lockstep with the state.
134
+ const currentHologramImageRef = useRef<string | undefined>(undefined);
121
135
  const [currentSecondaryFaceImage, setCurrentSecondaryFaceImage] = useState<
122
136
  string | undefined
123
137
  >(undefined);
@@ -188,10 +202,30 @@ const IdentityDocumentCamera = ({
188
202
  const isHologramDetectionInProgress = useRef(false); // Prevent concurrent hologram detection calls
189
203
  const isCompletionCallbackInvoked = useRef(false); // Prevent multiple callback invocations when COMPLETED
190
204
  const isStepTransitionInProgress = useRef(false); // Prevent duplicate step transitions from rapid frames
205
+ // The step a transition is currently in progress FROM. The latch above is
206
+ // reset asynchronously (via a nextStep effect), so in fast flows (e.g. demo
207
+ // mode auto-advancing) a later transition from a DIFFERENT step could be
208
+ // wrongly blocked while the latch is still set for the previous step. Gating
209
+ // on the originating step lets a genuine next-step transition through while
210
+ // still de-duping rapid repeat frames of the SAME transition.
211
+ const stepTransitionFrom = useRef<
212
+ 'SCAN_ID_FRONT_OR_PASSPORT' | 'SCAN_ID_BACK' | 'SCAN_HOLOGRAM' | null
213
+ >(null);
191
214
  const isCompletionAnnouncementSpoken = useRef(false); // Prevent duplicate "scan completed" voice guidance
192
215
 
193
216
  const faceDetectionErrorCount = useRef(0);
194
217
  const brightnessHistory = useRef<number[]>([]);
218
+ // Running camera-quality stats for the support diagnostic report.
219
+ const diagStats = useRef({
220
+ frames: 0,
221
+ brightnessSum: 0,
222
+ minBrightness: 255,
223
+ maxBrightness: 0,
224
+ frameW: 0,
225
+ frameH: 0,
226
+ hadLowBrightness: false,
227
+ hadBlurryFrames: false,
228
+ });
195
229
  const [faceDetectionEnabled, setFaceDetectionEnabled] = useState(true);
196
230
  const faceImages = useRef<string[]>([]);
197
231
  const hologramImageCountRef = useRef(0);
@@ -277,6 +311,7 @@ const IdentityDocumentCamera = ({
277
311
  setIsActive(true);
278
312
  isCompletionCallbackInvoked.current = false; // Reset callback flag when starting new scan
279
313
  isStepTransitionInProgress.current = false;
314
+ stepTransitionFrom.current = null;
280
315
  isCompletionAnnouncementSpoken.current = false;
281
316
  } else {
282
317
  setIsActive(false);
@@ -292,12 +327,27 @@ const IdentityDocumentCamera = ({
292
327
  validMRZConsecutiveCount.current = 0;
293
328
  mrzAggregator.current.reset();
294
329
  setMrzReliable(false);
330
+ // Start a fresh diagnostics session for the support report.
331
+ diagnostics.reset(Date.now());
332
+ diagStats.current = {
333
+ frames: 0,
334
+ brightnessSum: 0,
335
+ minBrightness: 255,
336
+ maxBrightness: 0,
337
+ frameW: 0,
338
+ frameH: 0,
339
+ hadLowBrightness: false,
340
+ hadBlurryFrames: false,
341
+ };
295
342
  cachedBarcode.current = null;
296
343
  isCompletionCallbackInvoked.current = false;
297
344
  isStepTransitionInProgress.current = false;
345
+ stepTransitionFrom.current = null;
298
346
  isCompletionAnnouncementSpoken.current = false;
299
347
  // Clear all captured image states from previous scan
348
+ currentFaceImageRef.current = undefined;
300
349
  setCurrentFaceImage(undefined);
350
+ currentHologramImageRef.current = undefined;
301
351
  setCurrentHologramImage(undefined);
302
352
  setCurrentSecondaryFaceImage(undefined);
303
353
  resetLastMessage();
@@ -311,9 +361,12 @@ const IdentityDocumentCamera = ({
311
361
  setLatestHologramFaceImage(undefined);
312
362
  isCompletionCallbackInvoked.current = false; // Reset callback flag on unmount
313
363
  isStepTransitionInProgress.current = false;
364
+ stepTransitionFrom.current = null;
314
365
  isCompletionAnnouncementSpoken.current = false;
315
366
  // Clear all captured image states on unmount
367
+ currentFaceImageRef.current = undefined;
316
368
  setCurrentFaceImage(undefined);
369
+ currentHologramImageRef.current = undefined;
317
370
  setCurrentHologramImage(undefined);
318
371
  setCurrentSecondaryFaceImage(undefined);
319
372
  resetLastMessage();
@@ -387,6 +440,7 @@ const IdentityDocumentCamera = ({
387
440
  isCompletionCallbackInvoked.current = false;
388
441
  }
389
442
  isStepTransitionInProgress.current = false;
443
+ stepTransitionFrom.current = null;
390
444
  }, [nextStep]);
391
445
 
392
446
  // Update status bar based on guide visibility
@@ -587,6 +641,7 @@ const IdentityDocumentCamera = ({
587
641
  setHologramImageCount(0);
588
642
  setLatestHologramFaceImage(undefined);
589
643
  // Clear previous hologram state to prevent premature completion
644
+ currentHologramImageRef.current = undefined;
590
645
  setCurrentHologramImage(undefined);
591
646
  setCurrentHologramMaskImage(undefined);
592
647
  }
@@ -643,11 +698,18 @@ const IdentityDocumentCamera = ({
643
698
  fromStep: 'SCAN_ID_FRONT_OR_PASSPORT' | 'SCAN_ID_BACK' | 'SCAN_HOLOGRAM',
644
699
  scannedData: DocumentScannedData
645
700
  ) => {
646
- if (isStepTransitionInProgress.current) {
701
+ // De-dupe only rapid repeat frames of the SAME originating step. A
702
+ // transition from a different step is a genuine advance (the async latch
703
+ // reset may not have run yet in fast flows like demo mode), so allow it.
704
+ if (
705
+ isStepTransitionInProgress.current &&
706
+ stepTransitionFrom.current === fromStep
707
+ ) {
647
708
  return;
648
709
  }
649
710
 
650
711
  isStepTransitionInProgress.current = true;
712
+ stepTransitionFrom.current = fromStep;
651
713
 
652
714
  // Torch only needed during SCAN_HOLOGRAM - turn off for all other transitions
653
715
  if (nextStepType !== 'SCAN_HOLOGRAM') {
@@ -880,9 +942,32 @@ const IdentityDocumentCamera = ({
880
942
  )
881
943
  : 0;
882
944
  const frameWeight = mrzBandHeight > 0 ? mrzBandHeight : 1;
945
+ // ML Kit per-symbol confidence over MRZ characters (Android bundled library
946
+ // only; undefined on iOS). When present it down-weights frames the
947
+ // recognizer was unsure about, so a confident frame outvotes a shaky one of
948
+ // the same band height on look-alike ties (A↔4, S↔5, B↔8).
949
+ const frameConfidence = scannedText?.textConfidence?.mean;
883
950
  const consensus = mrzAggregator.current.addFrame({
884
951
  text: textForValidation,
885
952
  weight: frameWeight,
953
+ confidence: frameConfidence,
954
+ });
955
+
956
+ // Record MRZ consensus metrics for the support diagnostic report (counts
957
+ // and pass/fail only — never the MRZ values themselves). `reachedStable`
958
+ // is sticky in the collector, so a later flicker can't unset it.
959
+ // `stabilityFrames`/`minContestedMargin` and the MRZ band pixel height are
960
+ // device-variance signals (convergence speed, look-alike resolution
961
+ // confidence, and whether the band cleared the ~16px/char OCR floor).
962
+ diagnostics.setMrz({
963
+ framesProcessed: consensus.frames,
964
+ reachedStable: consensus.stable,
965
+ format: consensus.validation?.format ?? undefined,
966
+ stabilityFrames: consensus.stableStreak,
967
+ minContestedMargin: Number.isFinite(consensus.minContestedMargin)
968
+ ? consensus.minContestedMargin
969
+ : 0,
970
+ maxMrzBandHeightPx: mrzBandHeight,
886
971
  });
887
972
 
888
973
  // Use the fused consensus when it is valid; otherwise fall back to a
@@ -922,6 +1007,9 @@ const IdentityDocumentCamera = ({
922
1007
  ? 'PASSPORT'
923
1008
  : detectedDocumentType;
924
1009
 
1010
+ // Record the detected document type for the diagnostic report.
1011
+ if (documentType) diagnostics.setDocument(documentType);
1012
+
925
1013
  // Crop faces once document type is confirmed or we're past the initial step
926
1014
  const shouldCropFaces =
927
1015
  documentType === 'ID_FRONT' ||
@@ -1005,6 +1093,7 @@ const IdentityDocumentCamera = ({
1005
1093
  // Continue scanning without locking this face
1006
1094
  } else {
1007
1095
  faceImageToUse = croppedFaces[0];
1096
+ currentFaceImageRef.current = croppedFaces[0];
1008
1097
  setCurrentFaceImage(croppedFaces[0]);
1009
1098
  }
1010
1099
  }
@@ -1304,8 +1393,8 @@ const IdentityDocumentCamera = ({
1304
1393
  if (primaryFaceOnly) {
1305
1394
  hologramFramesWithoutFace.current = 0;
1306
1395
 
1307
- if (currentHologramImage) {
1308
- scannedData.hologramImage = currentHologramImage;
1396
+ if (currentHologramImageRef.current) {
1397
+ scannedData.hologramImage = currentHologramImageRef.current;
1309
1398
  } else if (faceImages.current.length < HOLOGRAM_IMAGE_COUNT) {
1310
1399
  // Space out captures for better variation
1311
1400
  const timeSinceLastCapture =
@@ -1392,6 +1481,7 @@ const IdentityDocumentCamera = ({
1392
1481
  if (hologram) {
1393
1482
  setCurrentHologramMaskImage(hologramMask);
1394
1483
  scannedData.hologramImage = hologram;
1484
+ currentHologramImageRef.current = hologram;
1395
1485
  setCurrentHologramImage(hologram);
1396
1486
  if (isDebugEnabled()) {
1397
1487
  debugLog(
@@ -1423,8 +1513,8 @@ const IdentityDocumentCamera = ({
1423
1513
  } else {
1424
1514
  hologramFramesWithoutFace.current++;
1425
1515
  }
1426
- } else if (currentHologramImage) {
1427
- scannedData.hologramImage = currentHologramImage;
1516
+ } else if (currentHologramImageRef.current) {
1517
+ scannedData.hologramImage = currentHologramImageRef.current;
1428
1518
  }
1429
1519
 
1430
1520
  // Secondary face capture (continuous during initial scan and hologram detection)
@@ -1637,13 +1727,23 @@ const IdentityDocumentCamera = ({
1637
1727
  return;
1638
1728
  }
1639
1729
 
1640
- // Complete when hologram captured OR retries exhausted (not mid-collection)
1730
+ // Complete when hologram captured OR retries exhausted (not mid-collection).
1731
+ // Fall back to the previously locked face (ref) when the live frame's face
1732
+ // was rejected — the torch is on during hologram capture, so faceImageToUse
1733
+ // is frequently null even though a valid face was locked during the front
1734
+ // scan. Without this the gate's face requirement could never be satisfied.
1735
+ const faceForCompletion = faceImageToUse ?? currentFaceImageRef.current;
1641
1736
  const stepComplete =
1642
1737
  (!!scannedData.hologramImage ||
1643
1738
  (maxRetriesReached && !isCollecting)) &&
1644
- !!faceImageToUse; // Require face before completing
1739
+ !!faceForCompletion; // Require face before completing
1645
1740
 
1646
1741
  if (stepComplete) {
1742
+ // Make sure the locked face rides along even if this frame's face was
1743
+ // rejected.
1744
+ if (!scannedData.faceImage && faceForCompletion) {
1745
+ scannedData.faceImage = faceForCompletion;
1746
+ }
1647
1747
  // Ensure preserved MRZ data is included (current frame may not have
1648
1748
  // readable MRZ due to flash/tilting during hologram capture)
1649
1749
  if (!scannedData.mrzText && lastValidMRZText.current) {
@@ -1846,6 +1946,21 @@ const IdentityDocumentCamera = ({
1846
1946
  const avgBrightness =
1847
1947
  brightnessHistory.current.reduce((a, b) => a + b, 0) /
1848
1948
  brightnessHistory.current.length;
1949
+
1950
+ // Accumulate camera diagnostics (frame count, brightness range, dims) for
1951
+ // the support diagnostic report. Cheap, side-effect-only.
1952
+ diagStats.current.frames += 1;
1953
+ diagStats.current.brightnessSum += frameBrightness;
1954
+ diagStats.current.minBrightness = Math.min(
1955
+ diagStats.current.minBrightness,
1956
+ frameBrightness
1957
+ );
1958
+ diagStats.current.maxBrightness = Math.max(
1959
+ diagStats.current.maxBrightness,
1960
+ frameBrightness
1961
+ );
1962
+ diagStats.current.frameW = frame.width ?? diagStats.current.frameW;
1963
+ diagStats.current.frameH = frame.height ?? diagStats.current.frameH;
1849
1964
  const isOverallBright = avgBrightness >= MIN_BRIGHTNESS_THRESHOLD;
1850
1965
 
1851
1966
  // Check brightness in center region (area of interest)
@@ -1864,6 +1979,7 @@ const IdentityDocumentCamera = ({
1864
1979
  }
1865
1980
 
1866
1981
  setIsBrightnessLow(!isRegionBright);
1982
+ if (!isRegionBright) diagStats.current.hadLowBrightness = true;
1867
1983
 
1868
1984
  // Check blur only in center region (area of interest) to avoid false positives
1869
1985
  // from iOS depth-of-field background blur
@@ -1882,10 +1998,31 @@ const IdentityDocumentCamera = ({
1882
1998
  );
1883
1999
  isNotBlurry = !isBlurry;
1884
2000
  setIsFrameBlurry(isBlurry);
2001
+ if (isBlurry) diagStats.current.hadBlurryFrames = true;
1885
2002
  } catch (error) {
1886
2003
  setIsFrameBlurry(false);
1887
2004
  }
1888
2005
 
2006
+ // Flush accumulated camera stats into the diagnostics collector.
2007
+ const ds = diagStats.current;
2008
+ diagnostics.setCamera({
2009
+ frames: ds.frames,
2010
+ frameW: ds.frameW,
2011
+ frameH: ds.frameH,
2012
+ avgBrightness:
2013
+ ds.frames > 0 ? Math.round(ds.brightnessSum / ds.frames) : 0,
2014
+ minBrightness: ds.minBrightness === 255 && ds.frames === 0 ? 0 : ds.minBrightness,
2015
+ maxBrightness: ds.maxBrightness,
2016
+ hadLowBrightness: ds.hadLowBrightness,
2017
+ hadBlurryFrames: ds.hadBlurryFrames,
2018
+ // The actual gates the measured values are judged against — lets support
2019
+ // read "measured 45 vs floor 45" instead of just a pass/fail flag.
2020
+ thresholds: {
2021
+ minBrightness: MIN_BRIGHTNESS_THRESHOLD,
2022
+ blur: 50,
2023
+ },
2024
+ });
2025
+
1889
2026
  // Only proceed if image quality is acceptable
1890
2027
  const hasAcceptableQuality = isRegionBright && isNotBlurry;
1891
2028
 
@@ -1934,6 +2071,7 @@ const IdentityDocumentCamera = ({
1934
2071
  const scannedText: BlockText = {
1935
2072
  resultText,
1936
2073
  mrzOnlyText: undefined, // Will be set below if MRZ blocks detected
2074
+ textConfidence: frame.textConfidence,
1937
2075
  blocks: textBlocks.map((block) => ({
1938
2076
  blockText: block.text || '',
1939
2077
  blockFrame: block.blockFrame ?? {
@@ -30,6 +30,12 @@ export type BlockText = {
30
30
  blocks: BlocksData[];
31
31
  resultText: string;
32
32
  mrzOnlyText?: string;
33
+ /**
34
+ * Per-frame OCR confidence over MRZ characters from ML Kit per-symbol
35
+ * confidence (Android bundled library only; undefined on iOS). Feeds the
36
+ * multi-frame vote weight so low-confidence frames count less.
37
+ */
38
+ textConfidence?: { mean: number; min: number; count: number };
33
39
  };
34
40
 
35
41
  export type BlocksData = {
@@ -13,7 +13,6 @@ import {
13
13
  } from '@react-navigation/native';
14
14
  import { View, StyleSheet, Alert } from 'react-native';
15
15
  import { useTranslation } from 'react-i18next';
16
- import { useSafeAreaInsets } from 'react-native-safe-area-context';
17
16
  import i18n from '../../Translation';
18
17
  import StyledButton from './StyledButton';
19
18
  import { analyticsService } from '../Services/AnalyticsService';
@@ -37,7 +36,6 @@ const NavigationManager = forwardRef(
37
36
  const appContext = useContext(AppContext);
38
37
  const navigation = useNavigation();
39
38
  const { t } = useTranslation();
40
- const insets = useSafeAreaInsets();
41
39
 
42
40
  const routes = {
43
41
  VERIFICATION_SESSION_CHECK: 'VerificationSessionCheckScreen',
@@ -274,7 +272,7 @@ const NavigationManager = forwardRef(
274
272
  return (
275
273
  appContext.currentWorkflowStep &&
276
274
  (!appContext.currentWorkflowStep?.required || canSkipStep) && (
277
- <View style={[styles.container, { paddingBottom: insets.bottom }]}>
275
+ <View style={styles.container}>
278
276
  <StyledButton mode="text" onPress={goToNextRouteWithAlert}>
279
277
  {t('navigationManager.skipStepLabel')}
280
278
  </StyledButton>
@@ -285,11 +283,13 @@ const NavigationManager = forwardRef(
285
283
  );
286
284
 
287
285
  const styles = StyleSheet.create({
286
+ // Hug the text button: centered, minimal padding, no full-width opaque bar so
287
+ // it occupies the least area and never overlays sibling content. The screen's
288
+ // footer wrapper already applies the bottom safe-area inset.
288
289
  container: {
289
- justifyContent: 'center',
290
- alignItems: 'center',
291
- padding: 20,
292
- backgroundColor: 'white',
290
+ alignSelf: 'center',
291
+ paddingHorizontal: 16,
292
+ paddingVertical: 8,
293
293
  },
294
294
  });
295
295
 
@@ -62,6 +62,12 @@ export interface Frame {
62
62
  faces?: NativeFace[];
63
63
  textBlocks?: NativeTextBlock[];
64
64
  resultText?: string;
65
+ /**
66
+ * Per-frame OCR confidence over MRZ characters, from ML Kit per-symbol
67
+ * confidence. Android (bundled ML Kit library) only — iOS ML Kit exposes no
68
+ * symbol-level confidence, so this is undefined there.
69
+ */
70
+ textConfidence?: { mean: number; min: number; count: number };
65
71
  barcodes?: NativeBarcode[];
66
72
  }
67
73
 
@@ -3,6 +3,7 @@ import type {
3
3
  IdentificationInfo,
4
4
  WorkflowStep,
5
5
  } from '../Types/identificationInfo';
6
+ import type { DocumentReadResult } from '../Types/documentReadResult';
6
7
 
7
8
  export type AppContextType = {
8
9
  isDemoSession: boolean;
@@ -19,8 +20,12 @@ export type AppContextType = {
19
20
  currentWorkflowStep?: WorkflowStep;
20
21
  isDebugNavigated?: boolean;
21
22
  isTestVideoSession?: boolean;
23
+ /** Last document read result — available to upload services for real-name fields. */
24
+ lastDocumentRead?: DocumentReadResult;
22
25
  onCompleted?: () => void;
23
26
  onError?: (error: string) => void;
27
+ onDocumentRead?: (result: DocumentReadResult) => void;
28
+ setLastDocumentRead?: (result: DocumentReadResult) => void;
24
29
  setSessionId?: (id: string) => void;
25
30
  setBaseUrl?: (url: string) => void;
26
31
  setIsDemoSession?: (isDemoSession: boolean) => void;
@@ -49,8 +54,11 @@ export default createContext<AppContextType>({
49
54
  currentWorkflowStep: undefined,
50
55
  isDebugNavigated: false,
51
56
  isTestVideoSession: false,
57
+ lastDocumentRead: undefined,
52
58
  onCompleted: undefined,
53
59
  onError: undefined,
60
+ onDocumentRead: undefined,
61
+ setLastDocumentRead: undefined,
54
62
  setSessionId: undefined,
55
63
  setBaseUrl: undefined,
56
64
  });