@trustchex/react-native-sdk 1.495.11 → 1.496.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/src/main/java/com/trustchex/reactnativesdk/camera/TrustchexCameraView.kt +4 -1
- package/android/src/main/java/com/trustchex/reactnativesdk/mlkit/MLKitModule.kt +5 -1
- package/ios/Camera/TrustchexCameraView.swift +3 -0
- package/ios/MLKit/MLKitModule.swift +1 -1
- package/lib/module/Shared/Components/EIDScanner.js +35 -4
- package/lib/module/Shared/Components/IdentityDocumentCamera.constants.js +9 -0
- package/lib/module/Shared/Components/IdentityDocumentCamera.flows.js +38 -3
- package/lib/module/Shared/Components/IdentityDocumentCamera.js +56 -18
- package/lib/module/Shared/Components/IdentityDocumentCamera.utils.js +20 -5
- package/lib/module/Shared/EIDReader/eidReader.js +11 -1
- package/lib/module/Translation/Resources/en.js +3 -0
- package/lib/module/Translation/Resources/tr.js +3 -0
- 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 +1 -1
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.flows.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/EIDReader/eidReader.d.ts.map +1 -1
- package/lib/typescript/src/Translation/Resources/en.d.ts +3 -0
- package/lib/typescript/src/Translation/Resources/en.d.ts.map +1 -1
- package/lib/typescript/src/Translation/Resources/tr.d.ts +3 -0
- package/lib/typescript/src/Translation/Resources/tr.d.ts.map +1 -1
- package/lib/typescript/src/version.d.ts +1 -1
- package/lib/typescript/src/version.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/Shared/Components/EIDScanner.tsx +49 -2
- package/src/Shared/Components/IdentityDocumentCamera.constants.ts +9 -0
- package/src/Shared/Components/IdentityDocumentCamera.flows.ts +49 -3
- package/src/Shared/Components/IdentityDocumentCamera.tsx +64 -26
- package/src/Shared/Components/IdentityDocumentCamera.utils.ts +21 -3
- package/src/Shared/EIDReader/eidReader.ts +11 -1
- package/src/Translation/Resources/en.ts +5 -0
- package/src/Translation/Resources/tr.ts +5 -0
- package/src/version.ts +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"en.d.ts","sourceRoot":"","sources":["../../../../../src/Translation/Resources/en.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"en.d.ts","sourceRoot":"","sources":["../../../../../src/Translation/Resources/en.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,wBAyRE"}
|
|
@@ -111,11 +111,13 @@ declare const _default: {
|
|
|
111
111
|
'eidScannerScreen.placePassportOnNFC': string;
|
|
112
112
|
'eidScannerScreen.placeDocumentOnNFC': string;
|
|
113
113
|
'eidScannerScreen.readingDocument': string;
|
|
114
|
+
'eidScannerScreen.keepStill': string;
|
|
114
115
|
'eidScannerScreen.checkYourInformation': string;
|
|
115
116
|
'eidScannerScreen.approveAndContinue': string;
|
|
116
117
|
'eidScannerScreen.connecting': string;
|
|
117
118
|
'eidScannerScreen.readingMRZ': string;
|
|
118
119
|
'eidScannerScreen.readingFaceImage': string;
|
|
120
|
+
'eidScannerScreen.decodingFaceImage': string;
|
|
119
121
|
'eidScannerScreen.completing': string;
|
|
120
122
|
'eidScannerScreen.progress': string;
|
|
121
123
|
'identityDocumentCamera.guideHeader': string;
|
|
@@ -140,6 +142,7 @@ declare const _default: {
|
|
|
140
142
|
'identityDocumentCamera.hologramCheck': string;
|
|
141
143
|
'identityDocumentCamera.keepSteady': string;
|
|
142
144
|
'identityDocumentCamera.avoidBlur': string;
|
|
145
|
+
'identityDocumentCamera.reduceGlare': string;
|
|
143
146
|
'identityDocumentCamera.wrongSideFront': string;
|
|
144
147
|
'identityDocumentCamera.wrongSideBack': string;
|
|
145
148
|
'identityDocumentCamera.idCardDetected': string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tr.d.ts","sourceRoot":"","sources":["../../../../../src/Translation/Resources/tr.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"tr.d.ts","sourceRoot":"","sources":["../../../../../src/Translation/Resources/tr.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,wBA4RE"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const SDK_VERSION = "1.
|
|
1
|
+
export declare const SDK_VERSION = "1.496.0";
|
|
2
2
|
//# sourceMappingURL=version.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../../../src/version.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,WAAW,
|
|
1
|
+
{"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../../../src/version.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,WAAW,YAAY,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
Alert,
|
|
4
|
+
View,
|
|
5
|
+
Text,
|
|
6
|
+
Image,
|
|
7
|
+
StyleSheet,
|
|
8
|
+
Animated,
|
|
9
|
+
Vibration,
|
|
10
|
+
} from 'react-native';
|
|
3
11
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
4
12
|
import NFCManager from 'react-native-nfc-manager';
|
|
5
13
|
import DeviceInfo from 'react-native-device-info';
|
|
@@ -254,19 +262,31 @@ const EIDScanner = ({
|
|
|
254
262
|
// "reading document" spinner never clears and the user is stuck with no
|
|
255
263
|
// error. Cap the whole read; on timeout we throw a categorized error so
|
|
256
264
|
// the catch below surfaces a retry prompt and finally resets the UI.
|
|
265
|
+
// Fire a "chip detected" haptic exactly once, the moment progress first
|
|
266
|
+
// moves off 0 (the chip connected and the read began). Confirms contact
|
|
267
|
+
// to a user who can't see the screen while holding the phone to the doc.
|
|
268
|
+
let chipDetectedHapticFired = false;
|
|
257
269
|
const readPromise = eidReader(
|
|
258
270
|
documentNumber,
|
|
259
271
|
dateOfBirth,
|
|
260
272
|
dateOfExpiry,
|
|
261
273
|
(progressValue) => {
|
|
274
|
+
if (progressValue > 0 && !chipDetectedHapticFired) {
|
|
275
|
+
chipDetectedHapticFired = true;
|
|
276
|
+
Vibration.vibrate(40);
|
|
277
|
+
}
|
|
262
278
|
setProgress(progressValue);
|
|
263
279
|
// Update stage message based on progress
|
|
264
280
|
if (progressValue <= 20) {
|
|
265
281
|
setProgressStage(t('eidScannerScreen.connecting'));
|
|
266
282
|
} else if (progressValue <= 30) {
|
|
267
283
|
setProgressStage(t('eidScannerScreen.readingMRZ'));
|
|
268
|
-
} else if (progressValue <
|
|
284
|
+
} else if (progressValue < 95) {
|
|
269
285
|
setProgressStage(t('eidScannerScreen.readingFaceImage'));
|
|
286
|
+
} else if (progressValue < 100) {
|
|
287
|
+
// 95→100 is the (possibly slow) photo decode — give it its own
|
|
288
|
+
// stage so the bar doesn't look frozen at "Finalizing".
|
|
289
|
+
setProgressStage(t('eidScannerScreen.decodingFaceImage'));
|
|
270
290
|
} else {
|
|
271
291
|
setProgressStage(t('eidScannerScreen.completing'));
|
|
272
292
|
}
|
|
@@ -292,6 +312,11 @@ const EIDScanner = ({
|
|
|
292
312
|
}
|
|
293
313
|
}
|
|
294
314
|
if (passportData) {
|
|
315
|
+
// Success haptic: the phone is pressed against the document, so the
|
|
316
|
+
// user often can't see the screen — a buzz confirms the chip read
|
|
317
|
+
// finished. (The NFC step previously had no haptics at all, unlike the
|
|
318
|
+
// camera/liveness screens.)
|
|
319
|
+
Vibration.vibrate(120);
|
|
295
320
|
const scanDuration = Date.now() - startTime;
|
|
296
321
|
const documentData = normalizeFromMRZInfo(passportData.mrz);
|
|
297
322
|
const builtName = buildDocumentName(
|
|
@@ -429,6 +454,10 @@ const EIDScanner = ({
|
|
|
429
454
|
tag_lost: 'eidScannerScreen.nfcTagLost',
|
|
430
455
|
timeout: 'eidScannerScreen.nfcTimeout',
|
|
431
456
|
not_enabled: 'eidScannerScreen.nfcNotEnabled',
|
|
457
|
+
// The default/most-common "chip just wouldn't read" bucket. Show the
|
|
458
|
+
// actionable hold-flat-and-retry guidance rather than the generic
|
|
459
|
+
// "contact support" message — re-placing the phone usually fixes it.
|
|
460
|
+
reading_error: 'eidScannerScreen.nfcReadFailed',
|
|
432
461
|
};
|
|
433
462
|
|
|
434
463
|
if (category !== 'user_cancelled') {
|
|
@@ -831,6 +860,17 @@ const EIDScanner = ({
|
|
|
831
860
|
</Text>
|
|
832
861
|
)))}
|
|
833
862
|
|
|
863
|
+
{/* The connect/auth phase (progress still 0) is the most fragile part
|
|
864
|
+
of the read — the many PACE/BAC round-trips drop if the phone
|
|
865
|
+
moves. Tell the user to hold still here, not only once the bar
|
|
866
|
+
starts moving (the progress UI's "Hold steady" is gated on
|
|
867
|
+
progress > 0). */}
|
|
868
|
+
{hasNfc && isEnabled && isScanning && progress === 0 && (
|
|
869
|
+
<Text style={styles.keepStillHint}>
|
|
870
|
+
{t('eidScannerScreen.keepStill')}
|
|
871
|
+
</Text>
|
|
872
|
+
)}
|
|
873
|
+
|
|
834
874
|
{hasNfc && isEnabled && !isScanning && !isScanned && (
|
|
835
875
|
<View style={styles.buttonsContainer}>
|
|
836
876
|
<StyledButton mode="contained" onPress={readNFC}>
|
|
@@ -938,6 +978,13 @@ const styles = StyleSheet.create({
|
|
|
938
978
|
color: 'black',
|
|
939
979
|
textAlign: 'center',
|
|
940
980
|
},
|
|
981
|
+
keepStillHint: {
|
|
982
|
+
fontSize: 15,
|
|
983
|
+
color: '#666',
|
|
984
|
+
textAlign: 'center',
|
|
985
|
+
marginTop: 12,
|
|
986
|
+
paddingHorizontal: 24,
|
|
987
|
+
},
|
|
941
988
|
// ── ID Card result layout ──────────────────────────────────────────────
|
|
942
989
|
idCardWrapper: {
|
|
943
990
|
gap: 10,
|
|
@@ -17,6 +17,15 @@ export const REQUIRED_CONSISTENT_DOCTYPE_DETECTIONS = 3;
|
|
|
17
17
|
export const SIGNATURE_REGEX = /s[gi]g?n[au]?t[u]?r|imz[a]s?/i;
|
|
18
18
|
export const SIGNATURE_TEXT_REGEX = /signature|imza|İmza/i;
|
|
19
19
|
export const MRZ_BLOCK_PATTERN = /[A-Z0-9<]{8,}.*</i;
|
|
20
|
+
// A DENSE MRZ run — a contiguous block of MRZ characters that contains a chevron
|
|
21
|
+
// FILLER pair (`<<`). Real machine-readable zones are padded with `<<…`; a card
|
|
22
|
+
// FACE (ID front) has no MRZ, and the occasional stray single `<` OCR'd from a
|
|
23
|
+
// guilloche/chevron glyph will NOT contain a `<<` filler run. Used to decide
|
|
24
|
+
// "is a real MRZ visible on this front?" without the false positives of
|
|
25
|
+
// MRZ_BLOCK_PATTERN (whose `.*<` lets a lone detached `<` anywhere in the frame
|
|
26
|
+
// match) — that looseness could otherwise wedge a legitimate ID front that
|
|
27
|
+
// happens to OCR a stray `<`.
|
|
28
|
+
export const MRZ_DENSE_PATTERN = /[A-Z0-9<]{6,}<<[A-Z0-9<]*/i;
|
|
20
29
|
// Matches the line-1 start of ANY ICAO TD3 passport MRZ, so the camera routes
|
|
21
30
|
// every passport variant into the passport flow (not the ID-card back-side
|
|
22
31
|
// flow). Per ICAO 9303 Part 4: `P` + a type char (filler `<` or a subtype
|
|
@@ -30,7 +30,10 @@
|
|
|
30
30
|
|
|
31
31
|
import type { Face, DocumentType } from './IdentityDocumentCamera.types';
|
|
32
32
|
import type { MRZFields } from '../Types/mrzFields';
|
|
33
|
-
import {
|
|
33
|
+
import {
|
|
34
|
+
PASSPORT_MRZ_PATTERN,
|
|
35
|
+
MRZ_DENSE_PATTERN,
|
|
36
|
+
} from './IdentityDocumentCamera.constants';
|
|
34
37
|
import {
|
|
35
38
|
isIDCardDocumentCode,
|
|
36
39
|
isPassportDocumentCode,
|
|
@@ -58,6 +61,7 @@ export interface IDFrontFlowResult {
|
|
|
58
61
|
reason?: string;
|
|
59
62
|
nextAction?:
|
|
60
63
|
| 'WAIT_FOR_ELEMENTS'
|
|
64
|
+
| 'WAIT_FOR_MRZ'
|
|
61
65
|
| 'REJECT_AS_PASSPORT'
|
|
62
66
|
| 'PROCEED_TO_HOLOGRAM';
|
|
63
67
|
}
|
|
@@ -187,8 +191,17 @@ export function handleIDFrontFlow(
|
|
|
187
191
|
};
|
|
188
192
|
}
|
|
189
193
|
|
|
190
|
-
if
|
|
191
|
-
|
|
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
|
+
) {
|
|
192
205
|
return {
|
|
193
206
|
shouldProceed: false,
|
|
194
207
|
reason: 'Passport MRZ pattern visible - not an ID card',
|
|
@@ -208,6 +221,39 @@ export function handleIDFrontFlow(
|
|
|
208
221
|
};
|
|
209
222
|
}
|
|
210
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
|
+
|
|
211
257
|
// ============================================================================
|
|
212
258
|
// STEP 3: Check for signature (optional front side confirmation)
|
|
213
259
|
// ============================================================================
|
|
@@ -69,7 +69,6 @@ import {
|
|
|
69
69
|
REQUIRED_CONSISTENT_DOCTYPE_DETECTIONS,
|
|
70
70
|
SIGNATURE_TEXT_REGEX,
|
|
71
71
|
MRZ_BLOCK_PATTERN,
|
|
72
|
-
PASSPORT_MRZ_PATTERN,
|
|
73
72
|
MIN_CARD_FACE_SIZE_PERCENT,
|
|
74
73
|
} from './IdentityDocumentCamera.constants';
|
|
75
74
|
import type {
|
|
@@ -138,6 +137,11 @@ const IdentityDocumentCamera = ({
|
|
|
138
137
|
>(undefined);
|
|
139
138
|
const [isBrightnessLow, setIsBrightnessLow] = useState(false);
|
|
140
139
|
const [isFrameBlurry, setIsFrameBlurry] = useState(false);
|
|
140
|
+
// Glare was being silently rejected (frames dropped with no user feedback), so
|
|
141
|
+
// a user fighting reflections just saw the scan hang. Surface it so we can show
|
|
142
|
+
// an actionable "move away from glare" hint. Set at each glare rejection;
|
|
143
|
+
// cleared when a frame passes the brightness check (a fresh, glare-free frame).
|
|
144
|
+
const [isGlareDetected, setIsGlareDetected] = useState(false);
|
|
141
145
|
const [hasGuideShown, setHasGuideShown] = useState(false);
|
|
142
146
|
const [status, setStatus] = useState<
|
|
143
147
|
'SEARCHING' | 'SCANNING' | 'SCANNED' | 'INCORRECT'
|
|
@@ -235,6 +239,7 @@ const IdentityDocumentCamera = ({
|
|
|
235
239
|
frameH: 0,
|
|
236
240
|
hadLowBrightness: false,
|
|
237
241
|
hadBlurryFrames: false,
|
|
242
|
+
hadGlare: false,
|
|
238
243
|
});
|
|
239
244
|
const [faceDetectionEnabled, setFaceDetectionEnabled] = useState(true);
|
|
240
245
|
const faceImages = useRef<string[]>([]);
|
|
@@ -349,6 +354,7 @@ const IdentityDocumentCamera = ({
|
|
|
349
354
|
frameH: 0,
|
|
350
355
|
hadLowBrightness: false,
|
|
351
356
|
hadBlurryFrames: false,
|
|
357
|
+
hadGlare: false,
|
|
352
358
|
};
|
|
353
359
|
cachedBarcode.current = null;
|
|
354
360
|
isCompletionCallbackInvoked.current = false;
|
|
@@ -397,6 +403,7 @@ const IdentityDocumentCamera = ({
|
|
|
397
403
|
detectedDocumentType,
|
|
398
404
|
isBrightnessLow,
|
|
399
405
|
isFrameBlurry,
|
|
406
|
+
isGlareDetected,
|
|
400
407
|
allElementsDetected,
|
|
401
408
|
elementsOutsideScanArea,
|
|
402
409
|
t
|
|
@@ -419,6 +426,7 @@ const IdentityDocumentCamera = ({
|
|
|
419
426
|
hasGuideShown,
|
|
420
427
|
isBrightnessLow,
|
|
421
428
|
isFrameBlurry,
|
|
429
|
+
isGlareDetected,
|
|
422
430
|
nextStep,
|
|
423
431
|
status,
|
|
424
432
|
detectedDocumentType,
|
|
@@ -567,7 +575,9 @@ const IdentityDocumentCamera = ({
|
|
|
567
575
|
}
|
|
568
576
|
};
|
|
569
577
|
|
|
570
|
-
// Check if face image has glare (brightness exceeds threshold)
|
|
578
|
+
// Check if face image has glare (brightness exceeds threshold). Also surfaces
|
|
579
|
+
// the result to `isGlareDetected` so the user gets an actionable hint instead
|
|
580
|
+
// of a silently-stalled scan.
|
|
571
581
|
const checkFaceGlare = async (faceImage: string): Promise<boolean> => {
|
|
572
582
|
try {
|
|
573
583
|
// Check entire face region for glare
|
|
@@ -579,6 +589,8 @@ const IdentityDocumentCamera = ({
|
|
|
579
589
|
100, // Full face height
|
|
580
590
|
MAX_BRIGHTNESS_THRESHOLD
|
|
581
591
|
);
|
|
592
|
+
setIsGlareDetected(hasGlare);
|
|
593
|
+
if (hasGlare) diagStats.current.hadGlare = true;
|
|
582
594
|
return hasGlare;
|
|
583
595
|
} catch (error) {
|
|
584
596
|
return false; // Assume no glare on error
|
|
@@ -617,6 +629,8 @@ const IdentityDocumentCamera = ({
|
|
|
617
629
|
Math.round(height * 0.8),
|
|
618
630
|
MAX_BRIGHTNESS_THRESHOLD
|
|
619
631
|
);
|
|
632
|
+
setIsGlareDetected(hasGlare);
|
|
633
|
+
if (hasGlare) diagStats.current.hadGlare = true;
|
|
620
634
|
return hasGlare;
|
|
621
635
|
} catch (error) {
|
|
622
636
|
return false;
|
|
@@ -1169,7 +1183,9 @@ const IdentityDocumentCamera = ({
|
|
|
1169
1183
|
// Surface the reliable-detection signal to the render so the UI can show a
|
|
1170
1184
|
// green "GO" the moment (and only once) the consensus is stable + valid.
|
|
1171
1185
|
// Never flips green on an early/transient valid read.
|
|
1172
|
-
setMrzReliable((prev) =>
|
|
1186
|
+
setMrzReliable((prev) =>
|
|
1187
|
+
prev === mrzStableAndValid ? prev : mrzStableAndValid
|
|
1188
|
+
);
|
|
1173
1189
|
|
|
1174
1190
|
// Test-mode panel: show the MRZ ONLY once it is stable + valid (the same
|
|
1175
1191
|
// consensus matched across `stabilityTarget` consecutive frames). Showing
|
|
@@ -1179,9 +1195,7 @@ const IdentityDocumentCamera = ({
|
|
|
1179
1195
|
// check-digit-valid consensus and never an unstable intermediate.
|
|
1180
1196
|
if (testMode && mrzStableAndValid && mrzText) {
|
|
1181
1197
|
setTestModeData((prev) =>
|
|
1182
|
-
prev?.mrzText === mrzText
|
|
1183
|
-
? prev
|
|
1184
|
-
: { mrzText, timestamp: Date.now() }
|
|
1198
|
+
prev?.mrzText === mrzText ? prev : { mrzText, timestamp: Date.now() }
|
|
1185
1199
|
);
|
|
1186
1200
|
}
|
|
1187
1201
|
|
|
@@ -1194,30 +1208,42 @@ const IdentityDocumentCamera = ({
|
|
|
1194
1208
|
const hasFaces = primaryFaces.length > 0;
|
|
1195
1209
|
const hasSignature = /signature|imza|İmza/i.test(text);
|
|
1196
1210
|
const hasPassportMRZ = parsedMRZData?.fields?.documentCode === 'P';
|
|
1197
|
-
const hasPassportMRZPattern =
|
|
1198
|
-
mrzText && PASSPORT_MRZ_PATTERN.test(mrzText);
|
|
1199
|
-
|
|
1200
|
-
if (
|
|
1201
|
-
hasFaces ||
|
|
1202
|
-
hasSignature ||
|
|
1203
|
-
hasPassportMRZ ||
|
|
1204
|
-
hasPassportMRZPattern
|
|
1205
|
-
) {
|
|
1206
|
-
setStatus('INCORRECT');
|
|
1207
|
-
return;
|
|
1208
|
-
}
|
|
1209
1211
|
|
|
1210
|
-
//
|
|
1211
|
-
|
|
1212
|
+
// RECOVERY (must run BEFORE the wrong-side guard below): a passport was
|
|
1213
|
+
// misrouted to SCAN_ID_BACK. A passport has only its data page, which
|
|
1214
|
+
// carries a face + MRZ — exactly what the wrong-side guard treats as
|
|
1215
|
+
// "wrong side", so without this it would loop on INCORRECT forever
|
|
1216
|
+
// (the reported "stuck asking for ID back" case). If we have positive
|
|
1217
|
+
// passport evidence here, the front was already captured, so complete
|
|
1218
|
+
// as a passport instead of waiting for a back that does not exist.
|
|
1219
|
+
//
|
|
1220
|
+
// We deliberately gate ONLY on authoritative signals: a locked PASSPORT
|
|
1221
|
+
// classification, or a PARSED MRZ document code of 'P'. We do NOT test
|
|
1222
|
+
// the raw OCR `text` against PASSPORT_MRZ_PATTERN here — an ID card's
|
|
1223
|
+
// BACK legitimately carries an MRZ whose name line ("PAPPAS<<MARIA…",
|
|
1224
|
+
// "…<<PHILIPP…") can false-match the unanchored pattern and would then
|
|
1225
|
+
// wrongly complete a real ID card as a passport, skipping the back-side
|
|
1226
|
+
// MRZ + barcode validation entirely.
|
|
1227
|
+
if (detectedDocumentType === 'PASSPORT' || hasPassportMRZ) {
|
|
1212
1228
|
transitionStepWithCallback('COMPLETED', 'SCAN_ID_BACK', {
|
|
1213
1229
|
image,
|
|
1214
1230
|
documentType: 'PASSPORT',
|
|
1215
|
-
mrzText: mrzText ?? undefined,
|
|
1216
|
-
mrzFields:
|
|
1231
|
+
mrzText: mrzText ?? lastValidMRZText.current ?? undefined,
|
|
1232
|
+
mrzFields:
|
|
1233
|
+
parsedMRZData?.fields ??
|
|
1234
|
+
lastValidMRZFields.current ??
|
|
1235
|
+
undefined,
|
|
1217
1236
|
});
|
|
1218
1237
|
return;
|
|
1219
1238
|
}
|
|
1220
1239
|
|
|
1240
|
+
// Wrong-side detection: a face or signature means we're looking at a
|
|
1241
|
+
// front, not an ID-card back (which has neither).
|
|
1242
|
+
if (hasFaces || hasSignature) {
|
|
1243
|
+
setStatus('INCORRECT');
|
|
1244
|
+
return;
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1221
1247
|
setElementsOutsideScanArea([]);
|
|
1222
1248
|
|
|
1223
1249
|
const flowResult = handleIDBackFlow(
|
|
@@ -2030,6 +2056,15 @@ const IdentityDocumentCamera = ({
|
|
|
2030
2056
|
setIsBrightnessLow(!isRegionBright);
|
|
2031
2057
|
if (!isRegionBright) diagStats.current.hadLowBrightness = true;
|
|
2032
2058
|
|
|
2059
|
+
// Reset glare to false at the start of every frame. The glare helpers
|
|
2060
|
+
// (checkDocumentGlare/checkFaceGlare) only run at accept-time and set it
|
|
2061
|
+
// true when they find glare; without this per-frame clear the flag would
|
|
2062
|
+
// latch — a "Glare detected" warning would stick over a now-clean view
|
|
2063
|
+
// (and, once a face is locked, the face-glare site never runs again so it
|
|
2064
|
+
// could never clear at all). The accept-time checks run later in this same
|
|
2065
|
+
// frame's handlers and re-assert true if glare is still present.
|
|
2066
|
+
setIsGlareDetected(false);
|
|
2067
|
+
|
|
2033
2068
|
// Check blur only in center region (area of interest) to avoid false positives
|
|
2034
2069
|
// from iOS depth-of-field background blur
|
|
2035
2070
|
let isNotBlurry = true;
|
|
@@ -2060,7 +2095,8 @@ const IdentityDocumentCamera = ({
|
|
|
2060
2095
|
frameH: ds.frameH,
|
|
2061
2096
|
avgBrightness:
|
|
2062
2097
|
ds.frames > 0 ? Math.round(ds.brightnessSum / ds.frames) : 0,
|
|
2063
|
-
minBrightness:
|
|
2098
|
+
minBrightness:
|
|
2099
|
+
ds.minBrightness === 255 && ds.frames === 0 ? 0 : ds.minBrightness,
|
|
2064
2100
|
maxBrightness: ds.maxBrightness,
|
|
2065
2101
|
hadLowBrightness: ds.hadLowBrightness,
|
|
2066
2102
|
hadBlurryFrames: ds.hadBlurryFrames,
|
|
@@ -2654,13 +2690,15 @@ const IdentityDocumentCamera = ({
|
|
|
2654
2690
|
opacity: errorFlashAnim,
|
|
2655
2691
|
},
|
|
2656
2692
|
// 3. Warning (yellow) - quality issues
|
|
2657
|
-
(isBrightnessLow || isFrameBlurry) &&
|
|
2693
|
+
(isBrightnessLow || isFrameBlurry || isGlareDetected) &&
|
|
2694
|
+
styles.topZoneTextWarning,
|
|
2658
2695
|
// 4. Scanning (green) - all elements detected AND inside scan area
|
|
2659
2696
|
status === 'SCANNING' &&
|
|
2660
2697
|
allElementsDetected &&
|
|
2661
2698
|
elementsOutsideScanArea.length === 0 &&
|
|
2662
2699
|
!isBrightnessLow &&
|
|
2663
2700
|
!isFrameBlurry &&
|
|
2701
|
+
!isGlareDetected &&
|
|
2664
2702
|
styles.topZoneTextScanning,
|
|
2665
2703
|
// 5. Default (white) - aligning (not all detected OR elements outside scan area)
|
|
2666
2704
|
]}
|
|
@@ -2671,6 +2709,7 @@ const IdentityDocumentCamera = ({
|
|
|
2671
2709
|
detectedDocumentType,
|
|
2672
2710
|
isBrightnessLow,
|
|
2673
2711
|
isFrameBlurry,
|
|
2712
|
+
isGlareDetected,
|
|
2674
2713
|
allElementsDetected,
|
|
2675
2714
|
elementsOutsideScanArea,
|
|
2676
2715
|
t
|
|
@@ -2696,8 +2735,7 @@ const IdentityDocumentCamera = ({
|
|
|
2696
2735
|
: isBrightnessLow || isFrameBlurry
|
|
2697
2736
|
? '#FFC107'
|
|
2698
2737
|
: 'white',
|
|
2699
|
-
borderWidth:
|
|
2700
|
-
mrzReliable || status === 'SCANNING' ? 3 : 2,
|
|
2738
|
+
borderWidth: mrzReliable || status === 'SCANNING' ? 3 : 2,
|
|
2701
2739
|
},
|
|
2702
2740
|
]}
|
|
2703
2741
|
>
|
|
@@ -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
|
|
|
@@ -118,6 +119,7 @@ export function getStatusMessage(
|
|
|
118
119
|
detectedDocumentType: DocumentType,
|
|
119
120
|
isBrightnessLow: boolean,
|
|
120
121
|
isFrameBlurry: boolean,
|
|
122
|
+
isGlareDetected: boolean,
|
|
121
123
|
allElementsDetected: boolean,
|
|
122
124
|
elementsOutsideScanArea: string[],
|
|
123
125
|
t: (key: string, params?: Record<string, unknown>) => string
|
|
@@ -149,6 +151,14 @@ export function getStatusMessage(
|
|
|
149
151
|
return t('identityDocumentCamera.lowBrightness');
|
|
150
152
|
}
|
|
151
153
|
|
|
154
|
+
// Glare is a primary failure mode (reflections off the laminate/photo). It was
|
|
155
|
+
// previously detected but never communicated, so the scan just appeared to
|
|
156
|
+
// hang. Tell the user how to fix it. Ranked above blur because a glare hotspot
|
|
157
|
+
// both blocks acceptance and often reads as a "sharp" (non-blurry) frame.
|
|
158
|
+
if (isGlareDetected) {
|
|
159
|
+
return t('identityDocumentCamera.reduceGlare');
|
|
160
|
+
}
|
|
161
|
+
|
|
152
162
|
if (isFrameBlurry) {
|
|
153
163
|
return t('identityDocumentCamera.avoidBlur');
|
|
154
164
|
}
|
|
@@ -309,10 +319,18 @@ export function detectDocumentType(
|
|
|
309
319
|
return 'ID_FRONT';
|
|
310
320
|
}
|
|
311
321
|
|
|
312
|
-
// If signature present and NO passport MRZ pattern, likely ID_FRONT
|
|
313
|
-
//
|
|
322
|
+
// If signature present and NO passport MRZ pattern, likely ID_FRONT —
|
|
323
|
+
// BUT an ID-card front has no MRZ, so if a real (dense) MRZ is visible here
|
|
324
|
+
// (and it is not a confirmed ID code — that returned above) this is almost
|
|
325
|
+
// certainly a passport data page whose 'P' code just hasn't OCR'd yet.
|
|
326
|
+
// Stay UNKNOWN and let later frames resolve it, rather than locking
|
|
327
|
+
// ID_FRONT and routing a passport to a back side it does not have. We use
|
|
328
|
+
// MRZ_DENSE_PATTERN (needs a `<<` filler run) so a stray single `<` OCR'd
|
|
329
|
+
// from a card-face guilloche cannot wedge a genuine ID front at UNKNOWN.
|
|
330
|
+
const hasUnresolvedMrzBlock =
|
|
331
|
+
!!mrzText || MRZ_DENSE_PATTERN.test(ocrText);
|
|
314
332
|
const hasSignature = hasSignatureMatch;
|
|
315
|
-
if (hasSignature && !hasPassportMRZPattern) {
|
|
333
|
+
if (hasSignature && !hasPassportMRZPattern && !hasUnresolvedMrzBlock) {
|
|
316
334
|
return 'ID_FRONT';
|
|
317
335
|
}
|
|
318
336
|
}
|
|
@@ -732,8 +732,12 @@ const eidReader = async (
|
|
|
732
732
|
// step (it was previously outside, so an early DG2 failure went unlabelled).
|
|
733
733
|
const dg2InputStream = passportService.getInputStream(EIDService.EF_DG2);
|
|
734
734
|
await dg2InputStream.init();
|
|
735
|
+
// Map the DG2 byte stream to 30→95 (not →100): the JPEG2000 decode that
|
|
736
|
+
// follows can take a noticeable beat (esp. large passport portraits), and
|
|
737
|
+
// jumping to 100 before it made the UI sit at "100% Finalizing" looking
|
|
738
|
+
// frozen during the decode. We reserve 95→100 for the decode step below.
|
|
735
739
|
InputStream.onRead((totalBytesRead, fileLength) => {
|
|
736
|
-
progress = 30 + (totalBytesRead / fileLength) *
|
|
740
|
+
progress = 30 + (totalBytesRead / fileLength) * 65;
|
|
737
741
|
if (progressCallback) {
|
|
738
742
|
progressCallback(progress);
|
|
739
743
|
}
|
|
@@ -756,6 +760,12 @@ const eidReader = async (
|
|
|
756
760
|
const buffer = Buffer.alloc(imageLength);
|
|
757
761
|
await imageInputStream.readBytesWithOffset(buffer, 0, imageLength);
|
|
758
762
|
const rawMimeType = faceImageInfo.getMimeType();
|
|
763
|
+
// Signal the (potentially slow) photo decode so the UI shows a distinct
|
|
764
|
+
// "decoding photo" stage at 95% rather than appearing frozen at 100%.
|
|
765
|
+
progress = 95;
|
|
766
|
+
if (progressCallback) {
|
|
767
|
+
progressCallback(progress);
|
|
768
|
+
}
|
|
759
769
|
const converted = await convertJP2IfNeeded(buffer, rawMimeType);
|
|
760
770
|
imageAsBase64 = converted.base64;
|
|
761
771
|
mimeType = converted.mimeType;
|
|
@@ -143,11 +143,14 @@ export default {
|
|
|
143
143
|
'eidScannerScreen.placeDocumentOnNFC':
|
|
144
144
|
'Place your document near the NFC reader',
|
|
145
145
|
'eidScannerScreen.readingDocument': 'Hold steady - Reading document',
|
|
146
|
+
'eidScannerScreen.keepStill':
|
|
147
|
+
'Keep the document and phone still until the read completes.',
|
|
146
148
|
'eidScannerScreen.checkYourInformation': 'Review your information carefully',
|
|
147
149
|
'eidScannerScreen.approveAndContinue': 'Approve and Continue',
|
|
148
150
|
'eidScannerScreen.connecting': 'Establishing secure connection...',
|
|
149
151
|
'eidScannerScreen.readingMRZ': 'Reading document information...',
|
|
150
152
|
'eidScannerScreen.readingFaceImage': 'Reading photo data...',
|
|
153
|
+
'eidScannerScreen.decodingFaceImage': 'Decoding photo...',
|
|
151
154
|
'eidScannerScreen.completing': 'Finalizing verification...',
|
|
152
155
|
'eidScannerScreen.progress': 'Progress',
|
|
153
156
|
'identityDocumentCamera.guideHeader': 'Document Scan',
|
|
@@ -178,6 +181,8 @@ export default {
|
|
|
178
181
|
'identityDocumentCamera.hologramCheck': 'Hologram',
|
|
179
182
|
'identityDocumentCamera.keepSteady': 'Keep device steady',
|
|
180
183
|
'identityDocumentCamera.avoidBlur': 'Keep device steady',
|
|
184
|
+
'identityDocumentCamera.reduceGlare':
|
|
185
|
+
'Glare detected. Tilt the document or move away from direct light.',
|
|
181
186
|
'identityDocumentCamera.wrongSideFront':
|
|
182
187
|
'Show the front side of your document',
|
|
183
188
|
'identityDocumentCamera.wrongSideBack': 'Show the back side of your document',
|
|
@@ -143,12 +143,15 @@ export default {
|
|
|
143
143
|
'Pasaportunuzu NFC okuyucuya yaklaştırın',
|
|
144
144
|
'eidScannerScreen.placeDocumentOnNFC': 'Belgenizi NFC okuyucuya yaklaştırın',
|
|
145
145
|
'eidScannerScreen.readingDocument': 'Sabit tutun - Belge okunuyor',
|
|
146
|
+
'eidScannerScreen.keepStill':
|
|
147
|
+
'Okuma tamamlanana kadar belgeyi ve telefonu sabit tutun.',
|
|
146
148
|
'eidScannerScreen.checkYourInformation':
|
|
147
149
|
'Bilgilerinizi dikkatle kontrol edin',
|
|
148
150
|
'eidScannerScreen.approveAndContinue': 'Onayla ve Devam Et',
|
|
149
151
|
'eidScannerScreen.connecting': 'Güvenli bağlantı kuruluyor...',
|
|
150
152
|
'eidScannerScreen.readingMRZ': 'Belge bilgileri okunuyor...',
|
|
151
153
|
'eidScannerScreen.readingFaceImage': 'Fotoğraf verisi okunuyor...',
|
|
154
|
+
'eidScannerScreen.decodingFaceImage': 'Fotoğraf çözümleniyor...',
|
|
152
155
|
'eidScannerScreen.completing': 'Doğrulama tamamlanıyor...',
|
|
153
156
|
'eidScannerScreen.progress': 'İlerleme',
|
|
154
157
|
'identityDocumentCamera.guideHeader': 'Belge Taraması',
|
|
@@ -179,6 +182,8 @@ export default {
|
|
|
179
182
|
'identityDocumentCamera.hologramCheck': 'Hologram',
|
|
180
183
|
'identityDocumentCamera.keepSteady': 'Cihazı sabit tutun',
|
|
181
184
|
'identityDocumentCamera.avoidBlur': 'Cihazı sabit tutun',
|
|
185
|
+
'identityDocumentCamera.reduceGlare':
|
|
186
|
+
'Parlama algılandı. Belgeyi hafifçe eğin veya doğrudan ışıktan uzaklaşın.',
|
|
182
187
|
'identityDocumentCamera.wrongSideFront': 'Belgenizin ön yüzünü gösterin',
|
|
183
188
|
'identityDocumentCamera.wrongSideBack': 'Belgenizin arka yüzünü gösterin',
|
|
184
189
|
'identityDocumentCamera.idCardDetected': 'Kimlik kartı algılandı',
|
package/src/version.ts
CHANGED