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