@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.
Files changed (171) hide show
  1. package/android/src/main/java/com/trustchex/reactnativesdk/TrustchexSDKModule.kt +2 -8
  2. package/android/src/main/java/com/trustchex/reactnativesdk/camera/TrustchexCameraView.kt +102 -4
  3. package/ios/Camera/TrustchexCameraView.swift +151 -18
  4. package/ios/Permission/PermissionModule.m +22 -0
  5. package/ios/Permission/PermissionModule.swift +67 -0
  6. package/lib/module/Screens/Debug/NFCScanTestScreen.js +635 -0
  7. package/lib/module/Screens/Dynamic/ContractAcceptanceScreen.js +1 -4
  8. package/lib/module/Screens/Dynamic/IdentityDocumentEIDScanningScreen.js +17 -4
  9. package/lib/module/Screens/Dynamic/LivenessDetectionScreen.js +125 -25
  10. package/lib/module/Screens/Dynamic/VerbalConsentScreen.js +1079 -0
  11. package/lib/module/Screens/Dynamic/VideoCallScreen.js +10 -13
  12. package/lib/module/Screens/Static/ResultScreen.js +131 -22
  13. package/lib/module/Screens/Static/VerificationSessionCheckScreen.js +8 -0
  14. package/lib/module/Shared/Animations/recording.json +1 -0
  15. package/lib/module/Shared/Components/DebugNavigationPanel.js +69 -71
  16. package/lib/module/Shared/Components/EIDScanner.js +222 -115
  17. package/lib/module/Shared/Components/FaceCamera.js +14 -8
  18. package/lib/module/Shared/Components/IdentityDocumentCamera.flows.js +5 -3
  19. package/lib/module/Shared/Components/IdentityDocumentCamera.js +60 -42
  20. package/lib/module/Shared/Components/IdentityDocumentCamera.utils.js +13 -4
  21. package/lib/module/Shared/Components/NavigationManager.js +24 -16
  22. package/lib/module/Shared/EIDReader/aesSecureMessagingWrapper.js +51 -0
  23. package/lib/module/Shared/EIDReader/apduLevelPACECapable.js +3 -0
  24. package/lib/module/Shared/EIDReader/bacKey.js +16 -2
  25. package/lib/module/Shared/EIDReader/eidReader.js +354 -13
  26. package/lib/module/Shared/EIDReader/eidService.js +25 -1
  27. package/lib/module/Shared/EIDReader/nfcManagerCardService.js +4 -7
  28. package/lib/module/Shared/EIDReader/paceInfo.js +85 -0
  29. package/lib/module/Shared/EIDReader/paceKeySpec.js +51 -0
  30. package/lib/module/Shared/EIDReader/protocol/paceAPDUSender.js +100 -0
  31. package/lib/module/Shared/EIDReader/protocol/paceProtocol.js +655 -0
  32. package/lib/module/Shared/EIDReader/protocol/paceResult.js +37 -0
  33. package/lib/module/Shared/EIDReader/secureMessagingWrapper.js +27 -4
  34. package/lib/module/Shared/EIDReader/smartcards/commandAPDU.js +2 -1
  35. package/lib/module/Shared/EIDReader/tlv/tlv.helpers.js +1 -1
  36. package/lib/module/Shared/EIDReader/tlv/tlv.utils.js +6 -3
  37. package/lib/module/Shared/EIDReader/utils/aesCrypto.utils.js +189 -0
  38. package/lib/module/Shared/Libs/PERMISSIONS_MANAGER.md +268 -0
  39. package/lib/module/Shared/Libs/analytics.utils.js +4 -0
  40. package/lib/module/Shared/Libs/contains.js +1 -40
  41. package/lib/module/Shared/Libs/country-display.utils.js +34 -0
  42. package/lib/module/Shared/Libs/demo.utils.js +8 -0
  43. package/lib/module/Shared/Libs/index.js +20 -0
  44. package/lib/module/Shared/Libs/mrz.utils.js +3 -2
  45. package/lib/module/Shared/Libs/permissions.utils.js +199 -0
  46. package/lib/module/Shared/Libs/status-bar.utils.js +4 -2
  47. package/lib/module/Shared/Types/analytics.types.js +2 -0
  48. package/lib/module/Translation/Resources/en.js +42 -2
  49. package/lib/module/Translation/Resources/tr.js +42 -2
  50. package/lib/module/Trustchex.js +54 -20
  51. package/lib/module/version.js +1 -1
  52. package/lib/typescript/src/Screens/Debug/NFCScanTestScreen.d.ts +3 -0
  53. package/lib/typescript/src/Screens/Debug/NFCScanTestScreen.d.ts.map +1 -0
  54. package/lib/typescript/src/Screens/Dynamic/ContractAcceptanceScreen.d.ts.map +1 -1
  55. package/lib/typescript/src/Screens/Dynamic/IdentityDocumentEIDScanningScreen.d.ts.map +1 -1
  56. package/lib/typescript/src/Screens/Dynamic/LivenessDetectionScreen.d.ts.map +1 -1
  57. package/lib/typescript/src/Screens/Dynamic/VerbalConsentScreen.d.ts +3 -0
  58. package/lib/typescript/src/Screens/Dynamic/VerbalConsentScreen.d.ts.map +1 -0
  59. package/lib/typescript/src/Screens/Dynamic/VideoCallScreen.d.ts.map +1 -1
  60. package/lib/typescript/src/Screens/Static/ResultScreen.d.ts.map +1 -1
  61. package/lib/typescript/src/Screens/Static/VerificationSessionCheckScreen.d.ts.map +1 -1
  62. package/lib/typescript/src/Shared/Components/DebugNavigationPanel.d.ts.map +1 -1
  63. package/lib/typescript/src/Shared/Components/EIDScanner.d.ts.map +1 -1
  64. package/lib/typescript/src/Shared/Components/FaceCamera.d.ts.map +1 -1
  65. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.d.ts.map +1 -1
  66. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.flows.d.ts +1 -1
  67. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.flows.d.ts.map +1 -1
  68. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.utils.d.ts +5 -0
  69. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.utils.d.ts.map +1 -1
  70. package/lib/typescript/src/Shared/Components/NavigationManager.d.ts.map +1 -1
  71. package/lib/typescript/src/Shared/EIDReader/aesSecureMessagingWrapper.d.ts +18 -0
  72. package/lib/typescript/src/Shared/EIDReader/aesSecureMessagingWrapper.d.ts.map +1 -0
  73. package/lib/typescript/src/Shared/EIDReader/apduLevelPACECapable.d.ts +23 -0
  74. package/lib/typescript/src/Shared/EIDReader/apduLevelPACECapable.d.ts.map +1 -0
  75. package/lib/typescript/src/Shared/EIDReader/bacKey.d.ts +6 -0
  76. package/lib/typescript/src/Shared/EIDReader/bacKey.d.ts.map +1 -1
  77. package/lib/typescript/src/Shared/EIDReader/eidReader.d.ts.map +1 -1
  78. package/lib/typescript/src/Shared/EIDReader/eidService.d.ts +9 -0
  79. package/lib/typescript/src/Shared/EIDReader/eidService.d.ts.map +1 -1
  80. package/lib/typescript/src/Shared/EIDReader/nfcManagerCardService.d.ts.map +1 -1
  81. package/lib/typescript/src/Shared/EIDReader/paceInfo.d.ts +50 -0
  82. package/lib/typescript/src/Shared/EIDReader/paceInfo.d.ts.map +1 -0
  83. package/lib/typescript/src/Shared/EIDReader/paceKeySpec.d.ts +30 -0
  84. package/lib/typescript/src/Shared/EIDReader/paceKeySpec.d.ts.map +1 -0
  85. package/lib/typescript/src/Shared/EIDReader/protocol/paceAPDUSender.d.ts +17 -0
  86. package/lib/typescript/src/Shared/EIDReader/protocol/paceAPDUSender.d.ts.map +1 -0
  87. package/lib/typescript/src/Shared/EIDReader/protocol/paceProtocol.d.ts +105 -0
  88. package/lib/typescript/src/Shared/EIDReader/protocol/paceProtocol.d.ts.map +1 -0
  89. package/lib/typescript/src/Shared/EIDReader/protocol/paceResult.d.ts +24 -0
  90. package/lib/typescript/src/Shared/EIDReader/protocol/paceResult.d.ts.map +1 -0
  91. package/lib/typescript/src/Shared/EIDReader/secureMessagingWrapper.d.ts +15 -0
  92. package/lib/typescript/src/Shared/EIDReader/secureMessagingWrapper.d.ts.map +1 -1
  93. package/lib/typescript/src/Shared/EIDReader/smartcards/commandAPDU.d.ts.map +1 -1
  94. package/lib/typescript/src/Shared/EIDReader/tlv/tlv.utils.d.ts.map +1 -1
  95. package/lib/typescript/src/Shared/EIDReader/utils/aesCrypto.utils.d.ts +39 -0
  96. package/lib/typescript/src/Shared/EIDReader/utils/aesCrypto.utils.d.ts.map +1 -0
  97. package/lib/typescript/src/Shared/Libs/analytics.utils.d.ts.map +1 -1
  98. package/lib/typescript/src/Shared/Libs/contains.d.ts +0 -7
  99. package/lib/typescript/src/Shared/Libs/contains.d.ts.map +1 -1
  100. package/lib/typescript/src/Shared/Libs/country-display.utils.d.ts +2 -0
  101. package/lib/typescript/src/Shared/Libs/country-display.utils.d.ts.map +1 -0
  102. package/lib/typescript/src/Shared/Libs/demo.utils.d.ts.map +1 -1
  103. package/lib/typescript/src/Shared/Libs/http-client.d.ts +1 -1
  104. package/lib/typescript/src/Shared/Libs/http-client.d.ts.map +1 -1
  105. package/lib/typescript/src/Shared/Libs/index.d.ts +20 -0
  106. package/lib/typescript/src/Shared/Libs/index.d.ts.map +1 -0
  107. package/lib/typescript/src/Shared/Libs/mrz.utils.d.ts.map +1 -1
  108. package/lib/typescript/src/Shared/Libs/permissions.utils.d.ts +58 -0
  109. package/lib/typescript/src/Shared/Libs/permissions.utils.d.ts.map +1 -0
  110. package/lib/typescript/src/Shared/Libs/status-bar.utils.d.ts.map +1 -1
  111. package/lib/typescript/src/Shared/Types/analytics.types.d.ts +2 -0
  112. package/lib/typescript/src/Shared/Types/analytics.types.d.ts.map +1 -1
  113. package/lib/typescript/src/Shared/Types/identificationInfo.d.ts +10 -1
  114. package/lib/typescript/src/Shared/Types/identificationInfo.d.ts.map +1 -1
  115. package/lib/typescript/src/Translation/Resources/en.d.ts +41 -1
  116. package/lib/typescript/src/Translation/Resources/en.d.ts.map +1 -1
  117. package/lib/typescript/src/Translation/Resources/tr.d.ts +41 -1
  118. package/lib/typescript/src/Translation/Resources/tr.d.ts.map +1 -1
  119. package/lib/typescript/src/Trustchex.d.ts.map +1 -1
  120. package/lib/typescript/src/version.d.ts +1 -1
  121. package/package.json +7 -4
  122. package/src/Screens/Debug/NFCScanTestScreen.tsx +692 -0
  123. package/src/Screens/Dynamic/ContractAcceptanceScreen.tsx +1 -4
  124. package/src/Screens/Dynamic/IdentityDocumentEIDScanningScreen.tsx +21 -4
  125. package/src/Screens/Dynamic/LivenessDetectionScreen.tsx +165 -24
  126. package/src/Screens/Dynamic/VerbalConsentScreen.tsx +1401 -0
  127. package/src/Screens/Dynamic/VideoCallScreen.tsx +11 -20
  128. package/src/Screens/Static/ResultScreen.tsx +187 -31
  129. package/src/Screens/Static/VerificationSessionCheckScreen.tsx +9 -0
  130. package/src/Shared/Animations/recording.json +1 -0
  131. package/src/Shared/Components/DebugNavigationPanel.tsx +73 -48
  132. package/src/Shared/Components/EIDScanner.tsx +280 -119
  133. package/src/Shared/Components/FaceCamera.tsx +19 -16
  134. package/src/Shared/Components/IdentityDocumentCamera.flows.ts +7 -4
  135. package/src/Shared/Components/IdentityDocumentCamera.tsx +206 -191
  136. package/src/Shared/Components/IdentityDocumentCamera.utils.ts +13 -4
  137. package/src/Shared/Components/NavigationManager.tsx +27 -18
  138. package/src/Shared/EIDReader/aesSecureMessagingWrapper.ts +69 -0
  139. package/src/Shared/EIDReader/apduLevelPACECapable.ts +34 -0
  140. package/src/Shared/EIDReader/bacKey.ts +24 -8
  141. package/src/Shared/EIDReader/eidReader.ts +398 -12
  142. package/src/Shared/EIDReader/eidService.ts +49 -1
  143. package/src/Shared/EIDReader/nfcManagerCardService.ts +4 -6
  144. package/src/Shared/EIDReader/paceInfo.ts +159 -0
  145. package/src/Shared/EIDReader/paceKeySpec.ts +56 -0
  146. package/src/Shared/EIDReader/protocol/paceAPDUSender.ts +163 -0
  147. package/src/Shared/EIDReader/protocol/paceProtocol.ts +946 -0
  148. package/src/Shared/EIDReader/protocol/paceResult.ts +62 -0
  149. package/src/Shared/EIDReader/secureMessagingWrapper.ts +28 -10
  150. package/src/Shared/EIDReader/smartcards/commandAPDU.ts +2 -1
  151. package/src/Shared/EIDReader/tlv/tlv.helpers.ts +1 -1
  152. package/src/Shared/EIDReader/tlv/tlv.utils.ts +8 -5
  153. package/src/Shared/EIDReader/utils/aesCrypto.utils.ts +217 -0
  154. package/src/Shared/Libs/PERMISSIONS_MANAGER.md +268 -0
  155. package/src/Shared/Libs/analytics.utils.ts +4 -0
  156. package/src/Shared/Libs/contains.ts +0 -53
  157. package/src/Shared/Libs/country-display.utils.ts +55 -0
  158. package/src/Shared/Libs/crypto.utils.ts +2 -2
  159. package/src/Shared/Libs/demo.utils.ts +10 -0
  160. package/src/Shared/Libs/http-client.ts +12 -4
  161. package/src/Shared/Libs/index.ts +63 -0
  162. package/src/Shared/Libs/mrz.utils.ts +3 -2
  163. package/src/Shared/Libs/permissions.utils.ts +251 -0
  164. package/src/Shared/Libs/status-bar.utils.ts +4 -2
  165. package/src/Shared/Services/VideoSessionService.ts +1 -1
  166. package/src/Shared/Types/analytics.types.ts +2 -0
  167. package/src/Shared/Types/identificationInfo.ts +11 -0
  168. package/src/Translation/Resources/en.ts +64 -3
  169. package/src/Translation/Resources/tr.ts +63 -3
  170. package/src/Trustchex.tsx +53 -17
  171. 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
- val activity = currentActivity
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
- val activity = currentActivity
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 CameraSelector.DEFAULT_FRONT_CAMERA
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
- .setTargetResolution(targetResolution)
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
- .setTargetResolution(targetResolution)
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
- sendRecordingFinishedEvent(filePath)
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
- // For document scanning, prefer wide angle camera for all models
244
- // This provides consistent behavior across iPhone 15, 15 Pro, and other devices
245
- if let wideAngleCamera = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) {
246
- return wideAngleCamera
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
- // Fallback: Try dual camera
250
- if let dualCamera = AVCaptureDevice.default(.builtInDualCamera, for: .video, position: .back) {
251
- return dualCamera
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: Try triple camera
255
- if let tripleCamera = AVCaptureDevice.default(.builtInTripleCamera, for: .video, position: .back) {
256
- return tripleCamera
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 back camera
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 connection exists
491
- guard movieOutput.connection(with: .video) != nil else { return }
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).mp4"
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
- if let connection = self?.previewLayer?.connection {
651
- if connection.isVideoMirroringSupported {
652
- connection.isVideoMirrored = (type == "front")
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
+ }