@trustchex/react-native-sdk 1.500.1 → 1.501.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 (100) hide show
  1. package/android/src/main/java/com/trustchex/reactnativesdk/GeolocationModule.kt +159 -0
  2. package/android/src/main/java/com/trustchex/reactnativesdk/TrustchexSDKPackage.kt +12 -0
  3. package/ios/Permission/CoarseLocationProvider.swift +111 -0
  4. package/ios/Permission/PermissionModule.m +4 -0
  5. package/ios/Permission/PermissionModule.swift +21 -1
  6. package/lib/module/Screens/Dynamic/AddressCaptureScreen.js +230 -0
  7. package/lib/module/Screens/Dynamic/ContractAcceptanceScreen.js +12 -0
  8. package/lib/module/Screens/Dynamic/IdentityDocumentEIDScanningScreen.js +33 -10
  9. package/lib/module/Screens/Dynamic/IdentityDocumentScanningScreen.js +8 -10
  10. package/lib/module/Screens/Static/ResultScreen.js +74 -3
  11. package/lib/module/Shared/Components/EIDScanner.js +59 -3
  12. package/lib/module/Shared/Components/NavigationManager.js +6 -2
  13. package/lib/module/Shared/EIDReader/eidReader.js +52 -3
  14. package/lib/module/Shared/EIDReader/java/inputStream.js +6 -0
  15. package/lib/module/Shared/EIDReader/lds/icao/dg11File.js +60 -20
  16. package/lib/module/Shared/EIDReader/passiveAuth/der.js +93 -0
  17. package/lib/module/Shared/EIDReader/passiveAuth/integrity.js +71 -0
  18. package/lib/module/Shared/EIDReader/passiveAuth/sodParser.js +123 -0
  19. package/lib/module/Shared/Libs/chipVizMatch.js +93 -0
  20. package/lib/module/Shared/Libs/country-allow.js +16 -0
  21. package/lib/module/Shared/Libs/documentDataNormalizer.js +21 -11
  22. package/lib/module/Shared/Libs/geolocation.utils.js +77 -0
  23. package/lib/module/Shared/Libs/mrz.utils.js +24 -1
  24. package/lib/module/Shared/Libs/promise.utils.js +6 -0
  25. package/lib/module/Shared/Services/DataUploadService.js +18 -1
  26. package/lib/module/Translation/Resources/en.js +16 -0
  27. package/lib/module/Translation/Resources/tr.js +16 -0
  28. package/lib/module/Trustchex.js +4 -0
  29. package/lib/module/version.js +1 -1
  30. package/lib/typescript/src/Screens/Dynamic/AddressCaptureScreen.d.ts +9 -0
  31. package/lib/typescript/src/Screens/Dynamic/AddressCaptureScreen.d.ts.map +1 -0
  32. package/lib/typescript/src/Screens/Dynamic/ContractAcceptanceScreen.d.ts.map +1 -1
  33. package/lib/typescript/src/Screens/Dynamic/IdentityDocumentEIDScanningScreen.d.ts.map +1 -1
  34. package/lib/typescript/src/Screens/Dynamic/IdentityDocumentScanningScreen.d.ts.map +1 -1
  35. package/lib/typescript/src/Screens/Static/ResultScreen.d.ts.map +1 -1
  36. package/lib/typescript/src/Shared/Components/EIDScanner.d.ts +7 -1
  37. package/lib/typescript/src/Shared/Components/EIDScanner.d.ts.map +1 -1
  38. package/lib/typescript/src/Shared/Components/NavigationManager.d.ts.map +1 -1
  39. package/lib/typescript/src/Shared/EIDReader/eidReader.d.ts +14 -1
  40. package/lib/typescript/src/Shared/EIDReader/eidReader.d.ts.map +1 -1
  41. package/lib/typescript/src/Shared/EIDReader/java/inputStream.d.ts +3 -0
  42. package/lib/typescript/src/Shared/EIDReader/java/inputStream.d.ts.map +1 -1
  43. package/lib/typescript/src/Shared/EIDReader/lds/icao/dg11File.d.ts +20 -7
  44. package/lib/typescript/src/Shared/EIDReader/lds/icao/dg11File.d.ts.map +1 -1
  45. package/lib/typescript/src/Shared/EIDReader/passiveAuth/der.d.ts +31 -0
  46. package/lib/typescript/src/Shared/EIDReader/passiveAuth/der.d.ts.map +1 -0
  47. package/lib/typescript/src/Shared/EIDReader/passiveAuth/integrity.d.ts +31 -0
  48. package/lib/typescript/src/Shared/EIDReader/passiveAuth/integrity.d.ts.map +1 -0
  49. package/lib/typescript/src/Shared/EIDReader/passiveAuth/sodParser.d.ts +12 -0
  50. package/lib/typescript/src/Shared/EIDReader/passiveAuth/sodParser.d.ts.map +1 -0
  51. package/lib/typescript/src/Shared/Libs/chipVizMatch.d.ts +68 -0
  52. package/lib/typescript/src/Shared/Libs/chipVizMatch.d.ts.map +1 -0
  53. package/lib/typescript/src/Shared/Libs/country-allow.d.ts +10 -0
  54. package/lib/typescript/src/Shared/Libs/country-allow.d.ts.map +1 -0
  55. package/lib/typescript/src/Shared/Libs/documentDataNormalizer.d.ts.map +1 -1
  56. package/lib/typescript/src/Shared/Libs/geolocation.utils.d.ts +19 -0
  57. package/lib/typescript/src/Shared/Libs/geolocation.utils.d.ts.map +1 -0
  58. package/lib/typescript/src/Shared/Libs/mrz.utils.d.ts.map +1 -1
  59. package/lib/typescript/src/Shared/Libs/promise.utils.d.ts.map +1 -1
  60. package/lib/typescript/src/Shared/Services/DataUploadService.d.ts.map +1 -1
  61. package/lib/typescript/src/Shared/Types/documentData.d.ts +7 -0
  62. package/lib/typescript/src/Shared/Types/documentData.d.ts.map +1 -1
  63. package/lib/typescript/src/Shared/Types/documentReadResult.d.ts +20 -0
  64. package/lib/typescript/src/Shared/Types/documentReadResult.d.ts.map +1 -1
  65. package/lib/typescript/src/Shared/Types/identificationInfo.d.ts +95 -2
  66. package/lib/typescript/src/Shared/Types/identificationInfo.d.ts.map +1 -1
  67. package/lib/typescript/src/Translation/Resources/en.d.ts +16 -0
  68. package/lib/typescript/src/Translation/Resources/en.d.ts.map +1 -1
  69. package/lib/typescript/src/Translation/Resources/tr.d.ts +16 -0
  70. package/lib/typescript/src/Translation/Resources/tr.d.ts.map +1 -1
  71. package/lib/typescript/src/Trustchex.d.ts.map +1 -1
  72. package/lib/typescript/src/version.d.ts +1 -1
  73. package/package.json +1 -1
  74. package/src/Screens/Dynamic/AddressCaptureScreen.tsx +196 -0
  75. package/src/Screens/Dynamic/ContractAcceptanceScreen.tsx +12 -0
  76. package/src/Screens/Dynamic/IdentityDocumentEIDScanningScreen.tsx +46 -14
  77. package/src/Screens/Dynamic/IdentityDocumentScanningScreen.tsx +9 -11
  78. package/src/Screens/Static/ResultScreen.tsx +101 -6
  79. package/src/Shared/Components/EIDScanner.tsx +69 -2
  80. package/src/Shared/Components/NavigationManager.tsx +6 -0
  81. package/src/Shared/EIDReader/eidReader.ts +75 -2
  82. package/src/Shared/EIDReader/java/inputStream.ts +6 -0
  83. package/src/Shared/EIDReader/lds/icao/dg11File.ts +68 -22
  84. package/src/Shared/EIDReader/passiveAuth/der.ts +110 -0
  85. package/src/Shared/EIDReader/passiveAuth/integrity.ts +97 -0
  86. package/src/Shared/EIDReader/passiveAuth/sodParser.ts +142 -0
  87. package/src/Shared/Libs/chipVizMatch.ts +149 -0
  88. package/src/Shared/Libs/country-allow.ts +17 -0
  89. package/src/Shared/Libs/documentDataNormalizer.ts +24 -11
  90. package/src/Shared/Libs/geolocation.utils.ts +87 -0
  91. package/src/Shared/Libs/mrz.utils.ts +27 -1
  92. package/src/Shared/Libs/promise.utils.ts +11 -0
  93. package/src/Shared/Services/DataUploadService.ts +23 -0
  94. package/src/Shared/Types/documentData.ts +8 -0
  95. package/src/Shared/Types/documentReadResult.ts +20 -0
  96. package/src/Shared/Types/identificationInfo.ts +97 -2
  97. package/src/Translation/Resources/en.ts +21 -0
  98. package/src/Translation/Resources/tr.ts +21 -0
  99. package/src/Trustchex.tsx +5 -0
  100. package/src/version.ts +1 -1
