@trustchex/react-native-sdk 1.464.0 → 1.475.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 (64) hide show
  1. package/android/build.gradle +3 -3
  2. package/android/src/main/java/com/trustchex/reactnativesdk/camera/TrustchexCameraView.kt +190 -12
  3. package/ios/Camera/TrustchexCameraView.swift +244 -46
  4. package/ios/Permission/PermissionModule.m +22 -0
  5. package/ios/Permission/PermissionModule.swift +67 -0
  6. package/lib/module/Screens/Debug/MRZTestScreen.js +121 -147
  7. package/lib/module/Screens/Dynamic/LivenessDetectionScreen.js +24 -3
  8. package/lib/module/Screens/Dynamic/VerbalConsentScreen.js +1 -1
  9. package/lib/module/Screens/Dynamic/VideoCallScreen.js +7 -12
  10. package/lib/module/Screens/Static/ResultScreen.js +9 -1
  11. package/lib/module/Shared/Components/EIDScanner.js +166 -163
  12. package/lib/module/Shared/Components/FaceCamera.js +14 -8
  13. package/lib/module/Shared/Components/IdentityDocumentCamera.js +45 -10
  14. package/lib/module/Shared/Libs/MRZ_KNOWN_ISSUES.md +112 -0
  15. package/lib/module/Shared/Libs/PERMISSIONS_MANAGER.md +268 -0
  16. package/lib/module/Shared/Libs/index.js +20 -0
  17. package/lib/module/Shared/Libs/mrz.utils.js +570 -16
  18. package/lib/module/Shared/Libs/mrzFrameAggregator.js +301 -0
  19. package/lib/module/Shared/Libs/mrzOcrIntegration.js +109 -0
  20. package/lib/module/Shared/Libs/permissions.utils.js +199 -0
  21. package/lib/module/Translation/Resources/en.js +1 -0
  22. package/lib/module/Translation/Resources/tr.js +1 -0
  23. package/lib/module/version.js +1 -1
  24. package/lib/typescript/src/Screens/Debug/MRZTestScreen.d.ts.map +1 -1
  25. package/lib/typescript/src/Screens/Dynamic/LivenessDetectionScreen.d.ts.map +1 -1
  26. package/lib/typescript/src/Screens/Dynamic/VideoCallScreen.d.ts.map +1 -1
  27. package/lib/typescript/src/Screens/Static/ResultScreen.d.ts.map +1 -1
  28. package/lib/typescript/src/Shared/Components/EIDScanner.d.ts.map +1 -1
  29. package/lib/typescript/src/Shared/Components/FaceCamera.d.ts.map +1 -1
  30. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.d.ts.map +1 -1
  31. package/lib/typescript/src/Shared/Libs/index.d.ts +20 -0
  32. package/lib/typescript/src/Shared/Libs/index.d.ts.map +1 -0
  33. package/lib/typescript/src/Shared/Libs/mrz.utils.d.ts +8 -0
  34. package/lib/typescript/src/Shared/Libs/mrz.utils.d.ts.map +1 -1
  35. package/lib/typescript/src/Shared/Libs/mrzFrameAggregator.d.ts +76 -0
  36. package/lib/typescript/src/Shared/Libs/mrzFrameAggregator.d.ts.map +1 -0
  37. package/lib/typescript/src/Shared/Libs/mrzOcrIntegration.d.ts +73 -0
  38. package/lib/typescript/src/Shared/Libs/mrzOcrIntegration.d.ts.map +1 -0
  39. package/lib/typescript/src/Shared/Libs/permissions.utils.d.ts +58 -0
  40. package/lib/typescript/src/Shared/Libs/permissions.utils.d.ts.map +1 -0
  41. package/lib/typescript/src/Translation/Resources/en.d.ts +1 -0
  42. package/lib/typescript/src/Translation/Resources/en.d.ts.map +1 -1
  43. package/lib/typescript/src/Translation/Resources/tr.d.ts +1 -0
  44. package/lib/typescript/src/Translation/Resources/tr.d.ts.map +1 -1
  45. package/lib/typescript/src/version.d.ts +1 -1
  46. package/package.json +14 -10
  47. package/src/Screens/Debug/MRZTestScreen.tsx +137 -166
  48. package/src/Screens/Dynamic/LivenessDetectionScreen.tsx +45 -5
  49. package/src/Screens/Dynamic/VerbalConsentScreen.tsx +1 -1
  50. package/src/Screens/Dynamic/VideoCallScreen.tsx +8 -19
  51. package/src/Screens/Static/ResultScreen.tsx +18 -9
  52. package/src/Shared/Components/EIDScanner.tsx +210 -160
  53. package/src/Shared/Components/FaceCamera.tsx +19 -16
  54. package/src/Shared/Components/IdentityDocumentCamera.tsx +53 -13
  55. package/src/Shared/Libs/MRZ_KNOWN_ISSUES.md +112 -0
  56. package/src/Shared/Libs/PERMISSIONS_MANAGER.md +268 -0
  57. package/src/Shared/Libs/index.ts +63 -0
  58. package/src/Shared/Libs/mrz.utils.ts +639 -11
  59. package/src/Shared/Libs/mrzFrameAggregator.ts +370 -0
  60. package/src/Shared/Libs/mrzOcrIntegration.ts +175 -0
  61. package/src/Shared/Libs/permissions.utils.ts +251 -0
  62. package/src/Translation/Resources/en.ts +1 -0
  63. package/src/Translation/Resources/tr.ts +1 -0
  64. package/src/version.ts +1 -1
