@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.
Files changed (38) hide show
  1. package/android/src/main/java/com/trustchex/reactnativesdk/camera/TrustchexCameraView.kt +4 -1
  2. package/android/src/main/java/com/trustchex/reactnativesdk/mlkit/MLKitModule.kt +5 -1
  3. package/ios/Camera/TrustchexCameraView.swift +3 -0
  4. package/ios/MLKit/MLKitModule.swift +1 -1
  5. package/lib/module/Shared/Components/EIDScanner.js +35 -4
  6. package/lib/module/Shared/Components/IdentityDocumentCamera.constants.js +9 -0
  7. package/lib/module/Shared/Components/IdentityDocumentCamera.flows.js +38 -3
  8. package/lib/module/Shared/Components/IdentityDocumentCamera.js +56 -18
  9. package/lib/module/Shared/Components/IdentityDocumentCamera.utils.js +20 -5
  10. package/lib/module/Shared/EIDReader/eidReader.js +11 -1
  11. package/lib/module/Translation/Resources/en.js +3 -0
  12. package/lib/module/Translation/Resources/tr.js +3 -0
  13. package/lib/module/version.js +1 -1
  14. package/lib/typescript/src/Shared/Components/EIDScanner.d.ts.map +1 -1
  15. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.constants.d.ts +1 -0
  16. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.constants.d.ts.map +1 -1
  17. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.d.ts.map +1 -1
  18. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.flows.d.ts +1 -1
  19. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.flows.d.ts.map +1 -1
  20. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.utils.d.ts +1 -1
  21. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.utils.d.ts.map +1 -1
  22. package/lib/typescript/src/Shared/EIDReader/eidReader.d.ts.map +1 -1
  23. package/lib/typescript/src/Translation/Resources/en.d.ts +3 -0
  24. package/lib/typescript/src/Translation/Resources/en.d.ts.map +1 -1
  25. package/lib/typescript/src/Translation/Resources/tr.d.ts +3 -0
  26. package/lib/typescript/src/Translation/Resources/tr.d.ts.map +1 -1
  27. package/lib/typescript/src/version.d.ts +1 -1
  28. package/lib/typescript/src/version.d.ts.map +1 -1
  29. package/package.json +1 -1
  30. package/src/Shared/Components/EIDScanner.tsx +49 -2
  31. package/src/Shared/Components/IdentityDocumentCamera.constants.ts +9 -0
  32. package/src/Shared/Components/IdentityDocumentCamera.flows.ts +49 -3
  33. package/src/Shared/Components/IdentityDocumentCamera.tsx +64 -26
  34. package/src/Shared/Components/IdentityDocumentCamera.utils.ts +21 -3
  35. package/src/Shared/EIDReader/eidReader.ts +11 -1
  36. package/src/Translation/Resources/en.ts +5 -0
  37. package/src/Translation/Resources/tr.ts +5 -0
  38. package/src/version.ts +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"en.d.ts","sourceRoot":"","sources":["../../../../../src/Translation/Resources/en.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,wBAoRE"}
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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,wBAuRE"}
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.495.11";
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,aAAa,CAAC"}
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@trustchex/react-native-sdk",
3
- "version": "1.495.11",
3
+ "version": "1.496.0",
4
4
  "description": "Trustchex mobile app react native SDK for android or ios devices",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",
@@ -1,5 +1,13 @@
1
1
  import React, { useState, useEffect, useCallback, useRef } from 'react';
2
- import { Alert, View, Text, Image, StyleSheet, Animated } from 'react-native';
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 < 100) {
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 { PASSPORT_MRZ_PATTERN } from './IdentityDocumentCamera.constants';
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 (mrzText && PASSPORT_MRZ_PATTERN.test(mrzText)) {
191
- // Passport MRZ pattern visible even if not fully parsed
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) => (prev === mrzStableAndValid ? prev : mrzStableAndValid));
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
- // Safety: passport somehow reached ID_BACK step
1211
- if (detectedDocumentType === 'PASSPORT') {
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: parsedMRZData?.fields,
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: ds.minBrightness === 255 && ds.frames === 0 ? 0 : ds.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) && styles.topZoneTextWarning,
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
- // But we can't be 100% sure yet - passport MRZ might appear in next frames
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) * 70;
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
@@ -1,3 +1,3 @@
1
1
  // This file is auto-generated. Do not edit manually.
2
2
  // Version is synced from package.json during build.
3
- export const SDK_VERSION = '1.495.11';
3
+ export const SDK_VERSION = '1.496.0';