@trustchex/react-native-sdk 1.464.0 → 1.475.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/android/build.gradle +3 -3
  2. package/android/src/main/java/com/trustchex/reactnativesdk/camera/TrustchexCameraView.kt +190 -12
  3. package/ios/Camera/TrustchexCameraView.swift +244 -46
  4. package/ios/Permission/PermissionModule.m +22 -0
  5. package/ios/Permission/PermissionModule.swift +67 -0
  6. package/lib/module/Screens/Debug/MRZTestScreen.js +121 -147
  7. package/lib/module/Screens/Dynamic/LivenessDetectionScreen.js +24 -3
  8. package/lib/module/Screens/Dynamic/VerbalConsentScreen.js +1 -1
  9. package/lib/module/Screens/Dynamic/VideoCallScreen.js +7 -12
  10. package/lib/module/Screens/Static/ResultScreen.js +9 -1
  11. package/lib/module/Shared/Components/EIDScanner.js +166 -163
  12. package/lib/module/Shared/Components/FaceCamera.js +14 -8
  13. package/lib/module/Shared/Components/IdentityDocumentCamera.js +45 -10
  14. package/lib/module/Shared/Libs/MRZ_KNOWN_ISSUES.md +112 -0
  15. package/lib/module/Shared/Libs/PERMISSIONS_MANAGER.md +268 -0
  16. package/lib/module/Shared/Libs/index.js +20 -0
  17. package/lib/module/Shared/Libs/mrz.utils.js +570 -16
  18. package/lib/module/Shared/Libs/mrzFrameAggregator.js +301 -0
  19. package/lib/module/Shared/Libs/mrzOcrIntegration.js +109 -0
  20. package/lib/module/Shared/Libs/permissions.utils.js +199 -0
  21. package/lib/module/Translation/Resources/en.js +1 -0
  22. package/lib/module/Translation/Resources/tr.js +1 -0
  23. package/lib/module/version.js +1 -1
  24. package/lib/typescript/src/Screens/Debug/MRZTestScreen.d.ts.map +1 -1
  25. package/lib/typescript/src/Screens/Dynamic/LivenessDetectionScreen.d.ts.map +1 -1
  26. package/lib/typescript/src/Screens/Dynamic/VideoCallScreen.d.ts.map +1 -1
  27. package/lib/typescript/src/Screens/Static/ResultScreen.d.ts.map +1 -1
  28. package/lib/typescript/src/Shared/Components/EIDScanner.d.ts.map +1 -1
  29. package/lib/typescript/src/Shared/Components/FaceCamera.d.ts.map +1 -1
  30. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.d.ts.map +1 -1
  31. package/lib/typescript/src/Shared/Libs/index.d.ts +20 -0
  32. package/lib/typescript/src/Shared/Libs/index.d.ts.map +1 -0
  33. package/lib/typescript/src/Shared/Libs/mrz.utils.d.ts +8 -0
  34. package/lib/typescript/src/Shared/Libs/mrz.utils.d.ts.map +1 -1
  35. package/lib/typescript/src/Shared/Libs/mrzFrameAggregator.d.ts +76 -0
  36. package/lib/typescript/src/Shared/Libs/mrzFrameAggregator.d.ts.map +1 -0
  37. package/lib/typescript/src/Shared/Libs/mrzOcrIntegration.d.ts +73 -0
  38. package/lib/typescript/src/Shared/Libs/mrzOcrIntegration.d.ts.map +1 -0
  39. package/lib/typescript/src/Shared/Libs/permissions.utils.d.ts +58 -0
  40. package/lib/typescript/src/Shared/Libs/permissions.utils.d.ts.map +1 -0
  41. package/lib/typescript/src/Translation/Resources/en.d.ts +1 -0
  42. package/lib/typescript/src/Translation/Resources/en.d.ts.map +1 -1
  43. package/lib/typescript/src/Translation/Resources/tr.d.ts +1 -0
  44. package/lib/typescript/src/Translation/Resources/tr.d.ts.map +1 -1
  45. package/lib/typescript/src/version.d.ts +1 -1
  46. package/package.json +14 -10
  47. package/src/Screens/Debug/MRZTestScreen.tsx +137 -166
  48. package/src/Screens/Dynamic/LivenessDetectionScreen.tsx +45 -5
  49. package/src/Screens/Dynamic/VerbalConsentScreen.tsx +1 -1
  50. package/src/Screens/Dynamic/VideoCallScreen.tsx +8 -19
  51. package/src/Screens/Static/ResultScreen.tsx +18 -9
  52. package/src/Shared/Components/EIDScanner.tsx +210 -160
  53. package/src/Shared/Components/FaceCamera.tsx +19 -16
  54. package/src/Shared/Components/IdentityDocumentCamera.tsx +53 -13
  55. package/src/Shared/Libs/MRZ_KNOWN_ISSUES.md +112 -0
  56. package/src/Shared/Libs/PERMISSIONS_MANAGER.md +268 -0
  57. package/src/Shared/Libs/index.ts +63 -0
  58. package/src/Shared/Libs/mrz.utils.ts +639 -11
  59. package/src/Shared/Libs/mrzFrameAggregator.ts +370 -0
  60. package/src/Shared/Libs/mrzOcrIntegration.ts +175 -0
  61. package/src/Shared/Libs/permissions.utils.ts +251 -0
  62. package/src/Translation/Resources/en.ts +1 -0
  63. package/src/Translation/Resources/tr.ts +1 -0
  64. package/src/version.ts +1 -1
