@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
|
@@ -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
|
+
}
|
|
@@ -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
|
};
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import type { DocumentName } from '../Types/documentReadResult';
|
|
2
|
+
|
|
3
|
+
type DigraphMap = [string, string][];
|
|
4
|
+
|
|
5
|
+
// ICAO 9303 digraphs safe to apply universally — these appear across many
|
|
6
|
+
// national variants and are unlikely to collide with real letter sequences.
|
|
7
|
+
// SS→ß, TH→Þ etc. are excluded: too ambiguous (HASSAN, THOMAS).
|
|
8
|
+
const UNIVERSAL: DigraphMap = [
|
|
9
|
+
['OE', 'Ö'],
|
|
10
|
+
['UE', 'Ü'],
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
const GERMAN: DigraphMap = [
|
|
14
|
+
['AE', 'Ä'],
|
|
15
|
+
['OE', 'Ö'],
|
|
16
|
+
['UE', 'Ü'],
|
|
17
|
+
['SS', 'ß'],
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const SWEDISH: DigraphMap = [
|
|
21
|
+
['AA', 'Å'],
|
|
22
|
+
['AE', 'Æ'],
|
|
23
|
+
['OE', 'Ø'],
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
const DANISH: DigraphMap = [
|
|
27
|
+
['AA', 'Å'],
|
|
28
|
+
['AE', 'Æ'],
|
|
29
|
+
['OE', 'Ø'],
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
const NORWEGIAN: DigraphMap = [
|
|
33
|
+
['AA', 'Å'],
|
|
34
|
+
['AE', 'Æ'],
|
|
35
|
+
['OE', 'Ø'],
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
const FRENCH: DigraphMap = [
|
|
39
|
+
['OE', 'Œ'],
|
|
40
|
+
['AE', 'Æ'],
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
const ICELANDIC: DigraphMap = [
|
|
44
|
+
['AE', 'Æ'],
|
|
45
|
+
['OE', 'Ö'],
|
|
46
|
+
['TH', 'Þ'],
|
|
47
|
+
['D', 'Ð'],
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
const COUNTRY_MAP: Record<string, DigraphMap> = {
|
|
51
|
+
DEU: GERMAN,
|
|
52
|
+
AUT: GERMAN,
|
|
53
|
+
CHE: GERMAN,
|
|
54
|
+
SWE: SWEDISH,
|
|
55
|
+
DNK: DANISH,
|
|
56
|
+
NOR: NORWEGIAN,
|
|
57
|
+
FRA: FRENCH,
|
|
58
|
+
LUX: FRENCH,
|
|
59
|
+
ISL: ICELANDIC,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
function applyDigraphMap(token: string, map: DigraphMap): string {
|
|
63
|
+
let result = '';
|
|
64
|
+
let i = 0;
|
|
65
|
+
while (i < token.length) {
|
|
66
|
+
let matched = false;
|
|
67
|
+
for (const [digraph, unicode] of map) {
|
|
68
|
+
if (token.startsWith(digraph, i)) {
|
|
69
|
+
result += unicode;
|
|
70
|
+
i += digraph.length;
|
|
71
|
+
matched = true;
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (!matched) {
|
|
76
|
+
result += token[i];
|
|
77
|
+
i++;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function convertName(rawName: string, map: DigraphMap): string {
|
|
84
|
+
return rawName
|
|
85
|
+
.split(' ')
|
|
86
|
+
.map((word) => applyDigraphMap(word, map))
|
|
87
|
+
.join(' ');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function buildDocumentName(
|
|
91
|
+
rawFirst: string,
|
|
92
|
+
rawLast: string,
|
|
93
|
+
issuingState: string | null | undefined,
|
|
94
|
+
dg11First?: string | null,
|
|
95
|
+
dg11Last?: string | null
|
|
96
|
+
): DocumentName {
|
|
97
|
+
if (dg11First != null && dg11Last != null && (dg11First || dg11Last)) {
|
|
98
|
+
return {
|
|
99
|
+
rawFirst,
|
|
100
|
+
rawLast,
|
|
101
|
+
displayFirst: dg11First,
|
|
102
|
+
displayLast: dg11Last,
|
|
103
|
+
source: 'dg11',
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const country = (issuingState ?? '').toUpperCase().trim();
|
|
108
|
+
const map = COUNTRY_MAP[country];
|
|
109
|
+
|
|
110
|
+
if (map) {
|
|
111
|
+
return {
|
|
112
|
+
rawFirst,
|
|
113
|
+
rawLast,
|
|
114
|
+
displayFirst: convertName(rawFirst, map),
|
|
115
|
+
displayLast: convertName(rawLast, map),
|
|
116
|
+
source: 'reverse_table',
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// No country-specific table: apply OE→Ö and UE→Ü universally.
|
|
121
|
+
const displayFirst = convertName(rawFirst, UNIVERSAL);
|
|
122
|
+
const displayLast = convertName(rawLast, UNIVERSAL);
|
|
123
|
+
const changed = displayFirst !== rawFirst || displayLast !== rawLast;
|
|
124
|
+
return {
|
|
125
|
+
rawFirst,
|
|
126
|
+
rawLast,
|
|
127
|
+
displayFirst,
|
|
128
|
+
displayLast,
|
|
129
|
+
source: changed ? 'reverse_table' : 'raw',
|
|
130
|
+
};
|
|
131
|
+
}
|
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
ScannedIdentityDocument,
|
|
6
6
|
LivenessDetection,
|
|
7
7
|
} from '../Types/identificationInfo';
|
|
8
|
+
import type { DocumentName } from '../Types/documentReadResult';
|
|
8
9
|
import { getSessionKey, encryptWithAes } from '../Libs/crypto.utils';
|
|
9
10
|
import mrzUtils from '../Libs/mrz.utils';
|
|
10
11
|
import httpClient, { getSessionToken } from '../Libs/http-client';
|
|
@@ -46,7 +47,8 @@ export class DataUploadService {
|
|
|
46
47
|
async submitDocumentData(
|
|
47
48
|
identificationId: string,
|
|
48
49
|
scannedDocument: ScannedIdentityDocument,
|
|
49
|
-
sessionKey: string
|
|
50
|
+
sessionKey: string,
|
|
51
|
+
documentName?: DocumentName
|
|
50
52
|
): Promise<void> {
|
|
51
53
|
if (!scannedDocument || scannedDocument.documentType === 'UNKNOWN') {
|
|
52
54
|
debugLog('DataUploadService', 'No document data to submit');
|
|
@@ -63,6 +65,9 @@ export class DataUploadService {
|
|
|
63
65
|
type: mrzFields.documentCode,
|
|
64
66
|
name: mrzFields.firstName,
|
|
65
67
|
surname: mrzFields.lastName,
|
|
68
|
+
displayName: documentName?.source === 'dg11' ? documentName.displayFirst : undefined,
|
|
69
|
+
displaySurname: documentName?.source === 'dg11' ? documentName.displayLast : undefined,
|
|
70
|
+
nameSource: documentName?.source === 'dg11' ? documentName.source : undefined,
|
|
66
71
|
gender: this.getGenderEnumType(mrzFields.sex),
|
|
67
72
|
number: mrzFields.documentNumber,
|
|
68
73
|
country: mrzFields.issuingState,
|
|
@@ -270,7 +275,8 @@ export class DataUploadService {
|
|
|
270
275
|
*/
|
|
271
276
|
async uploadCollectedData(
|
|
272
277
|
identificationInfo: IdentificationInfo,
|
|
273
|
-
onProgress?: (progress: number) => void
|
|
278
|
+
onProgress?: (progress: number) => void,
|
|
279
|
+
documentName?: DocumentName
|
|
274
280
|
): Promise<boolean> {
|
|
275
281
|
const { identificationId, sessionId, scannedDocument, livenessDetection } =
|
|
276
282
|
identificationInfo;
|
|
@@ -329,7 +335,7 @@ export class DataUploadService {
|
|
|
329
335
|
if (scannedDocument) {
|
|
330
336
|
onProgress?.(0.1);
|
|
331
337
|
await runWithRetry(() =>
|
|
332
|
-
this.submitDocumentData(identificationId, scannedDocument, authToken)
|
|
338
|
+
this.submitDocumentData(identificationId, scannedDocument, authToken, documentName)
|
|
333
339
|
);
|
|
334
340
|
onProgress?.(0.3);
|
|
335
341
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified, source-independent document data produced after a scan — whether
|
|
3
|
+
* the data came from an NFC chip read or camera OCR. All fields are normalized
|
|
4
|
+
* to consistent formats regardless of which path produced them.
|
|
5
|
+
*/
|
|
6
|
+
export interface DocumentData {
|
|
7
|
+
/**
|
|
8
|
+
* Document type.
|
|
9
|
+
* - `"P"` — passport (TD3)
|
|
10
|
+
* - `"I"` — ID card (TD1 / TD2)
|
|
11
|
+
*/
|
|
12
|
+
documentType: 'P' | 'I' | string;
|
|
13
|
+
|
|
14
|
+
/** 3-letter ICAO issuing country code, e.g. `"TUR"`, `"DEU"`. */
|
|
15
|
+
issuingCountry: string;
|
|
16
|
+
|
|
17
|
+
/** 3-letter ICAO nationality code, e.g. `"TUR"`. */
|
|
18
|
+
nationality: string;
|
|
19
|
+
|
|
20
|
+
/** Document serial / number exactly as encoded on the document. */
|
|
21
|
+
documentNumber: string;
|
|
22
|
+
|
|
23
|
+
/** National identity number where applicable (e.g. Turkish TC kimlik no). Null if not present. */
|
|
24
|
+
personalNumber: string | null;
|
|
25
|
+
|
|
26
|
+
/** Last name — raw MRZ ASCII (OCR-B), e.g. `"OEZTUERK"`. */
|
|
27
|
+
lastName: string;
|
|
28
|
+
|
|
29
|
+
/** First name(s) — raw MRZ ASCII (OCR-B), e.g. `"GOEKHAN MEHMET"`. */
|
|
30
|
+
firstName: string;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Sex / gender.
|
|
34
|
+
* - `"M"` — male
|
|
35
|
+
* - `"F"` — female
|
|
36
|
+
* - `"X"` — unspecified / non-binary
|
|
37
|
+
* - `"U"` — unknown (not encoded on document)
|
|
38
|
+
*/
|
|
39
|
+
sex: 'M' | 'F' | 'X' | 'U';
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Date of birth in `YYYY-MM-DD` format, e.g. `"1985-01-15"`.
|
|
43
|
+
* Null if the document does not encode it.
|
|
44
|
+
*/
|
|
45
|
+
dateOfBirth: string | null;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Document expiry date in `YYYY-MM-DD` format, e.g. `"2030-12-31"`.
|
|
49
|
+
* Null if the document does not encode it.
|
|
50
|
+
*/
|
|
51
|
+
dateOfExpiry: string | null;
|
|
52
|
+
}
|