@trustchex/react-native-sdk 1.464.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/camera/TrustchexCameraView.kt +43 -3
- package/ios/Camera/TrustchexCameraView.swift +152 -27
- package/ios/Permission/PermissionModule.m +22 -0
- package/ios/Permission/PermissionModule.swift +67 -0
- 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 +4 -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 +7 -6
- package/lib/module/Shared/Libs/PERMISSIONS_MANAGER.md +268 -0
- package/lib/module/Shared/Libs/index.js +20 -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/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/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 +1 -1
- 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 +13 -9
- package/src/Shared/Components/EIDScanner.tsx +210 -160
- package/src/Shared/Components/FaceCamera.tsx +19 -16
- package/src/Shared/Components/IdentityDocumentCamera.tsx +7 -7
- package/src/Shared/Libs/PERMISSIONS_MANAGER.md +268 -0
- package/src/Shared/Libs/index.ts +63 -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
|
@@ -1,10 +1,13 @@
|
|
|
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,6 +17,8 @@ import android.util.Size
|
|
|
14
17
|
import android.widget.FrameLayout
|
|
15
18
|
import androidx.annotation.OptIn
|
|
16
19
|
import androidx.camera.core.*
|
|
20
|
+
import androidx.camera.core.resolutionselector.ResolutionSelector
|
|
21
|
+
import androidx.camera.core.resolutionselector.ResolutionStrategy
|
|
17
22
|
import androidx.camera.lifecycle.ProcessCameraProvider
|
|
18
23
|
import androidx.camera.video.*
|
|
19
24
|
import androidx.camera.view.PreviewView
|
|
@@ -303,10 +308,18 @@ class TrustchexCameraView(context: ThemedReactContext) : FrameLayout(context) {
|
|
|
303
308
|
"hd" -> Size(720, 1280) // Portrait HD
|
|
304
309
|
else -> Size(1080, 1920) // Portrait Full HD (default)
|
|
305
310
|
}
|
|
311
|
+
val resolutionSelector = ResolutionSelector.Builder()
|
|
312
|
+
.setResolutionStrategy(
|
|
313
|
+
ResolutionStrategy(
|
|
314
|
+
targetResolution,
|
|
315
|
+
ResolutionStrategy.FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER
|
|
316
|
+
)
|
|
317
|
+
)
|
|
318
|
+
.build()
|
|
306
319
|
|
|
307
320
|
// Preview use case
|
|
308
321
|
preview = Preview.Builder()
|
|
309
|
-
.
|
|
322
|
+
.setResolutionSelector(resolutionSelector)
|
|
310
323
|
.setTargetRotation(android.view.Surface.ROTATION_0) // Portrait
|
|
311
324
|
.build()
|
|
312
325
|
.also {
|
|
@@ -315,7 +328,7 @@ class TrustchexCameraView(context: ThemedReactContext) : FrameLayout(context) {
|
|
|
315
328
|
|
|
316
329
|
// Image analysis use case
|
|
317
330
|
imageAnalyzer = ImageAnalysis.Builder()
|
|
318
|
-
.
|
|
331
|
+
.setResolutionSelector(resolutionSelector)
|
|
319
332
|
.setTargetRotation(android.view.Surface.ROTATION_0) // Portrait
|
|
320
333
|
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
|
|
321
334
|
.setOutputImageFormat(ImageAnalysis.OUTPUT_IMAGE_FORMAT_YUV_420_888)
|
|
@@ -777,6 +790,16 @@ class TrustchexCameraView(context: ThemedReactContext) : FrameLayout(context) {
|
|
|
777
790
|
return
|
|
778
791
|
}
|
|
779
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
|
+
|
|
780
803
|
val videoCapture = videoCapture ?: run {
|
|
781
804
|
sendRecordingErrorEvent("VideoCapture not initialized")
|
|
782
805
|
return
|
|
@@ -813,7 +836,24 @@ class TrustchexCameraView(context: ThemedReactContext) : FrameLayout(context) {
|
|
|
813
836
|
sendRecordingErrorEvent("Recording error: ${event.cause?.message}")
|
|
814
837
|
} else {
|
|
815
838
|
val filePath = videoFile.absolutePath
|
|
816
|
-
|
|
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
|
+
}
|
|
817
857
|
}
|
|
818
858
|
}
|
|
819
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,36 +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 {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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 {
|
|
292
|
+
return AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .front)
|
|
293
|
+
}
|
|
294
|
+
|
|
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
|
+
}
|
|
249
311
|
}
|
|
250
312
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
|
255
326
|
}
|
|
256
327
|
|
|
257
|
-
// Fallback:
|
|
258
|
-
if let
|
|
259
|
-
return
|
|
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
|
|
260
331
|
}
|
|
261
332
|
|
|
262
|
-
// Fallback:
|
|
263
|
-
if let
|
|
264
|
-
return
|
|
333
|
+
// Fallback: triple-camera system.
|
|
334
|
+
if let cam = AVCaptureDevice.default(.builtInTripleCamera, for: .video, position: .back) {
|
|
335
|
+
return cam
|
|
265
336
|
}
|
|
266
337
|
|
|
267
|
-
// Final fallback: any
|
|
338
|
+
// Final fallback: any available camera.
|
|
268
339
|
return AVCaptureDevice.default(for: .video)
|
|
269
340
|
}
|
|
270
341
|
|
|
@@ -304,6 +375,12 @@ class TrustchexCameraView: UIView {
|
|
|
304
375
|
} else if camera.isFocusModeSupported(.autoFocus) {
|
|
305
376
|
camera.focusMode = .autoFocus
|
|
306
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
|
+
}
|
|
307
384
|
} else {
|
|
308
385
|
// Back camera: Optimized focus for document scanning across all models
|
|
309
386
|
if camera.isFocusModeSupported(.continuousAutoFocus) {
|
|
@@ -476,6 +553,22 @@ class TrustchexCameraView: UIView {
|
|
|
476
553
|
guard let session = self.captureSession, session.isRunning else { return }
|
|
477
554
|
guard let movieOutput = self.movieFileOutput else { return }
|
|
478
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
|
+
|
|
479
572
|
// Check actual AVFoundation recording state
|
|
480
573
|
if movieOutput.isRecording {
|
|
481
574
|
movieOutput.stopRecording()
|
|
@@ -495,12 +588,26 @@ class TrustchexCameraView: UIView {
|
|
|
495
588
|
return
|
|
496
589
|
}
|
|
497
590
|
|
|
498
|
-
// Verify video
|
|
499
|
-
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
|
|
500
607
|
|
|
501
608
|
// Create temporary file URL
|
|
502
609
|
let tempDir = NSTemporaryDirectory()
|
|
503
|
-
let fileName = "recording_\(Date().timeIntervalSince1970).
|
|
610
|
+
let fileName = "recording_\(Date().timeIntervalSince1970).mov"
|
|
504
611
|
let fileURL = URL(fileURLWithPath: tempDir).appendingPathComponent(fileName)
|
|
505
612
|
|
|
506
613
|
// Remove existing file if any
|
|
@@ -655,10 +762,18 @@ class TrustchexCameraView: UIView {
|
|
|
655
762
|
|
|
656
763
|
// Update mirroring on preview layer
|
|
657
764
|
DispatchQueue.main.async { [weak self] in
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
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
|
|
662
777
|
}
|
|
663
778
|
}
|
|
664
779
|
}
|
|
@@ -1047,6 +1162,16 @@ extension TrustchexCameraView: AVCaptureFileOutputRecordingDelegate {
|
|
|
1047
1162
|
|
|
1048
1163
|
// Fire callback
|
|
1049
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
|
+
|
|
1050
1175
|
DispatchQueue.main.async { [weak self] in
|
|
1051
1176
|
self?.onRecordingFinished?(["path": outputFileURL.path])
|
|
1052
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
|
+
}
|
|
@@ -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) {
|
|
@@ -695,7 +695,7 @@ const ResultScreen = () => {
|
|
|
695
695
|
}), /*#__PURE__*/_jsx(Text, {
|
|
696
696
|
style: styles.sectionText,
|
|
697
697
|
children: t('resultScreen.demoVideo')
|
|
698
|
-
}), /*#__PURE__*/_jsx(Video, {
|
|
698
|
+
}), consentVideo.videoPath ? /*#__PURE__*/_jsx(Video, {
|
|
699
699
|
source: {
|
|
700
700
|
uri: consentVideo.videoPath
|
|
701
701
|
},
|
|
@@ -703,6 +703,9 @@ const ResultScreen = () => {
|
|
|
703
703
|
style: styles.video,
|
|
704
704
|
controls: true,
|
|
705
705
|
muted: false
|
|
706
|
+
}) : /*#__PURE__*/_jsx(Text, {
|
|
707
|
+
style: styles.sectionText,
|
|
708
|
+
children: "N/A"
|
|
706
709
|
})]
|
|
707
710
|
}, `${consentVideo.videoPath}-${index}`))]
|
|
708
711
|
})]
|