@@ -7,116 +7,108 @@ import {
7
7
  StatusBar,
8
8
  TouchableOpacity,
9
9
  type NativeSyntheticEvent,
10
- type LayoutChangeEvent,
11
10
  } from 'react-native';
12
11
  import { SafeAreaView } from 'react-native-safe-area-context';
13
12
  import {
14
13
  TrustchexCamera,
15
14
  type TrustchexCameraHandle,
16
15
  type Frame,
17
- type NativeTextBlock,
18
16
  } from '../../Shared/Components/TrustchexCamera';
19
- import mrzUtils from '../../Shared/Libs/mrz.utils';
17
+ import { MRZFrameAggregator } from '../../Shared/Libs/mrzFrameAggregator';
20
18
  import { useKeepAwake } from '../../Shared/Libs/native-keep-awake.utils';
21
19
 
22
- interface MrzOverlayBlock extends NativeTextBlock {
23
- isMrz: boolean;
20
+ // All per-frame display data lives in ONE state object so a processed frame
21
+ // triggers a SINGLE re-render instead of 7 separate setState calls.
22
+ interface MrzView {
23
+ raw: string;
24
+ voted: string;
25
+ valid: boolean;
26
+ frames: number;
27
+ stable: boolean;
24
28
  }
25
29
 
30
+ const EMPTY_VIEW: MrzView = {
31
+ raw: '',
32
+ voted: 'Voting…',
33
+ valid: false,
34
+ frames: 0,
35
+ stable: false,
36
+ };
37
+
38
+ // Process the live OCR stream at this cadence. The camera delivers frames faster
39
+ // than the MRZ validation/voting can run; gating here keeps the JS thread free to
40
+ // service touches (so the buttons stay responsive) while still converging fast.
41
+ const MRZ_PROCESS_INTERVAL_MS = 400;
42
+
26
43
  const MRZTestScreen = () => {
27
44
  useKeepAwake();
28
45
  const cameraRef = useRef<TrustchexCameraHandle>(null);
29
- const [mrzText, setMrzText] = useState<string>('Waiting for MRZ...');
30
- const [mrzRawText, setMrzRawText] = useState<string>('');
31
- const [mrzValid, setMrzValid] = useState<boolean>(false);
32
- const [rawHistory, setRawHistory] = useState<string[]>([]);
46
+
47
+ // Single consolidated view-state; one setState per processed frame.
48
+ const [view, setView] = useState<MrzView>(EMPTY_VIEW);
33
49
  const [isPaused, setIsPaused] = useState<boolean>(false);
34
- const [overlayBlocks, setOverlayBlocks] = useState<MrzOverlayBlock[]>([]);
35
- const [cameraLayout, setCameraLayout] = useState({ width: 1, height: 1 });
36
- const [frameSize, setFrameSize] = useState({ width: 1080, height: 1920 });
37
50
 
38
- const handleCameraLayout = useCallback((e: LayoutChangeEvent) => {
39
- const { width, height } = e.nativeEvent.layout;
40
- setCameraLayout({ width, height });
41
- }, []);
51
+ // Refs for values the frame handler reads but that must NOT re-create it:
52
+ // keeping handleFrame identity-stable avoids re-subscribing the camera and
53
+ // keeps it cheap.
54
+ const pausedRef = useRef(false);
55
+ const lastProcessedAt = useRef(0);
56
+ const aggregator = useRef(new MRZFrameAggregator({ stabilityTarget: 2 }));
42
57
 
43
58
  const handleFrame = useCallback(
44
59
  (event: NativeSyntheticEvent<{ frame: Frame }>) => {
45
- if (isPaused) {
46
- return;
47
- }
48
-
49
- const frame = event.nativeEvent.frame;
50
-
51
- if (frame.width && frame.height) {
52
- setFrameSize({ width: frame.width, height: frame.height });
53
- }
60
+ if (pausedRef.current) return;
54
61
 
55
- if (frame.textBlocks && frame.textBlocks.length > 0) {
56
- const frameHeight = frame.height;
57
- const bottomThreshold = frameHeight * 0.5;
58
- const blocks: MrzOverlayBlock[] = frame.textBlocks.map((block) => {
59
- const blockText = block.text || '';
60
- return {
61
- ...block,
62
- isMrz:
63
- blockText.includes('<') &&
64
- blockText.length >= 12 &&
65
- (block.blockFrame ? block.blockFrame.y > bottomThreshold : false),
66
- };
67
- });
68
- setOverlayBlocks(blocks);
69
- } else {
70
- setOverlayBlocks([]);
71
- }
62
+ // Throttle the ENTIRE handler. Drop frames that arrive inside the window
63
+ // before doing ANY work (no parsing, no setState) so touch events land.
64
+ const now = Date.now();
65
+ if (now - lastProcessedAt.current < MRZ_PROCESS_INTERVAL_MS) return;
72
66
 
73
- if (frame.resultText) {
74
- setRawHistory((prev) => {
75
- if (prev[0] === frame.resultText) {
76
- return prev;
77
- }
78
- const next = [frame.resultText, ...prev];
79
- return next.slice(0, 20);
80
- });
81
- }
82
-
83
- if (frame.resultText && frame.resultText.includes('<')) {
84
- // Extract MRZ-only text from detected blocks
85
- let mrzOnlyText = '';
86
- if (frame.textBlocks && frame.textBlocks.length > 0) {
87
- // Filter blocks that look like MRZ (contain < and are in bottom half of frame)
88
- const frameHeight = frame.height;
89
- const bottomThreshold = frameHeight * 0.5;
90
-
91
- const mrzBlocks = frame.textBlocks.filter((block) => {
92
- const blockText = block.text || '';
93
- const hasFillers = blockText.includes('<');
94
- const isInBottomArea = block.blockFrame
95
- ? block.blockFrame.y > bottomThreshold
96
- : false;
97
- const isLongEnough = blockText.length >= 12;
98
- return hasFillers && isInBottomArea && isLongEnough;
99
- });
67
+ const frame = event.nativeEvent.frame;
68
+ const resultText = frame.resultText;
69
+ if (!resultText || !resultText.includes('<')) return;
70
+ lastProcessedAt.current = now;
100
71
 
101
- if (mrzBlocks.length > 0) {
102
- mrzOnlyText = mrzBlocks.map((b) => b.text).join('\n');
103
- }
104
- }
72
+ // Collect every MRZ-looking block (contains a filler) regardless of
73
+ // position/length the band is often fragmented and may not sit in the
74
+ // bottom half. Reconstruction downstream isolates it from front-of-card text.
75
+ const mrzBlocks = (frame.textBlocks ?? []).filter((b) =>
76
+ (b.text || '').includes('<')
77
+ );
78
+ const mrzOnlyText = mrzBlocks.length
79
+ ? mrzBlocks.map((b) => b.text).join('\n')
80
+ : resultText;
105
81
 
106
- // Fallback to full text if no MRZ blocks detected
107
- if (!mrzOnlyText) {
108
- mrzOnlyText = frame.resultText;
109
- }
82
+ const bandHeight = mrzBlocks.length
83
+ ? Math.max(0, ...mrzBlocks.map((b) => b.blockFrame?.height ?? 0))
84
+ : 0;
85
+ const consensus = aggregator.current.addFrame({
86
+ text: `${mrzOnlyText}\n${resultText}`,
87
+ weight: bandHeight > 0 ? bandHeight : 1,
88
+ });
110
89
 
111
- const fixedMrz = mrzUtils.fixMRZ(mrzOnlyText);
112
- setMrzRawText(mrzOnlyText);
113
- setMrzText(fixedMrz);
114
- setMrzValid(mrzUtils.validateMRZWithCorrections(fixedMrz).valid);
115
- }
90
+ // One setState → one re-render.
91
+ setView({
92
+ raw: mrzOnlyText,
93
+ voted: consensus.mrz ?? `Voting… (${consensus.frames} frames)`,
94
+ valid: !!consensus.validation?.valid,
95
+ frames: consensus.frames,
96
+ stable: consensus.stable,
97
+ });
116
98
  },
117
- [isPaused]
99
+ []
118
100
  );
119
101
 
102
+ const togglePause = useCallback(() => {
103
+ pausedRef.current = !pausedRef.current;
104
+ setIsPaused(pausedRef.current);
105
+ }, []);
106
+
107
+ const resetVoting = useCallback(() => {
108
+ aggregator.current.reset();
109
+ setView(EMPTY_VIEW);
110
+ }, []);
111
+
120
112
  return (
121
113
  <View style={styles.container}>
122
114
  <StatusBar
@@ -124,7 +116,7 @@ const MRZTestScreen = () => {
124
116
  backgroundColor="transparent"
125
117
  translucent
126
118
  />
127
- <View style={styles.cameraWrapper} onLayout={handleCameraLayout}>
119
+ <View style={styles.cameraWrapper}>
128
120
  <TrustchexCamera
129
121
  ref={cameraRef}
130
122
  style={StyleSheet.absoluteFill}
@@ -134,78 +126,50 @@ const MRZTestScreen = () => {
134
126
  enableTextRecognition={true}
135
127
  enableBarcodeScanning={false}
136
128
  includeBase64={false}
137
- targetFps={10}
129
+ targetFps={5}
138
130
  onFrameAvailable={handleFrame}
139
131
  />
140
- {overlayBlocks.map((block, i) => {
141
- const bf = block.blockFrame;
142
- if (!bf) return null;
143
- const scaleX = cameraLayout.width / frameSize.width;
144
- const scaleY = cameraLayout.height / frameSize.height;
145
- return (
146
- <View
147
- key={i}
148
- style={[
149
- styles.boundingBox,
150
- block.isMrz ? styles.boundingBoxMrz : styles.boundingBoxText,
151
- {
152
- left: bf.x * scaleX,
153
- top: bf.y * scaleY,
154
- width: bf.width * scaleX,
155
- height: bf.height * scaleY,
156
- },
157
- ]}
158
- >
159
- {block.isMrz && (
160
- <View style={styles.blockLabel}>
161
- <Text style={styles.blockLabelText}>MRZ</Text>
162
- </View>
163
- )}
164
- </View>
165
- );
166
- })}
132
+
133
+ {/* Scan-frame guide: matches the native OCR crop region. Only the data
134
+ INSIDE this box is OCR'd — align the card's MRZ band here. */}
135
+ <View pointerEvents="none" style={styles.scanGuide}>
136
+ <Text style={styles.scanGuideLabel}>Align MRZ inside this frame</Text>
137
+ </View>
167
138
  </View>
168
139
 
169
140
  <SafeAreaView style={styles.mrzPanel} edges={['bottom']}>
170
141
  <ScrollView style={styles.scrollView}>
171
142
  <View style={styles.panelContent}>
172
- <TouchableOpacity
173
- style={styles.pauseButton}
174
- onPress={() => setIsPaused(!isPaused)}
175
- >
176
- <Text style={styles.pauseButtonText}>
177
- {isPaused ? 'Resume Processing' : 'Pause Processing'}
178
- </Text>
179
- </TouchableOpacity>
143
+ <View style={styles.buttonRow}>
144
+ <TouchableOpacity
145
+ style={styles.pauseButton}
146
+ onPress={togglePause}
147
+ >
148
+ <Text style={styles.pauseButtonText}>
149
+ {isPaused ? 'Resume Processing' : 'Pause Processing'}
150
+ </Text>
151
+ </TouchableOpacity>
152
+ <TouchableOpacity
153
+ style={styles.pauseButton}
154
+ onPress={resetVoting}
155
+ >
156
+ <Text style={styles.pauseButtonText}>Reset Voting</Text>
157
+ </TouchableOpacity>
158
+ </View>
180
159
  <Text style={styles.title}>MRZ Text Read</Text>
181
- <Text style={styles.sectionTitle}>MRZ Raw Text</Text>
182
- <Text style={styles.mrzText}>
183
- {mrzRawText
184
- ? mrzRawText
185
- .split('\n')
186
- .map(
187
- (line, i) =>
188
- `Line ${i + 1}: ${line} (${line.length} chars)`
189
- )
190
- .join('\n')
191
- : 'No MRZ raw text'}
160
+
161
+ <Text style={styles.sectionTitle}>
162
+ ★ Multi-frame Voted MRZ — {view.frames} frames
163
+ {view.valid ? ' · VALID' : ''}
164
+ {view.stable ? ' · STABLE' : ''}
192
165
  </Text>
193
- <Text style={styles.sectionTitle}>MRZ Corrected Text</Text>
194
- <Text style={[styles.mrzText, mrzValid ? styles.mrzValid : null]}>
195
- {mrzText
196
- .split('\n')
197
- .map(
198
- (line, i) => `Line ${i + 1}: ${line} (${line.length} chars)`
199
- )
200
- .join('\n')}
166
+ <Text style={[styles.mrzText, view.valid ? styles.mrzValid : null]}>
167
+ {formatLines(view.voted)}
201
168
  </Text>
202
- <Text style={styles.sectionTitle}>Raw Text History</Text>
169
+
170
+ <Text style={styles.sectionTitle}>MRZ Raw Text (this frame)</Text>
203
171
  <Text style={styles.mrzText}>
204
- {rawHistory.length > 0
205
- ? rawHistory
206
- .map((text, i) => `#${i + 1}: ${text.replace(/\n/g, ' ')}`)
207
- .join('\n')
208
- : 'No history'}
172
+ {view.raw ? formatLines(view.raw) : 'No MRZ raw text'}
209
173
  </Text>
210
174
  </View>
211
175
  </ScrollView>
@@ -214,6 +178,13 @@ const MRZTestScreen = () => {
214
178
  );
215
179
  };
216
180
 
181
+ /** Render MRZ text as "Line N: <chars> (N chars)" rows. */
182
+ const formatLines = (text: string): string =>
183
+ text
184
+ .split('\n')
185
+ .map((line, i) => `Line ${i + 1}: ${line} (${line.length} chars)`)
186
+ .join('\n');
187
+
217
188
  const styles = StyleSheet.create({
218
189
  container: {
219
190
  flex: 1,
@@ -223,31 +194,26 @@ const styles = StyleSheet.create({
223
194
  flex: 2,
224
195
  position: 'relative',
225
196
  },
226
- boundingBox: {
197
+ scanGuide: {
227
198
  position: 'absolute',
228
- borderWidth: 1.5,
229
- },
230
- boundingBoxText: {
231
- borderColor: 'rgba(255, 255, 255, 0.25)',
232
- },
233
- boundingBoxMrz: {
234
- borderColor: '#FFA500',
199
+ // Mirrors the native OCR crop: top 25% / bottom 12% / 4% side margins.
200
+ top: '25%',
201
+ bottom: '12%',
202
+ left: '4%',
203
+ right: '4%',
235
204
  borderWidth: 2,
236
- backgroundColor: 'rgba(255, 165, 0, 0.07)',
237
- },
238
- blockLabel: {
239
- position: 'absolute',
240
- top: -14,
241
- left: 0,
242
- backgroundColor: '#FFA500',
243
- paddingHorizontal: 4,
244
- paddingVertical: 1,
245
- borderRadius: 2,
205
+ borderColor: '#00E5FF',
206
+ borderRadius: 6,
207
+ justifyContent: 'flex-start',
208
+ alignItems: 'center',
246
209
  },
247
- blockLabelText: {
248
- color: '#000000',
249
- fontSize: 8,
210
+ scanGuideLabel: {
211
+ color: '#00E5FF',
212
+ fontSize: 10,
250
213
  fontWeight: 'bold',
214
+ marginTop: -16,
215
+ backgroundColor: 'rgba(0,0,0,0.5)',
216
+ paddingHorizontal: 4,
251
217
  },
252
218
  mrzPanel: {
253
219
  flex: 1,
@@ -261,6 +227,11 @@ const styles = StyleSheet.create({
261
227
  panelContent: {
262
228
  padding: 10,
263
229
  },
230
+ buttonRow: {
231
+ flexDirection: 'row',
232
+ justifyContent: 'center',
233
+ gap: 8,
234
+ },
264
235
  pauseButton: {
265
236
  alignSelf: 'center',
266
237
  borderWidth: 1,
@@ -181,7 +181,6 @@ const LivenessDetectionScreen = () => {
181
181
  },
182
182
  FINISH: { instruction: t('livenessDetectionScreen.finish') },
183
183
  });
184
- const [instructionList, setInstructionList] = useState<string[]>([]);
185
184
  const [hasGuideShown, setHasGuideShown] = useState(false);
186
185
  const stoppingRecordingRef = useRef(false); // Track if we're already stopping to prevent multiple calls
187
186
 
@@ -220,7 +219,6 @@ const LivenessDetectionScreen = () => {
220
219
  .sort(() => Math.random() - 0.5);
221
220
  il.unshift('START');
222
221
  il.push('FINISH');
223
- setInstructionList(il);
224
222
  setInitialState({
225
223
  brightnessLow: false,
226
224
  faceDetected: false,
@@ -598,9 +596,51 @@ const LivenessDetectionScreen = () => {
598
596
  const circleR = previewSizeInFrame / 2;
599
597
  const faceDx = faceCenterX - circleCX;
600
598
  const faceDy = faceCenterY - circleCY;
601
- const previewContainsFace =
602
- faceDx * faceDx + faceDy * faceDy <=
603
- (circleR - faceCoreRadius) * (circleR - faceCoreRadius);
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
+ );
609
+
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;
604
644
  const multipleFacesDetected = faces.length > 1;
605
645
 
606
646
  if (!isImageBright) {
@@ -801,7 +801,7 @@ const VerbalConsentScreen = () => {
801
801
  if (!currentInstructionKey) {
802
802
  return;
803
803
  }
804
- speak(t(currentInstructionKey));
804
+ speak(t(currentInstructionKey)).catch(() => {});
805
805
  }, [
806
806
  appContext.currentWorkflowStep?.data?.voiceGuidanceActive,
807
807
  currentInstructionKey,
@@ -7,13 +7,13 @@ import {
7
7
  TouchableOpacity,
8
8
  View,
9
9
  ActivityIndicator,
10
- PermissionsAndroid,
11
10
  StatusBar,
12
11
  } from 'react-native';
13
12
  import { RTCView, MediaStream } from 'react-native-webrtc';
14
13
  import InCallManager from 'react-native-incall-manager';
15
14
  import LottieView from 'lottie-react-native';
16
15
  import { useTranslation } from 'react-i18next';
16
+ import PermissionManager from '../../Shared/Libs/permissions.utils';
17
17
  import { useStatusBarWhiteBackground } from '../../Shared/Libs/status-bar.utils';
18
18
  import { WebRTCService } from '../../Shared/Services/WebRTCService';
19
19
  import { VideoSessionService } from '../../Shared/Services/VideoSessionService';
@@ -373,25 +373,14 @@ const VideoCallScreen = ({ navigation }: any) => {
373
373
  });
374
374
  serviceRef.current = service;
375
375
 
376
- // Request Permissions
377
- if (Platform.OS === 'android') {
378
- const granted = await PermissionsAndroid.requestMultiple([
379
- PermissionsAndroid.PERMISSIONS.CAMERA,
380
- PermissionsAndroid.PERMISSIONS.RECORD_AUDIO,
381
- ]);
382
- if (
383
- granted['android.permission.CAMERA'] !==
384
- PermissionsAndroid.RESULTS.GRANTED ||
385
- granted['android.permission.RECORD_AUDIO'] !==
386
- PermissionsAndroid.RESULTS.GRANTED
387
- ) {
388
- if (mounted) setConnectionState('permissions_denied');
389
- return;
390
- }
376
+ // Request Camera and Microphone Permissions
377
+ const hasBothPermissions =
378
+ await PermissionManager.requestCameraAndMicrophone();
379
+
380
+ if (!hasBothPermissions) {
381
+ if (mounted) setConnectionState('permissions_denied');
382
+ return;
391
383
  }
392
- // For iOS, react-native-webrtc or VisionCamera usually triggers it,
393
- // but explicit request via library is better if we had one.
394
- // Assuming automatic or previously granted for now.
395
384
 
396
385
  try {
397
386
  const stream = await service.initialize();
@@ -100,10 +100,15 @@ const ResultScreen = () => {
100
100
 
101
101
  const formatGender = useCallback(
102
102
  (code?: string | null) => {
103
+ // The eID/NFC path stores sex as "M"/"F" (ICAO getStrCode), while the
104
+ // camera MRZ path (the `mrz` npm package) stores "male"/"female". Accept
105
+ // both so the ID scan shows the gender instead of "unspecified".
103
106
  switch (code?.toUpperCase()) {
104
107
  case 'M':
108
+ case 'MALE':
105
109
  return t('eidScannerScreen.genderMale');
106
110
  case 'F':
111
+ case 'FEMALE':
107
112
  return t('eidScannerScreen.genderFemale');
108
113
  default:
109
114
  return t('eidScannerScreen.genderUnspecified');
@@ -1055,15 +1060,19 @@ const ResultScreen = () => {
1055
1060
  <Text style={styles.sectionText}>
1056
1061
  {t('resultScreen.demoVideo')}
1057
1062
  </Text>
1058
- <Video
1059
- source={{
1060
- uri: consentVideo.videoPath,
1061
- }}
1062
- resizeMode="contain"
1063
- style={styles.video}
1064
- controls={true}
1065
- muted={false}
1066
- />
1063
+ {consentVideo.videoPath ? (
1064
+ <Video
1065
+ source={{
1066
+ uri: consentVideo.videoPath,
1067
+ }}
1068
+ resizeMode="contain"
1069
+ style={styles.video}
1070
+ controls={true}
1071
+ muted={false}
1072
+ />
1073
+ ) : (
1074
+ <Text style={styles.sectionText}>N/A</Text>
1075
+ )}
1067
1076
  </View>
1068
1077
  )
1069
1078
  )}