@@ -0,0 +1,159 @@
1
+ package com.trustchex.reactnativesdk
2
+
3
+ import android.Manifest
4
+ import android.content.Context
5
+ import android.content.pm.PackageManager
6
+ import android.location.Location
7
+ import android.location.LocationListener
8
+ import android.location.LocationManager
9
+ import android.os.Handler
10
+ import android.os.Looper
11
+ import android.os.SystemClock
12
+ import com.facebook.react.bridge.Promise
13
+ import com.facebook.react.bridge.ReactApplicationContext
14
+ import com.facebook.react.bridge.ReactContextBaseJavaModule
15
+ import com.facebook.react.bridge.ReactMethod
16
+ import java.util.Locale
17
+
18
+ /**
19
+ * One-shot coarse geolocation for the MASAK Tebliğ No. 32 Article 4/C(1)(e)
20
+ * technical-data requirement, using the system [LocationManager] — no
21
+ * third-party module. The host app must declare ACCESS_COARSE_LOCATION and have
22
+ * been granted it at runtime (RN handles the runtime request via
23
+ * PermissionsAndroid); otherwise this resolves to null.
24
+ *
25
+ * Everything is fail-soft: missing permission, no provider, an error, or a
26
+ * timeout all resolve to null (rounded "lat,lng" otherwise) so the verification
27
+ * flow proceeds — the backend treats a missing fix as "not provided".
28
+ */
29
+ class GeolocationModule(private val reactContext: ReactApplicationContext) :
30
+ ReactContextBaseJavaModule(reactContext) {
31
+
32
+ private val handler = Handler(Looper.getMainLooper())
33
+
34
+ override fun getName(): String = NAME
35
+
36
+ @ReactMethod
37
+ fun getCoarseLocation(timeoutMs: Double, promise: Promise) {
38
+ var settled = false
39
+ var manager: LocationManager? = null
40
+ var listener: LocationListener? = null
41
+ var timeoutRunnable: Runnable? = null
42
+
43
+ fun finish(value: String?) {
44
+ if (!settled) {
45
+ settled = true
46
+ timeoutRunnable?.let { handler.removeCallbacks(it) }
47
+ // Stop the live single-update so the radio/listener isn't left running
48
+ // after we've resolved (e.g. on timeout).
49
+ val l = listener
50
+ val m = manager
51
+ if (l != null && m != null) {
52
+ try {
53
+ m.removeUpdates(l)
54
+ } catch (e: Exception) {
55
+ // ignore
56
+ }
57
+ }
58
+ listener = null
59
+ promise.resolve(value)
60
+ }
61
+ }
62
+
63
+ val granted = reactContext.checkSelfPermission(
64
+ Manifest.permission.ACCESS_COARSE_LOCATION
65
+ ) == PackageManager.PERMISSION_GRANTED ||
66
+ reactContext.checkSelfPermission(
67
+ Manifest.permission.ACCESS_FINE_LOCATION
68
+ ) == PackageManager.PERMISSION_GRANTED
69
+ if (!granted) {
70
+ finish(null)
71
+ return
72
+ }
73
+
74
+ val locationManager =
75
+ reactContext.getSystemService(Context.LOCATION_SERVICE) as? LocationManager
76
+ if (locationManager == null) {
77
+ finish(null)
78
+ return
79
+ }
80
+ manager = locationManager
81
+
82
+ handler.post {
83
+ // A recent cached fix is a valid coarse answer and avoids waiting on the
84
+ // radio. An OLD one is not — submitting a stale coordinate as the
85
+ // "current" verification location would be wrong, so it is bounded by
86
+ // recency and otherwise ignored (matching iOS, which returns nil rather
87
+ // than a stale fix).
88
+ format(recentLastKnown(locationManager))?.let {
89
+ finish(it)
90
+ return@post
91
+ }
92
+
93
+ // Fail-soft timeout so a never-returning fix can't hang the flow. On
94
+ // timeout we resolve null (no fresh fix) — never a stale last-known.
95
+ val timeout = Runnable { finish(null) }
96
+ timeoutRunnable = timeout
97
+ handler.postDelayed(timeout, timeoutMs.toLong())
98
+
99
+ val provider = when {
100
+ locationManager.isProviderEnabledSafe(LocationManager.NETWORK_PROVIDER) ->
101
+ LocationManager.NETWORK_PROVIDER
102
+ locationManager.isProviderEnabledSafe(LocationManager.GPS_PROVIDER) ->
103
+ LocationManager.GPS_PROVIDER
104
+ else -> null
105
+ }
106
+ if (provider == null) {
107
+ finish(null)
108
+ return@post
109
+ }
110
+
111
+ val l = LocationListener { location -> finish(format(location)) }
112
+ listener = l
113
+ try {
114
+ locationManager.requestSingleUpdate(provider, l, Looper.getMainLooper())
115
+ } catch (e: Exception) {
116
+ finish(null)
117
+ }
118
+ }
119
+ }
120
+
121
+ /** Last-known fix, accepted only when newer than [MAX_AGE_MS]. */
122
+ private fun recentLastKnown(manager: LocationManager): Location? {
123
+ val location = try {
124
+ manager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER)
125
+ ?: manager.getLastKnownLocation(LocationManager.GPS_PROVIDER)
126
+ } catch (e: SecurityException) {
127
+ null
128
+ } catch (e: Exception) {
129
+ null
130
+ } ?: return null
131
+ val ageMs = SystemClock.elapsedRealtimeNanos() / 1_000_000 -
132
+ location.elapsedRealtimeNanos / 1_000_000
133
+ return if (ageMs in 0..MAX_AGE_MS) location else null
134
+ }
135
+
136
+ private fun format(location: Location?): String? {
137
+ if (location == null) return null
138
+ // Round to ~3 decimals (~110 m) — coarse, per privacy guidance. Force
139
+ // Locale.US so the decimal separator is always '.' — a comma-decimal locale
140
+ // (tr, de, ...) would otherwise corrupt the "lat,lng" contract.
141
+ val lat = String.format(Locale.US, "%.3f", location.latitude)
142
+ val lng = String.format(Locale.US, "%.3f", location.longitude)
143
+ return "$lat,$lng"
144
+ }
145
+
146
+ private fun LocationManager.isProviderEnabledSafe(provider: String): Boolean {
147
+ return try {
148
+ isProviderEnabled(provider)
149
+ } catch (e: Exception) {
150
+ false
151
+ }
152
+ }
153
+
154
+ companion object {
155
+ const val NAME = "GeolocationModule"
156
+ /** Accept a cached fix only when it is at most this old (2 minutes). */
157
+ private const val MAX_AGE_MS = 2 * 60 * 1000L
158
+ }
159
+ }
@@ -19,6 +19,7 @@ class TrustchexSDKPackage : BaseReactPackage() {
19
19
  "MLKitModule" -> MLKitModule(reactContext)
20
20
  "OpenCVModule" -> OpenCVModule(reactContext)
21
21
  ImageDecoderModule.NAME -> ImageDecoderModule(reactContext)
22
+ GeolocationModule.NAME -> GeolocationModule(reactContext)
22
23
  else -> null
23
24
  }
