@trustchex/react-native-sdk 1.500.2 → 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 (97) 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/promise.utils.js +6 -0
  24. package/lib/module/Shared/Services/DataUploadService.js +18 -1
  25. package/lib/module/Translation/Resources/en.js +16 -0
  26. package/lib/module/Translation/Resources/tr.js +16 -0
  27. package/lib/module/Trustchex.js +4 -0
  28. package/lib/module/version.js +1 -1
  29. package/lib/typescript/src/Screens/Dynamic/AddressCaptureScreen.d.ts +9 -0
  30. package/lib/typescript/src/Screens/Dynamic/AddressCaptureScreen.d.ts.map +1 -0
  31. package/lib/typescript/src/Screens/Dynamic/ContractAcceptanceScreen.d.ts.map +1 -1
  32. package/lib/typescript/src/Screens/Dynamic/IdentityDocumentEIDScanningScreen.d.ts.map +1 -1
  33. package/lib/typescript/src/Screens/Dynamic/IdentityDocumentScanningScreen.d.ts.map +1 -1
  34. package/lib/typescript/src/Screens/Static/ResultScreen.d.ts.map +1 -1
  35. package/lib/typescript/src/Shared/Components/EIDScanner.d.ts +7 -1
  36. package/lib/typescript/src/Shared/Components/EIDScanner.d.ts.map +1 -1
  37. package/lib/typescript/src/Shared/Components/NavigationManager.d.ts.map +1 -1
  38. package/lib/typescript/src/Shared/EIDReader/eidReader.d.ts +14 -1
  39. package/lib/typescript/src/Shared/EIDReader/eidReader.d.ts.map +1 -1
  40. package/lib/typescript/src/Shared/EIDReader/java/inputStream.d.ts +3 -0
  41. package/lib/typescript/src/Shared/EIDReader/java/inputStream.d.ts.map +1 -1
  42. package/lib/typescript/src/Shared/EIDReader/lds/icao/dg11File.d.ts +20 -7
  43. package/lib/typescript/src/Shared/EIDReader/lds/icao/dg11File.d.ts.map +1 -1
  44. package/lib/typescript/src/Shared/EIDReader/passiveAuth/der.d.ts +31 -0
  45. package/lib/typescript/src/Shared/EIDReader/passiveAuth/der.d.ts.map +1 -0
  46. package/lib/typescript/src/Shared/EIDReader/passiveAuth/integrity.d.ts +31 -0
  47. package/lib/typescript/src/Shared/EIDReader/passiveAuth/integrity.d.ts.map +1 -0
  48. package/lib/typescript/src/Shared/EIDReader/passiveAuth/sodParser.d.ts +12 -0
  49. package/lib/typescript/src/Shared/EIDReader/passiveAuth/sodParser.d.ts.map +1 -0
  50. package/lib/typescript/src/Shared/Libs/chipVizMatch.d.ts +68 -0
  51. package/lib/typescript/src/Shared/Libs/chipVizMatch.d.ts.map +1 -0
  52. package/lib/typescript/src/Shared/Libs/country-allow.d.ts +10 -0
  53. package/lib/typescript/src/Shared/Libs/country-allow.d.ts.map +1 -0
  54. package/lib/typescript/src/Shared/Libs/documentDataNormalizer.d.ts.map +1 -1
  55. package/lib/typescript/src/Shared/Libs/geolocation.utils.d.ts +19 -0
  56. package/lib/typescript/src/Shared/Libs/geolocation.utils.d.ts.map +1 -0
  57. package/lib/typescript/src/Shared/Libs/promise.utils.d.ts.map +1 -1
  58. package/lib/typescript/src/Shared/Services/DataUploadService.d.ts.map +1 -1
  59. package/lib/typescript/src/Shared/Types/documentData.d.ts +7 -0
  60. package/lib/typescript/src/Shared/Types/documentData.d.ts.map +1 -1
  61. package/lib/typescript/src/Shared/Types/documentReadResult.d.ts +20 -0
  62. package/lib/typescript/src/Shared/Types/documentReadResult.d.ts.map +1 -1
  63. package/lib/typescript/src/Shared/Types/identificationInfo.d.ts +95 -2
  64. package/lib/typescript/src/Shared/Types/identificationInfo.d.ts.map +1 -1
  65. package/lib/typescript/src/Translation/Resources/en.d.ts +16 -0
  66. package/lib/typescript/src/Translation/Resources/en.d.ts.map +1 -1
  67. package/lib/typescript/src/Translation/Resources/tr.d.ts +16 -0
  68. package/lib/typescript/src/Translation/Resources/tr.d.ts.map +1 -1
  69. package/lib/typescript/src/Trustchex.d.ts.map +1 -1
  70. package/lib/typescript/src/version.d.ts +1 -1
  71. package/package.json +1 -1
  72. package/src/Screens/Dynamic/AddressCaptureScreen.tsx +196 -0
  73. package/src/Screens/Dynamic/ContractAcceptanceScreen.tsx +12 -0
  74. package/src/Screens/Dynamic/IdentityDocumentEIDScanningScreen.tsx +46 -14
  75. package/src/Screens/Dynamic/IdentityDocumentScanningScreen.tsx +9 -11
  76. package/src/Screens/Static/ResultScreen.tsx +101 -6
  77. package/src/Shared/Components/EIDScanner.tsx +69 -2
  78. package/src/Shared/Components/NavigationManager.tsx +6 -0
  79. package/src/Shared/EIDReader/eidReader.ts +75 -2
  80. package/src/Shared/EIDReader/java/inputStream.ts +6 -0
  81. package/src/Shared/EIDReader/lds/icao/dg11File.ts +68 -22
  82. package/src/Shared/EIDReader/passiveAuth/der.ts +110 -0
  83. package/src/Shared/EIDReader/passiveAuth/integrity.ts +97 -0
  84. package/src/Shared/EIDReader/passiveAuth/sodParser.ts +142 -0
  85. package/src/Shared/Libs/chipVizMatch.ts +149 -0
  86. package/src/Shared/Libs/country-allow.ts +17 -0
  87. package/src/Shared/Libs/documentDataNormalizer.ts +24 -11
  88. package/src/Shared/Libs/geolocation.utils.ts +87 -0
  89. package/src/Shared/Libs/promise.utils.ts +11 -0
  90. package/src/Shared/Services/DataUploadService.ts +23 -0
  91. package/src/Shared/Types/documentData.ts +8 -0
  92. package/src/Shared/Types/documentReadResult.ts +20 -0
  93. package/src/Shared/Types/identificationInfo.ts +97 -2
  94. package/src/Translation/Resources/en.ts +21 -0
  95. package/src/Translation/Resources/tr.ts +21 -0
  96. package/src/Trustchex.tsx +5 -0
  97. package/src/version.ts +1 -1
