@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
package/README.md CHANGED
@@ -151,11 +151,19 @@ module.exports = {
151
151
  ### Basic
152
152
 
153
153
  ```tsx
154
- import Trustchex from '@trustchex/react-native-sdk';
154
+ import Trustchex, { type DocumentReadResult } from '@trustchex/react-native-sdk';
155
155
 
156
156
  <Trustchex
157
157
  baseUrl="https://your-server.com"
158
158
  sessionId="session-id"
159
+ onDocumentRead={(result: DocumentReadResult) => {
160
+ const { document } = result;
161
+ console.log(document.documentType); // "P" | "I"
162
+ console.log(document.lastName); // "MÜLLER"
163
+ console.log(document.firstName); // "JÜRGEN KARL"
164
+ console.log(document.dateOfBirth); // "1985-03-10" (ISO 8601)
165
+ console.log(document.dateOfExpiry); // "2030-11-05" (ISO 8601)
166
+ }}
159
167
  onCompleted={() => console.log('Completed')}
160
168
  onError={(error) => console.error(error)}
161
169
  />
@@ -210,14 +218,15 @@ const [baseUrl, sessionId] = handleDeepLink({
210
218
  ## API
211
219
 
212
220
  | Prop | Type | Description |
213
- |------|------|-------------|
221
+ | ---- | ---- | ----------- |
214
222
  | `baseUrl` | `string?` | Server URL |
215
223
  | `sessionId` | `string?` | Verification session ID |
216
224
  | `branding` | `object?` | Theme colors and logo |
217
225
  | `locale` | `'en' \| 'tr'?` | UI language |
218
226
  | `enableAnalytics` | `boolean?` | Enable analytics (default: `true`) |
219
- | `onCompleted` | `function?` | Success callback |
220
- | `onError` | `function?` | Error callback |
227
+ | `onDocumentRead` | `(result: DocumentReadResult) => void` | Called when document is read via NFC or OCR |
228
+ | `onCompleted` | `() => void` | Success callback |
229
+ | `onError` | `(error) => void` | Error callback |
221
230
 
222
231
  ## Analytics
223
232
 
@@ -229,7 +238,7 @@ The SDK includes **privacy-compliant analytics** that respects GDPR, KVKK, and P
229
238
  - ✅ **Data anonymization** - Device identifiers excluded
230
239
  - ✅ **Right to deletion** - Users can clear their data
231
240
 
232
- ### Usage
241
+ ### Analytics Configuration
233
242
 
234
243
  Analytics is enabled by default. To customize:
235
244
 
@@ -70,9 +70,6 @@ const IdentityDocumentEIDScanningScreen = () => {
70
70
  setAllowedDocumentTypes(appContext.currentWorkflowStep?.data?.allowedDocumentTypes ?? null);
71
71
  setAllowedCountries(appContext.currentWorkflowStep?.data?.allowedCountries ?? null);
72
72
  }, [appContext.currentWorkflowStep]);
73
- if (!appContext.currentWorkflowStep) {
74
- return null;
75
- }
76
73
  return /*#__PURE__*/_jsxs(SafeAreaView, {
77
74
  style: styles.safeAreaContainer,
78
75
  children: [documentNumber && dateOfBirth && dateOfExpiry ? /*#__PURE__*/_jsx(View, {
@@ -1,6 +1,8 @@
1
1
  "use strict";
2
2
 
3
3
  import React, { useContext, useEffect, useState } from 'react';
4
+ import { buildDocumentName } from "../../Shared/Libs/mrzTransliteration.js";
5
+ import { normalizeFromMRZFields } from "../../Shared/Libs/documentDataNormalizer.js";
4
6
  import { Alert, SafeAreaView, StyleSheet, View } from 'react-native';
5
7
  import { useSafeAreaInsets } from 'react-native-safe-area-context';
6
8
  import IdentityDocumentCamera from "../../Shared/Components/IdentityDocumentCamera.js";
@@ -78,6 +80,25 @@ const IdentityDocumentScanningScreen = () => {
78
80
 
79
81
  // Track successful document scan completion as funnel step
80
82
  trackFunnelStep('Document Scan Completed', 2, 5, 'contract_acceptance', true);
83
+ {
84
+ const documentData = normalizeFromMRZFields(mrzFields);
85
+ const faceImg = idFrontSideData?.faceImage || passportData?.faceImage;
86
+ const builtName = buildDocumentName(documentData.firstName, documentData.lastName, documentData.issuingCountry);
87
+ const documentReadResult = {
88
+ document: {
89
+ ...documentData,
90
+ firstName: builtName.displayFirst,
91
+ lastName: builtName.displayLast
92
+ },
93
+ name: builtName,
94
+ face: faceImg ? {
95
+ data: faceImg,
96
+ mimeType: 'image/jpeg'
97
+ } : undefined
98
+ };
99
+ appContext.setLastDocumentRead?.(documentReadResult);
100
+ appContext.onDocumentRead?.(documentReadResult);
101
+ }
81
102
  setTimeout(() => navigationManagerRef.current?.navigateToNextStep(), 1000);
82
103
  }
83
104
  }, [idFrontSideData, idBackSideData, passportData, appContext.identificationInfo, allowedCountries, allowedDocumentTypes, t, appContext]);
@@ -126,7 +126,7 @@ const VideoCallScreen = ({
126
126
  try {
127
127
  const uploadService = new DataUploadService(baseUrl);
128
128
  dataUploadServiceRef.current = uploadService;
129
- const uploaded = await uploadService.uploadCollectedData(identificationInfo);
129
+ const uploaded = await uploadService.uploadCollectedData(identificationInfo, undefined, appContext.lastDocumentRead?.name);
130
130
  if (!uploaded) {
131
131
  console.warn('[VideoCallScreen] Upload service returned false');
132
132
  if (mounted) {
@@ -178,14 +178,15 @@ const ResultScreen = () => {
178
178
  if (!scannedDocument.mrzFields) {
179
179
  throw new Error('MRZ fields not found');
180
180
  }
181
-
182
- // Backend payload must contain raw MRZ data only.
183
- // Human-friendly formatting is strictly for UI rendering paths.
184
181
  const rawMrzFields = scannedDocument.mrzFields;
182
+ const docName = appContext.lastDocumentRead?.name;
185
183
  const identificationDocument = {
186
184
  type: rawMrzFields.documentCode,
187
185
  name: rawMrzFields.firstName,
188
186
  surname: rawMrzFields.lastName,
187
+ displayName: docName?.source === 'dg11' ? docName.displayFirst : undefined,
188
+ displaySurname: docName?.source === 'dg11' ? docName.displayLast : undefined,
189
+ nameSource: docName?.source === 'dg11' ? docName.source : undefined,
189
190
  gender: getGenderEnumType(rawMrzFields.sex),
190
191
  number: rawMrzFields.documentNumber,
191
192
  country: rawMrzFields.issuingState,
@@ -204,7 +205,7 @@ const ResultScreen = () => {
204
205
  encryptedData,
205
206
  nonce
206
207
  });
207
- }, [apiUrl, getGenderEnumType]);
208
+ }, [apiUrl, appContext.lastDocumentRead, getGenderEnumType]);
208
209
  const uploadIdentificationMedia = useCallback(async (identificationId, scannedIdentityDocument, livenessDetection, skipIfAlreadyUploaded) => {
209
210
  // Skip media upload if already uploaded during video call
210
211
  if (skipIfAlreadyUploaded) {
@@ -533,18 +534,30 @@ const ResultScreen = () => {
533
534
  children: [/*#__PURE__*/_jsxs(Text, {
534
535
  style: styles.mrzInfoLabel,
535
536
  children: [t('eidScannerScreen.name'), ":"]
536
- }), /*#__PURE__*/_jsx(Text, {
537
- style: styles.mrzInfoText,
538
- children: appContext.identificationInfo.scannedDocument.mrzFields?.firstName
537
+ }), /*#__PURE__*/_jsxs(View, {
538
+ style: styles.mrzInfoValueGroup,
539
+ children: [/*#__PURE__*/_jsx(Text, {
540
+ style: styles.mrzInfoText,
541
+ children: appContext.lastDocumentRead?.name.source === 'dg11' ? appContext.lastDocumentRead.name.displayFirst : appContext.identificationInfo.scannedDocument.mrzFields?.firstName
542
+ }), appContext.lastDocumentRead?.name.source === 'dg11' && /*#__PURE__*/_jsx(Text, {
543
+ style: styles.mrzInfoSubText,
544
+ children: appContext.identificationInfo.scannedDocument.mrzFields?.firstName
545
+ })]
539
546
  })]
540
547
  }), /*#__PURE__*/_jsxs(View, {
541
548
  style: styles.mrzInfoItem,
542
549
  children: [/*#__PURE__*/_jsxs(Text, {
543
550
  style: styles.mrzInfoLabel,
544
551
  children: [t('eidScannerScreen.surname'), ":"]
545
- }), /*#__PURE__*/_jsx(Text, {
546
- style: styles.mrzInfoText,
547
- children: appContext.identificationInfo.scannedDocument.mrzFields?.lastName
552
+ }), /*#__PURE__*/_jsxs(View, {
553
+ style: styles.mrzInfoValueGroup,
554
+ children: [/*#__PURE__*/_jsx(Text, {
555
+ style: styles.mrzInfoText,
556
+ children: appContext.lastDocumentRead?.name.source === 'dg11' ? appContext.lastDocumentRead.name.displayLast : appContext.identificationInfo.scannedDocument.mrzFields?.lastName
557
+ }), appContext.lastDocumentRead?.name.source === 'dg11' && /*#__PURE__*/_jsx(Text, {
558
+ style: styles.mrzInfoSubText,
559
+ children: appContext.identificationInfo.scannedDocument.mrzFields?.lastName
560
+ })]
548
561
  })]
549
562
  }), /*#__PURE__*/_jsxs(View, {
550
563
  style: styles.mrzInfoItem,
@@ -882,6 +895,16 @@ const styles = StyleSheet.create({
882
895
  fontSize: 10,
883
896
  fontFamily: Platform.OS === 'ios' ? 'Courier' : 'monospace'
884
897
  },
898
+ mrzInfoValueGroup: {
899
+ flexDirection: 'column',
900
+ alignItems: 'flex-end'
901
+ },
902
+ mrzInfoSubText: {
903
+ color: '#aaa',
904
+ fontSize: 9,
905
+ fontFamily: Platform.OS === 'ios' ? 'Courier' : 'monospace',
906
+ marginTop: 1
907
+ },
885
908
  mrzTextContainer: {
886
909
  padding: 12,
887
910
  backgroundColor: '#F3F4F6',
@@ -9,6 +9,8 @@ import PermissionManager from "../Libs/permissions.utils.js";
9
9
  import { debugError } from "../Libs/debug.utils.js";
10
10
  import { eidReader } from "../EIDReader/eidReader.js";
11
11
  import NativeProgressBar from "./NativeProgressBar.js";
12
+ import { buildDocumentName } from "../Libs/mrzTransliteration.js";
13
+ import { normalizeFromMRZInfo } from "../Libs/documentDataNormalizer.js";
12
14
  import { useTranslation } from 'react-i18next';
13
15
  import AppContext from "../Contexts/AppContext.js";
14
16
  import StyledButton from "./StyledButton.js";
@@ -34,6 +36,7 @@ const EIDScanner = ({
34
36
  const [documentFaceImage, setDocumentFaceImage] = React.useState();
35
37
  const [documentMRZInfo, setDocumentMRZInfo] = React.useState();
36
38
  const [documentFaceImageMimeType, setDocumentFaceImageMimeType] = React.useState();
39
+ const [documentName, setDocumentName] = React.useState();
37
40
  const [progress, setProgress] = React.useState(0);
38
41
  const [progressStage, setProgressStage] = React.useState('');
39
42
 
@@ -99,6 +102,20 @@ const EIDScanner = ({
99
102
  }, [i18n.language]);
100
103
  const [voiceGuidanceMessage, setVoiceGuidanceMessage] = useState();
101
104
  const [attemptNumber, setAttemptNumber] = useState(0);
105
+ const getFieldsFromMRZ = useCallback(mrz => {
106
+ return {
107
+ documentCode: mrz.getDocumentCode(),
108
+ personalNumber: mrz.getPersonalNumber() || mrz.getOptionalData1(),
109
+ documentNumber: mrz.getDocumentNumber(),
110
+ firstName: mrz.getSecondaryIdentifier(),
111
+ lastName: mrz.getPrimaryIdentifier(),
112
+ birthDate: mrz.getDateOfBirth(),
113
+ expirationDate: mrz.getDateOfExpiry(),
114
+ issuingState: mrz.getIssuingState(),
115
+ sex: mrz.getGender().getStrCode(),
116
+ nationality: mrz.getNationality()
117
+ };
118
+ }, []);
102
119
  const readNFC = useCallback(async () => {
103
120
  const startTime = Date.now();
104
121
  const currentAttempt = attemptNumber + 1;
@@ -160,6 +177,25 @@ const EIDScanner = ({
160
177
  setDocumentMRZInfo(passportData.mrz);
161
178
  setDocumentFaceImageMimeType(passportData.mimeType);
162
179
  setIsScanned(true);
180
+ {
181
+ const documentData = normalizeFromMRZInfo(passportData.mrz);
182
+ const builtName = buildDocumentName(documentData.firstName, documentData.lastName, documentData.issuingCountry, passportData.dg11FirstName, passportData.dg11LastName);
183
+ setDocumentName(builtName);
184
+ const documentReadResult = {
185
+ document: {
186
+ ...documentData,
187
+ firstName: builtName.displayFirst,
188
+ lastName: builtName.displayLast
189
+ },
190
+ name: builtName,
191
+ face: {
192
+ data: passportData.faceImage,
193
+ mimeType: passportData.mimeType
194
+ }
195
+ };
196
+ appContext.setLastDocumentRead?.(documentReadResult);
197
+ appContext.onDocumentRead?.(documentReadResult);
198
+ }
163
199
  await trackEIDScanComplete(docType, scanDuration, currentAttempt).catch(() => {});
164
200
  }
165
201
  } else {
@@ -196,21 +232,7 @@ const EIDScanner = ({
196
232
  setProgressStage('');
197
233
  setHasGuideShown(false);
198
234
  }
199
- }, [documentNumber, dateOfBirth, dateOfExpiry, documentType, hasNfc, isEnabled, attemptNumber, t]);
200
- const getFieldsFromMRZ = useCallback(mrz => {
201
- return {
202
- documentCode: mrz.getDocumentCode(),
203
- personalNumber: mrz.getPersonalNumber() || mrz.getOptionalData1(),
204
- documentNumber: mrz.getDocumentNumber(),
205
- firstName: mrz.getSecondaryIdentifier(),
206
- lastName: mrz.getPrimaryIdentifier(),
207
- birthDate: mrz.getDateOfBirth(),
208
- expirationDate: mrz.getDateOfExpiry(),
209
- issuingState: mrz.getIssuingState(),
210
- sex: mrz.getGender().getStrCode(),
211
- nationality: mrz.getNationality()
212
- };
213
- }, []);
235
+ }, [documentNumber, dateOfBirth, dateOfExpiry, documentType, hasNfc, isEnabled, attemptNumber, t, getFieldsFromMRZ, appContext]);
214
236
  useEffect(() => {
215
237
  const checkIsSupported = async () => {
216
238
  const deviceIsSupported = await NFCManager.isSupported();
@@ -367,10 +389,12 @@ const EIDScanner = ({
367
389
  style: styles.idCardFields,
368
390
  children: [/*#__PURE__*/_jsx(IDCardField, {
369
391
  label: t('eidScannerScreen.surname'),
370
- value: documentMRZInfo.getPrimaryIdentifier()
392
+ value: documentName?.displayLast ?? documentMRZInfo.getPrimaryIdentifier(),
393
+ subValue: documentName?.source === 'dg11' ? documentMRZInfo.getPrimaryIdentifier() : undefined
371
394
  }), /*#__PURE__*/_jsx(IDCardField, {
372
395
  label: t('eidScannerScreen.name'),
373
- value: documentMRZInfo.getSecondaryIdentifier()
396
+ value: documentName?.displayFirst ?? documentMRZInfo.getSecondaryIdentifier(),
397
+ subValue: documentName?.source === 'dg11' ? documentMRZInfo.getSecondaryIdentifier() : undefined
374
398
  }), /*#__PURE__*/_jsx(IDCardField, {
375
399
  label: t('eidScannerScreen.documentNumber'),
376
400
  value: documentMRZInfo.getDocumentNumber()
@@ -627,6 +651,11 @@ const styles = StyleSheet.create({
627
651
  flexShrink: 1,
628
652
  flexWrap: 'wrap'
629
653
  },
654
+ idCardFieldSubValue: {
655
+ fontSize: 9,
656
+ color: '#aaa',
657
+ marginTop: 1
658
+ },
630
659
  guide: {
631
660
  flex: 1,
632
661
  display: 'flex',
@@ -658,7 +687,8 @@ const styles = StyleSheet.create({
658
687
  });
659
688
  const IDCardField = ({
660
689
  label,
661
- value
690
+ value,
691
+ subValue
662
692
  }) => /*#__PURE__*/_jsxs(View, {
663
693
  style: styles.idCardFieldItem,
664
694
  children: [/*#__PURE__*/_jsx(Text, {
@@ -668,6 +698,9 @@ const IDCardField = ({
668
698
  }), /*#__PURE__*/_jsx(Text, {
669
699
  style: styles.idCardFieldValue,
670
700
  children: value || '—'
671
- })]
701
+ }), subValue ? /*#__PURE__*/_jsx(Text, {
702
+ style: styles.idCardFieldSubValue,
703
+ children: subValue
704
+ }) : null]
672
705
  });
673
706
  export default EIDScanner;
@@ -24,8 +24,11 @@ export default /*#__PURE__*/createContext({
24
24
  currentWorkflowStep: undefined,
25
25
  isDebugNavigated: false,
26
26
  isTestVideoSession: false,
27
+ lastDocumentRead: undefined,
27
28
  onCompleted: undefined,
28
29
  onError: undefined,
30
+ onDocumentRead: undefined,
31
+ setLastDocumentRead: undefined,
29
32
  setSessionId: undefined,
30
33
  setBaseUrl: undefined
31
34
  });
@@ -6,6 +6,7 @@ import { NFCManagerCardService } from "./nfcManagerCardService.js";
6
6
  import { EIDService } from "./eidService.js";
7
7
  import { DG1File } from "./lds/icao/dg1File.js";
8
8
  import { DG2File } from "./lds/icao/dg2File.js";
9
+ import { DG11File } from "./lds/icao/dg11File.js";
9
10
  import { Buffer } from 'buffer';
10
11
  import { InputStream } from "./java/inputStream.js";
11
12
  import { debugLog } from "../Libs/debug.utils.js";
@@ -495,6 +496,20 @@ const eidReader = async (documentNumber, dateOfBirth, dateOfExpiry, progressCall
495
496
  progressCallback(progress);
496
497
  }
497
498
 
499
+ // Read DG11 (Additional Personal Data — printed name in UTF-8, optional)
500
+ let dg11FirstName = null;
501
+ let dg11LastName = null;
502
+ try {
503
+ const dg11InputStream = passportService.getInputStream(EIDService.EF_DG11);
504
+ await dg11InputStream.init();
505
+ const dg11File = new DG11File(dg11InputStream);
506
+ await dg11File.read();
507
+ dg11FirstName = dg11File.getFirstName();
508
+ dg11LastName = dg11File.getLastName();
509
+ } catch {
510
+ // DG11 is optional — not all chips have it, and access may be denied
511
+ }
512
+
498
513
  // Read DG2 (Face Image)
499
514
  const dg2Done = stepTimer();
500
515
  const dg2InputStream = passportService.getInputStream(EIDService.EF_DG2);
@@ -539,7 +554,9 @@ const eidReader = async (documentNumber, dateOfBirth, dateOfExpiry, progressCall
539
554
  return {
540
555
  mrz: mrzInfo,
541
556
  faceImage: imageAsBase64,
542
- mimeType: mimeType
557
+ mimeType: mimeType,
558
+ dg11FirstName,
559
+ dg11LastName
543
560
  };
544
561
  } catch (error) {
545
562
  const msg = error instanceof Error ? error.message : String(error);
@@ -0,0 +1,77 @@
1
+ "use strict";
2
+
3
+ import { Buffer } from 'buffer';
4
+ import { TLVInputStream } from "../../tlv/tlvInputStream.js";
5
+ import { DataGroup } from "../dataGroup.js";
6
+
7
+ /**
8
+ * DG11 — Additional Personal Data (ICAO 9303-10 §4.6.6).
9
+ *
10
+ * We only extract tag 0x5F0E (Name of Holder), which contains the full name
11
+ * exactly as printed on the document, in UTF-8, using the same `<<` primary/
12
+ * secondary delimiter as the MRZ (but with native Unicode characters).
13
+ *
14
+ * Example: "ÖZTÜRK<<GÖKHAN MEHMET" → lastName="ÖZTÜRK", firstName="GÖKHAN MEHMET"
15
+ *
16
+ * The file is optional on the chip — if absent or unreadable we return null
17
+ * so the caller can fall back to reverse transliteration.
18
+ */
19
+ export class DG11File extends DataGroup {
20
+ static DG11_TAG = 0x6b;
21
+ static TAG_LIST_TAG = 0x5c;
22
+ static NAME_OF_HOLDER_TAG = 0x5f0e;
23
+ firstName = null;
24
+ lastName = null;
25
+ constructor(is) {
26
+ super(DG11File.DG11_TAG, is);
27
+ }
28
+
29
+ /** Returns the first name from DG11, or null if not present. */
30
+ getFirstName() {
31
+ return this.firstName;
32
+ }
33
+
34
+ /** Returns the last name from DG11, or null if not present. */
35
+ getLastName() {
36
+ return this.lastName;
37
+ }
38
+ async read() {
39
+ await super.readObject();
40
+ }
41
+ async readContent(inputStream) {
42
+ const tlvIn = inputStream instanceof TLVInputStream ? inputStream : new TLVInputStream(inputStream);
43
+
44
+ // Skip the tag list (0x5C) — we only care about 0x5F0E
45
+ // The tag list tells us which optional tags are present; we probe directly.
46
+ try {
47
+ await tlvIn.skipToTag(DG11File.NAME_OF_HOLDER_TAG);
48
+ } catch {
49
+ // Tag not present in this chip's DG11
50
+ return;
51
+ }
52
+ const length = await tlvIn.readLength();
53
+ if (length <= 0) return;
54
+ const bytes = new Uint8Array(length);
55
+ await inputStream.readBytesWithOffset(bytes, 0, length);
56
+ const nameString = Buffer.from(bytes).toString('utf8').trim();
57
+ this.parseName(nameString);
58
+ }
59
+ writeContent() {
60
+ // Write not needed for reading
61
+ }
62
+
63
+ /**
64
+ * Split "PRIMARY<<SECONDARY" into last/first name components.
65
+ * Strips trailing filler `<` within each segment.
66
+ */
67
+ parseName(nameString) {
68
+ const delimIndex = nameString.indexOf('<<');
69
+ if (delimIndex < 0) {
70
+ this.lastName = nameString.replace(/<+$/, '').trim();
71
+ this.firstName = '';
72
+ return;
73
+ }
74
+ this.lastName = nameString.substring(0, delimIndex).replace(/<+$/, '').trim();
75
+ this.firstName = nameString.substring(delimIndex + 2).replace(/<$/, ' ').replace(/<+$/, '').trim();
76
+ }
77
+ }
@@ -279,7 +279,7 @@ export class MRZInfo extends AbstractLDSInfo {
279
279
  * @deprecated Class will become immutable
280
280
  */
281
281
  setPrimaryIdentifier(primaryIdentifier) {
282
- this.primaryIdentifier = MRZInfo.trimTrailingFillerChars(primaryIdentifier).replace('<', ' ');
282
+ this.primaryIdentifier = MRZInfo.trimTrailingFillerChars(primaryIdentifier).replace(/</g, ' ');
283
283
  this.checkDigit();
284
284
  }
285
285
 
@@ -772,11 +772,11 @@ export class MRZInfo extends AbstractLDSInfo {
772
772
  const delimIndex = mrzNameString.indexOf('<<');
773
773
  if (delimIndex < 0) {
774
774
  /* Only a primary identifier. */
775
- this.primaryIdentifier = MRZInfo.trimTrailingFillerChars(mrzNameString).replace('<', ' ');
775
+ this.primaryIdentifier = MRZInfo.trimTrailingFillerChars(mrzNameString).replace(/</g, ' ');
776
776
  this.secondaryIdentifier = '';
777
777
  return;
778
778
  }
779
- this.primaryIdentifier = MRZInfo.trimTrailingFillerChars(mrzNameString.substring(0, delimIndex)).replace('<', ' ');
779
+ this.primaryIdentifier = MRZInfo.trimTrailingFillerChars(mrzNameString.substring(0, delimIndex)).replace(/</g, ' ');
780
780
  const rest = mrzNameString.substring(delimIndex + 2);
781
781
  this.readSecondaryIdentifiers(rest);
782
782
  }
@@ -787,7 +787,7 @@ export class MRZInfo extends AbstractLDSInfo {
787
787
  * @param secondaryIdentifier the new secondary identifier
788
788
  */
789
789
  readSecondaryIdentifiers(secondaryIdentifier) {
790
- this.secondaryIdentifier = MRZInfo.trimTrailingFillerChars(secondaryIdentifier).replace('<', ' ');
790
+ this.secondaryIdentifier = MRZInfo.trimTrailingFillerChars(secondaryIdentifier).replace(/</g, ' ');
791
791
  }
792
792
 
793
793
  /**
@@ -0,0 +1,104 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Expand a 2-digit MRZ year to a 4-digit calendar year.
5
+ * ICAO 9303: YY >= 30 → 19YY, YY < 30 → 20YY.
6
+ * (Threshold 30 keeps all current valid documents in range.)
7
+ */
8
+ function expandYear(yy) {
9
+ const n = parseInt(yy, 10);
10
+ return n >= 30 ? `19${yy}` : `20${yy}`;
11
+ }
12
+
13
+ /**
14
+ * Convert a YYMMDD MRZ date string to ISO 8601 YYYY-MM-DD.
15
+ * Returns null for empty, null, or malformed input.
16
+ */
17
+ function mrzDateToISO(raw) {
18
+ if (!raw || raw.length !== 6) return null;
19
+ const yy = raw.slice(0, 2);
20
+ const mm = raw.slice(2, 4);
21
+ const dd = raw.slice(4, 6);
22
+ if (!/^\d{6}$/.test(raw)) return null;
23
+ return `${expandYear(yy)}-${mm}-${dd}`;
24
+ }
25
+
26
+ /**
27
+ * Normalize the MRZ document-code field to a single-letter form.
28
+ * The `mrz` npm package returns "ID" for TD1, while the chip returns "I".
29
+ */
30
+ function normalizeDocumentType(raw) {
31
+ if (!raw) return 'I';
32
+ const upper = raw.trim().toUpperCase();
33
+ if (upper === 'ID') return 'I';
34
+ // TD2 can be "IR", "AC", etc — take only the first character
35
+ return upper.charAt(0);
36
+ }
37
+
38
+ /**
39
+ * Normalize the sex field to a single uppercase letter.
40
+ * NFC chip: "M" | "F" | "U" | "X"
41
+ * mrz package: "male" | "female" | "" | null
42
+ */
43
+ function normalizeSex(raw) {
44
+ if (!raw) return 'U';
45
+ switch (raw.trim().toLowerCase()) {
46
+ case 'm':
47
+ case 'male':
48
+ return 'M';
49
+ case 'f':
50
+ case 'female':
51
+ return 'F';
52
+ case 'x':
53
+ case 'unspecified':
54
+ return 'X';
55
+ default:
56
+ return 'U';
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Trim a string and return null if empty or only filler chars.
62
+ */
63
+ function clean(val) {
64
+ if (!val) return null;
65
+ const t = val.replace(/</g, '').trim();
66
+ return t.length > 0 ? t : null;
67
+ }
68
+
69
+ /**
70
+ * Build a DocumentData from a chip-read MRZInfo object (NFC path).
71
+ */
72
+ export function normalizeFromMRZInfo(mrz) {
73
+ const rawPersonal = mrz.getPersonalNumber() ?? clean(mrz.getOptionalData1());
74
+ return {
75
+ documentType: normalizeDocumentType(mrz.getDocumentCode()),
76
+ issuingCountry: mrz.getIssuingState().replace(/<$/, '').trim(),
77
+ nationality: mrz.getNationality().replace(/<$/, '').trim(),
78
+ documentNumber: mrz.getDocumentNumber().trim(),
79
+ personalNumber: clean(rawPersonal),
80
+ lastName: mrz.getPrimaryIdentifier().trim(),
81
+ firstName: mrz.getSecondaryIdentifier().trim(),
82
+ sex: normalizeSex(mrz.getGender().getStrCode()),
83
+ dateOfBirth: mrzDateToISO(mrz.getDateOfBirth()),
84
+ dateOfExpiry: mrzDateToISO(mrz.getDateOfExpiry())
85
+ };
86
+ }
87
+
88
+ /**
89
+ * Build a DocumentData from an MRZFields object (camera OCR path).
90
+ */
91
+ export function normalizeFromMRZFields(fields) {
92
+ return {
93
+ documentType: normalizeDocumentType(fields.documentCode),
94
+ issuingCountry: clean(fields.issuingState) ?? '',
95
+ nationality: clean(fields.nationality) ?? '',
96
+ documentNumber: clean(fields.documentNumber) ?? '',
97
+ personalNumber: clean(fields.personalNumber),
98
+ lastName: clean(fields.lastName) ?? '',
99
+ firstName: clean(fields.firstName) ?? '',
100
+ sex: normalizeSex(fields.sex),
101
+ dateOfBirth: mrzDateToISO(fields.birthDate),
102
+ dateOfExpiry: mrzDateToISO(fields.expirationDate)
103
+ };
104
+ }
@@ -1134,18 +1134,13 @@ const validateMRZ = (mrzText, autocorrect = true) => {
1134
1134
  documentCode: result.fields.documentCode || null,
1135
1135
  issuingState: result.fields.issuingState || null,
1136
1136
  documentNumber: result.fields.documentNumber || null,
1137
- documentNumberCheckDigit: result.fields.documentNumberCheckDigit || null,
1138
1137
  nationality: result.fields.nationality || null,
1139
1138
  lastName: result.fields.lastName || null,
1140
1139
  firstName: result.fields.firstName || null,
1141
1140
  sex: result.fields.sex || null,
1142
1141
  birthDate: result.fields.birthDate || null,
1143
- birthDateCheckDigit: result.fields.birthDateCheckDigit || null,
1144
1142
  expirationDate: result.fields.expirationDate || null,
1145
- expirationDateCheckDigit: result.fields.expirationDateCheckDigit || null,
1146
1143
  personalNumber: result.fields.personalNumber || null,
1147
- personalNumberCheckDigit: result.fields.personalNumberCheckDigit || null,
1148
- compositeCheckDigit: result.fields.compositeCheckDigit || null,
1149
1144
  optional1: result.fields.optional1 || null,
1150
1145
  optional2: result.fields.optional2 || null
1151
1146
  };