@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.
- package/android/src/main/java/com/trustchex/reactnativesdk/camera/TrustchexCameraView.kt +76 -24
- package/android/src/main/java/com/trustchex/reactnativesdk/mlkit/MLKitModule.kt +1 -0
- package/ios/Camera/TrustchexCameraView.swift +20 -15
- package/lib/module/Screens/Dynamic/LivenessDetectionScreen.js +3 -2
- package/lib/module/Screens/Static/QrCodeScanningScreen.js +12 -2
- package/lib/module/Screens/Static/ResultScreen.js +145 -113
- package/lib/module/Shared/Components/DebugOverlay.js +5 -2
- package/lib/module/Shared/Components/DiagnosticReportButton.js +64 -0
- package/lib/module/Shared/Components/IdentityDocumentCamera.js +185 -30
- package/lib/module/Shared/Components/IdentityDocumentCamera.utils.js +8 -1
- package/lib/module/Shared/Components/NavigationManager.js +7 -9
- package/lib/module/Shared/EIDReader/eidReader.js +159 -38
- package/lib/module/Shared/Libs/MRZ_KNOWN_ISSUES.md +41 -0
- package/lib/module/Shared/Libs/SignalingClient.js +6 -5
- package/lib/module/Shared/Libs/crypto.utils.js +25 -3
- package/lib/module/Shared/Libs/deeplink.utils.js +37 -7
- package/lib/module/Shared/Libs/diagnosticReport.js +133 -0
- package/lib/module/Shared/Libs/diagnostics.js +171 -0
- package/lib/module/Shared/Libs/http-client.js +19 -6
- package/lib/module/Shared/Libs/mrz.utils.js +193 -33
- package/lib/module/Shared/Libs/mrzFrameAggregator.js +74 -3
- package/lib/module/Shared/Libs/native-device-info.utils.js +67 -0
- package/lib/module/Shared/Libs/promise.utils.js +4 -3
- package/lib/module/Shared/Libs/sendDiagnosticReport.js +130 -0
- package/lib/module/Shared/Libs/shuffle.utils.js +17 -0
- package/lib/module/Shared/Services/AnalyticsService.js +8 -2
- package/lib/module/Shared/Services/DataUploadService.js +114 -127
- package/lib/module/Shared/Services/VideoSessionService.js +4 -3
- package/lib/module/Translation/Resources/en.js +6 -0
- package/lib/module/Translation/Resources/tr.js +6 -0
- package/lib/module/Trustchex.js +20 -4
- package/lib/module/version.js +1 -1
- package/lib/typescript/src/Screens/Dynamic/LivenessDetectionScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Static/QrCodeScanningScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Static/ResultScreen.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/DebugOverlay.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/DiagnosticReportButton.d.ts +20 -0
- package/lib/typescript/src/Shared/Components/DiagnosticReportButton.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.types.d.ts +10 -0
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.types.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.utils.d.ts +1 -1
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/NavigationManager.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/TrustchexCamera.d.ts +10 -0
- package/lib/typescript/src/Shared/Components/TrustchexCamera.d.ts.map +1 -1
- package/lib/typescript/src/Shared/EIDReader/eidReader.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/SignalingClient.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/crypto.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/deeplink.utils.d.ts +1 -0
- package/lib/typescript/src/Shared/Libs/deeplink.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/diagnosticReport.d.ts +48 -0
- package/lib/typescript/src/Shared/Libs/diagnosticReport.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Libs/diagnostics.d.ts +145 -0
- package/lib/typescript/src/Shared/Libs/diagnostics.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Libs/http-client.d.ts +2 -0
- package/lib/typescript/src/Shared/Libs/http-client.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/mrz.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/mrzFrameAggregator.d.ts +55 -0
- package/lib/typescript/src/Shared/Libs/mrzFrameAggregator.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/native-device-info.utils.d.ts +33 -0
- package/lib/typescript/src/Shared/Libs/native-device-info.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/promise.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/sendDiagnosticReport.d.ts +30 -0
- package/lib/typescript/src/Shared/Libs/sendDiagnosticReport.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Libs/shuffle.utils.d.ts +3 -0
- package/lib/typescript/src/Shared/Libs/shuffle.utils.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Services/AnalyticsService.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Services/DataUploadService.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Services/VideoSessionService.d.ts.map +1 -1
- package/lib/typescript/src/Translation/Resources/en.d.ts +6 -0
- package/lib/typescript/src/Translation/Resources/en.d.ts.map +1 -1
- package/lib/typescript/src/Translation/Resources/tr.d.ts +6 -0
- package/lib/typescript/src/Translation/Resources/tr.d.ts.map +1 -1
- package/lib/typescript/src/Trustchex.d.ts.map +1 -1
- package/lib/typescript/src/version.d.ts +1 -1
- package/package.json +17 -3
- package/src/Screens/Dynamic/LivenessDetectionScreen.tsx +4 -3
- package/src/Screens/Static/QrCodeScanningScreen.tsx +19 -2
- package/src/Screens/Static/ResultScreen.tsx +172 -162
- package/src/Shared/Components/DebugOverlay.tsx +5 -2
- package/src/Shared/Components/DiagnosticReportButton.tsx +77 -0
- package/src/Shared/Components/IdentityDocumentCamera.tsx +200 -29
- package/src/Shared/Components/IdentityDocumentCamera.types.ts +6 -0
- package/src/Shared/Components/IdentityDocumentCamera.utils.ts +9 -1
- package/src/Shared/Components/NavigationManager.tsx +7 -7
- package/src/Shared/Components/TrustchexCamera.tsx +6 -0
- package/src/Shared/EIDReader/eidReader.ts +181 -52
- package/src/Shared/Libs/MRZ_KNOWN_ISSUES.md +41 -0
- package/src/Shared/Libs/SignalingClient.ts +10 -5
- package/src/Shared/Libs/crypto.utils.ts +41 -4
- package/src/Shared/Libs/deeplink.utils.ts +39 -7
- package/src/Shared/Libs/diagnosticReport.ts +206 -0
- package/src/Shared/Libs/diagnostics.ts +251 -0
- package/src/Shared/Libs/http-client.ts +24 -8
- package/src/Shared/Libs/mrz.utils.ts +200 -34
- package/src/Shared/Libs/mrzFrameAggregator.ts +112 -2
- package/src/Shared/Libs/native-device-info.utils.ts +116 -0
- package/src/Shared/Libs/promise.utils.ts +11 -5
- package/src/Shared/Libs/sendDiagnosticReport.ts +165 -0
- package/src/Shared/Libs/shuffle.utils.ts +15 -0
- package/src/Shared/Services/AnalyticsService.ts +17 -1
- package/src/Shared/Services/DataUploadService.ts +155 -166
- package/src/Shared/Services/VideoSessionService.ts +15 -3
- package/src/Translation/Resources/en.ts +7 -0
- package/src/Translation/Resources/tr.ts +7 -0
- package/src/Trustchex.tsx +26 -3
- 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);
|
|
@@ -127,6 +141,11 @@ const IdentityDocumentCamera = ({
|
|
|
127
141
|
const [status, setStatus] = useState<
|
|
128
142
|
'SEARCHING' | 'SCANNING' | 'SCANNED' | 'INCORRECT'
|
|
129
143
|
>('SEARCHING');
|
|
144
|
+
// True once the multi-frame voted MRZ consensus is STABLE + valid (the same
|
|
145
|
+
// reading matched across `stabilityTarget` consecutive frames, with a clear
|
|
146
|
+
// vote margin on contested cells). Drives the green "GO" signal — we never show
|
|
147
|
+
// green on an early/transient valid read, only on a reliably-detected MRZ.
|
|
148
|
+
const [mrzReliable, setMrzReliable] = useState(false);
|
|
130
149
|
const [nextStep, setNextStep] = useState<
|
|
131
150
|
'SCAN_ID_FRONT_OR_PASSPORT' | 'SCAN_ID_BACK' | 'SCAN_HOLOGRAM' | 'COMPLETED'
|
|
132
151
|
>('SCAN_ID_FRONT_OR_PASSPORT');
|
|
@@ -148,7 +167,10 @@ const IdentityDocumentCamera = ({
|
|
|
148
167
|
// Multi-frame per-character voting: fuses OCR across frames so a single-frame
|
|
149
168
|
// misread (B↔8, S↔5, <↔K, dropped/inserted digit) is outvoted instead of
|
|
150
169
|
// resetting progress. Drives the same `parsedMRZData`/stability the flow uses.
|
|
151
|
-
|
|
170
|
+
// stabilityTarget: 3 — the voted consensus must read the SAME valid MRZ across
|
|
171
|
+
// 3 consecutive contributing frames before we accept, so a transient look-alike
|
|
172
|
+
// misread (e.g. OCR-B "4" vs "A") can never lock in on one or two early frames.
|
|
173
|
+
const mrzAggregator = useRef(new MRZFrameAggregator({ stabilityTarget: 3 }));
|
|
152
174
|
|
|
153
175
|
// Document type stability tracking - require consistent detections from good quality frames
|
|
154
176
|
const lastDetectedDocType = useRef<
|
|
@@ -180,10 +202,30 @@ const IdentityDocumentCamera = ({
|
|
|
180
202
|
const isHologramDetectionInProgress = useRef(false); // Prevent concurrent hologram detection calls
|
|
181
203
|
const isCompletionCallbackInvoked = useRef(false); // Prevent multiple callback invocations when COMPLETED
|
|
182
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);
|
|
183
214
|
const isCompletionAnnouncementSpoken = useRef(false); // Prevent duplicate "scan completed" voice guidance
|
|
184
215
|
|
|
185
216
|
const faceDetectionErrorCount = useRef(0);
|
|
186
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
|
+
});
|
|
187
229
|
const [faceDetectionEnabled, setFaceDetectionEnabled] = useState(true);
|
|
188
230
|
const faceImages = useRef<string[]>([]);
|
|
189
231
|
const hologramImageCountRef = useRef(0);
|
|
@@ -269,6 +311,7 @@ const IdentityDocumentCamera = ({
|
|
|
269
311
|
setIsActive(true);
|
|
270
312
|
isCompletionCallbackInvoked.current = false; // Reset callback flag when starting new scan
|
|
271
313
|
isStepTransitionInProgress.current = false;
|
|
314
|
+
stepTransitionFrom.current = null;
|
|
272
315
|
isCompletionAnnouncementSpoken.current = false;
|
|
273
316
|
} else {
|
|
274
317
|
setIsActive(false);
|
|
@@ -283,12 +326,28 @@ const IdentityDocumentCamera = ({
|
|
|
283
326
|
lastValidMRZFields.current = null;
|
|
284
327
|
validMRZConsecutiveCount.current = 0;
|
|
285
328
|
mrzAggregator.current.reset();
|
|
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
|
+
};
|
|
286
342
|
cachedBarcode.current = null;
|
|
287
343
|
isCompletionCallbackInvoked.current = false;
|
|
288
344
|
isStepTransitionInProgress.current = false;
|
|
345
|
+
stepTransitionFrom.current = null;
|
|
289
346
|
isCompletionAnnouncementSpoken.current = false;
|
|
290
347
|
// Clear all captured image states from previous scan
|
|
348
|
+
currentFaceImageRef.current = undefined;
|
|
291
349
|
setCurrentFaceImage(undefined);
|
|
350
|
+
currentHologramImageRef.current = undefined;
|
|
292
351
|
setCurrentHologramImage(undefined);
|
|
293
352
|
setCurrentSecondaryFaceImage(undefined);
|
|
294
353
|
resetLastMessage();
|
|
@@ -302,9 +361,12 @@ const IdentityDocumentCamera = ({
|
|
|
302
361
|
setLatestHologramFaceImage(undefined);
|
|
303
362
|
isCompletionCallbackInvoked.current = false; // Reset callback flag on unmount
|
|
304
363
|
isStepTransitionInProgress.current = false;
|
|
364
|
+
stepTransitionFrom.current = null;
|
|
305
365
|
isCompletionAnnouncementSpoken.current = false;
|
|
306
366
|
// Clear all captured image states on unmount
|
|
367
|
+
currentFaceImageRef.current = undefined;
|
|
307
368
|
setCurrentFaceImage(undefined);
|
|
369
|
+
currentHologramImageRef.current = undefined;
|
|
308
370
|
setCurrentHologramImage(undefined);
|
|
309
371
|
setCurrentSecondaryFaceImage(undefined);
|
|
310
372
|
resetLastMessage();
|
|
@@ -378,6 +440,7 @@ const IdentityDocumentCamera = ({
|
|
|
378
440
|
isCompletionCallbackInvoked.current = false;
|
|
379
441
|
}
|
|
380
442
|
isStepTransitionInProgress.current = false;
|
|
443
|
+
stepTransitionFrom.current = null;
|
|
381
444
|
}, [nextStep]);
|
|
382
445
|
|
|
383
446
|
// Update status bar based on guide visibility
|
|
@@ -578,6 +641,7 @@ const IdentityDocumentCamera = ({
|
|
|
578
641
|
setHologramImageCount(0);
|
|
579
642
|
setLatestHologramFaceImage(undefined);
|
|
580
643
|
// Clear previous hologram state to prevent premature completion
|
|
644
|
+
currentHologramImageRef.current = undefined;
|
|
581
645
|
setCurrentHologramImage(undefined);
|
|
582
646
|
setCurrentHologramMaskImage(undefined);
|
|
583
647
|
}
|
|
@@ -609,6 +673,7 @@ const IdentityDocumentCamera = ({
|
|
|
609
673
|
lastValidMRZFields.current = null;
|
|
610
674
|
// New MRZ expected on the back — start voting fresh.
|
|
611
675
|
mrzAggregator.current.reset();
|
|
676
|
+
setMrzReliable(false);
|
|
612
677
|
}
|
|
613
678
|
validMRZConsecutiveCount.current = 0;
|
|
614
679
|
cachedBarcode.current = null; // Clear cached barcode on step change
|
|
@@ -633,11 +698,18 @@ const IdentityDocumentCamera = ({
|
|
|
633
698
|
fromStep: 'SCAN_ID_FRONT_OR_PASSPORT' | 'SCAN_ID_BACK' | 'SCAN_HOLOGRAM',
|
|
634
699
|
scannedData: DocumentScannedData
|
|
635
700
|
) => {
|
|
636
|
-
|
|
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
|
+
) {
|
|
637
708
|
return;
|
|
638
709
|
}
|
|
639
710
|
|
|
640
711
|
isStepTransitionInProgress.current = true;
|
|
712
|
+
stepTransitionFrom.current = fromStep;
|
|
641
713
|
|
|
642
714
|
// Torch only needed during SCAN_HOLOGRAM - turn off for all other transitions
|
|
643
715
|
if (nextStepType !== 'SCAN_HOLOGRAM') {
|
|
@@ -870,9 +942,32 @@ const IdentityDocumentCamera = ({
|
|
|
870
942
|
)
|
|
871
943
|
: 0;
|
|
872
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;
|
|
873
950
|
const consensus = mrzAggregator.current.addFrame({
|
|
874
951
|
text: textForValidation,
|
|
875
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,
|
|
876
971
|
});
|
|
877
972
|
|
|
878
973
|
// Use the fused consensus when it is valid; otherwise fall back to a
|
|
@@ -912,6 +1007,9 @@ const IdentityDocumentCamera = ({
|
|
|
912
1007
|
? 'PASSPORT'
|
|
913
1008
|
: detectedDocumentType;
|
|
914
1009
|
|
|
1010
|
+
// Record the detected document type for the diagnostic report.
|
|
1011
|
+
if (documentType) diagnostics.setDocument(documentType);
|
|
1012
|
+
|
|
915
1013
|
// Crop faces once document type is confirmed or we're past the initial step
|
|
916
1014
|
const shouldCropFaces =
|
|
917
1015
|
documentType === 'ID_FRONT' ||
|
|
@@ -995,6 +1093,7 @@ const IdentityDocumentCamera = ({
|
|
|
995
1093
|
// Continue scanning without locking this face
|
|
996
1094
|
} else {
|
|
997
1095
|
faceImageToUse = croppedFaces[0];
|
|
1096
|
+
currentFaceImageRef.current = croppedFaces[0];
|
|
998
1097
|
setCurrentFaceImage(croppedFaces[0]);
|
|
999
1098
|
}
|
|
1000
1099
|
}
|
|
@@ -1009,15 +1108,6 @@ const IdentityDocumentCamera = ({
|
|
|
1009
1108
|
// During SCAN_HOLOGRAM, allow processing even if text is not readable
|
|
1010
1109
|
}
|
|
1011
1110
|
|
|
1012
|
-
// Capture test mode data
|
|
1013
|
-
if (testMode && text && text.includes('<')) {
|
|
1014
|
-
const mrzOnlyText = scannedText?.mrzOnlyText || text;
|
|
1015
|
-
setTestModeData({
|
|
1016
|
-
mrzText: mrzOnlyText,
|
|
1017
|
-
timestamp: Date.now(),
|
|
1018
|
-
});
|
|
1019
|
-
}
|
|
1020
|
-
|
|
1021
1111
|
// MRZ stability: require consistent valid reads across frames
|
|
1022
1112
|
// Skip during SCAN_HOLOGRAM - document type already locked
|
|
1023
1113
|
const mrzHasRequiredFields = hasRequiredMRZFields(parsedMRZData?.fields);
|
|
@@ -1041,14 +1131,39 @@ const IdentityDocumentCamera = ({
|
|
|
1041
1131
|
}
|
|
1042
1132
|
// else: Invalid/no MRZ - skip frame without resetting (allows temporary OCR noise)
|
|
1043
1133
|
|
|
1044
|
-
//
|
|
1134
|
+
// Accept ONLY when the multi-frame voted consensus is stable.
|
|
1135
|
+
//
|
|
1136
|
+
// Early frames can OCR an ambiguous glyph wrongly yet still produce a
|
|
1137
|
+
// check-digit-VALID reading — e.g. a Turkish document number "A53032352"
|
|
1138
|
+
// misread as "453032352" (the OCR-B 4 looks like A, and 453032352 also
|
|
1139
|
+
// passes its check digit). The old code also accepted on a legacy path of
|
|
1140
|
+
// "N identical valid single-frame reads", which those transient frames
|
|
1141
|
+
// satisfy — locking in the WRONG number before the voted consensus settles
|
|
1142
|
+
// on the correct one. We now require `consensus.stable`: the per-character
|
|
1143
|
+
// vote must be unchanged across `stabilityTarget` consecutive contributing
|
|
1144
|
+
// frames, which is exactly the "only accept once stable" guarantee.
|
|
1145
|
+
// `addFrame` runs for every frame above, so the consensus is always current.
|
|
1045
1146
|
const mrzStableAndValid =
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1147
|
+
consensus.stable && parsedMRZData?.valid === true;
|
|
1148
|
+
|
|
1149
|
+
// Surface the reliable-detection signal to the render so the UI can show a
|
|
1150
|
+
// green "GO" the moment (and only once) the consensus is stable + valid.
|
|
1151
|
+
// Never flips green on an early/transient valid read.
|
|
1152
|
+
setMrzReliable((prev) => (prev === mrzStableAndValid ? prev : mrzStableAndValid));
|
|
1153
|
+
|
|
1154
|
+
// Test-mode panel: show the MRZ ONLY once it is stable + valid (the same
|
|
1155
|
+
// consensus matched across `stabilityTarget` consecutive frames). Showing
|
|
1156
|
+
// the raw per-frame OCR here would flash transient, possibly mis-converted
|
|
1157
|
+
// readings (e.g. a momentary "4" for "A") before the vote settles — the
|
|
1158
|
+
// opposite of what the screen should convey. We display the corrected,
|
|
1159
|
+
// check-digit-valid consensus and never an unstable intermediate.
|
|
1160
|
+
if (testMode && mrzStableAndValid && mrzText) {
|
|
1161
|
+
setTestModeData((prev) =>
|
|
1162
|
+
prev?.mrzText === mrzText
|
|
1163
|
+
? prev
|
|
1164
|
+
: { mrzText, timestamp: Date.now() }
|
|
1165
|
+
);
|
|
1166
|
+
}
|
|
1052
1167
|
|
|
1053
1168
|
// ============================================================================
|
|
1054
1169
|
// SCAN_ID_BACK STEP - Validate MRZ + barcode on back of ID card
|
|
@@ -1278,8 +1393,8 @@ const IdentityDocumentCamera = ({
|
|
|
1278
1393
|
if (primaryFaceOnly) {
|
|
1279
1394
|
hologramFramesWithoutFace.current = 0;
|
|
1280
1395
|
|
|
1281
|
-
if (
|
|
1282
|
-
scannedData.hologramImage =
|
|
1396
|
+
if (currentHologramImageRef.current) {
|
|
1397
|
+
scannedData.hologramImage = currentHologramImageRef.current;
|
|
1283
1398
|
} else if (faceImages.current.length < HOLOGRAM_IMAGE_COUNT) {
|
|
1284
1399
|
// Space out captures for better variation
|
|
1285
1400
|
const timeSinceLastCapture =
|
|
@@ -1366,6 +1481,7 @@ const IdentityDocumentCamera = ({
|
|
|
1366
1481
|
if (hologram) {
|
|
1367
1482
|
setCurrentHologramMaskImage(hologramMask);
|
|
1368
1483
|
scannedData.hologramImage = hologram;
|
|
1484
|
+
currentHologramImageRef.current = hologram;
|
|
1369
1485
|
setCurrentHologramImage(hologram);
|
|
1370
1486
|
if (isDebugEnabled()) {
|
|
1371
1487
|
debugLog(
|
|
@@ -1397,8 +1513,8 @@ const IdentityDocumentCamera = ({
|
|
|
1397
1513
|
} else {
|
|
1398
1514
|
hologramFramesWithoutFace.current++;
|
|
1399
1515
|
}
|
|
1400
|
-
} else if (
|
|
1401
|
-
scannedData.hologramImage =
|
|
1516
|
+
} else if (currentHologramImageRef.current) {
|
|
1517
|
+
scannedData.hologramImage = currentHologramImageRef.current;
|
|
1402
1518
|
}
|
|
1403
1519
|
|
|
1404
1520
|
// Secondary face capture (continuous during initial scan and hologram detection)
|
|
@@ -1611,13 +1727,23 @@ const IdentityDocumentCamera = ({
|
|
|
1611
1727
|
return;
|
|
1612
1728
|
}
|
|
1613
1729
|
|
|
1614
|
-
// 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;
|
|
1615
1736
|
const stepComplete =
|
|
1616
1737
|
(!!scannedData.hologramImage ||
|
|
1617
1738
|
(maxRetriesReached && !isCollecting)) &&
|
|
1618
|
-
!!
|
|
1739
|
+
!!faceForCompletion; // Require face before completing
|
|
1619
1740
|
|
|
1620
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
|
+
}
|
|
1621
1747
|
// Ensure preserved MRZ data is included (current frame may not have
|
|
1622
1748
|
// readable MRZ due to flash/tilting during hologram capture)
|
|
1623
1749
|
if (!scannedData.mrzText && lastValidMRZText.current) {
|
|
@@ -1820,6 +1946,21 @@ const IdentityDocumentCamera = ({
|
|
|
1820
1946
|
const avgBrightness =
|
|
1821
1947
|
brightnessHistory.current.reduce((a, b) => a + b, 0) /
|
|
1822
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;
|
|
1823
1964
|
const isOverallBright = avgBrightness >= MIN_BRIGHTNESS_THRESHOLD;
|
|
1824
1965
|
|
|
1825
1966
|
// Check brightness in center region (area of interest)
|
|
@@ -1838,6 +1979,7 @@ const IdentityDocumentCamera = ({
|
|
|
1838
1979
|
}
|
|
1839
1980
|
|
|
1840
1981
|
setIsBrightnessLow(!isRegionBright);
|
|
1982
|
+
if (!isRegionBright) diagStats.current.hadLowBrightness = true;
|
|
1841
1983
|
|
|
1842
1984
|
// Check blur only in center region (area of interest) to avoid false positives
|
|
1843
1985
|
// from iOS depth-of-field background blur
|
|
@@ -1856,10 +1998,31 @@ const IdentityDocumentCamera = ({
|
|
|
1856
1998
|
);
|
|
1857
1999
|
isNotBlurry = !isBlurry;
|
|
1858
2000
|
setIsFrameBlurry(isBlurry);
|
|
2001
|
+
if (isBlurry) diagStats.current.hadBlurryFrames = true;
|
|
1859
2002
|
} catch (error) {
|
|
1860
2003
|
setIsFrameBlurry(false);
|
|
1861
2004
|
}
|
|
1862
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
|
+
|
|
1863
2026
|
// Only proceed if image quality is acceptable
|
|
1864
2027
|
const hasAcceptableQuality = isRegionBright && isNotBlurry;
|
|
1865
2028
|
|
|
@@ -1908,6 +2071,7 @@ const IdentityDocumentCamera = ({
|
|
|
1908
2071
|
const scannedText: BlockText = {
|
|
1909
2072
|
resultText,
|
|
1910
2073
|
mrzOnlyText: undefined, // Will be set below if MRZ blocks detected
|
|
2074
|
+
textConfidence: frame.textConfidence,
|
|
1911
2075
|
blocks: textBlocks.map((block) => ({
|
|
1912
2076
|
blockText: block.text || '',
|
|
1913
2077
|
blockFrame: block.blockFrame ?? {
|
|
@@ -2430,8 +2594,11 @@ const IdentityDocumentCamera = ({
|
|
|
2430
2594
|
style={[
|
|
2431
2595
|
styles.topZoneText,
|
|
2432
2596
|
// Priority order for coloring (later styles override earlier ones)
|
|
2433
|
-
// 1. Success (green) - scan completed
|
|
2434
|
-
|
|
2597
|
+
// 1. Success/GO (green) - scan completed OR MRZ reliably detected
|
|
2598
|
+
// (stable consensus). This is the "GO" signal; never shown on a
|
|
2599
|
+
// transient/early valid read.
|
|
2600
|
+
(nextStep === 'COMPLETED' || mrzReliable) &&
|
|
2601
|
+
styles.topZoneTextSuccess,
|
|
2435
2602
|
// 2. Error (red) - wrong side - with flash opacity
|
|
2436
2603
|
status === 'INCORRECT' && styles.topZoneTextError,
|
|
2437
2604
|
status === 'INCORRECT' && {
|
|
@@ -2457,7 +2624,8 @@ const IdentityDocumentCamera = ({
|
|
|
2457
2624
|
isFrameBlurry,
|
|
2458
2625
|
allElementsDetected,
|
|
2459
2626
|
elementsOutsideScanArea,
|
|
2460
|
-
t
|
|
2627
|
+
t,
|
|
2628
|
+
mrzReliable
|
|
2461
2629
|
)}
|
|
2462
2630
|
</AnimatedText>
|
|
2463
2631
|
</View>
|
|
@@ -2469,7 +2637,9 @@ const IdentityDocumentCamera = ({
|
|
|
2469
2637
|
styles.scanArea,
|
|
2470
2638
|
{
|
|
2471
2639
|
borderColor:
|
|
2472
|
-
|
|
2640
|
+
// Green once the scan is done OR the MRZ is reliably detected
|
|
2641
|
+
// (stable consensus) — the "GO" signal. Never green before that.
|
|
2642
|
+
nextStep === 'COMPLETED' || mrzReliable
|
|
2473
2643
|
? '#4CAF50'
|
|
2474
2644
|
: status === 'INCORRECT'
|
|
2475
2645
|
? '#f44336'
|
|
@@ -2478,7 +2648,8 @@ const IdentityDocumentCamera = ({
|
|
|
2478
2648
|
: isBrightnessLow || isFrameBlurry
|
|
2479
2649
|
? '#FFC107'
|
|
2480
2650
|
: 'white',
|
|
2481
|
-
borderWidth:
|
|
2651
|
+
borderWidth:
|
|
2652
|
+
mrzReliable || status === 'SCANNING' ? 3 : 2,
|
|
2482
2653
|
},
|
|
2483
2654
|
]}
|
|
2484
2655
|
>
|
|
@@ -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 = {
|
|
@@ -96,12 +96,20 @@ export function getStatusMessage(
|
|
|
96
96
|
isFrameBlurry: boolean,
|
|
97
97
|
allElementsDetected: boolean,
|
|
98
98
|
elementsOutsideScanArea: string[],
|
|
99
|
-
t: (key: string, params?: Record<string, unknown>) => string
|
|
99
|
+
t: (key: string, params?: Record<string, unknown>) => string,
|
|
100
|
+
mrzReliable: boolean = false
|
|
100
101
|
): string {
|
|
101
102
|
if (nextStep === 'COMPLETED') {
|
|
102
103
|
return t('identityDocumentCamera.scanCompleted');
|
|
103
104
|
}
|
|
104
105
|
|
|
106
|
+
// MRZ reliably detected (stable consensus) — the green "GO" message. Ranks
|
|
107
|
+
// above the in-progress states so the user gets a clear positive signal the
|
|
108
|
+
// moment the reading has settled, but never on an early/transient valid read.
|
|
109
|
+
if (mrzReliable) {
|
|
110
|
+
return t('identityDocumentCamera.mrzDetected');
|
|
111
|
+
}
|
|
112
|
+
|
|
105
113
|
if (status === 'INCORRECT') {
|
|
106
114
|
if (nextStep === 'SCAN_ID_FRONT_OR_PASSPORT') {
|
|
107
115
|
return t('identityDocumentCamera.alignIDFront');
|
|
@@ -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={
|
|
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
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
|