@@ -83,9 +83,9 @@ dependencies {
83
83
  implementation "androidx.camera:camera-video:1.4.0"
84
84
 
85
85
  // Google ML Kit dependencies
86
- implementation 'com.google.mlkit:text-recognition:16.0.0'
87
- implementation 'com.google.mlkit:face-detection:16.1.5'
88
- implementation 'com.google.mlkit:barcode-scanning:17.1.0'
86
+ implementation 'com.google.mlkit:text-recognition:16.0.1'
87
+ implementation 'com.google.mlkit:face-detection:16.1.7'
88
+ implementation 'com.google.mlkit:barcode-scanning:17.3.0'
89
89
 
90
90
  // Google Play Services dependency for Tasks
91
91
  implementation 'com.google.android.gms:play-services-tasks:18.2.0'
@@ -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
- .setTargetResolution(targetResolution)
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
- .setTargetResolution(targetResolution)
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)
@@ -439,9 +452,20 @@ class TrustchexCameraView(context: ThemedReactContext) : FrameLayout(context) {
439
452
  // Brightness calculation restricted to scanning frame area (between 36% from top and 36% from bottom, 5% margins on sides)
440
453
  val averageBrightness = computeYPlaneBrightness(imageProxy, reportedWidth, reportedHeight)
441
454
 
442
- // Use original inputImage directly for text recognition
443
- // ML Kit works best with native camera frames, not processed bitmaps
444
- val textInputImage = inputImage
455
+ // Text recognition: process ONLY the data inside the on-screen scan
456
+ // frame. We capture the full image but crop to the scan-frame ROI
457
+ // (top 36% / bottom 36% / 5% side margins — the same rectangle the UI
458
+ // guide and brightness sampling use) and UPSCALE it before OCR. This
459
+ // (a) excludes front-of-card text and surrounding noise (screen glare,
460
+ // IDE/browser chrome) that pollute the MRZ read, and (b) gives ML Kit
461
+ // more pixels per character — ML Kit needs ≥16px/char, and a full-frame
462
+ // capture leaves a 30–44-char MRZ line below that floor. Block
463
+ // coordinates from the ROI pass are mapped back to the full portrait
464
+ // frame so the JS overlay/filters stay correct.
465
+ val roi = if (textRecognitionEnabled) {
466
+ buildScanFrameRoiImage(imageProxy, rotationDegrees, reportedWidth, reportedHeight)
467
+ } else null
468
+ val textInputImage = roi?.image ?: inputImage
445
469
 
446
470
  // Generate JPEG base64 only when JS side explicitly needs the image
447
471
  // NOTE: Do NOT auto-generate for face detection - too expensive, causes frame drops
@@ -526,13 +550,23 @@ class TrustchexCameraView(context: ThemedReactContext) : FrameLayout(context) {
526
550
  blockMap.putString("text", block.text)
527
551
  val bb = block.boundingBox
528
552
  if (bb != null) {
553
+ // When OCR ran on the upscaled scan-frame ROI, map
554
+ // the box back to full portrait-frame coordinates:
555
+ // divide by the ROI upscale, then add the ROI origin.
556
+ val s = roi?.scale ?: 1f
557
+ val ox = roi?.offsetX ?: 0
558
+ val oy = roi?.offsetY ?: 0
559
+ val left = (bb.left / s).toInt() + ox
560
+ val top = (bb.top / s).toInt() + oy
561
+ val w = (bb.width() / s).toInt()
562
+ val h = (bb.height() / s).toInt()
529
563
  val boxMap = Arguments.createMap()
530
- boxMap.putInt("x", bb.left)
531
- boxMap.putInt("y", bb.top)
532
- boxMap.putInt("width", bb.width())
533
- boxMap.putInt("height", bb.height())
534
- boxMap.putInt("boundingCenterX", bb.centerX())
535
- boxMap.putInt("boundingCenterY", bb.centerY())
564
+ boxMap.putInt("x", left)
565
+ boxMap.putInt("y", top)
566
+ boxMap.putInt("width", w)
567
+ boxMap.putInt("height", h)
568
+ boxMap.putInt("boundingCenterX", left + w / 2)
569
+ boxMap.putInt("boundingCenterY", top + h / 2)
536
570
  blockMap.putMap("blockFrame", boxMap)
537
571
  }
538
572
  blocksArray.pushMap(blockMap)
@@ -777,6 +811,16 @@ class TrustchexCameraView(context: ThemedReactContext) : FrameLayout(context) {
777
811
  return
778
812
  }
779
813
 
814
+ val hasMicPermission = ContextCompat.checkSelfPermission(
815
+ reactContext,
816
+ Manifest.permission.RECORD_AUDIO
817
+ ) == PackageManager.PERMISSION_GRANTED
818
+
819
+ if (!hasMicPermission) {
820
+ sendRecordingErrorEvent("Microphone permission is required for verbal consent recording")
821
+ return
822
+ }
823
+
780
824
  val videoCapture = videoCapture ?: run {
781
825
  sendRecordingErrorEvent("VideoCapture not initialized")
782
826
  return
@@ -813,7 +857,24 @@ class TrustchexCameraView(context: ThemedReactContext) : FrameLayout(context) {
813
857
  sendRecordingErrorEvent("Recording error: ${event.cause?.message}")
814
858
  } else {
815
859
  val filePath = videoFile.absolutePath
816
- sendRecordingFinishedEvent(filePath)
860
+ val hasAudioTrack = try {
861
+ val retriever = MediaMetadataRetriever()
862
+ retriever.setDataSource(filePath)
863
+ val hasAudio = retriever
864
+ .extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_AUDIO)
865
+ retriever.release()
866
+ hasAudio == "yes"
867
+ } catch (_: Exception) {
868
+ false
869
+ }
870
+
871
+ if (!hasAudioTrack) {
872
+ videoFile.delete()
873
+ currentRecordingFile = null
874
+ sendRecordingErrorEvent("Recorded video has no audio track")
875
+ } else {
876
+ sendRecordingFinishedEvent(filePath)
877
+ }
817
878
  }
818
879
  }
819
880
  else -> {}
@@ -977,6 +1038,123 @@ class TrustchexCameraView(context: ThemedReactContext) : FrameLayout(context) {
977
1038
  }
978
1039
  }
