@trustchex/react-native-sdk 1.464.0 → 1.475.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/build.gradle +3 -3
- package/android/src/main/java/com/trustchex/reactnativesdk/camera/TrustchexCameraView.kt +190 -12
- package/ios/Camera/TrustchexCameraView.swift +244 -46
- package/ios/Permission/PermissionModule.m +22 -0
- package/ios/Permission/PermissionModule.swift +67 -0
- package/lib/module/Screens/Debug/MRZTestScreen.js +121 -147
- package/lib/module/Screens/Dynamic/LivenessDetectionScreen.js +24 -3
- package/lib/module/Screens/Dynamic/VerbalConsentScreen.js +1 -1
- package/lib/module/Screens/Dynamic/VideoCallScreen.js +7 -12
- package/lib/module/Screens/Static/ResultScreen.js +9 -1
- package/lib/module/Shared/Components/EIDScanner.js +166 -163
- package/lib/module/Shared/Components/FaceCamera.js +14 -8
- package/lib/module/Shared/Components/IdentityDocumentCamera.js +45 -10
- package/lib/module/Shared/Libs/MRZ_KNOWN_ISSUES.md +112 -0
- package/lib/module/Shared/Libs/PERMISSIONS_MANAGER.md +268 -0
- package/lib/module/Shared/Libs/index.js +20 -0
- package/lib/module/Shared/Libs/mrz.utils.js +570 -16
- package/lib/module/Shared/Libs/mrzFrameAggregator.js +301 -0
- package/lib/module/Shared/Libs/mrzOcrIntegration.js +109 -0
- package/lib/module/Shared/Libs/permissions.utils.js +199 -0
- package/lib/module/Translation/Resources/en.js +1 -0
- package/lib/module/Translation/Resources/tr.js +1 -0
- package/lib/module/version.js +1 -1
- package/lib/typescript/src/Screens/Debug/MRZTestScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Dynamic/LivenessDetectionScreen.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/EIDScanner.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/FaceCamera.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/index.d.ts +20 -0
- package/lib/typescript/src/Shared/Libs/index.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Libs/mrz.utils.d.ts +8 -0
- package/lib/typescript/src/Shared/Libs/mrz.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/mrzFrameAggregator.d.ts +76 -0
- package/lib/typescript/src/Shared/Libs/mrzFrameAggregator.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Libs/mrzOcrIntegration.d.ts +73 -0
- package/lib/typescript/src/Shared/Libs/mrzOcrIntegration.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Libs/permissions.utils.d.ts +58 -0
- package/lib/typescript/src/Shared/Libs/permissions.utils.d.ts.map +1 -0
- package/lib/typescript/src/Translation/Resources/en.d.ts +1 -0
- package/lib/typescript/src/Translation/Resources/en.d.ts.map +1 -1
- package/lib/typescript/src/Translation/Resources/tr.d.ts +1 -0
- package/lib/typescript/src/Translation/Resources/tr.d.ts.map +1 -1
- package/lib/typescript/src/version.d.ts +1 -1
- package/package.json +14 -10
- package/src/Screens/Debug/MRZTestScreen.tsx +137 -166
- package/src/Screens/Dynamic/LivenessDetectionScreen.tsx +45 -5
- package/src/Screens/Dynamic/VerbalConsentScreen.tsx +1 -1
- package/src/Screens/Dynamic/VideoCallScreen.tsx +8 -19
- package/src/Screens/Static/ResultScreen.tsx +18 -9
- package/src/Shared/Components/EIDScanner.tsx +210 -160
- package/src/Shared/Components/FaceCamera.tsx +19 -16
- package/src/Shared/Components/IdentityDocumentCamera.tsx +53 -13
- package/src/Shared/Libs/MRZ_KNOWN_ISSUES.md +112 -0
- package/src/Shared/Libs/PERMISSIONS_MANAGER.md +268 -0
- package/src/Shared/Libs/index.ts +63 -0
- package/src/Shared/Libs/mrz.utils.ts +639 -11
- package/src/Shared/Libs/mrzFrameAggregator.ts +370 -0
- package/src/Shared/Libs/mrzOcrIntegration.ts +175 -0
- package/src/Shared/Libs/permissions.utils.ts +251 -0
- package/src/Translation/Resources/en.ts +1 -0
- package/src/Translation/Resources/tr.ts +1 -0
- package/src/version.ts +1 -1
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import mrzUtils from "./mrz.utils.js";
|
|
4
|
+
/**
|
|
5
|
+
* Multi-frame MRZ aggregator: per-character voting across video frames.
|
|
6
|
+
*
|
|
7
|
+
* A single OCR frame of a glossy ID card is noisy — different frames misread
|
|
8
|
+
* different characters (B↔8, S↔5, O↔0, <↔K, dropped/inserted digits). Instead of
|
|
9
|
+
* trusting one frame (or requiring N identical frames, which a single misread
|
|
10
|
+
* resets), this accumulates a per-position character vote across frames and reads
|
|
11
|
+
* out the majority consensus. Frames vote in proportion to a quality weight, so a
|
|
12
|
+
* sharper/closer frame counts more than a blurry one.
|
|
13
|
+
*
|
|
14
|
+
* This is a JS-only, ROVER-inspired post-OCR fusion (Recognizer Output Voting
|
|
15
|
+
* Error Reduction): each frame is first normalised into fixed-width MRZ lines via
|
|
16
|
+
* the existing reconstruction, lines are aligned by index and column, votes are
|
|
17
|
+
* tallied per (line, column), and the winning character per cell forms the
|
|
18
|
+
* consensus MRZ — which is then validated with check digits. Research shows
|
|
19
|
+
* post-OCR result integration beats single-frame OCR and image super-resolution
|
|
20
|
+
* for MRZ in a video stream.
|
|
21
|
+
*
|
|
22
|
+
* Why per-character voting over "N identical reads": a frame that misreads ONE
|
|
23
|
+
* character still contributes correct votes for the other 89 positions, so the
|
|
24
|
+
* consensus converges far faster and is robust to sporadic OCR errors.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/** Fixed line geometry per ICAO 9303 format. */
|
|
28
|
+
const FORMAT_GEOMETRY = {
|
|
29
|
+
TD1: {
|
|
30
|
+
lines: 3,
|
|
31
|
+
width: 30
|
|
32
|
+
},
|
|
33
|
+
TD2: {
|
|
34
|
+
lines: 2,
|
|
35
|
+
width: 36
|
|
36
|
+
},
|
|
37
|
+
TD3: {
|
|
38
|
+
lines: 2,
|
|
39
|
+
width: 44
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
const MRZ_CHAR = /^[A-Z0-9<]$/;
|
|
43
|
+
/**
|
|
44
|
+
* Reconstructs fixed-width MRZ candidate line-sets for a frame's raw OCR text.
|
|
45
|
+
* Reuses the same reconstruction the single-frame validator uses, so the
|
|
46
|
+
* aggregator sees the SAME normalised lines (filler/doc-code fixes applied),
|
|
47
|
+
* just fused across frames. Returns the best (longest, most filled) candidate.
|
|
48
|
+
*/
|
|
49
|
+
/** Does this exact line-set already match a known fixed-width MRZ format? */
|
|
50
|
+
const exactFormatLines = lines => {
|
|
51
|
+
for (const g of Object.values(FORMAT_GEOMETRY)) {
|
|
52
|
+
if (lines.length === g.lines && lines.every(l => l.length === g.width)) {
|
|
53
|
+
return lines;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
};
|
|
58
|
+
const linesForFrame = text => {
|
|
59
|
+
// Fast path: if the raw OCR already splits into clean fixed-width MRZ lines of
|
|
60
|
+
// a known format, use them directly. Reconstruction is tuned for noisy TD1 and
|
|
61
|
+
// can mis-shape an already-clean TD2/TD3 (e.g. force a 2×44 passport into a
|
|
62
|
+
// 3×30 layout), so prefer the as-is lines when they're exact.
|
|
63
|
+
const rawLines = text.split('\n').map(l => l.trim().replace(/[^A-Z0-9<]/gi, '').toUpperCase()).filter(Boolean);
|
|
64
|
+
const exact = exactFormatLines(rawLines);
|
|
65
|
+
if (exact) return exact;
|
|
66
|
+
const candidates = mrzUtils.reconstructMRZCandidates(text);
|
|
67
|
+
if (!candidates.length) {
|
|
68
|
+
// Fall back to the lightweight fixMRZ split for already-clean text.
|
|
69
|
+
const fixed = mrzUtils.fixMRZ(text).split('\n').map(l => l.trim()).filter(Boolean);
|
|
70
|
+
return fixed.length >= 2 ? fixed : null;
|
|
71
|
+
}
|
|
72
|
+
// Pick the best candidate and coerce it to an exact format geometry so it can
|
|
73
|
+
// be voted. A frame whose reconstruction is ALMOST a format (right line count,
|
|
74
|
+
// lines within a few chars of the width) still carries useful per-character
|
|
75
|
+
// votes — pad/truncate each line to the format width rather than discarding the
|
|
76
|
+
// whole frame. This keeps the vote accumulating every frame during continuous
|
|
77
|
+
// scanning instead of stalling on the occasional perfectly-shaped frame.
|
|
78
|
+
let best = null;
|
|
79
|
+
let bestScore = -1;
|
|
80
|
+
for (const cand of candidates) {
|
|
81
|
+
const geom = Object.values(FORMAT_GEOMETRY).find(g => g.lines === cand.length);
|
|
82
|
+
if (!geom) continue;
|
|
83
|
+
// Reject if any line is wildly off the expected width (likely not the band).
|
|
84
|
+
if (cand.some(l => Math.abs(l.length - geom.width) > 6)) continue;
|
|
85
|
+
const coerced = cand.map(l => l.length >= geom.width ? l.slice(0, geom.width) : l.padEnd(geom.width, '<'));
|
|
86
|
+
const isExactWidth = cand.every(l => l.length === geom.width);
|
|
87
|
+
const filled = coerced.join('').replace(/</g, '').length;
|
|
88
|
+
// Exact-shaped frames score a little higher, but near-shaped still qualify.
|
|
89
|
+
const score = (isExactWidth ? 10000 : 0) + filled;
|
|
90
|
+
if (score > bestScore) {
|
|
91
|
+
bestScore = score;
|
|
92
|
+
best = coerced;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return best;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Stateful aggregator. Feed it one frame's OCR text at a time via `addFrame`;
|
|
100
|
+
* after each it returns the current per-character-voted consensus and whether it
|
|
101
|
+
* is valid + stable. Reset between documents with `reset`.
|
|
102
|
+
*/
|
|
103
|
+
export class MRZFrameAggregator {
|
|
104
|
+
// format -> per-line, per-column vote tallies
|
|
105
|
+
tallies = {};
|
|
106
|
+
frameCount = 0;
|
|
107
|
+
lastConsensusMrz = null;
|
|
108
|
+
stableStreak = 0;
|
|
109
|
+
// Consecutive frames whose identifying region disagrees with the consensus.
|
|
110
|
+
mismatchStreak = 0;
|
|
111
|
+
constructor(options = {}) {
|
|
112
|
+
this.stabilityTarget = Math.max(1, options.stabilityTarget ?? 2);
|
|
113
|
+
this.maxFrames = Math.max(1, options.maxFrames ?? 60);
|
|
114
|
+
}
|
|
115
|
+
reset() {
|
|
116
|
+
this.tallies = {};
|
|
117
|
+
this.frameCount = 0;
|
|
118
|
+
this.lastConsensusMrz = null;
|
|
119
|
+
this.stableStreak = 0;
|
|
120
|
+
this.mismatchStreak = 0;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Returns true when the incoming frame's identifying region (the line-1 data
|
|
125
|
+
* span that holds the document number) has disagreed with the current
|
|
126
|
+
* consensus for enough consecutive frames to conclude a NEW document is in
|
|
127
|
+
* view — so the caller should drop the stale votes. A few disagreeing frames
|
|
128
|
+
* are tolerated as OCR noise; a sustained disagreement is a document swap.
|
|
129
|
+
*/
|
|
130
|
+
detectDocumentChange(format, lines) {
|
|
131
|
+
const tally = this.tallies[format];
|
|
132
|
+
if (!tally || this.frameCount < 3) {
|
|
133
|
+
this.mismatchStreak = 0;
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
// Compare the document-number-bearing region of line 1 (positions 5..14 in
|
|
137
|
+
// TD1; positions 0..9 in TD2/TD3) against the current per-cell winners.
|
|
138
|
+
const [start, end] = format === 'TD1' ? [5, 14] : [0, 9];
|
|
139
|
+
const consensusL1 = this.consensusLines(format)[format === 'TD1' ? 0 : 1];
|
|
140
|
+
const frameL1 = lines[format === 'TD1' ? 0 : 1] ?? '';
|
|
141
|
+
let diffs = 0;
|
|
142
|
+
let compared = 0;
|
|
143
|
+
for (let i = start; i < end; i++) {
|
|
144
|
+
const a = consensusL1[i];
|
|
145
|
+
const b = frameL1[i];
|
|
146
|
+
if (a === '<' || b === '<' || b === undefined) continue; // skip fillers/gaps
|
|
147
|
+
compared++;
|
|
148
|
+
if (a !== b) diffs++;
|
|
149
|
+
}
|
|
150
|
+
// Majority of the doc-number characters differ → this frame is a different doc.
|
|
151
|
+
const isMismatch = compared >= 4 && diffs >= Math.ceil(compared / 2);
|
|
152
|
+
this.mismatchStreak = isMismatch ? this.mismatchStreak + 1 : 0;
|
|
153
|
+
return this.mismatchStreak >= 3;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Frames contributed so far. */
|
|
157
|
+
get frames() {
|
|
158
|
+
return this.frameCount;
|
|
159
|
+
}
|
|
160
|
+
ensureTally(format) {
|
|
161
|
+
if (!this.tallies[format]) {
|
|
162
|
+
const geom = FORMAT_GEOMETRY[format];
|
|
163
|
+
this.tallies[format] = Array.from({
|
|
164
|
+
length: geom.lines
|
|
165
|
+
}, () => Array.from({
|
|
166
|
+
length: geom.width
|
|
167
|
+
}, () => ({})));
|
|
168
|
+
}
|
|
169
|
+
return this.tallies[format];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Ingest one frame and return the current consensus. The frame's lines are
|
|
174
|
+
* voted into the tally for whichever format they match; the consensus is then
|
|
175
|
+
* read out per format and validated, and the best valid (or best-effort)
|
|
176
|
+
* consensus is returned.
|
|
177
|
+
*/
|
|
178
|
+
addFrame(input) {
|
|
179
|
+
const weight = input.weight && input.weight > 0 ? input.weight : 1;
|
|
180
|
+
const lines = linesForFrame(input.text);
|
|
181
|
+
if (lines) {
|
|
182
|
+
// Determine the format this frame's lines fit (by line count + width).
|
|
183
|
+
const format = Object.keys(FORMAT_GEOMETRY).find(f => {
|
|
184
|
+
const g = FORMAT_GEOMETRY[f];
|
|
185
|
+
return lines.length === g.lines && lines.every(l => l.length === g.width);
|
|
186
|
+
});
|
|
187
|
+
if (format) {
|
|
188
|
+
// Auto-reset when a DIFFERENT document is shown: if this frame's
|
|
189
|
+
// identifying region (line 1 data, where the doc number lives) disagrees
|
|
190
|
+
// strongly with the accumulated consensus for several consecutive frames,
|
|
191
|
+
// the votes are stale — clear them so the new document can converge
|
|
192
|
+
// instead of staying "stable" on the previous one.
|
|
193
|
+
if (this.detectDocumentChange(format, lines)) {
|
|
194
|
+
this.reset();
|
|
195
|
+
}
|
|
196
|
+
const tally = this.ensureTally(format);
|
|
197
|
+
for (let li = 0; li < lines.length; li++) {
|
|
198
|
+
const line = lines[li];
|
|
199
|
+
for (let ci = 0; ci < line.length; ci++) {
|
|
200
|
+
const ch = line[ci];
|
|
201
|
+
if (!MRZ_CHAR.test(ch)) continue;
|
|
202
|
+
const cell = tally[li][ci];
|
|
203
|
+
cell[ch] = (cell[ch] ?? 0) + weight;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
this.frameCount++;
|
|
207
|
+
if (this.frameCount > this.maxFrames) this.decay();
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return this.currentConsensus();
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Halve all tallies to bound memory while preserving relative vote ratios. */
|
|
214
|
+
decay() {
|
|
215
|
+
for (const format of Object.keys(this.tallies)) {
|
|
216
|
+
for (const lineCells of this.tallies[format]) {
|
|
217
|
+
for (const cell of lineCells) {
|
|
218
|
+
for (const ch of Object.keys(cell)) {
|
|
219
|
+
cell[ch] /= 2;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
this.frameCount = Math.ceil(this.frameCount / 2);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Read out the winning character per cell for a format. */
|
|
228
|
+
consensusLines(format) {
|
|
229
|
+
const geom = FORMAT_GEOMETRY[format];
|
|
230
|
+
const tally = this.tallies[format];
|
|
231
|
+
const lines = [];
|
|
232
|
+
for (let li = 0; li < geom.lines; li++) {
|
|
233
|
+
let line = '';
|
|
234
|
+
for (let ci = 0; ci < geom.width; ci++) {
|
|
235
|
+
const cell = tally[li][ci];
|
|
236
|
+
let bestCh = '<';
|
|
237
|
+
let bestW = -1;
|
|
238
|
+
for (const ch of Object.keys(cell)) {
|
|
239
|
+
// Tie-break deterministically: higher weight wins; on a tie a non-filler
|
|
240
|
+
// beats a filler (fillers are the "no information" default), else lexical.
|
|
241
|
+
const w = cell[ch];
|
|
242
|
+
if (w > bestW || w === bestW && bestCh === '<' && ch !== '<' || w === bestW && bestCh !== '<' && ch !== '<' && ch < bestCh) {
|
|
243
|
+
bestW = w;
|
|
244
|
+
bestCh = ch;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
line += bestW < 0 ? '<' : bestCh;
|
|
248
|
+
}
|
|
249
|
+
lines.push(line);
|
|
250
|
+
}
|
|
251
|
+
return lines;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** Best consensus across all formats seen, validated with check digits. */
|
|
255
|
+
currentConsensus() {
|
|
256
|
+
const formats = Object.keys(this.tallies);
|
|
257
|
+
if (!formats.length) {
|
|
258
|
+
return {
|
|
259
|
+
mrz: null,
|
|
260
|
+
validation: null,
|
|
261
|
+
frames: this.frameCount,
|
|
262
|
+
stable: false
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
let bestMrz = null;
|
|
266
|
+
let bestValidation = null;
|
|
267
|
+
let bestValid = false;
|
|
268
|
+
for (const format of formats) {
|
|
269
|
+
const lines = this.consensusLines(format);
|
|
270
|
+
const mrz = lines.join('\n');
|
|
271
|
+
const validation = mrzUtils.validateMRZWithCorrections(mrz);
|
|
272
|
+
// Prefer a valid consensus; among valid, prefer the one the validator could
|
|
273
|
+
// fully correct (it sets correctedMrz). Among invalid, keep the first.
|
|
274
|
+
if (validation.valid && !bestValid) {
|
|
275
|
+
bestValid = true;
|
|
276
|
+
bestMrz = validation.correctedMrz ?? mrz;
|
|
277
|
+
bestValidation = validation;
|
|
278
|
+
} else if (!bestValid && bestMrz === null) {
|
|
279
|
+
bestMrz = mrz;
|
|
280
|
+
bestValidation = validation;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Stability: the (valid) consensus must be unchanged across consecutive
|
|
285
|
+
// contributing frames. Track the streak on the corrected/consensus string.
|
|
286
|
+
const stableKey = bestValid ? bestMrz : null;
|
|
287
|
+
if (stableKey && stableKey === this.lastConsensusMrz) {
|
|
288
|
+
this.stableStreak++;
|
|
289
|
+
} else {
|
|
290
|
+
this.stableStreak = stableKey ? 1 : 0;
|
|
291
|
+
}
|
|
292
|
+
this.lastConsensusMrz = stableKey;
|
|
293
|
+
return {
|
|
294
|
+
mrz: bestMrz,
|
|
295
|
+
validation: bestValidation,
|
|
296
|
+
frames: this.frameCount,
|
|
297
|
+
stable: bestValid && this.stableStreak >= this.stabilityTarget
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
export default MRZFrameAggregator;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { NativeModules } from 'react-native';
|
|
4
|
+
import mrzUtils from "./mrz.utils.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* On-device MRZ ↔ OCR integration test harness.
|
|
8
|
+
*
|
|
9
|
+
* Runs the REAL ML Kit v2 text recognizer (via the native `MLKitModule`) on a
|
|
10
|
+
* still test image, assembles the recognized text exactly like the camera path
|
|
11
|
+
* does, feeds it through `validateMRZWithCorrections`, and reports whether the
|
|
12
|
+
* OCR-misread MRZ was corrected to a check-digit-valid result.
|
|
13
|
+
*
|
|
14
|
+
* This must run on a device/emulator (ML Kit is native) — it is not a jest test.
|
|
15
|
+
* Feed it the synthetic MRZ images under
|
|
16
|
+
* `src/Screens/Debug/__fixtures__/mrz-images/` (generated by
|
|
17
|
+
* `scripts/gen-mrz-test-images.py`), e.g. read with react-native-fs and pass the
|
|
18
|
+
* base64 to `runMrzOcrSuite`. The check-digit-driven correction in
|
|
19
|
+
* `mrz.utils.ts` is what these images exercise end-to-end against real OCR.
|
|
20
|
+
*
|
|
21
|
+
* ML Kit Text Recognition v2:
|
|
22
|
+
* https://developers.google.com/ml-kit/vision/text-recognition/v2
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const MLKit = NativeModules.MLKitModule;
|
|
26
|
+
|
|
27
|
+
/** A single image-based test case. */
|
|
28
|
+
|
|
29
|
+
/** Assemble the OCR blocks into newline-separated text, mirroring the camera path. */
|
|
30
|
+
export const assembleOcrText = blocks => blocks.map(b => (b.text ?? '').trim()).filter(t => t.length > 0).join('\n');
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Runs one image through real ML Kit OCR + MRZ correction and checks expectations.
|
|
34
|
+
*/
|
|
35
|
+
export const runMrzOcrCase = async (testCase, now = Date.now) => {
|
|
36
|
+
const start = now();
|
|
37
|
+
if (!MLKit?.recognizeText) {
|
|
38
|
+
return {
|
|
39
|
+
name: testCase.name,
|
|
40
|
+
passed: false,
|
|
41
|
+
rawOcr: '',
|
|
42
|
+
validation: {
|
|
43
|
+
valid: false,
|
|
44
|
+
format: 'UNKNOWN',
|
|
45
|
+
error: 'native module'
|
|
46
|
+
},
|
|
47
|
+
reason: 'MLKitModule.recognizeText is unavailable (native module not linked)',
|
|
48
|
+
durationMs: now() - start
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
let blocks = [];
|
|
52
|
+
try {
|
|
53
|
+
blocks = await MLKit.recognizeText(testCase.base64);
|
|
54
|
+
} catch (err) {
|
|
55
|
+
return {
|
|
56
|
+
name: testCase.name,
|
|
57
|
+
passed: false,
|
|
58
|
+
rawOcr: '',
|
|
59
|
+
validation: {
|
|
60
|
+
valid: false,
|
|
61
|
+
format: 'UNKNOWN',
|
|
62
|
+
error: 'ocr error'
|
|
63
|
+
},
|
|
64
|
+
reason: `OCR threw: ${err instanceof Error ? err.message : String(err)}`,
|
|
65
|
+
durationMs: now() - start
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
const rawOcr = assembleOcrText(blocks);
|
|
69
|
+
const validation = mrzUtils.validateMRZWithCorrections(rawOcr);
|
|
70
|
+
const exp = testCase.expect ?? {};
|
|
71
|
+
const wantValid = exp.valid ?? true;
|
|
72
|
+
const checks = [];
|
|
73
|
+
if (validation.valid !== wantValid) {
|
|
74
|
+
checks.push(`valid=${validation.valid}, expected ${wantValid}`);
|
|
75
|
+
}
|
|
76
|
+
if (wantValid && validation.valid) {
|
|
77
|
+
if (exp.documentNumber && validation.fields?.documentNumber !== exp.documentNumber) {
|
|
78
|
+
checks.push(`documentNumber=${validation.fields?.documentNumber}, expected ${exp.documentNumber}`);
|
|
79
|
+
}
|
|
80
|
+
if (exp.birthDate && validation.fields?.birthDate !== exp.birthDate) {
|
|
81
|
+
checks.push(`birthDate=${validation.fields?.birthDate}, expected ${exp.birthDate}`);
|
|
82
|
+
}
|
|
83
|
+
if (exp.expirationDate && validation.fields?.expirationDate !== exp.expirationDate) {
|
|
84
|
+
checks.push(`expirationDate=${validation.fields?.expirationDate}, expected ${exp.expirationDate}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
name: testCase.name,
|
|
89
|
+
passed: checks.length === 0,
|
|
90
|
+
rawOcr,
|
|
91
|
+
validation,
|
|
92
|
+
reason: checks.join('; '),
|
|
93
|
+
durationMs: now() - start
|
|
94
|
+
};
|
|
95
|
+
};
|
|
96
|
+
/** Runs every case sequentially and aggregates pass/fail. */
|
|
97
|
+
export const runMrzOcrSuite = async cases => {
|
|
98
|
+
const results = [];
|
|
99
|
+
for (const c of cases) {
|
|
100
|
+
results.push(await runMrzOcrCase(c));
|
|
101
|
+
}
|
|
102
|
+
const passed = results.filter(r => r.passed).length;
|
|
103
|
+
return {
|
|
104
|
+
total: results.length,
|
|
105
|
+
passed,
|
|
106
|
+
failed: results.length - passed,
|
|
107
|
+
results
|
|
108
|
+
};
|
|
109
|
+
};
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { Platform, NativeModules, PermissionsAndroid } from 'react-native';
|
|
4
|
+
import { debugLog, debugError, debugWarn } from "./debug.utils.js";
|
|
5
|
+
const {
|
|
6
|
+
PermissionModule
|
|
7
|
+
} = NativeModules;
|
|
8
|
+
class PermissionManager {
|
|
9
|
+
static permissionCache = new Map();
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Request a single permission
|
|
13
|
+
*/
|
|
14
|
+
static async requestPermission(permission) {
|
|
15
|
+
try {
|
|
16
|
+
if (Platform.OS === 'android') {
|
|
17
|
+
return await this.requestAndroidPermission(permission);
|
|
18
|
+
} else {
|
|
19
|
+
return await this.requestIOSPermission(permission);
|
|
20
|
+
}
|
|
21
|
+
} catch (error) {
|
|
22
|
+
debugError('PermissionManager', `Failed to request ${permission}:`, error);
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Request multiple permissions at once
|
|
29
|
+
*/
|
|
30
|
+
static async requestPermissions(permissions) {
|
|
31
|
+
const results = new Map();
|
|
32
|
+
if (Platform.OS === 'android') {
|
|
33
|
+
const androidPermissions = permissions.map(p => this.permissionTypeToAndroid(p));
|
|
34
|
+
const granted = await PermissionsAndroid.requestMultiple(androidPermissions);
|
|
35
|
+
permissions.forEach((p, i) => {
|
|
36
|
+
const isGranted = granted[androidPermissions[i]] === PermissionsAndroid.RESULTS.GRANTED;
|
|
37
|
+
results.set(p, isGranted);
|
|
38
|
+
});
|
|
39
|
+
} else {
|
|
40
|
+
for (const permission of permissions) {
|
|
41
|
+
const isGranted = await this.requestIOSPermission(permission);
|
|
42
|
+
results.set(permission, isGranted);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return results;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Check if a permission is granted (without requesting)
|
|
50
|
+
*/
|
|
51
|
+
static async checkPermission(permission) {
|
|
52
|
+
try {
|
|
53
|
+
if (Platform.OS === 'android') {
|
|
54
|
+
const androidPerm = this.permissionTypeToAndroid(permission);
|
|
55
|
+
const result = await PermissionsAndroid.check(androidPerm);
|
|
56
|
+
return result;
|
|
57
|
+
} else {
|
|
58
|
+
return await this.checkIOSPermission(permission);
|
|
59
|
+
}
|
|
60
|
+
} catch (error) {
|
|
61
|
+
debugError('PermissionManager', `Failed to check ${permission} permission:`, error);
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Request permission with user-friendly error handling
|
|
68
|
+
*/
|
|
69
|
+
static async requestWithFallback(permission, onDenied) {
|
|
70
|
+
const isGranted = await this.requestPermission(permission);
|
|
71
|
+
if (!isGranted) {
|
|
72
|
+
debugLog('PermissionManager', `${permission} permission denied`);
|
|
73
|
+
onDenied?.();
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Convenience method: Request camera and microphone together (for video recording)
|
|
81
|
+
*/
|
|
82
|
+
static async requestCameraAndMicrophone() {
|
|
83
|
+
const permissions = await this.requestPermissions(['camera', 'microphone']);
|
|
84
|
+
const hasCamera = permissions.get('camera') ?? false;
|
|
85
|
+
const hasMicrophone = permissions.get('microphone') ?? false;
|
|
86
|
+
return hasCamera && hasMicrophone;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Convenience method: Request camera only
|
|
91
|
+
*/
|
|
92
|
+
static async requestCamera() {
|
|
93
|
+
return this.requestPermission('camera');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Convenience method: Request microphone only
|
|
98
|
+
*/
|
|
99
|
+
static async requestMicrophone() {
|
|
100
|
+
return this.requestPermission('microphone');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Convenience method: Request NFC only
|
|
105
|
+
*/
|
|
106
|
+
static async requestNFC() {
|
|
107
|
+
return this.requestPermission('nfc');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Check if camera and microphone are both accessible
|
|
112
|
+
*/
|
|
113
|
+
static async hasCameraAndMicrophone() {
|
|
114
|
+
const camera = await this.checkPermission('camera');
|
|
115
|
+
const microphone = await this.checkPermission('microphone');
|
|
116
|
+
return camera && microphone;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Check if camera is accessible
|
|
121
|
+
*/
|
|
122
|
+
static async hasCamera() {
|
|
123
|
+
return this.checkPermission('camera');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Check if microphone is accessible
|
|
128
|
+
*/
|
|
129
|
+
static async hasMicrophone() {
|
|
130
|
+
return this.checkPermission('microphone');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Check if NFC is accessible
|
|
135
|
+
*/
|
|
136
|
+
static async hasNFC() {
|
|
137
|
+
return this.checkPermission('nfc');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Private helper methods
|
|
141
|
+
|
|
142
|
+
static permissionTypeToAndroid(permission) {
|
|
143
|
+
const map = {
|
|
144
|
+
camera: PermissionsAndroid.PERMISSIONS.CAMERA,
|
|
145
|
+
microphone: PermissionsAndroid.PERMISSIONS.RECORD_AUDIO,
|
|
146
|
+
nfc: 'android.permission.NFC'
|
|
147
|
+
};
|
|
148
|
+
return map[permission];
|
|
149
|
+
}
|
|
150
|
+
static async requestAndroidPermission(permission) {
|
|
151
|
+
const androidPerm = this.permissionTypeToAndroid(permission);
|
|
152
|
+
const result = await PermissionsAndroid.request(androidPerm);
|
|
153
|
+
return result === PermissionsAndroid.RESULTS.GRANTED;
|
|
154
|
+
}
|
|
155
|
+
static async requestIOSPermission(permission) {
|
|
156
|
+
if (!PermissionModule) {
|
|
157
|
+
debugLog('PermissionManager', 'PermissionModule not available, assuming permission granted');
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
try {
|
|
161
|
+
switch (permission) {
|
|
162
|
+
case 'camera':
|
|
163
|
+
return await PermissionModule.requestCameraPermission();
|
|
164
|
+
case 'microphone':
|
|
165
|
+
return await PermissionModule.requestMicrophonePermission();
|
|
166
|
+
case 'nfc':
|
|
167
|
+
// NFC permission on iOS is automatically handled when NFCManager is initialized
|
|
168
|
+
// No explicit permission request needed
|
|
169
|
+
return true;
|
|
170
|
+
default:
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
} catch (error) {
|
|
174
|
+
debugError('PermissionManager', `iOS permission request failed for ${permission}:`, error);
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
static async checkIOSPermission(permission) {
|
|
179
|
+
if (!PermissionModule) {
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
try {
|
|
183
|
+
switch (permission) {
|
|
184
|
+
case 'camera':
|
|
185
|
+
return await PermissionModule.checkCameraPermission?.();
|
|
186
|
+
case 'microphone':
|
|
187
|
+
return await PermissionModule.checkMicrophonePermission?.();
|
|
188
|
+
case 'nfc':
|
|
189
|
+
return true;
|
|
190
|
+
default:
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
} catch (error) {
|
|
194
|
+
debugWarn('PermissionManager', `iOS permission check failed for ${permission}`, error);
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
export default PermissionManager;
|
|
@@ -84,6 +84,7 @@ export default {
|
|
|
84
84
|
'eidScannerScreen.docTypeResidence': 'Residence Permit',
|
|
85
85
|
'eidScannerScreen.docTypeVisa': 'Visa',
|
|
86
86
|
'eidScannerScreen.expirationDate': 'Expiration Date',
|
|
87
|
+
'eidScannerScreen.issuingState': 'Issuing State',
|
|
87
88
|
'eidScannerScreen.mrzText': 'MRZ Text',
|
|
88
89
|
'eidScannerScreen.nfcNotSupported': 'This device does not support NFC functionality.',
|
|
89
90
|
'eidScannerScreen.nfcNotEnabled': 'NFC is disabled on this device. Enable it in settings.',
|
|
@@ -84,6 +84,7 @@ export default {
|
|
|
84
84
|
'eidScannerScreen.docTypeResidence': 'Oturma İzni',
|
|
85
85
|
'eidScannerScreen.docTypeVisa': 'Vize',
|
|
86
86
|
'eidScannerScreen.expirationDate': 'Geçerlilik Tarihi',
|
|
87
|
+
'eidScannerScreen.issuingState': 'Veren Ülke',
|
|
87
88
|
'eidScannerScreen.mrzText': 'MRZ Metni',
|
|
88
89
|
'eidScannerScreen.nfcNotSupported': 'Bu cihaz NFC işlevini desteklememektedir.',
|
|
89
90
|
'eidScannerScreen.nfcNotEnabled': 'Cihazda NFC devre dışı. Lütfen ayarlardan etkinleştirin.',
|
package/lib/module/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MRZTestScreen.d.ts","sourceRoot":"","sources":["../../../../../src/Screens/Debug/MRZTestScreen.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"MRZTestScreen.d.ts","sourceRoot":"","sources":["../../../../../src/Screens/Debug/MRZTestScreen.tsx"],"names":[],"mappings":"AA0CA,QAAA,MAAM,aAAa,+CAwIlB,CAAC;AA+FF,eAAe,aAAa,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LivenessDetectionScreen.d.ts","sourceRoot":"","sources":["../../../../../src/Screens/Dynamic/LivenessDetectionScreen.tsx"],"names":[],"mappings":"AA2FA,QAAA,MAAM,uBAAuB,+
|
|
1
|
+
{"version":3,"file":"LivenessDetectionScreen.d.ts","sourceRoot":"","sources":["../../../../../src/Screens/Dynamic/LivenessDetectionScreen.tsx"],"names":[],"mappings":"AA2FA,QAAA,MAAM,uBAAuB,+CAy9B5B,CAAC;AA+GF,eAAe,uBAAuB,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VideoCallScreen.d.ts","sourceRoot":"","sources":["../../../../../src/Screens/Dynamic/VideoCallScreen.tsx"],"names":[],"mappings":"AAiCA,QAAA,MAAM,eAAe,GAAI,gBAAgB,GAAG,
|
|
1
|
+
{"version":3,"file":"VideoCallScreen.d.ts","sourceRoot":"","sources":["../../../../../src/Screens/Dynamic/VideoCallScreen.tsx"],"names":[],"mappings":"AAiCA,QAAA,MAAM,eAAe,GAAI,gBAAgB,GAAG,4CAwhB3C,CAAC;AAyLF,eAAe,eAAe,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ResultScreen.d.ts","sourceRoot":"","sources":["../../../../../src/Screens/Static/ResultScreen.tsx"],"names":[],"mappings":"AAqDA,QAAA,MAAM,YAAY,+
|
|
1
|
+
{"version":3,"file":"ResultScreen.d.ts","sourceRoot":"","sources":["../../../../../src/Screens/Static/ResultScreen.tsx"],"names":[],"mappings":"AAqDA,QAAA,MAAM,YAAY,+CAqiCjB,CAAC;AAqIF,eAAe,YAAY,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EIDScanner.d.ts","sourceRoot":"","sources":["../../../../../src/Shared/Components/EIDScanner.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"EIDScanner.d.ts","sourceRoot":"","sources":["../../../../../src/Shared/Components/EIDScanner.tsx"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAcpD,UAAU,eAAe;IACvB,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,CACd,OAAO,EAAE,SAAS,EAClB,SAAS,EAAE,MAAM,EACjB,iBAAiB,EAAE,MAAM,KACtB,IAAI,CAAC;IACV,cAAc,CAAC,EAAE,CAAC,SAAS,EAAE,OAAO,KAAK,IAAI,CAAC;CAC/C;AAED,QAAA,MAAM,UAAU,GAAI,6FAOjB,eAAe,4CAmpBjB,CAAC;AA4MF,eAAe,UAAU,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FaceCamera.d.ts","sourceRoot":"","sources":["../../../../../src/Shared/Components/FaceCamera.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"FaceCamera.d.ts","sourceRoot":"","sources":["../../../../../src/Shared/Components/FaceCamera.tsx"],"names":[],"mappings":"AAcA,OAAO,EAEL,KAAK,qBAAqB,EAE3B,MAAM,mBAAmB,CAAC;AAa3B,MAAM,MAAM,IAAI,GAAG;IACjB,MAAM,EAAE;QACN,CAAC,EAAE,MAAM,CAAC;QACV,CAAC,EAAE,MAAM,CAAC;QACV,KAAK,EAAE,MAAM,CAAC;QACd,MAAM,EAAE,MAAM,CAAC;KAChB,CAAC;IACF,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,sBAAsB,EAAE,MAAM,CAAC;IAC/B,uBAAuB,EAAE,MAAM,CAAC;CACjC,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,eAAe,EAAE,CACf,KAAK,EAAE,IAAI,EAAE,EACb,KAAK,EAAE,MAAM,EACb,aAAa,EAAE,OAAO,EACtB,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,MAAM,KAChB,IAAI,CAAC;IACV,mBAAmB,EAAE,CAAC,MAAM,EAAE,qBAAqB,KAAK,IAAI,CAAC;IAC7D,WAAW,CAAC,EAAE;QACZ,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,EAAE,MAAM,CAAC;QACb,KAAK,EAAE,MAAM,CAAC;QACd,MAAM,EAAE,MAAM,CAAC;KAChB,CAAC;CACH,CAAC;AAEF,QAAA,MAAM,UAAU,GAAI,wDAIjB,eAAe,4CAsTjB,CAAC;AA6BF,eAAe,UAAU,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"IdentityDocumentCamera.d.ts","sourceRoot":"","sources":["../../../../../src/Shared/Components/IdentityDocumentCamera.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"IdentityDocumentCamera.d.ts","sourceRoot":"","sources":["../../../../../src/Shared/Components/IdentityDocumentCamera.tsx"],"names":[],"mappings":"AAwEA,OAAO,KAAK,EACV,mBAAmB,EACnB,SAAS,EACT,2BAA2B,EAK5B,MAAM,gCAAgC,CAAC;AAGxC,YAAY,EAAE,mBAAmB,EAAE,SAAS,EAAE,2BAA2B,EAAE,CAAC;AAC5E,YAAY,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AAMnE,QAAA,MAAM,sBAAsB,GAAI,uDAI7B,2BAA2B,4CA04E7B,CAAC;AAiIF,eAAe,sBAAsB,CAAC"}
|