@trustchex/react-native-sdk 1.475.1 → 1.481.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/android/src/main/java/com/trustchex/reactnativesdk/camera/TrustchexCameraView.kt +76 -24
  2. package/android/src/main/java/com/trustchex/reactnativesdk/mlkit/MLKitModule.kt +1 -0
  3. package/ios/Camera/TrustchexCameraView.swift +20 -15
  4. package/lib/module/Screens/Dynamic/LivenessDetectionScreen.js +3 -2
  5. package/lib/module/Screens/Static/QrCodeScanningScreen.js +12 -2
  6. package/lib/module/Screens/Static/ResultScreen.js +145 -113
  7. package/lib/module/Shared/Components/DebugOverlay.js +5 -2
  8. package/lib/module/Shared/Components/DiagnosticReportButton.js +64 -0
  9. package/lib/module/Shared/Components/IdentityDocumentCamera.js +185 -30
  10. package/lib/module/Shared/Components/IdentityDocumentCamera.utils.js +8 -1
  11. package/lib/module/Shared/Components/NavigationManager.js +7 -9
  12. package/lib/module/Shared/EIDReader/eidReader.js +159 -38
  13. package/lib/module/Shared/Libs/MRZ_KNOWN_ISSUES.md +41 -0
  14. package/lib/module/Shared/Libs/SignalingClient.js +6 -5
  15. package/lib/module/Shared/Libs/crypto.utils.js +25 -3
  16. package/lib/module/Shared/Libs/deeplink.utils.js +37 -7
  17. package/lib/module/Shared/Libs/diagnosticReport.js +133 -0
  18. package/lib/module/Shared/Libs/diagnostics.js +171 -0
  19. package/lib/module/Shared/Libs/http-client.js +19 -6
  20. package/lib/module/Shared/Libs/mrz.utils.js +193 -33
  21. package/lib/module/Shared/Libs/mrzFrameAggregator.js +74 -3
  22. package/lib/module/Shared/Libs/native-device-info.utils.js +67 -0
  23. package/lib/module/Shared/Libs/promise.utils.js +4 -3
  24. package/lib/module/Shared/Libs/sendDiagnosticReport.js +130 -0
  25. package/lib/module/Shared/Libs/shuffle.utils.js +17 -0
  26. package/lib/module/Shared/Services/AnalyticsService.js +8 -2
  27. package/lib/module/Shared/Services/DataUploadService.js +114 -127
  28. package/lib/module/Shared/Services/VideoSessionService.js +4 -3
  29. package/lib/module/Translation/Resources/en.js +6 -0
  30. package/lib/module/Translation/Resources/tr.js +6 -0
  31. package/lib/module/Trustchex.js +20 -4
  32. package/lib/module/version.js +1 -1
  33. package/lib/typescript/src/Screens/Dynamic/LivenessDetectionScreen.d.ts.map +1 -1
  34. package/lib/typescript/src/Screens/Static/QrCodeScanningScreen.d.ts.map +1 -1
  35. package/lib/typescript/src/Screens/Static/ResultScreen.d.ts.map +1 -1
  36. package/lib/typescript/src/Shared/Components/DebugOverlay.d.ts.map +1 -1
  37. package/lib/typescript/src/Shared/Components/DiagnosticReportButton.d.ts +20 -0
  38. package/lib/typescript/src/Shared/Components/DiagnosticReportButton.d.ts.map +1 -0
  39. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.d.ts.map +1 -1
  40. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.types.d.ts +10 -0
  41. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.types.d.ts.map +1 -1
  42. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.utils.d.ts +1 -1
  43. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.utils.d.ts.map +1 -1
  44. package/lib/typescript/src/Shared/Components/NavigationManager.d.ts.map +1 -1
  45. package/lib/typescript/src/Shared/Components/TrustchexCamera.d.ts +10 -0
  46. package/lib/typescript/src/Shared/Components/TrustchexCamera.d.ts.map +1 -1
  47. package/lib/typescript/src/Shared/EIDReader/eidReader.d.ts.map +1 -1
  48. package/lib/typescript/src/Shared/Libs/SignalingClient.d.ts.map +1 -1
  49. package/lib/typescript/src/Shared/Libs/crypto.utils.d.ts.map +1 -1
  50. package/lib/typescript/src/Shared/Libs/deeplink.utils.d.ts +1 -0
  51. package/lib/typescript/src/Shared/Libs/deeplink.utils.d.ts.map +1 -1
  52. package/lib/typescript/src/Shared/Libs/diagnosticReport.d.ts +48 -0
  53. package/lib/typescript/src/Shared/Libs/diagnosticReport.d.ts.map +1 -0
  54. package/lib/typescript/src/Shared/Libs/diagnostics.d.ts +145 -0
  55. package/lib/typescript/src/Shared/Libs/diagnostics.d.ts.map +1 -0
  56. package/lib/typescript/src/Shared/Libs/http-client.d.ts +2 -0
  57. package/lib/typescript/src/Shared/Libs/http-client.d.ts.map +1 -1
  58. package/lib/typescript/src/Shared/Libs/mrz.utils.d.ts.map +1 -1
  59. package/lib/typescript/src/Shared/Libs/mrzFrameAggregator.d.ts +55 -0
  60. package/lib/typescript/src/Shared/Libs/mrzFrameAggregator.d.ts.map +1 -1
  61. package/lib/typescript/src/Shared/Libs/native-device-info.utils.d.ts +33 -0
  62. package/lib/typescript/src/Shared/Libs/native-device-info.utils.d.ts.map +1 -1
  63. package/lib/typescript/src/Shared/Libs/promise.utils.d.ts.map +1 -1
  64. package/lib/typescript/src/Shared/Libs/sendDiagnosticReport.d.ts +30 -0
  65. package/lib/typescript/src/Shared/Libs/sendDiagnosticReport.d.ts.map +1 -0
  66. package/lib/typescript/src/Shared/Libs/shuffle.utils.d.ts +3 -0
  67. package/lib/typescript/src/Shared/Libs/shuffle.utils.d.ts.map +1 -0
  68. package/lib/typescript/src/Shared/Services/AnalyticsService.d.ts.map +1 -1
  69. package/lib/typescript/src/Shared/Services/DataUploadService.d.ts.map +1 -1
  70. package/lib/typescript/src/Shared/Services/VideoSessionService.d.ts.map +1 -1
  71. package/lib/typescript/src/Translation/Resources/en.d.ts +6 -0
  72. package/lib/typescript/src/Translation/Resources/en.d.ts.map +1 -1
  73. package/lib/typescript/src/Translation/Resources/tr.d.ts +6 -0
  74. package/lib/typescript/src/Translation/Resources/tr.d.ts.map +1 -1
  75. package/lib/typescript/src/Trustchex.d.ts.map +1 -1
  76. package/lib/typescript/src/version.d.ts +1 -1
  77. package/package.json +17 -3
  78. package/src/Screens/Dynamic/LivenessDetectionScreen.tsx +4 -3
  79. package/src/Screens/Static/QrCodeScanningScreen.tsx +19 -2
  80. package/src/Screens/Static/ResultScreen.tsx +172 -162
  81. package/src/Shared/Components/DebugOverlay.tsx +5 -2
  82. package/src/Shared/Components/DiagnosticReportButton.tsx +77 -0
  83. package/src/Shared/Components/IdentityDocumentCamera.tsx +200 -29
  84. package/src/Shared/Components/IdentityDocumentCamera.types.ts +6 -0
  85. package/src/Shared/Components/IdentityDocumentCamera.utils.ts +9 -1
  86. package/src/Shared/Components/NavigationManager.tsx +7 -7
  87. package/src/Shared/Components/TrustchexCamera.tsx +6 -0
  88. package/src/Shared/EIDReader/eidReader.ts +181 -52
  89. package/src/Shared/Libs/MRZ_KNOWN_ISSUES.md +41 -0
  90. package/src/Shared/Libs/SignalingClient.ts +10 -5
  91. package/src/Shared/Libs/crypto.utils.ts +41 -4
  92. package/src/Shared/Libs/deeplink.utils.ts +39 -7
  93. package/src/Shared/Libs/diagnosticReport.ts +206 -0
  94. package/src/Shared/Libs/diagnostics.ts +251 -0
  95. package/src/Shared/Libs/http-client.ts +24 -8
  96. package/src/Shared/Libs/mrz.utils.ts +200 -34
  97. package/src/Shared/Libs/mrzFrameAggregator.ts +112 -2
  98. package/src/Shared/Libs/native-device-info.utils.ts +116 -0
  99. package/src/Shared/Libs/promise.utils.ts +11 -5
  100. package/src/Shared/Libs/sendDiagnosticReport.ts +165 -0
  101. package/src/Shared/Libs/shuffle.utils.ts +15 -0
  102. package/src/Shared/Services/AnalyticsService.ts +17 -1
  103. package/src/Shared/Services/DataUploadService.ts +155 -166
  104. package/src/Shared/Services/VideoSessionService.ts +15 -3
  105. package/src/Translation/Resources/en.ts +7 -0
  106. package/src/Translation/Resources/tr.ts +7 -0
  107. package/src/Trustchex.tsx +26 -3
  108. package/src/version.ts +1 -1
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+
3
+ import React, { useState } from 'react';
4
+ import { Alert } from 'react-native';
5
+ import { useTranslation } from 'react-i18next';
6
+ import StyledButton from "./StyledButton.js";
7
+ import { sendDiagnosticReport, isDiagnosticSharingAvailable } from "../Libs/sendDiagnosticReport.js";
8
+ import { SDK_VERSION } from "../../version.js";
9
+ import { jsx as _jsx } from "react/jsx-runtime";
10
+ /**
11
+ * "Report a scanning issue" button for the result screen.
12
+ *
13
+ * Shows a brief notice describing what will be shared (the draft itself is the
14
+ * user's review/consent step), then opens the OS share sheet — the user picks
15
+ * email / WhatsApp / Drive / etc. and sends the diagnostic bundle to support.
16
+ * Self-contained so the result screen only needs to render it with the scan
17
+ * data + captured images.
18
+ */
19
+ const DiagnosticReportButton = ({
20
+ scan,
21
+ sessionId,
22
+ images
23
+ }) => {
24
+ const {
25
+ t
26
+ } = useTranslation();
27
+ const [sending, setSending] = useState(false);
28
+
29
+ // `react-native-share` is optional — hide the button entirely when the host
30
+ // app hasn't installed it, so apps that don't want diagnostics see no UI.
31
+ if (!isDiagnosticSharingAvailable()) {
32
+ return null;
33
+ }
34
+ const onPress = () => {
35
+ if (sending) return;
36
+ // Brief notice — the share draft is the actual review/consent step.
37
+ Alert.alert(t('diagnosticReport.noticeTitle'), t('diagnosticReport.noticeBody'), [{
38
+ text: t('diagnosticReport.cancel'),
39
+ style: 'cancel'
40
+ }, {
41
+ text: t('diagnosticReport.continue'),
42
+ onPress: async () => {
43
+ setSending(true);
44
+ try {
45
+ await sendDiagnosticReport({
46
+ scan,
47
+ sdkVersion: SDK_VERSION,
48
+ sessionId,
49
+ images
50
+ });
51
+ } finally {
52
+ setSending(false);
53
+ }
54
+ }
55
+ }]);
56
+ };
57
+ return /*#__PURE__*/_jsx(StyledButton, {
58
+ mode: "outlined",
59
+ onPress: onPress,
60
+ disabled: sending,
61
+ children: t('diagnosticReport.button')
62
+ });
63
+ };
64
+ export default DiagnosticReportButton;
@@ -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';
@@ -22,7 +23,7 @@ import { useTheme } from "../Contexts/ThemeContext.js";
22
23
  import DebugOverlay, { TestModePanel } from "./DebugOverlay.js";