@@ -10,6 +10,8 @@ declare const _default: {
10
10
  'verificationSessionCheckScreen.or': string;
11
11
  'general.warning': string;
12
12
  'general.error': string;
13
+ 'general.continue': string;
14
+ 'general.cancel': string;
13
15
  'general.yes': string;
14
16
  'general.no': string;
15
17
  'general.letsGo': string;
@@ -24,6 +26,7 @@ declare const _default: {
24
26
  'termsOfUseAndDataPrivacyScreen.acceptAndContinue': string;
25
27
  'resultScreen.submitting': string;
26
28
  'resultScreen.submissionFailed': string;
29
+ 'resultScreen.sessionTerminated': string;
27
30
  'resultScreen.submissionFailedTitle': string;
28
31
  'resultScreen.submissionFailedRetryHint': string;
29
32
  'resultScreen.retrySubmission': string;
@@ -90,10 +93,23 @@ declare const _default: {
90
93
  'eidScannerScreen.nfcAuthFailed': string;
91
94
  'eidScannerScreen.nfcTagLost': string;
92
95
  'eidScannerScreen.nfcTimeout': string;
96
+ 'addressCaptureScreen.title': string;
97
+ 'addressCaptureScreen.subtitle': string;
98
+ 'addressCaptureScreen.prefilledFromChip': string;
99
+ 'addressCaptureScreen.addressLabel': string;
100
+ 'addressCaptureScreen.addressPlaceholder': string;
101
+ 'addressCaptureScreen.proofLabel': string;
102
+ 'addressCaptureScreen.proofHint': string;
103
+ 'addressCaptureScreen.captureProof': string;
104
+ 'addressCaptureScreen.retakeProof': string;
105
+ 'addressCaptureScreen.proofCameraHint': string;
106
+ 'addressCaptureScreen.capture': string;
93
107
  'eidScannerScreen.faceImageNotFound': string;
94
108
  'eidScannerScreen.documentCode': string;
95
109
  'eidScannerScreen.nationality': string;
96
110
  'eidScannerScreen.personalNumber': string;
111
+ 'eidScannerScreen.address': string;
112
+ 'eidScannerScreen.location': string;
97
113
  'eidScannerScreen.documentNumber': string;
98
114
  'eidScannerScreen.name': string;
99
115
  'eidScannerScreen.surname': string;
@@ -1 +1 @@
1
- {"version":3,"file":"tr.d.ts","sourceRoot":"","sources":["../../../../../src/Translation/Resources/tr.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,wBA4SE"}
1
+ {"version":3,"file":"tr.d.ts","sourceRoot":"","sources":["../../../../../src/Translation/Resources/tr.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,wBAiUE"}
@@ -1 +1 @@
1
- {"version":3,"file":"Trustchex.d.ts","sourceRoot":"","sources":["../../../src/Trustchex.tsx"],"names":[],"mappings":"AAAA,OAAO,KAMN,MAAM,OAAO,CAAC;AAKf,OAAO,gCAAgC,CAAC;AAmBxC,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,mCAAmC,CAAC;AAe5E,UAAU,iBAAiB;IACzB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,UAAU,cAAc;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IAC7B,MAAM,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;IACzB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,kBAAkB,KAAK,IAAI,CAAC;IACtD,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,QAAQ,CAAC,EAAE,OAAO,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,CAAC;IACpE,iGAAiG;IACjG,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,gGAAgG;IAChG,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B;AASD,QAAA,MAAM,SAAS,EAAE,KAAK,CAAC,EAAE,CAAC,cAAc,CAwQvC,CAAC;AAWF,eAAe,SAAS,CAAC"}
1
+ {"version":3,"file":"Trustchex.d.ts","sourceRoot":"","sources":["../../../src/Trustchex.tsx"],"names":[],"mappings":"AAAA,OAAO,KAMN,MAAM,OAAO,CAAC;AAKf,OAAO,gCAAgC,CAAC;AAoBxC,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,mCAAmC,CAAC;AAe5E,UAAU,iBAAiB;IACzB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,UAAU,cAAc;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IAC7B,MAAM,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;IACzB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,kBAAkB,KAAK,IAAI,CAAC;IACtD,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,QAAQ,CAAC,EAAE,OAAO,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,CAAC;IACpE,iGAAiG;IACjG,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,gGAAgG;IAChG,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B;AASD,QAAA,MAAM,SAAS,EAAE,KAAK,CAAC,EAAE,CAAC,cAAc,CA4QvC,CAAC;AAWF,eAAe,SAAS,CAAC"}
@@ -1,2 +1,2 @@
1
- export declare const SDK_VERSION = "1.500.2";
1
+ export declare const SDK_VERSION = "1.501.0";
2
2
  //# sourceMappingURL=version.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trustchex/react-native-sdk",
3
- "version": "1.500.2",
3
+ "version": "1.501.0",
4
4
  "description": "Trustchex mobile app react native SDK for android or ios devices",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",
@@ -0,0 +1,196 @@
1
+ import React, { useContext, useEffect, useRef, useState } from 'react';
2
+ import {
3
+ SafeAreaView,
4
+ View,
5
+ Text,
6
+ TextInput,
7
+ StyleSheet,
8
+ ScrollView,
9
+ Image,
10
+ TouchableOpacity,
11
+ } from 'react-native';
12
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
13
+ import { useTranslation } from 'react-i18next';
14
+ import {
15
+ TrustchexCamera,
16
+ type Frame,
17
+ } from '../../Shared/Components/TrustchexCamera';
18
+ import NavigationManager, {
19
+ type NavigationManagerRef,
20
+ } from '../../Shared/Components/NavigationManager';
21
+ import AppContext from '../../Shared/Contexts/AppContext';
22
+ import StyledButton from '../../Shared/Components/StyledButton';
23
+ import { useStatusBarWhiteBackground } from '../../Shared/Libs/status-bar.utils';
24
+ import {
25
+ useScreenTracking,
26
+ trackVerificationStart,
27
+ trackVerificationComplete,
28
+ } from '../../Shared/Libs/analytics.utils';
29
+ import { useKeepAwake } from '../../Shared/Libs/native-keep-awake.utils';
30
+
31
+ /**
32
+ * ADDRESS_CAPTURE step (MASAK Tebliğ No. 32 Article 4/C(1)(d)). The user confirms
33
+ * their address — pre-filled from the chip's DG11 address when available — and
34
+ * captures a proof document (residence cert / utility bill ≤3 months / public-
35
+ * authority doc). The address is verified later by an operator (manual review).
36
+ */
37
+ const AddressCaptureScreen = () => {
38
+ useKeepAwake();
39
+ useStatusBarWhiteBackground();
40
+ useScreenTracking('address_capture');
41
+
42
+ const appContext = useContext(AppContext);
43
+ const navigationManagerRef = useRef<NavigationManagerRef>(null);
44
+ const { t } = useTranslation();
45
+ const insets = useSafeAreaInsets();
46
+
47
+ // Pre-fill from the chip's DG11 address when the NFC read provided one.
48
+ const chipAddress =
49
+ appContext.lastDocumentRead?.document.address ?? '';
50
+ const [addressText, setAddressText] = useState<string>(chipAddress);
51
+ const [fromChip] = useState<boolean>(!!chipAddress);
52
+
53
+ const [proofImage, setProofImage] = useState<string | undefined>();
54
+ const [isCameraOpen, setIsCameraOpen] = useState(false);
55
+ const captureNextFrame = useRef(false);
56
+
57
+ // Whether the proof document is required is configurable per workflow step.
58
+ const proofRequired =
59
+ appContext.currentWorkflowStep?.data?.addressProofRequired !== false;
60
+
61
+ useEffect(() => {
62
+ trackVerificationStart('ADDRESS_CAPTURE');
63
+ }, []);
64
+
65
+ const onFrameAvailable = (frame: Frame) => {
66
+ if (!captureNextFrame.current) return;
67
+ const b64 = frame.base64Image;
68
+ if (b64) {
69
+ captureNextFrame.current = false;
70
+ setProofImage(b64);
71
+ setIsCameraOpen(false);
72
+ }
73
+ };
74
+
75
+ const canContinue =
76
+ addressText.trim().length > 0 && (!proofRequired || !!proofImage);
77
+
78
+ const onContinue = () => {
79
+ appContext.identificationInfo.address = {
80
+ addressText: addressText.trim(),
81
+ fromChip,
82
+ proofImage,
83
+ proofImageMimeType: proofImage ? 'image/jpeg' : undefined,
84
+ };
85
+ trackVerificationComplete('ADDRESS_CAPTURE', true, 1);
86
+ navigationManagerRef.current?.navigateToNextStep();
87
+ };
88
+
89
+ if (isCameraOpen) {
90
+ return (
91
+ <SafeAreaView style={styles.cameraContainer}>
92
+ <TrustchexCamera
93
+ style={styles.camera}
94
+ includeBase64
95
+ onFrameAvailable={(e) => onFrameAvailable(e.nativeEvent.frame)}
96
+ />
97
+ <View style={[styles.cameraControls, { paddingBottom: insets.bottom + 16 }]}>
98
+ <Text style={styles.cameraHint}>
99
+ {t('addressCaptureScreen.proofCameraHint')}
100
+ </Text>
101
+ <StyledButton onPress={() => (captureNextFrame.current = true)}>
102
+ {t('addressCaptureScreen.capture')}
103
+ </StyledButton>
104
+ <TouchableOpacity onPress={() => setIsCameraOpen(false)}>
105
+ <Text style={styles.cancel}>{t('general.cancel')}</Text>
106
+ </TouchableOpacity>
107
+ </View>
108
+ </SafeAreaView>
109
+ );
110
+ }
111
+
112
+ return (
113
+ <SafeAreaView style={styles.container}>
114
+ <ScrollView
115
+ contentContainerStyle={[styles.content, { paddingBottom: insets.bottom + 24 }]}
116
+ keyboardShouldPersistTaps="handled"
117
+ >
118
+ <Text style={styles.title}>{t('addressCaptureScreen.title')}</Text>
119
+ <Text style={styles.subtitle}>{t('addressCaptureScreen.subtitle')}</Text>
120
+
121
+ {fromChip ? (
122
+ <Text style={styles.chipHint}>
123
+ {t('addressCaptureScreen.prefilledFromChip')}
124
+ </Text>
125
+ ) : null}
126
+
127
+ <Text style={styles.label}>{t('addressCaptureScreen.addressLabel')}</Text>
128
+ <TextInput
129
+ style={styles.input}
130
+ value={addressText}
131
+ onChangeText={setAddressText}
132
+ placeholder={t('addressCaptureScreen.addressPlaceholder')}
133
+ multiline
134
+ textAlignVertical="top"
135
+ />
136
+
137
+ <Text style={styles.label}>{t('addressCaptureScreen.proofLabel')}</Text>
138
+ <Text style={styles.proofHint}>{t('addressCaptureScreen.proofHint')}</Text>
139
+ {proofImage ? (
140
+ <Image
141
+ source={{ uri: `data:image/jpeg;base64,${proofImage}` }}
142
+ style={styles.proofPreview}
143
+ resizeMode="cover"
144
+ />
145
+ ) : null}
146
+ <StyledButton onPress={() => setIsCameraOpen(true)}>
147
+ {proofImage
148
+ ? t('addressCaptureScreen.retakeProof')
149
+ : t('addressCaptureScreen.captureProof')}
150
+ </StyledButton>
151
+ </ScrollView>
152
+
153
+ <View style={[styles.footer, { paddingBottom: insets.bottom + 12 }]}>
154
+ <StyledButton disabled={!canContinue} onPress={onContinue}>
155
+ {t('general.continue')}
156
+ </StyledButton>
157
+ </View>
158
+
159
+ <NavigationManager ref={navigationManagerRef} />
160
+ </SafeAreaView>
161
+ );
162
+ };
163
+
164
+ const styles = StyleSheet.create({
165
+ container: { flex: 1, backgroundColor: '#FFFFFF' },
166
+ content: { padding: 20 },
167
+ title: { fontSize: 22, fontWeight: '700', color: '#111', marginBottom: 8 },
168
+ subtitle: { fontSize: 15, color: '#555', marginBottom: 16 },
169
+ chipHint: { fontSize: 13, color: '#2E7D32', marginBottom: 12 },
170
+ label: { fontSize: 14, fontWeight: '600', color: '#222', marginTop: 16, marginBottom: 6 },
171
+ input: {
172
+ borderWidth: 1,
173
+ borderColor: '#CCC',
174
+ borderRadius: 8,
175
+ padding: 12,
176
+ minHeight: 88,
177
+ fontSize: 15,
178
+ color: '#111',
179
+ },
180
+ proofHint: { fontSize: 13, color: '#777', marginBottom: 10 },
181
+ proofPreview: {
182
+ width: '100%',
183
+ height: 180,
184
+ borderRadius: 8,
185
+ marginBottom: 12,
186
+ backgroundColor: '#EEE',
187
+ },
188
+ footer: { paddingHorizontal: 20, paddingTop: 8, backgroundColor: '#FFFFFF' },
189
+ cameraContainer: { flex: 1, backgroundColor: '#000' },
190
+ camera: { flex: 1 },
191
+ cameraControls: { paddingHorizontal: 20, gap: 12, backgroundColor: '#000' },
192
+ cameraHint: { color: '#FFF', textAlign: 'center', marginBottom: 8 },
193
+ cancel: { color: '#FFF', textAlign: 'center', marginTop: 8, fontSize: 15 },
194
+ });
195
+
196
+ export default AddressCaptureScreen;
@@ -15,6 +15,7 @@ import AppContext from '../../Shared/Contexts/AppContext';
15
15
  import { getI18n, useTranslation } from 'react-i18next';
16
16
  import StyledButton from '../../Shared/Components/StyledButton';
17
17
  import NativeDeviceInfo from '../../Shared/Libs/native-device-info.utils';
18
+ import { captureCoarseGeolocation } from '../../Shared/Libs/geolocation.utils';
18
19
  import { speakWithDebounce } from '../../Shared/Libs/tts.utils';
19
20
  import { useStatusBarWhiteBackground } from '../../Shared/Libs/status-bar.utils';
20
21
  import {
@@ -174,6 +175,17 @@ const ContractAcceptanceScreen = () => {
174
175
  consent.contractIds.push(acceptedContractId);
175
176
  consent.deviceInfo = await generateHumanReadableIdentifier();
176
177
 
178
+ // MASAK 4/C(1)(e): when the workflow step opts in via
179
+ // collectGeolocation, capture coarse geolocation alongside the
180
+ // technical data. Fail-soft — a denied permission or missing
181
+ // module yields null and does not block.
182
+ if (appContext.currentWorkflowStep?.data?.collectGeolocation) {
183
+ // Mark that geolocation was REQUIRED so the server can raise a
184
+ // suspicion signal (4/C(1)(e)) when it comes back null (denied).
185
+ consent.geolocationExpected = true;
186
+ consent.geolocation = await captureCoarseGeolocation();
187
+ }
188
+
177
189
  // Update the context with the new device info
178
190
  appContext.identificationInfo.consent = consent;
179
191
  }
@@ -17,6 +17,7 @@ import {
17
17
  trackVerificationStart,
18
18
  trackVerificationComplete,
19
19
  } from '../../Shared/Libs/analytics.utils';
20
+ import { isCountryAllowed } from '../../Shared/Libs/country-allow';
20
21
  import { useKeepAwake } from '../../Shared/Libs/native-keep-awake.utils';
21
22
 
22
23
  const IdentityDocumentEIDScanningScreen = () => {
@@ -56,7 +57,7 @@ const IdentityDocumentEIDScanningScreen = () => {
56
57
  const [allowedDocumentTypes, setAllowedDocumentTypes] = useState<
57
58
  ('I' | 'P')[] | null
58
59
  >(null);
59
- const [allowedCountries, setAllowedCountries] = useState<'TUR'[] | null>(
60
+ const [allowedCountries, setAllowedCountries] = useState<string[] | null>(
60
61
  null
61
62
  );
62
63
 
@@ -105,6 +106,16 @@ const IdentityDocumentEIDScanningScreen = () => {
105
106
  dateOfBirth={dateOfBirth}
106
107
  dateOfExpiry={dateOfExpiry}
107
108
  documentType={documentType}
109
+ vizMrzFields={
110
+ // The printed/VIZ MRZ from the EID step's own camera scan. For a
111
+ // passport it's on the data page (passportData); for a TD1 ID card
112
+ // the MRZ is on the BACK (idBackSideData), so it must be included
113
+ // — without it, ID-card scans had no VIZ source and hard-blocked.
114
+ passportData?.mrzFields ??
115
+ idBackSideData?.mrzFields ??
116
+ idFrontSideData?.mrzFields ??
117
+ appContext.identificationInfo.scannedDocument?.mrzFields
118
+ }
108
119
  onScanSuccess={(mrzFields, faceImage, mimeType) => {
109
120
  if (mrzFields && faceImage && mimeType) {
110
121
  if (!appContext.identificationInfo.scannedDocument) {
@@ -118,6 +129,33 @@ const IdentityDocumentEIDScanningScreen = () => {
118
129
  'EID';
119
130
  appContext.identificationInfo.scannedDocument.mrzFields =
120
131
  mrzFields;
132
+ // Preserve the printed/VIZ MRZ separately (mrzFields above is
133
+ // the chip read) and record the chip↔VIZ verdict, so the
134
+ // backend can re-derive the authoritative match (MASAK 4/C).
135
+ appContext.identificationInfo.scannedDocument.vizMrzFields =
136
+ passportData?.mrzFields ??
137
+ idBackSideData?.mrzFields ??
138
+ idFrontSideData?.mrzFields;
139
+ appContext.identificationInfo.scannedDocument.chipVizMatch =
140
+ appContext.lastDocumentRead?.chipVizMatch;
141
+ // Passive Auth artifacts for server-side verification.
142
+ appContext.identificationInfo.scannedDocument.paSodBase64 =
143
+ appContext.lastDocumentRead?.passiveAuth?.sodBase64 ?? null;
144
+ appContext.identificationInfo.scannedDocument.paIntegrityStatus =
145
+ appContext.lastDocumentRead?.passiveAuth?.integrityStatus ??
146
+ null;
147
+ // MASAK 4/C(1)(d): if the chip's DG11 carried an address and no
148
+ // address was captured otherwise, seed it from the chip so it is
149
+ // sent to the backend (fromChip=true). A later ADDRESS_CAPTURE
150
+ // step can still let the user confirm/edit it.
151
+ const chipAddress =
152
+ appContext.lastDocumentRead?.document.address;
153
+ if (chipAddress && !appContext.identificationInfo.address) {
154
+ appContext.identificationInfo.address = {
155
+ addressText: chipAddress,
156
+ fromChip: true,
157
+ };
158
+ }
121
159
  appContext.identificationInfo.scannedDocument.faceImage =
122
160
  faceImage;
123
161
  appContext.identificationInfo.scannedDocument.faceImageMimeType =
@@ -137,19 +175,13 @@ const IdentityDocumentEIDScanningScreen = () => {
137
175
  ? 'PASSPORT'
138
176
  : 'UNKNOWN';
139
177
 
140
- if (allowedCountries && allowedCountries.length > 0) {
141
- const countryCode = mrzFields.issuingState;
142
- if (!allowedCountries.includes(countryCode as 'TUR')) {
143
- // Workflow validation - user's document country not in allowed list
144
- // Expected behavior, not a bug
145
- Alert.alert(
146
- t('general.error'),
147
- t('general.countryNotAllowed')
148
- );
149
- appContext.onError?.(t('general.countryNotAllowed'));
150
- navigationManagerRef.current?.reset();
151
- return;
152
- }
178
+ if (!isCountryAllowed(mrzFields.issuingState, allowedCountries)) {
179
+ // Workflow validation - user's document country not in allowed list
180
+ // Expected behavior, not a bug
181
+ Alert.alert(t('general.error'), t('general.countryNotAllowed'));
182
+ appContext.onError?.(t('general.countryNotAllowed'));
183
+ navigationManagerRef.current?.reset();
184
+ return;
153
185
  }
154
186
 
155
187
  if (allowedDocumentTypes && allowedDocumentTypes.length > 0) {
@@ -2,6 +2,7 @@ import React, { useContext, useEffect, useState } from 'react';
2
2
 
3
3
  import type { MRZFields } from '../../Shared/Types/mrzFields';
4
4
  import { buildDocumentName } from '../../Shared/Libs/mrzTransliteration';
5
+ import { isCountryAllowed } from '../../Shared/Libs/country-allow';
5
6
  import { normalizeFromMRZFields } from '../../Shared/Libs/documentDataNormalizer';
6
7
  import { Alert, SafeAreaView, StyleSheet, View } from 'react-native';
7
8
  import { useSafeAreaInsets } from 'react-native-safe-area-context';
@@ -36,7 +37,7 @@ const IdentityDocumentScanningScreen = () => {
36
37
  const [allowedDocumentTypes, setAllowedDocumentTypes] = useState<
37
38
  ('I' | 'P')[] | null
38
39
  >(null);
39
- const [allowedCountries, setAllowedCountries] = useState<'TUR'[] | null>(
40
+ const [allowedCountries, setAllowedCountries] = useState<string[] | null>(
40
41
  null
41
42
  );
42
43
 
@@ -100,16 +101,13 @@ const IdentityDocumentScanningScreen = () => {
100
101
  ? 'PASSPORT'
101
102
  : 'UNKNOWN';
102
103
 
103
- if (allowedCountries && allowedCountries.length > 0) {
104
- const countryCode = mrzFields.issuingState;
105
- if (!allowedCountries.includes(countryCode as 'TUR')) {
106
- // Workflow validation - user's document country not in allowed list
107
- // Expected behavior, not a bug
108
- Alert.alert(t('general.error'), t('general.countryNotAllowed'));
109
- appContext.onError?.(t('general.countryNotAllowed'));
110
- navigationManagerRef.current?.reset();
111
- return;
112
- }
104
+ if (!isCountryAllowed(mrzFields.issuingState, allowedCountries)) {
105
+ // Workflow validation - user's document country not in allowed list
106
+ // Expected behavior, not a bug
107
+ Alert.alert(t('general.error'), t('general.countryNotAllowed'));
108
+ appContext.onError?.(t('general.countryNotAllowed'));
109
+ navigationManagerRef.current?.reset();
110
+ return;
113
111
  }
114
112
 
115
113
  if (allowedDocumentTypes && allowedDocumentTypes.length > 0) {
@@ -94,6 +94,8 @@ const ResultScreen = () => {
94
94
  document: false,
95
95
  media: false,
96
96
  verbalConsentVideos: false,
97
+ address: false,
98
+ passiveAuth: false,
97
99
  });
98
100
  const [shouldShowDemoData, setShouldShowDemoData] = useState(false);
99
101
  const [deviceIdentifier, setDeviceIdentifier] = useState<string>('');
@@ -251,15 +253,65 @@ const ResultScreen = () => {
251
253
  JSON.stringify(appContext.identificationInfo.consent),
252
254
  sessionKey
253
255
  );
256
+ const response = await httpClient.post<
257
+ { terminate?: boolean; reason?: string | null },
258
+ { encryptedData: string; nonce: string }
259
+ >(`${apiUrl}/identifications/${identificationId}/consents`, {
260
+ encryptedData,
261
+ nonce,
262
+ });
263
+ // MASAK 4/C(1)(e): the server risk-evaluates the technical data and may
264
+ // instruct us to terminate the process. Honor it by aborting the flow.
265
+ if (response?.terminate) {
266
+ throw Object.assign(new Error('session terminated'), {
267
+ sessionTerminated: true,
268
+ terminateReason: response.reason ?? undefined,
269
+ });
270
+ }
271
+ },
272
+ [apiUrl, appContext.identificationInfo.consent]
273
+ );
274
+
275
+ // MASAK 4/C(1)(d): submit the captured address. The proof document image is
276
+ // uploaded separately as ADDRESS_PROOF_DOCUMENT media.
277
+ const submitIdentificationAddress = useCallback(
278
+ async (identificationId: string, sessionKey: string) => {
279
+ const address = appContext.identificationInfo.address;
280
+ if (!address?.addressText) return;
281
+ const { encryptedData, nonce } = encryptWithAes(
282
+ JSON.stringify({
283
+ addressText: address.addressText,
284
+ fromChip: address.fromChip,
285
+ }),
286
+ sessionKey
287
+ );
254
288
  await httpClient.post(
255
- `${apiUrl}/identifications/${identificationId}/consents`,
256
- {
257
- encryptedData,
258
- nonce,
259
- }
289
+ `${apiUrl}/identifications/${identificationId}/address`,
290
+ { encryptedData, nonce }
260
291
  );
261
292
  },
262
- [apiUrl, appContext.identificationInfo.consent]
293
+ [apiUrl, appContext.identificationInfo.address]
294
+ );
295
+
296
+ // MASAK 4/C(1)(b),(c): submit EF.SOD for server-side Passive Authentication
297
+ // (signature + CSCA chaining). Only when the eID step ran with passiveAuth.
298
+ const submitPassiveAuth = useCallback(
299
+ async (identificationId: string, sessionKey: string) => {
300
+ const sd = appContext.identificationInfo.scannedDocument;
301
+ if (!sd?.paSodBase64) return;
302
+ const { encryptedData, nonce } = encryptWithAes(
303
+ JSON.stringify({
304
+ sodBase64: sd.paSodBase64,
305
+ integrityStatus: sd.paIntegrityStatus ?? null,
306
+ }),
307
+ sessionKey
308
+ );
309
+ await httpClient.post(
310
+ `${apiUrl}/identifications/${identificationId}/passive-auth`,
311
+ { encryptedData, nonce }
312
+ );
313
+ },
314
+ [apiUrl, appContext.identificationInfo.scannedDocument]
263
315
  );
264
316
 
265
317
  const submitIdentificationDocument = useCallback(
@@ -287,6 +339,8 @@ const ResultScreen = () => {
287
339
  gender: getGenderEnumType(rawMrzFields.sex),
288
340
  number: rawMrzFields.documentNumber,
289
341
  country: rawMrzFields.issuingState,
342
+ // Raw ICAO nationality (for the MASAK 4/C(3) risky-country block).
343
+ nationality: rawMrzFields.nationality,
290
344
  barcodeValue: scannedDocument.barcodeValue,
291
345
  personalNumber: rawMrzFields.personalNumber || rawMrzFields.optional1,
292
346
  birthDate: mrzUtils.convertMRZDateToISODate(rawMrzFields.birthDate),
@@ -408,6 +462,12 @@ const ResultScreen = () => {
408
462
  await writeTempImage('DOCUMENT_HOLOGRAM_IMAGE.jpg', hologramImage);
409
463
  }
410
464
 
465
+ // MASAK 4/C(1)(d): the address proof document, when captured.
466
+ const addressProof = appContext.identificationInfo.address?.proofImage;
467
+ if (addressProof && addressProof !== '') {
468
+ await writeTempImage('ADDRESS_PROOF_DOCUMENT.jpg', addressProof);
469
+ }
470
+
411
471
  if (livenessDetection) {
412
472
  for (const instruction of livenessDetection.instructions) {
413
473
  if (instruction.photo !== '') {
@@ -587,6 +647,16 @@ const ResultScreen = () => {
587
647
  }
588
648
  setProgress(30);
589
649
 
650
+ if (
651
+ !completedSteps.current.address &&
652
+ appContext.identificationInfo.address?.addressText
653
+ ) {
654
+ await runWithRetry(() =>
655
+ submitIdentificationAddress(identificationId, sessionKey)
656
+ );
657
+ completedSteps.current.address = true;
658
+ }
659
+
590
660
  if (!alreadyUploaded) {
591
661
  const scannedIdentityDocument = identificationInfo.scannedDocument;
592
662
  if (
@@ -603,6 +673,16 @@ const ResultScreen = () => {
603
673
  );
604
674
  completedSteps.current.document = true;
605
675
  }
676
+
677
+ if (
678
+ !completedSteps.current.passiveAuth &&
679
+ scannedIdentityDocument?.paSodBase64
680
+ ) {
681
+ await runWithRetry(() =>
682
+ submitPassiveAuth(identificationId, sessionKey)
683
+ );
684
+ completedSteps.current.passiveAuth = true;
685
+ }
606
686
  setProgress(40);
607
687
 
608
688
  if (!completedSteps.current.media) {
@@ -656,6 +736,19 @@ const ResultScreen = () => {
656
736
  setIsSubmitting(false);
657
737
  setProgress(0);
658
738
 
739
+ // MASAK 4/C(1)(e): the server terminated the session on a risk signal.
740
+ // This is terminal, not recoverable — surface it and stop, with no
741
+ // retry affordance (re-submitting can't change the decision).
742
+ if (
743
+ error &&
744
+ typeof error === 'object' &&
745
+ (error as { sessionTerminated?: boolean }).sessionTerminated
746
+ ) {
747
+ appContext.onError?.(t('resultScreen.sessionTerminated'));
748
+ setSubmitFailed(true);
749
+ return;
750
+ }
751
+
659
752
  // Track verification failure
660
753
  const errorMessage =
661
754
  error instanceof Error ? error.message : 'Unknown error';
@@ -694,6 +787,8 @@ const ResultScreen = () => {
694
787
  finishIdentification,
695
788
  submitIdentificationDocument,
696
789
  submitIdentificationConsent,
790
+ submitIdentificationAddress,
791
+ submitPassiveAuth,
697
792
  t,
698
793
  appContext,
699
794
  ]