@trustchex/react-native-sdk 1.481.1 → 1.484.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.
- package/README.md +14 -5
- 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/EIDReader/lds/icao/mrzInfo.js +4 -4
- 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/EIDReader/lds/icao/mrzInfo.ts +4 -4
- 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
|
@@ -259,14 +259,16 @@ const ResultScreen = () => {
|
|
|
259
259
|
throw new Error('MRZ fields not found');
|
|
260
260
|
}
|
|
261
261
|
|
|
262
|
-
// Backend payload must contain raw MRZ data only.
|
|
263
|
-
// Human-friendly formatting is strictly for UI rendering paths.
|
|
264
262
|
const rawMrzFields = scannedDocument.mrzFields;
|
|
263
|
+
const docName = appContext.lastDocumentRead?.name;
|
|
265
264
|
|
|
266
265
|
const identificationDocument = {
|
|
267
266
|
type: rawMrzFields.documentCode,
|
|
268
267
|
name: rawMrzFields.firstName,
|
|
269
268
|
surname: rawMrzFields.lastName,
|
|
269
|
+
displayName: docName?.source === 'dg11' ? docName.displayFirst : undefined,
|
|
270
|
+
displaySurname: docName?.source === 'dg11' ? docName.displayLast : undefined,
|
|
271
|
+
nameSource: docName?.source === 'dg11' ? docName.source : undefined,
|
|
270
272
|
gender: getGenderEnumType(rawMrzFields.sex),
|
|
271
273
|
number: rawMrzFields.documentNumber,
|
|
272
274
|
country: rawMrzFields.issuingState,
|
|
@@ -292,7 +294,7 @@ const ResultScreen = () => {
|
|
|
292
294
|
}
|
|
293
295
|
);
|
|
294
296
|
},
|
|
295
|
-
[apiUrl, getGenderEnumType]
|
|
297
|
+
[apiUrl, appContext.lastDocumentRead, getGenderEnumType]
|
|
296
298
|
);
|
|
297
299
|
|
|
298
300
|
const uploadIdentificationMedia = useCallback(
|
|
@@ -790,23 +792,35 @@ const ResultScreen = () => {
|
|
|
790
792
|
<Text style={styles.mrzInfoLabel}>
|
|
791
793
|
{t('eidScannerScreen.name')}:
|
|
792
794
|
</Text>
|
|
793
|
-
<
|
|
794
|
-
{
|
|
795
|
-
appContext.
|
|
796
|
-
.
|
|
797
|
-
|
|
798
|
-
|
|
795
|
+
<View style={styles.mrzInfoValueGroup}>
|
|
796
|
+
<Text style={styles.mrzInfoText}>
|
|
797
|
+
{appContext.lastDocumentRead?.name.source === 'dg11'
|
|
798
|
+
? appContext.lastDocumentRead.name.displayFirst
|
|
799
|
+
: appContext.identificationInfo.scannedDocument.mrzFields?.firstName}
|
|
800
|
+
</Text>
|
|
801
|
+
{appContext.lastDocumentRead?.name.source === 'dg11' && (
|
|
802
|
+
<Text style={styles.mrzInfoSubText}>
|
|
803
|
+
{appContext.identificationInfo.scannedDocument.mrzFields?.firstName}
|
|
804
|
+
</Text>
|
|
805
|
+
)}
|
|
806
|
+
</View>
|
|
799
807
|
</View>
|
|
800
808
|
<View style={styles.mrzInfoItem}>
|
|
801
809
|
<Text style={styles.mrzInfoLabel}>
|
|
802
810
|
{t('eidScannerScreen.surname')}:
|
|
803
811
|
</Text>
|
|
804
|
-
<
|
|
805
|
-
{
|
|
806
|
-
appContext.
|
|
807
|
-
.
|
|
808
|
-
|
|
809
|
-
|
|
812
|
+
<View style={styles.mrzInfoValueGroup}>
|
|
813
|
+
<Text style={styles.mrzInfoText}>
|
|
814
|
+
{appContext.lastDocumentRead?.name.source === 'dg11'
|
|
815
|
+
? appContext.lastDocumentRead.name.displayLast
|
|
816
|
+
: appContext.identificationInfo.scannedDocument.mrzFields?.lastName}
|
|
817
|
+
</Text>
|
|
818
|
+
{appContext.lastDocumentRead?.name.source === 'dg11' && (
|
|
819
|
+
<Text style={styles.mrzInfoSubText}>
|
|
820
|
+
{appContext.identificationInfo.scannedDocument.mrzFields?.lastName}
|
|
821
|
+
</Text>
|
|
822
|
+
)}
|
|
823
|
+
</View>
|
|
810
824
|
</View>
|
|
811
825
|
<View style={styles.mrzInfoItem}>
|
|
812
826
|
<Text style={styles.mrzInfoLabel}>
|
|
@@ -1227,6 +1241,16 @@ const styles = StyleSheet.create({
|
|
|
1227
1241
|
fontSize: 10,
|
|
1228
1242
|
fontFamily: Platform.OS === 'ios' ? 'Courier' : 'monospace',
|
|
1229
1243
|
},
|
|
1244
|
+
mrzInfoValueGroup: {
|
|
1245
|
+
flexDirection: 'column',
|
|
1246
|
+
alignItems: 'flex-end',
|
|
1247
|
+
},
|
|
1248
|
+
mrzInfoSubText: {
|
|
1249
|
+
color: '#aaa',
|
|
1250
|
+
fontSize: 9,
|
|
1251
|
+
fontFamily: Platform.OS === 'ios' ? 'Courier' : 'monospace',
|
|
1252
|
+
marginTop: 1,
|
|
1253
|
+
},
|
|
1230
1254
|
mrzTextContainer: {
|
|
1231
1255
|
padding: 12,
|
|
1232
1256
|
backgroundColor: '#F3F4F6',
|
|
@@ -9,6 +9,8 @@ import { MRZInfo } from '../EIDReader/lds/icao/mrzInfo';
|
|
|
9
9
|
import { eidReader } from '../EIDReader/eidReader';
|
|
10
10
|
import NativeProgressBar from './NativeProgressBar';
|
|
11
11
|
import type { MRZFields } from '../Types/mrzFields';
|
|
12
|
+
import { buildDocumentName } from '../Libs/mrzTransliteration';
|
|
13
|
+
import { normalizeFromMRZInfo } from '../Libs/documentDataNormalizer';
|
|
12
14
|
import { useTranslation } from 'react-i18next';
|
|
13
15
|
import AppContext from '../Contexts/AppContext';
|
|
14
16
|
import StyledButton from './StyledButton';
|
|
@@ -52,6 +54,7 @@ const EIDScanner = ({
|
|
|
52
54
|
const [documentMRZInfo, setDocumentMRZInfo] = React.useState<MRZInfo>();
|
|
53
55
|
const [documentFaceImageMimeType, setDocumentFaceImageMimeType] =
|
|
54
56
|
React.useState<string>();
|
|
57
|
+
const [documentName, setDocumentName] = React.useState<import('../Types/documentReadResult').DocumentName>();
|
|
55
58
|
const [progress, setProgress] = React.useState(0);
|
|
56
59
|
const [progressStage, setProgressStage] = React.useState<string>('');
|
|
57
60
|
|
|
@@ -133,6 +136,21 @@ const EIDScanner = ({
|
|
|
133
136
|
const [voiceGuidanceMessage, setVoiceGuidanceMessage] = useState<string>();
|
|
134
137
|
const [attemptNumber, setAttemptNumber] = useState<number>(0);
|
|
135
138
|
|
|
139
|
+
const getFieldsFromMRZ = useCallback((mrz: MRZInfo) => {
|
|
140
|
+
return {
|
|
141
|
+
documentCode: mrz.getDocumentCode(),
|
|
142
|
+
personalNumber: mrz.getPersonalNumber() || mrz.getOptionalData1(),
|
|
143
|
+
documentNumber: mrz.getDocumentNumber(),
|
|
144
|
+
firstName: mrz.getSecondaryIdentifier(),
|
|
145
|
+
lastName: mrz.getPrimaryIdentifier(),
|
|
146
|
+
birthDate: mrz.getDateOfBirth(),
|
|
147
|
+
expirationDate: mrz.getDateOfExpiry(),
|
|
148
|
+
issuingState: mrz.getIssuingState(),
|
|
149
|
+
sex: mrz.getGender().getStrCode(),
|
|
150
|
+
nationality: mrz.getNationality(),
|
|
151
|
+
} satisfies MRZFields;
|
|
152
|
+
}, []);
|
|
153
|
+
|
|
136
154
|
const readNFC = useCallback(async () => {
|
|
137
155
|
const startTime = Date.now();
|
|
138
156
|
const currentAttempt = attemptNumber + 1;
|
|
@@ -228,6 +246,32 @@ const EIDScanner = ({
|
|
|
228
246
|
setDocumentMRZInfo(passportData.mrz);
|
|
229
247
|
setDocumentFaceImageMimeType(passportData.mimeType);
|
|
230
248
|
setIsScanned(true);
|
|
249
|
+
|
|
250
|
+
{
|
|
251
|
+
const documentData = normalizeFromMRZInfo(passportData.mrz);
|
|
252
|
+
const builtName = buildDocumentName(
|
|
253
|
+
documentData.firstName,
|
|
254
|
+
documentData.lastName,
|
|
255
|
+
documentData.issuingCountry,
|
|
256
|
+
passportData.dg11FirstName,
|
|
257
|
+
passportData.dg11LastName
|
|
258
|
+
);
|
|
259
|
+
setDocumentName(builtName);
|
|
260
|
+
const documentReadResult = {
|
|
261
|
+
document: {
|
|
262
|
+
...documentData,
|
|
263
|
+
firstName: builtName.displayFirst,
|
|
264
|
+
lastName: builtName.displayLast,
|
|
265
|
+
},
|
|
266
|
+
name: builtName,
|
|
267
|
+
face: {
|
|
268
|
+
data: passportData.faceImage,
|
|
269
|
+
mimeType: passportData.mimeType,
|
|
270
|
+
},
|
|
271
|
+
};
|
|
272
|
+
appContext.setLastDocumentRead?.(documentReadResult);
|
|
273
|
+
appContext.onDocumentRead?.(documentReadResult);
|
|
274
|
+
}
|
|
231
275
|
await trackEIDScanComplete(
|
|
232
276
|
docType,
|
|
233
277
|
scanDuration,
|
|
@@ -314,23 +358,10 @@ const EIDScanner = ({
|
|
|
314
358
|
isEnabled,
|
|
315
359
|
attemptNumber,
|
|
316
360
|
t,
|
|
361
|
+
getFieldsFromMRZ,
|
|
362
|
+
appContext,
|
|
317
363
|
]);
|
|
318
364
|
|
|
319
|
-
const getFieldsFromMRZ = useCallback((mrz: MRZInfo) => {
|
|
320
|
-
return {
|
|
321
|
-
documentCode: mrz.getDocumentCode(),
|
|
322
|
-
personalNumber: mrz.getPersonalNumber() || mrz.getOptionalData1(),
|
|
323
|
-
documentNumber: mrz.getDocumentNumber(),
|
|
324
|
-
firstName: mrz.getSecondaryIdentifier(),
|
|
325
|
-
lastName: mrz.getPrimaryIdentifier(),
|
|
326
|
-
birthDate: mrz.getDateOfBirth(),
|
|
327
|
-
expirationDate: mrz.getDateOfExpiry(),
|
|
328
|
-
issuingState: mrz.getIssuingState(),
|
|
329
|
-
sex: mrz.getGender().getStrCode(),
|
|
330
|
-
nationality: mrz.getNationality(),
|
|
331
|
-
} satisfies MRZFields;
|
|
332
|
-
}, []);
|
|
333
|
-
|
|
334
365
|
useEffect(() => {
|
|
335
366
|
const checkIsSupported = async () => {
|
|
336
367
|
const deviceIsSupported = await NFCManager.isSupported();
|
|
@@ -533,11 +564,13 @@ const EIDScanner = ({
|
|
|
533
564
|
<View style={styles.idCardFields}>
|
|
534
565
|
<IDCardField
|
|
535
566
|
label={t('eidScannerScreen.surname')}
|
|
536
|
-
value={documentMRZInfo.getPrimaryIdentifier()}
|
|
567
|
+
value={documentName?.displayLast ?? documentMRZInfo.getPrimaryIdentifier()}
|
|
568
|
+
subValue={documentName?.source === 'dg11' ? documentMRZInfo.getPrimaryIdentifier() : undefined}
|
|
537
569
|
/>
|
|
538
570
|
<IDCardField
|
|
539
571
|
label={t('eidScannerScreen.name')}
|
|
540
|
-
value={documentMRZInfo.getSecondaryIdentifier()}
|
|
572
|
+
value={documentName?.displayFirst ?? documentMRZInfo.getSecondaryIdentifier()}
|
|
573
|
+
subValue={documentName?.source === 'dg11' ? documentMRZInfo.getSecondaryIdentifier() : undefined}
|
|
541
574
|
/>
|
|
542
575
|
<IDCardField
|
|
543
576
|
label={t('eidScannerScreen.documentNumber')}
|
|
@@ -866,6 +899,11 @@ const styles = StyleSheet.create({
|
|
|
866
899
|
flexShrink: 1,
|
|
867
900
|
flexWrap: 'wrap',
|
|
868
901
|
},
|
|
902
|
+
idCardFieldSubValue: {
|
|
903
|
+
fontSize: 9,
|
|
904
|
+
color: '#aaa',
|
|
905
|
+
marginTop: 1,
|
|
906
|
+
},
|
|
869
907
|
guide: {
|
|
870
908
|
flex: 1,
|
|
871
909
|
display: 'flex',
|
|
@@ -896,12 +934,23 @@ const styles = StyleSheet.create({
|
|
|
896
934
|
},
|
|
897
935
|
});
|
|
898
936
|
|
|
899
|
-
const IDCardField = ({
|
|
937
|
+
const IDCardField = ({
|
|
938
|
+
label,
|
|
939
|
+
value,
|
|
940
|
+
subValue,
|
|
941
|
+
}: {
|
|
942
|
+
label: string;
|
|
943
|
+
value: string;
|
|
944
|
+
subValue?: string;
|
|
945
|
+
}) => (
|
|
900
946
|
<View style={styles.idCardFieldItem}>
|
|
901
947
|
<Text style={styles.idCardFieldLabel} numberOfLines={1}>
|
|
902
948
|
{label}
|
|
903
949
|
</Text>
|
|
904
950
|
<Text style={styles.idCardFieldValue}>{value || '—'}</Text>
|
|
951
|
+
{subValue ? (
|
|
952
|
+
<Text style={styles.idCardFieldSubValue}>{subValue}</Text>
|
|
953
|
+
) : null}
|
|
905
954
|
</View>
|
|
906
955
|
);
|
|
907
956
|
|
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
IdentificationInfo,
|
|
4
4
|
WorkflowStep,
|
|
5
5
|
} from '../Types/identificationInfo';
|
|
6
|
+
import type { DocumentReadResult } from '../Types/documentReadResult';
|
|
6
7
|
|
|
7
8
|
export type AppContextType = {
|
|
8
9
|
isDemoSession: boolean;
|
|
@@ -19,8 +20,12 @@ export type AppContextType = {
|
|
|
19
20
|
currentWorkflowStep?: WorkflowStep;
|
|
20
21
|
isDebugNavigated?: boolean;
|
|
21
22
|
isTestVideoSession?: boolean;
|
|
23
|
+
/** Last document read result — available to upload services for real-name fields. */
|
|
24
|
+
lastDocumentRead?: DocumentReadResult;
|
|
22
25
|
onCompleted?: () => void;
|
|
23
26
|
onError?: (error: string) => void;
|
|
27
|
+
onDocumentRead?: (result: DocumentReadResult) => void;
|
|
28
|
+
setLastDocumentRead?: (result: DocumentReadResult) => void;
|
|
24
29
|
setSessionId?: (id: string) => void;
|
|
25
30
|
setBaseUrl?: (url: string) => void;
|
|
26
31
|
setIsDemoSession?: (isDemoSession: boolean) => void;
|
|
@@ -49,8 +54,11 @@ export default createContext<AppContextType>({
|
|
|
49
54
|
currentWorkflowStep: undefined,
|
|
50
55
|
isDebugNavigated: false,
|
|
51
56
|
isTestVideoSession: false,
|
|
57
|
+
lastDocumentRead: undefined,
|
|
52
58
|
onCompleted: undefined,
|
|
53
59
|
onError: undefined,
|
|
60
|
+
onDocumentRead: undefined,
|
|
61
|
+
setLastDocumentRead: undefined,
|
|
54
62
|
setSessionId: undefined,
|
|
55
63
|
setBaseUrl: undefined,
|
|
56
64
|
});
|
|
@@ -4,6 +4,7 @@ import { NFCManagerCardService } from './nfcManagerCardService';
|
|
|
4
4
|
import { EIDService } from './eidService';
|
|
5
5
|
import { DG1File } from './lds/icao/dg1File';
|
|
6
6
|
import { DG2File } from './lds/icao/dg2File';
|
|
7
|
+
import { DG11File } from './lds/icao/dg11File';
|
|
7
8
|
import { FaceImageInfo } from './lds/iso19794/faceImageInfo';
|
|
8
9
|
import { Buffer } from 'buffer';
|
|
9
10
|
import { MRZInfo } from './lds/icao/mrzInfo';
|
|
@@ -322,7 +323,14 @@ const eidReader = async (
|
|
|
322
323
|
dateOfExpiry: string,
|
|
323
324
|
progressCallback?: (progress: number) => void
|
|
324
325
|
): Promise<
|
|
325
|
-
|
|
326
|
+
| {
|
|
327
|
+
mrz?: MRZInfo;
|
|
328
|
+
faceImage: string;
|
|
329
|
+
mimeType: string;
|
|
330
|
+
dg11FirstName: string | null;
|
|
331
|
+
dg11LastName: string | null;
|
|
332
|
+
}
|
|
333
|
+
| undefined
|
|
326
334
|
> => {
|
|
327
335
|
let progress = 0;
|
|
328
336
|
const nfcManagerCardService = new NFCManagerCardService();
|
|
@@ -566,6 +574,22 @@ const eidReader = async (
|
|
|
566
574
|
progressCallback(progress);
|
|
567
575
|
}
|
|
568
576
|
|
|
577
|
+
// Read DG11 (Additional Personal Data — printed name in UTF-8, optional)
|
|
578
|
+
let dg11FirstName: string | null = null;
|
|
579
|
+
let dg11LastName: string | null = null;
|
|
580
|
+
try {
|
|
581
|
+
const dg11InputStream = passportService.getInputStream(
|
|
582
|
+
EIDService.EF_DG11
|
|
583
|
+
);
|
|
584
|
+
await dg11InputStream.init();
|
|
585
|
+
const dg11File = new DG11File(dg11InputStream);
|
|
586
|
+
await dg11File.read();
|
|
587
|
+
dg11FirstName = dg11File.getFirstName();
|
|
588
|
+
dg11LastName = dg11File.getLastName();
|
|
589
|
+
} catch {
|
|
590
|
+
// DG11 is optional — not all chips have it, and access may be denied
|
|
591
|
+
}
|
|
592
|
+
|
|
569
593
|
// Read DG2 (Face Image)
|
|
570
594
|
const dg2Done = stepTimer();
|
|
571
595
|
const dg2InputStream = passportService.getInputStream(EIDService.EF_DG2);
|
|
@@ -615,6 +639,8 @@ const eidReader = async (
|
|
|
615
639
|
mrz: mrzInfo,
|
|
616
640
|
faceImage: imageAsBase64,
|
|
617
641
|
mimeType: mimeType,
|
|
642
|
+
dg11FirstName,
|
|
643
|
+
dg11LastName,
|
|
618
644
|
};
|
|
619
645
|
} catch (error) {
|
|
620
646
|
const msg = error instanceof Error ? error.message : String(error);
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { Buffer } from 'buffer';
|
|
2
|
+
import { InputStream } from '../../java/inputStream';
|
|
3
|
+
import { TLVInputStream } from '../../tlv/tlvInputStream';
|
|
4
|
+
import { DataGroup } from '../dataGroup';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* DG11 — Additional Personal Data (ICAO 9303-10 §4.6.6).
|
|
8
|
+
*
|
|
9
|
+
* We only extract tag 0x5F0E (Name of Holder), which contains the full name
|
|
10
|
+
* exactly as printed on the document, in UTF-8, using the same `<<` primary/
|
|
11
|
+
* secondary delimiter as the MRZ (but with native Unicode characters).
|
|
12
|
+
*
|
|
13
|
+
* Example: "ÖZTÜRK<<GÖKHAN MEHMET" → lastName="ÖZTÜRK", firstName="GÖKHAN MEHMET"
|
|
14
|
+
*
|
|
15
|
+
* The file is optional on the chip — if absent or unreadable we return null
|
|
16
|
+
* so the caller can fall back to reverse transliteration.
|
|
17
|
+
*/
|
|
18
|
+
export class DG11File extends DataGroup {
|
|
19
|
+
private static readonly DG11_TAG = 0x6b;
|
|
20
|
+
private static readonly TAG_LIST_TAG = 0x5c;
|
|
21
|
+
private static readonly NAME_OF_HOLDER_TAG = 0x5f0e;
|
|
22
|
+
|
|
23
|
+
private firstName: string | null = null;
|
|
24
|
+
private lastName: string | null = null;
|
|
25
|
+
|
|
26
|
+
constructor(is?: InputStream) {
|
|
27
|
+
super(DG11File.DG11_TAG, is);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Returns the first name from DG11, or null if not present. */
|
|
31
|
+
getFirstName(): string | null {
|
|
32
|
+
return this.firstName;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Returns the last name from DG11, or null if not present. */
|
|
36
|
+
getLastName(): string | null {
|
|
37
|
+
return this.lastName;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async read(): Promise<void> {
|
|
41
|
+
await super.readObject();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
protected async readContent(inputStream: InputStream): Promise<void> {
|
|
45
|
+
const tlvIn =
|
|
46
|
+
inputStream instanceof TLVInputStream
|
|
47
|
+
? inputStream
|
|
48
|
+
: new TLVInputStream(inputStream);
|
|
49
|
+
|
|
50
|
+
// Skip the tag list (0x5C) — we only care about 0x5F0E
|
|
51
|
+
// The tag list tells us which optional tags are present; we probe directly.
|
|
52
|
+
try {
|
|
53
|
+
await tlvIn.skipToTag(DG11File.NAME_OF_HOLDER_TAG);
|
|
54
|
+
} catch {
|
|
55
|
+
// Tag not present in this chip's DG11
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const length = await tlvIn.readLength();
|
|
60
|
+
if (length <= 0) return;
|
|
61
|
+
|
|
62
|
+
const bytes = new Uint8Array(length);
|
|
63
|
+
await inputStream.readBytesWithOffset(bytes, 0, length);
|
|
64
|
+
|
|
65
|
+
const nameString = Buffer.from(bytes).toString('utf8').trim();
|
|
66
|
+
this.parseName(nameString);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
protected writeContent(): void {
|
|
70
|
+
// Write not needed for reading
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Split "PRIMARY<<SECONDARY" into last/first name components.
|
|
75
|
+
* Strips trailing filler `<` within each segment.
|
|
76
|
+
*/
|
|
77
|
+
private parseName(nameString: string): void {
|
|
78
|
+
const delimIndex = nameString.indexOf('<<');
|
|
79
|
+
if (delimIndex < 0) {
|
|
80
|
+
this.lastName = nameString.replace(/<+$/, '').trim();
|
|
81
|
+
this.firstName = '';
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
this.lastName = nameString
|
|
85
|
+
.substring(0, delimIndex)
|
|
86
|
+
.replace(/<+$/, '')
|
|
87
|
+
.trim();
|
|
88
|
+
this.firstName = nameString
|
|
89
|
+
.substring(delimIndex + 2)
|
|
90
|
+
.replace(/<$/, ' ')
|
|
91
|
+
.replace(/<+$/, '')
|
|
92
|
+
.trim();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -304,7 +304,7 @@ export class MRZInfo extends AbstractLDSInfo {
|
|
|
304
304
|
public setPrimaryIdentifier(primaryIdentifier: string): void {
|
|
305
305
|
this.primaryIdentifier = MRZInfo.trimTrailingFillerChars(
|
|
306
306
|
primaryIdentifier
|
|
307
|
-
).replace(
|
|
307
|
+
).replace(/</g, ' ');
|
|
308
308
|
this.checkDigit();
|
|
309
309
|
}
|
|
310
310
|
|
|
@@ -916,13 +916,13 @@ export class MRZInfo extends AbstractLDSInfo {
|
|
|
916
916
|
/* Only a primary identifier. */
|
|
917
917
|
this.primaryIdentifier = MRZInfo.trimTrailingFillerChars(
|
|
918
918
|
mrzNameString
|
|
919
|
-
).replace(
|
|
919
|
+
).replace(/</g, ' ');
|
|
920
920
|
this.secondaryIdentifier = '';
|
|
921
921
|
return;
|
|
922
922
|
}
|
|
923
923
|
this.primaryIdentifier = MRZInfo.trimTrailingFillerChars(
|
|
924
924
|
mrzNameString.substring(0, delimIndex)
|
|
925
|
-
).replace(
|
|
925
|
+
).replace(/</g, ' ');
|
|
926
926
|
const rest = mrzNameString.substring(delimIndex + 2);
|
|
927
927
|
this.readSecondaryIdentifiers(rest);
|
|
928
928
|
}
|
|
@@ -935,7 +935,7 @@ export class MRZInfo extends AbstractLDSInfo {
|
|
|
935
935
|
private readSecondaryIdentifiers(secondaryIdentifier: string): void {
|
|
936
936
|
this.secondaryIdentifier = MRZInfo.trimTrailingFillerChars(
|
|
937
937
|
secondaryIdentifier
|
|
938
|
-
).replace(
|
|
938
|
+
).replace(/</g, ' ');
|
|
939
939
|
}
|
|
940
940
|
|
|
941
941
|
/**
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { MRZFields } from '../Types/mrzFields';
|
|
2
|
+
import type { DocumentData } from '../Types/documentData';
|
|
3
|
+
import type { MRZInfo } from '../EIDReader/lds/icao/mrzInfo';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Expand a 2-digit MRZ year to a 4-digit calendar year.
|
|
7
|
+
* ICAO 9303: YY >= 30 → 19YY, YY < 30 → 20YY.
|
|
8
|
+
* (Threshold 30 keeps all current valid documents in range.)
|
|
9
|
+
*/
|
|
10
|
+
function expandYear(yy: string): string {
|
|
11
|
+
const n = parseInt(yy, 10);
|
|
12
|
+
return n >= 30 ? `19${yy}` : `20${yy}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Convert a YYMMDD MRZ date string to ISO 8601 YYYY-MM-DD.
|
|
17
|
+
* Returns null for empty, null, or malformed input.
|
|
18
|
+
*/
|
|
19
|
+
function mrzDateToISO(raw: string | null | undefined): string | null {
|
|
20
|
+
if (!raw || raw.length !== 6) return null;
|
|
21
|
+
const yy = raw.slice(0, 2);
|
|
22
|
+
const mm = raw.slice(2, 4);
|
|
23
|
+
const dd = raw.slice(4, 6);
|
|
24
|
+
if (!/^\d{6}$/.test(raw)) return null;
|
|
25
|
+
return `${expandYear(yy)}-${mm}-${dd}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Normalize the MRZ document-code field to a single-letter form.
|
|
30
|
+
* The `mrz` npm package returns "ID" for TD1, while the chip returns "I".
|
|
31
|
+
*/
|
|
32
|
+
function normalizeDocumentType(raw: string | null | undefined): string {
|
|
33
|
+
if (!raw) return 'I';
|
|
34
|
+
const upper = raw.trim().toUpperCase();
|
|
35
|
+
if (upper === 'ID') return 'I';
|
|
36
|
+
// TD2 can be "IR", "AC", etc — take only the first character
|
|
37
|
+
return upper.charAt(0);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Normalize the sex field to a single uppercase letter.
|
|
42
|
+
* NFC chip: "M" | "F" | "U" | "X"
|
|
43
|
+
* mrz package: "male" | "female" | "" | null
|
|
44
|
+
*/
|
|
45
|
+
function normalizeSex(raw: string | null | undefined): 'M' | 'F' | 'X' | 'U' {
|
|
46
|
+
if (!raw) return 'U';
|
|
47
|
+
switch (raw.trim().toLowerCase()) {
|
|
48
|
+
case 'm':
|
|
49
|
+
case 'male':
|
|
50
|
+
return 'M';
|
|
51
|
+
case 'f':
|
|
52
|
+
case 'female':
|
|
53
|
+
return 'F';
|
|
54
|
+
case 'x':
|
|
55
|
+
case 'unspecified':
|
|
56
|
+
return 'X';
|
|
57
|
+
default:
|
|
58
|
+
return 'U';
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Trim a string and return null if empty or only filler chars.
|
|
64
|
+
*/
|
|
65
|
+
function clean(val: string | null | undefined): string | null {
|
|
66
|
+
if (!val) return null;
|
|
67
|
+
const t = val.replace(/</g, '').trim();
|
|
68
|
+
return t.length > 0 ? t : null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Build a DocumentData from a chip-read MRZInfo object (NFC path).
|
|
73
|
+
*/
|
|
74
|
+
export function normalizeFromMRZInfo(mrz: MRZInfo): DocumentData {
|
|
75
|
+
const rawPersonal =
|
|
76
|
+
mrz.getPersonalNumber() ?? clean(mrz.getOptionalData1());
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
documentType: normalizeDocumentType(mrz.getDocumentCode()),
|
|
80
|
+
issuingCountry: mrz.getIssuingState().replace(/<$/, '').trim(),
|
|
81
|
+
nationality: mrz.getNationality().replace(/<$/, '').trim(),
|
|
82
|
+
documentNumber: mrz.getDocumentNumber().trim(),
|
|
83
|
+
personalNumber: clean(rawPersonal),
|
|
84
|
+
lastName: mrz.getPrimaryIdentifier().trim(),
|
|
85
|
+
firstName: mrz.getSecondaryIdentifier().trim(),
|
|
86
|
+
sex: normalizeSex(mrz.getGender().getStrCode()),
|
|
87
|
+
dateOfBirth: mrzDateToISO(mrz.getDateOfBirth()),
|
|
88
|
+
dateOfExpiry: mrzDateToISO(mrz.getDateOfExpiry()),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Build a DocumentData from an MRZFields object (camera OCR path).
|
|
94
|
+
*/
|
|
95
|
+
export function normalizeFromMRZFields(fields: MRZFields): DocumentData {
|
|
96
|
+
return {
|
|
97
|
+
documentType: normalizeDocumentType(fields.documentCode),
|
|
98
|
+
issuingCountry: clean(fields.issuingState) ?? '',
|
|
99
|
+
nationality: clean(fields.nationality) ?? '',
|
|
100
|
+
documentNumber: clean(fields.documentNumber) ?? '',
|
|
101
|
+
personalNumber: clean(fields.personalNumber),
|
|
102
|
+
lastName: clean(fields.lastName) ?? '',
|
|
103
|
+
firstName: clean(fields.firstName) ?? '',
|
|
104
|
+
sex: normalizeSex(fields.sex),
|
|
105
|
+
dateOfBirth: mrzDateToISO(fields.birthDate),
|
|
106
|
+
dateOfExpiry: mrzDateToISO(fields.expirationDate),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
@@ -1244,18 +1244,13 @@ const validateMRZ = (
|
|
|
1244
1244
|
documentCode: result.fields.documentCode || null,
|
|
1245
1245
|
issuingState: result.fields.issuingState || null,
|
|
1246
1246
|
documentNumber: result.fields.documentNumber || null,
|
|
1247
|
-
documentNumberCheckDigit: result.fields.documentNumberCheckDigit || null,
|
|
1248
1247
|
nationality: result.fields.nationality || null,
|
|
1249
1248
|
lastName: result.fields.lastName || null,
|
|
1250
1249
|
firstName: result.fields.firstName || null,
|
|
1251
1250
|
sex: result.fields.sex || null,
|
|
1252
1251
|
birthDate: result.fields.birthDate || null,
|
|
1253
|
-
birthDateCheckDigit: result.fields.birthDateCheckDigit || null,
|
|
1254
1252
|
expirationDate: result.fields.expirationDate || null,
|
|
1255
|
-
expirationDateCheckDigit: result.fields.expirationDateCheckDigit || null,
|
|
1256
1253
|
personalNumber: result.fields.personalNumber || null,
|
|
1257
|
-
personalNumberCheckDigit: result.fields.personalNumberCheckDigit || null,
|
|
1258
|
-
compositeCheckDigit: result.fields.compositeCheckDigit || null,
|
|
1259
1254
|
optional1: result.fields.optional1 || null,
|
|
1260
1255
|
optional2: result.fields.optional2 || null,
|
|
1261
1256
|
};
|