@trustchex/react-native-sdk 1.409.0 → 1.472.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 (171) hide show
  1. package/android/src/main/java/com/trustchex/reactnativesdk/TrustchexSDKModule.kt +2 -8
  2. package/android/src/main/java/com/trustchex/reactnativesdk/camera/TrustchexCameraView.kt +102 -4
  3. package/ios/Camera/TrustchexCameraView.swift +151 -18
  4. package/ios/Permission/PermissionModule.m +22 -0
  5. package/ios/Permission/PermissionModule.swift +67 -0
  6. package/lib/module/Screens/Debug/NFCScanTestScreen.js +635 -0
  7. package/lib/module/Screens/Dynamic/ContractAcceptanceScreen.js +1 -4
  8. package/lib/module/Screens/Dynamic/IdentityDocumentEIDScanningScreen.js +17 -4
  9. package/lib/module/Screens/Dynamic/LivenessDetectionScreen.js +125 -25
  10. package/lib/module/Screens/Dynamic/VerbalConsentScreen.js +1079 -0
  11. package/lib/module/Screens/Dynamic/VideoCallScreen.js +10 -13
  12. package/lib/module/Screens/Static/ResultScreen.js +131 -22
  13. package/lib/module/Screens/Static/VerificationSessionCheckScreen.js +8 -0
  14. package/lib/module/Shared/Animations/recording.json +1 -0
  15. package/lib/module/Shared/Components/DebugNavigationPanel.js +69 -71
  16. package/lib/module/Shared/Components/EIDScanner.js +222 -115
  17. package/lib/module/Shared/Components/FaceCamera.js +14 -8
  18. package/lib/module/Shared/Components/IdentityDocumentCamera.flows.js +5 -3
  19. package/lib/module/Shared/Components/IdentityDocumentCamera.js +60 -42
  20. package/lib/module/Shared/Components/IdentityDocumentCamera.utils.js +13 -4
  21. package/lib/module/Shared/Components/NavigationManager.js +24 -16
  22. package/lib/module/Shared/EIDReader/aesSecureMessagingWrapper.js +51 -0
  23. package/lib/module/Shared/EIDReader/apduLevelPACECapable.js +3 -0
  24. package/lib/module/Shared/EIDReader/bacKey.js +16 -2
  25. package/lib/module/Shared/EIDReader/eidReader.js +354 -13
  26. package/lib/module/Shared/EIDReader/eidService.js +25 -1
  27. package/lib/module/Shared/EIDReader/nfcManagerCardService.js +4 -7
  28. package/lib/module/Shared/EIDReader/paceInfo.js +85 -0
  29. package/lib/module/Shared/EIDReader/paceKeySpec.js +51 -0
  30. package/lib/module/Shared/EIDReader/protocol/paceAPDUSender.js +100 -0
  31. package/lib/module/Shared/EIDReader/protocol/paceProtocol.js +655 -0
  32. package/lib/module/Shared/EIDReader/protocol/paceResult.js +37 -0
  33. package/lib/module/Shared/EIDReader/secureMessagingWrapper.js +27 -4
  34. package/lib/module/Shared/EIDReader/smartcards/commandAPDU.js +2 -1
  35. package/lib/module/Shared/EIDReader/tlv/tlv.helpers.js +1 -1
  36. package/lib/module/Shared/EIDReader/tlv/tlv.utils.js +6 -3
  37. package/lib/module/Shared/EIDReader/utils/aesCrypto.utils.js +189 -0
  38. package/lib/module/Shared/Libs/PERMISSIONS_MANAGER.md +268 -0
  39. package/lib/module/Shared/Libs/analytics.utils.js +4 -0
  40. package/lib/module/Shared/Libs/contains.js +1 -40
  41. package/lib/module/Shared/Libs/country-display.utils.js +34 -0
  42. package/lib/module/Shared/Libs/demo.utils.js +8 -0
  43. package/lib/module/Shared/Libs/index.js +20 -0
  44. package/lib/module/Shared/Libs/mrz.utils.js +3 -2
  45. package/lib/module/Shared/Libs/permissions.utils.js +199 -0
  46. package/lib/module/Shared/Libs/status-bar.utils.js +4 -2
  47. package/lib/module/Shared/Types/analytics.types.js +2 -0
  48. package/lib/module/Translation/Resources/en.js +42 -2
  49. package/lib/module/Translation/Resources/tr.js +42 -2
  50. package/lib/module/Trustchex.js +54 -20
  51. package/lib/module/version.js +1 -1
  52. package/lib/typescript/src/Screens/Debug/NFCScanTestScreen.d.ts +3 -0
  53. package/lib/typescript/src/Screens/Debug/NFCScanTestScreen.d.ts.map +1 -0
  54. package/lib/typescript/src/Screens/Dynamic/ContractAcceptanceScreen.d.ts.map +1 -1
  55. package/lib/typescript/src/Screens/Dynamic/IdentityDocumentEIDScanningScreen.d.ts.map +1 -1
  56. package/lib/typescript/src/Screens/Dynamic/LivenessDetectionScreen.d.ts.map +1 -1
  57. package/lib/typescript/src/Screens/Dynamic/VerbalConsentScreen.d.ts +3 -0
  58. package/lib/typescript/src/Screens/Dynamic/VerbalConsentScreen.d.ts.map +1 -0
  59. package/lib/typescript/src/Screens/Dynamic/VideoCallScreen.d.ts.map +1 -1
  60. package/lib/typescript/src/Screens/Static/ResultScreen.d.ts.map +1 -1
  61. package/lib/typescript/src/Screens/Static/VerificationSessionCheckScreen.d.ts.map +1 -1
  62. package/lib/typescript/src/Shared/Components/DebugNavigationPanel.d.ts.map +1 -1
  63. package/lib/typescript/src/Shared/Components/EIDScanner.d.ts.map +1 -1
  64. package/lib/typescript/src/Shared/Components/FaceCamera.d.ts.map +1 -1
  65. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.d.ts.map +1 -1
  66. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.flows.d.ts +1 -1
  67. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.flows.d.ts.map +1 -1
  68. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.utils.d.ts +5 -0
  69. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.utils.d.ts.map +1 -1
  70. package/lib/typescript/src/Shared/Components/NavigationManager.d.ts.map +1 -1
  71. package/lib/typescript/src/Shared/EIDReader/aesSecureMessagingWrapper.d.ts +18 -0
  72. package/lib/typescript/src/Shared/EIDReader/aesSecureMessagingWrapper.d.ts.map +1 -0
  73. package/lib/typescript/src/Shared/EIDReader/apduLevelPACECapable.d.ts +23 -0
  74. package/lib/typescript/src/Shared/EIDReader/apduLevelPACECapable.d.ts.map +1 -0
  75. package/lib/typescript/src/Shared/EIDReader/bacKey.d.ts +6 -0
  76. package/lib/typescript/src/Shared/EIDReader/bacKey.d.ts.map +1 -1
  77. package/lib/typescript/src/Shared/EIDReader/eidReader.d.ts.map +1 -1
  78. package/lib/typescript/src/Shared/EIDReader/eidService.d.ts +9 -0
  79. package/lib/typescript/src/Shared/EIDReader/eidService.d.ts.map +1 -1
  80. package/lib/typescript/src/Shared/EIDReader/nfcManagerCardService.d.ts.map +1 -1
  81. package/lib/typescript/src/Shared/EIDReader/paceInfo.d.ts +50 -0
  82. package/lib/typescript/src/Shared/EIDReader/paceInfo.d.ts.map +1 -0
  83. package/lib/typescript/src/Shared/EIDReader/paceKeySpec.d.ts +30 -0
  84. package/lib/typescript/src/Shared/EIDReader/paceKeySpec.d.ts.map +1 -0
  85. package/lib/typescript/src/Shared/EIDReader/protocol/paceAPDUSender.d.ts +17 -0
  86. package/lib/typescript/src/Shared/EIDReader/protocol/paceAPDUSender.d.ts.map +1 -0
  87. package/lib/typescript/src/Shared/EIDReader/protocol/paceProtocol.d.ts +105 -0
  88. package/lib/typescript/src/Shared/EIDReader/protocol/paceProtocol.d.ts.map +1 -0
  89. package/lib/typescript/src/Shared/EIDReader/protocol/paceResult.d.ts +24 -0
  90. package/lib/typescript/src/Shared/EIDReader/protocol/paceResult.d.ts.map +1 -0
  91. package/lib/typescript/src/Shared/EIDReader/secureMessagingWrapper.d.ts +15 -0
  92. package/lib/typescript/src/Shared/EIDReader/secureMessagingWrapper.d.ts.map +1 -1
  93. package/lib/typescript/src/Shared/EIDReader/smartcards/commandAPDU.d.ts.map +1 -1
  94. package/lib/typescript/src/Shared/EIDReader/tlv/tlv.utils.d.ts.map +1 -1
  95. package/lib/typescript/src/Shared/EIDReader/utils/aesCrypto.utils.d.ts +39 -0
  96. package/lib/typescript/src/Shared/EIDReader/utils/aesCrypto.utils.d.ts.map +1 -0
  97. package/lib/typescript/src/Shared/Libs/analytics.utils.d.ts.map +1 -1
  98. package/lib/typescript/src/Shared/Libs/contains.d.ts +0 -7
  99. package/lib/typescript/src/Shared/Libs/contains.d.ts.map +1 -1
  100. package/lib/typescript/src/Shared/Libs/country-display.utils.d.ts +2 -0
  101. package/lib/typescript/src/Shared/Libs/country-display.utils.d.ts.map +1 -0
  102. package/lib/typescript/src/Shared/Libs/demo.utils.d.ts.map +1 -1
  103. package/lib/typescript/src/Shared/Libs/http-client.d.ts +1 -1
  104. package/lib/typescript/src/Shared/Libs/http-client.d.ts.map +1 -1
  105. package/lib/typescript/src/Shared/Libs/index.d.ts +20 -0
  106. package/lib/typescript/src/Shared/Libs/index.d.ts.map +1 -0
  107. package/lib/typescript/src/Shared/Libs/mrz.utils.d.ts.map +1 -1
  108. package/lib/typescript/src/Shared/Libs/permissions.utils.d.ts +58 -0
  109. package/lib/typescript/src/Shared/Libs/permissions.utils.d.ts.map +1 -0
  110. package/lib/typescript/src/Shared/Libs/status-bar.utils.d.ts.map +1 -1
  111. package/lib/typescript/src/Shared/Types/analytics.types.d.ts +2 -0
  112. package/lib/typescript/src/Shared/Types/analytics.types.d.ts.map +1 -1
  113. package/lib/typescript/src/Shared/Types/identificationInfo.d.ts +10 -1
  114. package/lib/typescript/src/Shared/Types/identificationInfo.d.ts.map +1 -1
  115. package/lib/typescript/src/Translation/Resources/en.d.ts +41 -1
  116. package/lib/typescript/src/Translation/Resources/en.d.ts.map +1 -1
  117. package/lib/typescript/src/Translation/Resources/tr.d.ts +41 -1
  118. package/lib/typescript/src/Translation/Resources/tr.d.ts.map +1 -1
  119. package/lib/typescript/src/Trustchex.d.ts.map +1 -1
  120. package/lib/typescript/src/version.d.ts +1 -1
  121. package/package.json +7 -4
  122. package/src/Screens/Debug/NFCScanTestScreen.tsx +692 -0
  123. package/src/Screens/Dynamic/ContractAcceptanceScreen.tsx +1 -4
  124. package/src/Screens/Dynamic/IdentityDocumentEIDScanningScreen.tsx +21 -4
  125. package/src/Screens/Dynamic/LivenessDetectionScreen.tsx +165 -24
  126. package/src/Screens/Dynamic/VerbalConsentScreen.tsx +1401 -0
  127. package/src/Screens/Dynamic/VideoCallScreen.tsx +11 -20
  128. package/src/Screens/Static/ResultScreen.tsx +187 -31
  129. package/src/Screens/Static/VerificationSessionCheckScreen.tsx +9 -0
  130. package/src/Shared/Animations/recording.json +1 -0
  131. package/src/Shared/Components/DebugNavigationPanel.tsx +73 -48
  132. package/src/Shared/Components/EIDScanner.tsx +280 -119
  133. package/src/Shared/Components/FaceCamera.tsx +19 -16
  134. package/src/Shared/Components/IdentityDocumentCamera.flows.ts +7 -4
  135. package/src/Shared/Components/IdentityDocumentCamera.tsx +206 -191
  136. package/src/Shared/Components/IdentityDocumentCamera.utils.ts +13 -4
  137. package/src/Shared/Components/NavigationManager.tsx +27 -18
  138. package/src/Shared/EIDReader/aesSecureMessagingWrapper.ts +69 -0
  139. package/src/Shared/EIDReader/apduLevelPACECapable.ts +34 -0
  140. package/src/Shared/EIDReader/bacKey.ts +24 -8
  141. package/src/Shared/EIDReader/eidReader.ts +398 -12
  142. package/src/Shared/EIDReader/eidService.ts +49 -1
  143. package/src/Shared/EIDReader/nfcManagerCardService.ts +4 -6
  144. package/src/Shared/EIDReader/paceInfo.ts +159 -0
  145. package/src/Shared/EIDReader/paceKeySpec.ts +56 -0
  146. package/src/Shared/EIDReader/protocol/paceAPDUSender.ts +163 -0
  147. package/src/Shared/EIDReader/protocol/paceProtocol.ts +946 -0
  148. package/src/Shared/EIDReader/protocol/paceResult.ts +62 -0
  149. package/src/Shared/EIDReader/secureMessagingWrapper.ts +28 -10
  150. package/src/Shared/EIDReader/smartcards/commandAPDU.ts +2 -1
  151. package/src/Shared/EIDReader/tlv/tlv.helpers.ts +1 -1
  152. package/src/Shared/EIDReader/tlv/tlv.utils.ts +8 -5
  153. package/src/Shared/EIDReader/utils/aesCrypto.utils.ts +217 -0
  154. package/src/Shared/Libs/PERMISSIONS_MANAGER.md +268 -0
  155. package/src/Shared/Libs/analytics.utils.ts +4 -0
  156. package/src/Shared/Libs/contains.ts +0 -53
  157. package/src/Shared/Libs/country-display.utils.ts +55 -0
  158. package/src/Shared/Libs/crypto.utils.ts +2 -2
  159. package/src/Shared/Libs/demo.utils.ts +10 -0
  160. package/src/Shared/Libs/http-client.ts +12 -4
  161. package/src/Shared/Libs/index.ts +63 -0
  162. package/src/Shared/Libs/mrz.utils.ts +3 -2
  163. package/src/Shared/Libs/permissions.utils.ts +251 -0
  164. package/src/Shared/Libs/status-bar.utils.ts +4 -2
  165. package/src/Shared/Services/VideoSessionService.ts +1 -1
  166. package/src/Shared/Types/analytics.types.ts +2 -0
  167. package/src/Shared/Types/identificationInfo.ts +11 -0
  168. package/src/Translation/Resources/en.ts +64 -3
  169. package/src/Translation/Resources/tr.ts +63 -3
  170. package/src/Trustchex.tsx +53 -17
  171. package/src/version.ts +1 -1
