@trustchex/react-native-sdk 1.478.7 → 1.483.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/src/main/java/com/trustchex/reactnativesdk/camera/TrustchexCameraView.kt +57 -3
- package/ios/Camera/TrustchexCameraView.swift +20 -15
- package/lib/module/Screens/Dynamic/IdentityDocumentEIDScanningScreen.js +0 -3
- package/lib/module/Screens/Dynamic/IdentityDocumentScanningScreen.js +21 -0
- package/lib/module/Screens/Dynamic/VideoCallScreen.js +1 -1
- package/lib/module/Screens/Static/ResultScreen.js +75 -16
- package/lib/module/Shared/Components/DiagnosticReportButton.js +64 -0
- package/lib/module/Shared/Components/EIDScanner.js +52 -19
- package/lib/module/Shared/Components/IdentityDocumentCamera.js +132 -8
- package/lib/module/Shared/Components/NavigationManager.js +7 -9
- package/lib/module/Shared/Contexts/AppContext.js +3 -0
- package/lib/module/Shared/EIDReader/eidReader.js +167 -30
- package/lib/module/Shared/EIDReader/lds/icao/dg11File.js +77 -0
- package/lib/module/Shared/Libs/MRZ_KNOWN_ISSUES.md +7 -0
- package/lib/module/Shared/Libs/diagnosticReport.js +133 -0
- package/lib/module/Shared/Libs/diagnostics.js +171 -0
- package/lib/module/Shared/Libs/documentDataNormalizer.js +104 -0
- package/lib/module/Shared/Libs/mrz.utils.js +0 -5
- package/lib/module/Shared/Libs/mrzFrameAggregator.js +32 -5
- package/lib/module/Shared/Libs/mrzTransliteration.js +80 -0
- package/lib/module/Shared/Libs/native-device-info.utils.js +67 -0
- package/lib/module/Shared/Libs/sendDiagnosticReport.js +130 -0
- package/lib/module/Shared/Services/DataUploadService.js +6 -3
- package/lib/module/Shared/Types/documentData.js +1 -0
- package/lib/module/Shared/Types/documentReadResult.js +3 -0
- package/lib/module/Translation/Resources/en.js +5 -0
- package/lib/module/Translation/Resources/tr.js +5 -0
- package/lib/module/Trustchex.js +31 -4
- package/lib/module/version.js +1 -1
- package/lib/typescript/src/Screens/Dynamic/IdentityDocumentEIDScanningScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Dynamic/IdentityDocumentScanningScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Dynamic/VideoCallScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Static/ResultScreen.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/DiagnosticReportButton.d.ts +20 -0
- package/lib/typescript/src/Shared/Components/DiagnosticReportButton.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Components/EIDScanner.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.types.d.ts +10 -0
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.types.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/NavigationManager.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/TrustchexCamera.d.ts +10 -0
- package/lib/typescript/src/Shared/Components/TrustchexCamera.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Contexts/AppContext.d.ts +5 -0
- package/lib/typescript/src/Shared/Contexts/AppContext.d.ts.map +1 -1
- package/lib/typescript/src/Shared/EIDReader/eidReader.d.ts +2 -0
- package/lib/typescript/src/Shared/EIDReader/eidReader.d.ts.map +1 -1
- package/lib/typescript/src/Shared/EIDReader/lds/icao/dg11File.d.ts +35 -0
- package/lib/typescript/src/Shared/EIDReader/lds/icao/dg11File.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Libs/diagnosticReport.d.ts +48 -0
- package/lib/typescript/src/Shared/Libs/diagnosticReport.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Libs/diagnostics.d.ts +145 -0
- package/lib/typescript/src/Shared/Libs/diagnostics.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Libs/documentDataNormalizer.d.ts +12 -0
- package/lib/typescript/src/Shared/Libs/documentDataNormalizer.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Libs/mrz.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/mrzFrameAggregator.d.ts +33 -0
- package/lib/typescript/src/Shared/Libs/mrzFrameAggregator.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/mrzTransliteration.d.ts +3 -0
- package/lib/typescript/src/Shared/Libs/mrzTransliteration.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Libs/native-device-info.utils.d.ts +33 -0
- package/lib/typescript/src/Shared/Libs/native-device-info.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/sendDiagnosticReport.d.ts +30 -0
- package/lib/typescript/src/Shared/Libs/sendDiagnosticReport.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Services/DataUploadService.d.ts +3 -2
- package/lib/typescript/src/Shared/Services/DataUploadService.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Types/documentData.d.ts +44 -0
- package/lib/typescript/src/Shared/Types/documentData.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Types/documentReadResult.d.ts +44 -0
- package/lib/typescript/src/Shared/Types/documentReadResult.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Types/mrzFields.d.ts +1 -1
- package/lib/typescript/src/Shared/Types/mrzFields.d.ts.map +1 -1
- package/lib/typescript/src/Translation/Resources/en.d.ts +5 -0
- package/lib/typescript/src/Translation/Resources/en.d.ts.map +1 -1
- package/lib/typescript/src/Translation/Resources/tr.d.ts +5 -0
- package/lib/typescript/src/Translation/Resources/tr.d.ts.map +1 -1
- package/lib/typescript/src/Trustchex.d.ts +9 -0
- package/lib/typescript/src/Trustchex.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +2 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/version.d.ts +1 -1
- package/package.json +5 -1
- package/src/Screens/Dynamic/IdentityDocumentEIDScanningScreen.tsx +0 -4
- package/src/Screens/Dynamic/IdentityDocumentScanningScreen.tsx +23 -0
- package/src/Screens/Dynamic/VideoCallScreen.tsx +5 -1
- package/src/Screens/Static/ResultScreen.tsx +85 -16
- package/src/Shared/Components/DiagnosticReportButton.tsx +77 -0
- package/src/Shared/Components/EIDScanner.tsx +67 -18
- package/src/Shared/Components/IdentityDocumentCamera.tsx +145 -7
- package/src/Shared/Components/IdentityDocumentCamera.types.ts +6 -0
- package/src/Shared/Components/NavigationManager.tsx +7 -7
- package/src/Shared/Components/TrustchexCamera.tsx +6 -0
- package/src/Shared/Contexts/AppContext.ts +8 -0
- package/src/Shared/EIDReader/eidReader.ts +193 -47
- package/src/Shared/EIDReader/lds/icao/dg11File.ts +94 -0
- package/src/Shared/Libs/MRZ_KNOWN_ISSUES.md +7 -0
- package/src/Shared/Libs/diagnosticReport.ts +206 -0
- package/src/Shared/Libs/diagnostics.ts +251 -0
- package/src/Shared/Libs/documentDataNormalizer.ts +108 -0
- package/src/Shared/Libs/mrz.utils.ts +0 -5
- package/src/Shared/Libs/mrzFrameAggregator.ts +61 -3
- package/src/Shared/Libs/mrzTransliteration.ts +131 -0
- package/src/Shared/Libs/native-device-info.utils.ts +116 -0
- package/src/Shared/Libs/sendDiagnosticReport.ts +165 -0
- package/src/Shared/Services/DataUploadService.ts +9 -3
- package/src/Shared/Types/documentData.ts +52 -0
- package/src/Shared/Types/documentReadResult.ts +47 -0
- package/src/Shared/Types/mrzFields.ts +0 -5
- package/src/Translation/Resources/en.ts +6 -0
- package/src/Translation/Resources/tr.ts +6 -0
- package/src/Trustchex.tsx +48 -3
- package/src/index.tsx +7 -0
- package/src/version.ts +1 -1
|
@@ -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
|
+
}
|
|
@@ -118,6 +118,13 @@ Covered by the "real letters in names are not eaten" tests in `mrz.utils.test.ts
|
|
|
118
118
|
30–44-char MRZ line below that floor, so the native pipeline **crops to the
|
|
119
119
|
on-screen scan frame and upscales + grayscale/contrast-enhances** before OCR.
|
|
120
120
|
The MRZ band must be reasonably filled within the on-screen guide.
|
|
121
|
+
- Two distinct regions exist in the native code (don't confuse them):
|
|
122
|
+
the **MRZ-OCR crop** (`buildScanFrameRoiImage`: top 25% / bottom 12% / sides
|
|
123
|
+
4%, upscaled ≤2.5× to ≤2200px, grayscale, contrast 1.6×) and the
|
|
124
|
+
**brightness-sampling region** (the on-screen guide: 36% / 36% / 5%). Both are
|
|
125
|
+
the same on iOS and Android. The diagnostic report records the measured MRZ
|
|
126
|
+
band pixel height so you can tell when a device leaves the band below the
|
|
127
|
+
~16 px/char floor.
|
|
121
128
|
- **Glare / lamination.** Heavy glare on a glossy laminated card still degrades
|
|
122
129
|
OCR; the enhancement helps but does not eliminate it. Steadying the card and
|
|
123
130
|
reducing direct glare improves convergence.
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Builds the support diagnostic report from a scan session.
|
|
5
|
+
*
|
|
6
|
+
* Produces three things the result screen turns into a reviewable email:
|
|
7
|
+
* - `diagnostics.json` — technical signals (device, camera, MRZ, NFC steps).
|
|
8
|
+
* - `scan-data.json` — the scanned identity data + raw MRZ (PII; the user
|
|
9
|
+
* reviews it in the draft and chooses to send).
|
|
10
|
+
* - a short, size-bounded plain-text BODY summary that always fits an email.
|
|
11
|
+
*
|
|
12
|
+
* The full detail lives in the JSON attachments; the email body is intentionally
|
|
13
|
+
* compact so the draft opens reliably on every mail client.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** ICAO 9303 7-3-1 weighted check digit (local copy; mirrors mrz.utils). */
|
|
17
|
+
const icaoCheckDigit = str => {
|
|
18
|
+
const weights = [7, 3, 1];
|
|
19
|
+
let sum = 0;
|
|
20
|
+
for (let i = 0; i < str.length; i++) {
|
|
21
|
+
const c = str[i];
|
|
22
|
+
const v = c === '<' ? 0 : c >= '0' && c <= '9' ? c.charCodeAt(0) - 48 : c.charCodeAt(0) - 55;
|
|
23
|
+
sum = (sum + weights[i % 3] * v) % 10;
|
|
24
|
+
}
|
|
25
|
+
return String(sum);
|
|
26
|
+
};
|
|
27
|
+
/** Compute per-field check-digit pass/fail for a TD1 MRZ (best-effort). */
|
|
28
|
+
const computeTd1CheckDigits = mrzText => {
|
|
29
|
+
if (!mrzText) return undefined;
|
|
30
|
+
const lines = mrzText.split('\n').map(l => l.trim()).filter(Boolean);
|
|
31
|
+
if (lines.length < 2 || lines[0].length < 30 || lines[1].length < 30) {
|
|
32
|
+
return undefined; // only TD1 handled here; other formats omitted from summary
|
|
33
|
+
}
|
|
34
|
+
const l1 = lines[0];
|
|
35
|
+
const l2 = lines[1];
|
|
36
|
+
const result = {};
|
|
37
|
+
const check = (data, printed) => icaoCheckDigit(data) === printed ? 'pass' : 'fail';
|
|
38
|
+
|
|
39
|
+
// Turkish convention: a doc number may carry a leading serial LETTER, and the
|
|
40
|
+
// doc-number check digit is computed over the DIGITS ONLY (letter dropped).
|
|
41
|
+
// The TD1 composite then also diverges from strict ICAO for such cards (no
|
|
42
|
+
// interpretation of the letter reproduces it). To avoid reporting a misleading
|
|
43
|
+
// "fail" on a card the SDK legitimately accepted, detect the letter-prefix and
|
|
44
|
+
// treat both the doc-number and composite as validated under that convention.
|
|
45
|
+
const docData = l1.slice(5, 14);
|
|
46
|
+
const docPrinted = l1[14];
|
|
47
|
+
const hasLetterPrefix = /^[A-Z]/.test(docData);
|
|
48
|
+
const strictDocOk = icaoCheckDigit(docData) === docPrinted;
|
|
49
|
+
const turkishDocOk = hasLetterPrefix && icaoCheckDigit(docData.slice(1)) === docPrinted;
|
|
50
|
+
const docOk = strictDocOk || turkishDocOk;
|
|
51
|
+
result.documentNumber = docOk ? 'pass' : 'fail';
|
|
52
|
+
result.birthDate = check(l2.slice(0, 6), l2[6]);
|
|
53
|
+
result.expiry = check(l2.slice(8, 14), l2[14]);
|
|
54
|
+
const composite = l1.slice(5, 30) + l2.slice(0, 7) + l2.slice(8, 15) + l2.slice(18, 29);
|
|
55
|
+
const compStrict = check(composite, l2[29]);
|
|
56
|
+
// If the strict composite fails ONLY because of the Turkish letter-prefix
|
|
57
|
+
// doc number (which itself passed under the digits-only rule), don't surface a
|
|
58
|
+
// misleading failure — the SDK accepts these cards.
|
|
59
|
+
result.composite = compStrict === 'fail' && hasLetterPrefix && docOk ? 'pass' : compStrict;
|
|
60
|
+
// The Turkish convention was the deciding factor only when strict ICAO would
|
|
61
|
+
// have failed the doc number but the digits-only rule passes it.
|
|
62
|
+
const turkishLetterPrefix = !strictDocOk && turkishDocOk;
|
|
63
|
+
return {
|
|
64
|
+
checkDigits: result,
|
|
65
|
+
turkishLetterPrefix
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
const MAX_BODY_BYTES = 4096;
|
|
69
|
+
const yesNo = b => b ? 'yes' : 'no';
|
|
70
|
+
|
|
71
|
+
/** Assemble the full report bundle. Pure — no I/O, no native calls. */
|
|
72
|
+
export const buildDiagnosticReport = params => {
|
|
73
|
+
const {
|
|
74
|
+
diag,
|
|
75
|
+
device,
|
|
76
|
+
scan,
|
|
77
|
+
sdkVersion,
|
|
78
|
+
sessionId
|
|
79
|
+
} = params;
|
|
80
|
+
const computed = computeTd1CheckDigits(scan.mrzText);
|
|
81
|
+
const checkDigits = diag.mrz.checkDigits ?? computed?.checkDigits;
|
|
82
|
+
// Set the Turkish-letter-prefix flag from the final accepted MRZ (the collector
|
|
83
|
+
// can't know it during scanning; the report does, from the MRZ text).
|
|
84
|
+
const turkishLetterPrefixApplied = diag.mrz.turkishLetterPrefixApplied || computed?.turkishLetterPrefix === true;
|
|
85
|
+
|
|
86
|
+
// --- diagnostics.json (technical, low-PII) ---
|
|
87
|
+
const diagnosticsObj = {
|
|
88
|
+
schemaVersion: diag.schemaVersion,
|
|
89
|
+
generatedAt: diag.generatedAt,
|
|
90
|
+
sessionId: sessionId ?? null,
|
|
91
|
+
sdk: {
|
|
92
|
+
platform: device.platform,
|
|
93
|
+
sdkVersion,
|
|
94
|
+
buildNumber: device.buildNumber
|
|
95
|
+
},
|
|
96
|
+
device,
|
|
97
|
+
scan: {
|
|
98
|
+
documentType: scan.documentType ?? diag.documentType ?? null,
|
|
99
|
+
dataSource: scan.dataSource ?? diag.dataSource ?? null,
|
|
100
|
+
totalDurationMs: diag.totalDurationMs,
|
|
101
|
+
camera: diag.camera
|
|
102
|
+
},
|
|
103
|
+
mrz: {
|
|
104
|
+
...diag.mrz,
|
|
105
|
+
checkDigits: checkDigits ?? null,
|
|
106
|
+
turkishLetterPrefixApplied
|
|
107
|
+
},
|
|
108
|
+
nfc: diag.nfc,
|
|
109
|
+
errors: diag.errors
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// --- scan-data.json (PII; user reviews before sending) ---
|
|
113
|
+
const scanDataObj = {
|
|
114
|
+
documentType: scan.documentType ?? diag.documentType ?? null,
|
|
115
|
+
dataSource: scan.dataSource ?? diag.dataSource ?? null,
|
|
116
|
+
mrzText: scan.mrzText ?? null,
|
|
117
|
+
fields: scan.mrzFields ?? null,
|
|
118
|
+
barcodeValue: scan.barcodeValue ?? null
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// --- compact email body (always under MAX_BODY_BYTES) ---
|
|
122
|
+
const nfc = diag.nfc;
|
|
123
|
+
const nfcLine = nfc.attempted ? `${nfc.authProtocol}${nfc.paceFellBackToBac ? '→BAC' : ''} · ${nfc.succeeded ? 'success' : `failed at ${nfc.failureStep ?? '?'} (${nfc.errorCategory ?? 'error'})`}` : 'not attempted';
|
|
124
|
+
const checksLine = checkDigits ? Object.entries(checkDigits).map(([k, v]) => `${k}:${v}`).join(' ') : 'n/a';
|
|
125
|
+
const body = ['TRUSTCHEX SCAN REPORT', `Device: ${device.brand} ${device.model} · ${device.systemName} ${device.systemVersion}${device.apiLevel ? ` (API ${device.apiLevel})` : ''}`, `SDK: ${sdkVersion} (build ${device.buildNumber}) · ${device.manufacturer} · ${device.deviceId}`, `Document: ${scanDataObj.documentType ?? 'unknown'} · source: ${scanDataObj.dataSource ?? 'unknown'}`, `MRZ: ${diag.mrz.reachedStable ? 'stable' : 'NOT stable'} after ${diag.mrz.framesProcessed} frames · checks: ${checksLine}`, `Camera: ${diag.camera.frames} frames · brightness ${diag.camera.avgBrightness} (low:${yesNo(diag.camera.hadLowBrightness)} blur:${yesNo(diag.camera.hadBlurryFrames)})`, `NFC: ${nfcLine}`, sessionId ? `Session: ${sessionId}` : '', '', 'Describe the issue here:', '', '', '— Full diagnostics and document images are attached. Please review before sending; do not edit the attachments. —'].filter(Boolean).join('\n');
|
|
126
|
+
const subject = `Trustchex scan report — ${scanDataObj.documentType ?? 'doc'} — ${device.brand} ${device.model} — v${sdkVersion}${sessionId ? ` — ${sessionId}` : ''}`;
|
|
127
|
+
return {
|
|
128
|
+
diagnosticsJson: JSON.stringify(diagnosticsObj, null, 2),
|
|
129
|
+
scanDataJson: JSON.stringify(scanDataObj, null, 2),
|
|
130
|
+
body: body.length > MAX_BODY_BYTES ? body.slice(0, MAX_BODY_BYTES - 1) + '…' : body,
|
|
131
|
+
subject
|
|
132
|
+
};
|
|
133
|
+
};
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Diagnostics collector for the "Report a scanning issue" feature.
|
|
5
|
+
*
|
|
6
|
+
* Accumulates a PII-light technical snapshot of a single scan session — camera
|
|
7
|
+
* quality, MRZ consensus, and (the highest-value, most device-specific part) a
|
|
8
|
+
* structured per-step NFC/eID read log. The result screen serialises this into
|
|
9
|
+
* the diagnostic email the user reviews and sends to support.
|
|
10
|
+
*
|
|
11
|
+
* Design notes:
|
|
12
|
+
* - This is a MODULE-LEVEL SINGLETON so the deep camera/EID call stack can record
|
|
13
|
+
* into it without threading a prop/context through every layer. Call `reset()`
|
|
14
|
+
* at the start of a scan flow.
|
|
15
|
+
* - It stores only TECHNICAL signals (counts, timings, brightness, APDU status
|
|
16
|
+
* words, pass/fail). It deliberately holds NO identity data, images, MRZ text,
|
|
17
|
+
* keys, or nonces — the scanned identity/images shown to the user come from the
|
|
18
|
+
* result screen's own state, not from here, and are attached separately with
|
|
19
|
+
* the user's explicit review in the email draft.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/** A single step of the NFC/eID read, with its outcome and timing. */
|
|
23
|
+
|
|
24
|
+
const emptyCamera = () => ({
|
|
25
|
+
frames: 0,
|
|
26
|
+
frameW: 0,
|
|
27
|
+
frameH: 0,
|
|
28
|
+
avgBrightness: 0,
|
|
29
|
+
minBrightness: 0,
|
|
30
|
+
maxBrightness: 0,
|
|
31
|
+
hadLowBrightness: false,
|
|
32
|
+
hadBlurryFrames: false
|
|
33
|
+
});
|
|
34
|
+
const emptyMrz = () => ({
|
|
35
|
+
framesProcessed: 0,
|
|
36
|
+
reachedStable: false,
|
|
37
|
+
stabilityFrames: 0,
|
|
38
|
+
minContestedMargin: 0,
|
|
39
|
+
turkishLetterPrefixApplied: false
|
|
40
|
+
});
|
|
41
|
+
const emptyNfc = () => ({
|
|
42
|
+
attempted: false,
|
|
43
|
+
chipDetected: false,
|
|
44
|
+
authProtocol: 'none',
|
|
45
|
+
paceFellBackToBac: false,
|
|
46
|
+
steps: [],
|
|
47
|
+
retryCount: 0,
|
|
48
|
+
succeeded: false
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Accumulates one scan session's diagnostics. A single shared instance is
|
|
53
|
+
* exported below; tests can construct their own.
|
|
54
|
+
*/
|
|
55
|
+
export class DiagnosticsCollector {
|
|
56
|
+
startedAtMs = 0;
|
|
57
|
+
camera = emptyCamera();
|
|
58
|
+
mrz = emptyMrz();
|
|
59
|
+
nfc = emptyNfc();
|
|
60
|
+
errors = [];
|
|
61
|
+
|
|
62
|
+
/** Begin a fresh session — call when a scan flow starts. */
|
|
63
|
+
reset(nowMs) {
|
|
64
|
+
this.startedAtMs = nowMs;
|
|
65
|
+
this.documentType = undefined;
|
|
66
|
+
this.dataSource = undefined;
|
|
67
|
+
this.camera = emptyCamera();
|
|
68
|
+
this.mrz = emptyMrz();
|
|
69
|
+
this.nfc = emptyNfc();
|
|
70
|
+
this.errors = [];
|
|
71
|
+
}
|
|
72
|
+
setDocument(documentType, dataSource) {
|
|
73
|
+
if (documentType) this.documentType = documentType;
|
|
74
|
+
if (dataSource) this.dataSource = dataSource;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Merge a camera snapshot (called periodically as frames arrive). */
|
|
78
|
+
setCamera(c) {
|
|
79
|
+
this.camera = {
|
|
80
|
+
...this.camera,
|
|
81
|
+
...c
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Merge MRZ consensus metrics. `reachedStable` is STICKY (once true, stays
|
|
86
|
+
* true) so a later frame's flicker can't unset that the scan did stabilise;
|
|
87
|
+
* `maxMrzBandHeightPx` keeps the largest seen. */
|
|
88
|
+
setMrz(m) {
|
|
89
|
+
const reachedStable = this.mrz.reachedStable || m.reachedStable === true;
|
|
90
|
+
const maxMrzBandHeightPx = Math.max(this.mrz.maxMrzBandHeightPx ?? 0, m.maxMrzBandHeightPx ?? 0) || undefined;
|
|
91
|
+
this.mrz = {
|
|
92
|
+
...this.mrz,
|
|
93
|
+
...m,
|
|
94
|
+
reachedStable,
|
|
95
|
+
maxMrzBandHeightPx
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ---- NFC: the structured per-step log ----
|
|
100
|
+
|
|
101
|
+
nfcAttemptStarted() {
|
|
102
|
+
// A fresh attempt: bump retry count if a previous attempt left steps.
|
|
103
|
+
if (this.nfc.attempted) this.nfc.retryCount += 1;
|
|
104
|
+
this.nfc.attempted = true;
|
|
105
|
+
}
|
|
106
|
+
nfcChipDetected() {
|
|
107
|
+
this.nfc.chipDetected = true;
|
|
108
|
+
}
|
|
109
|
+
nfcSetAuth(protocol, oid) {
|
|
110
|
+
this.nfc.authProtocol = protocol;
|
|
111
|
+
if (oid) this.nfc.paceOid = oid;
|
|
112
|
+
}
|
|
113
|
+
nfcPaceFellBackToBac(reason) {
|
|
114
|
+
this.nfc.paceFellBackToBac = true;
|
|
115
|
+
if (reason && !this.nfc.paceFallbackReason) {
|
|
116
|
+
this.nfc.paceFallbackReason = reason;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Record one completed step. Keep messages short and free of PII. */
|
|
121
|
+
nfcStep(step) {
|
|
122
|
+
this.nfc.steps.push(step);
|
|
123
|
+
if (!step.ok && !this.nfc.failureStep) {
|
|
124
|
+
this.nfc.failureStep = step.step;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
nfcResult(succeeded, errorCategory) {
|
|
128
|
+
this.nfc.succeeded = succeeded;
|
|
129
|
+
if (succeeded) {
|
|
130
|
+
// The read ultimately succeeded — transient per-attempt failures (e.g. a
|
|
131
|
+
// recovered DG2 retry, or the expected EF.CardAccess 6982 on a BAC-only
|
|
132
|
+
// card) must NOT be reported as the outcome. Clear the failure markers.
|
|
133
|
+
this.nfc.failureStep = undefined;
|
|
134
|
+
this.nfc.errorCategory = undefined;
|
|
135
|
+
} else if (errorCategory) {
|
|
136
|
+
this.nfc.errorCategory = errorCategory;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
addError(e) {
|
|
140
|
+
this.errors.push(e);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Build the serialisable snapshot. */
|
|
144
|
+
snapshot(nowMs, isoNow) {
|
|
145
|
+
return {
|
|
146
|
+
schemaVersion: 1,
|
|
147
|
+
generatedAt: isoNow,
|
|
148
|
+
documentType: this.documentType,
|
|
149
|
+
dataSource: this.dataSource,
|
|
150
|
+
totalDurationMs: this.startedAtMs ? Math.max(0, nowMs - this.startedAtMs) : 0,
|
|
151
|
+
camera: {
|
|
152
|
+
...this.camera
|
|
153
|
+
},
|
|
154
|
+
mrz: {
|
|
155
|
+
...this.mrz
|
|
156
|
+
},
|
|
157
|
+
nfc: {
|
|
158
|
+
...this.nfc,
|
|
159
|
+
steps: this.nfc.steps.map(s => ({
|
|
160
|
+
...s
|
|
161
|
+
}))
|
|
162
|
+
},
|
|
163
|
+
errors: this.errors.map(e => ({
|
|
164
|
+
...e
|
|
165
|
+
}))
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Shared session-scoped collector used across the scan flow. */
|
|
171
|
+
export const diagnostics = new DiagnosticsCollector();
|
|
@@ -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
|
};
|
|
@@ -40,6 +40,12 @@ const FORMAT_GEOMETRY = {
|
|
|
40
40
|
}
|
|
41
41
|
};
|
|
42
42
|
const MRZ_CHAR = /^[A-Z0-9<]$/;
|
|
43
|
+
/**
|
|
44
|
+
* Floor applied to a frame's confidence multiplier. A frame ML Kit was very
|
|
45
|
+
* unsure about still contributes (it may carry the only reading of a cell), but
|
|
46
|
+
* at a fraction of a confident frame's weight.
|
|
47
|
+
*/
|
|
48
|
+
const MIN_CONFIDENCE_WEIGHT = 0.25;
|
|
43
49
|
/**
|
|
44
50
|
* Reconstructs fixed-width MRZ candidate line-sets for a frame's raw OCR text.
|
|
45
51
|
* Reuses the same reconstruction the single-frame validator uses, so the
|
|
@@ -181,7 +187,12 @@ export class MRZFrameAggregator {
|
|
|
181
187
|
* consensus is returned.
|
|
182
188
|
*/
|
|
183
189
|
addFrame(input) {
|
|
184
|
-
const
|
|
190
|
+
const baseWeight = input.weight && input.weight > 0 ? input.weight : 1;
|
|
191
|
+
// Fold OCR confidence into the weight (clamped to [floor, 1]). Absent
|
|
192
|
+
// confidence leaves the weight unchanged, so callers without per-symbol
|
|
193
|
+
// confidence (e.g. iOS ML Kit) keep the prior sharpness-only behaviour.
|
|
194
|
+
const confidenceFactor = typeof input.confidence === 'number' && Number.isFinite(input.confidence) ? Math.min(1, Math.max(MIN_CONFIDENCE_WEIGHT, input.confidence)) : 1;
|
|
195
|
+
const weight = baseWeight * confidenceFactor;
|
|
185
196
|
const lines = linesForFrame(input.text);
|
|
186
197
|
if (lines) {
|
|
187
198
|
// Determine the format this frame's lines fit (by line count + width).
|
|
@@ -244,18 +255,30 @@ export class MRZFrameAggregator {
|
|
|
244
255
|
* is what turns "looks valid this frame" into "the camera agrees across frames".
|
|
245
256
|
*/
|
|
246
257
|
consensusIsConfident(format) {
|
|
258
|
+
return this.minContestedMargin(format) >= this.minMargin;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Smallest winning vote margin across all CONTESTED cells (where >1 distinct
|
|
263
|
+
* char was voted). Returns Infinity when no cell is contested. Used both to
|
|
264
|
+
* gate confidence and to surface a diagnostic of how narrowly a look-alike was
|
|
265
|
+
* resolved at acceptance.
|
|
266
|
+
*/
|
|
267
|
+
minContestedMargin(format) {
|
|
247
268
|
const geom = FORMAT_GEOMETRY[format];
|
|
248
269
|
const tally = this.tallies[format];
|
|
270
|
+
let min = Infinity;
|
|
249
271
|
for (let li = 0; li < geom.lines; li++) {
|
|
250
272
|
for (let ci = 0; ci < geom.width; ci++) {
|
|
251
273
|
const cell = tally[li][ci];
|
|
252
274
|
const weights = Object.values(cell);
|
|
253
275
|
if (weights.length < 2) continue; // uncontested cell — no ambiguity
|
|
254
276
|
weights.sort((a, b) => b - a);
|
|
255
|
-
|
|
277
|
+
const margin = weights[0] - weights[1];
|
|
278
|
+
if (margin < min) min = margin;
|
|
256
279
|
}
|
|
257
280
|
}
|
|
258
|
-
return
|
|
281
|
+
return min;
|
|
259
282
|
}
|
|
260
283
|
|
|
261
284
|
/** Read out the winning character per cell for a format. */
|
|
@@ -293,7 +316,9 @@ export class MRZFrameAggregator {
|
|
|
293
316
|
mrz: null,
|
|
294
317
|
validation: null,
|
|
295
318
|
frames: this.frameCount,
|
|
296
|
-
stable: false
|
|
319
|
+
stable: false,
|
|
320
|
+
stableStreak: 0,
|
|
321
|
+
minContestedMargin: Infinity
|
|
297
322
|
};
|
|
298
323
|
}
|
|
299
324
|
let bestMrz = null;
|
|
@@ -338,7 +363,9 @@ export class MRZFrameAggregator {
|
|
|
338
363
|
mrz: bestMrz,
|
|
339
364
|
validation: bestValidation,
|
|
340
365
|
frames: this.frameCount,
|
|
341
|
-
stable: bestValid && confident && this.stableStreak >= this.stabilityTarget
|
|
366
|
+
stable: bestValid && confident && this.stableStreak >= this.stabilityTarget,
|
|
367
|
+
stableStreak: this.stableStreak,
|
|
368
|
+
minContestedMargin: bestFormat != null ? this.minContestedMargin(bestFormat) : Infinity
|
|
342
369
|
};
|
|
343
370
|
}
|
|
344
371
|
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// ICAO 9303 digraphs safe to apply universally — these appear across many
|
|
4
|
+
// national variants and are unlikely to collide with real letter sequences.
|
|
5
|
+
// SS→ß, TH→Þ etc. are excluded: too ambiguous (HASSAN, THOMAS).
|
|
6
|
+
const UNIVERSAL = [['OE', 'Ö'], ['UE', 'Ü']];
|
|
7
|
+
const GERMAN = [['AE', 'Ä'], ['OE', 'Ö'], ['UE', 'Ü'], ['SS', 'ß']];
|
|
8
|
+
const SWEDISH = [['AA', 'Å'], ['AE', 'Æ'], ['OE', 'Ø']];
|
|
9
|
+
const DANISH = [['AA', 'Å'], ['AE', 'Æ'], ['OE', 'Ø']];
|
|
10
|
+
const NORWEGIAN = [['AA', 'Å'], ['AE', 'Æ'], ['OE', 'Ø']];
|
|
11
|
+
const FRENCH = [['OE', 'Œ'], ['AE', 'Æ']];
|
|
12
|
+
const ICELANDIC = [['AE', 'Æ'], ['OE', 'Ö'], ['TH', 'Þ'], ['D', 'Ð']];
|
|
13
|
+
const COUNTRY_MAP = {
|
|
14
|
+
DEU: GERMAN,
|
|
15
|
+
AUT: GERMAN,
|
|
16
|
+
CHE: GERMAN,
|
|
17
|
+
SWE: SWEDISH,
|
|
18
|
+
DNK: DANISH,
|
|
19
|
+
NOR: NORWEGIAN,
|
|
20
|
+
FRA: FRENCH,
|
|
21
|
+
LUX: FRENCH,
|
|
22
|
+
ISL: ICELANDIC
|
|
23
|
+
};
|
|
24
|
+
function applyDigraphMap(token, map) {
|
|
25
|
+
let result = '';
|
|
26
|
+
let i = 0;
|
|
27
|
+
while (i < token.length) {
|
|
28
|
+
let matched = false;
|
|
29
|
+
for (const [digraph, unicode] of map) {
|
|
30
|
+
if (token.startsWith(digraph, i)) {
|
|
31
|
+
result += unicode;
|
|
32
|
+
i += digraph.length;
|
|
33
|
+
matched = true;
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (!matched) {
|
|
38
|
+
result += token[i];
|
|
39
|
+
i++;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
function convertName(rawName, map) {
|
|
45
|
+
return rawName.split(' ').map(word => applyDigraphMap(word, map)).join(' ');
|
|
46
|
+
}
|
|
47
|
+
export function buildDocumentName(rawFirst, rawLast, issuingState, dg11First, dg11Last) {
|
|
48
|
+
if (dg11First != null && dg11Last != null && (dg11First || dg11Last)) {
|
|
49
|
+
return {
|
|
50
|
+
rawFirst,
|
|
51
|
+
rawLast,
|
|
52
|
+
displayFirst: dg11First,
|
|
53
|
+
displayLast: dg11Last,
|
|
54
|
+
source: 'dg11'
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
const country = (issuingState ?? '').toUpperCase().trim();
|
|
58
|
+
const map = COUNTRY_MAP[country];
|
|
59
|
+
if (map) {
|
|
60
|
+
return {
|
|
61
|
+
rawFirst,
|
|
62
|
+
rawLast,
|
|
63
|
+
displayFirst: convertName(rawFirst, map),
|
|
64
|
+
displayLast: convertName(rawLast, map),
|
|
65
|
+
source: 'reverse_table'
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// No country-specific table: apply OE→Ö and UE→Ü universally.
|
|
70
|
+
const displayFirst = convertName(rawFirst, UNIVERSAL);
|
|
71
|
+
const displayLast = convertName(rawLast, UNIVERSAL);
|
|
72
|
+
const changed = displayFirst !== rawFirst || displayLast !== rawLast;
|
|
73
|
+
return {
|
|
74
|
+
rawFirst,
|
|
75
|
+
rawLast,
|
|
76
|
+
displayFirst,
|
|
77
|
+
displayLast,
|
|
78
|
+
source: changed ? 'reverse_table' : 'raw'
|
|
79
|
+
};
|
|
80
|
+
}
|