@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
|
@@ -4,11 +4,39 @@ import { NFCManagerCardService } from './nfcManagerCardService';
|
|
|
4
4
|
import { EIDService } from './eidService';
|
|
5
5
|
import { DG1File } from './lds/icao/dg1File';
|
|
6
6
|
import { DG2File } from './lds/icao/dg2File';
|
|
7
|
+
import { DG11File } from './lds/icao/dg11File';
|
|
7
8
|
import { FaceImageInfo } from './lds/iso19794/faceImageInfo';
|
|
8
9
|
import { Buffer } from 'buffer';
|
|
9
10
|
import { MRZInfo } from './lds/icao/mrzInfo';
|
|
10
11
|
import { InputStream } from './java/inputStream';
|
|
11
12
|
import { debugLog } from '../Libs/debug.utils';
|
|
13
|
+
import { diagnostics } from '../Libs/diagnostics';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Extract an APDU status word (hex, e.g. "6982") from an error message if the
|
|
17
|
+
* protocol layer included one. Returns undefined when none is present. Kept
|
|
18
|
+
* deliberately loose — the protocol senders embed "SW=...." / "(6982)" forms.
|
|
19
|
+
*/
|
|
20
|
+
const extractStatusWord = (msg: string): string | undefined => {
|
|
21
|
+
const m = msg.match(/\b(?:SW[:= ]?)?([0-9a-fA-F]{4})\b/);
|
|
22
|
+
return m ? m[1].toUpperCase() : undefined;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/** Categorise an NFC error message into a coarse, non-PII bucket. */
|
|
26
|
+
const categorizeNfcError = (msg: string): string => {
|
|
27
|
+
const m = msg.toLowerCase();
|
|
28
|
+
if (m.includes('cancel')) return 'user_cancelled';
|
|
29
|
+
if (m.includes('lost') || m.includes('steady')) return 'tag_lost';
|
|
30
|
+
if (m.includes('timeout')) return 'timeout';
|
|
31
|
+
if (m.includes('not supported') || m.includes('unsupported')) {
|
|
32
|
+
return 'unsupported';
|
|
33
|
+
}
|
|
34
|
+
if (m.includes('not enabled') || m.includes('disabled')) return 'not_enabled';
|
|
35
|
+
if (m.includes('auth') || m.includes('mutual') || m.includes('security')) {
|
|
36
|
+
return 'auth_failed';
|
|
37
|
+
}
|
|
38
|
+
return 'reading_error';
|
|
39
|
+
};
|
|
12
40
|
|
|
13
41
|
// --- Minimal PNG encoder (pure JS, no native deps) ---
|
|
14
42
|
// Uses deflate stored (uncompressed) blocks for O(n) encoding speed.
|
|
@@ -295,7 +323,14 @@ const eidReader = async (
|
|
|
295
323
|
dateOfExpiry: string,
|
|
296
324
|
progressCallback?: (progress: number) => void
|
|
297
325
|
): Promise<
|
|
298
|
-
|
|
326
|
+
| {
|
|
327
|
+
mrz?: MRZInfo;
|
|
328
|
+
faceImage: string;
|
|
329
|
+
mimeType: string;
|
|
330
|
+
dg11FirstName: string | null;
|
|
331
|
+
dg11LastName: string | null;
|
|
332
|
+
}
|
|
333
|
+
| undefined
|
|
299
334
|
> => {
|
|
300
335
|
let progress = 0;
|
|
301
336
|
const nfcManagerCardService = new NFCManagerCardService();
|
|
@@ -307,8 +342,40 @@ const eidReader = async (
|
|
|
307
342
|
true
|
|
308
343
|
);
|
|
309
344
|
|
|
345
|
+
// Structured NFC diagnostics: record each step's outcome + timing + APDU SW
|
|
346
|
+
// into the session collector so the result screen can surface device-specific
|
|
347
|
+
// read failures. Best-effort and side-effect-only — never alters the flow.
|
|
348
|
+
diagnostics.nfcAttemptStarted();
|
|
349
|
+
const stepTimer = () => {
|
|
350
|
+
const t0 = Date.now();
|
|
351
|
+
return (step: string, ok: boolean, error?: unknown) => {
|
|
352
|
+
const msg = error instanceof Error ? error.message : error ? String(error) : undefined;
|
|
353
|
+
// Store only the APDU status word and a COARSE category — never the raw
|
|
354
|
+
// error text, which could in theory embed field/PII values. The SW + step
|
|
355
|
+
// name + category carry all the actionable signal.
|
|
356
|
+
diagnostics.nfcStep({
|
|
357
|
+
step,
|
|
358
|
+
ok,
|
|
359
|
+
ms: Date.now() - t0,
|
|
360
|
+
sw: msg ? extractStatusWord(msg) : undefined,
|
|
361
|
+
error: msg ? categorizeNfcError(msg) : undefined,
|
|
362
|
+
});
|
|
363
|
+
};
|
|
364
|
+
};
|
|
365
|
+
|
|
310
366
|
try {
|
|
311
|
-
|
|
367
|
+
{
|
|
368
|
+
const done = stepTimer();
|
|
369
|
+
try {
|
|
370
|
+
await passportService.open();
|
|
371
|
+
// Reaching open() without throwing means the chip powered on / was seen.
|
|
372
|
+
diagnostics.nfcChipDetected();
|
|
373
|
+
done('open', true);
|
|
374
|
+
} catch (e) {
|
|
375
|
+
done('open', false, e);
|
|
376
|
+
throw e;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
312
379
|
|
|
313
380
|
progress = 1;
|
|
314
381
|
if (progressCallback) {
|
|
@@ -320,15 +387,20 @@ const eidReader = async (
|
|
|
320
387
|
// regardless of which application the chip activates on power-on.
|
|
321
388
|
let hasPACESucceeded = false;
|
|
322
389
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
390
|
+
{
|
|
391
|
+
const done = stepTimer();
|
|
392
|
+
try {
|
|
393
|
+
await passportService.sendSelectMF();
|
|
394
|
+
done('selectMF', true);
|
|
395
|
+
debugLog('EID', '[EID] SELECT MF OK');
|
|
396
|
+
} catch (mfErr) {
|
|
397
|
+
done('selectMF', false, mfErr);
|
|
398
|
+
const mfMsg = mfErr instanceof Error ? mfErr.message : String(mfErr);
|
|
399
|
+
debugLog(
|
|
400
|
+
'EID',
|
|
401
|
+
`[EID] SELECT MF failed (${mfMsg}), continuing with SFI read anyway`
|
|
402
|
+
);
|
|
403
|
+
}
|
|
332
404
|
}
|
|
333
405
|
|
|
334
406
|
progress = 2;
|
|
@@ -339,28 +411,33 @@ const eidReader = async (
|
|
|
339
411
|
// Read EF.CardAccess — kept in a separate try/catch so we can distinguish
|
|
340
412
|
// "file read failure" from "PACE protocol failure" in the logs.
|
|
341
413
|
let cardAccessBuf: Buffer | null = null;
|
|
342
|
-
|
|
343
|
-
const
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
buf
|
|
414
|
+
{
|
|
415
|
+
const done = stepTimer();
|
|
416
|
+
try {
|
|
417
|
+
const cardAccessStream = passportService.getInputStream(
|
|
418
|
+
EIDService.EF_CARD_ACCESS
|
|
419
|
+
);
|
|
420
|
+
await cardAccessStream.init();
|
|
421
|
+
const cardAccessLen = cardAccessStream.getLength();
|
|
422
|
+
const buf = Buffer.alloc(cardAccessLen);
|
|
423
|
+
for (let i = 0; i < cardAccessLen; i++) {
|
|
424
|
+
buf[i] = await cardAccessStream.read();
|
|
425
|
+
}
|
|
426
|
+
cardAccessBuf = buf;
|
|
427
|
+
done('readCardAccess', true);
|
|
428
|
+
debugLog(
|
|
429
|
+
'EID',
|
|
430
|
+
`[EID] EF.CardAccess read OK: ${cardAccessLen} bytes = ${cardAccessBuf.toString('hex')}`
|
|
431
|
+
);
|
|
432
|
+
} catch (readErr) {
|
|
433
|
+
done('readCardAccess', false, readErr);
|
|
434
|
+
const readMsg =
|
|
435
|
+
readErr instanceof Error ? readErr.message : String(readErr);
|
|
436
|
+
debugLog(
|
|
437
|
+
'EID',
|
|
438
|
+
`[EID] EF.CardAccess read FAILED: ${readMsg} — falling back to BAC`
|
|
439
|
+
);
|
|
351
440
|
}
|
|
352
|
-
cardAccessBuf = buf;
|
|
353
|
-
debugLog(
|
|
354
|
-
'EID',
|
|
355
|
-
`[EID] EF.CardAccess read OK: ${cardAccessLen} bytes = ${cardAccessBuf.toString('hex')}`
|
|
356
|
-
);
|
|
357
|
-
} catch (readErr) {
|
|
358
|
-
const readMsg =
|
|
359
|
-
readErr instanceof Error ? readErr.message : String(readErr);
|
|
360
|
-
debugLog(
|
|
361
|
-
'EID',
|
|
362
|
-
`[EID] EF.CardAccess read FAILED: ${readMsg} — falling back to BAC`
|
|
363
|
-
);
|
|
364
441
|
}
|
|
365
442
|
|
|
366
443
|
progress = 3;
|
|
@@ -369,6 +446,11 @@ const eidReader = async (
|
|
|
369
446
|
}
|
|
370
447
|
|
|
371
448
|
// If EF.CardAccess was read successfully, attempt PACE.
|
|
449
|
+
if (cardAccessBuf === null) {
|
|
450
|
+
// EF.CardAccess unreadable (the readCardAccess step already recorded the
|
|
451
|
+
// failure/SW) → PACE can't be attempted; BAC will be used.
|
|
452
|
+
diagnostics.nfcPaceFellBackToBac('no_card_access');
|
|
453
|
+
}
|
|
372
454
|
if (cardAccessBuf !== null) {
|
|
373
455
|
try {
|
|
374
456
|
const paceInfo = parsePACEInfoFromCardAccess(cardAccessBuf);
|
|
@@ -378,6 +460,7 @@ const eidReader = async (
|
|
|
378
460
|
'EID',
|
|
379
461
|
`[EID] PACE available: oid=${paceInfo.oid} parameterId=${paceInfo.parameterId}`
|
|
380
462
|
);
|
|
463
|
+
diagnostics.nfcSetAuth('PACE', paceInfo.oid);
|
|
381
464
|
// PACE is available - derive key from MRZ and perform PACE
|
|
382
465
|
const bacKey = new BACKey(documentNumber, dateOfBirth, dateOfExpiry);
|
|
383
466
|
const paceKey = PACEKeySpec.createMRZKey(bacKey.getKeySeedForPACE());
|
|
@@ -387,15 +470,23 @@ const eidReader = async (
|
|
|
387
470
|
progressCallback(progress);
|
|
388
471
|
}
|
|
389
472
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
473
|
+
const done = stepTimer();
|
|
474
|
+
try {
|
|
475
|
+
await passportService.doPACE(
|
|
476
|
+
paceKey,
|
|
477
|
+
paceInfo.oid,
|
|
478
|
+
paceInfo.parameterId,
|
|
479
|
+
progressCallback
|
|
480
|
+
);
|
|
481
|
+
hasPACESucceeded = true;
|
|
482
|
+
done('paceAuth', true);
|
|
483
|
+
debugLog('EID', '[EID] PACE succeeded');
|
|
484
|
+
} catch (paceErr) {
|
|
485
|
+
done('paceAuth', false, paceErr);
|
|
486
|
+
throw paceErr;
|
|
487
|
+
}
|
|
398
488
|
} else {
|
|
489
|
+
diagnostics.nfcPaceFellBackToBac('no_pace_info');
|
|
399
490
|
debugLog(
|
|
400
491
|
'EID',
|
|
401
492
|
'[EID] EF.CardAccess read OK but no PACE SecurityInfo found — document does not support PACE, falling back to BAC'
|
|
@@ -403,6 +494,7 @@ const eidReader = async (
|
|
|
403
494
|
}
|
|
404
495
|
} catch (paceError) {
|
|
405
496
|
// PACE protocol exchange failed — fall back to BAC
|
|
497
|
+
diagnostics.nfcPaceFellBackToBac('pace_failed');
|
|
406
498
|
const msg =
|
|
407
499
|
paceError instanceof Error ? paceError.message : String(paceError);
|
|
408
500
|
debugLog(
|
|
@@ -421,7 +513,16 @@ const eidReader = async (
|
|
|
421
513
|
}
|
|
422
514
|
|
|
423
515
|
// Select Applet for MRTD
|
|
424
|
-
|
|
516
|
+
{
|
|
517
|
+
const done = stepTimer();
|
|
518
|
+
try {
|
|
519
|
+
await passportService.sendSelectApplet(hasPACESucceeded);
|
|
520
|
+
done('selectApplet', true);
|
|
521
|
+
} catch (e) {
|
|
522
|
+
done('selectApplet', false, e);
|
|
523
|
+
throw e;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
425
526
|
|
|
426
527
|
progress = 10;
|
|
427
528
|
if (progressCallback) {
|
|
@@ -436,8 +537,16 @@ const eidReader = async (
|
|
|
436
537
|
await efComStream.read();
|
|
437
538
|
} catch (error) {
|
|
438
539
|
// EF_COM not available -> try to do BAC
|
|
540
|
+
diagnostics.nfcSetAuth('BAC');
|
|
439
541
|
const bacKey = new BACKey(documentNumber, dateOfBirth, dateOfExpiry);
|
|
440
|
-
|
|
542
|
+
const done = stepTimer();
|
|
543
|
+
try {
|
|
544
|
+
await passportService.doBAC(bacKey);
|
|
545
|
+
done('bacAuth', true);
|
|
546
|
+
} catch (e) {
|
|
547
|
+
done('bacAuth', false, e);
|
|
548
|
+
throw e;
|
|
549
|
+
}
|
|
441
550
|
}
|
|
442
551
|
}
|
|
443
552
|
|
|
@@ -447,17 +556,42 @@ const eidReader = async (
|
|
|
447
556
|
}
|
|
448
557
|
|
|
449
558
|
// Read DG1 (MRZ Info)
|
|
450
|
-
const
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
559
|
+
const dg1Done = stepTimer();
|
|
560
|
+
let mrzInfo: MRZInfo;
|
|
561
|
+
try {
|
|
562
|
+
const dg1InputStream = passportService.getInputStream(EIDService.EF_DG1);
|
|
563
|
+
await dg1InputStream.init();
|
|
564
|
+
const dg1File = new DG1File(undefined, dg1InputStream);
|
|
565
|
+
mrzInfo = await dg1File.getMRZInfo();
|
|
566
|
+
dg1Done('readDG1', true);
|
|
567
|
+
} catch (e) {
|
|
568
|
+
dg1Done('readDG1', false, e);
|
|
569
|
+
throw e;
|
|
570
|
+
}
|
|
454
571
|
|
|
455
572
|
progress = 30;
|
|
456
573
|
if (progressCallback) {
|
|
457
574
|
progressCallback(progress);
|
|
458
575
|
}
|
|
459
576
|
|
|
577
|
+
// Read DG11 (Additional Personal Data — printed name in UTF-8, optional)
|
|
578
|
+
let dg11FirstName: string | null = null;
|
|
579
|
+
let dg11LastName: string | null = null;
|
|
580
|
+
try {
|
|
581
|
+
const dg11InputStream = passportService.getInputStream(
|
|
582
|
+
EIDService.EF_DG11
|
|
583
|
+
);
|
|
584
|
+
await dg11InputStream.init();
|
|
585
|
+
const dg11File = new DG11File(dg11InputStream);
|
|
586
|
+
await dg11File.read();
|
|
587
|
+
dg11FirstName = dg11File.getFirstName();
|
|
588
|
+
dg11LastName = dg11File.getLastName();
|
|
589
|
+
} catch {
|
|
590
|
+
// DG11 is optional — not all chips have it, and access may be denied
|
|
591
|
+
}
|
|
592
|
+
|
|
460
593
|
// Read DG2 (Face Image)
|
|
594
|
+
const dg2Done = stepTimer();
|
|
461
595
|
const dg2InputStream = passportService.getInputStream(EIDService.EF_DG2);
|
|
462
596
|
await dg2InputStream.init();
|
|
463
597
|
InputStream.onRead((totalBytesRead, fileLength) => {
|
|
@@ -471,7 +605,14 @@ const eidReader = async (
|
|
|
471
605
|
let mimeType = '';
|
|
472
606
|
|
|
473
607
|
const allFaceImageInfos: FaceImageInfo[] = [];
|
|
474
|
-
|
|
608
|
+
let faceInfos;
|
|
609
|
+
try {
|
|
610
|
+
faceInfos = await dg2File.getFaceInfos();
|
|
611
|
+
dg2Done('readDG2', true);
|
|
612
|
+
} catch (e) {
|
|
613
|
+
dg2Done('readDG2', false, e);
|
|
614
|
+
throw e;
|
|
615
|
+
}
|
|
475
616
|
for (let faceInfo of faceInfos) {
|
|
476
617
|
allFaceImageInfos.push(...faceInfo.getFaceImageInfos());
|
|
477
618
|
}
|
|
@@ -493,12 +634,17 @@ const eidReader = async (
|
|
|
493
634
|
progressCallback(progress);
|
|
494
635
|
}
|
|
495
636
|
|
|
637
|
+
diagnostics.nfcResult(true);
|
|
496
638
|
return {
|
|
497
639
|
mrz: mrzInfo,
|
|
498
640
|
faceImage: imageAsBase64,
|
|
499
641
|
mimeType: mimeType,
|
|
642
|
+
dg11FirstName,
|
|
643
|
+
dg11LastName,
|
|
500
644
|
};
|
|
501
645
|
} catch (error) {
|
|
646
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
647
|
+
diagnostics.nfcResult(false, categorizeNfcError(msg));
|
|
502
648
|
debugLog('EID', 'Error reading passport data', error);
|
|
503
649
|
} finally {
|
|
504
650
|
await passportService.close();
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { Buffer } from 'buffer';
|
|
2
|
+
import { InputStream } from '../../java/inputStream';
|
|
3
|
+
import { TLVInputStream } from '../../tlv/tlvInputStream';
|
|
4
|
+
import { DataGroup } from '../dataGroup';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* DG11 — Additional Personal Data (ICAO 9303-10 §4.6.6).
|
|
8
|
+
*
|
|
9
|
+
* We only extract tag 0x5F0E (Name of Holder), which contains the full name
|
|
10
|
+
* exactly as printed on the document, in UTF-8, using the same `<<` primary/
|
|
11
|
+
* secondary delimiter as the MRZ (but with native Unicode characters).
|
|
12
|
+
*
|
|
13
|
+
* Example: "ÖZTÜRK<<GÖKHAN MEHMET" → lastName="ÖZTÜRK", firstName="GÖKHAN MEHMET"
|
|
14
|
+
*
|
|
15
|
+
* The file is optional on the chip — if absent or unreadable we return null
|
|
16
|
+
* so the caller can fall back to reverse transliteration.
|
|
17
|
+
*/
|
|
18
|
+
export class DG11File extends DataGroup {
|
|
19
|
+
private static readonly DG11_TAG = 0x6b;
|
|
20
|
+
private static readonly TAG_LIST_TAG = 0x5c;
|
|
21
|
+
private static readonly NAME_OF_HOLDER_TAG = 0x5f0e;
|
|
22
|
+
|
|
23
|
+
private firstName: string | null = null;
|
|
24
|
+
private lastName: string | null = null;
|
|
25
|
+
|
|
26
|
+
constructor(is?: InputStream) {
|
|
27
|
+
super(DG11File.DG11_TAG, is);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Returns the first name from DG11, or null if not present. */
|
|
31
|
+
getFirstName(): string | null {
|
|
32
|
+
return this.firstName;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Returns the last name from DG11, or null if not present. */
|
|
36
|
+
getLastName(): string | null {
|
|
37
|
+
return this.lastName;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async read(): Promise<void> {
|
|
41
|
+
await super.readObject();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
protected async readContent(inputStream: InputStream): Promise<void> {
|
|
45
|
+
const tlvIn =
|
|
46
|
+
inputStream instanceof TLVInputStream
|
|
47
|
+
? inputStream
|
|
48
|
+
: new TLVInputStream(inputStream);
|
|
49
|
+
|
|
50
|
+
// Skip the tag list (0x5C) — we only care about 0x5F0E
|
|
51
|
+
// The tag list tells us which optional tags are present; we probe directly.
|
|
52
|
+
try {
|
|
53
|
+
await tlvIn.skipToTag(DG11File.NAME_OF_HOLDER_TAG);
|
|
54
|
+
} catch {
|
|
55
|
+
// Tag not present in this chip's DG11
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const length = await tlvIn.readLength();
|
|
60
|
+
if (length <= 0) return;
|
|
61
|
+
|
|
62
|
+
const bytes = new Uint8Array(length);
|
|
63
|
+
await inputStream.readBytesWithOffset(bytes, 0, length);
|
|
64
|
+
|
|
65
|
+
const nameString = Buffer.from(bytes).toString('utf8').trim();
|
|
66
|
+
this.parseName(nameString);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
protected writeContent(): void {
|
|
70
|
+
// Write not needed for reading
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Split "PRIMARY<<SECONDARY" into last/first name components.
|
|
75
|
+
* Strips trailing filler `<` within each segment.
|
|
76
|
+
*/
|
|
77
|
+
private parseName(nameString: string): void {
|
|
78
|
+
const delimIndex = nameString.indexOf('<<');
|
|
79
|
+
if (delimIndex < 0) {
|
|
80
|
+
this.lastName = nameString.replace(/<+$/, '').trim();
|
|
81
|
+
this.firstName = '';
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
this.lastName = nameString
|
|
85
|
+
.substring(0, delimIndex)
|
|
86
|
+
.replace(/<+$/, '')
|
|
87
|
+
.trim();
|
|
88
|
+
this.firstName = nameString
|
|
89
|
+
.substring(delimIndex + 2)
|
|
90
|
+
.replace(/<$/, ' ')
|
|
91
|
+
.replace(/<+$/, '')
|
|
92
|
+
.trim();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -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,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds the support diagnostic report from a scan session.
|
|
3
|
+
*
|
|
4
|
+
* Produces three things the result screen turns into a reviewable email:
|
|
5
|
+
* - `diagnostics.json` — technical signals (device, camera, MRZ, NFC steps).
|
|
6
|
+
* - `scan-data.json` — the scanned identity data + raw MRZ (PII; the user
|
|
7
|
+
* reviews it in the draft and chooses to send).
|
|
8
|
+
* - a short, size-bounded plain-text BODY summary that always fits an email.
|
|
9
|
+
*
|
|
10
|
+
* The full detail lives in the JSON attachments; the email body is intentionally
|
|
11
|
+
* compact so the draft opens reliably on every mail client.
|
|
12
|
+
*/
|
|
13
|
+
import type { DiagnosticsSnapshot } from './diagnostics';
|
|
14
|
+
import type { DeviceDetails } from './native-device-info.utils';
|
|
15
|
+
import type { MRZFields } from '../Types/mrzFields';
|
|
16
|
+
|
|
17
|
+
/** ICAO 9303 7-3-1 weighted check digit (local copy; mirrors mrz.utils). */
|
|
18
|
+
const icaoCheckDigit = (str: string): string => {
|
|
19
|
+
const weights = [7, 3, 1];
|
|
20
|
+
let sum = 0;
|
|
21
|
+
for (let i = 0; i < str.length; i++) {
|
|
22
|
+
const c = str[i];
|
|
23
|
+
const v =
|
|
24
|
+
c === '<'
|
|
25
|
+
? 0
|
|
26
|
+
: c >= '0' && c <= '9'
|
|
27
|
+
? c.charCodeAt(0) - 48
|
|
28
|
+
: c.charCodeAt(0) - 55;
|
|
29
|
+
sum = (sum + weights[i % 3] * v) % 10;
|
|
30
|
+
}
|
|
31
|
+
return String(sum);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export interface ScanDataForReport {
|
|
35
|
+
documentType?: string;
|
|
36
|
+
dataSource?: string;
|
|
37
|
+
mrzText?: string | null;
|
|
38
|
+
mrzFields?: MRZFields | null;
|
|
39
|
+
barcodeValue?: string | null;
|
|
40
|
+
/** Which images were captured (presence only, used for the body summary). */
|
|
41
|
+
images?: {
|
|
42
|
+
mrzSide?: boolean;
|
|
43
|
+
front?: boolean;
|
|
44
|
+
face?: boolean;
|
|
45
|
+
nfcFace?: boolean;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface Td1CheckResult {
|
|
50
|
+
checkDigits: Record<string, 'pass' | 'fail'>;
|
|
51
|
+
/** True when the Turkish leading-letter (digits-only) convention was applied. */
|
|
52
|
+
turkishLetterPrefix: boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Compute per-field check-digit pass/fail for a TD1 MRZ (best-effort). */
|
|
56
|
+
const computeTd1CheckDigits = (
|
|
57
|
+
mrzText?: string | null
|
|
58
|
+
): Td1CheckResult | undefined => {
|
|
59
|
+
if (!mrzText) return undefined;
|
|
60
|
+
const lines = mrzText
|
|
61
|
+
.split('\n')
|
|
62
|
+
.map((l) => l.trim())
|
|
63
|
+
.filter(Boolean);
|
|
64
|
+
if (lines.length < 2 || lines[0].length < 30 || lines[1].length < 30) {
|
|
65
|
+
return undefined; // only TD1 handled here; other formats omitted from summary
|
|
66
|
+
}
|
|
67
|
+
const l1 = lines[0];
|
|
68
|
+
const l2 = lines[1];
|
|
69
|
+
const result: Record<string, 'pass' | 'fail'> = {};
|
|
70
|
+
const check = (data: string, printed: string) =>
|
|
71
|
+
icaoCheckDigit(data) === printed ? 'pass' : 'fail';
|
|
72
|
+
|
|
73
|
+
// Turkish convention: a doc number may carry a leading serial LETTER, and the
|
|
74
|
+
// doc-number check digit is computed over the DIGITS ONLY (letter dropped).
|
|
75
|
+
// The TD1 composite then also diverges from strict ICAO for such cards (no
|
|
76
|
+
// interpretation of the letter reproduces it). To avoid reporting a misleading
|
|
77
|
+
// "fail" on a card the SDK legitimately accepted, detect the letter-prefix and
|
|
78
|
+
// treat both the doc-number and composite as validated under that convention.
|
|
79
|
+
const docData = l1.slice(5, 14);
|
|
80
|
+
const docPrinted = l1[14];
|
|
81
|
+
const hasLetterPrefix = /^[A-Z]/.test(docData);
|
|
82
|
+
const strictDocOk = icaoCheckDigit(docData) === docPrinted;
|
|
83
|
+
const turkishDocOk =
|
|
84
|
+
hasLetterPrefix && icaoCheckDigit(docData.slice(1)) === docPrinted;
|
|
85
|
+
const docOk = strictDocOk || turkishDocOk;
|
|
86
|
+
result.documentNumber = docOk ? 'pass' : 'fail';
|
|
87
|
+
result.birthDate = check(l2.slice(0, 6), l2[6]);
|
|
88
|
+
result.expiry = check(l2.slice(8, 14), l2[14]);
|
|
89
|
+
|
|
90
|
+
const composite =
|
|
91
|
+
l1.slice(5, 30) + l2.slice(0, 7) + l2.slice(8, 15) + l2.slice(18, 29);
|
|
92
|
+
const compStrict = check(composite, l2[29]);
|
|
93
|
+
// If the strict composite fails ONLY because of the Turkish letter-prefix
|
|
94
|
+
// doc number (which itself passed under the digits-only rule), don't surface a
|
|
95
|
+
// misleading failure — the SDK accepts these cards.
|
|
96
|
+
result.composite =
|
|
97
|
+
compStrict === 'fail' && hasLetterPrefix && docOk ? 'pass' : compStrict;
|
|
98
|
+
// The Turkish convention was the deciding factor only when strict ICAO would
|
|
99
|
+
// have failed the doc number but the digits-only rule passes it.
|
|
100
|
+
const turkishLetterPrefix = !strictDocOk && turkishDocOk;
|
|
101
|
+
return { checkDigits: result, turkishLetterPrefix };
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export interface DiagnosticReportBundle {
|
|
105
|
+
/** Pretty-printed technical report. */
|
|
106
|
+
diagnosticsJson: string;
|
|
107
|
+
/** Pretty-printed scanned identity data (PII). */
|
|
108
|
+
scanDataJson: string;
|
|
109
|
+
/** Compact, size-bounded plain-text email body. */
|
|
110
|
+
body: string;
|
|
111
|
+
/** Suggested email subject. */
|
|
112
|
+
subject: string;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const MAX_BODY_BYTES = 4096;
|
|
116
|
+
|
|
117
|
+
const yesNo = (b: boolean | undefined) => (b ? 'yes' : 'no');
|
|
118
|
+
|
|
119
|
+
/** Assemble the full report bundle. Pure — no I/O, no native calls. */
|
|
120
|
+
export const buildDiagnosticReport = (params: {
|
|
121
|
+
diag: DiagnosticsSnapshot;
|
|
122
|
+
device: DeviceDetails;
|
|
123
|
+
scan: ScanDataForReport;
|
|
124
|
+
sdkVersion: string;
|
|
125
|
+
sessionId?: string;
|
|
126
|
+
}): DiagnosticReportBundle => {
|
|
127
|
+
const { diag, device, scan, sdkVersion, sessionId } = params;
|
|
128
|
+
|
|
129
|
+
const computed = computeTd1CheckDigits(scan.mrzText);
|
|
130
|
+
const checkDigits = diag.mrz.checkDigits ?? computed?.checkDigits;
|
|
131
|
+
// Set the Turkish-letter-prefix flag from the final accepted MRZ (the collector
|
|
132
|
+
// can't know it during scanning; the report does, from the MRZ text).
|
|
133
|
+
const turkishLetterPrefixApplied =
|
|
134
|
+
diag.mrz.turkishLetterPrefixApplied || computed?.turkishLetterPrefix === true;
|
|
135
|
+
|
|
136
|
+
// --- diagnostics.json (technical, low-PII) ---
|
|
137
|
+
const diagnosticsObj = {
|
|
138
|
+
schemaVersion: diag.schemaVersion,
|
|
139
|
+
generatedAt: diag.generatedAt,
|
|
140
|
+
sessionId: sessionId ?? null,
|
|
141
|
+
sdk: { platform: device.platform, sdkVersion, buildNumber: device.buildNumber },
|
|
142
|
+
device,
|
|
143
|
+
scan: {
|
|
144
|
+
documentType: scan.documentType ?? diag.documentType ?? null,
|
|
145
|
+
dataSource: scan.dataSource ?? diag.dataSource ?? null,
|
|
146
|
+
totalDurationMs: diag.totalDurationMs,
|
|
147
|
+
camera: diag.camera,
|
|
148
|
+
},
|
|
149
|
+
mrz: {
|
|
150
|
+
...diag.mrz,
|
|
151
|
+
checkDigits: checkDigits ?? null,
|
|
152
|
+
turkishLetterPrefixApplied,
|
|
153
|
+
},
|
|
154
|
+
nfc: diag.nfc,
|
|
155
|
+
errors: diag.errors,
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
// --- scan-data.json (PII; user reviews before sending) ---
|
|
159
|
+
const scanDataObj = {
|
|
160
|
+
documentType: scan.documentType ?? diag.documentType ?? null,
|
|
161
|
+
dataSource: scan.dataSource ?? diag.dataSource ?? null,
|
|
162
|
+
mrzText: scan.mrzText ?? null,
|
|
163
|
+
fields: scan.mrzFields ?? null,
|
|
164
|
+
barcodeValue: scan.barcodeValue ?? null,
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
// --- compact email body (always under MAX_BODY_BYTES) ---
|
|
168
|
+
const nfc = diag.nfc;
|
|
169
|
+
const nfcLine = nfc.attempted
|
|
170
|
+
? `${nfc.authProtocol}${nfc.paceFellBackToBac ? '→BAC' : ''} · ${
|
|
171
|
+
nfc.succeeded ? 'success' : `failed at ${nfc.failureStep ?? '?'} (${nfc.errorCategory ?? 'error'})`
|
|
172
|
+
}`
|
|
173
|
+
: 'not attempted';
|
|
174
|
+
const checksLine = checkDigits
|
|
175
|
+
? Object.entries(checkDigits)
|
|
176
|
+
.map(([k, v]) => `${k}:${v}`)
|
|
177
|
+
.join(' ')
|
|
178
|
+
: 'n/a';
|
|
179
|
+
|
|
180
|
+
const body = [
|
|
181
|
+
'TRUSTCHEX SCAN REPORT',
|
|
182
|
+
`Device: ${device.brand} ${device.model} · ${device.systemName} ${device.systemVersion}${device.apiLevel ? ` (API ${device.apiLevel})` : ''}`,
|
|
183
|
+
`SDK: ${sdkVersion} (build ${device.buildNumber}) · ${device.manufacturer} · ${device.deviceId}`,
|
|
184
|
+
`Document: ${scanDataObj.documentType ?? 'unknown'} · source: ${scanDataObj.dataSource ?? 'unknown'}`,
|
|
185
|
+
`MRZ: ${diag.mrz.reachedStable ? 'stable' : 'NOT stable'} after ${diag.mrz.framesProcessed} frames · checks: ${checksLine}`,
|
|
186
|
+
`Camera: ${diag.camera.frames} frames · brightness ${diag.camera.avgBrightness} (low:${yesNo(diag.camera.hadLowBrightness)} blur:${yesNo(diag.camera.hadBlurryFrames)})`,
|
|
187
|
+
`NFC: ${nfcLine}`,
|
|
188
|
+
sessionId ? `Session: ${sessionId}` : '',
|
|
189
|
+
'',
|
|
190
|
+
'Describe the issue here:',
|
|
191
|
+
'',
|
|
192
|
+
'',
|
|
193
|
+
'— Full diagnostics and document images are attached. Please review before sending; do not edit the attachments. —',
|
|
194
|
+
]
|
|
195
|
+
.filter(Boolean)
|
|
196
|
+
.join('\n');
|
|
197
|
+
|
|
198
|
+
const subject = `Trustchex scan report — ${scanDataObj.documentType ?? 'doc'} — ${device.brand} ${device.model} — v${sdkVersion}${sessionId ? ` — ${sessionId}` : ''}`;
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
diagnosticsJson: JSON.stringify(diagnosticsObj, null, 2),
|
|
202
|
+
scanDataJson: JSON.stringify(scanDataObj, null, 2),
|
|
203
|
+
body: body.length > MAX_BODY_BYTES ? body.slice(0, MAX_BODY_BYTES - 1) + '…' : body,
|
|
204
|
+
subject,
|
|
205
|
+
};
|
|
206
|
+
};
|