@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.
Files changed (44) hide show
  1. package/android/build.gradle +10 -0
  2. package/android/libs/jj2000-5.2.jar +0 -0
  3. package/android/src/main/java/com/trustchex/reactnativesdk/ImageDecoderModule.kt +88 -0
  4. package/android/src/main/java/com/trustchex/reactnativesdk/Jp2PgxDecoder.kt +135 -0
  5. package/android/src/main/java/com/trustchex/reactnativesdk/TrustchexSDKPackage.kt +12 -0
  6. package/android/src/test/java/com/trustchex/reactnativesdk/Jp2PgxDecoderTest.kt +143 -0
  7. package/android/src/test/resources/jp2/gray.jp2 +0 -0
  8. package/android/src/test/resources/jp2/solid_color.j2k +0 -0
  9. package/android/src/test/resources/jp2/solid_color.jp2 +0 -0
  10. package/lib/module/Shared/Components/EIDScanner.js +8 -5
  11. package/lib/module/Shared/Components/IdentityDocumentCamera.constants.js +9 -0
  12. package/lib/module/Shared/Components/IdentityDocumentCamera.flows.js +57 -19
  13. package/lib/module/Shared/Components/IdentityDocumentCamera.js +69 -14
  14. package/lib/module/Shared/Components/IdentityDocumentCamera.utils.js +33 -4
  15. package/lib/module/Shared/EIDReader/eidReader.js +11 -9
  16. package/lib/module/Shared/Libs/jp2Decode.js +12 -11
  17. package/lib/module/Shared/Services/DataUploadService.js +16 -4
  18. package/lib/module/version.js +1 -1
  19. package/lib/typescript/src/Shared/Components/EIDScanner.d.ts.map +1 -1
  20. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.constants.d.ts +1 -0
  21. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.constants.d.ts.map +1 -1
  22. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.d.ts.map +1 -1
  23. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.flows.d.ts +2 -2
  24. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.flows.d.ts.map +1 -1
  25. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.types.d.ts +21 -0
  26. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.types.d.ts.map +1 -1
  27. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.utils.d.ts +15 -0
  28. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.utils.d.ts.map +1 -1
  29. package/lib/typescript/src/Shared/EIDReader/eidReader.d.ts.map +1 -1
  30. package/lib/typescript/src/Shared/Libs/jp2Decode.d.ts +4 -3
  31. package/lib/typescript/src/Shared/Libs/jp2Decode.d.ts.map +1 -1
  32. package/lib/typescript/src/Shared/Services/DataUploadService.d.ts.map +1 -1
  33. package/lib/typescript/src/version.d.ts +1 -1
  34. package/package.json +1 -1
  35. package/src/Shared/Components/EIDScanner.tsx +7 -5
  36. package/src/Shared/Components/IdentityDocumentCamera.constants.ts +9 -0
  37. package/src/Shared/Components/IdentityDocumentCamera.flows.ts +78 -21
  38. package/src/Shared/Components/IdentityDocumentCamera.tsx +83 -27
  39. package/src/Shared/Components/IdentityDocumentCamera.types.ts +21 -0
  40. package/src/Shared/Components/IdentityDocumentCamera.utils.ts +36 -3
  41. package/src/Shared/EIDReader/eidReader.ts +11 -9
  42. package/src/Shared/Libs/jp2Decode.ts +15 -11
  43. package/src/Shared/Services/DataUploadService.ts +41 -8
  44. 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 { PASSPORT_MRZ_PATTERN } from './IdentityDocumentCamera.constants';
34
- import { isIDCardDocumentCode } from './IdentityDocumentCamera.utils';
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 (mrzText && PASSPORT_MRZ_PATTERN.test(mrzText)) {
187
- // 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
+ ) {
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
- // Any positive passport signal done (no back side).
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 === 'P';
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
- return 'COMPLETED';
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) => (prev === mrzStableAndValid ? prev : mrzStableAndValid));
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
- // Safety: passport somehow reached ID_BACK step
1191
- if (detectedDocumentType === 'PASSPORT') {
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: parsedMRZData?.fields,
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: ds.minBrightness === 255 && ds.frames === 0 ? 0 : ds.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
- // But we can't be 100% sure yet - passport MRZ might appear in next frames
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 JPEG2000 of any size
252
- // without blocking the JS thread, so it both fixes the iOS preview (RN's
253
- // <Image> can't render a raw image/jp2 data URI) and avoids the slow pure-JS
254
- // decoder entirely. Returns null on Android / when unavailable → fall through.
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
- // Too large to decode synchronously without freezing the UI keep the raw
265
- // JP2. The face image is still captured (the verification backend / face-match
266
- // accept JPEG2000); only the on-screen preview can't render it. This lets the
267
- // NFC read COMPLETE instead of hanging. (On Android the raw JP2 still renders
268
- // in <Image>; on iOS the native path above normally handles it.)
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> renders a JP2 data URI on Android (its
9
- * Fresco/Skia pipeline decodes it) but NOT on iOS (UIImage-backed <Image>
10
- * refuses the data URI), so the chip photo showed as a placeholder on iOS.
11
- *
12
- * iOS's ImageIO *can* decode JPEG2000, so the native `ImageDecoderModule`
13
- * round-trips the JP2 to JPEG. We only need this on iOS; on Android the raw
14
- * JP2 already renders, so there is no native counterpart and we no-op.
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 decoder.
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 (Android, or the module isn't linked) or fails — callers should
29
- * fall back to their existing behaviour (keep raw JP2).
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 (Platform.OS !== 'ios' || !ImageDecoder?.decodeJp2ToJpeg) {
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: documentName?.source === 'dg11' ? documentName.displayFirst : undefined,
69
- displaySurname: documentName?.source === 'dg11' ? documentName.displayLast : undefined,
70
- nameSource: documentName?.source === 'dg11' ? documentName.source : undefined,
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 (fileName: string, base64Data: string) => {
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: 'image/jpeg',
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
- await writeTempImage('FACE_IMAGE.jpg', faceImage);
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(identificationId, scannedDocument, authToken, documentName)
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
@@ -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.10';
3
+ export const SDK_VERSION = '1.495.12';