979
1040
 
1041
+ /** A scan-frame ROI prepared for OCR, with the transform back to full-frame coords. */
1042
+ private data class ScanFrameRoi(
1043
+ val image: InputImage,
1044
+ val offsetX: Int,
1045
+ val offsetY: Int,
1046
+ val scale: Float
1047
+ )
1048
+
1049
+ /**
1050
+ * Builds an [InputImage] cropped to the on-screen scan frame (top 36% /
1051
+ * bottom 36% / 5% side margins — the same rectangle the UI guide draws) and
1052
+ * UPSCALED so ML Kit gets more pixels per character. We capture the full frame
1053
+ * but OCR only this region, which removes front-of-card text and surrounding
1054
+ * noise and lifts a dense MRZ line above ML Kit's ≥16px/char floor. Returns
1055
+ * null on any failure so the caller falls back to full-frame OCR.
1056
+ */
1057
+ private fun buildScanFrameRoiImage(
1058
+ imageProxy: ImageProxy,
1059
+ rotationDegrees: Int,
1060
+ portraitWidth: Int,
1061
+ portraitHeight: Int
1062
+ ): ScanFrameRoi? {
1063
+ return try {
1064
+ val full = yuvImageToBitmap(imageProxy, rotationDegrees) ?: return null
1065
+
1066
+ // Scan-frame rectangle in the rotated (portrait) bitmap. Covers the
1067
+ // lower-middle band where the MRZ lands, with generous vertical
1068
+ // tolerance (top 25% → bottom 12%) so the band doesn't have to be
1069
+ // perfectly centered to be OCR'd. Keep it large enough for tolerance
1070
+ // but not the whole frame (front text/glare would creep back in).
1071
+ val scanTopPercent = 0.25
1072
+ val scanBottomPercent = 0.12
1073
+ val scanLeftPercent = 0.04
1074
+ val scanRightPercent = 0.04
1075
+ val left = (full.width * scanLeftPercent).toInt().coerceIn(0, full.width - 1)
1076
+ val right = (full.width * (1.0 - scanRightPercent)).toInt().coerceIn(left + 1, full.width)
1077
+ val top = (full.height * scanTopPercent).toInt().coerceIn(0, full.height - 1)
1078
+ val bottom = (full.height * (1.0 - scanBottomPercent)).toInt().coerceIn(top + 1, full.height)
1079
+ val cropW = right - left
1080
+ val cropH = bottom - top
1081
+ if (cropW < 8 || cropH < 8) {
1082
+ full.recycle()
1083
+ return null
1084
+ }
1085
+
1086
+ val cropped = Bitmap.createBitmap(full, left, top, cropW, cropH)
1087
+ full.recycle()
1088
+
1089
+ // Upscale so a ~30–44-char MRZ line clears the per-character pixel
1090
+ // minimum. Cap the long edge to keep OCR latency bounded.
1091
+ val maxLongEdge = 2200
1092
+ val longEdge = max(cropW, cropH)
1093
+ val rawScale = if (longEdge > 0) (maxLongEdge.toFloat() / longEdge) else 1f
1094
+ val scale = rawScale.coerceIn(1f, 2.5f)
1095
+ val outW = (cropW * scale).toInt()
1096
+ val outH = (cropH * scale).toInt()
1097
+ val scaled = if (scale != 1f) {
1098
+ val s = Bitmap.createScaledBitmap(cropped, outW, outH, true)
1099
+ cropped.recycle()
1100
+ s
1101
+ } else {
1102
+ cropped
1103
+ }
1104
+
1105
+ // Grayscale + contrast boost: the MRZ is black OCR-B on a light
1106
+ // background, so desaturating and stretching contrast makes glyphs
1107
+ // crisp and suppresses the dominant filler confusion (the "<" angle
1108
+ // bracket misread as C/E/K under glare). Done with a ColorMatrix on a
1109
+ // Canvas — no OpenCV dependency.
1110
+ val enhanced = enhanceForOcr(scaled)
1111
+ if (enhanced !== scaled) scaled.recycle()
1112
+
1113
+ // The bitmap is already rotated to portrait, so rotation = 0 here.
1114
+ ScanFrameRoi(
1115
+ image = InputImage.fromBitmap(enhanced, 0),
1116
+ offsetX = left,
1117
+ offsetY = top,
1118
+ scale = scale
1119
+ )
1120
+ } catch (e: Exception) {
1121
+ null
1122
+ }
1123
+ }
1124
+
1125
+ /**
1126
+ * Desaturate + boost contrast to make dark OCR-B glyphs pop against the light
1127
+ * MRZ background, which suppresses the "<" filler being misread as C/E/K under
1128
+ * glare. Uses a single ColorMatrix (saturation 0, then a contrast scale about
1129
+ * mid-grey). Returns the original bitmap unchanged on failure.
1130
+ */
1131
+ private fun enhanceForOcr(src: Bitmap): Bitmap {
1132
+ return try {
1133
+ val out = Bitmap.createBitmap(src.width, src.height, Bitmap.Config.ARGB_8888)
1134
+ val canvas = android.graphics.Canvas(out)
1135
+ val paint = android.graphics.Paint()
1136
+
1137
+ val gray = android.graphics.ColorMatrix().apply { setSaturation(0f) }
1138
+ // Contrast about mid-grey (128): out = c*in + (1-c)*128.
1139
+ val c = 1.6f
1140
+ val t = (1f - c) * 128f
1141
+ val contrast = android.graphics.ColorMatrix(
1142
+ floatArrayOf(
1143
+ c, 0f, 0f, 0f, t,
1144
+ 0f, c, 0f, 0f, t,
1145
+ 0f, 0f, c, 0f, t,
1146
+ 0f, 0f, 0f, 1f, 0f
1147
+ )
1148
+ )
1149
+ gray.postConcat(contrast)
1150
+ paint.colorFilter = android.graphics.ColorMatrixColorFilter(gray)
1151
+ canvas.drawBitmap(src, 0f, 0f, paint)
1152
+ out
1153
+ } catch (e: Exception) {
1154
+ src
1155
+ }
1156
+ }
1157
+
980
1158
  fun cleanup() {
981
1159
  // Cancel any active recording and delete file
982
1160
  if (activeRecording != null) {