@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.
Files changed (64) hide show
  1. package/lib/module/Screens/Dynamic/IdentityDocumentEIDScanningScreen.js +0 -3
  2. package/lib/module/Screens/Dynamic/IdentityDocumentScanningScreen.js +21 -0
  3. package/lib/module/Screens/Dynamic/VideoCallScreen.js +1 -1
  4. package/lib/module/Screens/Static/ResultScreen.js +33 -10
  5. package/lib/module/Shared/Components/EIDScanner.js +52 -19
  6. package/lib/module/Shared/Contexts/AppContext.js +3 -0
  7. package/lib/module/Shared/EIDReader/eidReader.js +18 -1
  8. package/lib/module/Shared/EIDReader/lds/icao/dg11File.js +77 -0
  9. package/lib/module/Shared/Libs/documentDataNormalizer.js +104 -0
  10. package/lib/module/Shared/Libs/mrz.utils.js +0 -5
  11. package/lib/module/Shared/Libs/mrzTransliteration.js +80 -0
  12. package/lib/module/Shared/Services/DataUploadService.js +6 -3
  13. package/lib/module/Shared/Types/documentData.js +1 -0
  14. package/lib/module/Shared/Types/documentReadResult.js +3 -0
  15. package/lib/module/Trustchex.js +12 -1
  16. package/lib/module/version.js +1 -1
  17. package/lib/typescript/src/Screens/Dynamic/IdentityDocumentEIDScanningScreen.d.ts.map +1 -1
  18. package/lib/typescript/src/Screens/Dynamic/IdentityDocumentScanningScreen.d.ts.map +1 -1
  19. package/lib/typescript/src/Screens/Dynamic/VideoCallScreen.d.ts.map +1 -1
  20. package/lib/typescript/src/Screens/Static/ResultScreen.d.ts.map +1 -1
  21. package/lib/typescript/src/Shared/Components/EIDScanner.d.ts.map +1 -1
  22. package/lib/typescript/src/Shared/Contexts/AppContext.d.ts +5 -0
  23. package/lib/typescript/src/Shared/Contexts/AppContext.d.ts.map +1 -1
  24. package/lib/typescript/src/Shared/EIDReader/eidReader.d.ts +2 -0
  25. package/lib/typescript/src/Shared/EIDReader/eidReader.d.ts.map +1 -1
  26. package/lib/typescript/src/Shared/EIDReader/lds/icao/dg11File.d.ts +35 -0
  27. package/lib/typescript/src/Shared/EIDReader/lds/icao/dg11File.d.ts.map +1 -0
  28. package/lib/typescript/src/Shared/Libs/documentDataNormalizer.d.ts +12 -0
  29. package/lib/typescript/src/Shared/Libs/documentDataNormalizer.d.ts.map +1 -0
  30. package/lib/typescript/src/Shared/Libs/mrz.utils.d.ts.map +1 -1
  31. package/lib/typescript/src/Shared/Libs/mrzTransliteration.d.ts +3 -0
  32. package/lib/typescript/src/Shared/Libs/mrzTransliteration.d.ts.map +1 -0
  33. package/lib/typescript/src/Shared/Services/DataUploadService.d.ts +3 -2
  34. package/lib/typescript/src/Shared/Services/DataUploadService.d.ts.map +1 -1
  35. package/lib/typescript/src/Shared/Types/documentData.d.ts +44 -0
  36. package/lib/typescript/src/Shared/Types/documentData.d.ts.map +1 -0
  37. package/lib/typescript/src/Shared/Types/documentReadResult.d.ts +44 -0
  38. package/lib/typescript/src/Shared/Types/documentReadResult.d.ts.map +1 -0
  39. package/lib/typescript/src/Shared/Types/mrzFields.d.ts +1 -1
  40. package/lib/typescript/src/Shared/Types/mrzFields.d.ts.map +1 -1
  41. package/lib/typescript/src/Trustchex.d.ts +9 -0
  42. package/lib/typescript/src/Trustchex.d.ts.map +1 -1
  43. package/lib/typescript/src/index.d.ts +2 -0
  44. package/lib/typescript/src/index.d.ts.map +1 -1
  45. package/lib/typescript/src/version.d.ts +1 -1
  46. package/package.json +1 -1
  47. package/src/Screens/Dynamic/IdentityDocumentEIDScanningScreen.tsx +0 -4
  48. package/src/Screens/Dynamic/IdentityDocumentScanningScreen.tsx +23 -0
  49. package/src/Screens/Dynamic/VideoCallScreen.tsx +5 -1
  50. package/src/Screens/Static/ResultScreen.tsx +39 -15
  51. package/src/Shared/Components/EIDScanner.tsx +67 -18
  52. package/src/Shared/Contexts/AppContext.ts +8 -0
  53. package/src/Shared/EIDReader/eidReader.ts +27 -1
  54. package/src/Shared/EIDReader/lds/icao/dg11File.ts +94 -0
  55. package/src/Shared/Libs/documentDataNormalizer.ts +108 -0
  56. package/src/Shared/Libs/mrz.utils.ts +0 -5
  57. package/src/Shared/Libs/mrzTransliteration.ts +131 -0
  58. package/src/Shared/Services/DataUploadService.ts +9 -3
  59. package/src/Shared/Types/documentData.ts +52 -0
  60. package/src/Shared/Types/documentReadResult.ts +47 -0
  61. package/src/Shared/Types/mrzFields.ts +0 -5
  62. package/src/Trustchex.tsx +22 -0
  63. package/src/index.tsx +7 -0
  64. 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 = ({ label, value }: { label: string; value: string }) => (
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
- { mrz?: MRZInfo; faceImage: string; mimeType: string } | undefined
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
+ }