@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
|
@@ -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":"
|
|
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;
|
|
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.
|
|
1
|
+
export declare const SDK_VERSION = "1.501.1";
|
|
2
2
|
//# sourceMappingURL=version.d.ts.map
|
package/package.json
CHANGED
|
@@ -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<
|
|
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 (
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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<
|
|
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 (
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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}/
|
|
256
|
-
{
|
|
257
|
-
encryptedData,
|
|
258
|
-
nonce,
|
|
259
|
-
}
|
|
289
|
+
`${apiUrl}/identifications/${identificationId}/address`,
|
|
290
|
+
{ encryptedData, nonce }
|
|
260
291
|
);
|
|
261
292
|
},
|
|
262
|
-
[apiUrl, appContext.identificationInfo.
|
|
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
|
]
|