@trustchex/react-native-sdk 1.475.1 → 1.481.1
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 +76 -24
- package/android/src/main/java/com/trustchex/reactnativesdk/mlkit/MLKitModule.kt +1 -0
- package/ios/Camera/TrustchexCameraView.swift +20 -15
- package/lib/module/Screens/Dynamic/LivenessDetectionScreen.js +3 -2
- package/lib/module/Screens/Static/QrCodeScanningScreen.js +12 -2
- package/lib/module/Screens/Static/ResultScreen.js +145 -113
- package/lib/module/Shared/Components/DebugOverlay.js +5 -2
- package/lib/module/Shared/Components/DiagnosticReportButton.js +64 -0
- package/lib/module/Shared/Components/IdentityDocumentCamera.js +185 -30
- package/lib/module/Shared/Components/IdentityDocumentCamera.utils.js +8 -1
- package/lib/module/Shared/Components/NavigationManager.js +7 -9
- package/lib/module/Shared/EIDReader/eidReader.js +159 -38
- package/lib/module/Shared/Libs/MRZ_KNOWN_ISSUES.md +41 -0
- package/lib/module/Shared/Libs/SignalingClient.js +6 -5
- package/lib/module/Shared/Libs/crypto.utils.js +25 -3
- package/lib/module/Shared/Libs/deeplink.utils.js +37 -7
- package/lib/module/Shared/Libs/diagnosticReport.js +133 -0
- package/lib/module/Shared/Libs/diagnostics.js +171 -0
- package/lib/module/Shared/Libs/http-client.js +19 -6
- package/lib/module/Shared/Libs/mrz.utils.js +193 -33
- package/lib/module/Shared/Libs/mrzFrameAggregator.js +74 -3
- package/lib/module/Shared/Libs/native-device-info.utils.js +67 -0
- package/lib/module/Shared/Libs/promise.utils.js +4 -3
- package/lib/module/Shared/Libs/sendDiagnosticReport.js +130 -0
- package/lib/module/Shared/Libs/shuffle.utils.js +17 -0
- package/lib/module/Shared/Services/AnalyticsService.js +8 -2
- package/lib/module/Shared/Services/DataUploadService.js +114 -127
- package/lib/module/Shared/Services/VideoSessionService.js +4 -3
- package/lib/module/Translation/Resources/en.js +6 -0
- package/lib/module/Translation/Resources/tr.js +6 -0
- package/lib/module/Trustchex.js +20 -4
- package/lib/module/version.js +1 -1
- package/lib/typescript/src/Screens/Dynamic/LivenessDetectionScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Static/QrCodeScanningScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Static/ResultScreen.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/DebugOverlay.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/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/IdentityDocumentCamera.utils.d.ts +1 -1
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.utils.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/EIDReader/eidReader.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/SignalingClient.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/crypto.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/deeplink.utils.d.ts +1 -0
- package/lib/typescript/src/Shared/Libs/deeplink.utils.d.ts.map +1 -1
- 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/http-client.d.ts +2 -0
- package/lib/typescript/src/Shared/Libs/http-client.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/mrz.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/mrzFrameAggregator.d.ts +55 -0
- package/lib/typescript/src/Shared/Libs/mrzFrameAggregator.d.ts.map +1 -1
- 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/promise.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/Libs/shuffle.utils.d.ts +3 -0
- package/lib/typescript/src/Shared/Libs/shuffle.utils.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Services/AnalyticsService.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Services/DataUploadService.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Services/VideoSessionService.d.ts.map +1 -1
- package/lib/typescript/src/Translation/Resources/en.d.ts +6 -0
- package/lib/typescript/src/Translation/Resources/en.d.ts.map +1 -1
- package/lib/typescript/src/Translation/Resources/tr.d.ts +6 -0
- package/lib/typescript/src/Translation/Resources/tr.d.ts.map +1 -1
- package/lib/typescript/src/Trustchex.d.ts.map +1 -1
- package/lib/typescript/src/version.d.ts +1 -1
- package/package.json +17 -3
- package/src/Screens/Dynamic/LivenessDetectionScreen.tsx +4 -3
- package/src/Screens/Static/QrCodeScanningScreen.tsx +19 -2
- package/src/Screens/Static/ResultScreen.tsx +172 -162
- package/src/Shared/Components/DebugOverlay.tsx +5 -2
- package/src/Shared/Components/DiagnosticReportButton.tsx +77 -0
- package/src/Shared/Components/IdentityDocumentCamera.tsx +200 -29
- package/src/Shared/Components/IdentityDocumentCamera.types.ts +6 -0
- package/src/Shared/Components/IdentityDocumentCamera.utils.ts +9 -1
- package/src/Shared/Components/NavigationManager.tsx +7 -7
- package/src/Shared/Components/TrustchexCamera.tsx +6 -0
- package/src/Shared/EIDReader/eidReader.ts +181 -52
- package/src/Shared/Libs/MRZ_KNOWN_ISSUES.md +41 -0
- package/src/Shared/Libs/SignalingClient.ts +10 -5
- package/src/Shared/Libs/crypto.utils.ts +41 -4
- package/src/Shared/Libs/deeplink.utils.ts +39 -7
- package/src/Shared/Libs/diagnosticReport.ts +206 -0
- package/src/Shared/Libs/diagnostics.ts +251 -0
- package/src/Shared/Libs/http-client.ts +24 -8
- package/src/Shared/Libs/mrz.utils.ts +200 -34
- package/src/Shared/Libs/mrzFrameAggregator.ts +112 -2
- package/src/Shared/Libs/native-device-info.utils.ts +116 -0
- package/src/Shared/Libs/promise.utils.ts +11 -5
- package/src/Shared/Libs/sendDiagnosticReport.ts +165 -0
- package/src/Shared/Libs/shuffle.utils.ts +15 -0
- package/src/Shared/Services/AnalyticsService.ts +17 -1
- package/src/Shared/Services/DataUploadService.ts +155 -166
- package/src/Shared/Services/VideoSessionService.ts +15 -3
- package/src/Translation/Resources/en.ts +7 -0
- package/src/Translation/Resources/tr.ts +7 -0
- package/src/Trustchex.tsx +26 -3
- package/src/version.ts +1 -1
|
@@ -14,6 +14,34 @@ const RECOVERY_BUDGET_MS = 80;
|
|
|
14
14
|
let recoveryDeadline = 0;
|
|
15
15
|
const recoveryExpired = (): boolean => Date.now() > recoveryDeadline;
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Pads a TD1 line to the fixed 30-char width.
|
|
19
|
+
*
|
|
20
|
+
* The naive `s.padEnd(30, '<')` is WRONG for the date record (line 2): its last
|
|
21
|
+
* character is the composite check digit, preceded by an 11-char optional-data
|
|
22
|
+
* filler run. OCR routinely UNDER-COUNTS that low-contrast `<<<` run, so the OCR
|
|
23
|
+
* string arrives short AND ending in the composite digit (e.g. a 28-char
|
|
24
|
+
* `…TUR<<<<<<<<<6`). `padEnd` then appends fillers AFTER the digit
|
|
25
|
+
* (`…6<<`), stranding the composite mid-line: the parser absorbs it into the
|
|
26
|
+
* optional field and the composite check fails, after which a recompute writes a
|
|
27
|
+
* WRONG digit over the misaligned layout — a plausible-but-malformed MRZ.
|
|
28
|
+
*
|
|
29
|
+
* The fix is layout-aware: when a short line ends in a digit, that digit is the
|
|
30
|
+
* trailing check digit, so re-grow the filler region BEFORE it. We only do this
|
|
31
|
+
* for the date record (`isDateRecord`); the doc record (line 1) ends in optional
|
|
32
|
+
* data, not a check digit, so plain right-padding is correct there.
|
|
33
|
+
*/
|
|
34
|
+
const padTD1Line = (s: string, isDateRecord: boolean = false): string => {
|
|
35
|
+
if (s.length >= 30) return s.slice(0, 30);
|
|
36
|
+
const last = s[s.length - 1];
|
|
37
|
+
if (isDateRecord && last >= '0' && last <= '9') {
|
|
38
|
+
// Trailing digit is the composite check digit — insert the dropped fillers
|
|
39
|
+
// BEFORE it so it lands at position 29.
|
|
40
|
+
return s.slice(0, -1).padEnd(29, '<') + last;
|
|
41
|
+
}
|
|
42
|
+
return s.padEnd(30, '<');
|
|
43
|
+
};
|
|
44
|
+
|
|
17
45
|
/**
|
|
18
46
|
* MRZ Format Types according to ICAO 9303
|
|
19
47
|
*/
|
|
@@ -341,11 +369,14 @@ const reconstructMRZCandidates = (rawText: string): string[][] => {
|
|
|
341
369
|
l1.slice(5);
|
|
342
370
|
let l2: string;
|
|
343
371
|
if (l2m) {
|
|
344
|
-
// Use the matched date record
|
|
345
|
-
|
|
372
|
+
// Use the matched date record. It is the TD1 line whose trailing char is
|
|
373
|
+
// the composite check digit, so pad layout-aware: a short line ending in
|
|
374
|
+
// a digit gets its dropped filler run restored BEFORE that digit (a
|
|
375
|
+
// plain right-pad would strand the composite mid-line). See padTD1Line.
|
|
376
|
+
l2 = padTD1Line(l2m, true);
|
|
346
377
|
} else {
|
|
347
378
|
const after = flat.indexOf(l1Source) + l1Source.length;
|
|
348
|
-
l2 =
|
|
379
|
+
l2 = padTD1Line(flat.slice(after, after + 30), true);
|
|
349
380
|
}
|
|
350
381
|
// Line 3 (name): the longest mostly-letters+filler OCR line that is NOT the
|
|
351
382
|
// doc or date line. OCR mangles the name line too — a leading "O" reads as
|
|
@@ -628,6 +659,118 @@ const icaoCheckDigit = (str: string): string => {
|
|
|
628
659
|
return String(sum);
|
|
629
660
|
};
|
|
630
661
|
|
|
662
|
+
/**
|
|
663
|
+
* Rebuilds the fixed-width MRZ lines from a parse result's per-field `ranges`,
|
|
664
|
+
* which carry the actual characters at known line/column spans. Works for both
|
|
665
|
+
* valid and invalid parses (the `mrz` library populates ranges either way), so
|
|
666
|
+
* callers can read the PRINTED characters — e.g. a check digit the parser
|
|
667
|
+
* rejected — straight from the reconstructed line. Lines default to all-filler.
|
|
668
|
+
*/
|
|
669
|
+
const linesFromParseRanges = (
|
|
670
|
+
result: ReturnType<typeof parse>,
|
|
671
|
+
width: number,
|
|
672
|
+
lineCount: number
|
|
673
|
+
): string[] => {
|
|
674
|
+
const buffers: string[][] = Array.from({ length: lineCount }, () =>
|
|
675
|
+
new Array(width).fill('<')
|
|
676
|
+
);
|
|
677
|
+
for (const detail of result.details ?? []) {
|
|
678
|
+
for (const range of detail.ranges ?? []) {
|
|
679
|
+
// `raw` (the actual characters at this span) exists at runtime but isn't in
|
|
680
|
+
// the `mrz` Range type.
|
|
681
|
+
const { line, start, raw = '' } = range as typeof range & { raw?: string };
|
|
682
|
+
const buf = buffers[line];
|
|
683
|
+
if (!buf) continue;
|
|
684
|
+
for (let i = 0; i < raw.length && start + i < width; i++) {
|
|
685
|
+
buf[start + i] = raw[i];
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
return buffers.map((b) => b.join(''));
|
|
690
|
+
};
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* Accepts a TD1 parse that the `mrz` library marks invalid SOLELY because of the
|
|
694
|
+
* Turkish e-ID document-number check-digit convention. (Full writeup: §2a in
|
|
695
|
+
* MRZ_KNOWN_ISSUES.md.)
|
|
696
|
+
*
|
|
697
|
+
* Turkish identity cards put a SERIAL LETTER at the start of the 9-char document
|
|
698
|
+
* number (e.g. "A53032352", "A49M38410"). Empirically — verified against real
|
|
699
|
+
* cards — Türkiye computes the document-number check digit over the number with
|
|
700
|
+
* that LEADING letter EXCLUDED (interior letters are kept), which diverges from
|
|
701
|
+
* strict ICAO 9303 (where A=10 is included). So a genuine card like "A53032352"
|
|
702
|
+
* (printed check digit 5) FAILS the strict check (which wants 7) even though the
|
|
703
|
+
* card is valid — `icaoCheckDigit("53032352") === "5"`.
|
|
704
|
+
*
|
|
705
|
+
* Without this, the downstream ambiguous-char recovery "fixes" the failing check
|
|
706
|
+
* by rewriting the real leading letter to a digit (A→4), corrupting the document
|
|
707
|
+
* number. This function instead recognises the legitimate Turkish reading and
|
|
708
|
+
* accepts the parse AS-IS, preserving the letter.
|
|
709
|
+
*
|
|
710
|
+
* It is deliberately narrow: it only fires for a TUR-issued TD1 whose document
|
|
711
|
+
* number starts with a letter, when the document-number check digit is failing
|
|
712
|
+
* (its signature) and the only OTHER failure permitted is the composite (the
|
|
713
|
+
* date checks must pass — an impossible date is real OCR damage, not this
|
|
714
|
+
* convention), and the leading-letter-dropped recomputation matches the printed
|
|
715
|
+
* document-number check digit.
|
|
716
|
+
*
|
|
717
|
+
* The leading serial letter MUST be preserved: the NFC eID chip derives its
|
|
718
|
+
* BAC/PACE access key from the document number exactly as stored on the chip
|
|
719
|
+
* (which includes the letter, e.g. "A53032352"). Rewriting it to a digit ("4…")
|
|
720
|
+
* — as the ambiguous-char recovery would, to satisfy strict ICAO — makes the
|
|
721
|
+
* chip read FAIL. So a real card must keep its letter even though the chip's
|
|
722
|
+
* MRZ carries a non-standard composite check digit that strict ICAO rejects.
|
|
723
|
+
*
|
|
724
|
+
* The TD1 COMPOSITE is therefore NOT required to match: on Turkish eIDs the
|
|
725
|
+
* composite is computed over the document-number region with the serial letter
|
|
726
|
+
* weighted in a way that diverges from strict ICAO (it cannot be reproduced by
|
|
727
|
+
* either including A=10 or dropping the letter), so it is not a reliable anchor
|
|
728
|
+
* here. The document-number check digit under the drop-leading-letter rule
|
|
729
|
+
* uniquely pins the numeric portion, and the letter is verbatim from OCR — the
|
|
730
|
+
* authoritative confirmation is the subsequent NFC read, which only succeeds
|
|
731
|
+
* with the correct (letter-preserving) document number.
|
|
732
|
+
*/
|
|
733
|
+
const acceptsTurkishLetterPrefix = (
|
|
734
|
+
result: ReturnType<typeof parse> | null
|
|
735
|
+
): boolean => {
|
|
736
|
+
if (!result || result.valid || result.format !== 'TD1') return false;
|
|
737
|
+
const fields = result.fields ?? {};
|
|
738
|
+
if (fields.issuingState !== 'TUR') return false;
|
|
739
|
+
const docNumber = fields.documentNumber ?? '';
|
|
740
|
+
// Must start with a serial LETTER (the Turkish convention this handles).
|
|
741
|
+
if (!/^[A-Z]/.test(docNumber)) return false;
|
|
742
|
+
|
|
743
|
+
const failing = new Set(
|
|
744
|
+
(result.details ?? []).filter((d) => !d.valid).map((d) => d.field)
|
|
745
|
+
);
|
|
746
|
+
// Only the document-number and/or composite checks may be off — any other
|
|
747
|
+
// failing check digit (dates) means real OCR damage, not this convention.
|
|
748
|
+
const allowed = new Set(['documentNumberCheckDigit', 'compositeCheckDigit']);
|
|
749
|
+
if (failing.size === 0) return false;
|
|
750
|
+
for (const f of failing) if (!allowed.has(f)) return false;
|
|
751
|
+
// The document-number check digit MUST be among the failures — that is the
|
|
752
|
+
// signature of the letter-prefix convention. If only the composite is off, this
|
|
753
|
+
// is a different problem (handled by fixTD1Composite / indel recovery).
|
|
754
|
+
if (!failing.has('documentNumberCheckDigit')) return false;
|
|
755
|
+
|
|
756
|
+
// Rebuild the fixed-width lines from the parse ranges. We read the PRINTED
|
|
757
|
+
// check digit straight from the line (the parsed `documentNumberCheckDigit`
|
|
758
|
+
// field is null when the check fails, so it can't be relied on here).
|
|
759
|
+
const lines = linesFromParseRanges(result, 30, 3);
|
|
760
|
+
const l1 = lines[0] ?? '';
|
|
761
|
+
if (l1.length < 30) return false;
|
|
762
|
+
|
|
763
|
+
// The printed document-number check digit (TD1 line 1, position 14) must equal
|
|
764
|
+
// the check digit computed over the document number WITHOUT its leading letter.
|
|
765
|
+
// This uniquely identifies the numeric portion; the leading letter is kept
|
|
766
|
+
// verbatim (NFC requires it). The composite is deliberately NOT checked here.
|
|
767
|
+
const printedDocCheck = l1[14];
|
|
768
|
+
const digitsAfterLetter = docNumber.slice(1);
|
|
769
|
+
if (icaoCheckDigit(digitsAfterLetter) !== printedDocCheck) return false;
|
|
770
|
+
|
|
771
|
+
return true;
|
|
772
|
+
};
|
|
773
|
+
|
|
631
774
|
/**
|
|
632
775
|
* When a TD1 MRZ fails ONLY on its composite check digit (every per-field check
|
|
633
776
|
* digit is valid), the composite is deterministically derivable from the other
|
|
@@ -638,6 +781,17 @@ const fixTD1Composite = (lines: string[]): ReturnType<typeof parse> | null => {
|
|
|
638
781
|
if (lines.length < 3 || lines[0].length !== 30 || lines[1].length !== 30) {
|
|
639
782
|
return null;
|
|
640
783
|
}
|
|
784
|
+
// Guard against a MISPLACED composite: when OCR drops filler(s) from the line-2
|
|
785
|
+
// optional run, a layout-naive right-pad lands the composite digit mid-line and
|
|
786
|
+
// pads the tail with "<". The result is length-30 but structurally wrong, and
|
|
787
|
+
// recomputing over it writes a bogus digit (a plausible-but-malformed MRZ). If
|
|
788
|
+
// the composite slot [29] is "<" yet a digit sits in the optional window
|
|
789
|
+
// [18..29), the line is misaligned — refuse the recompute and let the indel
|
|
790
|
+
// (filler-insertion) recovery realign it first.
|
|
791
|
+
const l2line = lines[1];
|
|
792
|
+
if (l2line[29] === '<' && /[0-9]/.test(l2line.slice(18, 29))) {
|
|
793
|
+
return null;
|
|
794
|
+
}
|
|
641
795
|
const r = parse(lines);
|
|
642
796
|
const bad = (r.details ?? []).filter((d) => !d.valid);
|
|
643
797
|
if (bad.length !== 1 || bad[0].field !== 'compositeCheckDigit') return null;
|
|
@@ -662,7 +816,10 @@ const fixTD1Composite = (lines: string[]): ReturnType<typeof parse> | null => {
|
|
|
662
816
|
* - line 2 (date record): delete each char in the birth..expiry-check window
|
|
663
817
|
* [0..15) (then right-pad to 30) — undoes a spurious inserted digit;
|
|
664
818
|
* - line 1 (doc record): delete each char in the doc-number..check window
|
|
665
|
-
* [5..15) — undoes an inserted char in the serial
|
|
819
|
+
* [5..15) — undoes an inserted char in the serial;
|
|
820
|
+
* - line 2 (date record): when OCR DROPPED filler(s) from the optional run, the
|
|
821
|
+
* line arrives short — insert a "<" across the optional window [18..29) to
|
|
822
|
+
* restore width 30 with the composite at position 29.
|
|
666
823
|
* Each candidate is re-padded to 30, the composite is recomputed, and only a
|
|
667
824
|
* fully check-digit-valid parse is accepted, so a wrong edit can never pass.
|
|
668
825
|
*/
|
|
@@ -670,13 +827,16 @@ const recoverTD1ByIndel = (
|
|
|
670
827
|
lines: string[]
|
|
671
828
|
): ReturnType<typeof parse> | null => {
|
|
672
829
|
if (lines.length < 3) return null;
|
|
673
|
-
|
|
674
|
-
|
|
830
|
+
// Layout-aware: line 2 (index 1) is the date record whose trailing char is the
|
|
831
|
+
// composite check digit, so a short line must have its dropped fillers restored
|
|
832
|
+
// BEFORE that digit, not appended after it (padEnd would strand the composite).
|
|
833
|
+
const pad30 = (s: string, li?: number) =>
|
|
834
|
+
s.length >= 30 ? s.slice(0, 30) : padTD1Line(s, li === 1);
|
|
675
835
|
|
|
676
836
|
// Which line/window to try edits in, based on which check digit is failing.
|
|
677
837
|
let base: ReturnType<typeof parse>;
|
|
678
838
|
try {
|
|
679
|
-
base = parse(lines.map(pad30));
|
|
839
|
+
base = parse(lines.map((l, li) => pad30(l, li)));
|
|
680
840
|
} catch {
|
|
681
841
|
return null;
|
|
682
842
|
}
|
|
@@ -706,15 +866,26 @@ const recoverTD1ByIndel = (
|
|
|
706
866
|
// Build all single-deletion candidates up front.
|
|
707
867
|
const candidates: string[][] = [];
|
|
708
868
|
for (const [li, from, to] of regions) {
|
|
709
|
-
const original = pad30(lines[li] ?? '');
|
|
869
|
+
const original = pad30(lines[li] ?? '', li);
|
|
710
870
|
for (let i = from; i < to && i < original.length; i++) {
|
|
711
|
-
const edited = pad30(original.slice(0, i) + original.slice(i + 1));
|
|
712
|
-
const candidate = lines.map(pad30);
|
|
871
|
+
const edited = pad30(original.slice(0, i) + original.slice(i + 1), li);
|
|
872
|
+
const candidate = lines.map((l, idx) => pad30(l, idx));
|
|
713
873
|
candidate[li] = edited;
|
|
714
874
|
candidates.push(candidate);
|
|
715
875
|
}
|
|
716
876
|
}
|
|
717
877
|
|
|
878
|
+
// Dropped-filler insertion: when OCR under-counts the line-2 optional filler
|
|
879
|
+
// run, the date record arrives SHORT. The layout-aware pad30 already restores
|
|
880
|
+
// it (fillers before the trailing composite digit), so include the raw padded
|
|
881
|
+
// date record as an explicit candidate — this repairs the common Turkish-ID
|
|
882
|
+
// case (`…TUR<<<<<<<<<6` read with 9 fillers instead of 11) by INSERTION, not a
|
|
883
|
+
// bogus composite recompute over a misaligned line.
|
|
884
|
+
if ((lines[1]?.length ?? 30) < 30) {
|
|
885
|
+
const padded = lines.map((l, idx) => pad30(l, idx));
|
|
886
|
+
candidates.push(padded);
|
|
887
|
+
}
|
|
888
|
+
|
|
718
889
|
// Cheap pass first: a plain parse or a composite recompute resolves the common
|
|
719
890
|
// case (a dropped/duplicated digit) in microseconds — try ALL deletions this
|
|
720
891
|
// way before paying for any substitution search.
|
|
@@ -911,26 +1082,7 @@ const correctedMrzFromParse = (
|
|
|
911
1082
|
const width = widths[result.format ?? ''];
|
|
912
1083
|
if (!width) return undefined;
|
|
913
1084
|
const lineCount = result.format === 'TD1' ? 3 : 2;
|
|
914
|
-
|
|
915
|
-
new Array(width).fill('<')
|
|
916
|
-
);
|
|
917
|
-
for (const detail of result.details ?? []) {
|
|
918
|
-
for (const range of detail.ranges ?? []) {
|
|
919
|
-
// `raw` (the actual characters at this span) exists at runtime but isn't in
|
|
920
|
-
// the `mrz` Range type.
|
|
921
|
-
const {
|
|
922
|
-
line,
|
|
923
|
-
start,
|
|
924
|
-
raw = '',
|
|
925
|
-
} = range as typeof range & { raw?: string };
|
|
926
|
-
const buf = buffers[line];
|
|
927
|
-
if (!buf) continue;
|
|
928
|
-
for (let i = 0; i < raw.length && start + i < width; i++) {
|
|
929
|
-
buf[start + i] = raw[i];
|
|
930
|
-
}
|
|
931
|
-
}
|
|
932
|
-
}
|
|
933
|
-
return buffers.map((b) => b.join('')).join('\n');
|
|
1085
|
+
return linesFromParseRanges(result, width, lineCount).join('\n');
|
|
934
1086
|
};
|
|
935
1087
|
|
|
936
1088
|
/**
|
|
@@ -967,6 +1119,10 @@ const validateMRZ = (
|
|
|
967
1119
|
// line + fragments). That must NOT short-circuit the recovery/reconstruction
|
|
968
1120
|
// path below — treat a throw here as "no valid parse yet" and keep going.
|
|
969
1121
|
let result: ReturnType<typeof parse> | null = null;
|
|
1122
|
+
// Set when a TUR card is accepted under the Turkish serial-letter convention
|
|
1123
|
+
// (strict ICAO marks it invalid, but the leading-letter-dropped check passes).
|
|
1124
|
+
// Treated as valid downstream so the real letter is preserved, not corrected.
|
|
1125
|
+
let turkishLetterPrefixAccepted = false;
|
|
970
1126
|
try {
|
|
971
1127
|
result = parse(fixedText, { autocorrect });
|
|
972
1128
|
} catch {
|
|
@@ -994,6 +1150,16 @@ const validateMRZ = (
|
|
|
994
1150
|
}
|
|
995
1151
|
}
|
|
996
1152
|
|
|
1153
|
+
// Turkish e-ID serial-letter convention: a genuine TUR card whose document
|
|
1154
|
+
// number starts with a serial LETTER (e.g. "A53032352") fails the strict
|
|
1155
|
+
// ICAO check (Türkiye omits the leading letter from the check-digit math). If
|
|
1156
|
+
// the card validates under that convention, accept it AS-IS — the leading
|
|
1157
|
+
// letter is REAL and must be preserved, NOT rewritten to a digit by the
|
|
1158
|
+
// ambiguous-char recovery below. See acceptsTurkishLetterPrefix.
|
|
1159
|
+
if (result && !result.valid && acceptsTurkishLetterPrefix(result)) {
|
|
1160
|
+
turkishLetterPrefixAccepted = true;
|
|
1161
|
+
}
|
|
1162
|
+
|
|
997
1163
|
// Primary recovery: exhaustive, CHECK-DIGIT-DRIVEN correction scoped to the
|
|
998
1164
|
// fields whose check digit actually fails. It tries every OCR-ambiguous
|
|
999
1165
|
// permutation within those fields — ordered by how likely the OCR error is
|
|
@@ -1002,7 +1168,7 @@ const validateMRZ = (
|
|
|
1002
1168
|
// digits all pass). This both (a) fixes a misread document number before the
|
|
1003
1169
|
// caller advances to the NFC/next screen, and (b) prefers the genuinely
|
|
1004
1170
|
// correct correction over an arbitrary check-digit-satisfying one.
|
|
1005
|
-
if (!result || !result.valid) {
|
|
1171
|
+
if (!turkishLetterPrefixAccepted && (!result || !result.valid)) {
|
|
1006
1172
|
const fixedLines = fixedText.split('\n').filter((l) => l.trim());
|
|
1007
1173
|
if (fixedLines.length >= 2) {
|
|
1008
1174
|
const recovered = recoverByFailingFields(fixedLines);
|
|
@@ -1015,7 +1181,7 @@ const validateMRZ = (
|
|
|
1015
1181
|
// Last-resort fallback: the older whole-string ambiguous sweep, only for
|
|
1016
1182
|
// inputs the field-scoped recovery can't address (e.g. an unrecognised
|
|
1017
1183
|
// format with no check-digit span map). Still requires a fully-valid parse.
|
|
1018
|
-
if (!result || !result.valid) {
|
|
1184
|
+
if (!turkishLetterPrefixAccepted && (!result || !result.valid)) {
|
|
1019
1185
|
const variants = generateAmbiguousVariants(fixedText);
|
|
1020
1186
|
for (const variant of variants) {
|
|
1021
1187
|
if (recoveryExpired()) break; // bail before blocking the JS thread
|
|
@@ -1037,7 +1203,7 @@ const validateMRZ = (
|
|
|
1037
1203
|
// or broke a field with spaces). Reconstruct fixed-width line-sets for every
|
|
1038
1204
|
// supported format (TD1/TD2/TD3) and validate each WITH CHECK DIGITS, keeping
|
|
1039
1205
|
// only one that passes — a wrong reconstruction can never leak through.
|
|
1040
|
-
if (!result || !result.valid) {
|
|
1206
|
+
if (!turkishLetterPrefixAccepted && (!result || !result.valid)) {
|
|
1041
1207
|
for (const reLines of reconstructMRZCandidates(mrzText)) {
|
|
1042
1208
|
// A reconstructed candidate for the wrong format (e.g. a 2-line TD2 guess
|
|
1043
1209
|
// built from a TD1 frame) makes the `mrz` parser THROW on line count —
|
|
@@ -1059,7 +1225,7 @@ const validateMRZ = (
|
|
|
1059
1225
|
}
|
|
1060
1226
|
}
|
|
1061
1227
|
|
|
1062
|
-
if (!result || !result.valid) {
|
|
1228
|
+
if (!result || (!result.valid && !turkishLetterPrefixAccepted)) {
|
|
1063
1229
|
return {
|
|
1064
1230
|
valid: false,
|
|
1065
1231
|
format: 'UNKNOWN',
|
|
@@ -43,8 +43,29 @@ export interface MRZFrameInput {
|
|
|
43
43
|
* focus-weighted voting beats unweighted.
|
|
44
44
|
*/
|
|
45
45
|
weight?: number;
|
|
46
|
+
/**
|
|
47
|
+
* Optional OCR confidence for this frame, in [0, 1] — e.g. ML Kit's mean
|
|
48
|
+
* per-symbol confidence over the MRZ characters (Android bundled library only;
|
|
49
|
+
* iOS ML Kit exposes no symbol confidence). When present it MULTIPLIES `weight`,
|
|
50
|
+
* so a frame the recognizer was unsure about contributes proportionally fewer
|
|
51
|
+
* votes than a confident one of the same sharpness. This is the per-frame form
|
|
52
|
+
* of confidence-weighted voting: it can't misalign cells (unlike threading
|
|
53
|
+
* per-column confidence through the column-shifting line reconstruction), yet
|
|
54
|
+
* still lets confident frames dominate look-alike ties (A↔4, S↔5, B↔8).
|
|
55
|
+
*
|
|
56
|
+
* A confidence floor is applied so a single near-zero frame is down-weighted
|
|
57
|
+
* but never fully silenced (it may still hold the only vote for some cell).
|
|
58
|
+
*/
|
|
59
|
+
confidence?: number;
|
|
46
60
|
}
|
|
47
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Floor applied to a frame's confidence multiplier. A frame ML Kit was very
|
|
64
|
+
* unsure about still contributes (it may carry the only reading of a cell), but
|
|
65
|
+
* at a fraction of a confident frame's weight.
|
|
66
|
+
*/
|
|
67
|
+
const MIN_CONFIDENCE_WEIGHT = 0.25;
|
|
68
|
+
|
|
48
69
|
export interface MRZConsensus {
|
|
49
70
|
/** Best consensus MRZ (newline-joined fixed-width lines), or null if none yet. */
|
|
50
71
|
mrz: string | null;
|
|
@@ -57,6 +78,18 @@ export interface MRZConsensus {
|
|
|
57
78
|
* last `stabilityTarget` contributing frames. Callers should advance only then.
|
|
58
79
|
*/
|
|
59
80
|
stable: boolean;
|
|
81
|
+
/**
|
|
82
|
+
* How many consecutive contributing frames the current (valid) consensus has
|
|
83
|
+
* stayed unchanged. Diagnostic: convergence speed across devices.
|
|
84
|
+
*/
|
|
85
|
+
stableStreak: number;
|
|
86
|
+
/**
|
|
87
|
+
* Smallest winning vote margin across all CONTESTED cells of the best format
|
|
88
|
+
* (cells where >1 distinct char was voted). Higher = more confident reading;
|
|
89
|
+
* a low value at acceptance means a look-alike was barely resolved. Infinity
|
|
90
|
+
* when no cell is contested (a perfectly clean read). Diagnostic only.
|
|
91
|
+
*/
|
|
92
|
+
minContestedMargin: number;
|
|
60
93
|
}
|
|
61
94
|
|
|
62
95
|
export interface MRZFrameAggregatorOptions {
|
|
@@ -67,6 +100,12 @@ export interface MRZFrameAggregatorOptions {
|
|
|
67
100
|
stabilityTarget?: number;
|
|
68
101
|
/** Drop the running tallies after this many frames to bound memory. Default 60. */
|
|
69
102
|
maxFrames?: number;
|
|
103
|
+
/**
|
|
104
|
+
* Minimum winning vote margin a contested cell needs before the consensus can
|
|
105
|
+
* be considered stable. Higher = more frames required to resolve a look-alike
|
|
106
|
+
* ambiguity (A↔4, S↔5) before accepting. Default 2.
|
|
107
|
+
*/
|
|
108
|
+
minMargin?: number;
|
|
70
109
|
}
|
|
71
110
|
|
|
72
111
|
interface CellVotes {
|
|
@@ -157,6 +196,10 @@ const linesForFrame = (text: string): string[] | null => {
|
|
|
157
196
|
export class MRZFrameAggregator {
|
|
158
197
|
private readonly stabilityTarget: number;
|
|
159
198
|
private readonly maxFrames: number;
|
|
199
|
+
// Minimum winning vote margin a CONTESTED cell needs before the consensus is
|
|
200
|
+
// considered confident (and thus eligible to be "stable"). Guards against an
|
|
201
|
+
// early look-alike misread (A↔4, S↔5, B↔8) locking in on a 1-vote lead.
|
|
202
|
+
private readonly minMargin: number;
|
|
160
203
|
// format -> per-line, per-column vote tallies
|
|
161
204
|
private tallies: Record<string, CellVotes[][]> = {};
|
|
162
205
|
private frameCount = 0;
|
|
@@ -168,6 +211,7 @@ export class MRZFrameAggregator {
|
|
|
168
211
|
constructor(options: MRZFrameAggregatorOptions = {}) {
|
|
169
212
|
this.stabilityTarget = Math.max(1, options.stabilityTarget ?? 2);
|
|
170
213
|
this.maxFrames = Math.max(1, options.maxFrames ?? 60);
|
|
214
|
+
this.minMargin = Math.max(0, options.minMargin ?? 2);
|
|
171
215
|
}
|
|
172
216
|
|
|
173
217
|
reset(): void {
|
|
@@ -233,7 +277,15 @@ export class MRZFrameAggregator {
|
|
|
233
277
|
* consensus is returned.
|
|
234
278
|
*/
|
|
235
279
|
addFrame(input: MRZFrameInput): MRZConsensus {
|
|
236
|
-
const
|
|
280
|
+
const baseWeight = input.weight && input.weight > 0 ? input.weight : 1;
|
|
281
|
+
// Fold OCR confidence into the weight (clamped to [floor, 1]). Absent
|
|
282
|
+
// confidence leaves the weight unchanged, so callers without per-symbol
|
|
283
|
+
// confidence (e.g. iOS ML Kit) keep the prior sharpness-only behaviour.
|
|
284
|
+
const confidenceFactor =
|
|
285
|
+
typeof input.confidence === 'number' && Number.isFinite(input.confidence)
|
|
286
|
+
? Math.min(1, Math.max(MIN_CONFIDENCE_WEIGHT, input.confidence))
|
|
287
|
+
: 1;
|
|
288
|
+
const weight = baseWeight * confidenceFactor;
|
|
237
289
|
const lines = linesForFrame(input.text);
|
|
238
290
|
|
|
239
291
|
if (lines) {
|
|
@@ -285,6 +337,47 @@ export class MRZFrameAggregator {
|
|
|
285
337
|
this.frameCount = Math.ceil(this.frameCount / 2);
|
|
286
338
|
}
|
|
287
339
|
|
|
340
|
+
/**
|
|
341
|
+
* Is every CONTESTED cell of this format decided by a clear margin?
|
|
342
|
+
*
|
|
343
|
+
* A cell is contested when more than one distinct character has received votes
|
|
344
|
+
* (e.g. position 5 of a Turkish doc number gets both "A" and the look-alike
|
|
345
|
+
* "4"). Per-character voting alone reports the consensus "stable" the instant an
|
|
346
|
+
* early misread takes a 1-vote lead and stays unchanged — so a transient
|
|
347
|
+
* "A"→"4" confusion locks in before the correct glyph accumulates enough votes.
|
|
348
|
+
*
|
|
349
|
+
* Requiring the winner to lead the runner-up by `minMargin` weight on every
|
|
350
|
+
* contested cell defers stability until the vote has genuinely settled: a 2-vs-1
|
|
351
|
+
* early lead is NOT confident; a 4-vs-1 (or clean uncontested) reading is. This
|
|
352
|
+
* is what turns "looks valid this frame" into "the camera agrees across frames".
|
|
353
|
+
*/
|
|
354
|
+
private consensusIsConfident(format: string): boolean {
|
|
355
|
+
return this.minContestedMargin(format) >= this.minMargin;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Smallest winning vote margin across all CONTESTED cells (where >1 distinct
|
|
360
|
+
* char was voted). Returns Infinity when no cell is contested. Used both to
|
|
361
|
+
* gate confidence and to surface a diagnostic of how narrowly a look-alike was
|
|
362
|
+
* resolved at acceptance.
|
|
363
|
+
*/
|
|
364
|
+
private minContestedMargin(format: string): number {
|
|
365
|
+
const geom = FORMAT_GEOMETRY[format];
|
|
366
|
+
const tally = this.tallies[format];
|
|
367
|
+
let min = Infinity;
|
|
368
|
+
for (let li = 0; li < geom.lines; li++) {
|
|
369
|
+
for (let ci = 0; ci < geom.width; ci++) {
|
|
370
|
+
const cell = tally[li][ci];
|
|
371
|
+
const weights = Object.values(cell);
|
|
372
|
+
if (weights.length < 2) continue; // uncontested cell — no ambiguity
|
|
373
|
+
weights.sort((a, b) => b - a);
|
|
374
|
+
const margin = weights[0] - weights[1];
|
|
375
|
+
if (margin < min) min = margin;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return min;
|
|
379
|
+
}
|
|
380
|
+
|
|
288
381
|
/** Read out the winning character per cell for a format. */
|
|
289
382
|
private consensusLines(format: string): string[] {
|
|
290
383
|
const geom = FORMAT_GEOMETRY[format];
|
|
@@ -325,12 +418,15 @@ export class MRZFrameAggregator {
|
|
|
325
418
|
validation: null,
|
|
326
419
|
frames: this.frameCount,
|
|
327
420
|
stable: false,
|
|
421
|
+
stableStreak: 0,
|
|
422
|
+
minContestedMargin: Infinity,
|
|
328
423
|
};
|
|
329
424
|
}
|
|
330
425
|
|
|
331
426
|
let bestMrz: string | null = null;
|
|
332
427
|
let bestValidation: MRZValidationResult | null = null;
|
|
333
428
|
let bestValid = false;
|
|
429
|
+
let bestFormat: string | null = null;
|
|
334
430
|
|
|
335
431
|
for (const format of formats) {
|
|
336
432
|
const lines = this.consensusLines(format);
|
|
@@ -342,9 +438,11 @@ export class MRZFrameAggregator {
|
|
|
342
438
|
bestValid = true;
|
|
343
439
|
bestMrz = validation.correctedMrz ?? mrz;
|
|
344
440
|
bestValidation = validation;
|
|
441
|
+
bestFormat = format;
|
|
345
442
|
} else if (!bestValid && bestMrz === null) {
|
|
346
443
|
bestMrz = mrz;
|
|
347
444
|
bestValidation = validation;
|
|
445
|
+
bestFormat = format;
|
|
348
446
|
}
|
|
349
447
|
}
|
|
350
448
|
|
|
@@ -358,11 +456,23 @@ export class MRZFrameAggregator {
|
|
|
358
456
|
}
|
|
359
457
|
this.lastConsensusMrz = stableKey;
|
|
360
458
|
|
|
459
|
+
// "Stable" requires BOTH an unchanged streak AND a confident vote: every
|
|
460
|
+
// contested cell must have resolved by a clear margin. The streak alone lets
|
|
461
|
+
// an early look-alike misread (A↔4) lock in the instant it takes a 1-vote
|
|
462
|
+
// lead and stops changing — the confidence gate defers acceptance until the
|
|
463
|
+
// correct glyph has genuinely won, not merely arrived first.
|
|
464
|
+
const confident =
|
|
465
|
+
bestValid && bestFormat != null && this.consensusIsConfident(bestFormat);
|
|
466
|
+
|
|
361
467
|
return {
|
|
362
468
|
mrz: bestMrz,
|
|
363
469
|
validation: bestValidation,
|
|
364
470
|
frames: this.frameCount,
|
|
365
|
-
stable:
|
|
471
|
+
stable:
|
|
472
|
+
bestValid && confident && this.stableStreak >= this.stabilityTarget,
|
|
473
|
+
stableStreak: this.stableStreak,
|
|
474
|
+
minContestedMargin:
|
|
475
|
+
bestFormat != null ? this.minContestedMargin(bestFormat) : Infinity,
|
|
366
476
|
};
|
|
367
477
|
}
|
|
368
478
|
}
|
|
@@ -1,6 +1,122 @@
|
|
|
1
1
|
import { Platform } from 'react-native';
|
|
2
2
|
import DeviceInfo from 'react-native-device-info';
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Full, non-PII device details for the support diagnostic report. Device-
|
|
6
|
+
* specific scanning/NFC issues correlate with hardware, OS level, memory and
|
|
7
|
+
* NFC capability, so we capture a broad-but-safe set. Excludes anything that
|
|
8
|
+
* identifies a person (uniqueId is a per-install vendor id, not PII, and is kept
|
|
9
|
+
* only for correlating repeat reports from the same device).
|
|
10
|
+
*/
|
|
11
|
+
export interface DeviceDetails {
|
|
12
|
+
platform: string; // 'ios' | 'android'
|
|
13
|
+
systemName: string; // 'iOS' | 'Android'
|
|
14
|
+
systemVersion: string;
|
|
15
|
+
apiLevel?: number; // Android API level
|
|
16
|
+
brand: string;
|
|
17
|
+
manufacturer: string;
|
|
18
|
+
model: string;
|
|
19
|
+
deviceId: string; // hardware model code, e.g. "iPhone16,1" / "tegu"
|
|
20
|
+
deviceType: string; // Handset / Tablet / …
|
|
21
|
+
isTablet: boolean;
|
|
22
|
+
isEmulator: boolean;
|
|
23
|
+
hasNotch: boolean;
|
|
24
|
+
totalMemoryMB?: number;
|
|
25
|
+
usedMemoryMB?: number;
|
|
26
|
+
totalDiskMB?: number;
|
|
27
|
+
freeDiskMB?: number;
|
|
28
|
+
batteryLevel?: number; // 0..1
|
|
29
|
+
appVersion: string;
|
|
30
|
+
buildNumber: string;
|
|
31
|
+
bundleId: string;
|
|
32
|
+
fontScale?: number;
|
|
33
|
+
installerPackage?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Resolve an async getter to a number, tolerating failure. */
|
|
37
|
+
const num = async (p: Promise<number>): Promise<number | undefined> => {
|
|
38
|
+
try {
|
|
39
|
+
const v = await p;
|
|
40
|
+
return typeof v === 'number' && isFinite(v) ? v : undefined;
|
|
41
|
+
} catch {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/** Resolve an async getter, falling back on failure. */
|
|
47
|
+
const safe = async <T>(p: Promise<T>, fallback: T): Promise<T> => {
|
|
48
|
+
try {
|
|
49
|
+
return await p;
|
|
50
|
+
} catch {
|
|
51
|
+
return fallback;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/** Read a synchronous getter, tolerating failure. */
|
|
56
|
+
const sync = <T>(fn: () => T, fallback: T): T => {
|
|
57
|
+
try {
|
|
58
|
+
return fn();
|
|
59
|
+
} catch {
|
|
60
|
+
return fallback;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/** Gather the full non-PII device profile for a diagnostic report. */
|
|
65
|
+
export const gatherDeviceDetails = async (): Promise<DeviceDetails> => {
|
|
66
|
+
const bytesToMB = (b?: number) =>
|
|
67
|
+
b === undefined ? undefined : Math.round(b / (1024 * 1024));
|
|
68
|
+
const isAndroid = Platform.OS === 'android';
|
|
69
|
+
|
|
70
|
+
const [
|
|
71
|
+
manufacturer,
|
|
72
|
+
isEmulator,
|
|
73
|
+
apiLevel,
|
|
74
|
+
installerPackage,
|
|
75
|
+
totalMemory,
|
|
76
|
+
usedMemory,
|
|
77
|
+
totalDisk,
|
|
78
|
+
freeDisk,
|
|
79
|
+
batteryLevel,
|
|
80
|
+
fontScale,
|
|
81
|
+
] = await Promise.all([
|
|
82
|
+
safe(DeviceInfo.getManufacturer(), 'unknown'),
|
|
83
|
+
safe(DeviceInfo.isEmulator(), false),
|
|
84
|
+
isAndroid ? num(DeviceInfo.getApiLevel()) : Promise.resolve(undefined),
|
|
85
|
+
safe(DeviceInfo.getInstallerPackageName(), 'unknown'),
|
|
86
|
+
num(DeviceInfo.getTotalMemory()),
|
|
87
|
+
num(DeviceInfo.getUsedMemory()),
|
|
88
|
+
num(DeviceInfo.getTotalDiskCapacity()),
|
|
89
|
+
num(DeviceInfo.getFreeDiskStorage()),
|
|
90
|
+
num(DeviceInfo.getBatteryLevel()),
|
|
91
|
+
num(DeviceInfo.getFontScale()),
|
|
92
|
+
]);
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
platform: Platform.OS,
|
|
96
|
+
systemName: isAndroid ? 'Android' : 'iOS',
|
|
97
|
+
systemVersion: sync(() => DeviceInfo.getSystemVersion(), 'unknown'),
|
|
98
|
+
apiLevel,
|
|
99
|
+
brand: sync(() => DeviceInfo.getBrand(), 'unknown'),
|
|
100
|
+
manufacturer,
|
|
101
|
+
model: sync(() => DeviceInfo.getModel(), 'unknown'),
|
|
102
|
+
deviceId: sync(() => DeviceInfo.getDeviceId(), 'unknown'),
|
|
103
|
+
deviceType: sync(() => DeviceInfo.getDeviceType(), 'unknown'),
|
|
104
|
+
isTablet: sync(() => DeviceInfo.isTablet(), false),
|
|
105
|
+
isEmulator,
|
|
106
|
+
hasNotch: sync(() => DeviceInfo.hasNotch(), false),
|
|
107
|
+
totalMemoryMB: bytesToMB(totalMemory),
|
|
108
|
+
usedMemoryMB: bytesToMB(usedMemory),
|
|
109
|
+
totalDiskMB: bytesToMB(totalDisk),
|
|
110
|
+
freeDiskMB: bytesToMB(freeDisk),
|
|
111
|
+
batteryLevel,
|
|
112
|
+
appVersion: sync(() => DeviceInfo.getVersion(), 'unknown'),
|
|
113
|
+
buildNumber: sync(() => DeviceInfo.getBuildNumber(), 'unknown'),
|
|
114
|
+
bundleId: sync(() => DeviceInfo.getBundleId(), 'unknown'),
|
|
115
|
+
fontScale,
|
|
116
|
+
installerPackage,
|
|
117
|
+
};
|
|
118
|
+
};
|
|
119
|
+
|
|
4
120
|
export const NativeDeviceInfo = {
|
|
5
121
|
generateHumanReadableIdentifier: async (): Promise<string> => {
|
|
6
122
|
try {
|