@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
@@ -4,99 +4,80 @@ import React, { useState, useRef, useCallback } from 'react';
4
4
  import { View, StyleSheet, Text, ScrollView, StatusBar, TouchableOpacity } from 'react-native';
5
5
  import { SafeAreaView } from 'react-native-safe-area-context';
6
6
  import { TrustchexCamera } from "../../Shared/Components/TrustchexCamera.js";
7
- import mrzUtils from "../../Shared/Libs/mrz.utils.js";
7
+ import { MRZFrameAggregator } from "../../Shared/Libs/mrzFrameAggregator.js";
8
8
  import { useKeepAwake } from "../../Shared/Libs/native-keep-awake.utils.js";
9
+
10
+ // All per-frame display data lives in ONE state object so a processed frame
11
+ // triggers a SINGLE re-render instead of 7 separate setState calls.
9
12
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
13
+ const EMPTY_VIEW = {
14
+ raw: '',
15
+ voted: 'Voting…',
16
+ valid: false,
17
+ frames: 0,
18
+ stable: false
19
+ };
20
+
21
+ // Process the live OCR stream at this cadence. The camera delivers frames faster
22
+ // than the MRZ validation/voting can run; gating here keeps the JS thread free to
23
+ // service touches (so the buttons stay responsive) while still converging fast.
24
+ const MRZ_PROCESS_INTERVAL_MS = 400;
10
25
  const MRZTestScreen = () => {
11
26
  useKeepAwake();
12
27
  const cameraRef = useRef(null);
13
- const [mrzText, setMrzText] = useState('Waiting for MRZ...');
14
- const [mrzRawText, setMrzRawText] = useState('');
15
- const [mrzValid, setMrzValid] = useState(false);
16
- const [rawHistory, setRawHistory] = useState([]);
28
+
29
+ // Single consolidated view-state; one setState per processed frame.
30
+ const [view, setView] = useState(EMPTY_VIEW);
17
31
  const [isPaused, setIsPaused] = useState(false);
18
- const [overlayBlocks, setOverlayBlocks] = useState([]);
19
- const [cameraLayout, setCameraLayout] = useState({
20
- width: 1,
21
- height: 1
22
- });
23
- const [frameSize, setFrameSize] = useState({
24
- width: 1080,
25
- height: 1920
26
- });
27
- const handleCameraLayout = useCallback(e => {
28
- const {
29
- width,
30
- height
31
- } = e.nativeEvent.layout;
32
- setCameraLayout({
33
- width,
34
- height
35
- });
36
- }, []);
32
+
33
+ // Refs for values the frame handler reads but that must NOT re-create it:
34
+ // keeping handleFrame identity-stable avoids re-subscribing the camera and
35
+ // keeps it cheap.
36
+ const pausedRef = useRef(false);
37
+ const lastProcessedAt = useRef(0);
38
+ const aggregator = useRef(new MRZFrameAggregator({
39
+ stabilityTarget: 2
40
+ }));
37
41
  const handleFrame = useCallback(event => {
38
- if (isPaused) {
39
- return;
40
- }
42
+ if (pausedRef.current) return;
43
+
44
+ // Throttle the ENTIRE handler. Drop frames that arrive inside the window
45
+ // before doing ANY work (no parsing, no setState) so touch events land.
46
+ const now = Date.now();
47
+ if (now - lastProcessedAt.current < MRZ_PROCESS_INTERVAL_MS) return;
41
48
  const frame = event.nativeEvent.frame;
42
- if (frame.width && frame.height) {
43
- setFrameSize({
44
- width: frame.width,
45
- height: frame.height
46
- });
47
- }
48
- if (frame.textBlocks && frame.textBlocks.length > 0) {
49
- const frameHeight = frame.height;
50
- const bottomThreshold = frameHeight * 0.5;
51
- const blocks = frame.textBlocks.map(block => {
52
- const blockText = block.text || '';
53
- return {
54
- ...block,
55
- isMrz: blockText.includes('<') && blockText.length >= 12 && (block.blockFrame ? block.blockFrame.y > bottomThreshold : false)
56
- };
57
- });
58
- setOverlayBlocks(blocks);
59
- } else {
60
- setOverlayBlocks([]);
61
- }
62
- if (frame.resultText) {
63
- setRawHistory(prev => {
64
- if (prev[0] === frame.resultText) {
65
- return prev;
66
- }
67
- const next = [frame.resultText, ...prev];
68
- return next.slice(0, 20);
69
- });
70
- }
71
- if (frame.resultText && frame.resultText.includes('<')) {
72
- // Extract MRZ-only text from detected blocks
73
- let mrzOnlyText = '';
74
- if (frame.textBlocks && frame.textBlocks.length > 0) {
75
- // Filter blocks that look like MRZ (contain < and are in bottom half of frame)
76
- const frameHeight = frame.height;
77
- const bottomThreshold = frameHeight * 0.5;
78
- const mrzBlocks = frame.textBlocks.filter(block => {
79
- const blockText = block.text || '';
80
- const hasFillers = blockText.includes('<');
81
- const isInBottomArea = block.blockFrame ? block.blockFrame.y > bottomThreshold : false;
82
- const isLongEnough = blockText.length >= 12;
83
- return hasFillers && isInBottomArea && isLongEnough;
84
- });
85
- if (mrzBlocks.length > 0) {
86
- mrzOnlyText = mrzBlocks.map(b => b.text).join('\n');
87
- }
88
- }
49
+ const resultText = frame.resultText;
50
+ if (!resultText || !resultText.includes('<')) return;
51
+ lastProcessedAt.current = now;
52
+
53
+ // Collect every MRZ-looking block (contains a filler) regardless of
54
+ // position/length — the band is often fragmented and may not sit in the
55
+ // bottom half. Reconstruction downstream isolates it from front-of-card text.
56
+ const mrzBlocks = (frame.textBlocks ?? []).filter(b => (b.text || '').includes('<'));
57
+ const mrzOnlyText = mrzBlocks.length ? mrzBlocks.map(b => b.text).join('\n') : resultText;
58
+ const bandHeight = mrzBlocks.length ? Math.max(0, ...mrzBlocks.map(b => b.blockFrame?.height ?? 0)) : 0;
59
+ const consensus = aggregator.current.addFrame({
60
+ text: `${mrzOnlyText}\n${resultText}`,
61
+ weight: bandHeight > 0 ? bandHeight : 1
62
+ });
89
63
 
90
- // Fallback to full text if no MRZ blocks detected
91
- if (!mrzOnlyText) {
92
- mrzOnlyText = frame.resultText;
93
- }
94
- const fixedMrz = mrzUtils.fixMRZ(mrzOnlyText);
95
- setMrzRawText(mrzOnlyText);
96
- setMrzText(fixedMrz);
97
- setMrzValid(mrzUtils.validateMRZWithCorrections(fixedMrz).valid);
98
- }
99
- }, [isPaused]);
64
+ // One setState one re-render.
65
+ setView({
66
+ raw: mrzOnlyText,
67
+ voted: consensus.mrz ?? `Voting… (${consensus.frames} frames)`,
68
+ valid: !!consensus.validation?.valid,
69
+ frames: consensus.frames,
70
+ stable: consensus.stable
71
+ });
72
+ }, []);
73
+ const togglePause = useCallback(() => {
74
+ pausedRef.current = !pausedRef.current;
75
+ setIsPaused(pausedRef.current);
76
+ }, []);
77
+ const resetVoting = useCallback(() => {
78
+ aggregator.current.reset();
79
+ setView(EMPTY_VIEW);
80
+ }, []);
100
81
  return /*#__PURE__*/_jsxs(View, {
101
82
  style: styles.container,
102
83
  children: [/*#__PURE__*/_jsx(StatusBar, {
@@ -105,7 +86,6 @@ const MRZTestScreen = () => {
105
86
  translucent: true
106
87
  }), /*#__PURE__*/_jsxs(View, {
107
88
  style: styles.cameraWrapper,
108
- onLayout: handleCameraLayout,
109
89
  children: [/*#__PURE__*/_jsx(TrustchexCamera, {
110
90
  ref: cameraRef,
111
91
  style: StyleSheet.absoluteFill,
@@ -115,28 +95,15 @@ const MRZTestScreen = () => {
115
95
  enableTextRecognition: true,
116
96
  enableBarcodeScanning: false,
117
97
  includeBase64: false,
118
- targetFps: 10,
98
+ targetFps: 5,
119
99
  onFrameAvailable: handleFrame
120
- }), overlayBlocks.map((block, i) => {
121
- const bf = block.blockFrame;
122
- if (!bf) return null;
123
- const scaleX = cameraLayout.width / frameSize.width;
124
- const scaleY = cameraLayout.height / frameSize.height;
125
- return /*#__PURE__*/_jsx(View, {
126
- style: [styles.boundingBox, block.isMrz ? styles.boundingBoxMrz : styles.boundingBoxText, {
127
- left: bf.x * scaleX,
128
- top: bf.y * scaleY,
129
- width: bf.width * scaleX,
130
- height: bf.height * scaleY
131
- }],
132
- children: block.isMrz && /*#__PURE__*/_jsx(View, {
133
- style: styles.blockLabel,
134
- children: /*#__PURE__*/_jsx(Text, {
135
- style: styles.blockLabelText,
136
- children: "MRZ"
137
- })
138
- })
139
- }, i);
100
+ }), /*#__PURE__*/_jsx(View, {
101
+ pointerEvents: "none",
102
+ style: styles.scanGuide,
103
+ children: /*#__PURE__*/_jsx(Text, {
104
+ style: styles.scanGuideLabel,
105
+ children: "Align MRZ inside this frame"
106
+ })
140
107
  })]
141
108
  }), /*#__PURE__*/_jsx(SafeAreaView, {
142
109
  style: styles.mrzPanel,
@@ -145,40 +112,47 @@ const MRZTestScreen = () => {
145
112
  style: styles.scrollView,
146
113
  children: /*#__PURE__*/_jsxs(View, {
147
114
  style: styles.panelContent,
148
- children: [/*#__PURE__*/_jsx(TouchableOpacity, {
149
- style: styles.pauseButton,
150
- onPress: () => setIsPaused(!isPaused),
151
- children: /*#__PURE__*/_jsx(Text, {
152
- style: styles.pauseButtonText,
153
- children: isPaused ? 'Resume Processing' : 'Pause Processing'
154
- })
115
+ children: [/*#__PURE__*/_jsxs(View, {
116
+ style: styles.buttonRow,
117
+ children: [/*#__PURE__*/_jsx(TouchableOpacity, {
118
+ style: styles.pauseButton,
119
+ onPress: togglePause,
120
+ children: /*#__PURE__*/_jsx(Text, {
121
+ style: styles.pauseButtonText,
122
+ children: isPaused ? 'Resume Processing' : 'Pause Processing'
123
+ })
124
+ }), /*#__PURE__*/_jsx(TouchableOpacity, {
125
+ style: styles.pauseButton,
126
+ onPress: resetVoting,
127
+ children: /*#__PURE__*/_jsx(Text, {
128
+ style: styles.pauseButtonText,
129
+ children: "Reset Voting"
130
+ })
131
+ })]
155
132
  }), /*#__PURE__*/_jsx(Text, {
156
133
  style: styles.title,
157
134
  children: "MRZ Text Read"
158
- }), /*#__PURE__*/_jsx(Text, {
159
- style: styles.sectionTitle,
160
- children: "MRZ Raw Text"
161
- }), /*#__PURE__*/_jsx(Text, {
162
- style: styles.mrzText,
163
- children: mrzRawText ? mrzRawText.split('\n').map((line, i) => `Line ${i + 1}: ${line} (${line.length} chars)`).join('\n') : 'No MRZ raw text'
164
- }), /*#__PURE__*/_jsx(Text, {
135
+ }), /*#__PURE__*/_jsxs(Text, {
165
136
  style: styles.sectionTitle,
166
- children: "MRZ Corrected Text"
137
+ children: ["\u2605 Multi-frame Voted MRZ \u2014 ", view.frames, " frames", view.valid ? ' · VALID' : '', view.stable ? ' · STABLE' : '']
167
138
  }), /*#__PURE__*/_jsx(Text, {
168
- style: [styles.mrzText, mrzValid ? styles.mrzValid : null],
169
- children: mrzText.split('\n').map((line, i) => `Line ${i + 1}: ${line} (${line.length} chars)`).join('\n')
139
+ style: [styles.mrzText, view.valid ? styles.mrzValid : null],
140
+ children: formatLines(view.voted)
170
141
  }), /*#__PURE__*/_jsx(Text, {
171
142
  style: styles.sectionTitle,
172
- children: "Raw Text History"
143
+ children: "MRZ Raw Text (this frame)"
173
144
  }), /*#__PURE__*/_jsx(Text, {
174
145
  style: styles.mrzText,
175
- children: rawHistory.length > 0 ? rawHistory.map((text, i) => `#${i + 1}: ${text.replace(/\n/g, ' ')}`).join('\n') : 'No history'
146
+ children: view.raw ? formatLines(view.raw) : 'No MRZ raw text'
176
147
  })]
177
148
  })
178
149
  })
179
150
  })]
180
151
  });
181
152
  };
153
+
154
+ /** Render MRZ text as "Line N: <chars> (N chars)" rows. */
155
+ const formatLines = text => text.split('\n').map((line, i) => `Line ${i + 1}: ${line} (${line.length} chars)`).join('\n');
182
156
  const styles = StyleSheet.create({
183
157
  container: {
184
158
  flex: 1,
@@ -188,31 +162,26 @@ const styles = StyleSheet.create({
188
162
  flex: 2,
189
163
  position: 'relative'
190
164
  },
191
- boundingBox: {
165
+ scanGuide: {
192
166
  position: 'absolute',
193
- borderWidth: 1.5
194
- },
195
- boundingBoxText: {
196
- borderColor: 'rgba(255, 255, 255, 0.25)'
197
- },
198
- boundingBoxMrz: {
199
- borderColor: '#FFA500',
167
+ // Mirrors the native OCR crop: top 25% / bottom 12% / 4% side margins.
168
+ top: '25%',
169
+ bottom: '12%',
170
+ left: '4%',
171
+ right: '4%',
200
172
  borderWidth: 2,
201
- backgroundColor: 'rgba(255, 165, 0, 0.07)'
202
- },
203
- blockLabel: {
204
- position: 'absolute',
205
- top: -14,
206
- left: 0,
207
- backgroundColor: '#FFA500',
208
- paddingHorizontal: 4,
209
- paddingVertical: 1,
210
- borderRadius: 2
173
+ borderColor: '#00E5FF',
174
+ borderRadius: 6,
175
+ justifyContent: 'flex-start',
176
+ alignItems: 'center'
211
177
  },
212
- blockLabelText: {
213
- color: '#000000',
214
- fontSize: 8,
215
- fontWeight: 'bold'
178
+ scanGuideLabel: {
179
+ color: '#00E5FF',
180
+ fontSize: 10,
181
+ fontWeight: 'bold',
182
+ marginTop: -16,
183
+ backgroundColor: 'rgba(0,0,0,0.5)',
184
+ paddingHorizontal: 4
216
185
  },
217
186
  mrzPanel: {
218
187
  flex: 1,
@@ -226,6 +195,11 @@ const styles = StyleSheet.create({
226
195
  panelContent: {
227
196
  padding: 10
228
197
  },
198
+ buttonRow: {
199
+ flexDirection: 'row',
200
+ justifyContent: 'center',
201
+ gap: 8
202
+ },
229
203
  pauseButton: {
230
204
  alignSelf: 'center',
231
205
  borderWidth: 1,
@@ -117,7 +117,6 @@ const LivenessDetectionScreen = () => {
117
117
  instruction: t('livenessDetectionScreen.finish')
118
118
  }
119
119
  });
120
- const [instructionList, setInstructionList] = useState([]);
121
120
  const [hasGuideShown, setHasGuideShown] = useState(false);
122
121
  const stoppingRecordingRef = useRef(false); // Track if we're already stopping to prevent multiple calls
123
122
 
@@ -143,7 +142,6 @@ const LivenessDetectionScreen = () => {
143
142
  instruction === 'LOOK_STRAIGHT_AND_BLINK' || !appContext.currentWorkflowStep?.data?.allowedLivenessInstructionTypes || appContext.currentWorkflowStep?.data?.allowedLivenessInstructionTypes.length === 0 || appContext.currentWorkflowStep?.data?.allowedLivenessInstructionTypes.includes(instruction))).sort(() => Math.random() - 0.5);
144
143
  il.unshift('START');
145
144
  il.push('FINISH');
146
- setInstructionList(il);
147
145
  setInitialState({
148
146
  brightnessLow: false,
149
147
  faceDetected: false,
@@ -440,7 +438,30 @@ const LivenessDetectionScreen = () => {
440
438
  const circleR = previewSizeInFrame / 2;
441
439
  const faceDx = faceCenterX - circleCX;
442
440
  const faceDy = faceCenterY - circleCY;
443
- const previewContainsFace = faceDx * faceDx + faceDy * faceDy <= (circleR - faceCoreRadius) * (circleR - faceCoreRadius);
441
+ const yawAbs = Math.abs(face.yawAngle || 0);
442
+ const yawFactor = Math.min(1, yawAbs / TURN_ANGLE_LIMIT);
443
+
444
+ // General bound tolerance (instruction-agnostic): adapt to head pose and
445
+ // face box deformation so containment remains stable across all actions.
446
+ const faceWidthGrowth = Math.max(0, face.bounds.width - face.bounds.height);
447
+
448
+ // On iOS, close faces tend to appear larger in-frame and lateral head
449
+ // turns shift center more aggressively. Apply an iOS-only proximity boost
450
+ // to horizontal tolerance to avoid false outside-circle resets.
451
+ const faceRadiusRatio = Math.min(1, faceCoreRadius / Math.max(1, circleR));
452
+ const iosProximityBoost = Platform.OS === 'ios' ? Math.max(0, faceRadiusRatio - 0.5) : 0;
453
+ const baseToleranceX = previewSizeInFrame * 0.05;
454
+ const yawToleranceX = face.bounds.width * 0.12 * yawFactor;
455
+ const shapeToleranceX = faceWidthGrowth * 0.1;
456
+ const iosToleranceX = face.bounds.width * 0.2 * iosProximityBoost;
457
+ const toleranceX = Math.max(0, baseToleranceX + yawToleranceX + shapeToleranceX + iosToleranceX, face.bounds.width * 0.05);
458
+ const pitchAbs = Math.abs(face.pitchAngle || 0);
459
+ const pitchFactor = Math.min(1, pitchAbs / TURN_ANGLE_LIMIT);
460
+ const toleranceY = Math.max(0, previewSizeInFrame * 0.03 + face.bounds.height * 0.03 * pitchFactor, face.bounds.height * 0.04);
461
+ const allowedRadiusX = Math.max(1, circleR - faceCoreRadius + toleranceX);
462
+ const allowedRadiusY = Math.max(1, circleR - faceCoreRadius + toleranceY);
463
+ const normalizedDistance = faceDx * faceDx / (allowedRadiusX * allowedRadiusX) + faceDy * faceDy / (allowedRadiusY * allowedRadiusY);
464
+ const previewContainsFace = normalizedDistance <= 1;
444
465
  const multipleFacesDetected = faces.length > 1;
445
466
  if (!isImageBright) {
446
467
  // Brightness too low - reset progress if we've started the flow
@@ -575,7 +575,7 @@ const VerbalConsentScreen = () => {
575
575
  if (!currentInstructionKey) {
576
576
  return;
577
577
  }
578
- speak(t(currentInstructionKey));
578
+ speak(t(currentInstructionKey)).catch(() => {});
579
579
  }, [appContext.currentWorkflowStep?.data?.voiceGuidanceActive, currentInstructionKey, hasGuideShown, t]);
580
580
  useEffect(() => {
581
581
  return () => {
@@ -1,11 +1,12 @@
1
1
  "use strict";
2
2
 
3
3
  import React, { useEffect, useState, useRef, useContext } from 'react';
4
- import { Dimensions, Platform, StyleSheet, Text, TouchableOpacity, View, ActivityIndicator, PermissionsAndroid, StatusBar } from 'react-native';
4
+ import { Dimensions, Platform, StyleSheet, Text, TouchableOpacity, View, ActivityIndicator, StatusBar } from 'react-native';
5
5
  import { RTCView } from 'react-native-webrtc';
6
6
  import InCallManager from 'react-native-incall-manager';
7
7
  import LottieView from 'lottie-react-native';
8
8
  import { useTranslation } from 'react-i18next';
9
+ import PermissionManager from "../../Shared/Libs/permissions.utils.js";
9
10
  import { useStatusBarWhiteBackground } from "../../Shared/Libs/status-bar.utils.js";
10
11
  import { WebRTCService } from "../../Shared/Services/WebRTCService.js";
11
12
  import { VideoSessionService } from "../../Shared/Services/VideoSessionService.js";
@@ -316,18 +317,12 @@ const VideoCallScreen = ({
316
317
  });
317
318
  serviceRef.current = service;
318
319
 
319
- // Request Permissions
320
- if (Platform.OS === 'android') {
321
- const granted = await PermissionsAndroid.requestMultiple([PermissionsAndroid.PERMISSIONS.CAMERA, PermissionsAndroid.PERMISSIONS.RECORD_AUDIO]);
322
- if (granted['android.permission.CAMERA'] !== PermissionsAndroid.RESULTS.GRANTED || granted['android.permission.RECORD_AUDIO'] !== PermissionsAndroid.RESULTS.GRANTED) {
323
- if (mounted) setConnectionState('permissions_denied');
324
- return;
325
- }
320
+ // Request Camera and Microphone Permissions
321
+ const hasBothPermissions = await PermissionManager.requestCameraAndMicrophone();
322
+ if (!hasBothPermissions) {
323
+ if (mounted) setConnectionState('permissions_denied');
324
+ return;
326
325
  }
327
- // For iOS, react-native-webrtc or VisionCamera usually triggers it,
328
- // but explicit request via library is better if we had one.
329
- // Assuming automatic or previously granted for now.
330
-
331
326
  try {
332
327
  const stream = await service.initialize();
333
328
  if (mounted) {
@@ -70,10 +70,15 @@ const ResultScreen = () => {
70
70
  }
71
71
  }, []);
72
72
  const formatGender = useCallback(code => {
73
+ // The eID/NFC path stores sex as "M"/"F" (ICAO getStrCode), while the
74
+ // camera MRZ path (the `mrz` npm package) stores "male"/"female". Accept
75
+ // both so the ID scan shows the gender instead of "unspecified".
73
76
  switch (code?.toUpperCase()) {
74
77
  case 'M':
78
+ case 'MALE':
75
79
  return t('eidScannerScreen.genderMale');
76
80
  case 'F':
81
+ case 'FEMALE':
77
82
  return t('eidScannerScreen.genderFemale');
78
83
  default:
79
84
  return t('eidScannerScreen.genderUnspecified');
@@ -695,7 +700,7 @@ const ResultScreen = () => {
695
700
  }), /*#__PURE__*/_jsx(Text, {
696
701
  style: styles.sectionText,
697
702
  children: t('resultScreen.demoVideo')
698
- }), /*#__PURE__*/_jsx(Video, {
703
+ }), consentVideo.videoPath ? /*#__PURE__*/_jsx(Video, {
699
704
  source: {
700
705
  uri: consentVideo.videoPath
701
706
  },
@@ -703,6 +708,9 @@ const ResultScreen = () => {
703
708
  style: styles.video,
704
709
  controls: true,
705
710
  muted: false
711
+ }) : /*#__PURE__*/_jsx(Text, {
712
+ style: styles.sectionText,
713
+ children: "N/A"
706
714
  })]
707
715
  }, `${consentVideo.videoPath}-${index}`))]
708
716
  })]