23
24
  import { getStatusMessage, getFrameToScreenTransform, transformBoundsToScreen, getScanAreaBounds, angleBetweenPoints, detectDocumentType, determineDocumentTypeToSet, areMRZFieldsEqual, hasRequiredMRZFields, validateFacePosition } from "./IdentityDocumentCamera.utils.js";
24
25
  import { handlePassportFlow, handleIDFrontFlow, handleIDBackFlow, getNextStepAfterHologram } from "./IdentityDocumentCamera.flows.js";
25
- import { HOLOGRAM_IMAGE_COUNT, HOLOGRAM_DETECTION_THRESHOLD, HOLOGRAM_DETECTION_RETRY_COUNT, HOLOGRAM_CAPTURE_INTERVAL, HOLOGRAM_MAX_FRAMES_WITHOUT_FACE, MIN_BRIGHTNESS_THRESHOLD, MAX_BRIGHTNESS_THRESHOLD, FACE_EDGE_MARGIN_PERCENT, MAX_CONSECUTIVE_QUALITY_FAILURES, REQUIRED_CONSISTENT_MRZ_READS, REQUIRED_CONSISTENT_DOCTYPE_DETECTIONS, SIGNATURE_TEXT_REGEX, MRZ_BLOCK_PATTERN, PASSPORT_MRZ_PATTERN, MIN_CARD_FACE_SIZE_PERCENT } from "./IdentityDocumentCamera.constants.js";
26
+ import { HOLOGRAM_IMAGE_COUNT, HOLOGRAM_DETECTION_THRESHOLD, HOLOGRAM_DETECTION_RETRY_COUNT, HOLOGRAM_CAPTURE_INTERVAL, HOLOGRAM_MAX_FRAMES_WITHOUT_FACE, MIN_BRIGHTNESS_THRESHOLD, MAX_BRIGHTNESS_THRESHOLD, FACE_EDGE_MARGIN_PERCENT, MAX_CONSECUTIVE_QUALITY_FAILURES, REQUIRED_CONSISTENT_DOCTYPE_DETECTIONS, SIGNATURE_TEXT_REGEX, MRZ_BLOCK_PATTERN, PASSPORT_MRZ_PATTERN, MIN_CARD_FACE_SIZE_PERCENT } from "./IdentityDocumentCamera.constants.js";
26
27
 
