@trustchex/react-native-sdk 1.334.0 → 1.354.0

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 (72) hide show
  1. package/lib/module/Screens/Dynamic/ContractAcceptanceScreen.js +8 -2
  2. package/lib/module/Screens/Dynamic/IdentityDocumentEIDScanningScreen.js +5 -1
  3. package/lib/module/Screens/Dynamic/IdentityDocumentScanningScreen.js +5 -1
  4. package/lib/module/Screens/Dynamic/LivenessDetectionScreen.js +29 -15
  5. package/lib/module/Screens/Static/OTPVerificationScreen.js +285 -0
  6. package/lib/module/Screens/Static/ResultScreen.js +90 -26
  7. package/lib/module/Screens/Static/VerificationSessionCheckScreen.js +48 -134
  8. package/lib/module/Shared/Components/DebugNavigationPanel.js +252 -0
  9. package/lib/module/Shared/Components/EIDScanner.js +142 -17
  10. package/lib/module/Shared/Components/FaceCamera.js +23 -11
  11. package/lib/module/Shared/Components/IdentityDocumentCamera.js +295 -44
  12. package/lib/module/Shared/Components/NavigationManager.js +19 -3
  13. package/lib/module/Shared/Config/camera-enhancement.config.js +58 -0
  14. package/lib/module/Shared/Contexts/AppContext.js +1 -0
  15. package/lib/module/Shared/Libs/camera.utils.js +221 -1
  16. package/lib/module/Shared/Libs/frame-enhancement.utils.js +133 -0
  17. package/lib/module/Shared/Libs/mrz.utils.js +98 -1
  18. package/lib/module/Translation/Resources/en.js +30 -0
  19. package/lib/module/Translation/Resources/tr.js +30 -0
  20. package/lib/module/Trustchex.js +49 -39
  21. package/lib/module/version.js +1 -1
  22. package/lib/typescript/src/Screens/Dynamic/ContractAcceptanceScreen.d.ts.map +1 -1
  23. package/lib/typescript/src/Screens/Dynamic/IdentityDocumentEIDScanningScreen.d.ts.map +1 -1
  24. package/lib/typescript/src/Screens/Dynamic/IdentityDocumentScanningScreen.d.ts.map +1 -1
  25. package/lib/typescript/src/Screens/Dynamic/LivenessDetectionScreen.d.ts.map +1 -1
  26. package/lib/typescript/src/Screens/Static/OTPVerificationScreen.d.ts +3 -0
  27. package/lib/typescript/src/Screens/Static/OTPVerificationScreen.d.ts.map +1 -0
  28. package/lib/typescript/src/Screens/Static/ResultScreen.d.ts.map +1 -1
  29. package/lib/typescript/src/Screens/Static/VerificationSessionCheckScreen.d.ts.map +1 -1
  30. package/lib/typescript/src/Shared/Components/DebugNavigationPanel.d.ts +3 -0
  31. package/lib/typescript/src/Shared/Components/DebugNavigationPanel.d.ts.map +1 -0
  32. package/lib/typescript/src/Shared/Components/EIDScanner.d.ts.map +1 -1
  33. package/lib/typescript/src/Shared/Components/FaceCamera.d.ts.map +1 -1
  34. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.d.ts.map +1 -1
  35. package/lib/typescript/src/Shared/Components/NavigationManager.d.ts.map +1 -1
  36. package/lib/typescript/src/Shared/Config/camera-enhancement.config.d.ts +54 -0
  37. package/lib/typescript/src/Shared/Config/camera-enhancement.config.d.ts.map +1 -0
  38. package/lib/typescript/src/Shared/Contexts/AppContext.d.ts +2 -0
  39. package/lib/typescript/src/Shared/Contexts/AppContext.d.ts.map +1 -1
  40. package/lib/typescript/src/Shared/Libs/camera.utils.d.ts +65 -1
  41. package/lib/typescript/src/Shared/Libs/camera.utils.d.ts.map +1 -1
  42. package/lib/typescript/src/Shared/Libs/frame-enhancement.utils.d.ts +25 -0
  43. package/lib/typescript/src/Shared/Libs/frame-enhancement.utils.d.ts.map +1 -0
  44. package/lib/typescript/src/Shared/Libs/mrz.utils.d.ts.map +1 -1
  45. package/lib/typescript/src/Translation/Resources/en.d.ts +30 -0
  46. package/lib/typescript/src/Translation/Resources/en.d.ts.map +1 -1
  47. package/lib/typescript/src/Translation/Resources/tr.d.ts +30 -0
  48. package/lib/typescript/src/Translation/Resources/tr.d.ts.map +1 -1
  49. package/lib/typescript/src/Trustchex.d.ts.map +1 -1
  50. package/lib/typescript/src/version.d.ts +1 -1
  51. package/package.json +3 -3
  52. package/src/Screens/Dynamic/ContractAcceptanceScreen.tsx +6 -2
  53. package/src/Screens/Dynamic/IdentityDocumentEIDScanningScreen.tsx +3 -1
  54. package/src/Screens/Dynamic/IdentityDocumentScanningScreen.tsx +3 -1
  55. package/src/Screens/Dynamic/LivenessDetectionScreen.tsx +27 -17
  56. package/src/Screens/Static/OTPVerificationScreen.tsx +379 -0
  57. package/src/Screens/Static/ResultScreen.tsx +160 -101
  58. package/src/Screens/Static/VerificationSessionCheckScreen.tsx +51 -196
  59. package/src/Shared/Components/DebugNavigationPanel.tsx +262 -0
  60. package/src/Shared/Components/EIDScanner.tsx +144 -19
  61. package/src/Shared/Components/FaceCamera.tsx +38 -21
  62. package/src/Shared/Components/IdentityDocumentCamera.tsx +399 -101
  63. package/src/Shared/Components/NavigationManager.tsx +19 -3
  64. package/src/Shared/Config/camera-enhancement.config.ts +46 -0
  65. package/src/Shared/Contexts/AppContext.ts +3 -0
  66. package/src/Shared/Libs/camera.utils.ts +240 -1
  67. package/src/Shared/Libs/frame-enhancement.utils.ts +217 -0
  68. package/src/Shared/Libs/mrz.utils.ts +78 -1
  69. package/src/Translation/Resources/en.ts +30 -0
  70. package/src/Translation/Resources/tr.ts +30 -0
  71. package/src/Trustchex.tsx +58 -46
  72. package/src/version.ts +1 -1