@@ -30,10 +30,27 @@ const IdentityDocumentEIDScanningScreen = () => {
30
30
  useState<DocumentScannedData | null>(null);
31
31
  const [passportData, setPassportData] =
32
32
  useState<DocumentScannedData | null>();
33
- const [documentNumber, setDocumentNumber] = useState<string>();
34
- const [dateOfBirth, setDateOfBirth] = useState<string>();
35
- const [dateOfExpiry, setDateOfExpiry] = useState<string>();
36
- const [documentType, setDocumentType] = useState<string>();
33
+ const [documentNumber, setDocumentNumber] = useState<string | undefined>(
34
+ () => {
35
+ const mrzFields =
36
+ appContext.identificationInfo.scannedDocument?.mrzFields;
37
+ return mrzFields?.documentNumber ?? undefined;
38
+ }
39
+ );
40
+ const [dateOfBirth, setDateOfBirth] = useState<string | undefined>(() => {
41
+ const mrzFields = appContext.identificationInfo.scannedDocument?.mrzFields;
42
+ return mrzFields?.birthDate ?? undefined;
43
+ });
44
+ const [dateOfExpiry, setDateOfExpiry] = useState<string | undefined>(() => {
45
+ const mrzFields = appContext.identificationInfo.scannedDocument?.mrzFields;
46
+ return mrzFields?.expirationDate ?? undefined;
47
+ });
48
+ const [documentType, setDocumentType] = useState<string | undefined>(() => {
49
+ const dc =
50
+ appContext.identificationInfo.scannedDocument?.mrzFields?.documentCode;
51
+ if (!dc) return undefined;
52
+ return dc === 'I' ? 'ID' : dc === 'P' ? 'PASSPORT' : 'UNKNOWN';
53
+ });
37
54
  const [isNFCSupported, setIsNFCSupported] = useState<boolean>(false);
38
55
  const { t } = useTranslation();
39
56
  const [allowedDocumentTypes, setAllowedDocumentTypes] = useState<
@@ -9,10 +9,13 @@ import React, {
9
9
  useRef,
10
10
  } from 'react';
11
11
  import {
12
+ Animated,
13
+ Easing,
12
14
  StyleSheet,
13
15
  Text,
14
16
  View,
15
17
  Dimensions,
18
+ Platform,
16
19
  Vibration,
17
20
  StatusBar,
18
21
  } from 'react-native';
@@ -27,7 +30,7 @@ import NavigationManager, {
27
30
  type NavigationManagerRef,
28
31
  } from '../../Shared/Components/NavigationManager';
29
32
  import AppContext from '../../Shared/Contexts/AppContext';
30
- import { contains, type Rect } from '../../Shared/Libs/contains';
33
+ import { type Rect } from '../../Shared/Libs/contains';
31
34
  import { useTranslation } from 'react-i18next';
32
35
  import StyledButton from '../../Shared/Components/StyledButton';
33
36
  import LottieView from 'lottie-react-native';
@@ -43,7 +46,7 @@ import { useKeepAwake } from '../../Shared/Libs/native-keep-awake.utils';
43
46
 
44
47
  const { width: windowWidth, height: windowHeight } = Dimensions.get('window');
45
48
 
46
- const PREVIEW_SIZE = windowWidth * 0.8;
49
+ const PREVIEW_SIZE = windowWidth * 0.95;
47
50
  const PREVIEW_RECT: Rect = {
48
51
  minX: (windowWidth - PREVIEW_SIZE) / 2,
49
52
  minY: (windowHeight - PREVIEW_SIZE) / 2,
@@ -96,10 +99,36 @@ const LivenessDetectionScreen = () => {
96
99
  const [isRecording, setIsRecording] = useState(false);
97
100
  const insets = useSafeAreaInsets();
98
101
  const referenceFaceTrackingId = useRef<number | null>(null);
102
+ const recordingDotOpacity = useRef(new Animated.Value(1)).current;
99
103
 
100
104
  // Track screen view and exit
101
105
  useScreenTracking('liveness_detection');
102
106
 
107
+ useEffect(() => {
108
+ if (isRecording) {
109
+ const pulse = Animated.loop(
110
+ Animated.sequence([
111
+ Animated.timing(recordingDotOpacity, {
112
+ toValue: 0.2,
113
+ duration: 600,
114
+ easing: Easing.inOut(Easing.ease),
115
+ useNativeDriver: true,
116
+ }),
117
+ Animated.timing(recordingDotOpacity, {
118
+ toValue: 1,
119
+ duration: 600,
120
+ easing: Easing.inOut(Easing.ease),
121
+ useNativeDriver: true,
122
+ }),
123
+ ])
124
+ );
125
+ pulse.start();
126
+ return () => pulse.stop();
127
+ } else {
128
+ recordingDotOpacity.setValue(1);
129
+ }
130
+ }, [isRecording, recordingDotOpacity]);
131
+
103
132
  // Configure status bar for white background
104
133
  useStatusBarWhiteBackground();
105
134
 
@@ -152,7 +181,6 @@ const LivenessDetectionScreen = () => {
152
181
  },
153
182
  FINISH: { instruction: t('livenessDetectionScreen.finish') },
154
183
  });
155
- const [instructionList, setInstructionList] = useState<string[]>([]);
156
184
  const [hasGuideShown, setHasGuideShown] = useState(false);
157
185
  const stoppingRecordingRef = useRef(false); // Track if we're already stopping to prevent multiple calls
158
186
 
@@ -167,7 +195,9 @@ const LivenessDetectionScreen = () => {
167
195
  } else {
168
196
  // Guide screen with white background - use dark icons
169
197
  StatusBar.setBarStyle('dark-content', true);
170
- StatusBar.setBackgroundColor('#ffffff', true);
198
+ if (Platform.OS === 'android') {
199
+ StatusBar.setBackgroundColor('#ffffff', true);
200
+ }
171
201
  }
172
202
  }, [hasGuideShown]);
