@trustchex/react-native-sdk 1.409.0 → 1.472.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/src/main/java/com/trustchex/reactnativesdk/TrustchexSDKModule.kt +2 -8
- package/android/src/main/java/com/trustchex/reactnativesdk/camera/TrustchexCameraView.kt +102 -4
- package/ios/Camera/TrustchexCameraView.swift +151 -18
- package/ios/Permission/PermissionModule.m +22 -0
- package/ios/Permission/PermissionModule.swift +67 -0
- package/lib/module/Screens/Debug/NFCScanTestScreen.js +635 -0
- package/lib/module/Screens/Dynamic/ContractAcceptanceScreen.js +1 -4
- package/lib/module/Screens/Dynamic/IdentityDocumentEIDScanningScreen.js +17 -4
- package/lib/module/Screens/Dynamic/LivenessDetectionScreen.js +125 -25
- package/lib/module/Screens/Dynamic/VerbalConsentScreen.js +1079 -0
- package/lib/module/Screens/Dynamic/VideoCallScreen.js +10 -13
- package/lib/module/Screens/Static/ResultScreen.js +131 -22
- package/lib/module/Screens/Static/VerificationSessionCheckScreen.js +8 -0
- package/lib/module/Shared/Animations/recording.json +1 -0
- package/lib/module/Shared/Components/DebugNavigationPanel.js +69 -71
- package/lib/module/Shared/Components/EIDScanner.js +222 -115
- package/lib/module/Shared/Components/FaceCamera.js +14 -8
- package/lib/module/Shared/Components/IdentityDocumentCamera.flows.js +5 -3
- package/lib/module/Shared/Components/IdentityDocumentCamera.js +60 -42
- package/lib/module/Shared/Components/IdentityDocumentCamera.utils.js +13 -4
- package/lib/module/Shared/Components/NavigationManager.js +24 -16
- package/lib/module/Shared/EIDReader/aesSecureMessagingWrapper.js +51 -0
- package/lib/module/Shared/EIDReader/apduLevelPACECapable.js +3 -0
- package/lib/module/Shared/EIDReader/bacKey.js +16 -2
- package/lib/module/Shared/EIDReader/eidReader.js +354 -13
- package/lib/module/Shared/EIDReader/eidService.js +25 -1
- package/lib/module/Shared/EIDReader/nfcManagerCardService.js +4 -7
- package/lib/module/Shared/EIDReader/paceInfo.js +85 -0
- package/lib/module/Shared/EIDReader/paceKeySpec.js +51 -0
- package/lib/module/Shared/EIDReader/protocol/paceAPDUSender.js +100 -0
- package/lib/module/Shared/EIDReader/protocol/paceProtocol.js +655 -0
- package/lib/module/Shared/EIDReader/protocol/paceResult.js +37 -0
- package/lib/module/Shared/EIDReader/secureMessagingWrapper.js +27 -4
- package/lib/module/Shared/EIDReader/smartcards/commandAPDU.js +2 -1
- package/lib/module/Shared/EIDReader/tlv/tlv.helpers.js +1 -1
- package/lib/module/Shared/EIDReader/tlv/tlv.utils.js +6 -3
- package/lib/module/Shared/EIDReader/utils/aesCrypto.utils.js +189 -0
- package/lib/module/Shared/Libs/PERMISSIONS_MANAGER.md +268 -0
- package/lib/module/Shared/Libs/analytics.utils.js +4 -0
- package/lib/module/Shared/Libs/contains.js +1 -40
- package/lib/module/Shared/Libs/country-display.utils.js +34 -0
- package/lib/module/Shared/Libs/demo.utils.js +8 -0
- package/lib/module/Shared/Libs/index.js +20 -0
- package/lib/module/Shared/Libs/mrz.utils.js +3 -2
- package/lib/module/Shared/Libs/permissions.utils.js +199 -0
- package/lib/module/Shared/Libs/status-bar.utils.js +4 -2
- package/lib/module/Shared/Types/analytics.types.js +2 -0
- package/lib/module/Translation/Resources/en.js +42 -2
- package/lib/module/Translation/Resources/tr.js +42 -2
- package/lib/module/Trustchex.js +54 -20
- package/lib/module/version.js +1 -1
- package/lib/typescript/src/Screens/Debug/NFCScanTestScreen.d.ts +3 -0
- package/lib/typescript/src/Screens/Debug/NFCScanTestScreen.d.ts.map +1 -0
- package/lib/typescript/src/Screens/Dynamic/ContractAcceptanceScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Dynamic/IdentityDocumentEIDScanningScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Dynamic/LivenessDetectionScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Dynamic/VerbalConsentScreen.d.ts +3 -0
- package/lib/typescript/src/Screens/Dynamic/VerbalConsentScreen.d.ts.map +1 -0
- 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/Screens/Static/VerificationSessionCheckScreen.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/DebugNavigationPanel.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/Components/IdentityDocumentCamera.flows.d.ts +1 -1
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.flows.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.utils.d.ts +5 -0
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/NavigationManager.d.ts.map +1 -1
- package/lib/typescript/src/Shared/EIDReader/aesSecureMessagingWrapper.d.ts +18 -0
- package/lib/typescript/src/Shared/EIDReader/aesSecureMessagingWrapper.d.ts.map +1 -0
- package/lib/typescript/src/Shared/EIDReader/apduLevelPACECapable.d.ts +23 -0
- package/lib/typescript/src/Shared/EIDReader/apduLevelPACECapable.d.ts.map +1 -0
- package/lib/typescript/src/Shared/EIDReader/bacKey.d.ts +6 -0
- package/lib/typescript/src/Shared/EIDReader/bacKey.d.ts.map +1 -1
- package/lib/typescript/src/Shared/EIDReader/eidReader.d.ts.map +1 -1
- package/lib/typescript/src/Shared/EIDReader/eidService.d.ts +9 -0
- package/lib/typescript/src/Shared/EIDReader/eidService.d.ts.map +1 -1
- package/lib/typescript/src/Shared/EIDReader/nfcManagerCardService.d.ts.map +1 -1
- package/lib/typescript/src/Shared/EIDReader/paceInfo.d.ts +50 -0
- package/lib/typescript/src/Shared/EIDReader/paceInfo.d.ts.map +1 -0
- package/lib/typescript/src/Shared/EIDReader/paceKeySpec.d.ts +30 -0
- package/lib/typescript/src/Shared/EIDReader/paceKeySpec.d.ts.map +1 -0
- package/lib/typescript/src/Shared/EIDReader/protocol/paceAPDUSender.d.ts +17 -0
- package/lib/typescript/src/Shared/EIDReader/protocol/paceAPDUSender.d.ts.map +1 -0
- package/lib/typescript/src/Shared/EIDReader/protocol/paceProtocol.d.ts +105 -0
- package/lib/typescript/src/Shared/EIDReader/protocol/paceProtocol.d.ts.map +1 -0
- package/lib/typescript/src/Shared/EIDReader/protocol/paceResult.d.ts +24 -0
- package/lib/typescript/src/Shared/EIDReader/protocol/paceResult.d.ts.map +1 -0
- package/lib/typescript/src/Shared/EIDReader/secureMessagingWrapper.d.ts +15 -0
- package/lib/typescript/src/Shared/EIDReader/secureMessagingWrapper.d.ts.map +1 -1
- package/lib/typescript/src/Shared/EIDReader/smartcards/commandAPDU.d.ts.map +1 -1
- package/lib/typescript/src/Shared/EIDReader/tlv/tlv.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/EIDReader/utils/aesCrypto.utils.d.ts +39 -0
- package/lib/typescript/src/Shared/EIDReader/utils/aesCrypto.utils.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Libs/analytics.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/contains.d.ts +0 -7
- package/lib/typescript/src/Shared/Libs/contains.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/country-display.utils.d.ts +2 -0
- package/lib/typescript/src/Shared/Libs/country-display.utils.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Libs/demo.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/http-client.d.ts +1 -1
- package/lib/typescript/src/Shared/Libs/http-client.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.map +1 -1
- 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/Shared/Libs/status-bar.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Types/analytics.types.d.ts +2 -0
- package/lib/typescript/src/Shared/Types/analytics.types.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Types/identificationInfo.d.ts +10 -1
- package/lib/typescript/src/Shared/Types/identificationInfo.d.ts.map +1 -1
- package/lib/typescript/src/Translation/Resources/en.d.ts +41 -1
- package/lib/typescript/src/Translation/Resources/en.d.ts.map +1 -1
- package/lib/typescript/src/Translation/Resources/tr.d.ts +41 -1
- package/lib/typescript/src/Translation/Resources/tr.d.ts.map +1 -1
- package/lib/typescript/src/Trustchex.d.ts.map +1 -1
- package/lib/typescript/src/version.d.ts +1 -1
- package/package.json +7 -4
- package/src/Screens/Debug/NFCScanTestScreen.tsx +692 -0
- package/src/Screens/Dynamic/ContractAcceptanceScreen.tsx +1 -4
- package/src/Screens/Dynamic/IdentityDocumentEIDScanningScreen.tsx +21 -4
- package/src/Screens/Dynamic/LivenessDetectionScreen.tsx +165 -24
- package/src/Screens/Dynamic/VerbalConsentScreen.tsx +1401 -0
- package/src/Screens/Dynamic/VideoCallScreen.tsx +11 -20
- package/src/Screens/Static/ResultScreen.tsx +187 -31
- package/src/Screens/Static/VerificationSessionCheckScreen.tsx +9 -0
- package/src/Shared/Animations/recording.json +1 -0
- package/src/Shared/Components/DebugNavigationPanel.tsx +73 -48
- package/src/Shared/Components/EIDScanner.tsx +280 -119
- package/src/Shared/Components/FaceCamera.tsx +19 -16
- package/src/Shared/Components/IdentityDocumentCamera.flows.ts +7 -4
- package/src/Shared/Components/IdentityDocumentCamera.tsx +206 -191
- package/src/Shared/Components/IdentityDocumentCamera.utils.ts +13 -4
- package/src/Shared/Components/NavigationManager.tsx +27 -18
- package/src/Shared/EIDReader/aesSecureMessagingWrapper.ts +69 -0
- package/src/Shared/EIDReader/apduLevelPACECapable.ts +34 -0
- package/src/Shared/EIDReader/bacKey.ts +24 -8
- package/src/Shared/EIDReader/eidReader.ts +398 -12
- package/src/Shared/EIDReader/eidService.ts +49 -1
- package/src/Shared/EIDReader/nfcManagerCardService.ts +4 -6
- package/src/Shared/EIDReader/paceInfo.ts +159 -0
- package/src/Shared/EIDReader/paceKeySpec.ts +56 -0
- package/src/Shared/EIDReader/protocol/paceAPDUSender.ts +163 -0
- package/src/Shared/EIDReader/protocol/paceProtocol.ts +946 -0
- package/src/Shared/EIDReader/protocol/paceResult.ts +62 -0
- package/src/Shared/EIDReader/secureMessagingWrapper.ts +28 -10
- package/src/Shared/EIDReader/smartcards/commandAPDU.ts +2 -1
- package/src/Shared/EIDReader/tlv/tlv.helpers.ts +1 -1
- package/src/Shared/EIDReader/tlv/tlv.utils.ts +8 -5
- package/src/Shared/EIDReader/utils/aesCrypto.utils.ts +217 -0
- package/src/Shared/Libs/PERMISSIONS_MANAGER.md +268 -0
- package/src/Shared/Libs/analytics.utils.ts +4 -0
- package/src/Shared/Libs/contains.ts +0 -53
- package/src/Shared/Libs/country-display.utils.ts +55 -0
- package/src/Shared/Libs/crypto.utils.ts +2 -2
- package/src/Shared/Libs/demo.utils.ts +10 -0
- package/src/Shared/Libs/http-client.ts +12 -4
- package/src/Shared/Libs/index.ts +63 -0
- package/src/Shared/Libs/mrz.utils.ts +3 -2
- package/src/Shared/Libs/permissions.utils.ts +251 -0
- package/src/Shared/Libs/status-bar.utils.ts +4 -2
- package/src/Shared/Services/VideoSessionService.ts +1 -1
- package/src/Shared/Types/analytics.types.ts +2 -0
- package/src/Shared/Types/identificationInfo.ts +11 -0
- package/src/Translation/Resources/en.ts +64 -3
- package/src/Translation/Resources/tr.ts +63 -3
- package/src/Trustchex.tsx +53 -17
- package/src/version.ts +1 -1
|
@@ -17,20 +17,14 @@ class TrustchexSDKModule(reactContext: ReactApplicationContext) :
|
|
|
17
17
|
@ReactMethod
|
|
18
18
|
override fun enableKeepAwake() {
|
|
19
19
|
UiThreadUtil.runOnUiThread {
|
|
20
|
-
|
|
21
|
-
activity?.runOnUiThread {
|
|
22
|
-
activity.window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
|
23
|
-
}
|
|
20
|
+
currentActivity?.window?.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
|
24
21
|
}
|
|
25
22
|
}
|
|
26
23
|
|
|
27
24
|
@ReactMethod
|
|
28
25
|
override fun disableKeepAwake() {
|
|
29
26
|
UiThreadUtil.runOnUiThread {
|
|
30
|
-
|
|
31
|
-
activity?.runOnUiThread {
|
|
32
|
-
activity.window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
|
33
|
-
}
|
|
27
|
+
currentActivity?.window?.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
|
34
28
|
}
|
|
35
29
|
}
|
|
36
30
|
|
|
@@ -1,17 +1,24 @@
|
|
|
1
1
|
package com.trustchex.reactnativesdk.camera
|
|
2
2
|
|
|
3
3
|
import android.annotation.SuppressLint
|
|
4
|
+
import android.Manifest
|
|
5
|
+
import android.content.pm.PackageManager
|
|
4
6
|
import android.graphics.Bitmap
|
|
5
7
|
import android.graphics.BitmapFactory
|
|
6
8
|
import android.graphics.ImageFormat
|
|
7
9
|
import android.graphics.Matrix
|
|
10
|
+
import android.media.MediaMetadataRetriever
|
|
8
11
|
import android.graphics.Rect
|
|
9
12
|
import android.graphics.YuvImage
|
|
10
13
|
import android.util.Base64
|
|
14
|
+
import android.hardware.camera2.CameraCharacteristics
|
|
15
|
+
import android.hardware.camera2.CameraManager
|
|
11
16
|
import android.util.Size
|
|
12
17
|
import android.widget.FrameLayout
|
|
13
18
|
import androidx.annotation.OptIn
|
|
14
19
|
import androidx.camera.core.*
|
|
20
|
+
import androidx.camera.core.resolutionselector.ResolutionSelector
|
|
21
|
+
import androidx.camera.core.resolutionselector.ResolutionStrategy
|
|
15
22
|
import androidx.camera.lifecycle.ProcessCameraProvider
|
|
16
23
|
import androidx.camera.video.*
|
|
17
24
|
import androidx.camera.view.PreviewView
|
|
@@ -154,7 +161,7 @@ class TrustchexCameraView(context: ThemedReactContext) : FrameLayout(context) {
|
|
|
154
161
|
|
|
155
162
|
private fun selectBestCamera(type: String): CameraSelector {
|
|
156
163
|
if (type == "front") {
|
|
157
|
-
return
|
|
164
|
+
return selectWidestFrontCamera()
|
|
158
165
|
}
|
|
159
166
|
|
|
160
167
|
// For back camera, try to select Ultra Wide camera for document scanning
|
|
@@ -181,6 +188,62 @@ class TrustchexCameraView(context: ThemedReactContext) : FrameLayout(context) {
|
|
|
181
188
|
return CameraSelector.DEFAULT_BACK_CAMERA
|
|
182
189
|
}
|
|
183
190
|
|
|
191
|
+
/**
|
|
192
|
+
* Select the front camera with the widest field of view.
|
|
193
|
+
* Many devices (e.g. Pixel 9a, Samsung S24) have multiple front cameras;
|
|
194
|
+
* the wider lens gives a better framing experience for face detection.
|
|
195
|
+
*/
|
|
196
|
+
@OptIn(androidx.camera.camera2.interop.ExperimentalCamera2Interop::class)
|
|
197
|
+
private fun selectWidestFrontCamera(): CameraSelector {
|
|
198
|
+
try {
|
|
199
|
+
val provider = cameraProvider ?: return CameraSelector.DEFAULT_FRONT_CAMERA
|
|
200
|
+
val cameraManager = context.getSystemService(android.content.Context.CAMERA_SERVICE) as? CameraManager
|
|
201
|
+
?: return CameraSelector.DEFAULT_FRONT_CAMERA
|
|
202
|
+
|
|
203
|
+
val frontCameras = provider.availableCameraInfos.filter { info ->
|
|
204
|
+
try {
|
|
205
|
+
val c2Info = androidx.camera.camera2.interop.Camera2CameraInfo.from(info)
|
|
206
|
+
val chars = cameraManager.getCameraCharacteristics(c2Info.cameraId)
|
|
207
|
+
chars.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT
|
|
208
|
+
} catch (_: Exception) { false }
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (frontCameras.size <= 1) return CameraSelector.DEFAULT_FRONT_CAMERA
|
|
212
|
+
|
|
213
|
+
// Pick the camera with the shortest focal length (widest FoV)
|
|
214
|
+
var bestInfo: androidx.camera.core.CameraInfo? = null
|
|
215
|
+
var shortestFocal = Float.MAX_VALUE
|
|
216
|
+
|
|
217
|
+
for (info in frontCameras) {
|
|
218
|
+
try {
|
|
219
|
+
val c2Info = androidx.camera.camera2.interop.Camera2CameraInfo.from(info)
|
|
220
|
+
val chars = cameraManager.getCameraCharacteristics(c2Info.cameraId)
|
|
221
|
+
val focalLengths = chars.get(CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS)
|
|
222
|
+
if (focalLengths != null && focalLengths.isNotEmpty()) {
|
|
223
|
+
val minFocal = focalLengths.min()
|
|
224
|
+
if (minFocal < shortestFocal) {
|
|
225
|
+
shortestFocal = minFocal
|
|
226
|
+
bestInfo = info
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
} catch (_: Exception) { /* skip */ }
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (bestInfo != null) {
|
|
233
|
+
val selectedInfo = bestInfo
|
|
234
|
+
return CameraSelector.Builder()
|
|
235
|
+
.requireLensFacing(CameraSelector.LENS_FACING_FRONT)
|
|
236
|
+
.addCameraFilter { cameraInfos ->
|
|
237
|
+
cameraInfos.filter { it == selectedInfo }.ifEmpty { cameraInfos }
|
|
238
|
+
}
|
|
239
|
+
.build()
|
|
240
|
+
}
|
|
241
|
+
} catch (_: Exception) {
|
|
242
|
+
// Fall back to default
|
|
243
|
+
}
|
|
244
|
+
return CameraSelector.DEFAULT_FRONT_CAMERA
|
|
245
|
+
}
|
|
246
|
+
|
|
184
247
|
fun setTorchEnabled(enabled: Boolean) {
|
|
185
248
|
torchEnabled = enabled
|
|
186
249
|
camera?.cameraControl?.enableTorch(enabled)
|
|
@@ -245,10 +308,18 @@ class TrustchexCameraView(context: ThemedReactContext) : FrameLayout(context) {
|
|
|
245
308
|
"hd" -> Size(720, 1280) // Portrait HD
|
|
246
309
|
else -> Size(1080, 1920) // Portrait Full HD (default)
|
|
247
310
|
}
|
|
311
|
+
val resolutionSelector = ResolutionSelector.Builder()
|
|
312
|
+
.setResolutionStrategy(
|
|
313
|
+
ResolutionStrategy(
|
|
314
|
+
targetResolution,
|
|
315
|
+
ResolutionStrategy.FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER
|
|
316
|
+
)
|
|
317
|
+
)
|
|
318
|
+
.build()
|
|
248
319
|
|
|
249
320
|
// Preview use case
|
|
250
321
|
preview = Preview.Builder()
|
|
251
|
-
.
|
|
322
|
+
.setResolutionSelector(resolutionSelector)
|
|
252
323
|
.setTargetRotation(android.view.Surface.ROTATION_0) // Portrait
|
|
253
324
|
.build()
|
|
254
325
|
.also {
|
|
@@ -257,7 +328,7 @@ class TrustchexCameraView(context: ThemedReactContext) : FrameLayout(context) {
|
|
|
257
328
|
|
|
258
329
|
// Image analysis use case
|
|
259
330
|
imageAnalyzer = ImageAnalysis.Builder()
|
|
260
|
-
.
|
|
331
|
+
.setResolutionSelector(resolutionSelector)
|
|
261
332
|
.setTargetRotation(android.view.Surface.ROTATION_0) // Portrait
|
|
262
333
|
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
|
|
263
334
|
.setOutputImageFormat(ImageAnalysis.OUTPUT_IMAGE_FORMAT_YUV_420_888)
|
|
@@ -719,6 +790,16 @@ class TrustchexCameraView(context: ThemedReactContext) : FrameLayout(context) {
|
|
|
719
790
|
return
|
|
720
791
|
}
|
|
721
792
|
|
|
793
|
+
val hasMicPermission = ContextCompat.checkSelfPermission(
|
|
794
|
+
reactContext,
|
|
795
|
+
Manifest.permission.RECORD_AUDIO
|
|
796
|
+
) == PackageManager.PERMISSION_GRANTED
|
|
797
|
+
|
|
798
|
+
if (!hasMicPermission) {
|
|
799
|
+
sendRecordingErrorEvent("Microphone permission is required for verbal consent recording")
|
|
800
|
+
return
|
|
801
|
+
}
|
|
802
|
+
|
|
722
803
|
val videoCapture = videoCapture ?: run {
|
|
723
804
|
sendRecordingErrorEvent("VideoCapture not initialized")
|
|
724
805
|
return
|
|
@@ -755,7 +836,24 @@ class TrustchexCameraView(context: ThemedReactContext) : FrameLayout(context) {
|
|
|
755
836
|
sendRecordingErrorEvent("Recording error: ${event.cause?.message}")
|
|
756
837
|
} else {
|
|
757
838
|
val filePath = videoFile.absolutePath
|
|
758
|
-
|
|
839
|
+
val hasAudioTrack = try {
|
|
840
|
+
val retriever = MediaMetadataRetriever()
|
|
841
|
+
retriever.setDataSource(filePath)
|
|
842
|
+
val hasAudio = retriever
|
|
843
|
+
.extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_AUDIO)
|
|
844
|
+
retriever.release()
|
|
845
|
+
hasAudio == "yes"
|
|
846
|
+
} catch (_: Exception) {
|
|
847
|
+
false
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
if (!hasAudioTrack) {
|
|
851
|
+
videoFile.delete()
|
|
852
|
+
currentRecordingFile = null
|
|
853
|
+
sendRecordingErrorEvent("Recorded video has no audio track")
|
|
854
|
+
} else {
|
|
855
|
+
sendRecordingFinishedEvent(filePath)
|
|
856
|
+
}
|
|
759
857
|
}
|
|
760
858
|
}
|
|
761
859
|
else -> {}
|
|
@@ -115,7 +115,24 @@ class TrustchexCameraView: UIView {
|
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
private func configureAudioSession() {
|
|
119
|
+
do {
|
|
120
|
+
let audioSession = AVAudioSession.sharedInstance()
|
|
121
|
+
// Set category to playAndRecord so we can record audio during video
|
|
122
|
+
try audioSession.setCategory(
|
|
123
|
+
.playAndRecord,
|
|
124
|
+
options: [.defaultToSpeaker, .duckOthers]
|
|
125
|
+
)
|
|
126
|
+
try audioSession.setActive(true, options: .notifyOthersOnDeactivation)
|
|
127
|
+
} catch {
|
|
128
|
+
// Audio session configuration failed - recording may not have audio
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
118
132
|
private func configureCaptureSession() {
|
|
133
|
+
// Configure audio session for recording with microphone
|
|
134
|
+
configureAudioSession()
|
|
135
|
+
|
|
119
136
|
let session = AVCaptureSession()
|
|
120
137
|
session.beginConfiguration()
|
|
121
138
|
|
|
@@ -178,11 +195,16 @@ class TrustchexCameraView: UIView {
|
|
|
178
195
|
let movieOutput = AVCaptureMovieFileOutput()
|
|
179
196
|
if session.canAddOutput(movieOutput) {
|
|
180
197
|
session.addOutput(movieOutput)
|
|
198
|
+
// Configure video connection
|
|
181
199
|
if let connection = movieOutput.connection(with: .video) {
|
|
182
200
|
if connection.isVideoOrientationSupported {
|
|
183
201
|
connection.videoOrientation = .portrait
|
|
184
202
|
}
|
|
185
203
|
}
|
|
204
|
+
// Configure audio connection for recording
|
|
205
|
+
if let audioConnection = movieOutput.connection(with: .audio) {
|
|
206
|
+
audioConnection.isEnabled = true
|
|
207
|
+
}
|
|
186
208
|
}
|
|
187
209
|
self.movieFileOutput = movieOutput
|
|
188
210
|
|
|
@@ -235,28 +257,85 @@ class TrustchexCameraView: UIView {
|
|
|
235
257
|
}
|
|
236
258
|
|
|
237
259
|
// MARK: - Camera Selection
|
|
260
|
+
|
|
261
|
+
/// Selects the best camera for the requested position using the same strategy
|
|
262
|
+
/// as the Android side:
|
|
263
|
+
/// • Front (face detection / verbal consent): pick the device with the widest
|
|
264
|
+
/// field of view by measuring `videoFieldOfView` across all available front
|
|
265
|
+
/// cameras — mirrors Android's focal-length comparison via Camera2.
|
|
266
|
+
/// • Back (document / EID / barcode scanning): wide-angle camera gives the
|
|
267
|
+
/// least barrel distortion and best optical sharpness; ultra-wide is avoided
|
|
268
|
+
/// intentionally because its distortion degrades OCR/MRZ accuracy.
|
|
238
269
|
private func selectBestCamera(for position: AVCaptureDevice.Position) -> AVCaptureDevice? {
|
|
239
270
|
if position == .front {
|
|
271
|
+
return selectWidestFrontCamera()
|
|
272
|
+
}
|
|
273
|
+
return selectBestBackCamera()
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/// Enumerates every front-facing camera the device exposes and returns the one
|
|
277
|
+
/// with the highest reported field-of-view, matching Android's
|
|
278
|
+
/// `selectWidestFrontCamera()` which picks the shortest focal length.
|
|
279
|
+
private func selectWidestFrontCamera() -> AVCaptureDevice? {
|
|
280
|
+
let discovery = AVCaptureDevice.DiscoverySession(
|
|
281
|
+
deviceTypes: [
|
|
282
|
+
.builtInUltraWideCamera,
|
|
283
|
+
.builtInTrueDepthCamera,
|
|
284
|
+
.builtInWideAngleCamera,
|
|
285
|
+
],
|
|
286
|
+
mediaType: .video,
|
|
287
|
+
position: .front
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
let devices = discovery.devices
|
|
291
|
+
guard !devices.isEmpty else {
|
|
240
292
|
return AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .front)
|
|
241
293
|
}
|
|
242
294
|
|
|
243
|
-
//
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
295
|
+
// Single device — no comparison needed.
|
|
296
|
+
if devices.count == 1 { return devices.first }
|
|
297
|
+
|
|
298
|
+
// Multiple front cameras: measure the maximum videoFieldOfView each device
|
|
299
|
+
// supports across all its formats and return the highest (widest) one.
|
|
300
|
+
var bestDevice: AVCaptureDevice? = nil
|
|
301
|
+
var widestFoV: Float = 0
|
|
302
|
+
|
|
303
|
+
for device in devices {
|
|
304
|
+
let maxFoV = device.formats
|
|
305
|
+
.map { $0.videoFieldOfView }
|
|
306
|
+
.max() ?? 0
|
|
307
|
+
if maxFoV > widestFoV {
|
|
308
|
+
widestFoV = maxFoV
|
|
309
|
+
bestDevice = device
|
|
310
|
+
}
|
|
247
311
|
}
|
|
248
312
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
313
|
+
return bestDevice
|
|
314
|
+
?? AVCaptureDevice.default(.builtInTrueDepthCamera, for: .video, position: .front)
|
|
315
|
+
?? AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .front)
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/// Returns the best back-facing camera for document scanning scenarios.
|
|
319
|
+
/// Wide-angle is preferred over ultra-wide because it has negligible barrel
|
|
320
|
+
/// distortion — critical for OCR/MRZ accuracy. This matches Android which
|
|
321
|
+
/// resolves to DEFAULT_BACK_CAMERA (the main wide-angle lens).
|
|
322
|
+
private func selectBestBackCamera() -> AVCaptureDevice? {
|
|
323
|
+
// Primary: main wide-angle lens — best for document/EID/barcode scanning.
|
|
324
|
+
if let cam = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) {
|
|
325
|
+
return cam
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Fallback: dual-camera system (manages wide + telephoto; exposes wide by default).
|
|
329
|
+
if let cam = AVCaptureDevice.default(.builtInDualCamera, for: .video, position: .back) {
|
|
330
|
+
return cam
|
|
252
331
|
}
|
|
253
332
|
|
|
254
|
-
// Fallback:
|
|
255
|
-
if let
|
|
256
|
-
return
|
|
333
|
+
// Fallback: triple-camera system.
|
|
334
|
+
if let cam = AVCaptureDevice.default(.builtInTripleCamera, for: .video, position: .back) {
|
|
335
|
+
return cam
|
|
257
336
|
}
|
|
258
337
|
|
|
259
|
-
// Final fallback: any
|
|
338
|
+
// Final fallback: any available camera.
|
|
260
339
|
return AVCaptureDevice.default(for: .video)
|
|
261
340
|
}
|
|
262
341
|
|
|
@@ -296,6 +375,12 @@ class TrustchexCameraView: UIView {
|
|
|
296
375
|
} else if camera.isFocusModeSupported(.autoFocus) {
|
|
297
376
|
camera.focusMode = .autoFocus
|
|
298
377
|
}
|
|
378
|
+
|
|
379
|
+
// Use the widest front-camera framing available on the device.
|
|
380
|
+
let widestZoom = camera.minAvailableVideoZoomFactor
|
|
381
|
+
if camera.videoZoomFactor != widestZoom {
|
|
382
|
+
camera.videoZoomFactor = widestZoom
|
|
383
|
+
}
|
|
299
384
|
} else {
|
|
300
385
|
// Back camera: Optimized focus for document scanning across all models
|
|
301
386
|
if camera.isFocusModeSupported(.continuousAutoFocus) {
|
|
@@ -468,6 +553,22 @@ class TrustchexCameraView: UIView {
|
|
|
468
553
|
guard let session = self.captureSession, session.isRunning else { return }
|
|
469
554
|
guard let movieOutput = self.movieFileOutput else { return }
|
|
470
555
|
|
|
556
|
+
// Ensure recording session is configured for video + microphone capture.
|
|
557
|
+
do {
|
|
558
|
+
let audioSession = AVAudioSession.sharedInstance()
|
|
559
|
+
try audioSession.setCategory(
|
|
560
|
+
.playAndRecord,
|
|
561
|
+
mode: .videoRecording,
|
|
562
|
+
options: [.defaultToSpeaker, .allowBluetooth, .allowBluetoothA2DP]
|
|
563
|
+
)
|
|
564
|
+
try audioSession.setActive(true, options: .notifyOthersOnDeactivation)
|
|
565
|
+
} catch {
|
|
566
|
+
DispatchQueue.main.async { [weak self] in
|
|
567
|
+
self?.onRecordingError?(["error": "Failed to activate audio session for recording"])
|
|
568
|
+
}
|
|
569
|
+
return
|
|
570
|
+
}
|
|
571
|
+
|
|
471
572
|
// Check actual AVFoundation recording state
|
|
472
573
|
if movieOutput.isRecording {
|
|
473
574
|
movieOutput.stopRecording()
|
|
@@ -487,12 +588,26 @@ class TrustchexCameraView: UIView {
|
|
|
487
588
|
return
|
|
488
589
|
}
|
|
489
590
|
|
|
490
|
-
// Verify video
|
|
491
|
-
guard movieOutput.connection(with: .video) != nil else {
|
|
591
|
+
// Verify both video and audio connections exist before recording
|
|
592
|
+
guard movieOutput.connection(with: .video) != nil else {
|
|
593
|
+
DispatchQueue.main.async { [weak self] in
|
|
594
|
+
self?.onRecordingError?(["error": "Video connection not available"])
|
|
595
|
+
}
|
|
596
|
+
return
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// Audio connection is required for verbal consent recordings.
|
|
600
|
+
guard let audioConnection = movieOutput.connection(with: .audio) else {
|
|
601
|
+
DispatchQueue.main.async { [weak self] in
|
|
602
|
+
self?.onRecordingError?(["error": "Microphone audio connection not available"])
|
|
603
|
+
}
|
|
604
|
+
return
|
|
605
|
+
}
|
|
606
|
+
audioConnection.isEnabled = true
|
|
492
607
|
|
|
493
608
|
// Create temporary file URL
|
|
494
609
|
let tempDir = NSTemporaryDirectory()
|
|
495
|
-
let fileName = "recording_\(Date().timeIntervalSince1970).
|
|
610
|
+
let fileName = "recording_\(Date().timeIntervalSince1970).mov"
|
|
496
611
|
let fileURL = URL(fileURLWithPath: tempDir).appendingPathComponent(fileName)
|
|
497
612
|
|
|
498
613
|
// Remove existing file if any
|
|
@@ -647,10 +762,18 @@ class TrustchexCameraView: UIView {
|
|
|
647
762
|
|
|
648
763
|
// Update mirroring on preview layer
|
|
649
764
|
DispatchQueue.main.async { [weak self] in
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
765
|
+
guard let connection = self?.previewLayer?.connection else { return }
|
|
766
|
+
guard connection.isVideoMirroringSupported else { return }
|
|
767
|
+
|
|
768
|
+
// Avoid AVFoundation exception when setting explicit mirroring while
|
|
769
|
+
// automatic mirroring is still enabled on the connection.
|
|
770
|
+
if connection.automaticallyAdjustsVideoMirroring {
|
|
771
|
+
connection.automaticallyAdjustsVideoMirroring = false
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
let shouldMirror = (type == "front")
|
|
775
|
+
if connection.isVideoMirrored != shouldMirror {
|
|
776
|
+
connection.isVideoMirrored = shouldMirror
|
|
654
777
|
}
|
|
655
778
|
}
|
|
656
779
|
}
|
|
@@ -1039,6 +1162,16 @@ extension TrustchexCameraView: AVCaptureFileOutputRecordingDelegate {
|
|
|
1039
1162
|
|
|
1040
1163
|
// Fire callback
|
|
1041
1164
|
if recordingSucceeded {
|
|
1165
|
+
let asset = AVURLAsset(url: outputFileURL)
|
|
1166
|
+
let hasAudioTrack = !asset.tracks(withMediaType: .audio).isEmpty
|
|
1167
|
+
|
|
1168
|
+
if !hasAudioTrack {
|
|
1169
|
+
DispatchQueue.main.async { [weak self] in
|
|
1170
|
+
self?.onRecordingError?(["error": "Recorded video has no audio track"])
|
|
1171
|
+
}
|
|
1172
|
+
return
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1042
1175
|
DispatchQueue.main.async { [weak self] in
|
|
1043
1176
|
self?.onRecordingFinished?(["path": outputFileURL.path])
|
|
1044
1177
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#import <React/RCTBridgeModule.h>
|
|
2
|
+
|
|
3
|
+
@interface RCT_EXTERN_MODULE(PermissionModule, NSObject)
|
|
4
|
+
|
|
5
|
+
RCT_EXTERN_METHOD(requestCameraPermission:(RCTPromiseResolveBlock)resolve
|
|
6
|
+
withRejecter:(RCTPromiseRejectBlock)reject)
|
|
7
|
+
|
|
8
|
+
RCT_EXTERN_METHOD(requestMicrophonePermission:(RCTPromiseResolveBlock)resolve
|
|
9
|
+
withRejecter:(RCTPromiseRejectBlock)reject)
|
|
10
|
+
|
|
11
|
+
RCT_EXTERN_METHOD(checkCameraPermission:(RCTPromiseResolveBlock)resolve
|
|
12
|
+
withRejecter:(RCTPromiseRejectBlock)reject)
|
|
13
|
+
|
|
14
|
+
RCT_EXTERN_METHOD(checkMicrophonePermission:(RCTPromiseResolveBlock)resolve
|
|
15
|
+
withRejecter:(RCTPromiseRejectBlock)reject)
|
|
16
|
+
|
|
17
|
+
+ (BOOL)requiresMainQueueSetup
|
|
18
|
+
{
|
|
19
|
+
return YES;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import React
|
|
2
|
+
import AVFoundation
|
|
3
|
+
|
|
4
|
+
@objc(PermissionModule)
|
|
5
|
+
class PermissionModule: NSObject {
|
|
6
|
+
|
|
7
|
+
@objc(requestCameraPermission:withRejecter:)
|
|
8
|
+
func requestCameraPermission(resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
|
9
|
+
DispatchQueue.main.async {
|
|
10
|
+
let status = AVCaptureDevice.authorizationStatus(for: .video)
|
|
11
|
+
|
|
12
|
+
switch status {
|
|
13
|
+
case .authorized:
|
|
14
|
+
resolve(true)
|
|
15
|
+
case .denied, .restricted:
|
|
16
|
+
resolve(false)
|
|
17
|
+
case .notDetermined:
|
|
18
|
+
AVCaptureDevice.requestAccess(for: .video) { granted in
|
|
19
|
+
resolve(granted)
|
|
20
|
+
}
|
|
21
|
+
@unknown default:
|
|
22
|
+
resolve(false)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@objc(requestMicrophonePermission:withRejecter:)
|
|
28
|
+
func requestMicrophonePermission(resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
|
29
|
+
DispatchQueue.main.async {
|
|
30
|
+
let status = AVAudioSession.sharedInstance().recordPermission
|
|
31
|
+
|
|
32
|
+
switch status {
|
|
33
|
+
case .granted:
|
|
34
|
+
resolve(true)
|
|
35
|
+
case .denied:
|
|
36
|
+
resolve(false)
|
|
37
|
+
case .undetermined:
|
|
38
|
+
AVAudioSession.sharedInstance().requestRecordPermission { granted in
|
|
39
|
+
resolve(granted)
|
|
40
|
+
}
|
|
41
|
+
@unknown default:
|
|
42
|
+
resolve(false)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@objc(checkCameraPermission:withRejecter:)
|
|
48
|
+
func checkCameraPermission(resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
|
49
|
+
DispatchQueue.main.async {
|
|
50
|
+
let status = AVCaptureDevice.authorizationStatus(for: .video)
|
|
51
|
+
resolve(status == .authorized)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@objc(checkMicrophonePermission:withRejecter:)
|
|
56
|
+
func checkMicrophonePermission(resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
|
57
|
+
DispatchQueue.main.async {
|
|
58
|
+
let status = AVAudioSession.sharedInstance().recordPermission
|
|
59
|
+
resolve(status == .granted)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@objc
|
|
64
|
+
static func requiresMainQueueSetup() -> Bool {
|
|
65
|
+
return true
|
|
66
|
+
}
|
|
67
|
+
}
|