@@ -13,6 +13,7 @@ import {
13
13
  Image,
14
14
  ActivityIndicator,
15
15
  } from 'react-native';
16
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
16
17
  import {
17
18
  Camera,
18
19
  runAtTargetFps,
@@ -44,10 +45,11 @@ import {
44
45
  OpenCV,
45
46
  ThresholdTypes,
46
47
  } from 'react-native-fast-opencv';
47
- import { getAverageBrightness } from '../Libs/camera.utils';
48
+ import { getAverageBrightness, getScanAreaCenterPoint, calculateExposureStep, isBlurry as checkBlurry } from '../Libs/camera.utils';
48
49
  import { useTranslation } from 'react-i18next';
49
50
  import LottieView from 'lottie-react-native';
50
51
  import StyledButton from './StyledButton';
52
+ import { SafeAreaView } from 'react-native-safe-area-context';
51
53
  import { type Barcode, scanCodes } from '../VisionCameraPlugins/BarcodeScanner';
52
54
  import { speakWithDebounce } from '../Libs/tts.utils';
53
55
  import AppContext from '../Contexts/AppContext';
@@ -105,10 +107,10 @@ type ElementsData = [
105
107
  export type PhotoOptions = {
106
108
  uri: string;
107
109
  orientation?:
108
- | 'landscapeRight'
109
- | 'portrait'
110
- | 'portraitUpsideDown'
111
- | 'landscapeLeft';
110
+ | 'landscapeRight'
111
+ | 'portrait'
112
+ | 'portraitUpsideDown'
113
+ | 'landscapeLeft';
112
114
  };
113
115
 
114
116
  export interface IdentityDocumentCameraProps {
@@ -135,6 +137,7 @@ const IdentityDocumentCamera = ({
135
137
  }: IdentityDocumentCameraProps) => {
136
138
  useKeepAwake();
137
139
  const theme = useTheme();
140
+ const insets = useSafeAreaInsets();
138
141
  const appContext = React.useContext(AppContext);
139
142
  const cameraRef = React.useRef<Camera>(null);
140
143
  const cameraPermission = useCameraPermission();
@@ -174,6 +177,7 @@ const IdentityDocumentCamera = ({
174
177
  string | undefined
175
178
  >(undefined);
176
179
  const [isBrightnessLow, setIsBrightnessLow] = useState(false);
180
+ const [isFrameBlurry, setIsFrameBlurry] = useState(false);
177
181
  const [hasGuideShown, setHasGuideShown] = useState(false);
178
182
  const [status, setStatus] = useState<
179
183
  'SEARCHING' | 'SCANNING' | 'SCANNED' | 'INCORRECT'
@@ -181,10 +185,17 @@ const IdentityDocumentCamera = ({
181
185
  const [nextStep, setNextStep] = useState<
182
186
  'SCAN_ID_FRONT_OR_PASSPORT' | 'SCAN_ID_BACK' | 'SCAN_HOLOGRAM' | 'COMPLETED'
183
187
  >('SCAN_ID_FRONT_OR_PASSPORT');
188
+ const [completedStep, setCompletedStep] = useState<
189
+ 'SCAN_ID_FRONT_OR_PASSPORT' | 'SCAN_ID_BACK' | 'SCAN_HOLOGRAM' | null
190
+ >(null);
191
+ const [detectedDocumentType, setDetectedDocumentType] = useState<
192
+ 'ID_FRONT' | 'ID_BACK' | 'PASSPORT' | 'UNKNOWN'
193
+ >('UNKNOWN');
184
194
  const hologramDetectionCurrentRetryCount = useSharedValue(0);
185
195
  const secondaryFaceDetectionCurrentRetryCount = useSharedValue(0);
186
196
  const mrzDetectionCurrentRetryCount = useSharedValue(0);
187
197
  const faceDetectionErrorCount = useSharedValue(0);
198
+ const consecutiveBlurCount = useSharedValue(0);
188
199
  const [faceDetectionEnabled, setFaceDetectionEnabled] = useState(true);
189
200
  const { t } = useTranslation();
190
201
  // const [boundingBox, setBoundingBox] = useState<Bounds>({
@@ -262,17 +273,64 @@ const IdentityDocumentCamera = ({
262
273
 
263
274
  useEffect(() => {
264
275
  if (hasGuideShown) {
265
- const message = isBrightnessLow
266
- ? t('identityDocumentCamera.lowBrightness')
267
- : nextStep === 'SCAN_ID_FRONT_OR_PASSPORT'
268
- ? t('identityDocumentCamera.alignPhotoSide')
269
- : nextStep === 'SCAN_HOLOGRAM'
270
- ? t('identityDocumentCamera.alignHologram')
271
- : nextStep === 'SCAN_ID_BACK'
272
- ? t('identityDocumentCamera.alignIDBackSide')
273
- : nextStep === 'COMPLETED'
274
- ? t('identityDocumentCamera.scanCompleted')
275
- : '';
276
+ let message = '';
277
+
278
+ // Priority: scanned > incorrect > blur during scanning > brightness > blur > step-specific
279
+ if (status === 'SCANNED') {
280
+ // Use step-specific completion messages
281
+ if (completedStep === 'SCAN_ID_FRONT_OR_PASSPORT') {
282
+ message = detectedDocumentType === 'PASSPORT'
283
+ ? t('identityDocumentCamera.passportScanned')
284
+ : t('identityDocumentCamera.frontSideScanned');
285
+ } else if (completedStep === 'SCAN_ID_BACK') {
286
+ message = t('identityDocumentCamera.backSideScanned');
287
+ } else if (completedStep === 'SCAN_HOLOGRAM') {
288
+ message = t('identityDocumentCamera.hologramVerified');
289
+ } else {
290
+ message = t('identityDocumentCamera.scanCompleted');
291
+ }
292
+ } else if (status === 'INCORRECT') {
293
+ // Wrong side detected - warn user
294
+ if (nextStep === 'SCAN_ID_FRONT_OR_PASSPORT') {
295
+ message = t('identityDocumentCamera.wrongSideFront');
296
+ } else if (nextStep === 'SCAN_ID_BACK') {
297
+ message = t('identityDocumentCamera.wrongSideBack');
298
+ }
299
+ } else if (isBrightnessLow) {
300
+ // Brightness warning takes priority over blur
301
+ message = t('identityDocumentCamera.lowBrightness');
302
+ } else if (isFrameBlurry) {
303
+ // Show blur warning only when brightness is sufficient
304
+ message = t('identityDocumentCamera.avoidBlur');
305
+ } else if (nextStep === 'SCAN_ID_FRONT_OR_PASSPORT') {
306
+ // Enhanced feedback based on detection status
307
+ if (status === 'SCANNING') {
308
+ if (currentFaceImage) {
309
+ // Document-specific detection message
310
+ if (detectedDocumentType === 'PASSPORT') {
311
+ message = t('identityDocumentCamera.passportDetected');
312
+ } else if (detectedDocumentType === 'ID_FRONT') {
313
+ message = t('identityDocumentCamera.idCardFrontDetected');
314
+ } else {
315
+ message = t('identityDocumentCamera.readingDocument');
316
+ }
317
+ } else {
318
+ message = t('identityDocumentCamera.readingDocument');
319
+ }
320
+ } else {
321
+ message = t('identityDocumentCamera.alignPhotoSide');
322
+ }
323
+ } else if (nextStep === 'SCAN_HOLOGRAM') {
324
+ message = t('identityDocumentCamera.alignHologram');
325
+ } else if (nextStep === 'SCAN_ID_BACK') {
326
+ if (status === 'SCANNING') {
327
+ message = t('identityDocumentCamera.readingDocument');
328
+ } else {
329
+ message = t('identityDocumentCamera.alignIDBackSide');
330
+ }
331
+ } else if (nextStep === 'COMPLETED') {
332
+ message = t('identityDocumentCamera.scanCompleted');
333
+ }
276
334
 
277
335
  if (
278
336
  appContext.currentWorkflowStep?.data?.voiceGuidanceActive &&
@@ -285,10 +343,58 @@ const IdentityDocumentCamera = ({
285
343
  appContext.currentWorkflowStep?.data?.voiceGuidanceActive,
286
344
  hasGuideShown,
287
345
  isBrightnessLow,
346
+ isFrameBlurry,
288
347
  nextStep,
348
+ status,
349
+ completedStep,
350
+ currentFaceImage,
351
+ detectedDocumentType,
289
352
  t,
290
353
  ]);
291
354
 
355
+ // Auto-reset INCORRECT status after showing warning briefly
356
+ useEffect(() => {
357
+ if (status === 'INCORRECT') {
358
+ const timeout = setTimeout(() => {
359
+ setStatus('SEARCHING');
360
+ }, 1500); // Show warning for 1.5 seconds
361
+ return () => clearTimeout(timeout);
362
+ }
363
+ }, [status]);
364
+
365
+ // Periodic autofocus - refocus on scan area center every 2.5 seconds
366
+ useEffect(() => {
367
+ if (!isActive || !device || !cameraRef.current || !device.supportsFocus) {
368
+ return;
369
+ }
370
+
371
+ // Only autofocus during searching and scanning states
372
+ if (status !== 'SEARCHING' && status !== 'SCANNING') {
373
+ return;
374
+ }
375
+
376
+ const autofocusInterval = setInterval(async () => {
377
+ try {
378
+ // Get camera dimensions (assuming format dimensions)
379
+ const width = format?.videoWidth ?? 1920;
380
+ const height = format?.videoHeight ?? 1080;
381
+
382
+ // Calculate center point of scan area
383
+ const centerPoint = getScanAreaCenterPoint(width, height);
384
+
385
+ // Focus on the center of the scan area
386
+ await cameraRef.current?.focus({
387
+ x: centerPoint.x,
388
+ y: centerPoint.y,
389
+ });
390
+ } catch (error) {
391
+ // Ignore autofocus errors
392
+ }
393
+ }, 2500); // Every 2.5 seconds
394
+
395
+ return () => clearInterval(autofocusInterval);
396
+ }, [isActive, device, format, status]);
397
+
292
398
  const detectDocumentType = (
293
399
  faces: Face[],
294
400
  ocrText: string,
@@ -411,6 +517,7 @@ const IdentityDocumentCamera = ({
411
517
  }
412
518
  };
413
519
 
520
+
414
521
  const detectHologram = (images: string[]) => {
415
522
  try {
416
523
  const lowerBound = OpenCV.createObject(ObjectType.Scalar, 40, 90, 90);
@@ -587,10 +694,26 @@ const IdentityDocumentCamera = ({
587
694
  | 'SCAN_ID_FRONT_OR_PASSPORT'
588
695
  | 'SCAN_ID_BACK'
589
696
  | 'SCAN_HOLOGRAM'
590
- | 'COMPLETED'
697
+ | 'COMPLETED',
698
+ fromStep?:
699
+ | 'SCAN_ID_FRONT_OR_PASSPORT'
700
+ | 'SCAN_ID_BACK'
701
+ | 'SCAN_HOLOGRAM'
591
702
  ) => {
703
+ // Track which step was just completed for showing specific message
704
+ if (fromStep) {
705
+ setCompletedStep(fromStep);
706
+ }
592
707
  setNextStep(nextStepType);
593
708
  Vibration.vibrate(100);
709
+
710
+ // Reset status after delay to show success animation fully before next step
711
+ if (nextStepType !== 'COMPLETED') {
712
+ setTimeout(() => {
713
+ setStatus('SEARCHING');
714
+ setCompletedStep(null);
715
+ }, 2000); // Show success checkmark for 2 seconds before transitioning
716
+ }
594
717
  };
595
718
 
596
719
  const handleBrightness = useRunOnJS(
@@ -600,6 +723,13 @@ const IdentityDocumentCamera = ({
600
723
  [setIsBrightnessLow]
601
724
  );
602
725
 
726
+ const handleBlurStatus = useRunOnJS(
727
+ (blurry: boolean) => {
728
+ setIsFrameBlurry(blurry);
729
+ },
730
+ [setIsFrameBlurry]
731
+ );
732
+
603
733
  const handleFaceAndText = useRunOnJS(
604
734
  async (
605
735
  text: string,
@@ -614,7 +744,7 @@ const IdentityDocumentCamera = ({
614
744
  isTorchOn &&
615
745
  (currentHologramImage ||
616
746
  hologramDetectionCurrentRetryCount.value >=
617
- HOLOGRAM_DETECTION_RETRY_COUNT)
747
+ HOLOGRAM_DETECTION_RETRY_COUNT)
618
748
  ) {
619
749
  setIsTorchOn(false);
620
750
  }
@@ -624,7 +754,18 @@ const IdentityDocumentCamera = ({
624
754
  return;
625
755
  }
626
756
 
757
+ // Early wrong side detection for SCAN_ID_BACK: if faces detected, it's the front side
758
+ if (nextStep === 'SCAN_ID_BACK' && faces.length > 0) {
759
+ console.log('[WRONG_SIDE] Back side expected but faces detected:', faces.length);
760
+ setStatus('INCORRECT');
761
+ return;
762
+ }
763
+
627
764
  if (!text || text.length < 10 || !image) {
765
+ // Log when searching to help debug
766
+ if (nextStep === 'SCAN_ID_BACK') {
767
+ console.log('[SCAN_ID_BACK] Searching... faces:', faces.length, 'text length:', text?.length || 0);
768
+ }
628
769
  setStatus('SEARCHING');
629
770
  return;
630
771
  }
@@ -652,16 +793,24 @@ const IdentityDocumentCamera = ({
652
793
  scannedData.faceImage = croppedFaces[0];
653
794
  setCurrentFaceImage(croppedFaces[0]);
654
795
 
655
- if (!onlyMRZScan) {
656
- if (
657
- (nextStep === 'SCAN_ID_FRONT_OR_PASSPORT' &&
658
- !(documentType === 'ID_FRONT' || documentType === 'PASSPORT')) ||
659
- (nextStep === 'SCAN_ID_BACK' && documentType !== 'ID_BACK')
660
- ) {
661
- setStatus('INCORRECT');
662
- return;
663
- }
796
+ // Track detected document type for UI feedback
797
+ if (documentType !== 'UNKNOWN') {
798
+ setDetectedDocumentType(documentType);
799
+ }
800
+
801
+ // Detect wrong side based on document type or face presence (works for both normal and eID scan)
802
+ // For ID_BACK step: if faces are detected, it's likely the front side (wrong)
803
+ // For FRONT step: if ID_BACK is detected, it's the wrong side
804
+ const isWrongSide =
805
+ (nextStep === 'SCAN_ID_FRONT_OR_PASSPORT' && documentType === 'ID_BACK') ||
806
+ (nextStep === 'SCAN_ID_BACK' && (documentType === 'ID_FRONT' || documentType === 'PASSPORT' || croppedFaces.length > 0));
807
+
808
+ if (isWrongSide) {
809
+ setStatus('INCORRECT');
810
+ return;
811
+ }
664
812
 
813
+ if (!onlyMRZScan) {
665
814
  if (croppedFaces.length > 0 && croppedFaces[0]) {
666
815
  if (currentFaceImage) {
667
816
  scannedData.faceImage = currentFaceImage;
@@ -711,22 +860,22 @@ const IdentityDocumentCamera = ({
711
860
  if (nextStep === 'SCAN_ID_FRONT_OR_PASSPORT') {
712
861
  setStatus('SCANNED');
713
862
  if (onlyMRZScan) {
714
- setNextStepAndVibrate('SCAN_ID_BACK');
863
+ setNextStepAndVibrate('SCAN_ID_BACK', 'SCAN_ID_FRONT_OR_PASSPORT');
715
864
  onIdentityDocumentScanned(scannedData);
716
865
  } else {
717
- setNextStepAndVibrate('SCAN_HOLOGRAM');
866
+ setNextStepAndVibrate('SCAN_HOLOGRAM', 'SCAN_ID_FRONT_OR_PASSPORT');
718
867
  }
719
868
  } else if (
720
869
  nextStep === 'SCAN_HOLOGRAM' &&
721
870
  (!!scannedData.hologramImage ||
722
871
  hologramDetectionCurrentRetryCount.value >=
723
- HOLOGRAM_DETECTION_RETRY_COUNT) &&
872
+ HOLOGRAM_DETECTION_RETRY_COUNT) &&
724
873
  (!!scannedData.secondaryFaceImage ||
725
874
  secondaryFaceDetectionCurrentRetryCount.value >=
726
- SECOND_FACE_DETECTION_RETRY_COUNT)
875
+ SECOND_FACE_DETECTION_RETRY_COUNT)
727
876
  ) {
728
877
  setStatus('SCANNED');
729
- setNextStepAndVibrate('SCAN_ID_BACK');
878
+ setNextStepAndVibrate('SCAN_ID_BACK', 'SCAN_HOLOGRAM');
730
879
  onIdentityDocumentScanned(scannedData);
731
880
  }
732
881
  } else if (documentType === 'PASSPORT') {
@@ -734,28 +883,41 @@ const IdentityDocumentCamera = ({
734
883
  nextStep === 'SCAN_ID_FRONT_OR_PASSPORT' &&
735
884
  !scannedData.hologramImage
736
885
  ) {
737
- setStatus('SCANNED');
886
+ // For passport, require valid MRZ before proceeding
738
887
  if (onlyMRZScan) {
739
- setNextStepAndVibrate('COMPLETED');
740
- onIdentityDocumentScanned(scannedData);
888
+ // eID scan: require valid MRZ
889
+ if (
890
+ !!scannedData.mrzText &&
891
+ (parsedMRZData?.valid ||
892
+ mrzDetectionCurrentRetryCount.value >= MRZ_VALIDATION_RETRY_COUNT)
893
+ ) {
894
+ setStatus('SCANNED');
895
+ setNextStepAndVibrate('COMPLETED', 'SCAN_ID_FRONT_OR_PASSPORT');
896
+ onIdentityDocumentScanned(scannedData);
897
+ } else if (!parsedMRZData?.valid) {
898
+ mrzDetectionCurrentRetryCount.value++;
899
+ setStatus('SCANNING');
900
+ }
741
901
  } else {
742
- setNextStepAndVibrate('SCAN_HOLOGRAM');
902
+ // Normal scan: proceed to hologram check (MRZ validated later)
903
+ setStatus('SCANNED');
904
+ setNextStepAndVibrate('SCAN_HOLOGRAM', 'SCAN_ID_FRONT_OR_PASSPORT');
743
905
  }
744
906
  } else if (
745
907
  ((nextStep === 'SCAN_HOLOGRAM' &&
746
908
  (!!scannedData.hologramImage ||
747
909
  hologramDetectionCurrentRetryCount.value >=
748
- HOLOGRAM_DETECTION_RETRY_COUNT) &&
910
+ HOLOGRAM_DETECTION_RETRY_COUNT) &&
749
911
  (!!scannedData.secondaryFaceImage ||
750
912
  secondaryFaceDetectionCurrentRetryCount.value >=
751
- SECOND_FACE_DETECTION_RETRY_COUNT)) ||
913
+ SECOND_FACE_DETECTION_RETRY_COUNT)) ||
752
914
  onlyMRZScan) &&
753
915
  !!scannedData.mrzText &&
754
916
  (parsedMRZData?.valid ||
755
917
  mrzDetectionCurrentRetryCount.value >= MRZ_VALIDATION_RETRY_COUNT)
756
918
  ) {
757
919
  setStatus('SCANNED');
758
- setNextStepAndVibrate('COMPLETED');
920
+ setNextStepAndVibrate('COMPLETED', 'SCAN_HOLOGRAM');
759
921
  onIdentityDocumentScanned(scannedData);
760
922
  } else if (!parsedMRZData?.valid) {
761
923
  mrzDetectionCurrentRetryCount.value++;
@@ -764,7 +926,7 @@ const IdentityDocumentCamera = ({
764
926
  if (
765
927
  ((parsedMRZData?.fields?.issuingState === 'TUR' &&
766
928
  barcode?.value?.trim() ===
767
- parsedMRZData?.fields?.optional1?.trim()) ||
929
+ parsedMRZData?.fields?.optional1?.trim()) ||
768
930
  parsedMRZData?.fields?.issuingState !== 'TUR' ||
769
931
  onlyMRZScan) &&
770
932
  nextStep === 'SCAN_ID_BACK' &&
@@ -774,7 +936,7 @@ const IdentityDocumentCamera = ({
774
936
  ) {
775
937
  scannedData.barcodeValue = barcode?.value ?? undefined;
776
938
  setStatus('SCANNED');
777
- setNextStepAndVibrate('COMPLETED');
939
+ setNextStepAndVibrate('COMPLETED', 'SCAN_ID_BACK');
778
940
  onIdentityDocumentScanned(scannedData);
779
941
  } else if (!parsedMRZData?.valid) {
780
942
  mrzDetectionCurrentRetryCount.value++;
@@ -808,25 +970,63 @@ const IdentityDocumentCamera = ({
808
970
  [exposure]
809
971
  );
810
972
 
973
+ // Focus trigger for when blur is detected (called from worklet)
974
+ const triggerFocus = useRunOnJS(
975
+ async () => {
976
+ if (!cameraRef.current || !device?.supportsFocus) {
977
+ return;
978
+ }
979
+ try {
980
+ const width = format?.videoWidth ?? 1920;
981
+ const height = format?.videoHeight ?? 1080;
982
+ const centerPoint = getScanAreaCenterPoint(width, height);
983
+ await cameraRef.current.focus({
984
+ x: centerPoint.x,
985
+ y: centerPoint.y,
986
+ });
987
+ } catch (error) {
988
+ // Ignore focus errors
989
+ }
990
+ },
991
+ [device, format]
992
+ );
993
+
811
994
  const handleExposureAndBrightness = (frame: Frame) => {
812
995
  'worklet';
813
996
  const averageBrightness = getAverageBrightness(frame);
814
997
  const minExposure = device?.minExposure ?? 0;
815
998
  const maxExposure = device?.maxExposure ?? 0;
816
- const lowerBrightnessBound = 40;
817
- const upperBrightnessBound = 120;
999
+
1000
+ // Dynamic thresholds based on scanning state using config values
1001
+ // Face detection requires higher minimum brightness for reliable detection
1002
+ const isFrontOrPassport = nextStep === 'SCAN_ID_FRONT_OR_PASSPORT';
1003
+ const isBack = nextStep === 'SCAN_ID_BACK';
1004
+
1005
+ // Using config values: faceDetection { low: 50, high: 110, target: 85 }, mrzScanning { low: 45, high: 130, target: 80 }
1006
+ const lowerBrightnessBound = isFrontOrPassport ? 50 : 40;
1007
+ const upperBrightnessBound = isBack ? 130 : 120;
1008
+ const targetBrightness = isFrontOrPassport ? 85 : 80;
1009
+
1010
+ // Smooth exposure adjustment with hysteresis to prevent oscillation
1011
+ // Only adjust if brightness is significantly outside the acceptable range
1012
+ const hysteresis = 5; // Dead zone to prevent jitter
818
1013
 
819
1014
  if (
820
- averageBrightness < lowerBrightnessBound &&
1015
+ averageBrightness < (lowerBrightnessBound - hysteresis) &&
821
1016
  exposureValue.value < maxExposure
822
1017
  ) {
823
- exposureValue.value = exposureValue.value + 1;
1018
+ // Increase exposure smoothly when too dark
1019
+ const step = calculateExposureStep(averageBrightness, targetBrightness);
1020
+ exposureValue.value = Math.min(maxExposure, exposureValue.value + step);
824
1021
  } else if (
825
- averageBrightness > upperBrightnessBound &&
1022
+ averageBrightness > (upperBrightnessBound + hysteresis) &&
826
1023
  exposureValue.value > minExposure
827
1024
  ) {
828
- exposureValue.value = exposureValue.value - 1;
1025
+ // Decrease exposure smoothly when too bright
1026
+ const step = calculateExposureStep(averageBrightness, targetBrightness);
1027
+ exposureValue.value = Math.max(minExposure, exposureValue.value - step);
829
1028
  }
1029
+ // When within acceptable range (with hysteresis), don't adjust - prevents oscillation
830
1030
 
831
1031
  const isBright = averageBrightness > lowerBrightnessBound;
832
1032
  handleExposureValue(exposureValue.value);
@@ -843,6 +1043,28 @@ const IdentityDocumentCamera = ({
843
1043
  return;
844
1044
  }
845
1045
 
1046
+
1047
+
1048
+ // Check for blur before processing - skip blurry frames
1049
+ // Use different thresholds: 25 for front (face detection), 30 for back (MRZ/text)
1050
+ // Higher thresholds with improved Laplacian algorithm using H+V gradients
1051
+ const isFront = nextStep === 'SCAN_ID_FRONT_OR_PASSPORT';
1052
+ const blurThreshold = isFront ? 25 : 30;
1053
+ const blurry = checkBlurry(frame, blurThreshold);
1054
+ handleBlurStatus(blurry);
1055
+ if (blurry) {
1056
+ consecutiveBlurCount.value++;
1057
+ // Only trigger focus after 2 consecutive blurry frames (matching Flutter)
1058
+ if (consecutiveBlurCount.value >= 2) {
1059
+ triggerFocus();
1060
+ consecutiveBlurCount.value = 0;
1061
+ }
1062
+ return;
1063
+ }
1064
+ // Reset blur count on sharp frame
1065
+ consecutiveBlurCount.value = 0;
1066
+
1067
+
846
1068
  // Validate frame dimensions before processing
847
1069
  if (
848
1070
  !frame.width ||
@@ -901,6 +1123,8 @@ const IdentityDocumentCamera = ({
901
1123
  }
902
1124
 
903
1125
  // Text recognition with error handling
1126
+ // Note: CLAHE enhancement is applied to captured images, not live frames
1127
+ // ML Kit plugins work directly on Frame objects and don't support Mat input
904
1128
  let scannedText: BlockText;
905
1129
  try {
906
1130
  scannedText = scanText(frame) as any as BlockText;
@@ -972,15 +1196,15 @@ const IdentityDocumentCamera = ({
972
1196
 
973
1197
  if (!permissionsRequested) {
974
1198
  return (
975
- <View style={styles.permissionContainer}>
1199
+ <SafeAreaView style={styles.permissionContainer}>
976
1200
  <ActivityIndicator size="large" color={theme.colors.primary} />
977
- </View>
1201
+ </SafeAreaView>
978
1202
  );
979
1203
  }
980
1204
 
981
1205
  if (!cameraPermission.hasPermission) {
982
1206
  return (
983
- <View style={styles.permissionContainer}>
1207
+ <SafeAreaView style={styles.permissionContainer}>
984
1208
  <Text style={styles.permissionText}>
985
1209
  {t('general.noCameraPermissionGiven')}
986
1210
  </Text>
@@ -992,17 +1216,17 @@ const IdentityDocumentCamera = ({
992
1216
  >
993
1217
  {t('general.openSettings')}
994
1218
  </StyledButton>
995
- </View>
1219
+ </SafeAreaView>
996
1220
  );
997
1221
  }
998
1222
 
999
1223
  if (device == null) {
1000
1224
  return (
1001
- <View style={styles.permissionContainer}>
1225
+ <SafeAreaView style={styles.permissionContainer}>
1002
1226
  <TextView style={styles.permissionText}>
1003
1227
  {t('general.noCameraDetected')}
1004
1228
  </TextView>
1005
- </View>
1229
+ </SafeAreaView>
1006
1230
  );
1007
1231
  }
1008
1232
 
@@ -1023,7 +1247,7 @@ const IdentityDocumentCamera = ({
1023
1247
  return (
1024
1248
  <View style={StyleSheet.absoluteFill}>
1025
1249
  {!hasGuideShown ? (
1026
- <View style={styles.guide}>
1250
+ <SafeAreaView style={styles.guide}>
1027
1251
  <LottieView
1028
1252
  source={require('../../Shared/Animations/id-or-passport.json')}
1029
1253
  style={styles.guideAnimation}
@@ -1055,7 +1279,7 @@ const IdentityDocumentCamera = ({
1055
1279
  >
1056
1280
  {t('general.letsGo')}
1057
1281
  </StyledButton>
1058
- </View>
1282
+ </SafeAreaView>
1059
1283
  ) : (
1060
1284
  <>
1061
1285
  <Camera
@@ -1075,19 +1299,74 @@ const IdentityDocumentCamera = ({
1075
1299
  isCameraInitialized.value = true;
1076
1300
  }}
1077
1301
  />
1078
- <View style={styles.topZone}>
1079
- <TextView style={styles.topZoneText}>
1080
- {isBrightnessLow
1081
- ? t('identityDocumentCamera.lowBrightness')
1082
- : nextStep === 'SCAN_ID_FRONT_OR_PASSPORT'
1083
- ? t('identityDocumentCamera.alignPhotoSide')
1302
+ <View style={[styles.topZone, { paddingTop: insets.top }]}>
1303
+ {/* Step Progress Indicator - show only after document type detected and not completed/scanned */}
1304
+ {nextStep !== 'COMPLETED' && status !== 'SCANNED' && detectedDocumentType !== 'UNKNOWN' && (
1305
+ <TextView style={styles.stepIndicator}>
1306
+ {nextStep === 'SCAN_ID_FRONT_OR_PASSPORT'
1307
+ ? `${t('identityDocumentCamera.frontSide')} • ${t('identityDocumentCamera.stepProgress', {
1308
+ current: 1,
1309
+ total: onlyMRZScan
1310
+ ? (detectedDocumentType === 'PASSPORT' ? 1 : 2)
1311
+ : (detectedDocumentType === 'PASSPORT' ? 2 : 3)
1312
+ })}`
1084
1313
  : nextStep === 'SCAN_HOLOGRAM'
1085
- ? t('identityDocumentCamera.alignHologram')
1314
+ ? `${t('identityDocumentCamera.hologramCheck')} • ${t('identityDocumentCamera.stepProgress', {
1315
+ current: 2,
1316
+ total: detectedDocumentType === 'PASSPORT' ? 2 : 3
1317
+ })}`
1086
1318
  : nextStep === 'SCAN_ID_BACK'
1087
- ? t('identityDocumentCamera.alignIDBackSide')
1088
- : nextStep === 'COMPLETED'
1089
- ? t('identityDocumentCamera.scanCompleted')
1090
- : ''}
1319
+ ? `${t('identityDocumentCamera.backSide')} • ${t('identityDocumentCamera.stepProgress', { current: 3, total: 3 })}`
1320
+ : ''}
1321
+ </TextView>
1322
+ )}
1323
+ {/* Status-based guidance text */}
1324
+ <TextView style={[
1325
+ styles.topZoneText,
1326
+ status === 'SCANNING' && styles.topZoneTextScanning,
1327
+ status === 'SCANNED' && styles.topZoneTextSuccess,
1328
+ status === 'INCORRECT' && styles.topZoneTextError,
1329
+ (isBrightnessLow || isFrameBlurry) && styles.topZoneTextWarning,
1330
+ ]}>
1331
+ {status === 'SCANNED'
1332
+ ? completedStep === 'SCAN_ID_FRONT_OR_PASSPORT'
1333
+ ? detectedDocumentType === 'PASSPORT'
1334
+ ? t('identityDocumentCamera.passportScanned')
1335
+ : t('identityDocumentCamera.frontSideScanned')
1336
+ : completedStep === 'SCAN_ID_BACK'
1337
+ ? t('identityDocumentCamera.backSideScanned')
1338
+ : completedStep === 'SCAN_HOLOGRAM'
1339
+ ? t('identityDocumentCamera.hologramVerified')
1340
+ : t('identityDocumentCamera.scanCompleted')
1341
+ : status === 'INCORRECT'
1342
+ ? nextStep === 'SCAN_ID_FRONT_OR_PASSPORT'
1343
+ ? t('identityDocumentCamera.wrongSideFront')
1344
+ : nextStep === 'SCAN_ID_BACK'
1345
+ ? t('identityDocumentCamera.wrongSideBack')
1346
+ : t('identityDocumentCamera.alignPhotoSide')
1347
+ : isBrightnessLow
1348
+ ? t('identityDocumentCamera.lowBrightness')
1349
+ : isFrameBlurry
1350
+ ? t('identityDocumentCamera.avoidBlur')
1351
+ : nextStep === 'SCAN_ID_FRONT_OR_PASSPORT'
1352
+ ? status === 'SCANNING'
1353
+ ? currentFaceImage
1354
+ ? detectedDocumentType === 'PASSPORT'
1355
+ ? t('identityDocumentCamera.passportDetected')
1356
+ : detectedDocumentType === 'ID_FRONT'
1357
+ ? t('identityDocumentCamera.idCardFrontDetected')
1358
+ : t('identityDocumentCamera.readingDocument')
1359
+ : t('identityDocumentCamera.readingDocument')
1360
+ : t('identityDocumentCamera.alignPhotoSide')
1361
+ : nextStep === 'SCAN_HOLOGRAM'
1362
+ ? t('identityDocumentCamera.alignHologram')
1363
+ : nextStep === 'SCAN_ID_BACK'
1364
+ ? status === 'SCANNING'
1365
+ ? t('identityDocumentCamera.readingDocument')
1366
+ : t('identityDocumentCamera.alignIDBackSide')
1367
+ : nextStep === 'COMPLETED'
1368
+ ? t('identityDocumentCamera.scanCompleted')
1369
+ : ''}
1091
1370
  </TextView>
1092
1371
  </View>
1093
1372
  <View style={styles.leftZone} />
@@ -1154,19 +1433,16 @@ const IdentityDocumentCamera = ({
1154
1433
  Status: {status}
1155
1434
  </TextView>
1156
1435
  <TextView style={styles.debugInfoText}>
1157
- {`Face: ${currentFaceImage ? '✓' : '✗'} ${
1158
- !faceDetectionEnabled ? '(DISABLED)' : ''
1159
- }`}
1436
+ {`Face: ${currentFaceImage ? '✓' : '✗'} ${!faceDetectionEnabled ? '(DISABLED)' : ''
1437
+ }`}
1160
1438
  </TextView>
1161
1439
  <TextView style={styles.debugInfoText}>
1162
- {`Hologram: ${currentHologramImage ? '✓' : '✗'} (${
1163
- hologramDetectionCurrentRetryCount.value
1164
- }/${HOLOGRAM_DETECTION_RETRY_COUNT})`}
1440
+ {`Hologram: ${currentHologramImage ? '✓' : '✗'} (${hologramDetectionCurrentRetryCount.value
1441
+ }/${HOLOGRAM_DETECTION_RETRY_COUNT})`}
1165
1442
  </TextView>
1166
1443
  <TextView style={styles.debugInfoText}>
1167
- {`2nd Face: ${currentSecondaryFaceImage ? '✓' : '✗'} (${
1168
- secondaryFaceDetectionCurrentRetryCount.value
1169
- }/${SECOND_FACE_DETECTION_RETRY_COUNT})`}
1444
+ {`2nd Face: ${currentSecondaryFaceImage ? '✓' : '✗'} (${secondaryFaceDetectionCurrentRetryCount.value
1445
+ }/${SECOND_FACE_DETECTION_RETRY_COUNT})`}
1170
1446
  </TextView>
1171
1447
  </View>
1172
1448
  )}
@@ -1177,51 +1453,54 @@ const IdentityDocumentCamera = ({
1177
1453
  {
1178
1454
  borderColor:
1179
1455
  status === 'SCANNED' || nextStep === 'COMPLETED'
1180
- ? 'green'
1456
+ ? '#4CAF50' // Green - success
1181
1457
  : status === 'INCORRECT'
1182
- ? 'red'
1183
- : 'white',
1458
+ ? '#f44336' // Red - error
1459
+ : status === 'SCANNING'
1460
+ ? '#2196F3' // Blue - processing
1461
+ : isBrightnessLow || isFrameBlurry
1462
+ ? '#FFC107' // Yellow - warning
1463
+ : 'white',
1464
+ borderWidth: status === 'SCANNING' ? 3 : 2,
1184
1465
  },
1185
1466
  ]}
1186
1467
  >
1187
- {nextStep === 'SCAN_HOLOGRAM' && isTorchOn && !isBrightnessLow && (
1468
+ {/* Only show ONE animation at a time - priority order: completed/scanned > brightness > hologram > scanning */}
1469
+ {nextStep === 'COMPLETED' || status === 'SCANNED' ? (
1188
1470
  <LottieView
1189
- source={require('../../Shared/Animations/hologram-scan.json')}
1471
+ source={require('../../Shared/Animations/success.json')}
1190
1472
  style={styles.animation}
1191
- loop={true}
1473
+ loop={false}
1192
1474
  autoPlay
1193
1475
  />
1194
- )}
1195
- {nextStep !== 'COMPLETED' && isBrightnessLow && (
1476
+ ) : isBrightnessLow ? (
1196
1477
  <LottieView
1197
1478
  source={require('../../Shared/Animations/light.json')}
1198
1479
  style={styles.animation}
1199
1480
  loop={true}
1200
1481
  autoPlay
1201
1482
  />
1202
- )}
1203
- {nextStep === 'COMPLETED' && (
1483
+ ) : nextStep === 'SCAN_HOLOGRAM' && isTorchOn ? (
1204
1484
  <LottieView
1205
- source={require('../../Shared/Animations/success.json')}
1485
+ source={require('../../Shared/Animations/hologram-scan.json')}
1206
1486
  style={styles.animation}
1207
- loop={false}
1487
+ loop={true}
1208
1488
  autoPlay
1209
1489
  />
1210
- )}
1490
+ ) : status === 'SCANNING' ? (
1491
+ <LottieView
1492
+ source={require('../../Shared/Animations/scanning.json')}
1493
+ style={styles.animation}
1494
+ loop={true}
1495
+ autoPlay
1496
+ />
1497
+ ) : null}
1211
1498
  </View>
1212
- {/* <View
1213
- style={{
1214
- borderColor: 'red',
1215
- borderWidth: 1,
1216
- position: 'absolute',
1217
- top: boundingBox.y,
1218
- left: boundingBox.x,
1219
- width: boundingBox.width,
1220
- height: boundingBox.height,
1221
- zIndex: 3,
1222
- }}
1223
- /> */}
1224
- <TouchableOpacity onPress={handleFocus} style={styles.focusArea} />
1499
+ <TouchableOpacity
1500
+ onPress={handleFocus}
1501
+ style={styles.focusArea}
1502
+ activeOpacity={1}
1503
+ />
1225
1504
  </>
1226
1505
  )}
1227
1506
  </View>
@@ -1281,6 +1560,13 @@ const styles = StyleSheet.create({
1281
1560
  justifyContent: 'flex-end',
1282
1561
  alignItems: 'center',
1283
1562
  },
1563
+ stepIndicator: {
1564
+ color: '#aaaaaa',
1565
+ fontSize: 14,
1566
+ textAlign: 'center',
1567
+ fontWeight: '500',
1568
+ marginBottom: 4,
1569
+ },
1284
1570
  topZoneText: {
1285
1571
  color: 'white',
1286
1572
  fontSize: 20,
@@ -1288,6 +1574,18 @@ const styles = StyleSheet.create({
1288
1574
  fontWeight: 'bold',
1289
1575
  padding: 20,
1290
1576
  },
1577
+ topZoneTextScanning: {
1578
+ color: '#2196F3', // Blue when scanning
1579
+ },
1580
+ topZoneTextSuccess: {
1581
+ color: '#4CAF50', // Green for success
1582
+ },
1583
+ topZoneTextWarning: {
1584
+ color: '#FFC107', // Yellow for warnings
1585
+ },
1586
+ topZoneTextError: {
1587
+ color: '#f44336', // Red for errors
1588
+ },
1291
1589
  leftZone: {
1292
1590
  position: 'absolute',
1293
1591
  top: '36%',