173
203
 
@@ -189,7 +219,6 @@ const LivenessDetectionScreen = () => {
189
219
  .sort(() => Math.random() - 0.5);
190
220
  il.unshift('START');
191
221
  il.push('FINISH');
192
- setInstructionList(il);
193
222
  setInitialState({
194
223
  brightnessLow: false,
195
224
  faceDetected: false,
@@ -289,6 +318,14 @@ const LivenessDetectionScreen = () => {
289
318
  return leftOpen >= 0.8 && rightOpen >= 0.8;
290
319
  };
291
320
 
321
+ const areEyesVisible = (face: Face) => {
322
+ // Relaxed check: eyes just need to be detected (not fully closed).
323
+ // Smiling naturally causes squinting, so a lower threshold is used.
324
+ const leftOpen = face.leftEyeOpenProbability ?? 0;
325
+ const rightOpen = face.rightEyeOpenProbability ?? 0;
326
+ return leftOpen >= 0.3 && rightOpen >= 0.3;
327
+ };
328
+
292
329
  const instructionReducer = (
293
330
  state: StateType,
294
331
  action: PossibleActions
@@ -526,26 +563,84 @@ const LivenessDetectionScreen = () => {
526
563
  );
527
564
  }
528
565
 
529
- // Calculate preview rect in frame coordinates (not screen coordinates)
530
- // Preview circle is 80% of frame width, centered
531
- const previewSizeInFrame = frameWidth * 0.8;
566
+ // Map the on-screen circle into frame coordinates.
567
+ // Camera uses FILL_CENTER / resizeAspectFill (cover): uniform scale filling
568
+ // the view, cropping overflow symmetrically.
569
+ const coverScale = Math.max(
570
+ windowWidth / frameWidth,
571
+ windowHeight / frameHeight
572
+ );
573
+ const offsetX = (frameWidth - windowWidth / coverScale) / 2;
574
+ const offsetY = (frameHeight - windowHeight / coverScale) / 2;
532
575
  const previewRectInFrame: Rect = {
533
- minX: (frameWidth - previewSizeInFrame) / 2,
534
- minY: (frameHeight - previewSizeInFrame) / 2,
535
- width: previewSizeInFrame,
536
- height: previewSizeInFrame,
576
+ minX: PREVIEW_RECT.minX / coverScale + offsetX,
577
+ minY: PREVIEW_RECT.minY / coverScale + offsetY,
578
+ width: PREVIEW_RECT.width / coverScale,
579
+ height: PREVIEW_RECT.height / coverScale,
537
580
  };
581
+ const previewSizeInFrame = PREVIEW_RECT.width / coverScale;
582
+
583
+ // Check containment using face center + yaw-stable core radius.
584
+ // When the head turns (yaw), the bounding box grows wider but height stays
585
+ // constant — so min(width, height)/2 gives a stable size that doesn't
586
+ // falsely trigger an outside-circle reset during TURN_HEAD_LEFT/RIGHT.
587
+ const faceCenterX = face.bounds.x + face.bounds.width / 2;
588
+ const faceCenterY = face.bounds.y + face.bounds.height / 2;
589
+ const faceCoreRadius = Math.max(
590
+ 0,
591
+ Math.min(face.bounds.width, face.bounds.height) / 2 -
592
+ PREVIEW_EDGE_OFFSET / 2
593
+ );
594
+ const circleCX = previewRectInFrame.minX + previewRectInFrame.width / 2;
595
+ const circleCY = previewRectInFrame.minY + previewRectInFrame.height / 2;
596
+ const circleR = previewSizeInFrame / 2;
597
+ const faceDx = faceCenterX - circleCX;
598
+ const faceDy = faceCenterY - circleCY;
599
+
600
+ const yawAbs = Math.abs(face.yawAngle || 0);
601
+ const yawFactor = Math.min(1, yawAbs / TURN_ANGLE_LIMIT);
602
+
603
+ // General bound tolerance (instruction-agnostic): adapt to head pose and
604
+ // face box deformation so containment remains stable across all actions.
605
+ const faceWidthGrowth = Math.max(
606
+ 0,
607
+ face.bounds.width - face.bounds.height
608
+ );
538
609
 
539
- const faceRectSmaller: Rect = {
540
- width: face.bounds.width - PREVIEW_EDGE_OFFSET,
541
- height: face.bounds.height - PREVIEW_EDGE_OFFSET,
542
- minY: face.bounds.y + PREVIEW_EDGE_OFFSET / 2,
543
- minX: face.bounds.x + PREVIEW_EDGE_OFFSET / 2,
544
- };
545
- const previewContainsFace = contains({
546
- outside: previewRectInFrame,
547
- inside: faceRectSmaller,
548
- });
610
+ // On iOS, close faces tend to appear larger in-frame and lateral head
611
+ // turns shift center more aggressively. Apply an iOS-only proximity boost
612
+ // to horizontal tolerance to avoid false outside-circle resets.
613
+ const faceRadiusRatio = Math.min(
614
+ 1,
615
+ faceCoreRadius / Math.max(1, circleR)
616
+ );
617
+ const iosProximityBoost =
618
+ Platform.OS === 'ios' ? Math.max(0, faceRadiusRatio - 0.5) : 0;
619
+
620
+ const baseToleranceX = previewSizeInFrame * 0.05;
621
+ const yawToleranceX = face.bounds.width * 0.12 * yawFactor;
622
+ const shapeToleranceX = faceWidthGrowth * 0.1;
623
+ const iosToleranceX = face.bounds.width * 0.2 * iosProximityBoost;
624
+ const toleranceX = Math.max(
625
+ 0,
626
+ baseToleranceX + yawToleranceX + shapeToleranceX + iosToleranceX,
627
+ face.bounds.width * 0.05
628
+ );
629
+
630
+ const pitchAbs = Math.abs(face.pitchAngle || 0);
631
+ const pitchFactor = Math.min(1, pitchAbs / TURN_ANGLE_LIMIT);
632
+ const toleranceY = Math.max(
633
+ 0,
634
+ previewSizeInFrame * 0.03 + face.bounds.height * 0.03 * pitchFactor,
635
+ face.bounds.height * 0.04
636
+ );
637
+
638
+ const allowedRadiusX = Math.max(1, circleR - faceCoreRadius + toleranceX);
639
+ const allowedRadiusY = Math.max(1, circleR - faceCoreRadius + toleranceY);
640
+ const normalizedDistance =
641
+ (faceDx * faceDx) / (allowedRadiusX * allowedRadiusX) +
642
+ (faceDy * faceDy) / (allowedRadiusY * allowedRadiusY);
643
+ const previewContainsFace = normalizedDistance <= 1;
549
644
  const multipleFacesDetected = faces.length > 1;
550
645
 
551
646
  if (!isImageBright) {
@@ -694,11 +789,12 @@ const LivenessDetectionScreen = () => {
694
789
  instructions.SMILE.minAngle < face.pitchAngle &&
695
790
  face.pitchAngle < instructions.SMILE.maxAngle;
696
791
 
697
- // Check if smiling with sufficient probability AND looking at camera AND eyes open
792
+ // Check if smiling with sufficient probability AND looking at camera AND eyes visible
793
+ // Use relaxed eye check — smiling naturally causes squinting
698
794
  if (
699
795
  smilingProb >= instructions.SMILE.minProbability &&
700
796
  isFacingCamera &&
701
- areEyesOpen(face)
797
+ areEyesVisible(face)
702
798
  ) {
703
799
  instructions.SMILE.photo = image;
704
800
  dispatch({ type: 'NEXT_INSTRUCTION', payload: 'SMILE' });
@@ -890,6 +986,24 @@ const LivenessDetectionScreen = () => {
890
986
  mask="url(#hole-mask)"
891
987
  />
892
988
  </Svg>
989
+ {isRecording && (
990
+ <View
991
+ style={[
992
+ styles.recordingIndicatorContainer,
993
+ { top: PREVIEW_RECT.minY - 40 },
994
+ ]}
995
+ >
996
+ <View style={styles.recordingIndicator}>
997
+ <Animated.View
998
+ style={[
999
+ styles.recordingDot,
1000
+ { opacity: recordingDotOpacity },
1001
+ ]}
1002
+ />
1003
+ <Text style={styles.recordingText}>REC</Text>
1004
+ </View>
1005
+ </View>
1006
+ )}
893
1007
  <View
894
1008
  style={[
895
1009
  styles.instructionsContainerBottom,
@@ -1014,6 +1128,33 @@ const styles = StyleSheet.create({
1014
1128
  width: '100%',
1015
1129
  zIndex: 1,
1016
1130
  },
1131
+ recordingIndicatorContainer: {
1132
+ position: 'absolute',
1133
+ width: '100%',
1134
+ alignItems: 'center',
1135
+ zIndex: 2,
1136
+ },
1137
+ recordingIndicator: {
1138
+ flexDirection: 'row',
1139
+ alignItems: 'center',
1140
+ gap: 8,
1141
+ backgroundColor: 'rgba(0, 0, 0, 0.6)',
1142
+ paddingVertical: 6,
1143
+ paddingHorizontal: 14,
1144
+ borderRadius: 20,
1145
+ },
1146
+ recordingDot: {
1147
+ width: 10,
1148
+ height: 10,
1149
+ borderRadius: 5,
1150
+ backgroundColor: '#ef4444',
1151
+ },
1152
+ recordingText: {
1153
+ color: '#ffffff',
1154
+ fontSize: 14,
1155
+ fontWeight: '700',
1156
+ letterSpacing: 1,
1157
+ },
1017
1158
  guide: {
1018
1159
  flex: 1,
1019
1160
  display: 'flex',