@trustchex/react-native-sdk 1.481.1 → 1.483.4
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/lib/module/Screens/Dynamic/IdentityDocumentEIDScanningScreen.js +0 -3
- package/lib/module/Screens/Dynamic/IdentityDocumentScanningScreen.js +21 -0
- package/lib/module/Screens/Dynamic/VideoCallScreen.js +1 -1
- package/lib/module/Screens/Static/ResultScreen.js +33 -10
- package/lib/module/Shared/Components/EIDScanner.js +52 -19
- package/lib/module/Shared/Contexts/AppContext.js +3 -0
- package/lib/module/Shared/EIDReader/eidReader.js +18 -1
- package/lib/module/Shared/EIDReader/lds/icao/dg11File.js +77 -0
- package/lib/module/Shared/Libs/documentDataNormalizer.js +104 -0
- package/lib/module/Shared/Libs/mrz.utils.js +0 -5
- package/lib/module/Shared/Libs/mrzTransliteration.js +80 -0
- package/lib/module/Shared/Services/DataUploadService.js +6 -3
- package/lib/module/Shared/Types/documentData.js +1 -0
- package/lib/module/Shared/Types/documentReadResult.js +3 -0
- package/lib/module/Trustchex.js +12 -1
- package/lib/module/version.js +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/Dynamic/VideoCallScreen.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.map +1 -1
- package/lib/typescript/src/Shared/Contexts/AppContext.d.ts +5 -0
- package/lib/typescript/src/Shared/Contexts/AppContext.d.ts.map +1 -1
- package/lib/typescript/src/Shared/EIDReader/eidReader.d.ts +2 -0
- package/lib/typescript/src/Shared/EIDReader/eidReader.d.ts.map +1 -1
- package/lib/typescript/src/Shared/EIDReader/lds/icao/dg11File.d.ts +35 -0
- package/lib/typescript/src/Shared/EIDReader/lds/icao/dg11File.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Libs/documentDataNormalizer.d.ts +12 -0
- package/lib/typescript/src/Shared/Libs/documentDataNormalizer.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Libs/mrz.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/mrzTransliteration.d.ts +3 -0
- package/lib/typescript/src/Shared/Libs/mrzTransliteration.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Services/DataUploadService.d.ts +3 -2
- package/lib/typescript/src/Shared/Services/DataUploadService.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Types/documentData.d.ts +44 -0
- package/lib/typescript/src/Shared/Types/documentData.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Types/documentReadResult.d.ts +44 -0
- package/lib/typescript/src/Shared/Types/documentReadResult.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Types/mrzFields.d.ts +1 -1
- package/lib/typescript/src/Shared/Types/mrzFields.d.ts.map +1 -1
- package/lib/typescript/src/Trustchex.d.ts +9 -0
- package/lib/typescript/src/Trustchex.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +2 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/version.d.ts +1 -1
- package/package.json +1 -1
- package/src/Screens/Dynamic/IdentityDocumentEIDScanningScreen.tsx +0 -4
- package/src/Screens/Dynamic/IdentityDocumentScanningScreen.tsx +23 -0
- package/src/Screens/Dynamic/VideoCallScreen.tsx +5 -1
- package/src/Screens/Static/ResultScreen.tsx +39 -15
- package/src/Shared/Components/EIDScanner.tsx +67 -18
- package/src/Shared/Contexts/AppContext.ts +8 -0
- package/src/Shared/EIDReader/eidReader.ts +27 -1
- package/src/Shared/EIDReader/lds/icao/dg11File.ts +94 -0
- package/src/Shared/Libs/documentDataNormalizer.ts +108 -0
- package/src/Shared/Libs/mrz.utils.ts +0 -5
- package/src/Shared/Libs/mrzTransliteration.ts +131 -0
- package/src/Shared/Services/DataUploadService.ts +9 -3
- package/src/Shared/Types/documentData.ts +52 -0
- package/src/Shared/Types/documentReadResult.ts +47 -0
- package/src/Shared/Types/mrzFields.ts +0 -5
- package/src/Trustchex.tsx +22 -0
- package/src/index.tsx +7 -0
- package/src/version.ts +1 -1
|
@@ -70,9 +70,6 @@ const IdentityDocumentEIDScanningScreen = () => {
|
|
|
70
70
|
setAllowedDocumentTypes(appContext.currentWorkflowStep?.data?.allowedDocumentTypes ?? null);
|
|
71
71
|
setAllowedCountries(appContext.currentWorkflowStep?.data?.allowedCountries ?? null);
|
|
72
72
|
}, [appContext.currentWorkflowStep]);
|
|
73
|
-
if (!appContext.currentWorkflowStep) {
|
|
74
|
-
return null;
|
|
75
|
-
}
|
|
76
73
|
return /*#__PURE__*/_jsxs(SafeAreaView, {
|
|
77
74
|
style: styles.safeAreaContainer,
|
|
78
75
|
children: [documentNumber && dateOfBirth && dateOfExpiry ? /*#__PURE__*/_jsx(View, {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
import React, { useContext, useEffect, useState } from 'react';
|
|
4
|
+
import { buildDocumentName } from "../../Shared/Libs/mrzTransliteration.js";
|
|
5
|
+
import { normalizeFromMRZFields } from "../../Shared/Libs/documentDataNormalizer.js";
|
|
4
6
|
import { Alert, SafeAreaView, StyleSheet, View } from 'react-native';
|
|
5
7
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
6
8
|
import IdentityDocumentCamera from "../../Shared/Components/IdentityDocumentCamera.js";
|
|
@@ -78,6 +80,25 @@ const IdentityDocumentScanningScreen = () => {
|
|
|
78
80
|
|
|
79
81
|
// Track successful document scan completion as funnel step
|
|
80
82
|
trackFunnelStep('Document Scan Completed', 2, 5, 'contract_acceptance', true);
|
|
83
|
+
{
|
|
84
|
+
const documentData = normalizeFromMRZFields(mrzFields);
|
|
85
|
+
const faceImg = idFrontSideData?.faceImage || passportData?.faceImage;
|
|
86
|
+
const builtName = buildDocumentName(documentData.firstName, documentData.lastName, documentData.issuingCountry);
|
|
87
|
+
const documentReadResult = {
|
|
88
|
+
document: {
|
|
89
|
+
...documentData,
|
|
90
|
+
firstName: builtName.displayFirst,
|
|
91
|
+
lastName: builtName.displayLast
|
|
92
|
+
},
|
|
93
|
+
name: builtName,
|
|
94
|
+
face: faceImg ? {
|
|
95
|
+
data: faceImg,
|
|
96
|
+
mimeType: 'image/jpeg'
|
|
97
|
+
} : undefined
|
|
98
|
+
};
|
|
99
|
+
appContext.setLastDocumentRead?.(documentReadResult);
|
|
100
|
+
appContext.onDocumentRead?.(documentReadResult);
|
|
101
|
+
}
|
|
81
102
|
setTimeout(() => navigationManagerRef.current?.navigateToNextStep(), 1000);
|
|
82
103
|
}
|
|
83
104
|
}, [idFrontSideData, idBackSideData, passportData, appContext.identificationInfo, allowedCountries, allowedDocumentTypes, t, appContext]);
|
|
@@ -126,7 +126,7 @@ const VideoCallScreen = ({
|
|
|
126
126
|
try {
|
|
127
127
|
const uploadService = new DataUploadService(baseUrl);
|
|
128
128
|
dataUploadServiceRef.current = uploadService;
|
|
129
|
-
const uploaded = await uploadService.uploadCollectedData(identificationInfo);
|
|
129
|
+
const uploaded = await uploadService.uploadCollectedData(identificationInfo, undefined, appContext.lastDocumentRead?.name);
|
|
130
130
|
if (!uploaded) {
|
|
131
131
|
console.warn('[VideoCallScreen] Upload service returned false');
|
|
132
132
|
if (mounted) {
|
|
@@ -178,14 +178,15 @@ const ResultScreen = () => {
|
|
|
178
178
|
if (!scannedDocument.mrzFields) {
|
|
179
179
|
throw new Error('MRZ fields not found');
|
|
180
180
|
}
|
|
181
|
-
|
|
182
|
-
// Backend payload must contain raw MRZ data only.
|
|
183
|
-
// Human-friendly formatting is strictly for UI rendering paths.
|
|
184
181
|
const rawMrzFields = scannedDocument.mrzFields;
|
|
182
|
+
const docName = appContext.lastDocumentRead?.name;
|
|
185
183
|
const identificationDocument = {
|
|
186
184
|
type: rawMrzFields.documentCode,
|
|
187
185
|
name: rawMrzFields.firstName,
|
|
188
186
|
surname: rawMrzFields.lastName,
|
|
187
|
+
displayName: docName?.source === 'dg11' ? docName.displayFirst : undefined,
|
|
188
|
+
displaySurname: docName?.source === 'dg11' ? docName.displayLast : undefined,
|
|
189
|
+
nameSource: docName?.source === 'dg11' ? docName.source : undefined,
|
|
189
190
|
gender: getGenderEnumType(rawMrzFields.sex),
|
|
190
191
|
number: rawMrzFields.documentNumber,
|
|
191
192
|
country: rawMrzFields.issuingState,
|
|
@@ -204,7 +205,7 @@ const ResultScreen = () => {
|
|
|
204
205
|
encryptedData,
|
|
205
206
|
nonce
|
|
206
207
|
});
|
|
207
|
-
}, [apiUrl, getGenderEnumType]);
|
|
208
|
+
}, [apiUrl, appContext.lastDocumentRead, getGenderEnumType]);
|
|
208
209
|
const uploadIdentificationMedia = useCallback(async (identificationId, scannedIdentityDocument, livenessDetection, skipIfAlreadyUploaded) => {
|
|
209
210
|
// Skip media upload if already uploaded during video call
|
|
210
211
|
if (skipIfAlreadyUploaded) {
|
|
@@ -533,18 +534,30 @@ const ResultScreen = () => {
|
|
|
533
534
|
children: [/*#__PURE__*/_jsxs(Text, {
|
|
534
535
|
style: styles.mrzInfoLabel,
|
|
535
536
|
children: [t('eidScannerScreen.name'), ":"]
|
|
536
|
-
}), /*#__PURE__*/
|
|
537
|
-
style: styles.
|
|
538
|
-
children:
|
|
537
|
+
}), /*#__PURE__*/_jsxs(View, {
|
|
538
|
+
style: styles.mrzInfoValueGroup,
|
|
539
|
+
children: [/*#__PURE__*/_jsx(Text, {
|
|
540
|
+
style: styles.mrzInfoText,
|
|
541
|
+
children: appContext.lastDocumentRead?.name.source === 'dg11' ? appContext.lastDocumentRead.name.displayFirst : appContext.identificationInfo.scannedDocument.mrzFields?.firstName
|
|
542
|
+
}), appContext.lastDocumentRead?.name.source === 'dg11' && /*#__PURE__*/_jsx(Text, {
|
|
543
|
+
style: styles.mrzInfoSubText,
|
|
544
|
+
children: appContext.identificationInfo.scannedDocument.mrzFields?.firstName
|
|
545
|
+
})]
|
|
539
546
|
})]
|
|
540
547
|
}), /*#__PURE__*/_jsxs(View, {
|
|
541
548
|
style: styles.mrzInfoItem,
|
|
542
549
|
children: [/*#__PURE__*/_jsxs(Text, {
|
|
543
550
|
style: styles.mrzInfoLabel,
|
|
544
551
|
children: [t('eidScannerScreen.surname'), ":"]
|
|
545
|
-
}), /*#__PURE__*/
|
|
546
|
-
style: styles.
|
|
547
|
-
children:
|
|
552
|
+
}), /*#__PURE__*/_jsxs(View, {
|
|
553
|
+
style: styles.mrzInfoValueGroup,
|
|
554
|
+
children: [/*#__PURE__*/_jsx(Text, {
|
|
555
|
+
style: styles.mrzInfoText,
|
|
556
|
+
children: appContext.lastDocumentRead?.name.source === 'dg11' ? appContext.lastDocumentRead.name.displayLast : appContext.identificationInfo.scannedDocument.mrzFields?.lastName
|
|
557
|
+
}), appContext.lastDocumentRead?.name.source === 'dg11' && /*#__PURE__*/_jsx(Text, {
|
|
558
|
+
style: styles.mrzInfoSubText,
|
|
559
|
+
children: appContext.identificationInfo.scannedDocument.mrzFields?.lastName
|
|
560
|
+
})]
|
|
548
561
|
})]
|
|
549
562
|
}), /*#__PURE__*/_jsxs(View, {
|
|
550
563
|
style: styles.mrzInfoItem,
|
|
@@ -882,6 +895,16 @@ const styles = StyleSheet.create({
|
|
|
882
895
|
fontSize: 10,
|
|
883
896
|
fontFamily: Platform.OS === 'ios' ? 'Courier' : 'monospace'
|
|
884
897
|
},
|
|
898
|
+
mrzInfoValueGroup: {
|
|
899
|
+
flexDirection: 'column',
|
|
900
|
+
alignItems: 'flex-end'
|
|
901
|
+
},
|
|
902
|
+
mrzInfoSubText: {
|
|
903
|
+
color: '#aaa',
|
|
904
|
+
fontSize: 9,
|
|
905
|
+
fontFamily: Platform.OS === 'ios' ? 'Courier' : 'monospace',
|
|
906
|
+
marginTop: 1
|
|
907
|
+
},
|
|
885
908
|
mrzTextContainer: {
|
|
886
909
|
padding: 12,
|
|
887
910
|
backgroundColor: '#F3F4F6',
|
|
@@ -9,6 +9,8 @@ import PermissionManager from "../Libs/permissions.utils.js";
|
|
|
9
9
|
import { debugError } from "../Libs/debug.utils.js";
|
|
10
10
|
import { eidReader } from "../EIDReader/eidReader.js";
|
|
11
11
|
import NativeProgressBar from "./NativeProgressBar.js";
|
|
12
|
+
import { buildDocumentName } from "../Libs/mrzTransliteration.js";
|
|
13
|
+
import { normalizeFromMRZInfo } from "../Libs/documentDataNormalizer.js";
|
|
12
14
|
import { useTranslation } from 'react-i18next';
|
|
13
15
|
import AppContext from "../Contexts/AppContext.js";
|
|
14
16
|
import StyledButton from "./StyledButton.js";
|
|
@@ -34,6 +36,7 @@ const EIDScanner = ({
|
|
|
34
36
|
const [documentFaceImage, setDocumentFaceImage] = React.useState();
|
|
35
37
|
const [documentMRZInfo, setDocumentMRZInfo] = React.useState();
|
|
36
38
|
const [documentFaceImageMimeType, setDocumentFaceImageMimeType] = React.useState();
|
|
39
|
+
const [documentName, setDocumentName] = React.useState();
|
|
37
40
|
const [progress, setProgress] = React.useState(0);
|
|
38
41
|
const [progressStage, setProgressStage] = React.useState('');
|
|
39
42
|
|
|
@@ -99,6 +102,20 @@ const EIDScanner = ({
|
|
|
99
102
|
}, [i18n.language]);
|
|
100
103
|
const [voiceGuidanceMessage, setVoiceGuidanceMessage] = useState();
|
|
101
104
|
const [attemptNumber, setAttemptNumber] = useState(0);
|
|
105
|
+
const getFieldsFromMRZ = useCallback(mrz => {
|
|
106
|
+
return {
|
|
107
|
+
documentCode: mrz.getDocumentCode(),
|
|
108
|
+
personalNumber: mrz.getPersonalNumber() || mrz.getOptionalData1(),
|
|
109
|
+
documentNumber: mrz.getDocumentNumber(),
|
|
110
|
+
firstName: mrz.getSecondaryIdentifier(),
|
|
111
|
+
lastName: mrz.getPrimaryIdentifier(),
|
|
112
|
+
birthDate: mrz.getDateOfBirth(),
|
|
113
|
+
expirationDate: mrz.getDateOfExpiry(),
|
|
114
|
+
issuingState: mrz.getIssuingState(),
|
|
115
|
+
sex: mrz.getGender().getStrCode(),
|
|
116
|
+
nationality: mrz.getNationality()
|
|
117
|
+
};
|
|
118
|
+
}, []);
|
|
102
119
|
const readNFC = useCallback(async () => {
|
|
103
120
|
const startTime = Date.now();
|
|
104
121
|
const currentAttempt = attemptNumber + 1;
|
|
@@ -160,6 +177,25 @@ const EIDScanner = ({
|
|
|
160
177
|
setDocumentMRZInfo(passportData.mrz);
|
|
161
178
|
setDocumentFaceImageMimeType(passportData.mimeType);
|
|
162
179
|
setIsScanned(true);
|
|
180
|
+
{
|
|
181
|
+
const documentData = normalizeFromMRZInfo(passportData.mrz);
|
|
182
|
+
const builtName = buildDocumentName(documentData.firstName, documentData.lastName, documentData.issuingCountry, passportData.dg11FirstName, passportData.dg11LastName);
|
|
183
|
+
setDocumentName(builtName);
|
|
184
|
+
const documentReadResult = {
|
|
185
|
+
document: {
|
|
186
|
+
...documentData,
|
|
187
|
+
firstName: builtName.displayFirst,
|
|
188
|
+
lastName: builtName.displayLast
|
|
189
|
+
},
|
|
190
|
+
name: builtName,
|
|
191
|
+
face: {
|
|
192
|
+
data: passportData.faceImage,
|
|
193
|
+
mimeType: passportData.mimeType
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
appContext.setLastDocumentRead?.(documentReadResult);
|
|
197
|
+
appContext.onDocumentRead?.(documentReadResult);
|
|
198
|
+
}
|
|
163
199
|
await trackEIDScanComplete(docType, scanDuration, currentAttempt).catch(() => {});
|
|
164
200
|
}
|
|
165
201
|
} else {
|
|
@@ -196,21 +232,7 @@ const EIDScanner = ({
|
|
|
196
232
|
setProgressStage('');
|
|
197
233
|
setHasGuideShown(false);
|
|
198
234
|
}
|
|
199
|
-
}, [documentNumber, dateOfBirth, dateOfExpiry, documentType, hasNfc, isEnabled, attemptNumber, t]);
|
|
200
|
-
const getFieldsFromMRZ = useCallback(mrz => {
|
|
201
|
-
return {
|
|
202
|
-
documentCode: mrz.getDocumentCode(),
|
|
203
|
-
personalNumber: mrz.getPersonalNumber() || mrz.getOptionalData1(),
|
|
204
|
-
documentNumber: mrz.getDocumentNumber(),
|
|
205
|
-
firstName: mrz.getSecondaryIdentifier(),
|
|
206
|
-
lastName: mrz.getPrimaryIdentifier(),
|
|
207
|
-
birthDate: mrz.getDateOfBirth(),
|
|
208
|
-
expirationDate: mrz.getDateOfExpiry(),
|
|
209
|
-
issuingState: mrz.getIssuingState(),
|
|
210
|
-
sex: mrz.getGender().getStrCode(),
|
|
211
|
-
nationality: mrz.getNationality()
|
|
212
|
-
};
|
|
213
|
-
}, []);
|
|
235
|
+
}, [documentNumber, dateOfBirth, dateOfExpiry, documentType, hasNfc, isEnabled, attemptNumber, t, getFieldsFromMRZ, appContext]);
|
|
214
236
|
useEffect(() => {
|
|
215
237
|
const checkIsSupported = async () => {
|
|
216
238
|
const deviceIsSupported = await NFCManager.isSupported();
|
|
@@ -367,10 +389,12 @@ const EIDScanner = ({
|
|
|
367
389
|
style: styles.idCardFields,
|
|
368
390
|
children: [/*#__PURE__*/_jsx(IDCardField, {
|
|
369
391
|
label: t('eidScannerScreen.surname'),
|
|
370
|
-
value: documentMRZInfo.getPrimaryIdentifier()
|
|
392
|
+
value: documentName?.displayLast ?? documentMRZInfo.getPrimaryIdentifier(),
|
|
393
|
+
subValue: documentName?.source === 'dg11' ? documentMRZInfo.getPrimaryIdentifier() : undefined
|
|
371
394
|
}), /*#__PURE__*/_jsx(IDCardField, {
|
|
372
395
|
label: t('eidScannerScreen.name'),
|
|
373
|
-
value: documentMRZInfo.getSecondaryIdentifier()
|
|
396
|
+
value: documentName?.displayFirst ?? documentMRZInfo.getSecondaryIdentifier(),
|
|
397
|
+
subValue: documentName?.source === 'dg11' ? documentMRZInfo.getSecondaryIdentifier() : undefined
|
|
374
398
|
}), /*#__PURE__*/_jsx(IDCardField, {
|
|
375
399
|
label: t('eidScannerScreen.documentNumber'),
|
|
376
400
|
value: documentMRZInfo.getDocumentNumber()
|
|
@@ -627,6 +651,11 @@ const styles = StyleSheet.create({
|
|
|
627
651
|
flexShrink: 1,
|
|
628
652
|
flexWrap: 'wrap'
|
|
629
653
|
},
|
|
654
|
+
idCardFieldSubValue: {
|
|
655
|
+
fontSize: 9,
|
|
656
|
+
color: '#aaa',
|
|
657
|
+
marginTop: 1
|
|
658
|
+
},
|
|
630
659
|
guide: {
|
|
631
660
|
flex: 1,
|
|
632
661
|
display: 'flex',
|
|
@@ -658,7 +687,8 @@ const styles = StyleSheet.create({
|
|
|
658
687
|
});
|
|
659
688
|
const IDCardField = ({
|
|
660
689
|
label,
|
|
661
|
-
value
|
|
690
|
+
value,
|
|
691
|
+
subValue
|
|
662
692
|
}) => /*#__PURE__*/_jsxs(View, {
|
|
663
693
|
style: styles.idCardFieldItem,
|
|
664
694
|
children: [/*#__PURE__*/_jsx(Text, {
|
|
@@ -668,6 +698,9 @@ const IDCardField = ({
|
|
|
668
698
|
}), /*#__PURE__*/_jsx(Text, {
|
|
669
699
|
style: styles.idCardFieldValue,
|
|
670
700
|
children: value || '—'
|
|
671
|
-
})
|
|
701
|
+
}), subValue ? /*#__PURE__*/_jsx(Text, {
|
|
702
|
+
style: styles.idCardFieldSubValue,
|
|
703
|
+
children: subValue
|
|
704
|
+
}) : null]
|
|
672
705
|
});
|
|
673
706
|
export default EIDScanner;
|
|
@@ -24,8 +24,11 @@ export default /*#__PURE__*/createContext({
|
|
|
24
24
|
currentWorkflowStep: undefined,
|
|
25
25
|
isDebugNavigated: false,
|
|
26
26
|
isTestVideoSession: false,
|
|
27
|
+
lastDocumentRead: undefined,
|
|
27
28
|
onCompleted: undefined,
|
|
28
29
|
onError: undefined,
|
|
30
|
+
onDocumentRead: undefined,
|
|
31
|
+
setLastDocumentRead: undefined,
|
|
29
32
|
setSessionId: undefined,
|
|
30
33
|
setBaseUrl: undefined
|
|
31
34
|
});
|
|
@@ -6,6 +6,7 @@ import { NFCManagerCardService } from "./nfcManagerCardService.js";
|
|
|
6
6
|
import { EIDService } from "./eidService.js";
|
|
7
7
|
import { DG1File } from "./lds/icao/dg1File.js";
|
|
8
8
|
import { DG2File } from "./lds/icao/dg2File.js";
|
|
9
|
+
import { DG11File } from "./lds/icao/dg11File.js";
|
|
9
10
|
import { Buffer } from 'buffer';
|
|
10
11
|
import { InputStream } from "./java/inputStream.js";
|
|
11
12
|
import { debugLog } from "../Libs/debug.utils.js";
|
|
@@ -495,6 +496,20 @@ const eidReader = async (documentNumber, dateOfBirth, dateOfExpiry, progressCall
|
|
|
495
496
|
progressCallback(progress);
|
|
496
497
|
}
|
|
497
498
|
|
|
499
|
+
// Read DG11 (Additional Personal Data — printed name in UTF-8, optional)
|
|
500
|
+
let dg11FirstName = null;
|
|
501
|
+
let dg11LastName = null;
|
|
502
|
+
try {
|
|
503
|
+
const dg11InputStream = passportService.getInputStream(EIDService.EF_DG11);
|
|
504
|
+
await dg11InputStream.init();
|
|
505
|
+
const dg11File = new DG11File(dg11InputStream);
|
|
506
|
+
await dg11File.read();
|
|
507
|
+
dg11FirstName = dg11File.getFirstName();
|
|
508
|
+
dg11LastName = dg11File.getLastName();
|
|
509
|
+
} catch {
|
|
510
|
+
// DG11 is optional — not all chips have it, and access may be denied
|
|
511
|
+
}
|
|
512
|
+
|
|
498
513
|
// Read DG2 (Face Image)
|
|
499
514
|
const dg2Done = stepTimer();
|
|
500
515
|
const dg2InputStream = passportService.getInputStream(EIDService.EF_DG2);
|
|
@@ -539,7 +554,9 @@ const eidReader = async (documentNumber, dateOfBirth, dateOfExpiry, progressCall
|
|
|
539
554
|
return {
|
|
540
555
|
mrz: mrzInfo,
|
|
541
556
|
faceImage: imageAsBase64,
|
|
542
|
-
mimeType: mimeType
|
|
557
|
+
mimeType: mimeType,
|
|
558
|
+
dg11FirstName,
|
|
559
|
+
dg11LastName
|
|
543
560
|
};
|
|
544
561
|
} catch (error) {
|
|
545
562
|
const msg = error instanceof Error ? error.message : String(error);
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { Buffer } from 'buffer';
|
|
4
|
+
import { TLVInputStream } from "../../tlv/tlvInputStream.js";
|
|
5
|
+
import { DataGroup } from "../dataGroup.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* DG11 — Additional Personal Data (ICAO 9303-10 §4.6.6).
|
|
9
|
+
*
|
|
10
|
+
* We only extract tag 0x5F0E (Name of Holder), which contains the full name
|
|
11
|
+
* exactly as printed on the document, in UTF-8, using the same `<<` primary/
|
|
12
|
+
* secondary delimiter as the MRZ (but with native Unicode characters).
|
|
13
|
+
*
|
|
14
|
+
* Example: "ÖZTÜRK<<GÖKHAN MEHMET" → lastName="ÖZTÜRK", firstName="GÖKHAN MEHMET"
|
|
15
|
+
*
|
|
16
|
+
* The file is optional on the chip — if absent or unreadable we return null
|
|
17
|
+
* so the caller can fall back to reverse transliteration.
|
|
18
|
+
*/
|
|
19
|
+
export class DG11File extends DataGroup {
|
|
20
|
+
static DG11_TAG = 0x6b;
|
|
21
|
+
static TAG_LIST_TAG = 0x5c;
|
|
22
|
+
static NAME_OF_HOLDER_TAG = 0x5f0e;
|
|
23
|
+
firstName = null;
|
|
24
|
+
lastName = null;
|
|
25
|
+
constructor(is) {
|
|
26
|
+
super(DG11File.DG11_TAG, is);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Returns the first name from DG11, or null if not present. */
|
|
30
|
+
getFirstName() {
|
|
31
|
+
return this.firstName;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Returns the last name from DG11, or null if not present. */
|
|
35
|
+
getLastName() {
|
|
36
|
+
return this.lastName;
|
|
37
|
+
}
|
|
38
|
+
async read() {
|
|
39
|
+
await super.readObject();
|
|
40
|
+
}
|
|
41
|
+
async readContent(inputStream) {
|
|
42
|
+
const tlvIn = inputStream instanceof TLVInputStream ? inputStream : new TLVInputStream(inputStream);
|
|
43
|
+
|
|
44
|
+
// Skip the tag list (0x5C) — we only care about 0x5F0E
|
|
45
|
+
// The tag list tells us which optional tags are present; we probe directly.
|
|
46
|
+
try {
|
|
47
|
+
await tlvIn.skipToTag(DG11File.NAME_OF_HOLDER_TAG);
|
|
48
|
+
} catch {
|
|
49
|
+
// Tag not present in this chip's DG11
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const length = await tlvIn.readLength();
|
|
53
|
+
if (length <= 0) return;
|
|
54
|
+
const bytes = new Uint8Array(length);
|
|
55
|
+
await inputStream.readBytesWithOffset(bytes, 0, length);
|
|
56
|
+
const nameString = Buffer.from(bytes).toString('utf8').trim();
|
|
57
|
+
this.parseName(nameString);
|
|
58
|
+
}
|
|
59
|
+
writeContent() {
|
|
60
|
+
// Write not needed for reading
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Split "PRIMARY<<SECONDARY" into last/first name components.
|
|
65
|
+
* Strips trailing filler `<` within each segment.
|
|
66
|
+
*/
|
|
67
|
+
parseName(nameString) {
|
|
68
|
+
const delimIndex = nameString.indexOf('<<');
|
|
69
|
+
if (delimIndex < 0) {
|
|
70
|
+
this.lastName = nameString.replace(/<+$/, '').trim();
|
|
71
|
+
this.firstName = '';
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
this.lastName = nameString.substring(0, delimIndex).replace(/<+$/, '').trim();
|
|
75
|
+
this.firstName = nameString.substring(delimIndex + 2).replace(/<$/, ' ').replace(/<+$/, '').trim();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Expand a 2-digit MRZ year to a 4-digit calendar year.
|
|
5
|
+
* ICAO 9303: YY >= 30 → 19YY, YY < 30 → 20YY.
|
|
6
|
+
* (Threshold 30 keeps all current valid documents in range.)
|
|
7
|
+
*/
|
|
8
|
+
function expandYear(yy) {
|
|
9
|
+
const n = parseInt(yy, 10);
|
|
10
|
+
return n >= 30 ? `19${yy}` : `20${yy}`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Convert a YYMMDD MRZ date string to ISO 8601 YYYY-MM-DD.
|
|
15
|
+
* Returns null for empty, null, or malformed input.
|
|
16
|
+
*/
|
|
17
|
+
function mrzDateToISO(raw) {
|
|
18
|
+
if (!raw || raw.length !== 6) return null;
|
|
19
|
+
const yy = raw.slice(0, 2);
|
|
20
|
+
const mm = raw.slice(2, 4);
|
|
21
|
+
const dd = raw.slice(4, 6);
|
|
22
|
+
if (!/^\d{6}$/.test(raw)) return null;
|
|
23
|
+
return `${expandYear(yy)}-${mm}-${dd}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Normalize the MRZ document-code field to a single-letter form.
|
|
28
|
+
* The `mrz` npm package returns "ID" for TD1, while the chip returns "I".
|
|
29
|
+
*/
|
|
30
|
+
function normalizeDocumentType(raw) {
|
|
31
|
+
if (!raw) return 'I';
|
|
32
|
+
const upper = raw.trim().toUpperCase();
|
|
33
|
+
if (upper === 'ID') return 'I';
|
|
34
|
+
// TD2 can be "IR", "AC", etc — take only the first character
|
|
35
|
+
return upper.charAt(0);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Normalize the sex field to a single uppercase letter.
|
|
40
|
+
* NFC chip: "M" | "F" | "U" | "X"
|
|
41
|
+
* mrz package: "male" | "female" | "" | null
|
|
42
|
+
*/
|
|
43
|
+
function normalizeSex(raw) {
|
|
44
|
+
if (!raw) return 'U';
|
|
45
|
+
switch (raw.trim().toLowerCase()) {
|
|
46
|
+
case 'm':
|
|
47
|
+
case 'male':
|
|
48
|
+
return 'M';
|
|
49
|
+
case 'f':
|
|
50
|
+
case 'female':
|
|
51
|
+
return 'F';
|
|
52
|
+
case 'x':
|
|
53
|
+
case 'unspecified':
|
|
54
|
+
return 'X';
|
|
55
|
+
default:
|
|
56
|
+
return 'U';
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Trim a string and return null if empty or only filler chars.
|
|
62
|
+
*/
|
|
63
|
+
function clean(val) {
|
|
64
|
+
if (!val) return null;
|
|
65
|
+
const t = val.replace(/</g, '').trim();
|
|
66
|
+
return t.length > 0 ? t : null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Build a DocumentData from a chip-read MRZInfo object (NFC path).
|
|
71
|
+
*/
|
|
72
|
+
export function normalizeFromMRZInfo(mrz) {
|
|
73
|
+
const rawPersonal = mrz.getPersonalNumber() ?? clean(mrz.getOptionalData1());
|
|
74
|
+
return {
|
|
75
|
+
documentType: normalizeDocumentType(mrz.getDocumentCode()),
|
|
76
|
+
issuingCountry: mrz.getIssuingState().replace(/<$/, '').trim(),
|
|
77
|
+
nationality: mrz.getNationality().replace(/<$/, '').trim(),
|
|
78
|
+
documentNumber: mrz.getDocumentNumber().trim(),
|
|
79
|
+
personalNumber: clean(rawPersonal),
|
|
80
|
+
lastName: mrz.getPrimaryIdentifier().trim(),
|
|
81
|
+
firstName: mrz.getSecondaryIdentifier().trim(),
|
|
82
|
+
sex: normalizeSex(mrz.getGender().getStrCode()),
|
|
83
|
+
dateOfBirth: mrzDateToISO(mrz.getDateOfBirth()),
|
|
84
|
+
dateOfExpiry: mrzDateToISO(mrz.getDateOfExpiry())
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Build a DocumentData from an MRZFields object (camera OCR path).
|
|
90
|
+
*/
|
|
91
|
+
export function normalizeFromMRZFields(fields) {
|
|
92
|
+
return {
|
|
93
|
+
documentType: normalizeDocumentType(fields.documentCode),
|
|
94
|
+
issuingCountry: clean(fields.issuingState) ?? '',
|
|
95
|
+
nationality: clean(fields.nationality) ?? '',
|
|
96
|
+
documentNumber: clean(fields.documentNumber) ?? '',
|
|
97
|
+
personalNumber: clean(fields.personalNumber),
|
|
98
|
+
lastName: clean(fields.lastName) ?? '',
|
|
99
|
+
firstName: clean(fields.firstName) ?? '',
|
|
100
|
+
sex: normalizeSex(fields.sex),
|
|
101
|
+
dateOfBirth: mrzDateToISO(fields.birthDate),
|
|
102
|
+
dateOfExpiry: mrzDateToISO(fields.expirationDate)
|
|
103
|
+
};
|
|
104
|
+
}
|
|
@@ -1134,18 +1134,13 @@ const validateMRZ = (mrzText, autocorrect = true) => {
|
|
|
1134
1134
|
documentCode: result.fields.documentCode || null,
|
|
1135
1135
|
issuingState: result.fields.issuingState || null,
|
|
1136
1136
|
documentNumber: result.fields.documentNumber || null,
|
|
1137
|
-
documentNumberCheckDigit: result.fields.documentNumberCheckDigit || null,
|
|
1138
1137
|
nationality: result.fields.nationality || null,
|
|
1139
1138
|
lastName: result.fields.lastName || null,
|
|
1140
1139
|
firstName: result.fields.firstName || null,
|
|
1141
1140
|
sex: result.fields.sex || null,
|
|
1142
1141
|
birthDate: result.fields.birthDate || null,
|
|
1143
|
-
birthDateCheckDigit: result.fields.birthDateCheckDigit || null,
|
|
1144
1142
|
expirationDate: result.fields.expirationDate || null,
|
|
1145
|
-
expirationDateCheckDigit: result.fields.expirationDateCheckDigit || null,
|
|
1146
1143
|
personalNumber: result.fields.personalNumber || null,
|
|
1147
|
-
personalNumberCheckDigit: result.fields.personalNumberCheckDigit || null,
|
|
1148
|
-
compositeCheckDigit: result.fields.compositeCheckDigit || null,
|
|
1149
1144
|
optional1: result.fields.optional1 || null,
|
|
1150
1145
|
optional2: result.fields.optional2 || null
|
|
1151
1146
|
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// ICAO 9303 digraphs safe to apply universally — these appear across many
|
|
4
|
+
// national variants and are unlikely to collide with real letter sequences.
|
|
5
|
+
// SS→ß, TH→Þ etc. are excluded: too ambiguous (HASSAN, THOMAS).
|
|
6
|
+
const UNIVERSAL = [['OE', 'Ö'], ['UE', 'Ü']];
|
|
7
|
+
const GERMAN = [['AE', 'Ä'], ['OE', 'Ö'], ['UE', 'Ü'], ['SS', 'ß']];
|
|
8
|
+
const SWEDISH = [['AA', 'Å'], ['AE', 'Æ'], ['OE', 'Ø']];
|
|
9
|
+
const DANISH = [['AA', 'Å'], ['AE', 'Æ'], ['OE', 'Ø']];
|
|
10
|
+
const NORWEGIAN = [['AA', 'Å'], ['AE', 'Æ'], ['OE', 'Ø']];
|
|
11
|
+
const FRENCH = [['OE', 'Œ'], ['AE', 'Æ']];
|
|
12
|
+
const ICELANDIC = [['AE', 'Æ'], ['OE', 'Ö'], ['TH', 'Þ'], ['D', 'Ð']];
|
|
13
|
+
const COUNTRY_MAP = {
|
|
14
|
+
DEU: GERMAN,
|
|
15
|
+
AUT: GERMAN,
|
|
16
|
+
CHE: GERMAN,
|
|
17
|
+
SWE: SWEDISH,
|
|
18
|
+
DNK: DANISH,
|
|
19
|
+
NOR: NORWEGIAN,
|
|
20
|
+
FRA: FRENCH,
|
|
21
|
+
LUX: FRENCH,
|
|
22
|
+
ISL: ICELANDIC
|
|
23
|
+
};
|
|
24
|
+
function applyDigraphMap(token, map) {
|
|
25
|
+
let result = '';
|
|
26
|
+
let i = 0;
|
|
27
|
+
while (i < token.length) {
|
|
28
|
+
let matched = false;
|
|
29
|
+
for (const [digraph, unicode] of map) {
|
|
30
|
+
if (token.startsWith(digraph, i)) {
|
|
31
|
+
result += unicode;
|
|
32
|
+
i += digraph.length;
|
|
33
|
+
matched = true;
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (!matched) {
|
|
38
|
+
result += token[i];
|
|
39
|
+
i++;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
function convertName(rawName, map) {
|
|
45
|
+
return rawName.split(' ').map(word => applyDigraphMap(word, map)).join(' ');
|
|
46
|
+
}
|
|
47
|
+
export function buildDocumentName(rawFirst, rawLast, issuingState, dg11First, dg11Last) {
|
|
48
|
+
if (dg11First != null && dg11Last != null && (dg11First || dg11Last)) {
|
|
49
|
+
return {
|
|
50
|
+
rawFirst,
|
|
51
|
+
rawLast,
|
|
52
|
+
displayFirst: dg11First,
|
|
53
|
+
displayLast: dg11Last,
|
|
54
|
+
source: 'dg11'
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
const country = (issuingState ?? '').toUpperCase().trim();
|
|
58
|
+
const map = COUNTRY_MAP[country];
|
|
59
|
+
if (map) {
|
|
60
|
+
return {
|
|
61
|
+
rawFirst,
|
|
62
|
+
rawLast,
|
|
63
|
+
displayFirst: convertName(rawFirst, map),
|
|
64
|
+
displayLast: convertName(rawLast, map),
|
|
65
|
+
source: 'reverse_table'
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// No country-specific table: apply OE→Ö and UE→Ü universally.
|
|
70
|
+
const displayFirst = convertName(rawFirst, UNIVERSAL);
|
|
71
|
+
const displayLast = convertName(rawLast, UNIVERSAL);
|
|
72
|
+
const changed = displayFirst !== rawFirst || displayLast !== rawLast;
|
|
73
|
+
return {
|
|
74
|
+
rawFirst,
|
|
75
|
+
rawLast,
|
|
76
|
+
displayFirst,
|
|
77
|
+
displayLast,
|
|
78
|
+
source: changed ? 'reverse_table' : 'raw'
|
|
79
|
+
};
|
|
80
|
+
}
|
|
@@ -27,7 +27,7 @@ export class DataUploadService {
|
|
|
27
27
|
/**
|
|
28
28
|
* Submit document data to the backend (same approach as ResultScreen)
|
|
29
29
|
*/
|
|
30
|
-
async submitDocumentData(identificationId, scannedDocument, sessionKey) {
|
|
30
|
+
async submitDocumentData(identificationId, scannedDocument, sessionKey, documentName) {
|
|
31
31
|
if (!scannedDocument || scannedDocument.documentType === 'UNKNOWN') {
|
|
32
32
|
debugLog('DataUploadService', 'No document data to submit');
|
|
33
33
|
return;
|
|
@@ -41,6 +41,9 @@ export class DataUploadService {
|
|
|
41
41
|
type: mrzFields.documentCode,
|
|
42
42
|
name: mrzFields.firstName,
|
|
43
43
|
surname: mrzFields.lastName,
|
|
44
|
+
displayName: documentName?.source === 'dg11' ? documentName.displayFirst : undefined,
|
|
45
|
+
displaySurname: documentName?.source === 'dg11' ? documentName.displayLast : undefined,
|
|
46
|
+
nameSource: documentName?.source === 'dg11' ? documentName.source : undefined,
|
|
44
47
|
gender: this.getGenderEnumType(mrzFields.sex),
|
|
45
48
|
number: mrzFields.documentNumber,
|
|
46
49
|
country: mrzFields.issuingState,
|
|
@@ -193,7 +196,7 @@ export class DataUploadService {
|
|
|
193
196
|
/**
|
|
194
197
|
* Upload all collected data (document + media) before video call
|
|
195
198
|
*/
|
|
196
|
-
async uploadCollectedData(identificationInfo, onProgress) {
|
|
199
|
+
async uploadCollectedData(identificationInfo, onProgress, documentName) {
|
|
197
200
|
const {
|
|
198
201
|
identificationId,
|
|
199
202
|
sessionId,
|
|
@@ -239,7 +242,7 @@ export class DataUploadService {
|
|
|
239
242
|
// Step 1: Submit document data (using same encryption as ResultScreen)
|
|
240
243
|
if (scannedDocument) {
|
|
241
244
|
onProgress?.(0.1);
|
|
242
|
-
await runWithRetry(() => this.submitDocumentData(identificationId, scannedDocument, authToken));
|
|
245
|
+
await runWithRetry(() => this.submitDocumentData(identificationId, scannedDocument, authToken, documentName));
|
|
243
246
|
onProgress?.(0.3);
|
|
244
247
|
}
|
|
245
248
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";
|