@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
|
@@ -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
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
const [
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
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 (
|
|
39
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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:
|
|
98
|
+
targetFps: 5,
|
|
119
99
|
onFrameAvailable: handleFrame
|
|
120
|
-
}),
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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__*/
|
|
149
|
-
style: styles.
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
children:
|
|
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__*/
|
|
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
|
|
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,
|
|
169
|
-
children:
|
|
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
|
|
143
|
+
children: "MRZ Raw Text (this frame)"
|
|
173
144
|
}), /*#__PURE__*/_jsx(Text, {
|
|
174
145
|
style: styles.mrzText,
|
|
175
|
-
children:
|
|
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
|
-
|
|
165
|
+
scanGuide: {
|
|
192
166
|
position: 'absolute',
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
213
|
-
color: '#
|
|
214
|
-
fontSize:
|
|
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
|
|
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,
|
|
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
|
-
|
|
321
|
-
|
|
322
|
-
if (
|
|
323
|
-
|
|
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
|
})]
|