@trustchex/react-native-sdk 1.503.2 → 1.505.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 (28) hide show
  1. package/lib/module/Screens/Dynamic/IdentityDocumentEIDScanningScreen.js +7 -0
  2. package/lib/module/Screens/Dynamic/IdentityDocumentScanningScreen.js +20 -3
  3. package/lib/module/Shared/Components/EIDScanner.js +21 -2
  4. package/lib/module/Shared/EIDReader/eidReader.js +9 -1
  5. package/lib/module/Shared/Libs/mrzTransliteration.js +46 -5
  6. package/lib/module/Shared/Libs/vizName.js +140 -0
  7. package/lib/module/version.js +1 -1
  8. package/lib/typescript/src/Screens/Dynamic/IdentityDocumentEIDScanningScreen.d.ts.map +1 -1
  9. package/lib/typescript/src/Screens/Dynamic/IdentityDocumentScanningScreen.d.ts.map +1 -1
  10. package/lib/typescript/src/Shared/Components/EIDScanner.d.ts +7 -1
  11. package/lib/typescript/src/Shared/Components/EIDScanner.d.ts.map +1 -1
  12. package/lib/typescript/src/Shared/EIDReader/eidReader.d.ts.map +1 -1
  13. package/lib/typescript/src/Shared/Libs/mrzTransliteration.d.ts +2 -1
  14. package/lib/typescript/src/Shared/Libs/mrzTransliteration.d.ts.map +1 -1
  15. package/lib/typescript/src/Shared/Libs/vizName.d.ts +74 -0
  16. package/lib/typescript/src/Shared/Libs/vizName.d.ts.map +1 -0
  17. package/lib/typescript/src/Shared/Types/documentReadResult.d.ts +8 -1
  18. package/lib/typescript/src/Shared/Types/documentReadResult.d.ts.map +1 -1
  19. package/lib/typescript/src/version.d.ts +1 -1
  20. package/package.json +1 -1
  21. package/src/Screens/Dynamic/IdentityDocumentEIDScanningScreen.tsx +10 -0
  22. package/src/Screens/Dynamic/IdentityDocumentScanningScreen.tsx +33 -3
  23. package/src/Shared/Components/EIDScanner.tsx +38 -2
  24. package/src/Shared/EIDReader/eidReader.ts +9 -1
  25. package/src/Shared/Libs/mrzTransliteration.ts +52 -1
  26. package/src/Shared/Libs/vizName.ts +192 -0
  27. package/src/Shared/Types/documentReadResult.ts +8 -1
  28. package/src/version.ts +1 -1
