@trustchex/react-native-sdk 1.267.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
@@ -3,6 +3,7 @@
3
3
  /* eslint-disable react-native/no-inline-styles */
4
4
  import React, { useEffect, useState } from 'react';
5
5
  import { View, StyleSheet, Text as TextView, Platform, Vibration, TouchableOpacity, Text, Linking, Image, ActivityIndicator } from 'react-native';
6
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
6
7
  import { Camera, runAtTargetFps, useCameraDevice, useCameraFormat, useCameraPermission, useFrameProcessor } from 'react-native-vision-camera';
7
8
  import { runAsync } from "../Libs/worklet.utils.js";
8
9
  import { useRunOnJS, useSharedValue } from 'react-native-worklets-core';
@@ -14,10 +15,11 @@ import { useKeepAwake } from "../Libs/native-keep-awake.utils.js";
14
15
  import ImageEditor from '@react-native-community/image-editor';
15
16
  import { useIsFocused } from '@react-navigation/native';
16
17
  import { AdaptiveThresholdTypes, ColorConversionCodes, DataTypes, ObjectType, OpenCV, ThresholdTypes } from 'react-native-fast-opencv';
17
- import { getAverageBrightness } from "../Libs/camera.utils.js";
18
+ import { getAverageBrightness, getScanAreaCenterPoint, calculateExposureStep, isBlurry as checkBlurry } from "../Libs/camera.utils.js";
18
19
  import { useTranslation } from 'react-i18next';
19
20
  import LottieView from 'lottie-react-native';
20
21
  import StyledButton from "./StyledButton.js";
22
+ import { SafeAreaView } from 'react-native-safe-area-context';
21
23
  import { scanCodes } from "../VisionCameraPlugins/BarcodeScanner/index.js";
22
24
  import { speakWithDebounce } from "../Libs/tts.utils.js";
23
25
  import AppContext from "../Contexts/AppContext.js";