27
28
  // Re-export types for backward compatibility
28
29
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
@@ -52,13 +53,31 @@ 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);
60
74
  const [hasGuideShown, setHasGuideShown] = useState(false);
61
75
  const [status, setStatus] = useState('SEARCHING');
76
+ // True once the multi-frame voted MRZ consensus is STABLE + valid (the same
77
+ // reading matched across `stabilityTarget` consecutive frames, with a clear
78
+ // vote margin on contested cells). Drives the green "GO" signal — we never show
79
+ // green on an early/transient valid read, only on a reliably-detected MRZ.
80
+ const [mrzReliable, setMrzReliable] = useState(false);
62
81
  const [nextStep, setNextStep] = useState('SCAN_ID_FRONT_OR_PASSPORT');
63
82
  const [_completedStep, setCompletedStep] = useState(null);
64
83
  const [detectedDocumentType, setDetectedDocumentType] = useState('UNKNOWN');
@@ -74,8 +93,11 @@ const IdentityDocumentCamera = ({
74
93
  // Multi-frame per-character voting: fuses OCR across frames so a single-frame
75
94
  // misread (B↔8, S↔5, <↔K, dropped/inserted digit) is outvoted instead of
76
95
  // resetting progress. Drives the same `parsedMRZData`/stability the flow uses.
96
+ // stabilityTarget: 3 — the voted consensus must read the SAME valid MRZ across
97
+ // 3 consecutive contributing frames before we accept, so a transient look-alike
98
+ // misread (e.g. OCR-B "4" vs "A") can never lock in on one or two early frames.
77
99
  const mrzAggregator = useRef(new MRZFrameAggregator({
78
- stabilityTarget: 2
100
+ stabilityTarget: 3
79
101
  }));
80
102
 
81
103
  // Document type stability tracking - require consistent detections from good quality frames
@@ -102,10 +124,28 @@ const IdentityDocumentCamera = ({
102
124
  const isHologramDetectionInProgress = useRef(false); // Prevent concurrent hologram detection calls
103
125
  const isCompletionCallbackInvoked = useRef(false); // Prevent multiple callback invocations when COMPLETED
104
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);
105
134
  const isCompletionAnnouncementSpoken = useRef(false); // Prevent duplicate "scan completed" voice guidance
106
135
 
107
136
  const faceDetectionErrorCount = useRef(0);
108
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
+ });
109
149
  const [faceDetectionEnabled, setFaceDetectionEnabled] = useState(true);
110
150
  const faceImages = useRef([]);
111
151
  const hologramImageCountRef = useRef(0);
@@ -143,6 +183,7 @@ const IdentityDocumentCamera = ({
143
183
  setIsActive(true);
144
184
  isCompletionCallbackInvoked.current = false; // Reset callback flag when starting new scan
145
185
  isStepTransitionInProgress.current = false;
186
+ stepTransitionFrom.current = null;
146
187
  isCompletionAnnouncementSpoken.current = false;
147
188
  } else {
148
189
  setIsActive(false);
@@ -157,12 +198,28 @@ const IdentityDocumentCamera = ({
157
198
  lastValidMRZFields.current = null;
158
199
  validMRZConsecutiveCount.current = 0;
159
200
  mrzAggregator.current.reset();
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
+ };
160
214
  cachedBarcode.current = null;
161
215
  isCompletionCallbackInvoked.current = false;
162
216
  isStepTransitionInProgress.current = false;
217
+ stepTransitionFrom.current = null;
163
218
  isCompletionAnnouncementSpoken.current = false;
164
219
  // Clear all captured image states from previous scan
220
+ currentFaceImageRef.current = undefined;
165
221
  setCurrentFaceImage(undefined);
222
+ currentHologramImageRef.current = undefined;
166
223
  setCurrentHologramImage(undefined);
167
224
  setCurrentSecondaryFaceImage(undefined);
168
225
  resetLastMessage();
@@ -175,9 +232,12 @@ const IdentityDocumentCamera = ({
175
232
  setLatestHologramFaceImage(undefined);
176
233
  isCompletionCallbackInvoked.current = false; // Reset callback flag on unmount
177
234
  isStepTransitionInProgress.current = false;
235
+ stepTransitionFrom.current = null;
178
236
  isCompletionAnnouncementSpoken.current = false;
179
237
  // Clear all captured image states on unmount
238
+ currentFaceImageRef.current = undefined;
180
239
  setCurrentFaceImage(undefined);
240
+ currentHologramImageRef.current = undefined;
181
241
  setCurrentHologramImage(undefined);
182
242
  setCurrentSecondaryFaceImage(undefined);
183
243
  resetLastMessage();
@@ -223,6 +283,7 @@ const IdentityDocumentCamera = ({
223
283
  isCompletionCallbackInvoked.current = false;
224
284
  }
225
285
  isStepTransitionInProgress.current = false;
286
+ stepTransitionFrom.current = null;
226
287
  }, [nextStep]);
227
288
 
228
289
  // Update status bar based on guide visibility
@@ -356,6 +417,7 @@ const IdentityDocumentCamera = ({
356
417
  setHologramImageCount(0);
357
418
  setLatestHologramFaceImage(undefined);
358
419
  // Clear previous hologram state to prevent premature completion
420
+ currentHologramImageRef.current = undefined;
359
421
  setCurrentHologramImage(undefined);
360
422
  setCurrentHologramMaskImage(undefined);
361
423
  }
@@ -386,6 +448,7 @@ const IdentityDocumentCamera = ({
386
448
  lastValidMRZFields.current = null;
387
449
  // New MRZ expected on the back — start voting fresh.
388
450
  mrzAggregator.current.reset();
451
+ setMrzReliable(false);
389
452
  }
390
453
  validMRZConsecutiveCount.current = 0;
391
454
  cachedBarcode.current = null; // Clear cached barcode on step change
@@ -398,10 +461,14 @@ const IdentityDocumentCamera = ({
398
461
  }
399
462
  }, [setIsTorchOn]);
400
463
  const transitionStepWithCallback = useCallback((nextStepType, fromStep, scannedData) => {
401
- 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) {
402
468
  return;
403
469
  }
404
470
  isStepTransitionInProgress.current = true;
471
+ stepTransitionFrom.current = fromStep;
405
472
 
406
473
  // Torch only needed during SCAN_HOLOGRAM - turn off for all other transitions
407
474
  if (nextStepType !== 'SCAN_HOLOGRAM') {
@@ -557,9 +624,30 @@ const IdentityDocumentCamera = ({
557
624
  // of letting one noisy frame reset progress.
558
625
  const mrzBandHeight = scannedText?.blocks?.length ? Math.max(0, ...scannedText.blocks.map(b => b.blockFrame?.height ?? 0)) : 0;
559
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;
560
632
  const consensus = mrzAggregator.current.addFrame({
561
633
  text: textForValidation,
562
- 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
563
651
  });
564
652
 
565
653
  // Use the fused consensus when it is valid; otherwise fall back to a
@@ -583,6 +671,9 @@ const IdentityDocumentCamera = ({
583
671
  // be wrong (ID_FRONT locked before passport MRZ became readable)
584
672
  const documentType = nextStep === 'SCAN_ID_FRONT_OR_PASSPORT' ? detectDocumentType(primaryFaces, text, parsedMRZData?.fields, frameWidth, mrzText) : parsedMRZData?.fields?.documentCode === 'P' ? 'PASSPORT' : detectedDocumentType;
585
673
 
674
+ // Record the detected document type for the diagnostic report.
675
+ if (documentType) diagnostics.setDocument(documentType);
676
+
586
677
  // Crop faces once document type is confirmed or we're past the initial step
587
678
  const shouldCropFaces = documentType === 'ID_FRONT' || documentType === 'PASSPORT' || nextStep !== 'SCAN_ID_FRONT_OR_PASSPORT';
588
679
  const croppedFaces = shouldCropFaces ? await getFaceImages(primaryFaces, image ?? '', frameWidth, frameHeight) : [];
@@ -636,6 +727,7 @@ const IdentityDocumentCamera = ({
636
727
  // Continue scanning without locking this face
637
728
  } else {
638
729
  faceImageToUse = croppedFaces[0];
730
+ currentFaceImageRef.current = croppedFaces[0];
639
731
  setCurrentFaceImage(croppedFaces[0]);
640
732
  }
641
733
  }
@@ -650,15 +742,6 @@ const IdentityDocumentCamera = ({
650
742
  // During SCAN_HOLOGRAM, allow processing even if text is not readable
651
743
  }
652
744
 
653
- // Capture test mode data
654
- if (testMode && text && text.includes('<')) {
655
- const mrzOnlyText = scannedText?.mrzOnlyText || text;
656
- setTestModeData({
657
- mrzText: mrzOnlyText,
658
- timestamp: Date.now()
659
- });
660
- }
661
-
662
745
  // MRZ stability: require consistent valid reads across frames
663
746
  // Skip during SCAN_HOLOGRAM - document type already locked
664
747
  const mrzHasRequiredFields = hasRequiredMRZFields(parsedMRZData?.fields);
@@ -674,12 +757,37 @@ const IdentityDocumentCamera = ({
674
757
  }
675
758
  // else: Invalid/no MRZ - skip frame without resetting (allows temporary OCR noise)
676
759
 
677
- // Check if we have enough consistent valid reads
678
- const mrzStableAndValid =
679
- // Either: the multi-frame voted consensus has converged (valid + stable),
680
- consensus.stable && parsedMRZData?.valid === true ||
681
- // or the legacy N-identical-reads path is satisfied.
682
- validMRZConsecutiveCount.current >= REQUIRED_CONSISTENT_MRZ_READS && parsedMRZData?.valid === true && areMRZFieldsEqual(lastValidMRZFields.current, parsedMRZData?.fields);
760
+ // Accept ONLY when the multi-frame voted consensus is stable.
761
+ //
762
+ // Early frames can OCR an ambiguous glyph wrongly yet still produce a
763
+ // check-digit-VALID reading — e.g. a Turkish document number "A53032352"
764
+ // misread as "453032352" (the OCR-B 4 looks like A, and 453032352 also
765
+ // passes its check digit). The old code also accepted on a legacy path of
766
+ // "N identical valid single-frame reads", which those transient frames
767
+ // satisfy — locking in the WRONG number before the voted consensus settles
768
+ // on the correct one. We now require `consensus.stable`: the per-character
769
+ // vote must be unchanged across `stabilityTarget` consecutive contributing
770
+ // frames, which is exactly the "only accept once stable" guarantee.
771
+ // `addFrame` runs for every frame above, so the consensus is always current.
772
+ const mrzStableAndValid = consensus.stable && parsedMRZData?.valid === true;
773
+
774
+ // Surface the reliable-detection signal to the render so the UI can show a
775
+ // green "GO" the moment (and only once) the consensus is stable + valid.
776
+ // Never flips green on an early/transient valid read.
777
+ setMrzReliable(prev => prev === mrzStableAndValid ? prev : mrzStableAndValid);
778
+
779
+ // Test-mode panel: show the MRZ ONLY once it is stable + valid (the same
780
+ // consensus matched across `stabilityTarget` consecutive frames). Showing
781
+ // the raw per-frame OCR here would flash transient, possibly mis-converted
782
+ // readings (e.g. a momentary "4" for "A") before the vote settles — the
783
+ // opposite of what the screen should convey. We display the corrected,
784
+ // check-digit-valid consensus and never an unstable intermediate.
785
+ if (testMode && mrzStableAndValid && mrzText) {
786
+ setTestModeData(prev => prev?.mrzText === mrzText ? prev : {
787
+ mrzText,
788
+ timestamp: Date.now()
789
+ });
790
+ }
683
791
 
684
792
  // ============================================================================
685
793
  // SCAN_ID_BACK STEP - Validate MRZ + barcode on back of ID card
@@ -834,8 +942,8 @@ const IdentityDocumentCamera = ({
834
942
  // Skip face position validation for hologram — flash toggling causes position jitter
835
943
  if (primaryFaceOnly) {
836
944
  hologramFramesWithoutFace.current = 0;
837
- if (currentHologramImage) {
838
- scannedData.hologramImage = currentHologramImage;
945
+ if (currentHologramImageRef.current) {
946
+ scannedData.hologramImage = currentHologramImageRef.current;
839
947
  } else if (faceImages.current.length < HOLOGRAM_IMAGE_COUNT) {
840
948
  // Space out captures for better variation
841
949
  const timeSinceLastCapture = Date.now() - lastHologramCaptureTime.current;
@@ -895,6 +1003,7 @@ const IdentityDocumentCamera = ({
895
1003
  if (hologram) {
896
1004
  setCurrentHologramMaskImage(hologramMask);
897
1005
  scannedData.hologramImage = hologram;
1006
+ currentHologramImageRef.current = hologram;
898
1007
  setCurrentHologramImage(hologram);
899
1008
  if (isDebugEnabled()) {
900
1009
  debugLog('IdentityDocumentCamera', '[Hologram] ✓ Saved hologram image');
@@ -920,8 +1029,8 @@ const IdentityDocumentCamera = ({
920
1029
  } else {
921
1030
  hologramFramesWithoutFace.current++;
922
1031
  }
923
- } else if (currentHologramImage) {
924
- scannedData.hologramImage = currentHologramImage;
1032
+ } else if (currentHologramImageRef.current) {
1033
+ scannedData.hologramImage = currentHologramImageRef.current;
925
1034
  }
926
1035
 
927
1036
  // Secondary face capture (continuous during initial scan and hologram detection)
@@ -1055,10 +1164,20 @@ const IdentityDocumentCamera = ({
1055
1164
  return;
1056
1165
  }
1057
1166
 
1058
- // Complete when hologram captured OR retries exhausted (not mid-collection)
1059
- 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
1060
1174
 
1061
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
+ }
1062
1181
  // Ensure preserved MRZ data is included (current frame may not have
1063
1182
  // readable MRZ due to flash/tilting during hologram capture)
1064
1183
  if (!scannedData.mrzText && lastValidMRZText.current) {
@@ -1170,6 +1289,15 @@ const IdentityDocumentCamera = ({
1170
1289
  brightnessHistory.current.shift();
1171
1290
  }
1172
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;
1173
1301
  const isOverallBright = avgBrightness >= MIN_BRIGHTNESS_THRESHOLD;
1174
1302
 
1175
1303
  // Check brightness in center region (area of interest)
@@ -1180,6 +1308,7 @@ const IdentityDocumentCamera = ({
1180
1308
  isRegionBright = isOverallBright;
1181
1309
  }
1182
1310
  setIsBrightnessLow(!isRegionBright);
1311
+ if (!isRegionBright) diagStats.current.hadLowBrightness = true;
1183
1312
 
1184
1313
  // Check blur only in center region (area of interest) to avoid false positives
1185
1314
  // from iOS depth-of-field background blur
@@ -1200,10 +1329,30 @@ const IdentityDocumentCamera = ({
1200
1329
  );
1201
1330
  isNotBlurry = !isBlurry;
1202
1331
  setIsFrameBlurry(isBlurry);
1332
+ if (isBlurry) diagStats.current.hadBlurryFrames = true;
1203
1333
  } catch (error) {
1204
1334
  setIsFrameBlurry(false);
1205
1335
  }
1206
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
+
1207
1356
  // Only proceed if image quality is acceptable
1208
1357
  const hasAcceptableQuality = isRegionBright && isNotBlurry;
1209
1358
 
@@ -1248,6 +1397,7 @@ const IdentityDocumentCamera = ({
1248
1397
  resultText,
1249
1398
  mrzOnlyText: undefined,
1250
1399
  // Will be set below if MRZ blocks detected
1400
+ textConfidence: frame.textConfidence,
1251
1401
  blocks: textBlocks.map(block => ({
1252
1402
  blockText: block.text || '',
1253
1403
  blockFrame: block.blockFrame ?? {
@@ -1635,8 +1785,10 @@ const IdentityDocumentCamera = ({
1635
1785
  }), /*#__PURE__*/_jsx(AnimatedText, {
1636
1786
  style: [styles.topZoneText,
1637
1787
  // Priority order for coloring (later styles override earlier ones)
1638
- // 1. Success (green) - scan completed
1639
- nextStep === 'COMPLETED' && styles.topZoneTextSuccess,
1788
+ // 1. Success/GO (green) - scan completed OR MRZ reliably detected
1789
+ // (stable consensus). This is the "GO" signal; never shown on a
1790
+ // transient/early valid read.
1791
+ (nextStep === 'COMPLETED' || mrzReliable) && styles.topZoneTextSuccess,
1640
1792
  // 2. Error (red) - wrong side - with flash opacity
1641
1793
  status === 'INCORRECT' && styles.topZoneTextError, status === 'INCORRECT' && {
1642
1794
  opacity: errorFlashAnim
@@ -1647,7 +1799,7 @@ const IdentityDocumentCamera = ({
1647
1799
  status === 'SCANNING' && allElementsDetected && elementsOutsideScanArea.length === 0 && !isBrightnessLow && !isFrameBlurry && styles.topZoneTextScanning
1648
1800
  // 5. Default (white) - aligning (not all detected OR elements outside scan area)
1649
1801
  ],
1650
- children: getStatusMessage(nextStep, status, detectedDocumentType, isBrightnessLow, isFrameBlurry, allElementsDetected, elementsOutsideScanArea, t)
1802
+ children: getStatusMessage(nextStep, status, detectedDocumentType, isBrightnessLow, isFrameBlurry, allElementsDetected, elementsOutsideScanArea, t, mrzReliable)
1651
1803
  })]
1652
1804
  }), /*#__PURE__*/_jsx(View, {
1653
1805
  style: styles.leftZone
@@ -1657,8 +1809,11 @@ const IdentityDocumentCamera = ({
1657
1809
  style: styles.bottomZone
1658
1810
  }), /*#__PURE__*/_jsx(View, {
1659
1811
  style: [styles.scanArea, {
1660
- borderColor: nextStep === 'COMPLETED' ? '#4CAF50' : status === 'INCORRECT' ? '#f44336' : status === 'SCANNING' ? '#2196F3' : isBrightnessLow || isFrameBlurry ? '#FFC107' : 'white',
1661
- borderWidth: status === 'SCANNING' ? 3 : 2
1812
+ borderColor:
1813
+ // Green once the scan is done OR the MRZ is reliably detected
1814
+ // (stable consensus) — the "GO" signal. Never green before that.
1815
+ nextStep === 'COMPLETED' || mrzReliable ? '#4CAF50' : status === 'INCORRECT' ? '#f44336' : status === 'SCANNING' ? '#2196F3' : isBrightnessLow || isFrameBlurry ? '#FFC107' : 'white',
1816
+ borderWidth: mrzReliable || status === 'SCANNING' ? 3 : 2
1662
1817
  }],
1663
1818
  children: nextStep === 'COMPLETED' ? /*#__PURE__*/_jsx(LottieView, {
1664
1819
  source: require('../../Shared/Animations/success.json'),
@@ -78,10 +78,17 @@ export function transformBoundsToScreen(bounds, scale, offsetX, offsetY) {
78
78
  * Unified status message logic used by both voice guidance and render text.
79
79
  * Returns the appropriate i18n key arguments for the current scan state.
80
80
  */
81
- export function getStatusMessage(nextStep, status, detectedDocumentType, isBrightnessLow, isFrameBlurry, allElementsDetected, elementsOutsideScanArea, t) {
81
+ export function getStatusMessage(nextStep, status, detectedDocumentType, isBrightnessLow, isFrameBlurry, allElementsDetected, elementsOutsideScanArea, t, mrzReliable = false) {
82
82
  if (nextStep === 'COMPLETED') {
83
83
  return t('identityDocumentCamera.scanCompleted');
84
84
  }
85
+
86
+ // MRZ reliably detected (stable consensus) — the green "GO" message. Ranks
87
+ // above the in-progress states so the user gets a clear positive signal the
88
+ // moment the reading has settled, but never on an early/transient valid read.
89
+ if (mrzReliable) {
90
+ return t('identityDocumentCamera.mrzDetected');
91
+ }
85
92
  if (status === 'INCORRECT') {
86
93
  if (nextStep === 'SCAN_ID_FRONT_OR_PASSPORT') {
87
94
  return t('identityDocumentCamera.alignIDFront');
@@ -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;