24
25
  }
@@ -86,6 +87,17 @@ class TrustchexSDKPackage : BaseReactPackage() {
86
87
  false // isTurboModule
87
88
  )
88
89
 
90
+ // Coarse geolocation module (MASAK 4/C(1)(e))
91
+ moduleInfos[GeolocationModule.NAME] =
92
+ ReactModuleInfo(
93
+ GeolocationModule.NAME,
94
+ GeolocationModule.NAME,
95
+ false, // canOverrideExistingModule
96
+ false, // needsEagerInit
97
+ false, // isCxxModule
98
+ false // isTurboModule
99
+ )
100
+
89
101
  moduleInfos
90
102
  }
91
103
  }
@@ -0,0 +1,111 @@
1
+ import CoreLocation
2
+
3
+ /// One-shot coarse geolocation for the MASAK Tebliğ No. 32 Article 4/C(1)(e)
4
+ /// technical-data requirement, using the system Core Location framework — no
5
+ /// third-party module. Requests when-in-use authorization (the host app must
6
+ /// declare `NSLocationWhenInUseUsageDescription`), takes a single
7
+ /// reduced-accuracy fix, and returns "lat,lng" rounded to ~3 decimals (~110 m),
8
+ /// or nil.
9
+ ///
10
+ /// Everything is fail-soft: denied permission, disabled services, an error, or a
11
+ /// timeout all resolve to nil so the verification flow proceeds — the backend
12
+ /// treats a missing fix as "not provided".
13
+ final class CoarseLocationProvider: NSObject, CLLocationManagerDelegate {
14
+ private let manager = CLLocationManager()
15
+ private var completion: ((String?) -> Void)?
16
+ private var timeoutWork: DispatchWorkItem?
17
+ private var finished = false
18
+
19
+ /// Request a single coarse fix. The completion is always called exactly once,
20
+ /// on the main thread, with "lat,lng" or nil.
21
+ func requestOnce(timeoutMs: Int, completion: @escaping (String?) -> Void) {
22
+ self.completion = completion
23
+
24
+ guard CLLocationManager.locationServicesEnabled() else {
25
+ finish(nil)
26
+ return
27
+ }
28
+
29
+ manager.delegate = self
30
+ manager.desiredAccuracy = kCLLocationAccuracyKilometer
31
+ if #available(iOS 14.0, *) {
32
+ manager.desiredAccuracy = kCLLocationAccuracyReduced
33
+ }
34
+
35
+ // Fail-soft timeout so a never-returning fix can't hang the flow.
36
+ let work = DispatchWorkItem { [weak self] in self?.finish(nil) }
37
+ timeoutWork = work
38
+ DispatchQueue.main.asyncAfter(
39
+ deadline: .now() + .milliseconds(timeoutMs),
40
+ execute: work
41
+ )
42
+
43
+ let status: CLAuthorizationStatus
44
+ if #available(iOS 14.0, *) {
45
+ status = manager.authorizationStatus
46
+ } else {
47
+ status = CLLocationManager.authorizationStatus()
48
+ }
49
+ handle(status: status)
50
+ }
51
+
52
+ private func handle(status: CLAuthorizationStatus) {
53
+ switch status {
54
+ case .notDetermined:
55
+ manager.requestWhenInUseAuthorization()
56
+ case .authorizedWhenInUse, .authorizedAlways:
57
+ manager.requestLocation()
58
+ default:
59
+ finish(nil) // denied / restricted
60
+ }
61
+ }
62
+
63
+ // MARK: CLLocationManagerDelegate
64
+
65
+ @available(iOS 14.0, *)
66
+ func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
67
+ handle(status: manager.authorizationStatus)
68
+ }
69
+
70
+ func locationManager(
71
+ _ manager: CLLocationManager,
72
+ didChangeAuthorization status: CLAuthorizationStatus
73
+ ) {
74
+ // iOS 13 path (the iOS 14 delegate above supersedes this when available).
75
+ if #available(iOS 14.0, *) { return }
76
+ handle(status: status)
77
+ }
78
+
79
+ func locationManager(
80
+ _ manager: CLLocationManager,
81
+ didUpdateLocations locations: [CLLocation]
82
+ ) {
83
+ guard let coordinate = locations.last?.coordinate else {
84
+ finish(nil)
85
+ return
86
+ }
87
+ // Round to ~3 decimals (~110 m) — coarse, per privacy guidance.
88
+ let lat = String(format: "%.3f", coordinate.latitude)
89
+ let lng = String(format: "%.3f", coordinate.longitude)
90
+ finish("\(lat),\(lng)")
91
+ }
92
+
93
+ func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
94
+ finish(nil)
95
+ }
96
+
97
+ private func finish(_ value: String?) {
98
+ guard !finished else { return }
99
+ finished = true
100
+ timeoutWork?.cancel()
101
+ timeoutWork = nil
102
+ manager.delegate = nil
103
+ let completion = self.completion
104
+ self.completion = nil
105
+ if Thread.isMainThread {
106
+ completion?(value)
107
+ } else {
108
+ DispatchQueue.main.async { completion?(value) }
109
+ }
110
+ }
111
+ }
@@ -14,6 +14,10 @@ RCT_EXTERN_METHOD(checkCameraPermission:(RCTPromiseResolveBlock)resolve
14
14
  RCT_EXTERN_METHOD(checkMicrophonePermission:(RCTPromiseResolveBlock)resolve
15
15
  withRejecter:(RCTPromiseRejectBlock)reject)
16
16
 
17
+ RCT_EXTERN_METHOD(getCoarseLocation:(nonnull NSNumber *)timeoutMs
18
+ resolver:(RCTPromiseResolveBlock)resolve
19
+ withRejecter:(RCTPromiseRejectBlock)reject)
20
+
17
21
  + (BOOL)requiresMainQueueSetup
18
22
  {
19
23
  return YES;
@@ -1,9 +1,29 @@
1
1
  import React
2
2
  import AVFoundation
3
+ import CoreLocation
3
4
 
4
5
  @objc(PermissionModule)
5
6
  class PermissionModule: NSObject {
6
-
7
+
8
+ // Each in-flight one-shot request is retained until it completes, so
9
+ // overlapping calls don't release one another's provider mid-flight.
10
+ private var locationProviders: [ObjectIdentifier: CoarseLocationProvider] = [:]
11
+
12
+ /// MASAK Tebliğ 32 Art. 4/C(1)(e): one-shot coarse geolocation via the system
13
+ /// Core Location framework — no third-party module. Resolves "lat,lng"
14
+ /// rounded to ~3 decimals (~110 m) or nil; never rejects (fail-soft).
15
+ @objc(getCoarseLocation:resolver:withRejecter:)
16
+ func getCoarseLocation(timeoutMs: NSNumber, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
17
+ DispatchQueue.main.async {
18
+ let provider = CoarseLocationProvider()
19
+ self.locationProviders[ObjectIdentifier(provider)] = provider
20
+ provider.requestOnce(timeoutMs: timeoutMs.intValue) { [weak self] coordinate in
21
+ self?.locationProviders[ObjectIdentifier(provider)] = nil
22
+ resolve(coordinate)
23
+ }
24
+ }
25
+ }
26
+
7
27
  @objc(requestCameraPermission:withRejecter:)
8
28
  func requestCameraPermission(resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
9
29
  DispatchQueue.main.async {
@@ -0,0 +1,230 @@
1
+ "use strict";
2
+
3
+ import React, { useContext, useEffect, useRef, useState } from 'react';
4
+ import { SafeAreaView, View, Text, TextInput, StyleSheet, ScrollView, Image, TouchableOpacity } from 'react-native';
5
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
6
+ import { useTranslation } from 'react-i18next';
7
+ import { TrustchexCamera } from "../../Shared/Components/TrustchexCamera.js";
8
+ import NavigationManager from "../../Shared/Components/NavigationManager.js";
9
+ import AppContext from "../../Shared/Contexts/AppContext.js";
10
+ import StyledButton from "../../Shared/Components/StyledButton.js";
11
+ import { useStatusBarWhiteBackground } from "../../Shared/Libs/status-bar.utils.js";
12
+ import { useScreenTracking, trackVerificationStart, trackVerificationComplete } from "../../Shared/Libs/analytics.utils.js";
13
+ import { useKeepAwake } from "../../Shared/Libs/native-keep-awake.utils.js";
14
+
15
+ /**
16
+ * ADDRESS_CAPTURE step (MASAK Tebliğ No. 32 Article 4/C(1)(d)). The user confirms
17
+ * their address — pre-filled from the chip's DG11 address when available — and
18
+ * captures a proof document (residence cert / utility bill ≤3 months / public-
19
+ * authority doc). The address is verified later by an operator (manual review).
20
+ */
21
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
22
+ const AddressCaptureScreen = () => {
23
+ useKeepAwake();
24
+ useStatusBarWhiteBackground();
25
+ useScreenTracking('address_capture');
26
+ const appContext = useContext(AppContext);
27
+ const navigationManagerRef = useRef(null);
28
+ const {
29
+ t
30
+ } = useTranslation();
31
+ const insets = useSafeAreaInsets();
32
+
33
+ // Pre-fill from the chip's DG11 address when the NFC read provided one.
34
+ const chipAddress = appContext.lastDocumentRead?.document.address ?? '';
35
+ const [addressText, setAddressText] = useState(chipAddress);
36
+ const [fromChip] = useState(!!chipAddress);
37
+ const [proofImage, setProofImage] = useState();
38
+ const [isCameraOpen, setIsCameraOpen] = useState(false);
39
+ const captureNextFrame = useRef(false);
40
+
41
+ // Whether the proof document is required is configurable per workflow step.
42
+ const proofRequired = appContext.currentWorkflowStep?.data?.addressProofRequired !== false;
43
+ useEffect(() => {
44
+ trackVerificationStart('ADDRESS_CAPTURE');
45
+ }, []);
46
+ const onFrameAvailable = frame => {
47
+ if (!captureNextFrame.current) return;
48
+ const b64 = frame.base64Image;
49
+ if (b64) {
50
+ captureNextFrame.current = false;
51
+ setProofImage(b64);
52
+ setIsCameraOpen(false);
53
+ }
54
+ };
55
+ const canContinue = addressText.trim().length > 0 && (!proofRequired || !!proofImage);
56
+ const onContinue = () => {
57
+ appContext.identificationInfo.address = {
58
+ addressText: addressText.trim(),
59
+ fromChip,
60
+ proofImage,
61
+ proofImageMimeType: proofImage ? 'image/jpeg' : undefined
62
+ };
63
+ trackVerificationComplete('ADDRESS_CAPTURE', true, 1);
64
+ navigationManagerRef.current?.navigateToNextStep();
65
+ };
66
+ if (isCameraOpen) {
67
+ return /*#__PURE__*/_jsxs(SafeAreaView, {
68
+ style: styles.cameraContainer,
69
+ children: [/*#__PURE__*/_jsx(TrustchexCamera, {
70
+ style: styles.camera,
71
+ includeBase64: true,
72
+ onFrameAvailable: e => onFrameAvailable(e.nativeEvent.frame)
73
+ }), /*#__PURE__*/_jsxs(View, {
74
+ style: [styles.cameraControls, {
75
+ paddingBottom: insets.bottom + 16
76
+ }],
77
+ children: [/*#__PURE__*/_jsx(Text, {
78
+ style: styles.cameraHint,
79
+ children: t('addressCaptureScreen.proofCameraHint')
80
+ }), /*#__PURE__*/_jsx(StyledButton, {
81
+ onPress: () => captureNextFrame.current = true,
82
+ children: t('addressCaptureScreen.capture')
83
+ }), /*#__PURE__*/_jsx(TouchableOpacity, {
84
+ onPress: () => setIsCameraOpen(false),
85
+ children: /*#__PURE__*/_jsx(Text, {
86
+ style: styles.cancel,
87
+ children: t('general.cancel')
88
+ })
89
+ })]
90
+ })]
91
+ });
92
+ }
93
+ return /*#__PURE__*/_jsxs(SafeAreaView, {
94
+ style: styles.container,
95
+ children: [/*#__PURE__*/_jsxs(ScrollView, {
96
+ contentContainerStyle: [styles.content, {
97
+ paddingBottom: insets.bottom + 24
98
+ }],
99
+ keyboardShouldPersistTaps: "handled",
100
+ children: [/*#__PURE__*/_jsx(Text, {
101
+ style: styles.title,
102
+ children: t('addressCaptureScreen.title')
103
+ }), /*#__PURE__*/_jsx(Text, {
104
+ style: styles.subtitle,
105
+ children: t('addressCaptureScreen.subtitle')
106
+ }), fromChip ? /*#__PURE__*/_jsx(Text, {
107
+ style: styles.chipHint,
108
+ children: t('addressCaptureScreen.prefilledFromChip')
109
+ }) : null, /*#__PURE__*/_jsx(Text, {
110
+ style: styles.label,
111
+ children: t('addressCaptureScreen.addressLabel')
112
+ }), /*#__PURE__*/_jsx(TextInput, {
113
+ style: styles.input,
114
+ value: addressText,
115
+ onChangeText: setAddressText,
116
+ placeholder: t('addressCaptureScreen.addressPlaceholder'),
117
+ multiline: true,
118
+ textAlignVertical: "top"
119
+ }), /*#__PURE__*/_jsx(Text, {
120
+ style: styles.label,
121
+ children: t('addressCaptureScreen.proofLabel')
122
+ }), /*#__PURE__*/_jsx(Text, {
123
+ style: styles.proofHint,
124
+ children: t('addressCaptureScreen.proofHint')
125
+ }), proofImage ? /*#__PURE__*/_jsx(Image, {
126
+ source: {
127
+ uri: `data:image/jpeg;base64,${proofImage}`
128
+ },
129
+ style: styles.proofPreview,
130
+ resizeMode: "cover"
131
+ }) : null, /*#__PURE__*/_jsx(StyledButton, {
132
+ onPress: () => setIsCameraOpen(true),
133
+ children: proofImage ? t('addressCaptureScreen.retakeProof') : t('addressCaptureScreen.captureProof')
134
+ })]
135
+ }), /*#__PURE__*/_jsx(View, {
136
+ style: [styles.footer, {
137
+ paddingBottom: insets.bottom + 12
138
+ }],
139
+ children: /*#__PURE__*/_jsx(StyledButton, {
140
+ disabled: !canContinue,
141
+ onPress: onContinue,
142
+ children: t('general.continue')
143
+ })
144
+ }), /*#__PURE__*/_jsx(NavigationManager, {
145
+ ref: navigationManagerRef
146
+ })]
147
+ });
148
+ };
149
+ const styles = StyleSheet.create({
150
+ container: {
151
+ flex: 1,
152
+ backgroundColor: '#FFFFFF'
153
+ },
154
+ content: {
155
+ padding: 20
156
+ },
157
+ title: {
158
+ fontSize: 22,
159
+ fontWeight: '700',
160
+ color: '#111',
161
+ marginBottom: 8
162
+ },
163
+ subtitle: {
164
+ fontSize: 15,
165
+ color: '#555',
166
+ marginBottom: 16
167
+ },
168
+ chipHint: {
169
+ fontSize: 13,
170
+ color: '#2E7D32',
171
+ marginBottom: 12
172
+ },
173
+ label: {
174
+ fontSize: 14,
175
+ fontWeight: '600',
176
+ color: '#222',
177
+ marginTop: 16,
178
+ marginBottom: 6
179
+ },
180
+ input: {
181
+ borderWidth: 1,
182
+ borderColor: '#CCC',
183
+ borderRadius: 8,
184
+ padding: 12,
185
+ minHeight: 88,
186
+ fontSize: 15,
187
+ color: '#111'
188
+ },
189
+ proofHint: {
190
+ fontSize: 13,
191
+ color: '#777',
192
+ marginBottom: 10
193
+ },
194
+ proofPreview: {
195
+ width: '100%',
196
+ height: 180,
197
+ borderRadius: 8,
198
+ marginBottom: 12,
199
+ backgroundColor: '#EEE'
200
+ },
201
+ footer: {
202
+ paddingHorizontal: 20,
203
+ paddingTop: 8,
204
+ backgroundColor: '#FFFFFF'
205
+ },
206
+ cameraContainer: {
207
+ flex: 1,
208
+ backgroundColor: '#000'
209
+ },
210
+ camera: {
211
+ flex: 1
212
+ },
213
+ cameraControls: {
214
+ paddingHorizontal: 20,
215
+ gap: 12,
216
+ backgroundColor: '#000'
217
+ },
218
+ cameraHint: {
219
+ color: '#FFF',
220
+ textAlign: 'center',
221
+ marginBottom: 8
222
+ },
223
+ cancel: {
224
+ color: '#FFF',
225
+ textAlign: 'center',
226
+ marginTop: 8,
227
+ fontSize: 15
228
+ }
229
+ });
230
+ export default AddressCaptureScreen;
@@ -9,6 +9,7 @@ import AppContext from "../../Shared/Contexts/AppContext.js";
9
9
  import { getI18n, useTranslation } from 'react-i18next';
10
10
  import StyledButton from "../../Shared/Components/StyledButton.js";
11
11
  import NativeDeviceInfo from "../../Shared/Libs/native-device-info.utils.js";
12
+ import { captureCoarseGeolocation } from "../../Shared/Libs/geolocation.utils.js";
12
13
  import { speakWithDebounce } from "../../Shared/Libs/tts.utils.js";
13
14
  import { useStatusBarWhiteBackground } from "../../Shared/Libs/status-bar.utils.js";
14
15
  import { trackFunnelStep, useScreenTracking, trackConsentGiven, trackVerificationStart, trackVerificationComplete, trackError } from "../../Shared/Libs/analytics.utils.js";
@@ -141,6 +142,17 @@ const ContractAcceptanceScreen = () => {
141
142
  consent.contractIds.push(acceptedContractId);
142
143
  consent.deviceInfo = await generateHumanReadableIdentifier();
143
144
 
145
+ // MASAK 4/C(1)(e): when the workflow step opts in via
146
+ // collectGeolocation, capture coarse geolocation alongside the
147
+ // technical data. Fail-soft — a denied permission or missing
148
+ // module yields null and does not block.
149
+ if (appContext.currentWorkflowStep?.data?.collectGeolocation) {
150
+ // Mark that geolocation was REQUIRED so the server can raise a
151
+ // suspicion signal (4/C(1)(e)) when it comes back null (denied).
152
+ consent.geolocationExpected = true;
153
+ consent.geolocation = await captureCoarseGeolocation();
154
+ }
155
+
144
156
  // Update the context with the new device info
145
157
  appContext.identificationInfo.consent = consent;
146
158
  }
@@ -9,6 +9,7 @@ import AppContext from "../../Shared/Contexts/AppContext.js";
9
9
  import IdentityDocumentCamera from "../../Shared/Components/IdentityDocumentCamera.js";
10
10
  import { useTranslation } from 'react-i18next';
11
11
  import { trackFunnelStep, useScreenTracking, trackVerificationStart, trackVerificationComplete } from "../../Shared/Libs/analytics.utils.js";
12
+ import { isCountryAllowed } from "../../Shared/Libs/country-allow.js";
12
13
  import { useKeepAwake } from "../../Shared/Libs/native-keep-awake.utils.js";
13
14
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
14
15
  const IdentityDocumentEIDScanningScreen = () => {
@@ -79,6 +80,12 @@ const IdentityDocumentEIDScanningScreen = () => {
79
80
  dateOfBirth: dateOfBirth,
80
81
  dateOfExpiry: dateOfExpiry,
81
82
  documentType: documentType,
83
+ vizMrzFields:
84
+ // The printed/VIZ MRZ from the EID step's own camera scan. For a
85
+ // passport it's on the data page (passportData); for a TD1 ID card
86
+ // the MRZ is on the BACK (idBackSideData), so it must be included
87
+ // — without it, ID-card scans had no VIZ source and hard-blocked.
88
+ passportData?.mrzFields ?? idBackSideData?.mrzFields ?? idFrontSideData?.mrzFields ?? appContext.identificationInfo.scannedDocument?.mrzFields,
82
89
  onScanSuccess: (mrzFields, faceImage, mimeType) => {
83
90
  if (mrzFields && faceImage && mimeType) {
84
91
  if (!appContext.identificationInfo.scannedDocument) {
@@ -89,6 +96,25 @@ const IdentityDocumentEIDScanningScreen = () => {
89
96
  }
90
97
  appContext.identificationInfo.scannedDocument.dataSource = 'EID';
91
98
  appContext.identificationInfo.scannedDocument.mrzFields = mrzFields;
99
+ // Preserve the printed/VIZ MRZ separately (mrzFields above is
100
+ // the chip read) and record the chip↔VIZ verdict, so the
101
+ // backend can re-derive the authoritative match (MASAK 4/C).
102
+ appContext.identificationInfo.scannedDocument.vizMrzFields = passportData?.mrzFields ?? idBackSideData?.mrzFields ?? idFrontSideData?.mrzFields;
103
+ appContext.identificationInfo.scannedDocument.chipVizMatch = appContext.lastDocumentRead?.chipVizMatch;
104
+ // Passive Auth artifacts for server-side verification.
105
+ appContext.identificationInfo.scannedDocument.paSodBase64 = appContext.lastDocumentRead?.passiveAuth?.sodBase64 ?? null;
106
+ appContext.identificationInfo.scannedDocument.paIntegrityStatus = appContext.lastDocumentRead?.passiveAuth?.integrityStatus ?? null;
107
+ // MASAK 4/C(1)(d): if the chip's DG11 carried an address and no
108
+ // address was captured otherwise, seed it from the chip so it is
109
+ // sent to the backend (fromChip=true). A later ADDRESS_CAPTURE
110
+ // step can still let the user confirm/edit it.
111
+ const chipAddress = appContext.lastDocumentRead?.document.address;
112
+ if (chipAddress && !appContext.identificationInfo.address) {
113
+ appContext.identificationInfo.address = {
114
+ addressText: chipAddress,
115
+ fromChip: true
116
+ };
117
+ }
92
118
  appContext.identificationInfo.scannedDocument.faceImage = faceImage;
93
119
  appContext.identificationInfo.scannedDocument.faceImageMimeType = mimeType;
94
120
  appContext.identificationInfo.scannedDocument.mrzText ||= idBackSideData?.mrzText || passportData?.mrzText;
@@ -96,16 +122,13 @@ const IdentityDocumentEIDScanningScreen = () => {
96
122
  appContext.identificationInfo.scannedDocument.backImage ||= idBackSideData?.image;
97
123
  appContext.identificationInfo.scannedDocument.barcodeValue ||= idBackSideData?.barcodeValue;
98
124
  appContext.identificationInfo.scannedDocument.documentType = mrzFields.documentCode === 'I' ? 'ID' : mrzFields.documentCode === 'P' ? 'PASSPORT' : 'UNKNOWN';
99
- if (allowedCountries && allowedCountries.length > 0) {
100
- const countryCode = mrzFields.issuingState;
101
- if (!allowedCountries.includes(countryCode)) {
102
- // Workflow validation - user's document country not in allowed list
103
- // Expected behavior, not a bug
104
- Alert.alert(t('general.error'), t('general.countryNotAllowed'));
105
- appContext.onError?.(t('general.countryNotAllowed'));
106
- navigationManagerRef.current?.reset();
107
- return;
108
- }
125
+ if (!isCountryAllowed(mrzFields.issuingState, allowedCountries)) {
126
+ // Workflow validation - user's document country not in allowed list
127
+ // Expected behavior, not a bug
128
+ Alert.alert(t('general.error'), t('general.countryNotAllowed'));
129
+ appContext.onError?.(t('general.countryNotAllowed'));
130
+ navigationManagerRef.current?.reset();
131
+ return;
109
132
  }
110
133
  if (allowedDocumentTypes && allowedDocumentTypes.length > 0) {
111
134
  const documentCode = mrzFields.documentCode;