@@ -39,6 +41,7 @@ const IdentityDocumentCamera = ({
39
41
  }) => {
40
42
  useKeepAwake();
41
43
  const theme = useTheme();
44
+ const insets = useSafeAreaInsets();
42
45
  const appContext = React.useContext(AppContext);
43
46
  const cameraRef = React.useRef(null);
44
47
  const cameraPermission = useCameraPermission();
@@ -68,13 +71,17 @@ const IdentityDocumentCamera = ({
68
71
  const [currentHologramImage, setCurrentHologramImage] = useState(undefined);
69
72
  const [currentSecondaryFaceImage, setCurrentSecondaryFaceImage] = useState(undefined);
70
73
  const [isBrightnessLow, setIsBrightnessLow] = useState(false);
74
+ const [isFrameBlurry, setIsFrameBlurry] = useState(false);
71
75
  const [hasGuideShown, setHasGuideShown] = useState(false);
72
76
  const [status, setStatus] = useState('SEARCHING');
73
77
  const [nextStep, setNextStep] = useState('SCAN_ID_FRONT_OR_PASSPORT');
78
+ const [completedStep, setCompletedStep] = useState(null);
79
+ const [detectedDocumentType, setDetectedDocumentType] = useState('UNKNOWN');
74
80
  const hologramDetectionCurrentRetryCount = useSharedValue(0);
75
81
  const secondaryFaceDetectionCurrentRetryCount = useSharedValue(0);
76
82
  const mrzDetectionCurrentRetryCount = useSharedValue(0);
77
83
  const faceDetectionErrorCount = useSharedValue(0);
84
+ const consecutiveBlurCount = useSharedValue(0);
78
85
  const [faceDetectionEnabled, setFaceDetectionEnabled] = useState(true);
79
86
  const {
80
87
  t
@@ -146,12 +153,109 @@ const IdentityDocumentCamera = ({
146
153
  }, [device, format, isFocused, hologramDetectionCurrentRetryCount, secondaryFaceDetectionCurrentRetryCount, mrzDetectionCurrentRetryCount, faceDetectionErrorCount]);
147
154
  useEffect(() => {
148
155
  if (hasGuideShown) {
149
- const message = isBrightnessLow ? t('identityDocumentCamera.lowBrightness') : nextStep === 'SCAN_ID_FRONT_OR_PASSPORT' ? t('identityDocumentCamera.alignPhotoSide') : nextStep === 'SCAN_HOLOGRAM' ? t('identityDocumentCamera.alignHologram') : nextStep === 'SCAN_ID_BACK' ? t('identityDocumentCamera.alignIDBackSide') : nextStep === 'COMPLETED' ? t('identityDocumentCamera.scanCompleted') : '';
156
+ let message = '';
157
+
158
+ // Priority: scanned > incorrect > blur during scanning > brightness > blur > step-specific
159
+ if (status === 'SCANNED') {
160
+ // Use step-specific completion messages
161
+ if (completedStep === 'SCAN_ID_FRONT_OR_PASSPORT') {
162
+ message = detectedDocumentType === 'PASSPORT' ? t('identityDocumentCamera.passportScanned') : t('identityDocumentCamera.frontSideScanned');
163
+ } else if (completedStep === 'SCAN_ID_BACK') {
164
+ message = t('identityDocumentCamera.backSideScanned');
165
+ } else if (completedStep === 'SCAN_HOLOGRAM') {
166
+ message = t('identityDocumentCamera.hologramVerified');
167
+ } else {
168
+ message = t('identityDocumentCamera.scanCompleted');
169
+ }
170
+ } else if (status === 'INCORRECT') {
171
+ // Wrong side detected - warn user
172
+ if (nextStep === 'SCAN_ID_FRONT_OR_PASSPORT') {
173
+ message = t('identityDocumentCamera.wrongSideFront');
174
+ } else if (nextStep === 'SCAN_ID_BACK') {
175
+ message = t('identityDocumentCamera.wrongSideBack');
176
+ }
177
+ } else if (isBrightnessLow) {
178
+ // Brightness warning takes priority over blur
179
+ message = t('identityDocumentCamera.lowBrightness');
180
+ } else if (isFrameBlurry) {
181
+ // Show blur warning only when brightness is sufficient
182
+ message = t('identityDocumentCamera.avoidBlur');
183
+ } else if (nextStep === 'SCAN_ID_FRONT_OR_PASSPORT') {
184
+ // Enhanced feedback based on detection status
185
+ if (status === 'SCANNING') {
186
+ if (currentFaceImage) {
187
+ // Document-specific detection message
188
+ if (detectedDocumentType === 'PASSPORT') {
189
+ message = t('identityDocumentCamera.passportDetected');
190
+ } else if (detectedDocumentType === 'ID_FRONT') {
191
+ message = t('identityDocumentCamera.idCardFrontDetected');
192
+ } else {
193
+ message = t('identityDocumentCamera.readingDocument');
194
+ }
195
+ } else {
196
+ message = t('identityDocumentCamera.readingDocument');
197
+ }
198
+ } else {
199
+ message = t('identityDocumentCamera.alignPhotoSide');
200
+ }
201
+ } else if (nextStep === 'SCAN_HOLOGRAM') {
202
+ message = t('identityDocumentCamera.alignHologram');
203
+ } else if (nextStep === 'SCAN_ID_BACK') {
204
+ if (status === 'SCANNING') {
205
+ message = t('identityDocumentCamera.readingDocument');
206
+ } else {
207
+ message = t('identityDocumentCamera.alignIDBackSide');
208
+ }
209
+ } else if (nextStep === 'COMPLETED') {
210
+ message = t('identityDocumentCamera.scanCompleted');
211
+ }
150
212
  if (appContext.currentWorkflowStep?.data?.voiceGuidanceActive && message) {
151
213
  speakWithDebounce(message);
152
214
  }
153
215
  }
154
- }, [appContext.currentWorkflowStep?.data?.voiceGuidanceActive, hasGuideShown, isBrightnessLow, nextStep, t]);
216
+ }, [appContext.currentWorkflowStep?.data?.voiceGuidanceActive, hasGuideShown, isBrightnessLow, isFrameBlurry, nextStep, status, completedStep, currentFaceImage, detectedDocumentType, t]);
217
+
218
+ // Auto-reset INCORRECT status after showing warning briefly
219
+ useEffect(() => {
220
+ if (status === 'INCORRECT') {
221
+ const timeout = setTimeout(() => {
222
+ setStatus('SEARCHING');
223
+ }, 1500); // Show warning for 1.5 seconds
224
+ return () => clearTimeout(timeout);
225
+ }
226
+ }, [status]);
227
+
228
+ // Periodic autofocus - refocus on scan area center every 2.5 seconds
229
+ useEffect(() => {
230
+ if (!isActive || !device || !cameraRef.current || !device.supportsFocus) {
231
+ return;
232
+ }
233
+
234
+ // Only autofocus during searching and scanning states
235
+ if (status !== 'SEARCHING' && status !== 'SCANNING') {
236
+ return;
237
+ }
238
+ const autofocusInterval = setInterval(async () => {
239
+ try {
240
+ // Get camera dimensions (assuming format dimensions)
241
+ const width = format?.videoWidth ?? 1920;
242
+ const height = format?.videoHeight ?? 1080;
243
+
244
+ // Calculate center point of scan area
245
+ const centerPoint = getScanAreaCenterPoint(width, height);
246
+
247
+ // Focus on the center of the scan area
248
+ await cameraRef.current?.focus({
249
+ x: centerPoint.x,
250
+ y: centerPoint.y
251
+ });
252
+ } catch (error) {
253
+ // Ignore autofocus errors
254
+ }
255
+ }, 2500); // Every 2.5 seconds
256
+
257
+ return () => clearInterval(autofocusInterval);
258
+ }, [isActive, device, format, status]);
155
259
  const detectDocumentType = (faces, ocrText, mrzFields) => {
156
260
  if (faces.length > 0 && !mrzFields && ocrText?.includes('Signature')
157
261
  // ocrText?.includes('Surname') &&
@@ -357,13 +461,28 @@ const IdentityDocumentCamera = ({
357
461
  }
358
462
  return croppedFaces;
359
463
  };
360
- const setNextStepAndVibrate = nextStepType => {
464
+ const setNextStepAndVibrate = (nextStepType, fromStep) => {
465
+ // Track which step was just completed for showing specific message
466
+ if (fromStep) {
467
+ setCompletedStep(fromStep);
468
+ }
361
469
  setNextStep(nextStepType);
362
470
  Vibration.vibrate(100);
471
+
472
+ // Reset status after delay to show success animation fully before next step
473
+ if (nextStepType !== 'COMPLETED') {
474
+ setTimeout(() => {
475
+ setStatus('SEARCHING');
476
+ setCompletedStep(null);
477
+ }, 2000); // Show success checkmark for 2 seconds before transitioning
478
+ }
363
479
  };
364
480
  const handleBrightness = useRunOnJS(isBright => {
365
481
  setIsBrightnessLow(!isBright);
366
482
  }, [setIsBrightnessLow]);
483
+ const handleBlurStatus = useRunOnJS(blurry => {
484
+ setIsFrameBlurry(blurry);
485
+ }, [setIsFrameBlurry]);
367
486
  const handleFaceAndText = useRunOnJS(async (text, faces, frameWidth, frameHeight, barcode, image) => {
368
487
  if (device?.hasTorch && isTorchOn && (currentHologramImage || hologramDetectionCurrentRetryCount.value >= HOLOGRAM_DETECTION_RETRY_COUNT)) {
369
488
  setIsTorchOn(false);
@@ -372,7 +491,18 @@ const IdentityDocumentCamera = ({
372
491
  setStatus('SCANNED');
373
492
  return;
374
493
  }
494
+
495
+ // Early wrong side detection for SCAN_ID_BACK: if faces detected, it's the front side
496
+ if (nextStep === 'SCAN_ID_BACK' && faces.length > 0) {
497
+ console.log('[WRONG_SIDE] Back side expected but faces detected:', faces.length);
498
+ setStatus('INCORRECT');
499
+ return;
500
+ }
375
501
  if (!text || text.length < 10 || !image) {
502
+ // Log when searching to help debug
503
+ if (nextStep === 'SCAN_ID_BACK') {
504
+ console.log('[SCAN_ID_BACK] Searching... faces:', faces.length, 'text length:', text?.length || 0);
505
+ }
376
506
  setStatus('SEARCHING');
377
507
  return;
378
508
  }
@@ -390,11 +520,21 @@ const IdentityDocumentCamera = ({
390
520
  };
391
521
  scannedData.faceImage = croppedFaces[0];
392
522
  setCurrentFaceImage(croppedFaces[0]);
523
+
524
+ // Track detected document type for UI feedback
525
+ if (documentType !== 'UNKNOWN') {
526
+ setDetectedDocumentType(documentType);
527
+ }
528
+
529
+ // Detect wrong side based on document type or face presence (works for both normal and eID scan)
530
+ // For ID_BACK step: if faces are detected, it's likely the front side (wrong)
531
+ // For FRONT step: if ID_BACK is detected, it's the wrong side
532
+ const isWrongSide = nextStep === 'SCAN_ID_FRONT_OR_PASSPORT' && documentType === 'ID_BACK' || nextStep === 'SCAN_ID_BACK' && (documentType === 'ID_FRONT' || documentType === 'PASSPORT' || croppedFaces.length > 0);
533
+ if (isWrongSide) {
534
+ setStatus('INCORRECT');
535
+ return;
536
+ }
393
537
  if (!onlyMRZScan) {
394
- if (nextStep === 'SCAN_ID_FRONT_OR_PASSPORT' && !(documentType === 'ID_FRONT' || documentType === 'PASSPORT') || nextStep === 'SCAN_ID_BACK' && documentType !== 'ID_BACK') {
395
- setStatus('INCORRECT');
396
- return;
397
- }
398
538
  if (croppedFaces.length > 0 && croppedFaces[0]) {
399
539
  if (currentFaceImage) {
400
540
  scannedData.faceImage = currentFaceImage;
@@ -433,28 +573,37 @@ const IdentityDocumentCamera = ({
433
573
  if (nextStep === 'SCAN_ID_FRONT_OR_PASSPORT') {
434
574
  setStatus('SCANNED');
435
575
  if (onlyMRZScan) {
436
- setNextStepAndVibrate('SCAN_ID_BACK');
576
+ setNextStepAndVibrate('SCAN_ID_BACK', 'SCAN_ID_FRONT_OR_PASSPORT');
437
577
  onIdentityDocumentScanned(scannedData);
438
578
  } else {
439
- setNextStepAndVibrate('SCAN_HOLOGRAM');
579
+ setNextStepAndVibrate('SCAN_HOLOGRAM', 'SCAN_ID_FRONT_OR_PASSPORT');
440
580
  }
441
581
  } else if (nextStep === 'SCAN_HOLOGRAM' && (!!scannedData.hologramImage || hologramDetectionCurrentRetryCount.value >= HOLOGRAM_DETECTION_RETRY_COUNT) && (!!scannedData.secondaryFaceImage || secondaryFaceDetectionCurrentRetryCount.value >= SECOND_FACE_DETECTION_RETRY_COUNT)) {
442
582
  setStatus('SCANNED');
443
- setNextStepAndVibrate('SCAN_ID_BACK');
583
+ setNextStepAndVibrate('SCAN_ID_BACK', 'SCAN_HOLOGRAM');
444
584
  onIdentityDocumentScanned(scannedData);
445
585
  }
446
586
  } else if (documentType === 'PASSPORT') {
447
587
  if (nextStep === 'SCAN_ID_FRONT_OR_PASSPORT' && !scannedData.hologramImage) {
448
- setStatus('SCANNED');
588
+ // For passport, require valid MRZ before proceeding
449
589
  if (onlyMRZScan) {
450
- setNextStepAndVibrate('COMPLETED');
451
- onIdentityDocumentScanned(scannedData);
590
+ // eID scan: require valid MRZ
591
+ if (!!scannedData.mrzText && (parsedMRZData?.valid || mrzDetectionCurrentRetryCount.value >= MRZ_VALIDATION_RETRY_COUNT)) {
592
+ setStatus('SCANNED');
593
+ setNextStepAndVibrate('COMPLETED', 'SCAN_ID_FRONT_OR_PASSPORT');
594
+ onIdentityDocumentScanned(scannedData);
595
+ } else if (!parsedMRZData?.valid) {
596
+ mrzDetectionCurrentRetryCount.value++;
597
+ setStatus('SCANNING');
598
+ }
452
599
  } else {
453
- setNextStepAndVibrate('SCAN_HOLOGRAM');
600
+ // Normal scan: proceed to hologram check (MRZ validated later)
601
+ setStatus('SCANNED');
602
+ setNextStepAndVibrate('SCAN_HOLOGRAM', 'SCAN_ID_FRONT_OR_PASSPORT');
454
603
  }
455
604
  } else if ((nextStep === 'SCAN_HOLOGRAM' && (!!scannedData.hologramImage || hologramDetectionCurrentRetryCount.value >= HOLOGRAM_DETECTION_RETRY_COUNT) && (!!scannedData.secondaryFaceImage || secondaryFaceDetectionCurrentRetryCount.value >= SECOND_FACE_DETECTION_RETRY_COUNT) || onlyMRZScan) && !!scannedData.mrzText && (parsedMRZData?.valid || mrzDetectionCurrentRetryCount.value >= MRZ_VALIDATION_RETRY_COUNT)) {
456
605
  setStatus('SCANNED');
457
- setNextStepAndVibrate('COMPLETED');
606
+ setNextStepAndVibrate('COMPLETED', 'SCAN_HOLOGRAM');
458
607
  onIdentityDocumentScanned(scannedData);
459
608
  } else if (!parsedMRZData?.valid) {
460
609
  mrzDetectionCurrentRetryCount.value++;
@@ -463,7 +612,7 @@ const IdentityDocumentCamera = ({
463
612
  if ((parsedMRZData?.fields?.issuingState === 'TUR' && barcode?.value?.trim() === parsedMRZData?.fields?.optional1?.trim() || parsedMRZData?.fields?.issuingState !== 'TUR' || onlyMRZScan) && nextStep === 'SCAN_ID_BACK' && !!scannedData.mrzText && (parsedMRZData?.valid || mrzDetectionCurrentRetryCount.value >= MRZ_VALIDATION_RETRY_COUNT)) {
464
613
  scannedData.barcodeValue = barcode?.value ?? undefined;
465
614
  setStatus('SCANNED');
466
- setNextStepAndVibrate('COMPLETED');
615
+ setNextStepAndVibrate('COMPLETED', 'SCAN_ID_BACK');
467
616
  onIdentityDocumentScanned(scannedData);
468
617
  } else if (!parsedMRZData?.valid) {
469
618
  mrzDetectionCurrentRetryCount.value++;
@@ -483,19 +632,56 @@ const IdentityDocumentCamera = ({
483
632
  const handleExposureValue = useRunOnJS(value => {
484
633
  setExposure(value);
485
634
  }, [exposure]);
635
+
636
+ // Focus trigger for when blur is detected (called from worklet)
637
+ const triggerFocus = useRunOnJS(async () => {
638
+ if (!cameraRef.current || !device?.supportsFocus) {
639
+ return;
640
+ }
641
+ try {
642
+ const width = format?.videoWidth ?? 1920;
643
+ const height = format?.videoHeight ?? 1080;
644
+ const centerPoint = getScanAreaCenterPoint(width, height);
645
+ await cameraRef.current.focus({
646
+ x: centerPoint.x,
647
+ y: centerPoint.y
648
+ });
649
+ } catch (error) {
650
+ // Ignore focus errors
651
+ }
652
+ }, [device, format]);
486
653
  const handleExposureAndBrightness = frame => {
487
654
  'worklet';
488
655
 
489
656
  const averageBrightness = getAverageBrightness(frame);
490
657
  const minExposure = device?.minExposure ?? 0;
491
658
  const maxExposure = device?.maxExposure ?? 0;
492
- const lowerBrightnessBound = 40;
493
- const upperBrightnessBound = 120;
494
- if (averageBrightness < lowerBrightnessBound && exposureValue.value < maxExposure) {
495
- exposureValue.value = exposureValue.value + 1;
496
- } else if (averageBrightness > upperBrightnessBound && exposureValue.value > minExposure) {
497
- exposureValue.value = exposureValue.value - 1;
659
+
660
+ // Dynamic thresholds based on scanning state using config values
661
+ // Face detection requires higher minimum brightness for reliable detection
662
+ const isFrontOrPassport = nextStep === 'SCAN_ID_FRONT_OR_PASSPORT';
663
+ const isBack = nextStep === 'SCAN_ID_BACK';
664
+
665
+ // Using config values: faceDetection { low: 50, high: 110, target: 85 }, mrzScanning { low: 45, high: 130, target: 80 }
666
+ const lowerBrightnessBound = isFrontOrPassport ? 50 : 40;
667
+ const upperBrightnessBound = isBack ? 130 : 120;
668
+ const targetBrightness = isFrontOrPassport ? 85 : 80;
669
+
670
+ // Smooth exposure adjustment with hysteresis to prevent oscillation
671
+ // Only adjust if brightness is significantly outside the acceptable range
672
+ const hysteresis = 5; // Dead zone to prevent jitter
673
+
674
+ if (averageBrightness < lowerBrightnessBound - hysteresis && exposureValue.value < maxExposure) {
675
+ // Increase exposure smoothly when too dark
676
+ const step = calculateExposureStep(averageBrightness, targetBrightness);
677
+ exposureValue.value = Math.min(maxExposure, exposureValue.value + step);
678
+ } else if (averageBrightness > upperBrightnessBound + hysteresis && exposureValue.value > minExposure) {
679
+ // Decrease exposure smoothly when too bright
680
+ const step = calculateExposureStep(averageBrightness, targetBrightness);
681
+ exposureValue.value = Math.max(minExposure, exposureValue.value - step);
498
682
  }
683
+ // When within acceptable range (with hysteresis), don't adjust - prevents oscillation
684
+
499
685
  const isBright = averageBrightness > lowerBrightnessBound;
500
686
  handleExposureValue(exposureValue.value);
501
687
  handleBrightness(isBright);
@@ -510,6 +696,25 @@ const IdentityDocumentCamera = ({
510
696
  return;
511
697
  }
512
698
 
699
+ // Check for blur before processing - skip blurry frames
700
+ // Use different thresholds: 25 for front (face detection), 30 for back (MRZ/text)
701
+ // Higher thresholds with improved Laplacian algorithm using H+V gradients
702
+ const isFront = nextStep === 'SCAN_ID_FRONT_OR_PASSPORT';
703
+ const blurThreshold = isFront ? 25 : 30;
704
+ const blurry = checkBlurry(frame, blurThreshold);
705
+ handleBlurStatus(blurry);
706
+ if (blurry) {
707
+ consecutiveBlurCount.value++;
708
+ // Only trigger focus after 2 consecutive blurry frames (matching Flutter)
709
+ if (consecutiveBlurCount.value >= 2) {
710
+ triggerFocus();
711
+ consecutiveBlurCount.value = 0;
712
+ }
713
+ return;
714
+ }
715
+ // Reset blur count on sharp frame
716
+ consecutiveBlurCount.value = 0;
717
+
513
718
  // Validate frame dimensions before processing
514
719
  if (!frame.width || !frame.height || frame.width <= 0 || frame.height <= 0) {
515
720
  console.warn('Invalid frame dimensions:', {
@@ -562,6 +767,8 @@ const IdentityDocumentCamera = ({
562
767
  }
563
768
 
564
769
  // Text recognition with error handling
770
+ // Note: CLAHE enhancement is applied to captured images, not live frames
771
+ // ML Kit plugins work directly on Frame objects and don't support Mat input
565
772
  let scannedText;
566
773
  try {
567
774
  scannedText = scanText(frame);
@@ -623,7 +830,7 @@ const IdentityDocumentCamera = ({
623
830
  }
624
831
  }, [handleFaceAndText, isCameraInitialized]);
625
832
  if (!permissionsRequested) {
626
- return /*#__PURE__*/_jsx(View, {
833
+ return /*#__PURE__*/_jsx(SafeAreaView, {
627
834
  style: styles.permissionContainer,
628
835
  children: /*#__PURE__*/_jsx(ActivityIndicator, {
629
836
  size: "large",
@@ -632,7 +839,7 @@ const IdentityDocumentCamera = ({
632
839
  });
633
840
  }
634
841
  if (!cameraPermission.hasPermission) {
635
- return /*#__PURE__*/_jsxs(View, {
842
+ return /*#__PURE__*/_jsxs(SafeAreaView, {
636
843
  style: styles.permissionContainer,
637
844
  children: [/*#__PURE__*/_jsx(Text, {
638
845
  style: styles.permissionText,
@@ -647,7 +854,7 @@ const IdentityDocumentCamera = ({
647
854
  });
648
855
  }
649
856
  if (device == null) {
650
- return /*#__PURE__*/_jsx(View, {
857
+ return /*#__PURE__*/_jsx(SafeAreaView, {
651
858
  style: styles.permissionContainer,
652
859
  children: /*#__PURE__*/_jsx(TextView, {
653
860
  style: styles.permissionText,
@@ -673,7 +880,7 @@ const IdentityDocumentCamera = ({
673
880
  };
674
881
  return /*#__PURE__*/_jsx(View, {
675
882
  style: StyleSheet.absoluteFill,
676
- children: !hasGuideShown ? /*#__PURE__*/_jsxs(View, {
883
+ children: !hasGuideShown ? /*#__PURE__*/_jsxs(SafeAreaView, {
677
884
  style: styles.guide,
678
885
  children: [/*#__PURE__*/_jsx(LottieView, {
679
886
  source: require('../../Shared/Animations/id-or-passport.json'),
@@ -722,12 +929,26 @@ const IdentityDocumentCamera = ({
722
929
  onInitialized: () => {
723
930
  isCameraInitialized.value = true;
724
931
  }
725
- }), /*#__PURE__*/_jsx(View, {
726
- style: styles.topZone,
727
- children: /*#__PURE__*/_jsx(TextView, {
728
- style: styles.topZoneText,
729
- children: isBrightnessLow ? t('identityDocumentCamera.lowBrightness') : nextStep === 'SCAN_ID_FRONT_OR_PASSPORT' ? t('identityDocumentCamera.alignPhotoSide') : nextStep === 'SCAN_HOLOGRAM' ? t('identityDocumentCamera.alignHologram') : nextStep === 'SCAN_ID_BACK' ? t('identityDocumentCamera.alignIDBackSide') : nextStep === 'COMPLETED' ? t('identityDocumentCamera.scanCompleted') : ''
730
- })
932
+ }), /*#__PURE__*/_jsxs(View, {
933
+ style: [styles.topZone, {
934
+ paddingTop: insets.top
935
+ }],
936
+ children: [nextStep !== 'COMPLETED' && status !== 'SCANNED' && detectedDocumentType !== 'UNKNOWN' && /*#__PURE__*/_jsx(TextView, {
937
+ style: styles.stepIndicator,
938
+ children: nextStep === 'SCAN_ID_FRONT_OR_PASSPORT' ? `${t('identityDocumentCamera.frontSide')} • ${t('identityDocumentCamera.stepProgress', {
939
+ current: 1,
940
+ total: onlyMRZScan ? detectedDocumentType === 'PASSPORT' ? 1 : 2 : detectedDocumentType === 'PASSPORT' ? 2 : 3
941
+ })}` : nextStep === 'SCAN_HOLOGRAM' ? `${t('identityDocumentCamera.hologramCheck')} • ${t('identityDocumentCamera.stepProgress', {
942
+ current: 2,
943
+ total: detectedDocumentType === 'PASSPORT' ? 2 : 3
944
+ })}` : nextStep === 'SCAN_ID_BACK' ? `${t('identityDocumentCamera.backSide')} • ${t('identityDocumentCamera.stepProgress', {
945
+ current: 3,
946
+ total: 3
947
+ })}` : ''
948
+ }), /*#__PURE__*/_jsx(TextView, {
949
+ style: [styles.topZoneText, status === 'SCANNING' && styles.topZoneTextScanning, status === 'SCANNED' && styles.topZoneTextSuccess, status === 'INCORRECT' && styles.topZoneTextError, (isBrightnessLow || isFrameBlurry) && styles.topZoneTextWarning],
950
+ children: status === 'SCANNED' ? completedStep === 'SCAN_ID_FRONT_OR_PASSPORT' ? detectedDocumentType === 'PASSPORT' ? t('identityDocumentCamera.passportScanned') : t('identityDocumentCamera.frontSideScanned') : completedStep === 'SCAN_ID_BACK' ? t('identityDocumentCamera.backSideScanned') : completedStep === 'SCAN_HOLOGRAM' ? t('identityDocumentCamera.hologramVerified') : t('identityDocumentCamera.scanCompleted') : status === 'INCORRECT' ? nextStep === 'SCAN_ID_FRONT_OR_PASSPORT' ? t('identityDocumentCamera.wrongSideFront') : nextStep === 'SCAN_ID_BACK' ? t('identityDocumentCamera.wrongSideBack') : t('identityDocumentCamera.alignPhotoSide') : isBrightnessLow ? t('identityDocumentCamera.lowBrightness') : isFrameBlurry ? t('identityDocumentCamera.avoidBlur') : nextStep === 'SCAN_ID_FRONT_OR_PASSPORT' ? status === 'SCANNING' ? currentFaceImage ? detectedDocumentType === 'PASSPORT' ? t('identityDocumentCamera.passportDetected') : detectedDocumentType === 'ID_FRONT' ? t('identityDocumentCamera.idCardFrontDetected') : t('identityDocumentCamera.readingDocument') : t('identityDocumentCamera.readingDocument') : t('identityDocumentCamera.alignPhotoSide') : nextStep === 'SCAN_HOLOGRAM' ? t('identityDocumentCamera.alignHologram') : nextStep === 'SCAN_ID_BACK' ? status === 'SCANNING' ? t('identityDocumentCamera.readingDocument') : t('identityDocumentCamera.alignIDBackSide') : nextStep === 'COMPLETED' ? t('identityDocumentCamera.scanCompleted') : ''
951
+ })]
731
952
  }), /*#__PURE__*/_jsx(View, {
732
953
  style: styles.leftZone
733
954
  }), /*#__PURE__*/_jsx(View, {
@@ -797,29 +1018,40 @@ const IdentityDocumentCamera = ({
797
1018
  children: `2nd Face: ${currentSecondaryFaceImage ? '✓' : '✗'} (${secondaryFaceDetectionCurrentRetryCount.value}/${SECOND_FACE_DETECTION_RETRY_COUNT})`
798
1019
  })]
799
1020
  })]
800
- }), /*#__PURE__*/_jsxs(View, {
1021
+ }), /*#__PURE__*/_jsx(View, {
801
1022
  style: [styles.scanArea, {
802
- borderColor: status === 'SCANNED' || nextStep === 'COMPLETED' ? 'green' : status === 'INCORRECT' ? 'red' : 'white'
1023
+ borderColor: status === 'SCANNED' || nextStep === 'COMPLETED' ? '#4CAF50' // Green - success
1024
+ : status === 'INCORRECT' ? '#f44336' // Red - error
1025
+ : status === 'SCANNING' ? '#2196F3' // Blue - processing
1026
+ : isBrightnessLow || isFrameBlurry ? '#FFC107' // Yellow - warning
1027
+ : 'white',
1028
+ borderWidth: status === 'SCANNING' ? 3 : 2
803
1029
  }],
804
- children: [nextStep === 'SCAN_HOLOGRAM' && isTorchOn && !isBrightnessLow && /*#__PURE__*/_jsx(LottieView, {
805
- source: require('../../Shared/Animations/hologram-scan.json'),
1030
+ children: nextStep === 'COMPLETED' || status === 'SCANNED' ? /*#__PURE__*/_jsx(LottieView, {
1031
+ source: require('../../Shared/Animations/success.json'),
806
1032
  style: styles.animation,
807
- loop: true,
1033
+ loop: false,
808
1034
  autoPlay: true
809
- }), nextStep !== 'COMPLETED' && isBrightnessLow && /*#__PURE__*/_jsx(LottieView, {
1035
+ }) : isBrightnessLow ? /*#__PURE__*/_jsx(LottieView, {
810
1036
  source: require('../../Shared/Animations/light.json'),
811
1037
  style: styles.animation,
812
1038
  loop: true,
813
1039
  autoPlay: true
814
- }), nextStep === 'COMPLETED' && /*#__PURE__*/_jsx(LottieView, {
815
- source: require('../../Shared/Animations/success.json'),
1040
+ }) : nextStep === 'SCAN_HOLOGRAM' && isTorchOn ? /*#__PURE__*/_jsx(LottieView, {
1041
+ source: require('../../Shared/Animations/hologram-scan.json'),
816
1042
  style: styles.animation,
817
- loop: false,
1043
+ loop: true,
818
1044
  autoPlay: true
819
- })]
1045
+ }) : status === 'SCANNING' ? /*#__PURE__*/_jsx(LottieView, {
1046
+ source: require('../../Shared/Animations/scanning.json'),
1047
+ style: styles.animation,
1048
+ loop: true,
1049
+ autoPlay: true
1050
+ }) : null
820
1051
  }), /*#__PURE__*/_jsx(TouchableOpacity, {
821
1052
  onPress: handleFocus,
822
- style: styles.focusArea
1053
+ style: styles.focusArea,
1054
+ activeOpacity: 1
823
1055
  })]
824
1056
  })
825
1057
  });
@@ -877,6 +1109,13 @@ const styles = StyleSheet.create({
877
1109
  justifyContent: 'flex-end',
878
1110
  alignItems: 'center'
879
1111
  },
1112
+ stepIndicator: {
1113
+ color: '#aaaaaa',
1114
+ fontSize: 14,
1115
+ textAlign: 'center',
1116
+ fontWeight: '500',
1117
+ marginBottom: 4
1118
+ },
880
1119
  topZoneText: {
881
1120
  color: 'white',
882
1121
  fontSize: 20,
@@ -884,6 +1123,18 @@ const styles = StyleSheet.create({
884
1123
  fontWeight: 'bold',
885
1124
  padding: 20
886
1125
  },
1126
+ topZoneTextScanning: {
1127
+ color: '#2196F3' // Blue when scanning
1128
+ },
1129
+ topZoneTextSuccess: {
1130
+ color: '#4CAF50' // Green for success
1131
+ },
1132
+ topZoneTextWarning: {
1133
+ color: '#FFC107' // Yellow for warnings
1134
+ },
1135
+ topZoneTextError: {
1136
+ color: '#f44336' // Red for errors
1137
+ },
887
1138
  leftZone: {
888
1139
  position: 'absolute',
889
1140
  top: '36%',
@@ -5,6 +5,7 @@ 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';
8
9
  import i18n from "../../Translation/index.js";
9
10
  import StyledButton from "./StyledButton.js";
10
11
  import { analyticsService } from "../Services/AnalyticsService.js";
@@ -21,6 +22,7 @@ const NavigationManager = /*#__PURE__*/forwardRef(({
21
22
  const {
22
23
  t
23
24
  } = useTranslation();
25
+ const insets = useSafeAreaInsets();
24
26
  const routes = {
25
27
  VERIFICATION_SESSION_CHECK: 'VerificationSessionCheckScreen',
26
28
  DYNAMIC_ROUTES: {
@@ -42,6 +44,12 @@ const NavigationManager = /*#__PURE__*/forwardRef(({
42
44
  };
43
45
  }, [navigation]);
44
46
  const getNextRoute = useCallback((workflowSteps, currentWorkFlowStep) => {
47
+ // If this was a debug navigation, go directly to result screen
48
+ if (appContext.isDebugNavigated) {
49
+ appContext.isDebugNavigated = false;
50
+ appContext.currentWorkflowStep = undefined;
51
+ return routes.RESULT;
52
+ }
45
53
  const currentStepIndex = workflowSteps?.findIndex(step => step.id ? step.id === currentWorkFlowStep?.id : step.type === currentWorkFlowStep?.type) ?? -1;
46
54
  const nextStep = workflowSteps && currentStepIndex < workflowSteps.length ? workflowSteps[currentStepIndex + 1] : null;
47
55
  if (!nextStep) {
@@ -105,10 +113,10 @@ const NavigationManager = /*#__PURE__*/forwardRef(({
105
113
  }
106
114
  isNavigating = true;
107
115
  try {
116
+ // Preserve demo session state when resetting
117
+ const wasDemoSession = appContext.isDemoSession;
108
118
  appContext.currentWorkflowStep = undefined;
109
119
  appContext.workflowSteps = undefined;
110
- appContext.isDemoSession = false;
111
- analyticsService.setDemoSession(false);
112
120
  appContext.identificationInfo = {
113
121
  sessionId: '',
114
122
  identificationId: '',
@@ -124,6 +132,12 @@ const NavigationManager = /*#__PURE__*/forwardRef(({
124
132
  secondaryColor: appContext.branding?.secondaryColor || '#CCCCCC',
125
133
  tertiaryColor: appContext.branding?.tertiaryColor || '#FF0000'
126
134
  };
135
+
136
+ // Only reset demo mode if it wasn't a demo session
137
+ if (!wasDemoSession) {
138
+ appContext.setIsDemoSession?.(false);
139
+ analyticsService.setDemoSession(false);
140
+ }
127
141
  navigation.dispatch(CommonActions.reset({
128
142
  index: 0,
129
143
  routes: [{
@@ -151,7 +165,9 @@ const NavigationManager = /*#__PURE__*/forwardRef(({
151
165
  allowSkipStep: appContext.currentWorkflowStep?.required
152
166
  }));
153
167
  return appContext.currentWorkflowStep && (!appContext.currentWorkflowStep?.required || canSkipStep) && /*#__PURE__*/_jsx(View, {
154
- style: styles.container,
168
+ style: [styles.container, {
169
+ paddingBottom: insets.bottom
170
+ }],
155
171
  children: /*#__PURE__*/_jsx(StyledButton, {
156
172
  mode: "text",
157
173
  onPress: goToNextRouteWithAlert,
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Centralized configuration for camera enhancements
5
+ * Including autofocus, brightness/exposure, and contrast enhancement
6
+ */
7
+
8
+ export const ENHANCEMENT_CONFIG = {
9
+ autofocus: {
10
+ enabled: true,
11
+ intervalMs: 2500,
12
+ suspendOnDetection: true
13
+ },
14
+ brightness: {
15
+ thresholds: {
16
+ general: {
17
+ low: 40,
18
+ high: 120,
19
+ target: 80
20
+ },
21
+ faceDetection: {
22
+ low: 50,
23
+ high: 110,
24
+ target: 85
25
+ },
26
+ mrzScanning: {
27
+ low: 45,
28
+ high: 130,
29
+ target: 80
30
+ }
31
+ },
32
+ adaptiveStep: true,
33
+ maxStepSize: 2,
34
+ // Reduced from 3 for smoother transitions
35
+ hysteresis: 5 // Dead zone to prevent oscillation
36
+ },
37
+ contrast: {
38
+ enabled: true,
39
+ clahe: {
40
+ clipLimit: 2.0,
41
+ tileGridSize: [8, 8]
42
+ },
43
+ applyWhen: {
44
+ mrzFailing: true,
45
+ faceFailing: true,
46
+ documentBackSide: true,
47
+ retryThreshold: 2
48
+ },
49
+ performanceMode: 'adaptive'
50
+ },
51
+ performance: {
52
+ maxFrameProcessingTime: 180,
53
+ // ms
54
+ autoDisableThreshold: 200,
55
+ // ms
56
+ cachingEnabled: true
57
+ }
58
+ };
@@ -22,6 +22,7 @@ export default /*#__PURE__*/createContext({
22
22
  },
23
23
  workflowSteps: [],
24
24
  currentWorkflowStep: undefined,
25
+ isDebugNavigated: false,
25
26
  onCompleted: undefined,
26
27
  onError: undefined,
27
28
  setSessionId: undefined,