@trustchex/react-native-sdk 1.500.2 → 1.501.1
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/GeolocationModule.kt +159 -0
- package/android/src/main/java/com/trustchex/reactnativesdk/TrustchexSDKPackage.kt +12 -0
- package/ios/Permission/CoarseLocationProvider.swift +111 -0
- package/ios/Permission/PermissionModule.m +4 -0
- package/ios/Permission/PermissionModule.swift +21 -1
- package/lib/module/Screens/Dynamic/AddressCaptureScreen.js +230 -0
- package/lib/module/Screens/Dynamic/ContractAcceptanceScreen.js +12 -0
- package/lib/module/Screens/Dynamic/IdentityDocumentEIDScanningScreen.js +33 -10
- package/lib/module/Screens/Dynamic/IdentityDocumentScanningScreen.js +8 -10
- package/lib/module/Screens/Static/ResultScreen.js +74 -3
- package/lib/module/Shared/Components/EIDScanner.js +59 -3
- package/lib/module/Shared/Components/NavigationManager.js +6 -2
- package/lib/module/Shared/EIDReader/eidReader.js +52 -3
- package/lib/module/Shared/EIDReader/java/inputStream.js +6 -0
- package/lib/module/Shared/EIDReader/lds/icao/dg11File.js +60 -20
- package/lib/module/Shared/EIDReader/passiveAuth/der.js +93 -0
- package/lib/module/Shared/EIDReader/passiveAuth/integrity.js +71 -0
- package/lib/module/Shared/EIDReader/passiveAuth/sodParser.js +123 -0
- package/lib/module/Shared/Libs/chipVizMatch.js +93 -0
- package/lib/module/Shared/Libs/country-allow.js +16 -0
- package/lib/module/Shared/Libs/documentDataNormalizer.js +21 -11
- package/lib/module/Shared/Libs/geolocation.utils.js +77 -0
- package/lib/module/Shared/Libs/promise.utils.js +6 -0
- package/lib/module/Shared/Services/DataUploadService.js +18 -1
- package/lib/module/Translation/Resources/en.js +16 -0
- package/lib/module/Translation/Resources/tr.js +16 -0
- package/lib/module/Trustchex.js +4 -0
- package/lib/module/version.js +1 -1
- package/lib/typescript/src/Screens/Dynamic/AddressCaptureScreen.d.ts +9 -0
- package/lib/typescript/src/Screens/Dynamic/AddressCaptureScreen.d.ts.map +1 -0
- package/lib/typescript/src/Screens/Dynamic/ContractAcceptanceScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Dynamic/IdentityDocumentEIDScanningScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Dynamic/IdentityDocumentScanningScreen.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 +7 -1
- package/lib/typescript/src/Shared/Components/EIDScanner.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/NavigationManager.d.ts.map +1 -1
- package/lib/typescript/src/Shared/EIDReader/eidReader.d.ts +14 -1
- package/lib/typescript/src/Shared/EIDReader/eidReader.d.ts.map +1 -1
- package/lib/typescript/src/Shared/EIDReader/java/inputStream.d.ts +3 -0
- package/lib/typescript/src/Shared/EIDReader/java/inputStream.d.ts.map +1 -1
- package/lib/typescript/src/Shared/EIDReader/lds/icao/dg11File.d.ts +20 -7
- package/lib/typescript/src/Shared/EIDReader/lds/icao/dg11File.d.ts.map +1 -1
- package/lib/typescript/src/Shared/EIDReader/passiveAuth/der.d.ts +31 -0
- package/lib/typescript/src/Shared/EIDReader/passiveAuth/der.d.ts.map +1 -0
- package/lib/typescript/src/Shared/EIDReader/passiveAuth/integrity.d.ts +31 -0
- package/lib/typescript/src/Shared/EIDReader/passiveAuth/integrity.d.ts.map +1 -0
- package/lib/typescript/src/Shared/EIDReader/passiveAuth/sodParser.d.ts +12 -0
- package/lib/typescript/src/Shared/EIDReader/passiveAuth/sodParser.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Libs/chipVizMatch.d.ts +68 -0
- package/lib/typescript/src/Shared/Libs/chipVizMatch.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Libs/country-allow.d.ts +10 -0
- package/lib/typescript/src/Shared/Libs/country-allow.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Libs/documentDataNormalizer.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/geolocation.utils.d.ts +19 -0
- package/lib/typescript/src/Shared/Libs/geolocation.utils.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Libs/promise.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Services/DataUploadService.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Types/documentData.d.ts +7 -0
- package/lib/typescript/src/Shared/Types/documentData.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Types/documentReadResult.d.ts +20 -0
- package/lib/typescript/src/Shared/Types/documentReadResult.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Types/identificationInfo.d.ts +95 -2
- package/lib/typescript/src/Shared/Types/identificationInfo.d.ts.map +1 -1
- package/lib/typescript/src/Translation/Resources/en.d.ts +16 -0
- package/lib/typescript/src/Translation/Resources/en.d.ts.map +1 -1
- package/lib/typescript/src/Translation/Resources/tr.d.ts +16 -0
- package/lib/typescript/src/Translation/Resources/tr.d.ts.map +1 -1
- package/lib/typescript/src/Trustchex.d.ts.map +1 -1
- package/lib/typescript/src/version.d.ts +1 -1
- package/package.json +1 -1
- package/src/Screens/Dynamic/AddressCaptureScreen.tsx +196 -0
- package/src/Screens/Dynamic/ContractAcceptanceScreen.tsx +12 -0
- package/src/Screens/Dynamic/IdentityDocumentEIDScanningScreen.tsx +46 -14
- package/src/Screens/Dynamic/IdentityDocumentScanningScreen.tsx +9 -11
- package/src/Screens/Static/ResultScreen.tsx +101 -6
- package/src/Shared/Components/EIDScanner.tsx +69 -2
- package/src/Shared/Components/NavigationManager.tsx +6 -0
- package/src/Shared/EIDReader/eidReader.ts +75 -2
- package/src/Shared/EIDReader/java/inputStream.ts +6 -0
- package/src/Shared/EIDReader/lds/icao/dg11File.ts +68 -22
- package/src/Shared/EIDReader/passiveAuth/der.ts +110 -0
- package/src/Shared/EIDReader/passiveAuth/integrity.ts +97 -0
- package/src/Shared/EIDReader/passiveAuth/sodParser.ts +142 -0
- package/src/Shared/Libs/chipVizMatch.ts +149 -0
- package/src/Shared/Libs/country-allow.ts +17 -0
- package/src/Shared/Libs/documentDataNormalizer.ts +24 -11
- package/src/Shared/Libs/geolocation.utils.ts +87 -0
- package/src/Shared/Libs/promise.utils.ts +11 -0
- package/src/Shared/Services/DataUploadService.ts +23 -0
- package/src/Shared/Types/documentData.ts +8 -0
- package/src/Shared/Types/documentReadResult.ts +20 -0
- package/src/Shared/Types/identificationInfo.ts +97 -2
- package/src/Translation/Resources/en.ts +21 -0
- package/src/Translation/Resources/tr.ts +21 -0
- package/src/Trustchex.tsx +5 -0
- 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 (
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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;
|