@trustchex/react-native-sdk 1.495.10 → 1.495.12
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 +10 -0
- package/android/libs/jj2000-5.2.jar +0 -0
- package/android/src/main/java/com/trustchex/reactnativesdk/ImageDecoderModule.kt +88 -0
- package/android/src/main/java/com/trustchex/reactnativesdk/Jp2PgxDecoder.kt +135 -0
- package/android/src/main/java/com/trustchex/reactnativesdk/TrustchexSDKPackage.kt +12 -0
- package/android/src/test/java/com/trustchex/reactnativesdk/Jp2PgxDecoderTest.kt +143 -0
- package/android/src/test/resources/jp2/gray.jp2 +0 -0
- package/android/src/test/resources/jp2/solid_color.j2k +0 -0
- package/android/src/test/resources/jp2/solid_color.jp2 +0 -0
- package/lib/module/Shared/Components/EIDScanner.js +8 -5
- package/lib/module/Shared/Components/IdentityDocumentCamera.constants.js +9 -0
- package/lib/module/Shared/Components/IdentityDocumentCamera.flows.js +57 -19
- package/lib/module/Shared/Components/IdentityDocumentCamera.js +69 -14
- package/lib/module/Shared/Components/IdentityDocumentCamera.utils.js +33 -4
- package/lib/module/Shared/EIDReader/eidReader.js +11 -9
- package/lib/module/Shared/Libs/jp2Decode.js +12 -11
- package/lib/module/Shared/Services/DataUploadService.js +16 -4
- package/lib/module/version.js +1 -1
- package/lib/typescript/src/Shared/Components/EIDScanner.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.constants.d.ts +1 -0
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.constants.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.flows.d.ts +2 -2
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.flows.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.types.d.ts +21 -0
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.types.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.utils.d.ts +15 -0
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/EIDReader/eidReader.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/jp2Decode.d.ts +4 -3
- package/lib/typescript/src/Shared/Libs/jp2Decode.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Services/DataUploadService.d.ts.map +1 -1
- package/lib/typescript/src/version.d.ts +1 -1
- package/package.json +1 -1
- package/src/Shared/Components/EIDScanner.tsx +7 -5
- package/src/Shared/Components/IdentityDocumentCamera.constants.ts +9 -0
- package/src/Shared/Components/IdentityDocumentCamera.flows.ts +78 -21
- package/src/Shared/Components/IdentityDocumentCamera.tsx +83 -27
- package/src/Shared/Components/IdentityDocumentCamera.types.ts +21 -0
- package/src/Shared/Components/IdentityDocumentCamera.utils.ts +36 -3
- package/src/Shared/EIDReader/eidReader.ts +11 -9
- package/src/Shared/Libs/jp2Decode.ts +15 -11
- package/src/Shared/Services/DataUploadService.ts +41 -8
- package/src/version.ts +1 -1
|
@@ -28,10 +28,17 @@
|
|
|
28
28
|
* - No face, has MRZ with code 'I'
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
|
-
import type { Face } from './IdentityDocumentCamera.types';
|
|
31
|
+
import type { Face, DocumentType } from './IdentityDocumentCamera.types';
|
|
32
32
|
import type { MRZFields } from '../Types/mrzFields';
|
|
33
|
-
import {
|
|
34
|
-
|
|
33
|
+
import {
|
|
34
|
+
PASSPORT_MRZ_PATTERN,
|
|
35
|
+
MRZ_DENSE_PATTERN,
|
|
36
|
+
} from './IdentityDocumentCamera.constants';
|
|
37
|
+
import {
|
|
38
|
+
isIDCardDocumentCode,
|
|
39
|
+
isPassportDocumentCode,
|
|
40
|
+
documentHasBackSide,
|
|
41
|
+
} from './IdentityDocumentCamera.utils';
|
|
35
42
|
|
|
36
43
|
/**
|
|
37
44
|
* After this many retry attempts for ID_FRONT flow, proceed with face detection alone
|
|
@@ -54,6 +61,7 @@ export interface IDFrontFlowResult {
|
|
|
54
61
|
reason?: string;
|
|
55
62
|
nextAction?:
|
|
56
63
|
| 'WAIT_FOR_ELEMENTS'
|
|
64
|
+
| 'WAIT_FOR_MRZ'
|
|
57
65
|
| 'REJECT_AS_PASSPORT'
|
|
58
66
|
| 'PROCEED_TO_HOLOGRAM';
|
|
59
67
|
}
|
|
@@ -183,8 +191,17 @@ export function handleIDFrontFlow(
|
|
|
183
191
|
};
|
|
184
192
|
}
|
|
185
193
|
|
|
186
|
-
if
|
|
187
|
-
|
|
194
|
+
// Passport MRZ pattern visible even if not fully parsed. Check BOTH the
|
|
195
|
+
// parsed `mrzText` (null until the MRZ validates) AND the raw OCR `text` — a
|
|
196
|
+
// passport's MRZ is dense and frequently OCRs a few frames after its face +
|
|
197
|
+
// signature, so on the accepting frame `mrzText` is often still null while the
|
|
198
|
+
// raw `P<XXX...` run is already visible. Testing only `mrzText` (the old bug)
|
|
199
|
+
// let such a passport pass as an ID front and get routed to a back side that
|
|
200
|
+
// does not exist.
|
|
201
|
+
if (
|
|
202
|
+
(mrzText && PASSPORT_MRZ_PATTERN.test(mrzText)) ||
|
|
203
|
+
PASSPORT_MRZ_PATTERN.test(text)
|
|
204
|
+
) {
|
|
188
205
|
return {
|
|
189
206
|
shouldProceed: false,
|
|
190
207
|
reason: 'Passport MRZ pattern visible - not an ID card',
|
|
@@ -204,6 +221,39 @@ export function handleIDFrontFlow(
|
|
|
204
221
|
};
|
|
205
222
|
}
|
|
206
223
|
|
|
224
|
+
// ============================================================================
|
|
225
|
+
// STEP 2b: An ID-card FRONT has NO MRZ (the MRZ is on the back). So if a real
|
|
226
|
+
// MRZ is currently visible on this "front", it is almost certainly a passport
|
|
227
|
+
// data page whose 'P' code we just haven't parsed yet — do NOT accept it as an
|
|
228
|
+
// ID front; wait for the MRZ to resolve (→ a 'P' code routes it to the
|
|
229
|
+
// passport flow). This is the guard that stops a passport (face + signature +
|
|
230
|
+
// not-yet-parsed MRZ) being accepted as ID_FRONT and then getting stuck at
|
|
231
|
+
// SCAN_ID_BACK, which rejects the passport's photo page because it has a face.
|
|
232
|
+
//
|
|
233
|
+
// We use MRZ_DENSE_PATTERN (requires a `<<` filler run), NOT the loose
|
|
234
|
+
// MRZ_BLOCK_PATTERN — a card face has no MRZ, but a stray single `<` OCR'd
|
|
235
|
+
// from a guilloche could match the loose pattern and wedge a real ID front.
|
|
236
|
+
// We ALSO honor the signature retry threshold: a real ID front genuinely has
|
|
237
|
+
// no MRZ, so if a "dense MRZ" keeps matching frame after frame past the
|
|
238
|
+
// threshold it is OCR noise, not a passport — fall through and let STEP 3's
|
|
239
|
+
// face-only acceptance proceed rather than waiting forever.
|
|
240
|
+
const mrzVisibleOnFront = !!mrzText || MRZ_DENSE_PATTERN.test(text);
|
|
241
|
+
const stillWaitingForMrz = retryCount <= SIGNATURE_RETRY_THRESHOLD;
|
|
242
|
+
if (
|
|
243
|
+
mrzVisibleOnFront &&
|
|
244
|
+
stillWaitingForMrz &&
|
|
245
|
+
!isIDCardDocumentCode(mrzFields?.documentCode)
|
|
246
|
+
) {
|
|
247
|
+
// An ID-card MRZ code (I/A/C) is allowed to proceed (handled in STEP 4); any
|
|
248
|
+
// other visible MRZ → keep waiting for it to parse and reveal a 'P' code.
|
|
249
|
+
return {
|
|
250
|
+
shouldProceed: false,
|
|
251
|
+
reason:
|
|
252
|
+
'MRZ block visible on front - waiting to confirm it is not a passport',
|
|
253
|
+
nextAction: 'WAIT_FOR_MRZ',
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
207
257
|
// ============================================================================
|
|
208
258
|
// STEP 3: Check for signature (optional front side confirmation)
|
|
209
259
|
// ============================================================================
|
|
@@ -350,27 +400,34 @@ export function handleIDBackFlow(
|
|
|
350
400
|
export function getNextStepAfterHologram(
|
|
351
401
|
detectedDocumentType: 'ID_FRONT' | 'ID_BACK' | 'PASSPORT' | 'UNKNOWN',
|
|
352
402
|
currentFrameDocType: 'ID_FRONT' | 'ID_BACK' | 'PASSPORT' | 'UNKNOWN',
|
|
353
|
-
mrzDocCode: string | undefined
|
|
403
|
+
mrzDocCode: string | undefined,
|
|
404
|
+
// True if a passport MRZ pattern / 'P' code was seen in ANY front frame this
|
|
405
|
+
// session — not just the accepting one. The passport MRZ is dense and often
|
|
406
|
+
// OCRs a few frames AFTER the face + signature, so a passport can get locked
|
|
407
|
+
// as ID_FRONT before its MRZ is read. Latching the signal across frames is
|
|
408
|
+
// what stops that passport from being wrongly routed to SCAN_ID_BACK here.
|
|
409
|
+
sawPassportSignal: boolean = false
|
|
354
410
|
): 'COMPLETED' | 'SCAN_ID_BACK' {
|
|
355
|
-
//
|
|
411
|
+
// Resolve to a single document type, passport-biased: any positive passport
|
|
412
|
+
// signal wins (a passport must never wait for a back that does not exist),
|
|
413
|
+
// then positive ID-card evidence (a non-'P' MRZ code, or an ID_FRONT
|
|
414
|
+
// classification — an ID front legitimately has no MRZ, so the classification
|
|
415
|
+
// is the only signal we get), else UNKNOWN.
|
|
356
416
|
const isPassport =
|
|
417
|
+
sawPassportSignal ||
|
|
357
418
|
detectedDocumentType === 'PASSPORT' ||
|
|
358
419
|
currentFrameDocType === 'PASSPORT' ||
|
|
359
|
-
mrzDocCode
|
|
360
|
-
|
|
361
|
-
if (isPassport) {
|
|
362
|
-
return 'COMPLETED';
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
// Positive ID-card evidence → scan the back. Either a non-'P' MRZ code, or a
|
|
366
|
-
// front classified as ID_FRONT (an ID front legitimately has no MRZ, so the
|
|
367
|
-
// classification is the only signal we get).
|
|
368
|
-
const hasIdCardMrzCode = !!mrzDocCode && mrzDocCode !== 'P';
|
|
420
|
+
isPassportDocumentCode(mrzDocCode);
|
|
421
|
+
const hasIdCardMrzCode = !!mrzDocCode && !isPassportDocumentCode(mrzDocCode);
|
|
369
422
|
const isIdFront =
|
|
370
423
|
detectedDocumentType === 'ID_FRONT' || currentFrameDocType === 'ID_FRONT';
|
|
371
|
-
if (hasIdCardMrzCode || isIdFront) {
|
|
372
|
-
return 'SCAN_ID_BACK';
|
|
373
|
-
}
|
|
374
424
|
|
|
375
|
-
|
|
425
|
+
const resolved: DocumentType = isPassport
|
|
426
|
+
? 'PASSPORT'
|
|
427
|
+
: hasIdCardMrzCode || isIdFront
|
|
428
|
+
? 'ID_FRONT'
|
|
429
|
+
: 'UNKNOWN';
|
|
430
|
+
|
|
431
|
+
// Single source of truth for "is there a back side left to scan?".
|
|
432
|
+
return documentHasBackSide(resolved) ? 'SCAN_ID_BACK' : 'COMPLETED';
|
|
376
433
|
}
|
|
@@ -47,6 +47,7 @@ import {
|
|
|
47
47
|
areMRZFieldsEqual,
|
|
48
48
|
hasRequiredMRZFields,
|
|
49
49
|
validateFacePosition,
|
|
50
|
+
isPassportDocumentCode,
|
|
50
51
|
} from './IdentityDocumentCamera.utils';
|
|
51
52
|
import {
|
|
52
53
|
handlePassportFlow,
|
|
@@ -68,7 +69,6 @@ import {
|
|
|
68
69
|
REQUIRED_CONSISTENT_DOCTYPE_DETECTIONS,
|
|
69
70
|
SIGNATURE_TEXT_REGEX,
|
|
70
71
|
MRZ_BLOCK_PATTERN,
|
|
71
|
-
PASSPORT_MRZ_PATTERN,
|
|
72
72
|
MIN_CARD_FACE_SIZE_PERCENT,
|
|
73
73
|
} from './IdentityDocumentCamera.constants';
|
|
74
74
|
import type {
|
|
@@ -178,6 +178,15 @@ const IdentityDocumentCamera = ({
|
|
|
178
178
|
>('UNKNOWN');
|
|
179
179
|
const consistentDocTypeCount = useRef(0);
|
|
180
180
|
|
|
181
|
+
// Latches true the moment a passport MRZ pattern / 'P' document code is seen in
|
|
182
|
+
// ANY front frame this session. The passport MRZ is dense and often OCRs a few
|
|
183
|
+
// frames AFTER the face + signature, so without this a passport can be locked
|
|
184
|
+
// as ID_FRONT (which has no MRZ on its front) and then wrongly asked for a back
|
|
185
|
+
// side that does not exist. Once latched, the front is never accepted as
|
|
186
|
+
// ID_FRONT and the post-hologram routing always completes instead of scanning
|
|
187
|
+
// a back. Reset only when a brand-new scan session starts.
|
|
188
|
+
const sawPassportSignal = useRef(false);
|
|
189
|
+
|
|
181
190
|
// Frame quality tracking - persist across callbacks
|
|
182
191
|
const lastFrameQuality = useRef({
|
|
183
192
|
hasAcceptableQuality: true,
|
|
@@ -325,6 +334,7 @@ const IdentityDocumentCamera = ({
|
|
|
325
334
|
lastValidMRZText.current = null;
|
|
326
335
|
lastValidMRZFields.current = null;
|
|
327
336
|
validMRZConsecutiveCount.current = 0;
|
|
337
|
+
sawPassportSignal.current = false;
|
|
328
338
|
mrzAggregator.current.reset();
|
|
329
339
|
setMrzReliable(false);
|
|
330
340
|
// Start a fresh diagnostics session for the support report.
|
|
@@ -666,6 +676,15 @@ const IdentityDocumentCamera = ({
|
|
|
666
676
|
|
|
667
677
|
// Reset MRZ retry counter for each new step so retries start fresh
|
|
668
678
|
mrzDetectionCurrentRetryCount.current = 0;
|
|
679
|
+
// A transition back to the initial front step means a brand-new document
|
|
680
|
+
// scan is starting, so the sticky passport signal from any previous
|
|
681
|
+
// document must NOT carry over — otherwise an ID card scanned after a
|
|
682
|
+
// passport (without the camera unfocusing) would be forced into the
|
|
683
|
+
// passport flow and skip its back side. (The focus-based reset above only
|
|
684
|
+
// fires when the camera goes inactive.)
|
|
685
|
+
if (nextStepType === 'SCAN_ID_FRONT_OR_PASSPORT') {
|
|
686
|
+
sawPassportSignal.current = false;
|
|
687
|
+
}
|
|
669
688
|
// Only clear MRZ text when entering SCAN_ID_BACK (new MRZ expected).
|
|
670
689
|
// Preserve across SCAN_HOLOGRAM so passport completion has MRZ data.
|
|
671
690
|
if (nextStepType === 'SCAN_ID_BACK') {
|
|
@@ -1149,7 +1168,9 @@ const IdentityDocumentCamera = ({
|
|
|
1149
1168
|
// Surface the reliable-detection signal to the render so the UI can show a
|
|
1150
1169
|
// green "GO" the moment (and only once) the consensus is stable + valid.
|
|
1151
1170
|
// Never flips green on an early/transient valid read.
|
|
1152
|
-
setMrzReliable((prev) =>
|
|
1171
|
+
setMrzReliable((prev) =>
|
|
1172
|
+
prev === mrzStableAndValid ? prev : mrzStableAndValid
|
|
1173
|
+
);
|
|
1153
1174
|
|
|
1154
1175
|
// Test-mode panel: show the MRZ ONLY once it is stable + valid (the same
|
|
1155
1176
|
// consensus matched across `stabilityTarget` consecutive frames). Showing
|
|
@@ -1159,9 +1180,7 @@ const IdentityDocumentCamera = ({
|
|
|
1159
1180
|
// check-digit-valid consensus and never an unstable intermediate.
|
|
1160
1181
|
if (testMode && mrzStableAndValid && mrzText) {
|
|
1161
1182
|
setTestModeData((prev) =>
|
|
1162
|
-
prev?.mrzText === mrzText
|
|
1163
|
-
? prev
|
|
1164
|
-
: { mrzText, timestamp: Date.now() }
|
|
1183
|
+
prev?.mrzText === mrzText ? prev : { mrzText, timestamp: Date.now() }
|
|
1165
1184
|
);
|
|
1166
1185
|
}
|
|
1167
1186
|
|
|
@@ -1174,30 +1193,42 @@ const IdentityDocumentCamera = ({
|
|
|
1174
1193
|
const hasFaces = primaryFaces.length > 0;
|
|
1175
1194
|
const hasSignature = /signature|imza|İmza/i.test(text);
|
|
1176
1195
|
const hasPassportMRZ = parsedMRZData?.fields?.documentCode === 'P';
|
|
1177
|
-
const hasPassportMRZPattern =
|
|
1178
|
-
mrzText && PASSPORT_MRZ_PATTERN.test(mrzText);
|
|
1179
|
-
|
|
1180
|
-
if (
|
|
1181
|
-
hasFaces ||
|
|
1182
|
-
hasSignature ||
|
|
1183
|
-
hasPassportMRZ ||
|
|
1184
|
-
hasPassportMRZPattern
|
|
1185
|
-
) {
|
|
1186
|
-
setStatus('INCORRECT');
|
|
1187
|
-
return;
|
|
1188
|
-
}
|
|
1189
1196
|
|
|
1190
|
-
//
|
|
1191
|
-
|
|
1197
|
+
// RECOVERY (must run BEFORE the wrong-side guard below): a passport was
|
|
1198
|
+
// misrouted to SCAN_ID_BACK. A passport has only its data page, which
|
|
1199
|
+
// carries a face + MRZ — exactly what the wrong-side guard treats as
|
|
1200
|
+
// "wrong side", so without this it would loop on INCORRECT forever
|
|
1201
|
+
// (the reported "stuck asking for ID back" case). If we have positive
|
|
1202
|
+
// passport evidence here, the front was already captured, so complete
|
|
1203
|
+
// as a passport instead of waiting for a back that does not exist.
|
|
1204
|
+
//
|
|
1205
|
+
// We deliberately gate ONLY on authoritative signals: a locked PASSPORT
|
|
1206
|
+
// classification, or a PARSED MRZ document code of 'P'. We do NOT test
|
|
1207
|
+
// the raw OCR `text` against PASSPORT_MRZ_PATTERN here — an ID card's
|
|
1208
|
+
// BACK legitimately carries an MRZ whose name line ("PAPPAS<<MARIA…",
|
|
1209
|
+
// "…<<PHILIPP…") can false-match the unanchored pattern and would then
|
|
1210
|
+
// wrongly complete a real ID card as a passport, skipping the back-side
|
|
1211
|
+
// MRZ + barcode validation entirely.
|
|
1212
|
+
if (detectedDocumentType === 'PASSPORT' || hasPassportMRZ) {
|
|
1192
1213
|
transitionStepWithCallback('COMPLETED', 'SCAN_ID_BACK', {
|
|
1193
1214
|
image,
|
|
1194
1215
|
documentType: 'PASSPORT',
|
|
1195
|
-
mrzText: mrzText ?? undefined,
|
|
1196
|
-
mrzFields:
|
|
1216
|
+
mrzText: mrzText ?? lastValidMRZText.current ?? undefined,
|
|
1217
|
+
mrzFields:
|
|
1218
|
+
parsedMRZData?.fields ??
|
|
1219
|
+
lastValidMRZFields.current ??
|
|
1220
|
+
undefined,
|
|
1197
1221
|
});
|
|
1198
1222
|
return;
|
|
1199
1223
|
}
|
|
1200
1224
|
|
|
1225
|
+
// Wrong-side detection: a face or signature means we're looking at a
|
|
1226
|
+
// front, not an ID-card back (which has neither).
|
|
1227
|
+
if (hasFaces || hasSignature) {
|
|
1228
|
+
setStatus('INCORRECT');
|
|
1229
|
+
return;
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1201
1232
|
setElementsOutsideScanArea([]);
|
|
1202
1233
|
|
|
1203
1234
|
const flowResult = handleIDBackFlow(
|
|
@@ -1764,7 +1795,8 @@ const IdentityDocumentCamera = ({
|
|
|
1764
1795
|
const nextStepAfterHologram = getNextStepAfterHologram(
|
|
1765
1796
|
detectedDocumentType,
|
|
1766
1797
|
documentType,
|
|
1767
|
-
mrzDocCode
|
|
1798
|
+
mrzDocCode,
|
|
1799
|
+
sawPassportSignal.current
|
|
1768
1800
|
);
|
|
1769
1801
|
|
|
1770
1802
|
transitionStepWithCallback(
|
|
@@ -1783,11 +1815,35 @@ const IdentityDocumentCamera = ({
|
|
|
1783
1815
|
// INITIAL SCAN STEP - Detect document type and validate
|
|
1784
1816
|
// ============================================================================
|
|
1785
1817
|
if (nextStep === 'SCAN_ID_FRONT_OR_PASSPORT') {
|
|
1818
|
+
// Latch ANY passport signal seen in a front frame — the parsed 'P' code
|
|
1819
|
+
// OR the raw passport MRZ pattern (which survives even when full MRZ
|
|
1820
|
+
// parsing fails). The passport MRZ often OCRs a few frames after the
|
|
1821
|
+
// face + signature, so we remember it across frames; once latched, this
|
|
1822
|
+
// front can never be accepted as an ID card / asked for a back side.
|
|
1823
|
+
// Only AUTHORITATIVE passport signals latch — a parsed MRZ document code
|
|
1824
|
+
// of 'P', or a settled PASSPORT classification. We deliberately do NOT
|
|
1825
|
+
// latch on a raw PASSPORT_MRZ_PATTERN substring match against OCR text:
|
|
1826
|
+
// that loose test can fire on ID-card MRZ noise, and because the latch is
|
|
1827
|
+
// sticky and one-directional a single false match would force the
|
|
1828
|
+
// passport flow for the rest of the session and silently skip the ID
|
|
1829
|
+
// card's back side. The parsed 'P' code still covers the lagging-MRZ case
|
|
1830
|
+
// (it just requires the MRZ to have actually parsed first).
|
|
1831
|
+
if (
|
|
1832
|
+
isPassportDocumentCode(parsedMRZData?.fields?.documentCode) ||
|
|
1833
|
+
documentType === 'PASSPORT' ||
|
|
1834
|
+
detectedDocumentType === 'PASSPORT'
|
|
1835
|
+
) {
|
|
1836
|
+
sawPassportSignal.current = true;
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1786
1839
|
// Determine which flow handler to use.
|
|
1787
1840
|
// Current-frame passport detection always takes precedence over a locked
|
|
1788
|
-
// ID_FRONT — passport MRZ may not appear until later frames.
|
|
1841
|
+
// ID_FRONT — passport MRZ may not appear until later frames. A latched
|
|
1842
|
+
// passport signal also forces the passport flow, so a passport that was
|
|
1843
|
+
// momentarily locked as ID_FRONT (before its MRZ was read) is corrected
|
|
1844
|
+
// instead of being driven into the ID-card back-side flow.
|
|
1789
1845
|
const flowDocumentType =
|
|
1790
|
-
documentType === 'PASSPORT'
|
|
1846
|
+
documentType === 'PASSPORT' || sawPassportSignal.current
|
|
1791
1847
|
? 'PASSPORT'
|
|
1792
1848
|
: detectedDocumentType !== 'UNKNOWN'
|
|
1793
1849
|
? detectedDocumentType
|
|
@@ -2015,7 +2071,8 @@ const IdentityDocumentCamera = ({
|
|
|
2015
2071
|
frameH: ds.frameH,
|
|
2016
2072
|
avgBrightness:
|
|
2017
2073
|
ds.frames > 0 ? Math.round(ds.brightnessSum / ds.frames) : 0,
|
|
2018
|
-
minBrightness:
|
|
2074
|
+
minBrightness:
|
|
2075
|
+
ds.minBrightness === 255 && ds.frames === 0 ? 0 : ds.minBrightness,
|
|
2019
2076
|
maxBrightness: ds.maxBrightness,
|
|
2020
2077
|
hadLowBrightness: ds.hadLowBrightness,
|
|
2021
2078
|
hadBlurryFrames: ds.hadBlurryFrames,
|
|
@@ -2651,8 +2708,7 @@ const IdentityDocumentCamera = ({
|
|
|
2651
2708
|
: isBrightnessLow || isFrameBlurry
|
|
2652
2709
|
? '#FFC107'
|
|
2653
2710
|
: 'white',
|
|
2654
|
-
borderWidth:
|
|
2655
|
-
mrzReliable || status === 'SCANNING' ? 3 : 2,
|
|
2711
|
+
borderWidth: mrzReliable || status === 'SCANNING' ? 3 : 2,
|
|
2656
2712
|
},
|
|
2657
2713
|
]}
|
|
2658
2714
|
>
|
|
@@ -115,6 +115,27 @@ export type ScanStep =
|
|
|
115
115
|
|
|
116
116
|
export type ScanStatus = 'SEARCHING' | 'SCANNING' | 'SCANNED' | 'INCORRECT';
|
|
117
117
|
|
|
118
|
+
/**
|
|
119
|
+
* What the camera has classified the document in view as.
|
|
120
|
+
*
|
|
121
|
+
* The single most important property of a document type is whether it has a
|
|
122
|
+
* BACK SIDE that still needs scanning — that is what drives the scan flow:
|
|
123
|
+
*
|
|
124
|
+
* PASSPORT — ICAO TD3 booklet. The MRZ, photo and all data live on ONE page
|
|
125
|
+
* (the data page). There is NO second machine-readable side, so a
|
|
126
|
+
* passport must NEVER be routed to SCAN_ID_BACK. Detected by the
|
|
127
|
+
* TD3 MRZ document code 'P' / the passport MRZ pattern.
|
|
128
|
+
* ID_FRONT — the photo/front side of a TD1/TD2 ID card. The front carries the
|
|
129
|
+
* photo + signature but usually NO MRZ (the MRZ is on the back),
|
|
130
|
+
* so the front alone is NOT enough — it MUST be followed by a back
|
|
131
|
+
* scan (SCAN_ID_BACK).
|
|
132
|
+
* ID_BACK — the back side of a TD1/TD2 ID card. Carries the MRZ (document
|
|
133
|
+
* code 'I'/'A'/'C') and usually a barcode. Terminal side.
|
|
134
|
+
* UNKNOWN — not yet classified, or signals conflict.
|
|
135
|
+
*
|
|
136
|
+
* Use `documentHasBackSide()` / `isPassportDocumentCode()` rather than comparing
|
|
137
|
+
* these strings ad-hoc, so the "is this a passport?" rule lives in one place.
|
|
138
|
+
*/
|
|
118
139
|
export type DocumentType = 'ID_FRONT' | 'ID_BACK' | 'PASSPORT' | 'UNKNOWN';
|
|
119
140
|
|
|
120
141
|
export type CompletedStep =
|
|
@@ -9,6 +9,7 @@ import type { MRZFields } from '../Types/mrzFields';
|
|
|
9
9
|
import {
|
|
10
10
|
SIGNATURE_REGEX,
|
|
11
11
|
PASSPORT_MRZ_PATTERN,
|
|
12
|
+
MRZ_DENSE_PATTERN,
|
|
12
13
|
} from './IdentityDocumentCamera.constants';
|
|
13
14
|
import { debugLog, isDebugEnabled } from '../Libs/debug.utils';
|
|
14
15
|
|
|
@@ -21,6 +22,30 @@ export function isIDCardDocumentCode(code: string | null | undefined): boolean {
|
|
|
21
22
|
return code.startsWith('I') || code.startsWith('A') || code.startsWith('C');
|
|
22
23
|
}
|
|
23
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Checks if a document code represents a passport (ICAO TD3).
|
|
27
|
+
* Per ICAO 9303 Part 4, the first character of a passport MRZ document code is
|
|
28
|
+
* 'P' — for the ordinary `P<`, the German `PP`, and the diplomatic/service/
|
|
29
|
+
* official `PD`/`PS`/`PO` variants alike.
|
|
30
|
+
*/
|
|
31
|
+
export function isPassportDocumentCode(
|
|
32
|
+
code: string | null | undefined
|
|
33
|
+
): boolean {
|
|
34
|
+
if (!code) return false;
|
|
35
|
+
return code.startsWith('P');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Single source of truth for "does this document have a second side left to
|
|
40
|
+
* scan?". A passport's data all lives on one page, so it has no back; an ID
|
|
41
|
+
* card's MRZ lives on the back, so an ID front always does. Used by the
|
|
42
|
+
* post-front / post-hologram routing so the "ask for the back side?" decision
|
|
43
|
+
* is never re-derived ad-hoc (the bug where passports were sent to SCAN_ID_BACK).
|
|
44
|
+
*/
|
|
45
|
+
export function documentHasBackSide(documentType: DocumentType): boolean {
|
|
46
|
+
return documentType === 'ID_FRONT';
|
|
47
|
+
}
|
|
48
|
+
|
|
24
49
|
/**
|
|
25
50
|
* Frame-to-screen coordinate transform using FILL_CENTER scaling
|
|
26
51
|
*/
|
|
@@ -285,10 +310,18 @@ export function detectDocumentType(
|
|
|
285
310
|
return 'ID_FRONT';
|
|
286
311
|
}
|
|
287
312
|
|
|
288
|
-
// If signature present and NO passport MRZ pattern, likely ID_FRONT
|
|
289
|
-
//
|
|
313
|
+
// If signature present and NO passport MRZ pattern, likely ID_FRONT —
|
|
314
|
+
// BUT an ID-card front has no MRZ, so if a real (dense) MRZ is visible here
|
|
315
|
+
// (and it is not a confirmed ID code — that returned above) this is almost
|
|
316
|
+
// certainly a passport data page whose 'P' code just hasn't OCR'd yet.
|
|
317
|
+
// Stay UNKNOWN and let later frames resolve it, rather than locking
|
|
318
|
+
// ID_FRONT and routing a passport to a back side it does not have. We use
|
|
319
|
+
// MRZ_DENSE_PATTERN (needs a `<<` filler run) so a stray single `<` OCR'd
|
|
320
|
+
// from a card-face guilloche cannot wedge a genuine ID front at UNKNOWN.
|
|
321
|
+
const hasUnresolvedMrzBlock =
|
|
322
|
+
!!mrzText || MRZ_DENSE_PATTERN.test(ocrText);
|
|
290
323
|
const hasSignature = hasSignatureMatch;
|
|
291
|
-
if (hasSignature && !hasPassportMRZPattern) {
|
|
324
|
+
if (hasSignature && !hasPassportMRZPattern && !hasUnresolvedMrzBlock) {
|
|
292
325
|
return 'ID_FRONT';
|
|
293
326
|
}
|
|
294
327
|
}
|
|
@@ -248,10 +248,11 @@ async function convertJP2IfNeeded(
|
|
|
248
248
|
return { base64: imageBuffer.toString('base64'), mimeType };
|
|
249
249
|
}
|
|
250
250
|
|
|
251
|
-
// Prefer the native decoder (iOS ImageIO). It decodes
|
|
252
|
-
//
|
|
253
|
-
// <Image> can't render a raw image/jp2 data URI
|
|
254
|
-
// decoder entirely. Returns null
|
|
251
|
+
// Prefer the native decoder (iOS ImageIO / Android jj2000). It decodes
|
|
252
|
+
// JPEG2000 of any size off the JS thread, so it fixes the on-screen preview on
|
|
253
|
+
// BOTH platforms (RN's <Image> can't render a raw image/jp2 data URI on either)
|
|
254
|
+
// and avoids the slow pure-JS decoder entirely. Returns null only when the
|
|
255
|
+
// native module is unavailable/unlinked or it fails → fall through.
|
|
255
256
|
const native = await decodeJp2ToJpeg(imageBuffer);
|
|
256
257
|
if (native) {
|
|
257
258
|
debugLog(
|
|
@@ -261,11 +262,12 @@ async function convertJP2IfNeeded(
|
|
|
261
262
|
return native;
|
|
262
263
|
}
|
|
263
264
|
|
|
264
|
-
//
|
|
265
|
-
// JP2. The face image is still captured
|
|
266
|
-
// accept JPEG2000); only the on-screen
|
|
267
|
-
// NFC read COMPLETE instead of hanging.
|
|
268
|
-
//
|
|
265
|
+
// Native decode unavailable AND too large to decode synchronously in JS
|
|
266
|
+
// without freezing the UI — keep the raw JP2. The face image is still captured
|
|
267
|
+
// (the verification backend / face-match accept JPEG2000); only the on-screen
|
|
268
|
+
// preview can't render it. This lets the NFC read COMPLETE instead of hanging.
|
|
269
|
+
// In normal operation the native path above handles this on both platforms; we
|
|
270
|
+
// only reach here if the native module failed or isn't linked.
|
|
269
271
|
if (imageBuffer.length > MAX_JP2_DECODE_BYTES) {
|
|
270
272
|
debugLog(
|
|
271
273
|
'EID',
|
|
@@ -5,13 +5,13 @@ import { Buffer } from 'buffer';
|
|
|
5
5
|
* Native JPEG2000 decoder bridge.
|
|
6
6
|
*
|
|
7
7
|
* The passport chip portrait (DG2) is frequently stored as JPEG2000
|
|
8
|
-
* (image/jp2). React Native's <Image>
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* iOS
|
|
13
|
-
*
|
|
14
|
-
*
|
|
8
|
+
* (image/jp2). React Native's <Image> CANNOT render a JP2 data URI on EITHER
|
|
9
|
+
* platform — iOS's UIImage-backed <Image> refuses it, and Android's Fresco/Skia
|
|
10
|
+
* pipeline has no JPEG2000 decoder (the chip photo showed as a blank/white
|
|
11
|
+
* placeholder). Both platforms therefore need a native round-trip to JPEG:
|
|
12
|
+
* - iOS: ImageIO decodes JPEG2000 natively.
|
|
13
|
+
* - Android: the bundled pure-Java jj2000 decoder (no ImageIO/NDK).
|
|
14
|
+
* Both are exposed as the `ImageDecoderModule.decodeJp2ToJpeg` native method.
|
|
15
15
|
*/
|
|
16
16
|
interface ImageDecoderNativeModule {
|
|
17
17
|
decodeJp2ToJpeg(base64Jp2: string): Promise<string>;
|
|
@@ -22,16 +22,20 @@ const ImageDecoder = (
|
|
|
22
22
|
).ImageDecoderModule;
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
|
-
* Decode a JPEG2000 buffer to JPEG using the native iOS
|
|
25
|
+
* Decode a JPEG2000 buffer to JPEG using the native decoder (iOS ImageIO /
|
|
26
|
+
* Android jj2000).
|
|
26
27
|
*
|
|
27
28
|
* Returns the JPEG bytes + mime type on success, or null when native decode is
|
|
28
|
-
* unavailable (
|
|
29
|
-
*
|
|
29
|
+
* unavailable (the module isn't linked) or fails — callers should fall back to
|
|
30
|
+
* their existing behaviour (keep raw JP2).
|
|
30
31
|
*/
|
|
31
32
|
export async function decodeJp2ToJpeg(
|
|
32
33
|
jp2Buffer: Buffer
|
|
33
34
|
): Promise<{ base64: string; mimeType: string } | null> {
|
|
34
|
-
if (
|
|
35
|
+
if (
|
|
36
|
+
(Platform.OS !== 'ios' && Platform.OS !== 'android') ||
|
|
37
|
+
!ImageDecoder?.decodeJp2ToJpeg
|
|
38
|
+
) {
|
|
35
39
|
return null;
|
|
36
40
|
}
|
|
37
41
|
try {
|
|
@@ -65,9 +65,12 @@ export class DataUploadService {
|
|
|
65
65
|
type: mrzFields.documentCode,
|
|
66
66
|
name: mrzFields.firstName,
|
|
67
67
|
surname: mrzFields.lastName,
|
|
68
|
-
displayName:
|
|
69
|
-
|
|
70
|
-
|
|
68
|
+
displayName:
|
|
69
|
+
documentName?.source === 'dg11' ? documentName.displayFirst : undefined,
|
|
70
|
+
displaySurname:
|
|
71
|
+
documentName?.source === 'dg11' ? documentName.displayLast : undefined,
|
|
72
|
+
nameSource:
|
|
73
|
+
documentName?.source === 'dg11' ? documentName.source : undefined,
|
|
71
74
|
gender: this.getGenderEnumType(mrzFields.sex),
|
|
72
75
|
number: mrzFields.documentNumber,
|
|
73
76
|
country: mrzFields.issuingState,
|
|
@@ -137,7 +140,11 @@ export class DataUploadService {
|
|
|
137
140
|
// or not.
|
|
138
141
|
const tempPaths: string[] = [];
|
|
139
142
|
|
|
140
|
-
const writeTempImage = async (
|
|
143
|
+
const writeTempImage = async (
|
|
144
|
+
fileName: string,
|
|
145
|
+
base64Data: string,
|
|
146
|
+
mimeType: string = 'image/jpeg'
|
|
147
|
+
) => {
|
|
141
148
|
const filePath = decodeURIComponent(
|
|
142
149
|
`${RNFS.TemporaryDirectoryPath}/${fileName}`
|
|
143
150
|
);
|
|
@@ -147,11 +154,25 @@ export class DataUploadService {
|
|
|
147
154
|
name: 'files',
|
|
148
155
|
filename: fileName,
|
|
149
156
|
filepath: filePath,
|
|
150
|
-
filetype
|
|
157
|
+
// The filename extension AND this filetype must reflect the REAL format.
|
|
158
|
+
// The backend keys its jp2→jpeg conversion off the .jp2 extension, so a
|
|
159
|
+
// raw JP2 mislabeled as .jpg/image/jpeg reaches face-verification as a
|
|
160
|
+
// corrupt JPEG and is rejected. Pass the actual mimeType for anything
|
|
161
|
+
// that isn't already JPEG (the chip portrait can be jp2/png).
|
|
162
|
+
filetype: mimeType,
|
|
151
163
|
});
|
|
152
164
|
return filePath;
|
|
153
165
|
};
|
|
154
166
|
|
|
167
|
+
// Pick a file extension that matches the image's real mime type so the
|
|
168
|
+
// backend handles it correctly (it converts .jp2 → jpeg on receipt).
|
|
169
|
+
const extensionForMime = (mimeType?: string) =>
|
|
170
|
+
mimeType === 'image/jp2'
|
|
171
|
+
? 'jp2'
|
|
172
|
+
: mimeType === 'image/png'
|
|
173
|
+
? 'png'
|
|
174
|
+
: 'jpg';
|
|
175
|
+
|
|
155
176
|
try {
|
|
156
177
|
// Add document front image
|
|
157
178
|
const frontImage = scannedDocument?.frontImage;
|
|
@@ -165,10 +186,17 @@ export class DataUploadService {
|
|
|
165
186
|
await writeTempImage('DOCUMENT_BACK_IMAGE.jpg', backImage);
|
|
166
187
|
}
|
|
167
188
|
|
|
168
|
-
// Add face image from document
|
|
189
|
+
// Add face image from document. The NFC chip portrait (DG2) can be JPEG,
|
|
190
|
+
// PNG (JS-decoded), or — when native JP2 decode was unavailable — raw JP2,
|
|
191
|
+
// so the extension/filetype must track the real mime type, not assume JPEG.
|
|
169
192
|
const faceImage = scannedDocument?.faceImage;
|
|
170
193
|
if (faceImage && faceImage !== '') {
|
|
171
|
-
|
|
194
|
+
const faceMime = scannedDocument?.faceImageMimeType || 'image/jpeg';
|
|
195
|
+
await writeTempImage(
|
|
196
|
+
`FACE_IMAGE.${extensionForMime(faceMime)}`,
|
|
197
|
+
faceImage,
|
|
198
|
+
faceMime
|
|
199
|
+
);
|
|
172
200
|
}
|
|
173
201
|
|
|
174
202
|
// Add secondary face image from document (optional)
|
|
@@ -335,7 +363,12 @@ export class DataUploadService {
|
|
|
335
363
|
if (scannedDocument) {
|
|
336
364
|
onProgress?.(0.1);
|
|
337
365
|
await runWithRetry(() =>
|
|
338
|
-
this.submitDocumentData(
|
|
366
|
+
this.submitDocumentData(
|
|
367
|
+
identificationId,
|
|
368
|
+
scannedDocument,
|
|
369
|
+
authToken,
|
|
370
|
+
documentName
|
|
371
|
+
)
|
|
339
372
|
);
|
|
340
373
|
onProgress?.(0.3);
|
|
341
374
|
}
|
package/src/version.ts
CHANGED