@@ -86,6 +86,13 @@ const IdentityDocumentEIDScanningScreen = () => {
86
86
  // the MRZ is on the BACK (idBackSideData), so it must be included
87
87
  // — without it, ID-card scans had no VIZ source and hard-blocked.
88
88
  passportData?.mrzFields ?? idBackSideData?.mrzFields ?? idFrontSideData?.mrzFields ?? appContext.identificationInfo.scannedDocument?.mrzFields,
89
+ vizImage:
90
+ // Full data-page image from the EID step's camera scan. EIDScanner
91
+ // runs a one-shot OCR pass over it to recover the diacritic printed
92
+ // name when the chip's DG11 is absent or one-sided (e.g. Turkish
93
+ // passports). The live camera only OCRs a cropped MRZ region, so the
94
+ // printed name must be recovered from the full image here.
95
+ passportData?.image ?? idFrontSideData?.image ?? idBackSideData?.image,
89
96
  onScanSuccess: (mrzFields, faceImage, mimeType) => {
90
97
  if (mrzFields && faceImage && mimeType) {
91
98
  if (!appContext.identificationInfo.scannedDocument) {
@@ -2,6 +2,8 @@
2
2
 
3
3
  import React, { useContext, useEffect, useState } from 'react';
4
4
  import { buildDocumentName } from "../../Shared/Libs/mrzTransliteration.js";
5
+ import { recoverVizName, recognizeVizBlocks } from "../../Shared/Libs/vizName.js";
6
+ import { debugLog } from "../../Shared/Libs/debug.utils.js";
5
7
  import { isCountryAllowed } from "../../Shared/Libs/country-allow.js";
6
8
  import { normalizeFromMRZFields } from "../../Shared/Libs/documentDataNormalizer.js";
7
9
  import { Alert, SafeAreaView, StyleSheet, View } from 'react-native';
@@ -90,10 +92,25 @@ const IdentityDocumentScanningScreen = () => {
90
92
 
91
93
  // Track successful document scan completion as funnel step
92
94
  trackFunnelStep('Document Scan Completed', 2, 5, 'contract_acceptance', true);
93
- {
95
+ void (async () => {
94
96
  const documentData = normalizeFromMRZFields(mrzFields);
95
97
  const faceImg = idFrontSideData?.faceImage || passportData?.faceImage;
96
- const builtName = buildDocumentName(documentData.firstName, documentData.lastName, documentData.issuingCountry);
98
+ // Recover the printed (VIZ) name with diacritics the MRZ cannot carry.
99
+ // The live camera OCRs only a cropped MRZ region, so the printed name was
100
+ // never recognised there — run a one-shot full-frame OCR pass on the
101
+ // captured data-page image to surface it. The MRZ sits at the bottom, so
102
+ // restrict the name search to blocks above it.
103
+ const docImage = idFrontSideData?.image || passportData?.image;
104
+ const vizBlocks = await recognizeVizBlocks(docImage);
105
+ const mrzTop = vizBlocks.length ? Math.max(...vizBlocks.map(b => b.top)) : undefined;
106
+ const viz = vizBlocks.length ? recoverVizName(vizBlocks, documentData.firstName, documentData.lastName, mrzTop) : null;
107
+ debugLog('VIZName', 'ocr recovery', {
108
+ vizBlockCount: vizBlocks.length,
109
+ matchedFirst: !!viz?.vizFirst,
110
+ matchedLast: !!viz?.vizLast,
111
+ mismatch: viz?.mismatch ?? false
112
+ });
113
+ const builtName = buildDocumentName(documentData.firstName, documentData.lastName, documentData.issuingCountry, undefined, undefined, viz);
97
114
  const documentReadResult = {
98
115
  document: {
99
116
  ...documentData,
@@ -108,7 +125,7 @@ const IdentityDocumentScanningScreen = () => {
108
125
  };
109
126
  appContext.setLastDocumentRead?.(documentReadResult);
110
127
  appContext.onDocumentRead?.(documentReadResult);
111
- }
128
+ })();
112
129
  setTimeout(() => navigationManagerRef.current?.navigateToNextStep(), 1000);
113
130
  }
114
131
  }, [idFrontSideData, idBackSideData, passportData, appContext.identificationInfo, allowedCountries, allowedDocumentTypes, t, appContext]);
@@ -7,10 +7,11 @@ import NFCManager from 'react-native-nfc-manager';
7
7
  import DeviceInfo from 'react-native-device-info';
8
8
  import PermissionManager from "../Libs/permissions.utils.js";
9
9
  import mrzUtils from "../Libs/mrz.utils.js";
10
- import { debugError } from "../Libs/debug.utils.js";
10
+ import { debugError, debugLog } from "../Libs/debug.utils.js";
11
11
  import { eidReader } from "../EIDReader/eidReader.js";
12
12
  import NativeProgressBar from "./NativeProgressBar.js";
13
13
  import { buildDocumentName } from "../Libs/mrzTransliteration.js";
14
+ import { recoverVizName, recognizeVizBlocks } from "../Libs/vizName.js";
14
15
  import { normalizeFromMRZInfo, normalizeFromMRZFields } from "../Libs/documentDataNormalizer.js";
15
16
  import { compareChipToViz } from "../Libs/chipVizMatch.js";
16
17
  import { useTranslation } from 'react-i18next';
@@ -36,6 +37,7 @@ const EIDScanner = ({
36
37
  dateOfExpiry,
37
38
  documentType,
38
39
  vizMrzFields,
40
+ vizImage,
39
41
  onScanSuccess,
40
42
  isNFCSupported
41
43
  }) => {
@@ -246,7 +248,24 @@ const EIDScanner = ({
246
248
  Vibration.vibrate(120);
247
249
  const scanDuration = Date.now() - startTime;
248
250
  const documentData = normalizeFromMRZInfo(passportData.mrz);
249
- const builtName = buildDocumentName(documentData.firstName, documentData.lastName, documentData.issuingCountry, passportData.dg11FirstName, passportData.dg11LastName);
251
+ // DG11 (the authenticated chip's own printed name) is the preferred
252
+ // correction. When it is absent or one-sided, fall back to recovering
253
+ // the diacritic printed name from the EID step's VIZ camera blocks —
254
+ // this is what fixes e.g. Turkish passports whose DG11 has no usable
255
+ // name. buildDocumentName ranks viz_ocr above dg11, so only pass the
256
+ // VIZ candidate when DG11 did NOT provide a clean two-part name.
257
+ const dg11HasBothNames = !!passportData.dg11FirstName && !!passportData.dg11LastName;
258
+ // Only OCR the VIZ when DG11 didn't already provide a clean name.
259
+ const vizBlocks = !dg11HasBothNames ? await recognizeVizBlocks(vizImage) : [];
260
+ const viz = vizBlocks.length ? recoverVizName(vizBlocks, documentData.firstName, documentData.lastName, Math.max(...vizBlocks.map(b => b.top))) : null;
261
+ debugLog('VIZName', 'eid recovery', {
262
+ dg11HasBothNames,
263
+ vizBlockCount: vizBlocks.length,
264
+ matchedFirst: !!viz?.vizFirst,
265
+ matchedLast: !!viz?.vizLast,
266
+ mismatch: viz?.mismatch ?? false
267
+ });
268
+ const builtName = buildDocumentName(documentData.firstName, documentData.lastName, documentData.issuingCountry, passportData.dg11FirstName, passportData.dg11LastName, viz);
250
269
  // MASAK Tebliğ No. 32 Article 4/C(1)(b): the chip identity data must
251
270
  // match the data printed on the passport. The printed/VIZ MRZ is the
252
271
  // camera scan the EID step itself runs to obtain the BAC/PACE key,
@@ -621,8 +621,16 @@ const eidReader = async (documentNumber, dateOfBirth, dateOfExpiry, progressCall
621
621
  dg11FirstName = dg11File.getFirstName();
622
622
  dg11LastName = dg11File.getLastName();
623
623
  dg11Address = dg11File.getAddress();
624
- } catch {
624
+ debugLog('EIDReader', 'DG11 read', {
625
+ firstName: dg11FirstName,
626
+ lastName: dg11LastName,
627
+ hasAddress: !!dg11Address
628
+ });
629
+ } catch (e) {
625
630
  // DG11 is optional — not all chips have it, and access may be denied
631
+ debugLog('EIDReader', 'DG11 not available', {
632
+ error: e instanceof Error ? e.message : String(e)
633
+ });
626
634
  }
627
635
 
628
636
  // Read DG2 (Face Image)
@@ -10,17 +10,32 @@ const DANISH = [['AA', 'Å'], ['AE', 'Æ'], ['OE', 'Ø']];
10
10
  const NORWEGIAN = [['AA', 'Å'], ['AE', 'Æ'], ['OE', 'Ø']];
11
11
  const FRENCH = [['OE', 'Œ'], ['AE', 'Æ']];
12
12
  const ICELANDIC = [['AE', 'Æ'], ['OE', 'Ö'], ['TH', 'Þ'], ['D', 'Ð']];
13
+
14
+ // Finnish/Estonian share the Swedish-style AE→Ä, OE→Ö pair (no Å in names is
15
+ // common, but AA→Å is safe to include for Finnish-Swedish surnames).
16
+ const FINNISH = [['AE', 'Ä'], ['OE', 'Ö'], ['AA', 'Å']];
17
+ const ESTONIAN = [['AE', 'Ä'], ['OE', 'Ö'], ['UE', 'Ü']];
13
18
  const COUNTRY_MAP = {
14
19
  DEU: GERMAN,
15
20
  AUT: GERMAN,
16
21
  CHE: GERMAN,
22
+ LIE: GERMAN,
17
23
  SWE: SWEDISH,
18
24
  DNK: DANISH,
19
25
  NOR: NORWEGIAN,
20
26
  FRA: FRENCH,
21
27
  LUX: FRENCH,
22
- ISL: ICELANDIC
28
+ BEL: FRENCH,
29
+ ISL: ICELANDIC,
30
+ FIN: FINNISH,
31
+ EST: ESTONIAN
23
32
  };
33
+ // NOTE: Turkish (TUR), Hungarian, Polish, Czech, Slovak, etc. are intentionally
34
+ // absent. Their diacritics (Ş Ğ İ Ç, ő ű, ł, ř ž, ą ę) collapse to bare ASCII in
35
+ // the MRZ with NO digraph, so they cannot be reconstructed from the MRZ at all.
36
+ // For those documents the only correct source is the printed VIZ (viz_ocr) or
37
+ // the chip's DG11 — see buildDocumentName's priority order.
38
+
24
39
  function applyDigraphMap(token, map) {
25
40
  let result = '';
26
41
  let i = 0;
@@ -44,7 +59,30 @@ function applyDigraphMap(token, map) {
44
59
  function convertName(rawName, map) {
45
60
  return rawName.split(' ').map(word => applyDigraphMap(word, map)).join(' ');
46
61
  }
47
- export function buildDocumentName(rawFirst, rawLast, issuingState, dg11First, dg11Last) {
62
+ export function buildDocumentName(rawFirst, rawLast, issuingState, dg11First, dg11Last, viz) {
63
+ // Highest priority: the printed VIZ name recovered via OCR. This is the only
64
+ // source that can restore diacritics the MRZ cannot represent (e.g. Turkish
65
+ // Ş Ğ İ Ç) when the chip's DG11 is absent. `recoverVizName` only returns a
66
+ // candidate that folds to the SAME ASCII as the MRZ name, so accepting it
67
+ // restores diacritics without changing the letters. A letter-level
68
+ // discrepancy comes back as `mismatch` (no candidate applied) and is surfaced
69
+ // via `nameMismatch` for operator review — the MRZ name stays authoritative.
70
+ if (viz && (viz.vizFirst || viz.vizLast)) {
71
+ return {
72
+ rawFirst,
73
+ rawLast,
74
+ displayFirst: viz.vizFirst ?? rawFirst,
75
+ displayLast: viz.vizLast ?? rawLast,
76
+ source: 'viz_ocr',
77
+ nameMismatch: viz.mismatch || undefined
78
+ };
79
+ }
80
+
81
+ // A VIZ name was detected but disagreed with the MRZ at the letter level and
82
+ // could not be applied. The MRZ stays authoritative through the branches
83
+ // below, but we carry the review flag through so the discrepancy is recorded.
84
+ const nameMismatch = viz?.mismatch ? true : undefined;
85
+
48
86
  // Prefer DG11 (the chip's printed name in UTF-8) ONLY when it carries a proper
49
87
  // two-part split — both a surname AND a given name. That is the authoritative
50
88
  // correction for chips whose DG1 MRZ name lacks a `<<` separator (e.g. Turkish
@@ -64,7 +102,8 @@ export function buildDocumentName(rawFirst, rawLast, issuingState, dg11First, dg
64
102
  rawLast,
65
103
  displayFirst: dg11First,
66
104
  displayLast: dg11Last,
67
- source: 'dg11'
105
+ source: 'dg11',
106
+ nameMismatch
68
107
  };
69
108
  }
70
109
  const country = (issuingState ?? '').toUpperCase().trim();
@@ -75,7 +114,8 @@ export function buildDocumentName(rawFirst, rawLast, issuingState, dg11First, dg
75
114
  rawLast,
76
115
  displayFirst: convertName(rawFirst, map),
77
116
  displayLast: convertName(rawLast, map),
78
- source: 'reverse_table'
117
+ source: 'reverse_table',
118
+ nameMismatch
79
119
  };
80
120
  }
81
121
 
@@ -88,6 +128,7 @@ export function buildDocumentName(rawFirst, rawLast, issuingState, dg11First, dg
88
128
  rawLast,
89
129
  displayFirst,
90
130
  displayLast,
91
- source: changed ? 'reverse_table' : 'raw'
131
+ source: changed ? 'reverse_table' : 'raw',
132
+ nameMismatch
92
133
  };
93
134
  }
@@ -0,0 +1,140 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Visual Inspection Zone (VIZ) printed-name recovery.
5
+ *
6
+ * The MRZ stores names in ASCII OCR-B with no diacritics, and for some scripts
7
+ * the diacritics are unrecoverable from the MRZ alone — Turkish in particular
8
+ * collapses Ş→S, Ğ→G, İ→I, Ç→C, Ö→O, Ü→U with no ICAO digraph, so "ŞİMŞEK"
9
+ * reads as "SIMSEK" and the true form is lost. The chip's DG11 carries the
10
+ * printed name in UTF-8, but it is optional and often absent.
11
+ *
12
+ * The printed name IS, however, on the document's data page in its native form.
13
+ * ML Kit already recognises every text block on the page (with bounding boxes)
14
+ * for MRZ parsing; this module reuses those blocks to recover the diacritic
15
+ * printed name and reconcile it against the MRZ.
16
+ *
17
+ * Conflict policy (per product decision): the MRZ name stays authoritative. We
18
+ * accept a VIZ candidate ONLY when it folds to the same ASCII as the MRZ name
19
+ * (i.e. it is the same name, just with diacritics restored). When the VIZ name
20
+ * differs at the letter level — a possible OCR misread or a genuine discrepancy
21
+ * — we keep the MRZ value and raise a `nameMismatch` flag for operator review
22
+ * rather than silently overwriting the machine-read name.
23
+ */
24
+
25
+ import { NativeModules } from 'react-native';
26
+
27
+ /** One recognised text block, in the shape ML Kit returns to JS. */
28
+
29
+ /**
30
+ * Run a single full-frame OCR pass over the captured document image and return
31
+ * its text blocks.
32
+ *
33
+ * The live camera path OCRs only a cropped MRZ region (tuned for MRZ sharpness),
34
+ * so the printed name above it is never recognised there. This one-shot pass on
35
+ * the FULL captured image surfaces the printed VIZ name for {@link recoverVizName}.
36
+ * It is best-effort: returns [] when the native module is missing, the image is
37
+ * absent, or OCR throws — the caller then simply falls back to DG11/MRZ.
38
+ *
39
+ * @param base64Image Full document image (no data-URI prefix), or null/undefined.
40
+ */
41
+ export async function recognizeVizBlocks(base64Image) {
42
+ // Resolved per-call (not captured at import) so module-load ordering can't
43
+ // null it out, and so it is mockable in tests.
44
+ const mlkit = NativeModules.MLKitModule;
45
+ if (!base64Image || !mlkit?.recognizeText) return [];
46
+ try {
47
+ const blocks = await mlkit.recognizeText(base64Image);
48
+ return (blocks ?? []).filter(b => !!b?.text).map(b => ({
49
+ text: b.text,
50
+ top: b.boundingBox?.top ?? 0
51
+ }));
52
+ } catch {
53
+ return [];
54
+ }
55
+ }
56
+ /**
57
+ * Fold a string to bare ASCII upper-case letters for comparison: strip
58
+ * combining marks via NFD, map the handful of letters that have no combining
59
+ * decomposition (ı, ł, ø, đ, ð, ß, æ, œ, þ), drop everything that is not A–Z.
60
+ * This is the canonical form in which an MRZ name and its printed counterpart
61
+ * become directly comparable.
62
+ */
63
+ export function foldAscii(value) {
64
+ if (!value) return '';
65
+ return value.normalize('NFD').replace(/[̀-ͯ]/g, '') // combining diacritical marks
66
+ .replace(/ı/g, 'i').replace(/İ/g, 'I').replace(/ø/gi, 'O').replace(/ł/gi, 'L').replace(/đ/gi, 'D').replace(/ð/gi, 'D').replace(/þ/gi, 'TH').replace(/ß/g, 'SS').replace(/æ/gi, 'AE').replace(/œ/gi, 'OE').toUpperCase().replace(/[^A-Z]/g, '');
67
+ }
68
+
69
+ /** Split a printed-name line into upper-case word tokens (letters + marks). */
70
+ function tokenize(line) {
71
+ return line.split(/[\s,]+/).map(w => w.trim()).filter(w => w.length > 0 && /\p{L}/u.test(w)).map(w => w.toUpperCase());
72
+ }
73
+
74
+ /**
75
+ * Find the printed token sequence whose folded ASCII equals the folded MRZ name.
76
+ * Returns the diacritic-preserving printed form when found, else null.
77
+ *
78
+ * The candidate lines are searched in document order; the matched window is the
79
+ * shortest run of consecutive tokens whose concatenated fold equals the target.
80
+ * Because the comparison is on the FOLDED form, only an exact same-name match
81
+ * succeeds — a different printed name will not fold-match and yields null.
82
+ */
83
+ function recoverPrintedName(lines, mrzName) {
84
+ const target = foldAscii(mrzName);
85
+ if (!target) return null;
86
+ for (const line of lines) {
87
+ const tokens = tokenize(line);
88
+ // Slide a window over the tokens; the printed name may sit among other
89
+ // label text on the same block ("Surname / Nom ÖZTÜRK").
90
+ for (let start = 0; start < tokens.length; start++) {
91
+ let acc = '';
92
+ const picked = [];
93
+ for (let end = start; end < tokens.length; end++) {
94
+ acc += foldAscii(tokens[end]);
95
+ picked.push(tokens[end]);
96
+ if (acc.length > target.length) break;
97
+ if (acc === target) {
98
+ return picked.join(' ');
99
+ }
100
+ }
101
+ }
102
+ }
103
+ return null;
104
+ }
105
+
106
+ /**
107
+ * Recover the diacritic printed name from VIZ text blocks and reconcile it with
108
+ * the MRZ-derived raw names.
109
+ *
110
+ * @param blocks Recognised text blocks (text + vertical position).
111
+ * @param mrzFirst Raw MRZ given name (ASCII, may be empty).
112
+ * @param mrzLast Raw MRZ surname (ASCII, may be empty).
113
+ * @param mrzTop Vertical position of the MRZ block, when known. Name fields
114
+ * sit ABOVE the MRZ on the data page; passing this restricts the
115
+ * search to blocks above it, avoiding false matches in the MRZ
116
+ * itself. When undefined, all blocks are searched.
117
+ */
118
+ export function recoverVizName(blocks, mrzFirst, mrzLast, mrzTop) {
119
+ const above = typeof mrzTop === 'number' ? blocks.filter(b => b.top < mrzTop) : blocks;
120
+ // Each block's text may hold several printed lines.
121
+ const lines = above.flatMap(b => b.text.split(/\r?\n/));
122
+ const vizFirst = mrzFirst ? recoverPrintedName(lines, mrzFirst) : null;
123
+ const vizLast = mrzLast ? recoverPrintedName(lines, mrzLast) : null;
124
+
125
+ // Detect a letter-level discrepancy: a printed line clearly carries the name
126
+ // field but its fold does not equal the MRZ name. We approximate this by
127
+ // checking whether ANY line fold-matches; if the MRZ name is non-trivial and
128
+ // nothing matched, the printed name on the page disagrees with the MRZ.
129
+ const lastMissing = !!mrzLast && vizLast === null;
130
+ const firstMissing = !!mrzFirst && vizFirst === null;
131
+ // Only call it a mismatch when there is printed name-like text to compare
132
+ // against — otherwise the VIZ simply was not captured (not a conflict).
133
+ const hasPrintedNameText = lines.some(l => tokenize(l).length > 0);
134
+ const mismatch = hasPrintedNameText && (lastMissing || firstMissing);
135
+ return {
136
+ vizFirst,
137
+ vizLast,
138
+ mismatch
139
+ };
140
+ }
@@ -2,4 +2,4 @@
2
2
 
3
3
  // This file is auto-generated. Do not edit manually.
4
4
  // Version is synced from package.json during build.
5
- export const SDK_VERSION = '1.503.2';
5
+ export const SDK_VERSION = '1.505.0';
@@ -1 +1 @@
1
- {"version":3,"file":"IdentityDocumentEIDScanningScreen.d.ts","sourceRoot":"","sources":["../../../../../src/Screens/Dynamic/IdentityDocumentEIDScanningScreen.tsx"],"names":[],"mappings":"AAsBA,QAAA,MAAM,iCAAiC,+CA0PtC,CAAC;AAoBF,eAAe,iCAAiC,CAAC"}
1
+ {"version":3,"file":"IdentityDocumentEIDScanningScreen.d.ts","sourceRoot":"","sources":["../../../../../src/Screens/Dynamic/IdentityDocumentEIDScanningScreen.tsx"],"names":[],"mappings":"AAsBA,QAAA,MAAM,iCAAiC,+CAoQtC,CAAC;AAoBF,eAAe,iCAAiC,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"IdentityDocumentScanningScreen.d.ts","sourceRoot":"","sources":["../../../../../src/Screens/Dynamic/IdentityDocumentScanningScreen.tsx"],"names":[],"mappings":"AAwBA,QAAA,MAAM,8BAA8B,+CAqLnC,CAAC;AAgBF,eAAe,8BAA8B,CAAC"}
1
+ {"version":3,"file":"IdentityDocumentScanningScreen.d.ts","sourceRoot":"","sources":["../../../../../src/Screens/Dynamic/IdentityDocumentScanningScreen.tsx"],"names":[],"mappings":"AA0BA,QAAA,MAAM,8BAA8B,+CAiNnC,CAAC;AAgBF,eAAe,8BAA8B,CAAC"}
@@ -10,9 +10,15 @@ interface eIDScannerProps {
10
10
  * because scannedDocument.mrzFields is only persisted after the chip read.
11
11
  */
12
12
  vizMrzFields?: MRZFields;
13
+ /**
14
+ * Full data-page image from the EID step's camera scan. A one-shot OCR pass
15
+ * recovers the diacritic printed name when the chip's DG11 is absent or
16
+ * one-sided — DG11 (the authenticated chip's own name) still takes precedence.
17
+ */
18
+ vizImage?: string;
13
19
  onScanSuccess?: (mrzInfo: MRZFields, faceImage: string, faceImageMimeType: string) => void;
14
20
  isNFCSupported?: (supported: boolean) => void;
15
21
  }
16
- declare const EIDScanner: ({ documentNumber, dateOfBirth, dateOfExpiry, documentType, vizMrzFields, onScanSuccess, isNFCSupported, }: eIDScannerProps) => import("react/jsx-runtime").JSX.Element;
22
+ declare const EIDScanner: ({ documentNumber, dateOfBirth, dateOfExpiry, documentType, vizMrzFields, vizImage, onScanSuccess, isNFCSupported, }: eIDScannerProps) => import("react/jsx-runtime").JSX.Element;
17
23
  export default EIDScanner;
18
24
  //# sourceMappingURL=EIDScanner.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"EIDScanner.d.ts","sourceRoot":"","sources":["../../../../../src/Shared/Components/EIDScanner.tsx"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AA6BpD,UAAU,eAAe;IACvB,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,YAAY,CAAC,EAAE,SAAS,CAAC;IACzB,aAAa,CAAC,EAAE,CACd,OAAO,EAAE,SAAS,EAClB,SAAS,EAAE,MAAM,EACjB,iBAAiB,EAAE,MAAM,KACtB,IAAI,CAAC;IACV,cAAc,CAAC,EAAE,CAAC,SAAS,EAAE,OAAO,KAAK,IAAI,CAAC;CAC/C;AAED,QAAA,MAAM,UAAU,GAAI,2GAQjB,eAAe,4CAi6BjB,CAAC;AAsOF,eAAe,UAAU,CAAC"}
1
+ {"version":3,"file":"EIDScanner.d.ts","sourceRoot":"","sources":["../../../../../src/Shared/Components/EIDScanner.tsx"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AA8BpD,UAAU,eAAe;IACvB,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,YAAY,CAAC,EAAE,SAAS,CAAC;IACzB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,CACd,OAAO,EAAE,SAAS,EAClB,SAAS,EAAE,MAAM,EACjB,iBAAiB,EAAE,MAAM,KACtB,IAAI,CAAC;IACV,cAAc,CAAC,EAAE,CAAC,SAAS,EAAE,OAAO,KAAK,IAAI,CAAC;CAC/C;AAED,QAAA,MAAM,UAAU,GAAI,qHASjB,eAAe,4CA67BjB,CAAC;AAsOF,eAAe,UAAU,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"eidReader.d.ts","sourceRoot":"","sources":["../../../../../src/Shared/EIDReader/eidReader.ts"],"names":[],"mappings":"AAYA,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AA2Y7C,QAAA,MAAM,SAAS,GACb,gBAAgB,MAAM,EACtB,aAAa,MAAM,EACnB,cAAc,MAAM,EACpB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,EAC7C,UAAU;IAAE,WAAW,CAAC,EAAE,OAAO,CAAA;CAAE,KAClC,OAAO,CACN;IACE,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B;;;;OAIG;IACH,WAAW,CAAC,EAAE;QACZ,SAAS,EAAE,OAAO,yBAAyB,EAAE,eAAe,GAAG,IAAI,CAAC;QACpE,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;QACzB,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,CAAC;CACH,GACD,SAAS,CAocZ,CAAC;AAEF,OAAO,EAAE,SAAS,EAAE,CAAC"}
1
+ {"version":3,"file":"eidReader.d.ts","sourceRoot":"","sources":["../../../../../src/Shared/EIDReader/eidReader.ts"],"names":[],"mappings":"AAYA,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AA2Y7C,QAAA,MAAM,SAAS,GACb,gBAAgB,MAAM,EACtB,aAAa,MAAM,EACnB,cAAc,MAAM,EACpB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,EAC7C,UAAU;IAAE,WAAW,CAAC,EAAE,OAAO,CAAA;CAAE,KAClC,OAAO,CACN;IACE,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B;;;;OAIG;IACH,WAAW,CAAC,EAAE;QACZ,SAAS,EAAE,OAAO,yBAAyB,EAAE,eAAe,GAAG,IAAI,CAAC;QACpE,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;QACzB,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,CAAC;CACH,GACD,SAAS,CA4cZ,CAAC;AAEF,OAAO,EAAE,SAAS,EAAE,CAAC"}
@@ -1,3 +1,4 @@
1
1
  import type { DocumentName } from '../Types/documentReadResult';
2
- export declare function buildDocumentName(rawFirst: string, rawLast: string, issuingState: string | null | undefined, dg11First?: string | null, dg11Last?: string | null): DocumentName;
2
+ import type { VizNameCandidate } from './vizName';
3
+ export declare function buildDocumentName(rawFirst: string, rawLast: string, issuingState: string | null | undefined, dg11First?: string | null, dg11Last?: string | null, viz?: VizNameCandidate | null): DocumentName;
3
4
  //# sourceMappingURL=mrzTransliteration.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"mrzTransliteration.d.ts","sourceRoot":"","sources":["../../../../../src/Shared/Libs/mrzTransliteration.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAyFhE,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,YAAY,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACvC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,EACzB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,GACvB,YAAY,CAgDd"}
1
+ {"version":3,"file":"mrzTransliteration.d.ts","sourceRoot":"","sources":["../../../../../src/Shared/Libs/mrzTransliteration.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAChE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAgHlD,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,YAAY,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACvC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,EACzB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,EACxB,GAAG,CAAC,EAAE,gBAAgB,GAAG,IAAI,GAC5B,YAAY,CA0Ed"}
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Visual Inspection Zone (VIZ) printed-name recovery.
3
+ *
4
+ * The MRZ stores names in ASCII OCR-B with no diacritics, and for some scripts
5
+ * the diacritics are unrecoverable from the MRZ alone — Turkish in particular
6
+ * collapses Ş→S, Ğ→G, İ→I, Ç→C, Ö→O, Ü→U with no ICAO digraph, so "ŞİMŞEK"
7
+ * reads as "SIMSEK" and the true form is lost. The chip's DG11 carries the
8
+ * printed name in UTF-8, but it is optional and often absent.
9
+ *
10
+ * The printed name IS, however, on the document's data page in its native form.
11
+ * ML Kit already recognises every text block on the page (with bounding boxes)
12
+ * for MRZ parsing; this module reuses those blocks to recover the diacritic
13
+ * printed name and reconcile it against the MRZ.
14
+ *
15
+ * Conflict policy (per product decision): the MRZ name stays authoritative. We
16
+ * accept a VIZ candidate ONLY when it folds to the same ASCII as the MRZ name
17
+ * (i.e. it is the same name, just with diacritics restored). When the VIZ name
18
+ * differs at the letter level — a possible OCR misread or a genuine discrepancy
19
+ * — we keep the MRZ value and raise a `nameMismatch` flag for operator review
20
+ * rather than silently overwriting the machine-read name.
21
+ */
22
+ /** One recognised text block, in the shape ML Kit returns to JS. */
23
+ export interface VizTextBlock {
24
+ /** Full recognised text of the block (may contain multiple lines). */
25
+ text: string;
26
+ /** Vertical position of the block's top edge, in image pixels. */
27
+ top: number;
28
+ }
29
+ /**
30
+ * Run a single full-frame OCR pass over the captured document image and return
31
+ * its text blocks.
32
+ *
33
+ * The live camera path OCRs only a cropped MRZ region (tuned for MRZ sharpness),
34
+ * so the printed name above it is never recognised there. This one-shot pass on
35
+ * the FULL captured image surfaces the printed VIZ name for {@link recoverVizName}.
36
+ * It is best-effort: returns [] when the native module is missing, the image is
37
+ * absent, or OCR throws — the caller then simply falls back to DG11/MRZ.
38
+ *
39
+ * @param base64Image Full document image (no data-URI prefix), or null/undefined.
40
+ */
41
+ export declare function recognizeVizBlocks(base64Image: string | null | undefined): Promise<VizTextBlock[]>;
42
+ export interface VizNameCandidate {
43
+ /** Diacritic given name recovered from the VIZ, or null when none matched. */
44
+ vizFirst: string | null;
45
+ /** Diacritic surname recovered from the VIZ, or null when none matched. */
46
+ vizLast: string | null;
47
+ /**
48
+ * True when a printed name line was found but its folded ASCII does not match
49
+ * the MRZ name — the candidate is NOT applied; the caller should flag review.
50
+ */
51
+ mismatch: boolean;
52
+ }
53
+ /**
54
+ * Fold a string to bare ASCII upper-case letters for comparison: strip
55
+ * combining marks via NFD, map the handful of letters that have no combining
56
+ * decomposition (ı, ł, ø, đ, ð, ß, æ, œ, þ), drop everything that is not A–Z.
57
+ * This is the canonical form in which an MRZ name and its printed counterpart
58
+ * become directly comparable.
59
+ */
60
+ export declare function foldAscii(value: string | null | undefined): string;
61
+ /**
62
+ * Recover the diacritic printed name from VIZ text blocks and reconcile it with
63
+ * the MRZ-derived raw names.
64
+ *
65
+ * @param blocks Recognised text blocks (text + vertical position).
66
+ * @param mrzFirst Raw MRZ given name (ASCII, may be empty).
67
+ * @param mrzLast Raw MRZ surname (ASCII, may be empty).
68
+ * @param mrzTop Vertical position of the MRZ block, when known. Name fields
69
+ * sit ABOVE the MRZ on the data page; passing this restricts the
70
+ * search to blocks above it, avoiding false matches in the MRZ
71
+ * itself. When undefined, all blocks are searched.
72
+ */
73
+ export declare function recoverVizName(blocks: VizTextBlock[], mrzFirst: string, mrzLast: string, mrzTop?: number): VizNameCandidate;
74
+ //# sourceMappingURL=vizName.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vizName.d.ts","sourceRoot":"","sources":["../../../../../src/Shared/Libs/vizName.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAIH,oEAAoE;AACpE,MAAM,WAAW,YAAY;IAC3B,sEAAsE;IACtE,IAAI,EAAE,MAAM,CAAC;IACb,kEAAkE;IAClE,GAAG,EAAE,MAAM,CAAC;CACb;AAWD;;;;;;;;;;;GAWG;AACH,wBAAsB,kBAAkB,CACtC,WAAW,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GACrC,OAAO,CAAC,YAAY,EAAE,CAAC,CAczB;AAED,MAAM,WAAW,gBAAgB;IAC/B,8EAA8E;IAC9E,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,2EAA2E;IAC3E,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB;;;OAGG;IACH,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAiBlE;AA4CD;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,CAC5B,MAAM,EAAE,YAAY,EAAE,EACtB,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,MAAM,CAAC,EAAE,MAAM,GACd,gBAAgB,CAuBlB"}
@@ -3,11 +3,12 @@ import type { ChipVizMatchResult } from '../Libs/chipVizMatch';
3
3
  /**
4
4
  * How the display name was derived from the raw MRZ ASCII name.
5
5
  *
6
+ * - `viz_ocr` — recovered from the printed VIZ via OCR (exact printed name with diacritics)
6
7
  * - `dg11` — read from the chip's DG11 supplemental data file (exact printed name)
7
8
  * - `reverse_table` — reconstructed via country-aware ICAO 9303 reverse transliteration table
8
9
  * - `raw` — no conversion applied; display equals the raw MRZ value
9
10
  */
10
- export type DocumentNameSource = 'dg11' | 'reverse_table' | 'raw';
11
+ export type DocumentNameSource = 'viz_ocr' | 'dg11' | 'reverse_table' | 'raw';
11
12
  export interface DocumentName {
12
13
  /** First name exactly as stored in the MRZ (ASCII, OCR-B, e.g. "GOEKHAN"). */
13
14
  rawFirst: string;
@@ -19,6 +20,12 @@ export interface DocumentName {
19
20
  displayLast: string;
20
21
  /** How displayFirst / displayLast were derived. */
21
22
  source: DocumentNameSource;
23
+ /**
24
+ * True when a printed VIZ name was detected but disagreed with the MRZ name
25
+ * at the letter level (beyond diacritics). The MRZ value is kept as the
26
+ * display name; this flag signals the discrepancy for operator review.
27
+ */
28
+ nameMismatch?: boolean;
22
29
  }
23
30
  export interface DocumentFaceImage {
24
31
  /** Base64-encoded image data (no data-URI prefix). */
@@ -1 +1 @@
1
- {"version":3,"file":"documentReadResult.d.ts","sourceRoot":"","sources":["../../../../../src/Shared/Types/documentReadResult.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACnD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAE/D;;;;;;GAMG;AACH,MAAM,MAAM,kBAAkB,GAAG,MAAM,GAAG,eAAe,GAAG,KAAK,CAAC;AAElE,MAAM,WAAW,YAAY;IAC3B,8EAA8E;IAC9E,QAAQ,EAAE,MAAM,CAAC;IACjB,8EAA8E;IAC9E,OAAO,EAAE,MAAM,CAAC;IAChB,iEAAiE;IACjE,YAAY,EAAE,MAAM,CAAC;IACrB,gEAAgE;IAChE,WAAW,EAAE,MAAM,CAAC;IACpB,mDAAmD;IACnD,MAAM,EAAE,kBAAkB,CAAC;CAC5B;AAED,MAAM,WAAW,iBAAiB;IAChC,sDAAsD;IACtD,IAAI,EAAE,MAAM,CAAC;IACb,mDAAmD;IACnD,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,kBAAkB;IACjC,mFAAmF;IACnF,QAAQ,EAAE,YAAY,CAAC;IACvB;;;;OAIG;IACH,IAAI,EAAE,YAAY,CAAC;IACnB;;;;OAIG;IACH,IAAI,CAAC,EAAE,iBAAiB,CAAC;IACzB;;;;;OAKG;IACH,YAAY,CAAC,EAAE,kBAAkB,CAAC;IAClC;;;;;OAKG;IACH,WAAW,CAAC,EAAE;QACZ,eAAe,EAAE,OAAO,GAAG,UAAU,GAAG,YAAY,GAAG,IAAI,CAAC;QAC5D,qEAAqE;QACrE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,CAAC;CACH"}
1
+ {"version":3,"file":"documentReadResult.d.ts","sourceRoot":"","sources":["../../../../../src/Shared/Types/documentReadResult.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACnD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAE/D;;;;;;;GAOG;AACH,MAAM,MAAM,kBAAkB,GAAG,SAAS,GAAG,MAAM,GAAG,eAAe,GAAG,KAAK,CAAC;AAE9E,MAAM,WAAW,YAAY;IAC3B,8EAA8E;IAC9E,QAAQ,EAAE,MAAM,CAAC;IACjB,8EAA8E;IAC9E,OAAO,EAAE,MAAM,CAAC;IAChB,iEAAiE;IACjE,YAAY,EAAE,MAAM,CAAC;IACrB,gEAAgE;IAChE,WAAW,EAAE,MAAM,CAAC;IACpB,mDAAmD;IACnD,MAAM,EAAE,kBAAkB,CAAC;IAC3B;;;;OAIG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,iBAAiB;IAChC,sDAAsD;IACtD,IAAI,EAAE,MAAM,CAAC;IACb,mDAAmD;IACnD,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,kBAAkB;IACjC,mFAAmF;IACnF,QAAQ,EAAE,YAAY,CAAC;IACvB;;;;OAIG;IACH,IAAI,EAAE,YAAY,CAAC;IACnB;;;;OAIG;IACH,IAAI,CAAC,EAAE,iBAAiB,CAAC;IACzB;;;;;OAKG;IACH,YAAY,CAAC,EAAE,kBAAkB,CAAC;IAClC;;;;;OAKG;IACH,WAAW,CAAC,EAAE;QACZ,eAAe,EAAE,OAAO,GAAG,UAAU,GAAG,YAAY,GAAG,IAAI,CAAC;QAC5D,qEAAqE;QACrE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,CAAC;CACH"}
@@ -1,2 +1,2 @@
1
- export declare const SDK_VERSION = "1.503.2";
1
+ export declare const SDK_VERSION = "1.505.0";
2
2
  //# sourceMappingURL=version.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trustchex/react-native-sdk",
3
- "version": "1.503.2",
3
+ "version": "1.505.0",
4
4
  "description": "Trustchex mobile app react native SDK for android or ios devices",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",
@@ -116,6 +116,16 @@ const IdentityDocumentEIDScanningScreen = () => {
116
116
  idFrontSideData?.mrzFields ??
117
117
  appContext.identificationInfo.scannedDocument?.mrzFields
118
118
  }
119
+ vizImage={
120
+ // Full data-page image from the EID step's camera scan. EIDScanner
121
+ // runs a one-shot OCR pass over it to recover the diacritic printed
122
+ // name when the chip's DG11 is absent or one-sided (e.g. Turkish
123
+ // passports). The live camera only OCRs a cropped MRZ region, so the
124
+ // printed name must be recovered from the full image here.
125
+ passportData?.image ??
126
+ idFrontSideData?.image ??
127
+ idBackSideData?.image
128
+ }
119
129
  onScanSuccess={(mrzFields, faceImage, mimeType) => {
120
130
  if (mrzFields && faceImage && mimeType) {
121
131
  if (!appContext.identificationInfo.scannedDocument) {
@@ -2,6 +2,8 @@ import React, { useContext, useEffect, useState } from 'react';
2
2
 
3
3
  import type { MRZFields } from '../../Shared/Types/mrzFields';
4
4
  import { buildDocumentName } from '../../Shared/Libs/mrzTransliteration';
5
+ import { recoverVizName, recognizeVizBlocks } from '../../Shared/Libs/vizName';
6
+ import { debugLog } from '../../Shared/Libs/debug.utils';
5
7
  import { isCountryAllowed } from '../../Shared/Libs/country-allow';
6
8
  import { normalizeFromMRZFields } from '../../Shared/Libs/documentDataNormalizer';
7
9
  import { Alert, SafeAreaView, StyleSheet, View } from 'react-native';
@@ -134,13 +136,41 @@ const IdentityDocumentScanningScreen = () => {
134
136
  true
135
137
  );
136
138
 
137
- {
139
+ void (async () => {
138
140
  const documentData = normalizeFromMRZFields(mrzFields);
139
141
  const faceImg = idFrontSideData?.faceImage || passportData?.faceImage;
142
+ // Recover the printed (VIZ) name with diacritics the MRZ cannot carry.
143
+ // The live camera OCRs only a cropped MRZ region, so the printed name was
144
+ // never recognised there — run a one-shot full-frame OCR pass on the
145
+ // captured data-page image to surface it. The MRZ sits at the bottom, so
146
+ // restrict the name search to blocks above it.
147
+ const docImage = (idFrontSideData?.image ||
148
+ passportData?.image) as string | undefined;
149
+ const vizBlocks = await recognizeVizBlocks(docImage);
150
+ const mrzTop = vizBlocks.length
151
+ ? Math.max(...vizBlocks.map((b) => b.top))
152
+ : undefined;
153
+ const viz = vizBlocks.length
154
+ ? recoverVizName(
155
+ vizBlocks,
156
+ documentData.firstName,
157
+ documentData.lastName,
158
+ mrzTop
159
+ )
160
+ : null;
161
+ debugLog('VIZName', 'ocr recovery', {
162
+ vizBlockCount: vizBlocks.length,
163
+ matchedFirst: !!viz?.vizFirst,
164
+ matchedLast: !!viz?.vizLast,
165
+ mismatch: viz?.mismatch ?? false,
166
+ });
140
167
  const builtName = buildDocumentName(
141
168
  documentData.firstName,
142
169
  documentData.lastName,
143
- documentData.issuingCountry
170
+ documentData.issuingCountry,
171
+ undefined,
172
+ undefined,
173
+ viz
144
174
  );
145
175
  const documentReadResult = {
146
176
  document: {
@@ -153,7 +183,7 @@ const IdentityDocumentScanningScreen = () => {
153
183
  };
154
184
  appContext.setLastDocumentRead?.(documentReadResult);
155
185
  appContext.onDocumentRead?.(documentReadResult);
156
- }
186
+ })();
157
187
 
158
188
  setTimeout(
159
189
  () => navigationManagerRef.current?.navigateToNextStep(),
@@ -13,12 +13,13 @@ import NFCManager from 'react-native-nfc-manager';
13
13
  import DeviceInfo from 'react-native-device-info';
14
14
  import PermissionManager from '../Libs/permissions.utils';
15
15
  import mrzUtils from '../Libs/mrz.utils';
16
- import { debugError } from '../Libs/debug.utils';
16
+ import { debugError, debugLog } from '../Libs/debug.utils';
17
17
  import { MRZInfo } from '../EIDReader/lds/icao/mrzInfo';
18
18
  import { eidReader } from '../EIDReader/eidReader';
19
19
  import NativeProgressBar from './NativeProgressBar';
20
20
  import type { MRZFields } from '../Types/mrzFields';
21
21
  import { buildDocumentName } from '../Libs/mrzTransliteration';
22
+ import { recoverVizName, recognizeVizBlocks } from '../Libs/vizName';
22
23
  import type { DocumentName } from '../Types/documentReadResult';
23
24
  import {
24
25
  normalizeFromMRZInfo,
@@ -57,6 +58,12 @@ interface eIDScannerProps {
57
58
  * because scannedDocument.mrzFields is only persisted after the chip read.
58
59
  */
59
60
  vizMrzFields?: MRZFields;
61
+ /**
62
+ * Full data-page image from the EID step's camera scan. A one-shot OCR pass
63
+ * recovers the diacritic printed name when the chip's DG11 is absent or
64
+ * one-sided — DG11 (the authenticated chip's own name) still takes precedence.
65
+ */
66
+ vizImage?: string;
60
67
  onScanSuccess?: (
61
68
  mrzInfo: MRZFields,
62
69
  faceImage: string,
@@ -71,6 +78,7 @@ const EIDScanner = ({
71
78
  dateOfExpiry,
72
79
  documentType,
73
80
  vizMrzFields,
81
+ vizImage,
74
82
  onScanSuccess,
75
83
  isNFCSupported,
76
84
  }: eIDScannerProps) => {
@@ -337,12 +345,40 @@ const EIDScanner = ({
337
345
  Vibration.vibrate(120);
338
346
  const scanDuration = Date.now() - startTime;
339
347
  const documentData = normalizeFromMRZInfo(passportData.mrz);
348
+ // DG11 (the authenticated chip's own printed name) is the preferred
349
+ // correction. When it is absent or one-sided, fall back to recovering
350
+ // the diacritic printed name from the EID step's VIZ camera blocks —
351
+ // this is what fixes e.g. Turkish passports whose DG11 has no usable
352
+ // name. buildDocumentName ranks viz_ocr above dg11, so only pass the
353
+ // VIZ candidate when DG11 did NOT provide a clean two-part name.
354
+ const dg11HasBothNames =
355
+ !!passportData.dg11FirstName && !!passportData.dg11LastName;
356
+ // Only OCR the VIZ when DG11 didn't already provide a clean name.
357
+ const vizBlocks = !dg11HasBothNames
358
+ ? await recognizeVizBlocks(vizImage)
359
+ : [];
360
+ const viz = vizBlocks.length
361
+ ? recoverVizName(
362
+ vizBlocks,
363
+ documentData.firstName,
364
+ documentData.lastName,
365
+ Math.max(...vizBlocks.map((b) => b.top))
366
+ )
367
+ : null;
368
+ debugLog('VIZName', 'eid recovery', {
369
+ dg11HasBothNames,
370
+ vizBlockCount: vizBlocks.length,
371
+ matchedFirst: !!viz?.vizFirst,
372
+ matchedLast: !!viz?.vizLast,
373
+ mismatch: viz?.mismatch ?? false,
374
+ });
340
375
  const builtName = buildDocumentName(
341
376
  documentData.firstName,
342
377
  documentData.lastName,
343
378
  documentData.issuingCountry,
344
379
  passportData.dg11FirstName,
345
- passportData.dg11LastName
380
+ passportData.dg11LastName,
381
+ viz
346
382
  );
347
383
  // MASAK Tebliğ No. 32 Article 4/C(1)(b): the chip identity data must
348
384
  // match the data printed on the passport. The printed/VIZ MRZ is the
@@ -732,8 +732,16 @@ const eidReader = async (
732
732
  dg11FirstName = dg11File.getFirstName();
733
733
  dg11LastName = dg11File.getLastName();
734
734
  dg11Address = dg11File.getAddress();
735
- } catch {
735
+ debugLog('EIDReader', 'DG11 read', {
736
+ firstName: dg11FirstName,
737
+ lastName: dg11LastName,
738
+ hasAddress: !!dg11Address,
739
+ });
740
+ } catch (e) {
736
741
  // DG11 is optional — not all chips have it, and access may be denied
742
+ debugLog('EIDReader', 'DG11 not available', {
743
+ error: e instanceof Error ? e.message : String(e),
744
+ });
737
745
  }
738
746
 
739
747
  // Read DG2 (Face Image)
@@ -1,4 +1,5 @@
1
1
  import type { DocumentName } from '../Types/documentReadResult';
2
+ import type { VizNameCandidate } from './vizName';
2
3
 
3
4
  type DigraphMap = [string, string][];
4
5
 
@@ -47,17 +48,40 @@ const ICELANDIC: DigraphMap = [
47
48
  ['D', 'Ð'],
48
49
  ];
49
50
 
51
+ // Finnish/Estonian share the Swedish-style AE→Ä, OE→Ö pair (no Å in names is
52
+ // common, but AA→Å is safe to include for Finnish-Swedish surnames).
53
+ const FINNISH: DigraphMap = [
54
+ ['AE', 'Ä'],
55
+ ['OE', 'Ö'],
56
+ ['AA', 'Å'],
57
+ ];
58
+
59
+ const ESTONIAN: DigraphMap = [
60
+ ['AE', 'Ä'],
61
+ ['OE', 'Ö'],
62
+ ['UE', 'Ü'],
63
+ ];
64
+
50
65
  const COUNTRY_MAP: Record<string, DigraphMap> = {
51
66
  DEU: GERMAN,
52
67
  AUT: GERMAN,
53
68
  CHE: GERMAN,
69
+ LIE: GERMAN,
54
70
  SWE: SWEDISH,
55
71
  DNK: DANISH,
56
72
  NOR: NORWEGIAN,
57
73
  FRA: FRENCH,
58
74
  LUX: FRENCH,
75
+ BEL: FRENCH,
59
76
  ISL: ICELANDIC,
77
+ FIN: FINNISH,
78
+ EST: ESTONIAN,
60
79
  };
80
+ // NOTE: Turkish (TUR), Hungarian, Polish, Czech, Slovak, etc. are intentionally
81
+ // absent. Their diacritics (Ş Ğ İ Ç, ő ű, ł, ř ž, ą ę) collapse to bare ASCII in
82
+ // the MRZ with NO digraph, so they cannot be reconstructed from the MRZ at all.
83
+ // For those documents the only correct source is the printed VIZ (viz_ocr) or
84
+ // the chip's DG11 — see buildDocumentName's priority order.
61
85
 
62
86
  function applyDigraphMap(token: string, map: DigraphMap): string {
63
87
  let result = '';
@@ -92,8 +116,32 @@ export function buildDocumentName(
92
116
  rawLast: string,
93
117
  issuingState: string | null | undefined,
94
118
  dg11First?: string | null,
95
- dg11Last?: string | null
119
+ dg11Last?: string | null,
120
+ viz?: VizNameCandidate | null
96
121
  ): DocumentName {
122
+ // Highest priority: the printed VIZ name recovered via OCR. This is the only
123
+ // source that can restore diacritics the MRZ cannot represent (e.g. Turkish
124
+ // Ş Ğ İ Ç) when the chip's DG11 is absent. `recoverVizName` only returns a
125
+ // candidate that folds to the SAME ASCII as the MRZ name, so accepting it
126
+ // restores diacritics without changing the letters. A letter-level
127
+ // discrepancy comes back as `mismatch` (no candidate applied) and is surfaced
128
+ // via `nameMismatch` for operator review — the MRZ name stays authoritative.
129
+ if (viz && (viz.vizFirst || viz.vizLast)) {
130
+ return {
131
+ rawFirst,
132
+ rawLast,
133
+ displayFirst: viz.vizFirst ?? rawFirst,
134
+ displayLast: viz.vizLast ?? rawLast,
135
+ source: 'viz_ocr',
136
+ nameMismatch: viz.mismatch || undefined,
137
+ };
138
+ }
139
+
140
+ // A VIZ name was detected but disagreed with the MRZ at the letter level and
141
+ // could not be applied. The MRZ stays authoritative through the branches
142
+ // below, but we carry the review flag through so the discrepancy is recorded.
143
+ const nameMismatch = viz?.mismatch ? true : undefined;
144
+
97
145
  // Prefer DG11 (the chip's printed name in UTF-8) ONLY when it carries a proper
98
146
  // two-part split — both a surname AND a given name. That is the authoritative
99
147
  // correction for chips whose DG1 MRZ name lacks a `<<` separator (e.g. Turkish
@@ -114,6 +162,7 @@ export function buildDocumentName(
114
162
  displayFirst: dg11First as string,
115
163
  displayLast: dg11Last as string,
116
164
  source: 'dg11',
165
+ nameMismatch,
117
166
  };
118
167
  }
119
168
 
@@ -127,6 +176,7 @@ export function buildDocumentName(
127
176
  displayFirst: convertName(rawFirst, map),
128
177
  displayLast: convertName(rawLast, map),
129
178
  source: 'reverse_table',
179
+ nameMismatch,
130
180
  };
131
181
  }
132
182
 
@@ -140,5 +190,6 @@ export function buildDocumentName(
140
190
  displayFirst,
141
191
  displayLast,
142
192
  source: changed ? 'reverse_table' : 'raw',
193
+ nameMismatch,
143
194
  };
144
195
  }
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Visual Inspection Zone (VIZ) printed-name recovery.
3
+ *
4
+ * The MRZ stores names in ASCII OCR-B with no diacritics, and for some scripts
5
+ * the diacritics are unrecoverable from the MRZ alone — Turkish in particular
6
+ * collapses Ş→S, Ğ→G, İ→I, Ç→C, Ö→O, Ü→U with no ICAO digraph, so "ŞİMŞEK"
7
+ * reads as "SIMSEK" and the true form is lost. The chip's DG11 carries the
8
+ * printed name in UTF-8, but it is optional and often absent.
9
+ *
10
+ * The printed name IS, however, on the document's data page in its native form.
11
+ * ML Kit already recognises every text block on the page (with bounding boxes)
12
+ * for MRZ parsing; this module reuses those blocks to recover the diacritic
13
+ * printed name and reconcile it against the MRZ.
14
+ *
15
+ * Conflict policy (per product decision): the MRZ name stays authoritative. We
16
+ * accept a VIZ candidate ONLY when it folds to the same ASCII as the MRZ name
17
+ * (i.e. it is the same name, just with diacritics restored). When the VIZ name
18
+ * differs at the letter level — a possible OCR misread or a genuine discrepancy
19
+ * — we keep the MRZ value and raise a `nameMismatch` flag for operator review
20
+ * rather than silently overwriting the machine-read name.
21
+ */
22
+
23
+ import { NativeModules } from 'react-native';
24
+
25
+ /** One recognised text block, in the shape ML Kit returns to JS. */
26
+ export interface VizTextBlock {
27
+ /** Full recognised text of the block (may contain multiple lines). */
28
+ text: string;
29
+ /** Vertical position of the block's top edge, in image pixels. */
30
+ top: number;
31
+ }
32
+
33
+ interface NativeTextBlock {
34
+ text?: string;
35
+ boundingBox?: { left: number; top: number; right: number; bottom: number };
36
+ }
37
+
38
+ interface MLKitNativeModule {
39
+ recognizeText: (base64Image: string) => Promise<NativeTextBlock[]>;
40
+ }
41
+
42
+ /**
43
+ * Run a single full-frame OCR pass over the captured document image and return
44
+ * its text blocks.
45
+ *
46
+ * The live camera path OCRs only a cropped MRZ region (tuned for MRZ sharpness),
47
+ * so the printed name above it is never recognised there. This one-shot pass on
48
+ * the FULL captured image surfaces the printed VIZ name for {@link recoverVizName}.
49
+ * It is best-effort: returns [] when the native module is missing, the image is
50
+ * absent, or OCR throws — the caller then simply falls back to DG11/MRZ.
51
+ *
52
+ * @param base64Image Full document image (no data-URI prefix), or null/undefined.
53
+ */
54
+ export async function recognizeVizBlocks(
55
+ base64Image: string | null | undefined
56
+ ): Promise<VizTextBlock[]> {
57
+ // Resolved per-call (not captured at import) so module-load ordering can't
58
+ // null it out, and so it is mockable in tests.
59
+ const mlkit = (NativeModules as { MLKitModule?: MLKitNativeModule })
60
+ .MLKitModule;
61
+ if (!base64Image || !mlkit?.recognizeText) return [];
62
+ try {
63
+ const blocks = await mlkit.recognizeText(base64Image);
64
+ return (blocks ?? [])
65
+ .filter((b): b is NativeTextBlock & { text: string } => !!b?.text)
66
+ .map((b) => ({ text: b.text, top: b.boundingBox?.top ?? 0 }));
67
+ } catch {
68
+ return [];
69
+ }
70
+ }
71
+
72
+ export interface VizNameCandidate {
73
+ /** Diacritic given name recovered from the VIZ, or null when none matched. */
74
+ vizFirst: string | null;
75
+ /** Diacritic surname recovered from the VIZ, or null when none matched. */
76
+ vizLast: string | null;
77
+ /**
78
+ * True when a printed name line was found but its folded ASCII does not match
79
+ * the MRZ name — the candidate is NOT applied; the caller should flag review.
80
+ */
81
+ mismatch: boolean;
82
+ }
83
+
84
+ /**
85
+ * Fold a string to bare ASCII upper-case letters for comparison: strip
86
+ * combining marks via NFD, map the handful of letters that have no combining
87
+ * decomposition (ı, ł, ø, đ, ð, ß, æ, œ, þ), drop everything that is not A–Z.
88
+ * This is the canonical form in which an MRZ name and its printed counterpart
89
+ * become directly comparable.
90
+ */
91
+ export function foldAscii(value: string | null | undefined): string {
92
+ if (!value) return '';
93
+ return value
94
+ .normalize('NFD')
95
+ .replace(/[̀-ͯ]/g, '') // combining diacritical marks
96
+ .replace(/ı/g, 'i')
97
+ .replace(/İ/g, 'I')
98
+ .replace(/ø/gi, 'O')
99
+ .replace(/ł/gi, 'L')
100
+ .replace(/đ/gi, 'D')
101
+ .replace(/ð/gi, 'D')
102
+ .replace(/þ/gi, 'TH')
103
+ .replace(/ß/g, 'SS')
104
+ .replace(/æ/gi, 'AE')
105
+ .replace(/œ/gi, 'OE')
106
+ .toUpperCase()
107
+ .replace(/[^A-Z]/g, '');
108
+ }
109
+
110
+ /** Split a printed-name line into upper-case word tokens (letters + marks). */
111
+ function tokenize(line: string): string[] {
112
+ return line
113
+ .split(/[\s,]+/)
114
+ .map((w) => w.trim())
115
+ .filter((w) => w.length > 0 && /\p{L}/u.test(w))
116
+ .map((w) => w.toUpperCase());
117
+ }
118
+
119
+ /**
120
+ * Find the printed token sequence whose folded ASCII equals the folded MRZ name.
121
+ * Returns the diacritic-preserving printed form when found, else null.
122
+ *
123
+ * The candidate lines are searched in document order; the matched window is the
124
+ * shortest run of consecutive tokens whose concatenated fold equals the target.
125
+ * Because the comparison is on the FOLDED form, only an exact same-name match
126
+ * succeeds — a different printed name will not fold-match and yields null.
127
+ */
128
+ function recoverPrintedName(lines: string[], mrzName: string): string | null {
129
+ const target = foldAscii(mrzName);
130
+ if (!target) return null;
131
+
132
+ for (const line of lines) {
133
+ const tokens = tokenize(line);
134
+ // Slide a window over the tokens; the printed name may sit among other
135
+ // label text on the same block ("Surname / Nom ÖZTÜRK").
136
+ for (let start = 0; start < tokens.length; start++) {
137
+ let acc = '';
138
+ const picked: string[] = [];
139
+ for (let end = start; end < tokens.length; end++) {
140
+ acc += foldAscii(tokens[end]);
141
+ picked.push(tokens[end]);
142
+ if (acc.length > target.length) break;
143
+ if (acc === target) {
144
+ return picked.join(' ');
145
+ }
146
+ }
147
+ }
148
+ }
149
+ return null;
150
+ }
151
+
152
+ /**
153
+ * Recover the diacritic printed name from VIZ text blocks and reconcile it with
154
+ * the MRZ-derived raw names.
155
+ *
156
+ * @param blocks Recognised text blocks (text + vertical position).
157
+ * @param mrzFirst Raw MRZ given name (ASCII, may be empty).
158
+ * @param mrzLast Raw MRZ surname (ASCII, may be empty).
159
+ * @param mrzTop Vertical position of the MRZ block, when known. Name fields
160
+ * sit ABOVE the MRZ on the data page; passing this restricts the
161
+ * search to blocks above it, avoiding false matches in the MRZ
162
+ * itself. When undefined, all blocks are searched.
163
+ */
164
+ export function recoverVizName(
165
+ blocks: VizTextBlock[],
166
+ mrzFirst: string,
167
+ mrzLast: string,
168
+ mrzTop?: number
169
+ ): VizNameCandidate {
170
+ const above =
171
+ typeof mrzTop === 'number'
172
+ ? blocks.filter((b) => b.top < mrzTop)
173
+ : blocks;
174
+ // Each block's text may hold several printed lines.
175
+ const lines = above.flatMap((b) => b.text.split(/\r?\n/));
176
+
177
+ const vizFirst = mrzFirst ? recoverPrintedName(lines, mrzFirst) : null;
178
+ const vizLast = mrzLast ? recoverPrintedName(lines, mrzLast) : null;
179
+
180
+ // Detect a letter-level discrepancy: a printed line clearly carries the name
181
+ // field but its fold does not equal the MRZ name. We approximate this by
182
+ // checking whether ANY line fold-matches; if the MRZ name is non-trivial and
183
+ // nothing matched, the printed name on the page disagrees with the MRZ.
184
+ const lastMissing = !!mrzLast && vizLast === null;
185
+ const firstMissing = !!mrzFirst && vizFirst === null;
186
+ // Only call it a mismatch when there is printed name-like text to compare
187
+ // against — otherwise the VIZ simply was not captured (not a conflict).
188
+ const hasPrintedNameText = lines.some((l) => tokenize(l).length > 0);
189
+ const mismatch = hasPrintedNameText && (lastMissing || firstMissing);
190
+
191
+ return { vizFirst, vizLast, mismatch };
192
+ }
@@ -4,11 +4,12 @@ import type { ChipVizMatchResult } from '../Libs/chipVizMatch';
4
4
  /**
5
5
  * How the display name was derived from the raw MRZ ASCII name.
6
6
  *
7
+ * - `viz_ocr` — recovered from the printed VIZ via OCR (exact printed name with diacritics)
7
8
  * - `dg11` — read from the chip's DG11 supplemental data file (exact printed name)
8
9
  * - `reverse_table` — reconstructed via country-aware ICAO 9303 reverse transliteration table
9
10
  * - `raw` — no conversion applied; display equals the raw MRZ value
10
11
  */
11
- export type DocumentNameSource = 'dg11' | 'reverse_table' | 'raw';
12
+ export type DocumentNameSource = 'viz_ocr' | 'dg11' | 'reverse_table' | 'raw';
12
13
 
13
14
  export interface DocumentName {
14
15
  /** First name exactly as stored in the MRZ (ASCII, OCR-B, e.g. "GOEKHAN"). */
@@ -21,6 +22,12 @@ export interface DocumentName {
21
22
  displayLast: string;
22
23
  /** How displayFirst / displayLast were derived. */
23
24
  source: DocumentNameSource;
25
+ /**
26
+ * True when a printed VIZ name was detected but disagreed with the MRZ name
27
+ * at the letter level (beyond diacritics). The MRZ value is kept as the
28
+ * display name; this flag signals the discrepancy for operator review.
29
+ */
30
+ nameMismatch?: boolean;
24
31
  }
25
32
 
26
33
  export interface DocumentFaceImage {
package/src/version.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  // This file is auto-generated. Do not edit manually.
2
2
  // Version is synced from package.json during build.
3
- export const SDK_VERSION = '1.503.2';
3
+ export const SDK_VERSION = '1.505.0';