@trustchex/react-native-sdk 1.487.0 → 1.489.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 (83) hide show
  1. package/ios/ImageDecoder/ImageDecoderModule.m +12 -0
  2. package/ios/ImageDecoder/ImageDecoderModule.swift +43 -0
  3. package/lib/module/Shared/Components/EIDScanner.js +100 -37
  4. package/lib/module/Shared/Components/IdentityDocumentCamera.constants.js +9 -1
  5. package/lib/module/Shared/Components/IdentityDocumentCamera.flows.js +22 -2
  6. package/lib/module/Shared/Components/IdentityDocumentCamera.js +1 -1
  7. package/lib/module/Shared/Components/IdentityDocumentCamera.utils.js +7 -7
  8. package/lib/module/Shared/EIDReader/cardFileInputStream.js +25 -2
  9. package/lib/module/Shared/EIDReader/defaultFileSystem.js +56 -6
  10. package/lib/module/Shared/EIDReader/eidReader.js +138 -16
  11. package/lib/module/Shared/EIDReader/lds/icao/dg11File.js +23 -6
  12. package/lib/module/Shared/EIDReader/nfcManagerCardService.js +79 -9
  13. package/lib/module/Shared/EIDReader/protocol/paceDH.js +101 -0
  14. package/lib/module/Shared/EIDReader/protocol/paceErrors.js +34 -0
  15. package/lib/module/Shared/EIDReader/protocol/paceProtocol.js +53 -4
  16. package/lib/module/Shared/EIDReader/protocol/readBinaryAPDUSender.js +7 -3
  17. package/lib/module/Shared/Libs/country-display.utils.js +13 -1
  18. package/lib/module/Shared/Libs/diagnosticReport.js +24 -1
  19. package/lib/module/Shared/Libs/diagnostics.js +30 -8
  20. package/lib/module/Shared/Libs/jp2Decode.js +43 -0
  21. package/lib/module/Shared/Libs/mrz.utils.js +22 -5
  22. package/lib/module/Shared/Libs/mrzTransliteration.js +14 -1
  23. package/lib/module/Translation/Resources/en.js +12 -0
  24. package/lib/module/Translation/Resources/tr.js +12 -0
  25. package/lib/module/version.js +1 -1
  26. package/lib/typescript/src/Shared/Components/EIDScanner.d.ts.map +1 -1
  27. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.constants.d.ts.map +1 -1
  28. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.d.ts.map +1 -1
  29. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.flows.d.ts +11 -0
  30. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.flows.d.ts.map +1 -1
  31. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.utils.d.ts +1 -1
  32. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.utils.d.ts.map +1 -1
  33. package/lib/typescript/src/Shared/EIDReader/cardFileInputStream.d.ts +3 -0
  34. package/lib/typescript/src/Shared/EIDReader/cardFileInputStream.d.ts.map +1 -1
  35. package/lib/typescript/src/Shared/EIDReader/defaultFileSystem.d.ts +4 -0
  36. package/lib/typescript/src/Shared/EIDReader/defaultFileSystem.d.ts.map +1 -1
  37. package/lib/typescript/src/Shared/EIDReader/eidReader.d.ts.map +1 -1
  38. package/lib/typescript/src/Shared/EIDReader/lds/icao/dg11File.d.ts.map +1 -1
  39. package/lib/typescript/src/Shared/EIDReader/nfcManagerCardService.d.ts +9 -1
  40. package/lib/typescript/src/Shared/EIDReader/nfcManagerCardService.d.ts.map +1 -1
  41. package/lib/typescript/src/Shared/EIDReader/protocol/paceDH.d.ts +56 -0
  42. package/lib/typescript/src/Shared/EIDReader/protocol/paceDH.d.ts.map +1 -0
  43. package/lib/typescript/src/Shared/EIDReader/protocol/paceErrors.d.ts +26 -0
  44. package/lib/typescript/src/Shared/EIDReader/protocol/paceErrors.d.ts.map +1 -0
  45. package/lib/typescript/src/Shared/EIDReader/protocol/paceProtocol.d.ts.map +1 -1
  46. package/lib/typescript/src/Shared/EIDReader/protocol/readBinaryAPDUSender.d.ts.map +1 -1
  47. package/lib/typescript/src/Shared/Libs/country-display.utils.d.ts.map +1 -1
  48. package/lib/typescript/src/Shared/Libs/diagnosticReport.d.ts.map +1 -1
  49. package/lib/typescript/src/Shared/Libs/diagnostics.d.ts +9 -1
  50. package/lib/typescript/src/Shared/Libs/diagnostics.d.ts.map +1 -1
  51. package/lib/typescript/src/Shared/Libs/jp2Decode.d.ts +13 -0
  52. package/lib/typescript/src/Shared/Libs/jp2Decode.d.ts.map +1 -0
  53. package/lib/typescript/src/Shared/Libs/mrz.utils.d.ts.map +1 -1
  54. package/lib/typescript/src/Shared/Libs/mrzTransliteration.d.ts.map +1 -1
  55. package/lib/typescript/src/Translation/Resources/en.d.ts +12 -0
  56. package/lib/typescript/src/Translation/Resources/en.d.ts.map +1 -1
  57. package/lib/typescript/src/Translation/Resources/tr.d.ts +12 -0
  58. package/lib/typescript/src/Translation/Resources/tr.d.ts.map +1 -1
  59. package/lib/typescript/src/version.d.ts +1 -1
  60. package/package.json +1 -1
  61. package/src/Shared/Components/EIDScanner.tsx +153 -69
  62. package/src/Shared/Components/IdentityDocumentCamera.constants.ts +9 -1
  63. package/src/Shared/Components/IdentityDocumentCamera.flows.ts +22 -2
  64. package/src/Shared/Components/IdentityDocumentCamera.tsx +1 -2
  65. package/src/Shared/Components/IdentityDocumentCamera.utils.ts +6 -8
  66. package/src/Shared/EIDReader/cardFileInputStream.ts +30 -6
  67. package/src/Shared/EIDReader/defaultFileSystem.ts +75 -17
  68. package/src/Shared/EIDReader/eidReader.ts +172 -24
  69. package/src/Shared/EIDReader/lds/icao/dg11File.ts +28 -13
  70. package/src/Shared/EIDReader/nfcManagerCardService.ts +91 -13
  71. package/src/Shared/EIDReader/protocol/paceDH.ts +183 -0
  72. package/src/Shared/EIDReader/protocol/paceErrors.ts +41 -0
  73. package/src/Shared/EIDReader/protocol/paceProtocol.ts +72 -6
  74. package/src/Shared/EIDReader/protocol/readBinaryAPDUSender.ts +7 -4
  75. package/src/Shared/Libs/country-display.utils.ts +12 -1
  76. package/src/Shared/Libs/diagnosticReport.ts +32 -1
  77. package/src/Shared/Libs/diagnostics.ts +38 -14
  78. package/src/Shared/Libs/jp2Decode.ts +48 -0
  79. package/src/Shared/Libs/mrz.utils.ts +27 -6
  80. package/src/Shared/Libs/mrzTransliteration.ts +16 -3
  81. package/src/Translation/Resources/en.ts +19 -0
  82. package/src/Translation/Resources/tr.ts +19 -0
  83. package/src/version.ts +1 -1
