@trustchex/react-native-sdk 1.475.1 → 1.478.7
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/src/main/java/com/trustchex/reactnativesdk/camera/TrustchexCameraView.kt +19 -21
- package/android/src/main/java/com/trustchex/reactnativesdk/mlkit/MLKitModule.kt +1 -0
- package/lib/module/Screens/Dynamic/LivenessDetectionScreen.js +3 -2
- package/lib/module/Screens/Static/QrCodeScanningScreen.js +12 -2
- package/lib/module/Screens/Static/ResultScreen.js +103 -107
- package/lib/module/Shared/Components/DebugOverlay.js +5 -2
- package/lib/module/Shared/Components/IdentityDocumentCamera.js +53 -22
- package/lib/module/Shared/Components/IdentityDocumentCamera.utils.js +8 -1
- package/lib/module/Shared/EIDReader/eidReader.js +15 -14
- package/lib/module/Shared/Libs/MRZ_KNOWN_ISSUES.md +34 -0
- package/lib/module/Shared/Libs/SignalingClient.js +6 -5
- package/lib/module/Shared/Libs/crypto.utils.js +25 -3
- package/lib/module/Shared/Libs/deeplink.utils.js +37 -7
- package/lib/module/Shared/Libs/http-client.js +19 -6
- package/lib/module/Shared/Libs/mrz.utils.js +193 -33
- package/lib/module/Shared/Libs/mrzFrameAggregator.js +45 -1
- package/lib/module/Shared/Libs/promise.utils.js +4 -3
- package/lib/module/Shared/Libs/shuffle.utils.js +17 -0
- package/lib/module/Shared/Services/AnalyticsService.js +8 -2
- package/lib/module/Shared/Services/DataUploadService.js +114 -127
- package/lib/module/Shared/Services/VideoSessionService.js +4 -3
- 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/Dynamic/LivenessDetectionScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Static/QrCodeScanningScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Static/ResultScreen.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/DebugOverlay.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.utils.d.ts +1 -1
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/EIDReader/eidReader.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/SignalingClient.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/crypto.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/deeplink.utils.d.ts +1 -0
- package/lib/typescript/src/Shared/Libs/deeplink.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/http-client.d.ts +2 -0
- package/lib/typescript/src/Shared/Libs/http-client.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/mrz.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/mrzFrameAggregator.d.ts +22 -0
- package/lib/typescript/src/Shared/Libs/mrzFrameAggregator.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/promise.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/shuffle.utils.d.ts +3 -0
- package/lib/typescript/src/Shared/Libs/shuffle.utils.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Services/AnalyticsService.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Services/DataUploadService.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Services/VideoSessionService.d.ts.map +1 -1
- 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 +13 -3
- package/src/Screens/Dynamic/LivenessDetectionScreen.tsx +4 -3
- package/src/Screens/Static/QrCodeScanningScreen.tsx +19 -2
- package/src/Screens/Static/ResultScreen.tsx +126 -161
- package/src/Shared/Components/DebugOverlay.tsx +5 -2
- package/src/Shared/Components/IdentityDocumentCamera.tsx +55 -22
- package/src/Shared/Components/IdentityDocumentCamera.utils.ts +9 -1
- package/src/Shared/EIDReader/eidReader.ts +23 -14
- package/src/Shared/Libs/MRZ_KNOWN_ISSUES.md +34 -0
- package/src/Shared/Libs/SignalingClient.ts +10 -5
- package/src/Shared/Libs/crypto.utils.ts +41 -4
- package/src/Shared/Libs/deeplink.utils.ts +39 -7
- package/src/Shared/Libs/http-client.ts +24 -8
- package/src/Shared/Libs/mrz.utils.ts +200 -34
- package/src/Shared/Libs/mrzFrameAggregator.ts +53 -1
- package/src/Shared/Libs/promise.utils.ts +11 -5
- package/src/Shared/Libs/shuffle.utils.ts +15 -0
- package/src/Shared/Services/AnalyticsService.ts +17 -1
- package/src/Shared/Services/DataUploadService.ts +155 -166
- package/src/Shared/Services/VideoSessionService.ts +15 -3
- package/src/Translation/Resources/en.ts +1 -0
- package/src/Translation/Resources/tr.ts +1 -0
- package/src/version.ts +1 -1
|
@@ -116,6 +116,7 @@ class TrustchexCameraView(context: ThemedReactContext) : FrameLayout(context) {
|
|
|
116
116
|
// Prevent concurrent ML Kit processing (drop frames while busy)
|
|
117
117
|
private val isProcessing = AtomicBoolean(false)
|
|
118
118
|
|
|
119
|
+
@Volatile
|
|
119
120
|
private var lastFrameTime = 0L
|
|
120
121
|
private val minFrameInterval get() = 1000L / targetFps
|
|
121
122
|
|
|
@@ -164,27 +165,6 @@ class TrustchexCameraView(context: ThemedReactContext) : FrameLayout(context) {
|
|
|
164
165
|
return selectWidestFrontCamera()
|
|
165
166
|
}
|
|
166
167
|
|
|
167
|
-
// For back camera, try to select Ultra Wide camera for document scanning
|
|
168
|
-
val cameraProvider = cameraProvider ?: return CameraSelector.DEFAULT_BACK_CAMERA
|
|
169
|
-
|
|
170
|
-
try {
|
|
171
|
-
// Check for Ultra Wide camera
|
|
172
|
-
val ultraWideSelector = CameraSelector.Builder()
|
|
173
|
-
.requireLensFacing(CameraSelector.LENS_FACING_BACK)
|
|
174
|
-
.build()
|
|
175
|
-
|
|
176
|
-
val cameras = cameraProvider.availableCameraInfos.filter {
|
|
177
|
-
ultraWideSelector.filter(listOf(it)).isNotEmpty()
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// Heuristic: If we have 3+ back cameras, index 2 is often Ultra Wide
|
|
181
|
-
if (cameras.size >= 3) {
|
|
182
|
-
// Heuristic: If we have 3+ back cameras, index 2 is often Ultra Wide
|
|
183
|
-
}
|
|
184
|
-
} catch (e: Exception) {
|
|
185
|
-
// Failed to enumerate cameras
|
|
186
|
-
}
|
|
187
|
-
|
|
188
168
|
return CameraSelector.DEFAULT_BACK_CAMERA
|
|
189
169
|
}
|
|
190
170
|
|
|
@@ -758,6 +738,24 @@ class TrustchexCameraView(context: ThemedReactContext) : FrameLayout(context) {
|
|
|
758
738
|
}
|
|
759
739
|
}
|
|
760
740
|
}
|
|
741
|
+
} else if (uvPixelStride == 1) {
|
|
742
|
+
val vRowStride = vPlane.rowStride
|
|
743
|
+
|
|
744
|
+
for (row in 0 until uvHeight) {
|
|
745
|
+
val uRowOffset = row * uvRowStride
|
|
746
|
+
val vRowOffset = row * vRowStride
|
|
747
|
+
for (col in 0 until uvWidth) {
|
|
748
|
+
val uIndex = uRowOffset + col
|
|
749
|
+
val vIndex = vRowOffset + col
|
|
750
|
+
if (vIndex < vBuffer.capacity() && uIndex < uBuffer.capacity()) {
|
|
751
|
+
nv21[nv21Offset++] = vBuffer.get(vIndex)
|
|
752
|
+
nv21[nv21Offset++] = uBuffer.get(uIndex)
|
|
753
|
+
} else {
|
|
754
|
+
nv21[nv21Offset++] = 128.toByte()
|
|
755
|
+
nv21[nv21Offset++] = 128.toByte()
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
}
|
|
761
759
|
}
|
|
762
760
|
|
|
763
761
|
// Convert YUV to JPEG with optimized quality (75% for faster encoding)
|
|
@@ -233,6 +233,7 @@ class MLKitModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaM
|
|
|
233
233
|
private fun base64ToInputImage(base64: String): InputImage {
|
|
234
234
|
val imageBytes = Base64.decode(base64, Base64.NO_WRAP)
|
|
235
235
|
val bitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
|
|
236
|
+
?: throw IllegalArgumentException("Failed to decode image data")
|
|
236
237
|
return InputImage.fromBitmap(bitmap, 0)
|
|
237
238
|
}
|
|
238
239
|
|
|
@@ -13,6 +13,7 @@ import { useTranslation } from 'react-i18next';
|
|
|
13
13
|
import StyledButton from "../../Shared/Components/StyledButton.js";
|
|
14
14
|
import LottieView from 'lottie-react-native';
|
|
15
15
|
import { speak, resetLastMessage } from "../../Shared/Libs/tts.utils.js";
|
|
16
|
+
import { secureShuffle } from "../../Shared/Libs/shuffle.utils.js";
|
|
16
17
|
import { useStatusBarWhiteBackground } from "../../Shared/Libs/status-bar.utils.js";
|
|
17
18
|
import { trackFunnelStep, useScreenTracking, trackVerificationStart, trackVerificationComplete } from "../../Shared/Libs/analytics.utils.js";
|
|
18
19
|
import { useKeepAwake } from "../../Shared/Libs/native-keep-awake.utils.js";
|
|
@@ -137,9 +138,9 @@ const LivenessDetectionScreen = () => {
|
|
|
137
138
|
}
|
|
138
139
|
}, [hasGuideShown]);
|
|
139
140
|
useEffect(() => {
|
|
140
|
-
const il = Object.keys(instructions).filter(instruction => !['START', 'FINISH'].includes(instruction) && (
|
|
141
|
+
const il = secureShuffle(Object.keys(instructions).filter(instruction => !['START', 'FINISH'].includes(instruction) && (
|
|
141
142
|
// Look straight and blink is always included
|
|
142
|
-
instruction === 'LOOK_STRAIGHT_AND_BLINK' || !appContext.currentWorkflowStep?.data?.allowedLivenessInstructionTypes || appContext.currentWorkflowStep?.data?.allowedLivenessInstructionTypes.length === 0 || appContext.currentWorkflowStep?.data?.allowedLivenessInstructionTypes.includes(instruction)))
|
|
143
|
+
instruction === 'LOOK_STRAIGHT_AND_BLINK' || !appContext.currentWorkflowStep?.data?.allowedLivenessInstructionTypes || appContext.currentWorkflowStep?.data?.allowedLivenessInstructionTypes.length === 0 || appContext.currentWorkflowStep?.data?.allowedLivenessInstructionTypes.includes(instruction))));
|
|
143
144
|
il.unshift('START');
|
|
144
145
|
il.push('FINISH');
|
|
145
146
|
setInitialState({
|
|
@@ -5,7 +5,8 @@ import { SafeAreaView, StyleSheet } from 'react-native';
|
|
|
5
5
|
import AppContext from "../../Shared/Contexts/AppContext.js";
|
|
6
6
|
import QrCodeScannerCamera from "../../Shared/Components/QrCodeScannerCamera.js";
|
|
7
7
|
import { useNavigation } from '@react-navigation/native';
|
|
8
|
-
import { handleDeepLink } from "../../Shared/Libs/deeplink.utils.js";
|
|
8
|
+
import { handleDeepLink, getUrlHost } from "../../Shared/Libs/deeplink.utils.js";
|
|
9
|
+
import { logWarn } from "../../Shared/Libs/debug.utils.js";
|
|
9
10
|
import { useKeepAwake } from "../../Shared/Libs/native-keep-awake.utils.js";
|
|
10
11
|
import { useStatusBarWhiteBackground } from "../../Shared/Libs/status-bar.utils.js";
|
|
11
12
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
@@ -17,9 +18,18 @@ const QrCodeScanningScreen = () => {
|
|
|
17
18
|
// Configure status bar for white background
|
|
18
19
|
useStatusBarWhiteBackground();
|
|
19
20
|
const onQrCodeScanned = data => {
|
|
20
|
-
|
|
21
|
+
let [bUrl, sId] = handleDeepLink({
|
|
21
22
|
url: data
|
|
22
23
|
});
|
|
24
|
+
|
|
25
|
+
// A scanned QR may not repoint an already-configured backend to another
|
|
26
|
+
// host — uploads carry identity documents and biometrics. Keep the
|
|
27
|
+
// configured baseUrl and only take the session id from the QR.
|
|
28
|
+
const configuredBaseUrl = appContext.baseUrl;
|
|
29
|
+
if (bUrl && configuredBaseUrl && getUrlHost(bUrl) !== getUrlHost(configuredBaseUrl)) {
|
|
30
|
+
logWarn('QrCodeScanningScreen', 'Ignoring QR-provided baseUrl with mismatched host');
|
|
31
|
+
bUrl = configuredBaseUrl;
|
|
32
|
+
}
|
|
23
33
|
if (bUrl && sId) {
|
|
24
34
|
if (appContext.setBaseUrl) {
|
|
25
35
|
appContext.setBaseUrl(bUrl);
|
|
@@ -4,7 +4,7 @@ import React, { useCallback, useContext, useEffect, useMemo, useState } from 're
|
|
|
4
4
|
import { Alert, Image, Platform, SafeAreaView, ScrollView, StyleSheet, Text, View } from 'react-native';
|
|
5
5
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
6
6
|
import AppContext from "../../Shared/Contexts/AppContext.js";
|
|
7
|
-
import httpClient from "../../Shared/Libs/http-client.js";
|
|
7
|
+
import httpClient, { getSessionToken } from "../../Shared/Libs/http-client.js";
|
|
8
8
|
import RNFS from 'react-native-fs';
|
|
9
9
|
import NativeProgressBar from "../../Shared/Components/NativeProgressBar.js";
|
|
10
10
|
import LottieView from 'lottie-react-native';
|
|
@@ -185,130 +185,122 @@ const ResultScreen = () => {
|
|
|
185
185
|
console.log('[ResultScreen] Media already uploaded during video call, skipping');
|
|
186
186
|
return;
|
|
187
187
|
}
|
|
188
|
+
const uploadSessionToken = getSessionToken();
|
|
188
189
|
const uploadFileOptions = {
|
|
189
190
|
toUrl: `${apiUrl}/identifications/${identificationId}/media`,
|
|
190
191
|
method: 'POST',
|
|
191
192
|
headers: {
|
|
192
|
-
Accept: 'application/json'
|
|
193
|
+
Accept: 'application/json',
|
|
194
|
+
...(uploadSessionToken ? {
|
|
195
|
+
'X-Session-Token': uploadSessionToken
|
|
196
|
+
} : {})
|
|
193
197
|
},
|
|
194
198
|
files: [],
|
|
195
199
|
progress: res => {
|
|
196
200
|
setProgress(60 + res.totalBytesSent / res.totalBytesExpectedToSend * 30);
|
|
197
201
|
}
|
|
198
202
|
};
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
});
|
|
208
|
-
}
|
|
209
|
-
const backImage = scannedIdentityDocument?.backImage;
|
|
210
|
-
if (backImage && backImage !== '') {
|
|
211
|
-
await RNFS.writeFile(decodeURIComponent(`${RNFS.TemporaryDirectoryPath}/DOCUMENT_BACK_IMAGE.jpg`), backImage, 'base64');
|
|
212
|
-
uploadFileOptions.files.push({
|
|
213
|
-
name: 'files',
|
|
214
|
-
filename: 'DOCUMENT_BACK_IMAGE.jpg',
|
|
215
|
-
filepath: decodeURIComponent(`${RNFS.TemporaryDirectoryPath}/DOCUMENT_BACK_IMAGE.jpg`),
|
|
216
|
-
filetype: 'image/jpeg'
|
|
217
|
-
});
|
|
218
|
-
}
|
|
219
|
-
const faceImage = scannedIdentityDocument?.faceImage;
|
|
220
|
-
if (faceImage && faceImage !== '') {
|
|
221
|
-
scannedIdentityDocument.faceImageMimeType ||= 'image/jpeg';
|
|
222
|
-
const extension = scannedIdentityDocument.faceImageMimeType === 'image/jp2' ? 'jp2' : scannedIdentityDocument.faceImageMimeType === 'image/png' ? 'png' : 'jpg';
|
|
223
|
-
await RNFS.writeFile(decodeURIComponent(`${RNFS.TemporaryDirectoryPath}/FACE_IMAGE.${extension}`), faceImage, 'base64');
|
|
224
|
-
uploadFileOptions.files.push({
|
|
225
|
-
name: 'files',
|
|
226
|
-
filename: `FACE_IMAGE.${extension}`,
|
|
227
|
-
filepath: decodeURIComponent(`${RNFS.TemporaryDirectoryPath}/FACE_IMAGE.${extension}`),
|
|
228
|
-
filetype: scannedIdentityDocument.faceImageMimeType
|
|
229
|
-
});
|
|
230
|
-
}
|
|
231
|
-
const secondaryFaceImage = scannedIdentityDocument?.secondaryFaceImage;
|
|
232
|
-
if (secondaryFaceImage && secondaryFaceImage !== '') {
|
|
233
|
-
await RNFS.writeFile(decodeURIComponent(`${RNFS.TemporaryDirectoryPath}/DOCUMENT_SECONDARY_FACE_IMAGE.jpg`), secondaryFaceImage, 'base64');
|
|
234
|
-
uploadFileOptions.files.push({
|
|
235
|
-
name: 'files',
|
|
236
|
-
filename: 'DOCUMENT_SECONDARY_FACE_IMAGE.jpg',
|
|
237
|
-
filepath: decodeURIComponent(`${RNFS.TemporaryDirectoryPath}/DOCUMENT_SECONDARY_FACE_IMAGE.jpg`),
|
|
238
|
-
filetype: 'image/jpeg'
|
|
239
|
-
});
|
|
240
|
-
}
|
|
241
|
-
const hologramImage = scannedIdentityDocument?.hologramImage;
|
|
242
|
-
if (hologramImage && hologramImage !== '') {
|
|
243
|
-
await RNFS.writeFile(decodeURIComponent(`${RNFS.TemporaryDirectoryPath}/DOCUMENT_HOLOGRAM_IMAGE.jpg`), hologramImage, 'base64');
|
|
203
|
+
|
|
204
|
+
// Plaintext identity images/video staged on disk for the multipart
|
|
205
|
+
// upload; everything tracked here is deleted in the finally below.
|
|
206
|
+
const tempPaths = [];
|
|
207
|
+
const writeTempImage = async (fileName, base64Data, filetype = 'image/jpeg') => {
|
|
208
|
+
const filePath = decodeURIComponent(`${RNFS.TemporaryDirectoryPath}/${fileName}`);
|
|
209
|
+
await RNFS.writeFile(filePath, base64Data, 'base64');
|
|
210
|
+
tempPaths.push(filePath);
|
|
244
211
|
uploadFileOptions.files.push({
|
|
245
212
|
name: 'files',
|
|
246
|
-
filename:
|
|
247
|
-
filepath:
|
|
248
|
-
filetype
|
|
213
|
+
filename: fileName,
|
|
214
|
+
filepath: filePath,
|
|
215
|
+
filetype
|
|
249
216
|
});
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
217
|
+
return filePath;
|
|
218
|
+
};
|
|
219
|
+
try {
|
|
220
|
+
const frontImage = scannedIdentityDocument?.frontImage;
|
|
221
|
+
if (frontImage && frontImage !== '') {
|
|
222
|
+
await writeTempImage('DOCUMENT_FRONT_IMAGE.jpg', frontImage);
|
|
223
|
+
}
|
|
224
|
+
const backImage = scannedIdentityDocument?.backImage;
|
|
225
|
+
if (backImage && backImage !== '') {
|
|
226
|
+
await writeTempImage('DOCUMENT_BACK_IMAGE.jpg', backImage);
|
|
227
|
+
}
|
|
228
|
+
const faceImage = scannedIdentityDocument?.faceImage;
|
|
229
|
+
if (faceImage && faceImage !== '') {
|
|
230
|
+
scannedIdentityDocument.faceImageMimeType ||= 'image/jpeg';
|
|
231
|
+
const extension = scannedIdentityDocument.faceImageMimeType === 'image/jp2' ? 'jp2' : scannedIdentityDocument.faceImageMimeType === 'image/png' ? 'png' : 'jpg';
|
|
232
|
+
await writeTempImage(`FACE_IMAGE.${extension}`, faceImage, scannedIdentityDocument.faceImageMimeType);
|
|
233
|
+
}
|
|
234
|
+
const secondaryFaceImage = scannedIdentityDocument?.secondaryFaceImage;
|
|
235
|
+
if (secondaryFaceImage && secondaryFaceImage !== '') {
|
|
236
|
+
await writeTempImage('DOCUMENT_SECONDARY_FACE_IMAGE.jpg', secondaryFaceImage);
|
|
237
|
+
}
|
|
238
|
+
const hologramImage = scannedIdentityDocument?.hologramImage;
|
|
239
|
+
if (hologramImage && hologramImage !== '') {
|
|
240
|
+
await writeTempImage('DOCUMENT_HOLOGRAM_IMAGE.jpg', hologramImage);
|
|
241
|
+
}
|
|
242
|
+
if (livenessDetection) {
|
|
243
|
+
for (const instruction of livenessDetection.instructions) {
|
|
244
|
+
if (instruction.photo !== '') {
|
|
245
|
+
const filePath = await writeTempImage(`LIVENESS_${instruction.instruction}_IMAGE.jpg`, instruction.photo);
|
|
246
|
+
if (instruction.instruction === 'LOOK_STRAIGHT_AND_BLINK') {
|
|
247
|
+
uploadFileOptions.files.push({
|
|
248
|
+
name: 'files',
|
|
249
|
+
filename: 'SELFIE_IMAGE.jpg',
|
|
250
|
+
filepath: filePath,
|
|
251
|
+
filetype: 'image/jpeg'
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
const livenessVideoPath = livenessDetection.videoPath;
|
|
257
|
+
if (livenessVideoPath) {
|
|
258
|
+
let videoFilePath;
|
|
259
|
+
if (Platform.OS === 'ios') {
|
|
260
|
+
const tmpTimestamp = new Date().getTime();
|
|
261
|
+
const tempDir = `${RNFS.TemporaryDirectoryPath}/${tmpTimestamp}`;
|
|
262
|
+
await RNFS.mkdir(tempDir);
|
|
263
|
+
tempPaths.push(tempDir);
|
|
264
|
+
videoFilePath = `${tempDir}/LIVENESS_VIDEO.mp4`;
|
|
265
|
+
} else {
|
|
266
|
+
videoFilePath = `${RNFS.TemporaryDirectoryPath}/LIVENESS_VIDEO.mp4`;
|
|
267
|
+
tempPaths.push(videoFilePath);
|
|
268
|
+
}
|
|
269
|
+
await RNFS.copyFile(livenessVideoPath, videoFilePath);
|
|
270
|
+
|
|
271
|
+
// Compress video with maximum compression settings
|
|
272
|
+
const compressedVideoPath = await VideoCompressor.compress(videoFilePath, {
|
|
273
|
+
compressionMethod: 'manual',
|
|
274
|
+
bitrate: 500000,
|
|
275
|
+
// 500 kbps
|
|
276
|
+
maxSize: 1280,
|
|
277
|
+
// HD 720p
|
|
278
|
+
minimumFileSizeForCompress: 0 // Always compress
|
|
279
|
+
}, compressionProgress => {
|
|
280
|
+
// Map compression progress (0-1) to main progress (40-60%)
|
|
281
|
+
setProgress(40 + compressionProgress * 20);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
// Convert file:// URI to regular path for RNFS upload
|
|
285
|
+
const normalizedPath = compressedVideoPath.replace('file://', '');
|
|
286
|
+
tempPaths.push(normalizedPath);
|
|
255
287
|
uploadFileOptions.files.push({
|
|
256
288
|
name: 'files',
|
|
257
|
-
filename:
|
|
258
|
-
filepath:
|
|
259
|
-
filetype: '
|
|
289
|
+
filename: 'LIVENESS_VIDEO.mp4',
|
|
290
|
+
filepath: normalizedPath,
|
|
291
|
+
filetype: 'video/mp4'
|
|
260
292
|
});
|
|
261
|
-
if (instruction.instruction === 'LOOK_STRAIGHT_AND_BLINK') {
|
|
262
|
-
uploadFileOptions.files.push({
|
|
263
|
-
name: 'files',
|
|
264
|
-
filename: 'SELFIE_IMAGE.jpg',
|
|
265
|
-
filepath: decodeURIComponent(`${RNFS.TemporaryDirectoryPath}/LIVENESS_${instruction.instruction}_IMAGE.jpg`),
|
|
266
|
-
filetype: 'image/jpeg'
|
|
267
|
-
});
|
|
268
|
-
}
|
|
269
293
|
}
|
|
270
294
|
}
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
let videoFilePath;
|
|
274
|
-
if (Platform.OS === 'ios') {
|
|
275
|
-
const tmpTimestamp = new Date().getTime();
|
|
276
|
-
await RNFS.mkdir(`${RNFS.TemporaryDirectoryPath}/${tmpTimestamp}`);
|
|
277
|
-
videoFilePath = `${RNFS.TemporaryDirectoryPath}/${tmpTimestamp}/LIVENESS_VIDEO.mp4`;
|
|
278
|
-
} else {
|
|
279
|
-
videoFilePath = `${RNFS.TemporaryDirectoryPath}/LIVENESS_VIDEO.mp4`;
|
|
280
|
-
}
|
|
281
|
-
await RNFS.copyFile(livenessVideoPath, videoFilePath);
|
|
282
|
-
|
|
283
|
-
// Compress video with maximum compression settings
|
|
284
|
-
const compressedVideoPath = await VideoCompressor.compress(videoFilePath, {
|
|
285
|
-
compressionMethod: 'manual',
|
|
286
|
-
bitrate: 500000,
|
|
287
|
-
// 500 kbps
|
|
288
|
-
maxSize: 1280,
|
|
289
|
-
// HD 720p
|
|
290
|
-
minimumFileSizeForCompress: 0 // Always compress
|
|
291
|
-
}, compressionProgress => {
|
|
292
|
-
// Map compression progress (0-1) to main progress (40-60%)
|
|
293
|
-
setProgress(40 + compressionProgress * 20);
|
|
294
|
-
});
|
|
295
|
-
|
|
296
|
-
// Convert file:// URI to regular path for RNFS upload
|
|
297
|
-
const normalizedPath = compressedVideoPath.replace('file://', '');
|
|
298
|
-
uploadFileOptions.files.push({
|
|
299
|
-
name: 'files',
|
|
300
|
-
filename: 'LIVENESS_VIDEO.mp4',
|
|
301
|
-
filepath: normalizedPath,
|
|
302
|
-
filetype: 'video/mp4'
|
|
303
|
-
});
|
|
295
|
+
if (uploadFileOptions.files.length === 0) {
|
|
296
|
+
return;
|
|
304
297
|
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
throw new Error('Media upload failed');
|
|
298
|
+
const response = await RNFS.uploadFiles(uploadFileOptions).promise;
|
|
299
|
+
if (![200, 201, 204].includes(response.statusCode)) {
|
|
300
|
+
throw new Error('Media upload failed');
|
|
301
|
+
}
|
|
302
|
+
} finally {
|
|
303
|
+
await Promise.all(tempPaths.map(path => RNFS.unlink(path).catch(() => {})));
|
|
312
304
|
}
|
|
313
305
|
}, [apiUrl]);
|
|
314
306
|
const uploadVerbalConsentVideos = useCallback(async (identificationId, verbalConsentVideos) => {
|
|
@@ -335,11 +327,15 @@ const ResultScreen = () => {
|
|
|
335
327
|
minimumFileSizeForCompress: 0 // Always compress
|
|
336
328
|
});
|
|
337
329
|
const compressedNormalizedPath = compressedVideoPath.replace('file://', '');
|
|
330
|
+
const consentSessionToken = getSessionToken();
|
|
338
331
|
const response = await RNFS.uploadFiles({
|
|
339
332
|
toUrl: `${apiUrl}/identifications/${identificationId}/media`,
|
|
340
333
|
method: 'POST',
|
|
341
334
|
headers: {
|
|
342
|
-
Accept: 'application/json'
|
|
335
|
+
Accept: 'application/json',
|
|
336
|
+
...(consentSessionToken ? {
|
|
337
|
+
'X-Session-Token': consentSessionToken
|
|
338
|
+
} : {})
|
|
343
339
|
},
|
|
344
340
|
fields: {
|
|
345
341
|
consentTitle: consentVideo.title,
|
|
@@ -518,13 +518,16 @@ export const TestModePanel = /*#__PURE__*/React.memo(function TestModePanel({
|
|
|
518
518
|
},
|
|
519
519
|
children: [/*#__PURE__*/_jsx(TextView, {
|
|
520
520
|
style: {
|
|
521
|
-
|
|
521
|
+
// Green "GO" header: the panel only ever receives a STABLE + valid
|
|
522
|
+
// consensus MRZ (the caller withholds it until then), so showing it
|
|
523
|
+
// here IS the reliable-detection signal.
|
|
524
|
+
color: '#4CAF50',
|
|
522
525
|
fontSize: 12,
|
|
523
526
|
fontWeight: 'bold',
|
|
524
527
|
marginBottom: 8,
|
|
525
528
|
textAlign: 'center'
|
|
526
529
|
},
|
|
527
|
-
children: "MRZ
|
|
530
|
+
children: "\u2713 MRZ STABLE \u2014 GO"
|
|
528
531
|
}), /*#__PURE__*/_jsx(TextView, {
|
|
529
532
|
style: {
|
|
530
533
|
color: '#FFFFFF',
|
|
@@ -22,7 +22,7 @@ import { useTheme } from "../Contexts/ThemeContext.js";
|
|
|
22
22
|
import DebugOverlay, { TestModePanel } from "./DebugOverlay.js";
|
|
23
23
|
import { getStatusMessage, getFrameToScreenTransform, transformBoundsToScreen, getScanAreaBounds, angleBetweenPoints, detectDocumentType, determineDocumentTypeToSet, areMRZFieldsEqual, hasRequiredMRZFields, validateFacePosition } from "./IdentityDocumentCamera.utils.js";
|
|
24
24
|
import { handlePassportFlow, handleIDFrontFlow, handleIDBackFlow, getNextStepAfterHologram } from "./IdentityDocumentCamera.flows.js";
|
|
25
|
-
import { HOLOGRAM_IMAGE_COUNT, HOLOGRAM_DETECTION_THRESHOLD, HOLOGRAM_DETECTION_RETRY_COUNT, HOLOGRAM_CAPTURE_INTERVAL, HOLOGRAM_MAX_FRAMES_WITHOUT_FACE, MIN_BRIGHTNESS_THRESHOLD, MAX_BRIGHTNESS_THRESHOLD, FACE_EDGE_MARGIN_PERCENT, MAX_CONSECUTIVE_QUALITY_FAILURES,
|
|
25
|
+
import { HOLOGRAM_IMAGE_COUNT, HOLOGRAM_DETECTION_THRESHOLD, HOLOGRAM_DETECTION_RETRY_COUNT, HOLOGRAM_CAPTURE_INTERVAL, HOLOGRAM_MAX_FRAMES_WITHOUT_FACE, MIN_BRIGHTNESS_THRESHOLD, MAX_BRIGHTNESS_THRESHOLD, FACE_EDGE_MARGIN_PERCENT, MAX_CONSECUTIVE_QUALITY_FAILURES, REQUIRED_CONSISTENT_DOCTYPE_DETECTIONS, SIGNATURE_TEXT_REGEX, MRZ_BLOCK_PATTERN, PASSPORT_MRZ_PATTERN, MIN_CARD_FACE_SIZE_PERCENT } from "./IdentityDocumentCamera.constants.js";
|
|
26
26
|
|
|
27
27
|
// Re-export types for backward compatibility
|
|
28
28
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
@@ -59,6 +59,11 @@ const IdentityDocumentCamera = ({
|
|
|
59
59
|
const [isFrameBlurry, setIsFrameBlurry] = useState(false);
|
|
60
60
|
const [hasGuideShown, setHasGuideShown] = useState(false);
|
|
61
61
|
const [status, setStatus] = useState('SEARCHING');
|
|
62
|
+
// True once the multi-frame voted MRZ consensus is STABLE + valid (the same
|
|
63
|
+
// reading matched across `stabilityTarget` consecutive frames, with a clear
|
|
64
|
+
// vote margin on contested cells). Drives the green "GO" signal — we never show
|
|
65
|
+
// green on an early/transient valid read, only on a reliably-detected MRZ.
|
|
66
|
+
const [mrzReliable, setMrzReliable] = useState(false);
|
|
62
67
|
const [nextStep, setNextStep] = useState('SCAN_ID_FRONT_OR_PASSPORT');
|
|
63
68
|
const [_completedStep, setCompletedStep] = useState(null);
|
|
64
69
|
const [detectedDocumentType, setDetectedDocumentType] = useState('UNKNOWN');
|
|
@@ -74,8 +79,11 @@ const IdentityDocumentCamera = ({
|
|
|
74
79
|
// Multi-frame per-character voting: fuses OCR across frames so a single-frame
|
|
75
80
|
// misread (B↔8, S↔5, <↔K, dropped/inserted digit) is outvoted instead of
|
|
76
81
|
// resetting progress. Drives the same `parsedMRZData`/stability the flow uses.
|
|
82
|
+
// stabilityTarget: 3 — the voted consensus must read the SAME valid MRZ across
|
|
83
|
+
// 3 consecutive contributing frames before we accept, so a transient look-alike
|
|
84
|
+
// misread (e.g. OCR-B "4" vs "A") can never lock in on one or two early frames.
|
|
77
85
|
const mrzAggregator = useRef(new MRZFrameAggregator({
|
|
78
|
-
stabilityTarget:
|
|
86
|
+
stabilityTarget: 3
|
|
79
87
|
}));
|
|
80
88
|
|
|
81
89
|
// Document type stability tracking - require consistent detections from good quality frames
|
|
@@ -157,6 +165,7 @@ const IdentityDocumentCamera = ({
|
|
|
157
165
|
lastValidMRZFields.current = null;
|
|
158
166
|
validMRZConsecutiveCount.current = 0;
|
|
159
167
|
mrzAggregator.current.reset();
|
|
168
|
+
setMrzReliable(false);
|
|
160
169
|
cachedBarcode.current = null;
|
|
161
170
|
isCompletionCallbackInvoked.current = false;
|
|
162
171
|
isStepTransitionInProgress.current = false;
|
|
@@ -386,6 +395,7 @@ const IdentityDocumentCamera = ({
|
|
|
386
395
|
lastValidMRZFields.current = null;
|
|
387
396
|
// New MRZ expected on the back — start voting fresh.
|
|
388
397
|
mrzAggregator.current.reset();
|
|
398
|
+
setMrzReliable(false);
|
|
389
399
|
}
|
|
390
400
|
validMRZConsecutiveCount.current = 0;
|
|
391
401
|
cachedBarcode.current = null; // Clear cached barcode on step change
|
|
@@ -650,15 +660,6 @@ const IdentityDocumentCamera = ({
|
|
|
650
660
|
// During SCAN_HOLOGRAM, allow processing even if text is not readable
|
|
651
661
|
}
|
|
652
662
|
|
|
653
|
-
// Capture test mode data
|
|
654
|
-
if (testMode && text && text.includes('<')) {
|
|
655
|
-
const mrzOnlyText = scannedText?.mrzOnlyText || text;
|
|
656
|
-
setTestModeData({
|
|
657
|
-
mrzText: mrzOnlyText,
|
|
658
|
-
timestamp: Date.now()
|
|
659
|
-
});
|
|
660
|
-
}
|
|
661
|
-
|
|
662
663
|
// MRZ stability: require consistent valid reads across frames
|
|
663
664
|
// Skip during SCAN_HOLOGRAM - document type already locked
|
|
664
665
|
const mrzHasRequiredFields = hasRequiredMRZFields(parsedMRZData?.fields);
|
|
@@ -674,12 +675,37 @@ const IdentityDocumentCamera = ({
|
|
|
674
675
|
}
|
|
675
676
|
// else: Invalid/no MRZ - skip frame without resetting (allows temporary OCR noise)
|
|
676
677
|
|
|
677
|
-
//
|
|
678
|
-
|
|
679
|
-
//
|
|
680
|
-
|
|
681
|
-
//
|
|
682
|
-
|
|
678
|
+
// Accept ONLY when the multi-frame voted consensus is stable.
|
|
679
|
+
//
|
|
680
|
+
// Early frames can OCR an ambiguous glyph wrongly yet still produce a
|
|
681
|
+
// check-digit-VALID reading — e.g. a Turkish document number "A53032352"
|
|
682
|
+
// misread as "453032352" (the OCR-B 4 looks like A, and 453032352 also
|
|
683
|
+
// passes its check digit). The old code also accepted on a legacy path of
|
|
684
|
+
// "N identical valid single-frame reads", which those transient frames
|
|
685
|
+
// satisfy — locking in the WRONG number before the voted consensus settles
|
|
686
|
+
// on the correct one. We now require `consensus.stable`: the per-character
|
|
687
|
+
// vote must be unchanged across `stabilityTarget` consecutive contributing
|
|
688
|
+
// frames, which is exactly the "only accept once stable" guarantee.
|
|
689
|
+
// `addFrame` runs for every frame above, so the consensus is always current.
|
|
690
|
+
const mrzStableAndValid = consensus.stable && parsedMRZData?.valid === true;
|
|
691
|
+
|
|
692
|
+
// Surface the reliable-detection signal to the render so the UI can show a
|
|
693
|
+
// green "GO" the moment (and only once) the consensus is stable + valid.
|
|
694
|
+
// Never flips green on an early/transient valid read.
|
|
695
|
+
setMrzReliable(prev => prev === mrzStableAndValid ? prev : mrzStableAndValid);
|
|
696
|
+
|
|
697
|
+
// Test-mode panel: show the MRZ ONLY once it is stable + valid (the same
|
|
698
|
+
// consensus matched across `stabilityTarget` consecutive frames). Showing
|
|
699
|
+
// the raw per-frame OCR here would flash transient, possibly mis-converted
|
|
700
|
+
// readings (e.g. a momentary "4" for "A") before the vote settles — the
|
|
701
|
+
// opposite of what the screen should convey. We display the corrected,
|
|
702
|
+
// check-digit-valid consensus and never an unstable intermediate.
|
|
703
|
+
if (testMode && mrzStableAndValid && mrzText) {
|
|
704
|
+
setTestModeData(prev => prev?.mrzText === mrzText ? prev : {
|
|
705
|
+
mrzText,
|
|
706
|
+
timestamp: Date.now()
|
|
707
|
+
});
|
|
708
|
+
}
|
|
683
709
|
|
|
684
710
|
// ============================================================================
|
|
685
711
|
// SCAN_ID_BACK STEP - Validate MRZ + barcode on back of ID card
|
|
@@ -1635,8 +1661,10 @@ const IdentityDocumentCamera = ({
|
|
|
1635
1661
|
}), /*#__PURE__*/_jsx(AnimatedText, {
|
|
1636
1662
|
style: [styles.topZoneText,
|
|
1637
1663
|
// Priority order for coloring (later styles override earlier ones)
|
|
1638
|
-
// 1. Success (green) - scan completed
|
|
1639
|
-
|
|
1664
|
+
// 1. Success/GO (green) - scan completed OR MRZ reliably detected
|
|
1665
|
+
// (stable consensus). This is the "GO" signal; never shown on a
|
|
1666
|
+
// transient/early valid read.
|
|
1667
|
+
(nextStep === 'COMPLETED' || mrzReliable) && styles.topZoneTextSuccess,
|
|
1640
1668
|
// 2. Error (red) - wrong side - with flash opacity
|
|
1641
1669
|
status === 'INCORRECT' && styles.topZoneTextError, status === 'INCORRECT' && {
|
|
1642
1670
|
opacity: errorFlashAnim
|
|
@@ -1647,7 +1675,7 @@ const IdentityDocumentCamera = ({
|
|
|
1647
1675
|
status === 'SCANNING' && allElementsDetected && elementsOutsideScanArea.length === 0 && !isBrightnessLow && !isFrameBlurry && styles.topZoneTextScanning
|
|
1648
1676
|
// 5. Default (white) - aligning (not all detected OR elements outside scan area)
|
|
1649
1677
|
],
|
|
1650
|
-
children: getStatusMessage(nextStep, status, detectedDocumentType, isBrightnessLow, isFrameBlurry, allElementsDetected, elementsOutsideScanArea, t)
|
|
1678
|
+
children: getStatusMessage(nextStep, status, detectedDocumentType, isBrightnessLow, isFrameBlurry, allElementsDetected, elementsOutsideScanArea, t, mrzReliable)
|
|
1651
1679
|
})]
|
|
1652
1680
|
}), /*#__PURE__*/_jsx(View, {
|
|
1653
1681
|
style: styles.leftZone
|
|
@@ -1657,8 +1685,11 @@ const IdentityDocumentCamera = ({
|
|
|
1657
1685
|
style: styles.bottomZone
|
|
1658
1686
|
}), /*#__PURE__*/_jsx(View, {
|
|
1659
1687
|
style: [styles.scanArea, {
|
|
1660
|
-
borderColor:
|
|
1661
|
-
|
|
1688
|
+
borderColor:
|
|
1689
|
+
// Green once the scan is done OR the MRZ is reliably detected
|
|
1690
|
+
// (stable consensus) — the "GO" signal. Never green before that.
|
|
1691
|
+
nextStep === 'COMPLETED' || mrzReliable ? '#4CAF50' : status === 'INCORRECT' ? '#f44336' : status === 'SCANNING' ? '#2196F3' : isBrightnessLow || isFrameBlurry ? '#FFC107' : 'white',
|
|
1692
|
+
borderWidth: mrzReliable || status === 'SCANNING' ? 3 : 2
|
|
1662
1693
|
}],
|
|
1663
1694
|
children: nextStep === 'COMPLETED' ? /*#__PURE__*/_jsx(LottieView, {
|
|
1664
1695
|
source: require('../../Shared/Animations/success.json'),
|
|
@@ -78,10 +78,17 @@ export function transformBoundsToScreen(bounds, scale, offsetX, offsetY) {
|
|
|
78
78
|
* Unified status message logic used by both voice guidance and render text.
|
|
79
79
|
* Returns the appropriate i18n key arguments for the current scan state.
|
|
80
80
|
*/
|
|
81
|
-
export function getStatusMessage(nextStep, status, detectedDocumentType, isBrightnessLow, isFrameBlurry, allElementsDetected, elementsOutsideScanArea, t) {
|
|
81
|
+
export function getStatusMessage(nextStep, status, detectedDocumentType, isBrightnessLow, isFrameBlurry, allElementsDetected, elementsOutsideScanArea, t, mrzReliable = false) {
|
|
82
82
|
if (nextStep === 'COMPLETED') {
|
|
83
83
|
return t('identityDocumentCamera.scanCompleted');
|
|
84
84
|
}
|
|
85
|
+
|
|
86
|
+
// MRZ reliably detected (stable consensus) — the green "GO" message. Ranks
|
|
87
|
+
// above the in-progress states so the user gets a clear positive signal the
|
|
88
|
+
// moment the reading has settled, but never on an early/transient valid read.
|
|
89
|
+
if (mrzReliable) {
|
|
90
|
+
return t('identityDocumentCamera.mrzDetected');
|
|
91
|
+
}
|
|
85
92
|
if (status === 'INCORRECT') {
|
|
86
93
|
if (nextStep === 'SCAN_ID_FRONT_OR_PASSPORT') {
|
|
87
94
|
return t('identityDocumentCamera.alignIDFront');
|