@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.
- package/android/build.gradle +3 -3
- package/android/src/main/java/com/trustchex/reactnativesdk/camera/TrustchexCameraView.kt +190 -12
- package/ios/Camera/TrustchexCameraView.swift +244 -46
- package/ios/Permission/PermissionModule.m +22 -0
- package/ios/Permission/PermissionModule.swift +67 -0
- package/lib/module/Screens/Debug/MRZTestScreen.js +121 -147
- package/lib/module/Screens/Dynamic/LivenessDetectionScreen.js +24 -3
- package/lib/module/Screens/Dynamic/VerbalConsentScreen.js +1 -1
- package/lib/module/Screens/Dynamic/VideoCallScreen.js +7 -12
- package/lib/module/Screens/Static/ResultScreen.js +9 -1
- package/lib/module/Shared/Components/EIDScanner.js +166 -163
- package/lib/module/Shared/Components/FaceCamera.js +14 -8
- package/lib/module/Shared/Components/IdentityDocumentCamera.js +45 -10
- package/lib/module/Shared/Libs/MRZ_KNOWN_ISSUES.md +112 -0
- package/lib/module/Shared/Libs/PERMISSIONS_MANAGER.md +268 -0
- package/lib/module/Shared/Libs/index.js +20 -0
- package/lib/module/Shared/Libs/mrz.utils.js +570 -16
- package/lib/module/Shared/Libs/mrzFrameAggregator.js +301 -0
- package/lib/module/Shared/Libs/mrzOcrIntegration.js +109 -0
- package/lib/module/Shared/Libs/permissions.utils.js +199 -0
- package/lib/module/Translation/Resources/en.js +1 -0
- package/lib/module/Translation/Resources/tr.js +1 -0
- package/lib/module/version.js +1 -1
- package/lib/typescript/src/Screens/Debug/MRZTestScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Dynamic/LivenessDetectionScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Dynamic/VideoCallScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Static/ResultScreen.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/EIDScanner.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/FaceCamera.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/index.d.ts +20 -0
- package/lib/typescript/src/Shared/Libs/index.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Libs/mrz.utils.d.ts +8 -0
- package/lib/typescript/src/Shared/Libs/mrz.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/mrzFrameAggregator.d.ts +76 -0
- package/lib/typescript/src/Shared/Libs/mrzFrameAggregator.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Libs/mrzOcrIntegration.d.ts +73 -0
- package/lib/typescript/src/Shared/Libs/mrzOcrIntegration.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Libs/permissions.utils.d.ts +58 -0
- package/lib/typescript/src/Shared/Libs/permissions.utils.d.ts.map +1 -0
- package/lib/typescript/src/Translation/Resources/en.d.ts +1 -0
- package/lib/typescript/src/Translation/Resources/en.d.ts.map +1 -1
- package/lib/typescript/src/Translation/Resources/tr.d.ts +1 -0
- package/lib/typescript/src/Translation/Resources/tr.d.ts.map +1 -1
- package/lib/typescript/src/version.d.ts +1 -1
- package/package.json +14 -10
- package/src/Screens/Debug/MRZTestScreen.tsx +137 -166
- package/src/Screens/Dynamic/LivenessDetectionScreen.tsx +45 -5
- package/src/Screens/Dynamic/VerbalConsentScreen.tsx +1 -1
- package/src/Screens/Dynamic/VideoCallScreen.tsx +8 -19
- package/src/Screens/Static/ResultScreen.tsx +18 -9
- package/src/Shared/Components/EIDScanner.tsx +210 -160
- package/src/Shared/Components/FaceCamera.tsx +19 -16
- package/src/Shared/Components/IdentityDocumentCamera.tsx +53 -13
- package/src/Shared/Libs/MRZ_KNOWN_ISSUES.md +112 -0
- package/src/Shared/Libs/PERMISSIONS_MANAGER.md +268 -0
- package/src/Shared/Libs/index.ts +63 -0
- package/src/Shared/Libs/mrz.utils.ts +639 -11
- package/src/Shared/Libs/mrzFrameAggregator.ts +370 -0
- package/src/Shared/Libs/mrzOcrIntegration.ts +175 -0
- package/src/Shared/Libs/permissions.utils.ts +251 -0
- package/src/Translation/Resources/en.ts +1 -0
- package/src/Translation/Resources/tr.ts +1 -0
- 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
|
|
17
|
+
import { MRZFrameAggregator } from '../../Shared/Libs/mrzFrameAggregator';
|
|
20
18
|
import { useKeepAwake } from '../../Shared/Libs/native-keep-awake.utils';
|
|
21
19
|
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
const [
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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 (
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
102
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
[
|
|
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}
|
|
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={
|
|
129
|
+
targetFps={5}
|
|
138
130
|
onFrameAvailable={handleFrame}
|
|
139
131
|
/>
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
<
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
{
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
182
|
-
<Text style={styles.
|
|
183
|
-
{
|
|
184
|
-
|
|
185
|
-
|
|
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.
|
|
194
|
-
|
|
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
|
-
|
|
169
|
+
|
|
170
|
+
<Text style={styles.sectionTitle}>MRZ Raw Text (this frame)</Text>
|
|
203
171
|
<Text style={styles.mrzText}>
|
|
204
|
-
{
|
|
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
|
-
|
|
197
|
+
scanGuide: {
|
|
227
198
|
position: 'absolute',
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
|
|
248
|
-
color: '#
|
|
249
|
-
fontSize:
|
|
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
|
-
|
|
602
|
-
|
|
603
|
-
|
|
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
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
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
|
)}
|