@@ -0,0 +1,12 @@
1
+ #import <React/RCTBridgeModule.h>
2
+
3
+ @interface RCT_EXTERN_MODULE(ImageDecoderModule, NSObject)
4
+
5
+ // Decode a JPEG2000 (JP2) base64 string and re-encode it as JPEG base64.
6
+ // iOS's <Image> cannot render an image/jp2 data URI, but ImageIO/UIImage
7
+ // decodes JPEG2000 natively, so we round-trip it to JPEG here.
8
+ RCT_EXTERN_METHOD(decodeJp2ToJpeg:(NSString *)base64Jp2
9
+ resolver:(RCTPromiseResolveBlock)resolve
10
+ rejecter:(RCTPromiseRejectBlock)reject)
11
+
12
+ @end
@@ -0,0 +1,43 @@
1
+ import Foundation
2
+ import React
3
+ import UIKit
4
+ import ImageIO
5
+
6
+ /// Decodes images that React Native's <Image> cannot render directly on iOS.
7
+ ///
8
+ /// The driving case is the passport chip portrait (DG2), which is frequently
9
+ /// stored as JPEG2000 (image/jp2). Android's image pipeline decodes JP2 data
10
+ /// URIs natively, but iOS's <Image> does not — so the chip photo shows as a
11
+ /// placeholder on iOS. iOS's ImageIO *can* decode JPEG2000, so we round-trip
12
+ /// the JP2 to JPEG here and hand back base64 the JS <Image> can render.
13
+ @objc(ImageDecoderModule)
14
+ class ImageDecoderModule: NSObject {
15
+
16
+ @objc
17
+ static func requiresMainQueueSetup() -> Bool {
18
+ return false
19
+ }
20
+
21
+ @objc
22
+ func decodeJp2ToJpeg(_ base64Jp2: String,
23
+ resolver resolve: @escaping RCTPromiseResolveBlock,
24
+ rejecter reject: @escaping RCTPromiseRejectBlock) {
25
+ guard let jp2Data = Data(base64Encoded: base64Jp2) else {
26
+ reject("INVALID_BASE64", "Could not decode base64 JP2 input", nil)
27
+ return
28
+ }
29
+
30
+ // ImageIO decodes JPEG2000 on iOS even though <Image> won't render it.
31
+ guard let image = UIImage(data: jp2Data) else {
32
+ reject("DECODE_FAILED", "ImageIO could not decode the JP2 data", nil)
33
+ return
34
+ }
35
+
36
+ guard let jpegData = image.jpegData(compressionQuality: 0.95) else {
37
+ reject("ENCODE_FAILED", "Could not re-encode decoded image as JPEG", nil)
38
+ return
39
+ }
40
+
41
+ resolve(jpegData.base64EncodedString())
42
+ }
43
+ }
@@ -44,6 +44,13 @@ const EIDScanner = ({
44
44
  // Animation for pulse indicator
45
45
  const pulseAnim = useRef(new Animated.Value(1)).current;
46
46
 
47
+ // Strip MRZ filler (`<`) and trim; returns '' for empty/filler-only input so
48
+ // the UI can hide fields the chip didn't actually provide.
49
+ const cleanField = useCallback(value => {
50
+ if (!value) return '';
51
+ return value.replace(/</g, ' ').replace(/\s+/g, ' ').trim();
52
+ }, []);
53
+
47
54
  // Format date from YYMMDD to DD/MM/YYYY (matching Flutter SDK)
48
55
  const formatDate = useCallback(dateStr => {
49
56
  if (!dateStr) return '';
@@ -103,13 +110,20 @@ const EIDScanner = ({
103
110
  }, [i18n.language]);
104
111
  const [voiceGuidanceMessage, setVoiceGuidanceMessage] = useState();
105
112
  const [attemptNumber, setAttemptNumber] = useState(0);
106
- const getFieldsFromMRZ = useCallback(mrz => {
113
+ const getFieldsFromMRZ = useCallback((mrz, name) => {
114
+ // Prefer the resolved name (DG11 printed name / transliteration) over the
115
+ // raw DG1 split. Some chips — notably Turkish PACE passports — return a DG1
116
+ // name field with no `<<` separator, so getPrimaryIdentifier() holds the
117
+ // FULL name and getSecondaryIdentifier() is empty ("name as surname"). The
118
+ // resolved name corrects that; fall back to the raw split when absent.
119
+ const firstName = name?.displayFirst || mrz.getSecondaryIdentifier();
120
+ const lastName = name?.displayLast || mrz.getPrimaryIdentifier();
107
121
  return {
108
122
  documentCode: mrzUtils.normalizeDocumentCode(mrz.getDocumentCode()),
109
123
  personalNumber: mrz.getPersonalNumber() || mrz.getOptionalData1(),
110
124
  documentNumber: mrz.getDocumentNumber(),
111
- firstName: mrz.getSecondaryIdentifier(),
112
- lastName: mrz.getPrimaryIdentifier(),
125
+ firstName,
126
+ lastName,
113
127
  birthDate: mrz.getDateOfBirth(),
114
128
  expirationDate: mrz.getDateOfExpiry(),
115
129
  issuingState: mrz.getIssuingState(),
@@ -191,7 +205,7 @@ const EIDScanner = ({
191
205
  appContext.setLastDocumentRead?.(documentReadResult);
192
206
  appContext.onDocumentRead?.(documentReadResult);
193
207
  if (appContext.skipNfcResultScreen && onScanSuccess) {
194
- onScanSuccess(getFieldsFromMRZ(passportData.mrz), passportData.faceImage, passportData.mimeType);
208
+ onScanSuccess(getFieldsFromMRZ(passportData.mrz, builtName), passportData.faceImage, passportData.mimeType);
195
209
  } else {
196
210
  setDocumentFaceImage(passportData.faceImage);
197
211
  setDocumentMRZInfo(passportData.mrz);
@@ -228,7 +242,37 @@ const EIDScanner = ({
228
242
  // Track EID scan failure with detailed metadata
229
243
  await trackEIDScanFailed(docType, errorType, errorMessage, scanDuration, currentAttempt, DeviceInfo.getModel(), DeviceInfo.getSystemVersion()).catch(() => {});
230
244
  console.debug('NFC scan error:', error);
231
- // Ignore error - let user retry
245
+
246
+ // Surface an accurate message instead of failing silently. eidReader
247
+ // attaches `nfcCategory` + `nfcCode`.
248
+ // - User-correctable failures (wrong doc data, card moved/too slow, NFC
249
+ // off) get a specific, actionable message and NO code — there's a clear
250
+ // action to take, a support code would just be noise.
251
+ // - Everything else is an unexpected/system error: show a generic message
252
+ // plus the reportable code (e.g. NFC-READ-6A82) so the user can quote it.
253
+ // - user_cancelled stays silent (the user dismissed the native NFC sheet).
254
+ const tagged = error;
255
+ const category = tagged?.nfcCategory ?? errorType;
256
+
257
+ // category -> actionable message key for the user-mistake cases.
258
+ const ACTIONABLE = {
259
+ auth_failed: 'eidScannerScreen.nfcAuthFailed',
260
+ tag_lost: 'eidScannerScreen.nfcTagLost',
261
+ timeout: 'eidScannerScreen.nfcTimeout',
262
+ not_enabled: 'eidScannerScreen.nfcNotEnabled'
263
+ };
264
+ if (category !== 'user_cancelled') {
265
+ const actionableKey = ACTIONABLE[category];
266
+ if (actionableKey) {
267
+ // Clear user action available — meaningful message, no support code.
268
+ Alert.alert(t('eidScannerScreen.error'), t(actionableKey));
269
+ } else {
270
+ // Unexpected/system error — generic message + reportable support code.
271
+ const errorCode = tagged?.nfcCode ?? 'NFC-READ';
272
+ const message = `${t('eidScannerScreen.nfcGenericError')}\n\n${t('eidScannerScreen.errorCode')}: ${errorCode}`;
273
+ Alert.alert(t('eidScannerScreen.error'), message);
274
+ }
275
+ }
232
276
  } finally {
233
277
  setIsScanning(false);
234
278
  setProgress(0);
@@ -374,7 +418,11 @@ const EIDScanner = ({
374
418
  style: styles.idCardPhotoContainer,
375
419
  children: /*#__PURE__*/_jsx(View, {
376
420
  style: styles.idCardPhotoFrame,
377
- children: documentFaceImage && (documentFaceImageMimeType === 'image/jpeg' || documentFaceImageMimeType === 'image/png') ? /*#__PURE__*/_jsx(Image, {
421
+ children: documentFaceImage && (documentFaceImageMimeType === 'image/jpeg' || documentFaceImageMimeType === 'image/png' ||
422
+ // Android's <Image> decodes a raw JP2 data URI; on iOS
423
+ // the chip JP2 is converted to JPEG before this point
424
+ // (native ImageIO), so iOS never reaches here as jp2.
425
+ documentFaceImageMimeType === 'image/jp2') ? /*#__PURE__*/_jsx(Image, {
378
426
  source: {
379
427
  uri: `data:${documentFaceImageMimeType};base64,${documentFaceImage}`
380
428
  },
@@ -388,36 +436,51 @@ const EIDScanner = ({
388
436
  })
389
437
  })
390
438
  })
391
- }), /*#__PURE__*/_jsxs(View, {
439
+ }), /*#__PURE__*/_jsx(View, {
392
440
  style: styles.idCardFields,
393
- children: [/*#__PURE__*/_jsx(IDCardField, {
394
- label: t('eidScannerScreen.surname'),
395
- value: documentName?.displayLast ?? documentMRZInfo.getPrimaryIdentifier()
396
- }), /*#__PURE__*/_jsx(IDCardField, {
397
- label: t('eidScannerScreen.name'),
398
- value: documentName?.displayFirst ?? documentMRZInfo.getSecondaryIdentifier()
399
- }), /*#__PURE__*/_jsx(IDCardField, {
400
- label: t('eidScannerScreen.documentNumber'),
401
- value: documentMRZInfo.getDocumentNumber()
402
- }), /*#__PURE__*/_jsx(IDCardField, {
403
- label: t('eidScannerScreen.birthDate'),
404
- value: formatDate(documentMRZInfo.getDateOfBirth())
405
- }), /*#__PURE__*/_jsx(IDCardField, {
406
- label: t('eidScannerScreen.expirationDate'),
407
- value: formatDate(documentMRZInfo.getDateOfExpiry())
408
- }), /*#__PURE__*/_jsx(IDCardField, {
409
- label: t('eidScannerScreen.gender'),
410
- value: formatGender(documentMRZInfo.getGender().getStrCode())
411
- }), /*#__PURE__*/_jsx(IDCardField, {
412
- label: t('eidScannerScreen.nationality'),
413
- value: formatNationality(documentMRZInfo.getNationality())
414
- }), /*#__PURE__*/_jsx(IDCardField, {
415
- label: t('eidScannerScreen.issuingState'),
416
- value: formatNationality(documentMRZInfo.getIssuingState())
417
- }), !!(documentMRZInfo.getPersonalNumber() || documentMRZInfo.getOptionalData1()) && /*#__PURE__*/_jsx(IDCardField, {
418
- label: t('eidScannerScreen.personalNumber'),
419
- value: documentMRZInfo.getPersonalNumber() || documentMRZInfo.getOptionalData1() || ''
420
- })]
441
+ children: (() => {
442
+ const surname = documentName?.displayLast ?? cleanField(documentMRZInfo.getPrimaryIdentifier());
443
+ const givenNames = documentName?.displayFirst ?? cleanField(documentMRZInfo.getSecondaryIdentifier());
444
+ const docNumber = cleanField(documentMRZInfo.getDocumentNumber());
445
+ const birthDate = formatDate(documentMRZInfo.getDateOfBirth());
446
+ const expiry = formatDate(documentMRZInfo.getDateOfExpiry());
447
+ const gender = formatGender(documentMRZInfo.getGender().getStrCode());
448
+ const nationality = formatNationality(documentMRZInfo.getNationality());
449
+ const issuingState = formatNationality(documentMRZInfo.getIssuingState());
450
+ const personalNumber = cleanField(documentMRZInfo.getPersonalNumber() || documentMRZInfo.getOptionalData1());
451
+ const fields = [{
452
+ label: t('eidScannerScreen.surname'),
453
+ value: surname
454
+ }, {
455
+ label: t('eidScannerScreen.name'),
456
+ value: givenNames
457
+ }, {
458
+ label: t('eidScannerScreen.documentNumber'),
459
+ value: docNumber
460
+ }, {
461
+ label: t('eidScannerScreen.birthDate'),
462
+ value: birthDate
463
+ }, {
464
+ label: t('eidScannerScreen.expirationDate'),
465
+ value: expiry
466
+ }, {
467
+ label: t('eidScannerScreen.gender'),
468
+ value: gender
469
+ }, {
470
+ label: t('eidScannerScreen.nationality'),
471
+ value: nationality
472
+ }, {
473
+ label: t('eidScannerScreen.issuingState'),
474
+ value: issuingState
475
+ }, {
476
+ label: t('eidScannerScreen.personalNumber'),
477
+ value: personalNumber
478
+ }];
479
+ return fields.filter(f => f.value && f.value.trim().length > 0).map(f => /*#__PURE__*/_jsx(IDCardField, {
480
+ label: f.label,
481
+ value: f.value
482
+ }, f.label));
483
+ })()
421
484
  })]
422
485
  })]
423
486
  }), hasNfc && isEnabled && isScanned && /*#__PURE__*/_jsx(View, {
@@ -427,7 +490,7 @@ const EIDScanner = ({
427
490
  disabled: !documentFaceImage || !documentMRZInfo,
428
491
  onPress: () => {
429
492
  if (onScanSuccess && documentMRZInfo && documentFaceImage && documentFaceImageMimeType) {
430
- onScanSuccess(getFieldsFromMRZ(documentMRZInfo), documentFaceImage, documentFaceImageMimeType);
493
+ onScanSuccess(getFieldsFromMRZ(documentMRZInfo, documentName), documentFaceImage, documentFaceImageMimeType);
431
494
  }
432
495
  },
433
496
  children: t('eidScannerScreen.approveAndContinue')
@@ -477,7 +540,7 @@ const EIDScanner = ({
477
540
  disabled: !documentFaceImage || !documentMRZInfo,
478
541
  onPress: () => {
479
542
  if (onScanSuccess && documentMRZInfo && documentFaceImage && documentFaceImageMimeType) {
480
- onScanSuccess(getFieldsFromMRZ(documentMRZInfo), documentFaceImage, documentFaceImageMimeType);
543
+ onScanSuccess(getFieldsFromMRZ(documentMRZInfo, documentName), documentFaceImage, documentFaceImageMimeType);
481
544
  }
482
545
  },
483
546
  children: t('eidScannerScreen.approveAndContinue')
@@ -17,7 +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
- export const PASSPORT_MRZ_PATTERN = /P<[A-Z]{3}/;
20
+ // Matches the line-1 start of ANY ICAO TD3 passport MRZ, so the camera routes
21
+ // every passport variant into the passport flow (not the ID-card back-side
22
+ // flow). Per ICAO 9303 Part 4: `P` + a type char (filler `<` or a subtype
23
+ // letter — `P<`, `PP` German, `PD`/`PS`/`PO` diplomatic/service/official) +
24
+ // the issuing state (3 letters, OR the German 1-letter `D` padded with filler,
25
+ // e.g. `P<D<<` / `PPD<<`). Previously only `P<[A-Z]{3}` matched, so German `PP`
26
+ // and 1-letter-issuer passports fell through to the ID-card flow and were
27
+ // wrongly asked for a back side.
28
+ export const PASSPORT_MRZ_PATTERN = /P[A-Z<](?:[A-Z]{3}|[A-Z]<<)[A-Z<]*<</;
21
29
 
22
30
  // BDDK Madde 7 - Security Feature Validation
23
31
  // When NFC chip reading unavailable, must verify at least 4 visual security features
@@ -261,12 +261,32 @@ export function handleIDBackFlow(mrzText, mrzFields, mrzValid, mrzStableAndValid
261
261
  *
262
262
  * PASSPORT: → COMPLETED (no back side)
263
263
  * ID CARD: → SCAN_ID_BACK
264
+ *
265
+ * Routing is PASSPORT-BIASED on purpose: a passport has no back side, so if we
266
+ * route it to SCAN_ID_BACK it deadlocks forever waiting for a back that will
267
+ * never appear. We therefore only go to SCAN_ID_BACK when there is POSITIVE
268
+ * evidence the document is an ID card (a parsed MRZ document code that is
269
+ * present and not 'P'). When the type is ambiguous — e.g. the passport MRZ was
270
+ * never read as 'P' so the front step locked `detectedDocumentType` as
271
+ * 'ID_FRONT' (the torch/tilt during hologram capture makes the MRZ unreadable),
272
+ * or no MRZ code survived at all — we COMPLETE rather than wait for a
273
+ * non-existent back side. A two-sided ID card always yields an 'I'/'A'/'C'
274
+ * (non-'P') MRZ code, so genuine ID cards still route to SCAN_ID_BACK.
264
275
  */
265
276
  export function getNextStepAfterHologram(detectedDocumentType, currentFrameDocType, mrzDocCode) {
266
- // Check multiple sources to determine if it's a passport
277
+ // Any positive passport signal done (no back side).
267
278
  const isPassport = detectedDocumentType === 'PASSPORT' || currentFrameDocType === 'PASSPORT' || mrzDocCode === 'P';
268
279
  if (isPassport) {
269
280
  return 'COMPLETED';
270
281
  }
271
- return 'SCAN_ID_BACK';
282
+
283
+ // Only continue to the back side when we have POSITIVE proof it's an ID card:
284
+ // a real MRZ document code that is present and not a passport. Without that
285
+ // proof, default to COMPLETED so a misclassified passport can't hang waiting
286
+ // for a back side that doesn't exist.
287
+ const hasIdCardMrzCode = !!mrzDocCode && mrzDocCode !== 'P';
288
+ if (hasIdCardMrzCode) {
289
+ return 'SCAN_ID_BACK';
290
+ }
291
+ return 'COMPLETED';
272
292
  }
@@ -1799,7 +1799,7 @@ const IdentityDocumentCamera = ({
1799
1799
  status === 'SCANNING' && allElementsDetected && elementsOutsideScanArea.length === 0 && !isBrightnessLow && !isFrameBlurry && styles.topZoneTextScanning
1800
1800
  // 5. Default (white) - aligning (not all detected OR elements outside scan area)
1801
1801
  ],
1802
- children: getStatusMessage(nextStep, status, detectedDocumentType, isBrightnessLow, isFrameBlurry, allElementsDetected, elementsOutsideScanArea, t, mrzReliable)
1802
+ children: getStatusMessage(nextStep, status, detectedDocumentType, isBrightnessLow, isFrameBlurry, allElementsDetected, elementsOutsideScanArea, t)
1803
1803
  })]
1804
1804
  }), /*#__PURE__*/_jsx(View, {
1805
1805
  style: styles.leftZone
@@ -78,17 +78,17 @@ export function transformBoundsToScreen(bounds, scale, offsetX, offsetY) {
78
78
  * Unified status message logic used by both voice guidance and render text.
79
79
  * Returns the appropriate i18n key arguments for the current scan state.
80
80
  */
81
- export function getStatusMessage(nextStep, status, detectedDocumentType, isBrightnessLow, isFrameBlurry, allElementsDetected, elementsOutsideScanArea, t, mrzReliable = false) {
81
+ export function getStatusMessage(nextStep, status, detectedDocumentType, isBrightnessLow, isFrameBlurry, allElementsDetected, elementsOutsideScanArea, t) {
82
82
  if (nextStep === 'COMPLETED') {
83
83
  return t('identityDocumentCamera.scanCompleted');
84
84
  }
85
85
 
86
- // MRZ reliably detected (stable consensus) the green "GO" message. Ranks
87
- // above the in-progress states so the user gets a clear positive signal the
88
- // moment the reading has settled, but never on an early/transient valid read.
89
- if (mrzReliable) {
90
- return t('identityDocumentCamera.mrzDetected');
91
- }
86
+ // NOTE: the "MRZ detected — hold steady" message was intentionally removed
87
+ // (along with its former `mrzReliable` parameter). User-facing guidance
88
+ // should only describe the SCAN STEP (front side, back side, hologram, etc.),
89
+ // not low-level MRZ-detection state. We fall through to the step-specific
90
+ // messages below.
91
+
92
92
  if (status === 'INCORRECT') {
93
93
  if (nextStep === 'SCAN_ID_FRONT_OR_PASSPORT') {
94
94
  return t('identityDocumentCamera.alignIDFront');
@@ -3,6 +3,9 @@
3
3
  import { InputStream } from "./java/inputStream.js";
4
4
  import { Buffer } from 'buffer';
5
5
  export class CardFileInputStream extends InputStream {
6
+ /** Max re-issues of a single block read before failing — mitigates transient
7
+ * iOS NFC transceive drops during large-file reads (e.g. DG2). */
8
+ static MAX_BLOCK_READ_RETRIES = 5;
6
9
  constructor(maxBlockSize, fs) {
7
10
  super();
8
11
  this.fs = fs;
@@ -49,8 +52,28 @@ export class CardFileInputStream extends InputStream {
49
52
  newOffsetBufferInFile = this.offsetBufferInFile + this.bufferLength;
50
53
  let newOffsetInBuffer = 0;
51
54
  let newBufferLength = 0;
52
- while (newBufferLength === 0) {
53
- newBufferLength = await this.fillBufferFromFile(this.path, newOffsetBufferInFile, le);
55
+ // Retry a failed block read. The iOS NFC stack (react-native-nfc-manager)
56
+ // throws transient "READ BINARY" transceive errors during the many
57
+ // block reads of a large file like DG2 (face image); a single drop
58
+ // previously aborted the whole read with "Could not read file". Re-issue
59
+ // the SAME block (offset unchanged) a few times before giving up — this
60
+ // is idempotent (no secure-messaging state advances on a failed read).
61
+ let attempt = 0;
62
+ for (;;) {
63
+ try {
64
+ newBufferLength = 0;
65
+ while (newBufferLength === 0) {
66
+ newBufferLength = await this.fillBufferFromFile(this.path, newOffsetBufferInFile, le);
67
+ }
68
+ break;
69
+ } catch (blockError) {
70
+ if (++attempt >= CardFileInputStream.MAX_BLOCK_READ_RETRIES) {
71
+ throw blockError;
72
+ }
73
+ // Brief backoff: an immediate re-issue after a transient NFC drop
74
+ // usually fails again; a short pause lets the RF field re-stabilise.
75
+ await new Promise(r => setTimeout(r, 80 * attempt));
76
+ }
54
77
  }
55
78
  this.offsetBufferInFile = newOffsetBufferInFile;
56
79
  this.offsetInBuffer = newOffsetInBuffer;
@@ -6,9 +6,11 @@ import { DefaultFileInfo } from "./defaultFileInfo.js";
6
6
  import { ISO7816_SW } from "./smartcards/iso7816.js";
7
7
  import { SecureMessagingWrapper } from "./secureMessagingWrapper.js";
8
8
  import { TripleDesSecureMessagingWrapper } from "./tripleDesSecureMessagingWrapper.js";
9
+ import { AESSecureMessagingWrapper } from "./aesSecureMessagingWrapper.js";
9
10
  import { TLVInputStream } from "./tlv/tlvInputStream.js";
10
11
  import { CVCAFile } from "./lds/cvcaFile.js";
11
12
  import { Buffer } from 'buffer';
13
+ import { Platform } from 'react-native';
12
14
  export class DefaultFileSystem {
13
15
  static NO_SFI = -1;
14
16
  static READ_AHEAD_LENGTH = 8;
@@ -19,7 +21,27 @@ export class DefaultFileSystem {
19
21
  this.isSelected = false;
20
22
  this.isSFIEnabled = isSFIEnabled;
21
23
  this.fidToSFI = fidToSFI;
22
- this.maxReadBinaryLength = EID_CONSTANTS.EXTENDED_MAX_TRANSCEIVE_LENGTH;
24
+ // iOS CoreNFC (via react-native-nfc-manager's sendCommandAPDUIOS) does not
25
+ // accept the EXTENDED-length APDU byte form this code emits when ne > 256
26
+ // (a 7-byte "00 XX XX" Le). Such a READ BINARY is rejected outright, so on
27
+ // iOS EVERY DG2 read failed ("Exception during READ BINARY") even though
28
+ // PACE — which only used short Le=256 — succeeded. Start at a short-form
29
+ // length on iOS (ne <= 256) so every READ BINARY stays in the short APDU
30
+ // form. Android's transceive() handles extended APDUs, so keep extended
31
+ // there for speed. JMRTD/NFCPassportReader likewise read in small blocks.
32
+ this.maxReadBinaryLength = Platform.OS === 'ios' ? EID_CONSTANTS.DEFAULT_MAX_BLOCKSIZE // 223 → short-form Le, SM-safe
33
+ : EID_CONSTANTS.EXTENDED_MAX_TRANSCEIVE_LENGTH;
34
+ }
35
+
36
+ /** Deep-copy a secure-messaging wrapper PRESERVING its cipher type (AES vs
37
+ * 3DES), so the SSC can be rewound for a retry without changing the MAC/enc
38
+ * algorithm. Non-SM wrappers are returned as-is. */
39
+ cloneWrapper(wrapper) {
40
+ if (!(wrapper instanceof SecureMessagingWrapper)) {
41
+ return wrapper;
42
+ }
43
+ const args = [wrapper.getEncryptionKey(), wrapper.getMACKey(), wrapper.getMaxTransceiveLength(), wrapper.getShouldCheckMAC(), wrapper.getSendSequenceCounter()];
44
+ return wrapper.getType() === 'AES' ? new AESSecureMessagingWrapper(...args) : new TripleDesSecureMessagingWrapper(...args);
23
45
  }
24
46
  setWrapper(wrapper) {
25
47
  this.oldWrapper = this.wrapper;
@@ -89,13 +111,36 @@ export class DefaultFileSystem {
89
111
  const result = buffer.slice(offset, offset + responseLength);
90
112
  return result;
91
113
  } catch (error) {
92
- const sw = error.getSW();
93
- if ((sw & ISO7816_SW.WRONG_LENGTH) === ISO7816_SW.WRONG_LENGTH && this.maxReadBinaryLength > EID_CONSTANTS.DEFAULT_MAX_BLOCKSIZE) {
94
- this.wrapper = this.oldWrapper;
114
+ // Only APDU-level errors expose getSW(); a transient native NFC transceive
115
+ // error does NOT, so calling it unguarded threw a TypeError and masked the
116
+ // real failure. Guard it before testing for the WRONG_LENGTH downgrade.
117
+ const sw = typeof error.getSW === 'function' ? error.getSW() : undefined;
118
+ const isWrongLength = sw != null && (sw & ISO7816_SW.WRONG_LENGTH) === ISO7816_SW.WRONG_LENGTH;
119
+
120
+ // Downgrade the read size and signal a retry (empty result) when EITHER:
121
+ // - the chip returned 6Cxx WRONG_LENGTH (the standard JMRTD path), OR
122
+ // - the read failed transiently with NO status word while we were still
123
+ // at extended length. iOS CoreNFC (via react-native-nfc-manager) drops
124
+ // the connection on too-large extended-length READ BINARY on some chips
125
+ // (e.g. German passport DG2) instead of returning 6Cxx, so the chip
126
+ // never gets to ask for a smaller length. Dropping to the conservative
127
+ // 223-byte block makes the next read succeed. Reference readers
128
+ // (JMRTD, NFCPassportReader) read large files in small blocks (~0xA0).
129
+ // Restore the pre-send secure-messaging wrapper. A failed wrapped READ
130
+ // BINARY still advanced the send-sequence-counter, so a retry MUST reset
131
+ // it or every subsequent read fails the MAC. (This is why the read
132
+ // previously stalled partway through DG2: each retry reused a desynced
133
+ // SSC.) JMRTD does the same reset on its downgrade path.
134
+ this.wrapper = this.oldWrapper;
135
+ if ((isWrongLength || sw == null) && this.maxReadBinaryLength > EID_CONSTANTS.DEFAULT_MAX_BLOCKSIZE) {
95
136
  this.maxReadBinaryLength = EID_CONSTANTS.DEFAULT_MAX_BLOCKSIZE;
96
137
  return new Uint8Array(0);
97
138
  }
98
- throw new Error(`Read binary failed on file ${fileInfo == null ? this.selectedFID.toString(16) : fileInfo}`);
139
+
140
+ // Preserve the original error (a transient transceive failure at an
141
+ // already-small block size) so the retry layer above can recognise and
142
+ // re-issue it, instead of flattening every cause into one opaque string.
143
+ throw error instanceof Error ? error : new Error(`Read binary failed on file ${fileInfo == null ? this.selectedFID.toString(16) : fileInfo}`);
99
144
  }
100
145
  }
101
146
  async getFileInfo() {
@@ -139,7 +184,12 @@ export class DefaultFileSystem {
139
184
  await this.service.sendSelectFile(this.wrapper, fid);
140
185
  }
141
186
  sendReadBinaryNoSFI(offset, le, isTLVEncodedOffsetNeeded) {
142
- this.oldWrapper = this.wrapper instanceof SecureMessagingWrapper ? new TripleDesSecureMessagingWrapper(this.wrapper.getEncryptionKey(), this.wrapper.getMACKey(), this.wrapper.getMaxTransceiveLength(), this.wrapper.getShouldCheckMAC(), this.wrapper.getSendSequenceCounter()) : this.wrapper;
187
+ // Snapshot the wrapper BEFORE the send so a failed read can rewind the SSC.
188
+ // The copy MUST preserve the actual wrapper type — PACE sessions are AES
189
+ // (the German passport uses AES-CMAC). Previously this always built a 3DES
190
+ // wrapper, so an AES session's retry used a 3DES wrapper with AES keys and
191
+ // every subsequent read failed the MAC — the cause of the DG2 stall.
192
+ this.oldWrapper = this.cloneWrapper(this.wrapper);
143
193
  return this.service.sendReadBinary(this.wrapper, DefaultFileSystem.NO_SFI, offset, le, false, isTLVEncodedOffsetNeeded);
144
194
  }
145
195
  sendReadBinary(sfi, offset, le, isTLVEncodedOffsetNeeded) {