@trustchex/react-native-sdk 1.464.0 → 1.475.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 (64) hide show
  1. package/android/build.gradle +3 -3
  2. package/android/src/main/java/com/trustchex/reactnativesdk/camera/TrustchexCameraView.kt +190 -12
  3. package/ios/Camera/TrustchexCameraView.swift +244 -46
  4. package/ios/Permission/PermissionModule.m +22 -0
  5. package/ios/Permission/PermissionModule.swift +67 -0
  6. package/lib/module/Screens/Debug/MRZTestScreen.js +121 -147
  7. package/lib/module/Screens/Dynamic/LivenessDetectionScreen.js +24 -3
  8. package/lib/module/Screens/Dynamic/VerbalConsentScreen.js +1 -1
  9. package/lib/module/Screens/Dynamic/VideoCallScreen.js +7 -12
  10. package/lib/module/Screens/Static/ResultScreen.js +9 -1
  11. package/lib/module/Shared/Components/EIDScanner.js +166 -163
  12. package/lib/module/Shared/Components/FaceCamera.js +14 -8
  13. package/lib/module/Shared/Components/IdentityDocumentCamera.js +45 -10
  14. package/lib/module/Shared/Libs/MRZ_KNOWN_ISSUES.md +112 -0
  15. package/lib/module/Shared/Libs/PERMISSIONS_MANAGER.md +268 -0
  16. package/lib/module/Shared/Libs/index.js +20 -0
  17. package/lib/module/Shared/Libs/mrz.utils.js +570 -16
  18. package/lib/module/Shared/Libs/mrzFrameAggregator.js +301 -0
  19. package/lib/module/Shared/Libs/mrzOcrIntegration.js +109 -0
  20. package/lib/module/Shared/Libs/permissions.utils.js +199 -0
  21. package/lib/module/Translation/Resources/en.js +1 -0
  22. package/lib/module/Translation/Resources/tr.js +1 -0
  23. package/lib/module/version.js +1 -1
  24. package/lib/typescript/src/Screens/Debug/MRZTestScreen.d.ts.map +1 -1
  25. package/lib/typescript/src/Screens/Dynamic/LivenessDetectionScreen.d.ts.map +1 -1
  26. package/lib/typescript/src/Screens/Dynamic/VideoCallScreen.d.ts.map +1 -1
  27. package/lib/typescript/src/Screens/Static/ResultScreen.d.ts.map +1 -1
  28. package/lib/typescript/src/Shared/Components/EIDScanner.d.ts.map +1 -1
  29. package/lib/typescript/src/Shared/Components/FaceCamera.d.ts.map +1 -1
  30. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.d.ts.map +1 -1
  31. package/lib/typescript/src/Shared/Libs/index.d.ts +20 -0
  32. package/lib/typescript/src/Shared/Libs/index.d.ts.map +1 -0
  33. package/lib/typescript/src/Shared/Libs/mrz.utils.d.ts +8 -0
  34. package/lib/typescript/src/Shared/Libs/mrz.utils.d.ts.map +1 -1
  35. package/lib/typescript/src/Shared/Libs/mrzFrameAggregator.d.ts +76 -0
  36. package/lib/typescript/src/Shared/Libs/mrzFrameAggregator.d.ts.map +1 -0
  37. package/lib/typescript/src/Shared/Libs/mrzOcrIntegration.d.ts +73 -0
  38. package/lib/typescript/src/Shared/Libs/mrzOcrIntegration.d.ts.map +1 -0
  39. package/lib/typescript/src/Shared/Libs/permissions.utils.d.ts +58 -0
  40. package/lib/typescript/src/Shared/Libs/permissions.utils.d.ts.map +1 -0
  41. package/lib/typescript/src/Translation/Resources/en.d.ts +1 -0
  42. package/lib/typescript/src/Translation/Resources/en.d.ts.map +1 -1
  43. package/lib/typescript/src/Translation/Resources/tr.d.ts +1 -0
  44. package/lib/typescript/src/Translation/Resources/tr.d.ts.map +1 -1
  45. package/lib/typescript/src/version.d.ts +1 -1
  46. package/package.json +14 -10
  47. package/src/Screens/Debug/MRZTestScreen.tsx +137 -166
  48. package/src/Screens/Dynamic/LivenessDetectionScreen.tsx +45 -5
  49. package/src/Screens/Dynamic/VerbalConsentScreen.tsx +1 -1
  50. package/src/Screens/Dynamic/VideoCallScreen.tsx +8 -19
  51. package/src/Screens/Static/ResultScreen.tsx +18 -9
  52. package/src/Shared/Components/EIDScanner.tsx +210 -160
  53. package/src/Shared/Components/FaceCamera.tsx +19 -16
  54. package/src/Shared/Components/IdentityDocumentCamera.tsx +53 -13
  55. package/src/Shared/Libs/MRZ_KNOWN_ISSUES.md +112 -0
  56. package/src/Shared/Libs/PERMISSIONS_MANAGER.md +268 -0
  57. package/src/Shared/Libs/index.ts +63 -0
  58. package/src/Shared/Libs/mrz.utils.ts +639 -11
  59. package/src/Shared/Libs/mrzFrameAggregator.ts +370 -0
  60. package/src/Shared/Libs/mrzOcrIntegration.ts +175 -0
  61. package/src/Shared/Libs/permissions.utils.ts +251 -0
  62. package/src/Translation/Resources/en.ts +1 -0
  63. package/src/Translation/Resources/tr.ts +1 -0
  64. package/src/version.ts +1 -1
@@ -5,6 +5,8 @@ import { Alert, View, Text, Image, StyleSheet, Animated } from 'react-native';
5
5
  import { useSafeAreaInsets } from 'react-native-safe-area-context';
6
6
  import NFCManager from 'react-native-nfc-manager';
7
7
  import DeviceInfo from 'react-native-device-info';
8
+ import PermissionManager from "../Libs/permissions.utils.js";
9
+ import { debugError } from "../Libs/debug.utils.js";
8
10
  import { eidReader } from "../EIDReader/eidReader.js";
9
11
  import NativeProgressBar from "./NativeProgressBar.js";
10
12
  import { useTranslation } from 'react-i18next';
@@ -107,6 +109,15 @@ const EIDScanner = ({
107
109
  setProgressStage('');
108
110
  resetLastMessage();
109
111
 
112
+ // Check NFC permissions before attempting to scan
113
+ const hasNFCPermission = await PermissionManager.requestPermission('nfc');
114
+ if (!hasNFCPermission) {
115
+ debugError('EIDScanner', 'NFC permission denied');
116
+ setIsScanning(false);
117
+ Alert.alert(t('eidScannerScreen.error'), t('eidScannerScreen.nfcPermissionDenied', 'NFC permission is required to scan your document'));
118
+ return;
119
+ }
120
+
110
121
  // Track EID scan start using analytics helper
111
122
  const docType = documentType || 'UNKNOWN';
112
123
 
@@ -320,120 +331,82 @@ const EIDScanner = ({
320
331
  children: t('eidScannerScreen.startScanning')
321
332
  })]
322
333
  }) : /*#__PURE__*/_jsxs(_Fragment, {
323
- children: [isScanned && /*#__PURE__*/_jsxs(View, {
324
- style: styles.resultCard,
334
+ children: [isScanned && documentMRZInfo && /*#__PURE__*/_jsxs(View, {
335
+ style: styles.idCardWrapper,
325
336
  children: [/*#__PURE__*/_jsxs(View, {
326
- style: styles.resultHeader,
327
- children: [documentFaceImage && (documentFaceImageMimeType === 'image/jpeg' || documentFaceImageMimeType === 'image/png') ? /*#__PURE__*/_jsx(Image, {
328
- source: {
329
- uri: `data:${documentFaceImageMimeType};base64,${documentFaceImage}`
330
- },
331
- style: styles.faceImage,
332
- resizeMode: "cover"
333
- }) : /*#__PURE__*/_jsx(View, {
334
- style: styles.faceImagePlaceholder,
337
+ style: styles.idCard,
338
+ children: [/*#__PURE__*/_jsx(View, {
339
+ style: [styles.idCardStrip, {
340
+ backgroundColor: appContext.branding.primaryColor
341
+ }],
335
342
  children: /*#__PURE__*/_jsx(Text, {
336
- style: styles.faceImagePlaceholderText,
337
- children: "?"
338
- })
339
- }), documentMRZInfo && /*#__PURE__*/_jsxs(View, {
340
- style: styles.resultHeaderInfo,
341
- children: [/*#__PURE__*/_jsxs(Text, {
342
- style: styles.fullName,
343
- numberOfLines: 2,
344
- children: [documentMRZInfo.getSecondaryIdentifier(), ' ', documentMRZInfo.getPrimaryIdentifier()]
345
- }), /*#__PURE__*/_jsxs(Text, {
346
- style: styles.subInfo,
347
- children: [formatNationality(documentMRZInfo.getNationality()), " \xB7", ' ', formatDocumentType(documentMRZInfo.getDocumentCode())]
348
- }), /*#__PURE__*/_jsxs(Text, {
349
- style: styles.subInfo,
350
- children: [formatGender(documentMRZInfo.getGender().getStrCode()), " \xB7", ' ', formatDate(documentMRZInfo.getDateOfBirth())]
351
- })]
352
- })]
353
- }), documentMRZInfo && /*#__PURE__*/_jsxs(View, {
354
- style: styles.mrzInfo,
355
- children: [/*#__PURE__*/_jsxs(View, {
356
- style: styles.mrzInfoItem,
357
- children: [/*#__PURE__*/_jsx(Text, {
358
- style: styles.mrzInfoLabel,
359
- children: t('eidScannerScreen.name')
360
- }), /*#__PURE__*/_jsx(Text, {
361
- style: styles.mrzInfoText,
362
- children: documentMRZInfo.getSecondaryIdentifier()
363
- })]
364
- }), /*#__PURE__*/_jsxs(View, {
365
- style: styles.mrzInfoItem,
366
- children: [/*#__PURE__*/_jsx(Text, {
367
- style: styles.mrzInfoLabel,
368
- children: t('eidScannerScreen.surname')
369
- }), /*#__PURE__*/_jsx(Text, {
370
- style: styles.mrzInfoText,
371
- children: documentMRZInfo.getPrimaryIdentifier()
372
- })]
373
- }), /*#__PURE__*/_jsxs(View, {
374
- style: styles.mrzInfoItem,
375
- children: [/*#__PURE__*/_jsx(Text, {
376
- style: styles.mrzInfoLabel,
377
- children: t('eidScannerScreen.documentCode')
378
- }), /*#__PURE__*/_jsx(Text, {
379
- style: styles.mrzInfoText,
343
+ style: styles.idCardStripDocType,
380
344
  children: formatDocumentType(documentMRZInfo.getDocumentCode())
381
- })]
382
- }), /*#__PURE__*/_jsxs(View, {
383
- style: styles.mrzInfoItem,
384
- children: [/*#__PURE__*/_jsx(Text, {
385
- style: styles.mrzInfoLabel,
386
- children: t('eidScannerScreen.documentNumber')
387
- }), /*#__PURE__*/_jsx(Text, {
388
- style: styles.mrzInfoText,
389
- children: documentMRZInfo.getDocumentNumber()
390
- })]
391
- }), /*#__PURE__*/_jsxs(View, {
392
- style: styles.mrzInfoItem,
393
- children: [/*#__PURE__*/_jsx(Text, {
394
- style: styles.mrzInfoLabel,
395
- children: t('eidScannerScreen.personalNumber')
396
- }), /*#__PURE__*/_jsx(Text, {
397
- style: styles.mrzInfoText,
398
- children: documentMRZInfo.getOptionalData1()
399
- })]
400
- }), /*#__PURE__*/_jsxs(View, {
401
- style: styles.mrzInfoItem,
402
- children: [/*#__PURE__*/_jsx(Text, {
403
- style: styles.mrzInfoLabel,
404
- children: t('eidScannerScreen.nationality')
405
- }), /*#__PURE__*/_jsx(Text, {
406
- style: styles.mrzInfoText,
407
- children: formatNationality(documentMRZInfo.getNationality())
408
- })]
409
- }), /*#__PURE__*/_jsxs(View, {
410
- style: styles.mrzInfoItem,
411
- children: [/*#__PURE__*/_jsx(Text, {
412
- style: styles.mrzInfoLabel,
413
- children: t('eidScannerScreen.gender')
414
- }), /*#__PURE__*/_jsx(Text, {
415
- style: styles.mrzInfoText,
416
- children: formatGender(documentMRZInfo.getGender().getStrCode())
417
- })]
418
- }), /*#__PURE__*/_jsxs(View, {
419
- style: styles.mrzInfoItem,
420
- children: [/*#__PURE__*/_jsx(Text, {
421
- style: styles.mrzInfoLabel,
422
- children: t('eidScannerScreen.birthDate')
423
- }), /*#__PURE__*/_jsx(Text, {
424
- style: styles.mrzInfoText,
425
- children: formatDate(documentMRZInfo.getDateOfBirth())
426
- })]
345
+ })
427
346
  }), /*#__PURE__*/_jsxs(View, {
428
- style: styles.mrzInfoItem,
429
- children: [/*#__PURE__*/_jsx(Text, {
430
- style: styles.mrzInfoLabel,
431
- children: t('eidScannerScreen.expirationDate')
432
- }), /*#__PURE__*/_jsx(Text, {
433
- style: styles.mrzInfoText,
434
- children: formatDate(documentMRZInfo.getDateOfExpiry())
347
+ style: styles.idCardBody,
348
+ children: [/*#__PURE__*/_jsx(View, {
349
+ style: styles.idCardPhotoContainer,
350
+ children: /*#__PURE__*/_jsx(View, {
351
+ style: styles.idCardPhotoFrame,
352
+ children: documentFaceImage && (documentFaceImageMimeType === 'image/jpeg' || documentFaceImageMimeType === 'image/png') ? /*#__PURE__*/_jsx(Image, {
353
+ source: {
354
+ uri: `data:${documentFaceImageMimeType};base64,${documentFaceImage}`
355
+ },
356
+ style: styles.idCardPhoto,
357
+ resizeMode: "cover"
358
+ }) : /*#__PURE__*/_jsx(View, {
359
+ style: styles.idCardPhotoPlaceholder,
360
+ children: /*#__PURE__*/_jsx(Text, {
361
+ style: styles.idCardPhotoPlaceholderText,
362
+ children: "?"
363
+ })
364
+ })
365
+ })
366
+ }), /*#__PURE__*/_jsxs(View, {
367
+ style: styles.idCardFields,
368
+ children: [/*#__PURE__*/_jsx(IDCardField, {
369
+ label: t('eidScannerScreen.surname'),
370
+ value: documentMRZInfo.getPrimaryIdentifier()
371
+ }), /*#__PURE__*/_jsx(IDCardField, {
372
+ label: t('eidScannerScreen.name'),
373
+ value: documentMRZInfo.getSecondaryIdentifier()
374
+ }), /*#__PURE__*/_jsx(IDCardField, {
375
+ label: t('eidScannerScreen.documentNumber'),
376
+ value: documentMRZInfo.getDocumentNumber()
377
+ }), /*#__PURE__*/_jsx(IDCardField, {
378
+ label: t('eidScannerScreen.birthDate'),
379
+ value: formatDate(documentMRZInfo.getDateOfBirth())
380
+ }), /*#__PURE__*/_jsx(IDCardField, {
381
+ label: t('eidScannerScreen.expirationDate'),
382
+ value: formatDate(documentMRZInfo.getDateOfExpiry())
383
+ }), /*#__PURE__*/_jsx(IDCardField, {
384
+ label: t('eidScannerScreen.gender'),
385
+ value: formatGender(documentMRZInfo.getGender().getStrCode())
386
+ }), /*#__PURE__*/_jsx(IDCardField, {
387
+ label: t('eidScannerScreen.nationality'),
388
+ value: formatNationality(documentMRZInfo.getNationality())
389
+ }), /*#__PURE__*/_jsx(IDCardField, {
390
+ label: t('eidScannerScreen.issuingState'),
391
+ value: formatNationality(documentMRZInfo.getIssuingState())
392
+ }), !!(documentMRZInfo.getPersonalNumber() || documentMRZInfo.getOptionalData1()) && /*#__PURE__*/_jsx(IDCardField, {
393
+ label: t('eidScannerScreen.personalNumber'),
394
+ value: documentMRZInfo.getPersonalNumber() || documentMRZInfo.getOptionalData1() || ''
395
+ })]
435
396
  })]
436
397
  })]
398
+ }), hasNfc && isEnabled && isScanned && /*#__PURE__*/_jsx(View, {
399
+ style: styles.buttonsContainer,
400
+ children: /*#__PURE__*/_jsx(StyledButton, {
401
+ mode: "contained",
402
+ disabled: !documentFaceImage || !documentMRZInfo,
403
+ onPress: () => {
404
+ if (onScanSuccess && documentMRZInfo && documentFaceImage && documentFaceImageMimeType) {
405
+ onScanSuccess(getFieldsFromMRZ(documentMRZInfo), documentFaceImage, documentFaceImageMimeType);
406
+ }
407
+ },
408
+ children: t('eidScannerScreen.approveAndContinue')
409
+ })
437
410
  })]
438
411
  }), !hasNfc && /*#__PURE__*/_jsx(Text, {
439
412
  style: styles.mainText,
@@ -472,7 +445,7 @@ const EIDScanner = ({
472
445
  onPress: readNFC,
473
446
  children: t('eidScannerScreen.startScanning')
474
447
  })
475
- }), hasNfc && isEnabled && isScanned && /*#__PURE__*/_jsx(View, {
448
+ }), hasNfc && isEnabled && isScanned && !documentMRZInfo && /*#__PURE__*/_jsx(View, {
476
449
  style: styles.buttonsContainer,
477
450
  children: /*#__PURE__*/_jsx(StyledButton, {
478
451
  mode: "contained",
@@ -557,86 +530,102 @@ const styles = StyleSheet.create({
557
530
  color: 'black',
558
531
  textAlign: 'center'
559
532
  },
560
- resultCard: {
533
+ // ── ID Card result layout ──────────────────────────────────────────────
534
+ idCardWrapper: {
535
+ gap: 10
536
+ },
537
+ idCard: {
561
538
  backgroundColor: '#fff',
562
- borderRadius: 16,
563
- padding: 16,
564
- gap: 16,
539
+ borderRadius: 14,
540
+ overflow: 'hidden',
565
541
  shadowColor: '#000',
566
542
  shadowOffset: {
567
543
  width: 0,
568
- height: 2
544
+ height: 3
569
545
  },
570
- shadowOpacity: 0.08,
571
- shadowRadius: 8,
572
- elevation: 3,
546
+ shadowOpacity: 0.12,
547
+ shadowRadius: 10,
548
+ elevation: 4,
573
549
  borderWidth: 1,
574
- borderColor: '#f0f0f0'
550
+ borderColor: '#e8e8e8'
575
551
  },
576
- resultHeader: {
552
+ idCardStrip: {
577
553
  flexDirection: 'row',
578
- gap: 14,
579
- alignItems: 'flex-start'
580
- },
581
- resultHeaderInfo: {
582
- flex: 1,
583
- gap: 4,
584
- justifyContent: 'center'
554
+ justifyContent: 'space-between',
555
+ alignItems: 'center',
556
+ paddingHorizontal: 12,
557
+ paddingVertical: 6
585
558
  },
586
- fullName: {
587
- fontSize: 17,
559
+ idCardStripDocType: {
560
+ color: '#fff',
561
+ fontSize: 12,
588
562
  fontWeight: '700',
589
- color: '#111',
590
- letterSpacing: 0.2
563
+ letterSpacing: 0.5,
564
+ textTransform: 'uppercase'
591
565
  },
592
- subInfo: {
593
- fontSize: 13,
594
- color: '#666',
595
- fontWeight: '500'
566
+ idCardStripIssuer: {
567
+ color: 'rgba(255,255,255,0.85)',
568
+ fontSize: 12,
569
+ fontWeight: '600',
570
+ letterSpacing: 0.3
596
571
  },
597
- faceImage: {
598
- width: 90,
599
- height: 115,
572
+ idCardBody: {
573
+ flexDirection: 'row',
574
+ alignItems: 'stretch'
575
+ },
576
+ idCardPhotoContainer: {
577
+ width: 110,
578
+ padding: 8,
579
+ alignSelf: 'stretch'
580
+ },
581
+ idCardPhotoFrame: {
582
+ flex: 1,
600
583
  borderRadius: 10,
601
- overflow: 'hidden'
584
+ overflow: 'hidden',
585
+ borderWidth: 1,
586
+ borderColor: 'rgba(0,0,0,0.12)'
587
+ },
588
+ idCardPhoto: {
589
+ flex: 1
602
590
  },
603
- faceImagePlaceholder: {
604
- width: 90,
605
- height: 115,
591
+ idCardPhotoPlaceholder: {
592
+ flex: 1,
606
593
  borderRadius: 10,
607
594
  backgroundColor: '#f5f5f5',
608
595
  alignItems: 'center',
609
596
  justifyContent: 'center'
610
597
  },
611
- faceImagePlaceholderText: {
612
- fontSize: 32,
598
+ idCardPhotoPlaceholderText: {
599
+ fontSize: 30,
613
600
  color: '#ccc'
614
601
  },
615
- mrzInfo: {
616
- flexDirection: 'column',
617
- width: '100%',
618
- gap: 2
619
- },
620
- mrzInfoItem: {
602
+ idCardFields: {
603
+ flex: 1,
621
604
  flexDirection: 'row',
622
- justifyContent: 'space-between',
623
- alignItems: 'center',
624
- paddingVertical: 8,
625
- borderBottomColor: '#f0f0f0',
626
- borderBottomWidth: 1
605
+ flexWrap: 'wrap',
606
+ gap: 6,
607
+ alignContent: 'flex-start',
608
+ padding: 10
627
609
  },
628
- mrzInfoLabel: {
629
- color: '#888',
630
- fontSize: 13,
631
- fontWeight: '500'
610
+ idCardFieldItem: {
611
+ flexGrow: 1,
612
+ flexShrink: 1,
613
+ flexBasis: '45%',
614
+ minWidth: 0
632
615
  },
633
- mrzInfoText: {
634
- color: '#111',
635
- fontSize: 13,
616
+ idCardFieldLabel: {
617
+ fontSize: 8,
636
618
  fontWeight: '600',
637
- textAlign: 'right',
619
+ color: '#999',
620
+ letterSpacing: 0.3,
621
+ marginBottom: 1
622
+ },
623
+ idCardFieldValue: {
624
+ fontSize: 11,
625
+ fontWeight: '600',
626
+ color: '#111',
638
627
  flexShrink: 1,
639
- marginLeft: 8
628
+ flexWrap: 'wrap'
640
629
  },
641
630
  guide: {
642
631
  flex: 1,
@@ -667,4 +656,18 @@ const styles = StyleSheet.create({
667
656
  fontSize: 14
668
657
  }
669
658
  });
659
+ const IDCardField = ({
660
+ label,
661
+ value
662
+ }) => /*#__PURE__*/_jsxs(View, {
663
+ style: styles.idCardFieldItem,
664
+ children: [/*#__PURE__*/_jsx(Text, {
665
+ style: styles.idCardFieldLabel,
666
+ numberOfLines: 1,
667
+ children: label
668
+ }), /*#__PURE__*/_jsx(Text, {
669
+ style: styles.idCardFieldValue,
670
+ children: value || '—'
671
+ })]
672
+ });
670
673
  export default EIDScanner;
@@ -3,8 +3,9 @@
3
3
  import { useIsFocused } from '@react-navigation/native';
4
4
  import { useKeepAwake } from "../Libs/native-keep-awake.utils.js";
5
5
  import { debugLog, debugWarn, debugError, logError } from "../Libs/debug.utils.js";
6
+ import PermissionManager from "../Libs/permissions.utils.js";
6
7
  import React, { useEffect, useState, useRef, useCallback } from 'react';
7
- import { StyleSheet, Text, View, Platform, Linking, ActivityIndicator, NativeModules, PermissionsAndroid } from 'react-native';
8
+ import { StyleSheet, Text, View, Linking, ActivityIndicator, NativeModules } from 'react-native';
8
9
  import { TrustchexCamera } from "./TrustchexCamera.js";
9
10
  import { useTranslation } from 'react-i18next';
10
11
  import StyledButton from "./StyledButton.js";
@@ -41,13 +42,18 @@ const FaceCamera = ({
41
42
  } = useTranslation();
42
43
  useEffect(() => {
43
44
  const requestPermissions = async () => {
44
- if (Platform.OS === 'android') {
45
- const cameraGranted = await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.CAMERA);
46
- const audioGranted = await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.RECORD_AUDIO);
47
- setHasPermission(cameraGranted === PermissionsAndroid.RESULTS.GRANTED && audioGranted === PermissionsAndroid.RESULTS.GRANTED);
48
- } else {
49
- // iOS permissions handled by native camera
50
- setHasPermission(true);
45
+ try {
46
+ const permissions = await PermissionManager.requestPermissions(['camera', 'microphone']);
47
+ const hasCamera = permissions.get('camera') ?? false;
48
+ const hasMicrophone = permissions.get('microphone') ?? false;
49
+ debugLog('FaceCamera', 'Permissions requested:', {
50
+ camera: hasCamera,
51
+ microphone: hasMicrophone
52
+ });
53
+ setHasPermission(hasCamera && hasMicrophone);
54
+ } catch (error) {
55
+ debugError('FaceCamera', 'Permission request failed:', error);
56
+ setHasPermission(false);
51
57
  }
52
58
  setPermissionsRequested(true);
53
59
  };
@@ -2,11 +2,13 @@
2
2
 
3
3
  /* eslint-disable react-native/no-inline-styles */
4
4
  import React, { useEffect, useState, useRef, useCallback } from 'react';
5
- import { View, StyleSheet, Text as TextView, Platform, StatusBar, Vibration, Linking, ActivityIndicator, PermissionsAndroid, Animated } from 'react-native';
5
+ import { View, StyleSheet, Text as TextView, Platform, StatusBar, Vibration, Linking, ActivityIndicator, Animated } from 'react-native';
6
6
  import { useSafeAreaInsets } from 'react-native-safe-area-context';
7
7
  import { TrustchexCamera } from "./TrustchexCamera.js";
8
8
  import { NativeModules } from 'react-native';
9
+ import PermissionManager from "../Libs/permissions.utils.js";
9
10
  import mrzUtils from "../Libs/mrz.utils.js";
11
+ import { MRZFrameAggregator } from "../Libs/mrzFrameAggregator.js";
10
12
  import { useKeepAwake } from "../Libs/native-keep-awake.utils.js";
11
13
  import { useIsFocused } from '@react-navigation/native';
12
14
  import { useTranslation } from 'react-i18next';
@@ -69,6 +71,12 @@ const IdentityDocumentCamera = ({
69
71
  const lastValidMRZText = useRef(null);
70
72
  const lastValidMRZFields = useRef(null);
71
73
  const validMRZConsecutiveCount = useRef(0);
74
+ // Multi-frame per-character voting: fuses OCR across frames so a single-frame
75
+ // misread (B↔8, S↔5, <↔K, dropped/inserted digit) is outvoted instead of
76
+ // resetting progress. Drives the same `parsedMRZData`/stability the flow uses.
77
+ const mrzAggregator = useRef(new MRZFrameAggregator({
78
+ stabilityTarget: 2
79
+ }));
72
80
 
73
81
  // Document type stability tracking - require consistent detections from good quality frames
74
82
  const lastDetectedDocType = useRef('UNKNOWN');
@@ -120,11 +128,11 @@ const IdentityDocumentCamera = ({
120
128
  } = useTranslation();
121
129
  useEffect(() => {
122
130
  const requestPermissions = async () => {
123
- if (Platform.OS === 'android') {
124
- const granted = await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.CAMERA);
125
- setHasPermission(granted === PermissionsAndroid.RESULTS.GRANTED);
126
- } else {
127
- setHasPermission(true);
131
+ try {
132
+ const hasCameraPermission = await PermissionManager.requestPermission('camera');
133
+ setHasPermission(hasCameraPermission);
134
+ } catch (error) {
135
+ setHasPermission(false);
128
136
  }
129
137
  setPermissionsRequested(true);
130
138
  };
@@ -148,6 +156,7 @@ const IdentityDocumentCamera = ({
148
156
  lastValidMRZText.current = null;
149
157
  lastValidMRZFields.current = null;
150
158
  validMRZConsecutiveCount.current = 0;
159
+ mrzAggregator.current.reset();
151
160
  cachedBarcode.current = null;
152
161
  isCompletionCallbackInvoked.current = false;
153
162
  isStepTransitionInProgress.current = false;
@@ -375,6 +384,8 @@ const IdentityDocumentCamera = ({
375
384
  if (nextStepType === 'SCAN_ID_BACK') {
376
385
  lastValidMRZText.current = null;
377
386
  lastValidMRZFields.current = null;
387
+ // New MRZ expected on the back — start voting fresh.
388
+ mrzAggregator.current.reset();
378
389
  }
379
390
  validMRZConsecutiveCount.current = 0;
380
391
  cachedBarcode.current = null; // Clear cached barcode on step change
@@ -537,13 +548,33 @@ const IdentityDocumentCamera = ({
537
548
  // Prefer MRZ-only text if available (from detected MRZ blocks),
538
549
  // otherwise fall back to all text (for backward compatibility)
539
550
  const textForValidation = scannedText?.mrzOnlyText || text;
540
- const mrzValidationResult = mrzUtils.validateMRZWithCorrections(textForValidation);
551
+
552
+ // Multi-frame per-character voting: feed this frame's OCR into the
553
+ // aggregator (weighted by the MRZ band's pixel height as a sharpness/
554
+ // closeness proxy — a taller band means more pixels per character, which
555
+ // ML Kit reads more reliably) and prefer the fused consensus. This outvotes
556
+ // single-frame OCR errors (B↔8, S↔5, <↔K, dropped/inserted digit) instead
557
+ // of letting one noisy frame reset progress.
558
+ const mrzBandHeight = scannedText?.blocks?.length ? Math.max(0, ...scannedText.blocks.map(b => b.blockFrame?.height ?? 0)) : 0;
559
+ const frameWeight = mrzBandHeight > 0 ? mrzBandHeight : 1;
560
+ const consensus = mrzAggregator.current.addFrame({
561
+ text: textForValidation,
562
+ weight: frameWeight
563
+ });
564
+
565
+ // Use the fused consensus when it is valid; otherwise fall back to a
566
+ // single-frame validation so document-type detection still works on the
567
+ // early frames. Only run the (expensive) single-frame pass when the
568
+ // consensus has NOT yet validated — once it has, the per-frame validation
569
+ // is redundant work that would needlessly load the JS thread.
570
+ const mrzValidationResult = consensus.validation?.valid && consensus.validation.fields ? consensus.validation : mrzUtils.validateMRZWithCorrections(textForValidation);
541
571
  const parsedMRZData = {
542
572
  valid: mrzValidationResult.valid,
543
573
  fields: mrzValidationResult.fields || null
544
574
  };
545
- // Extract raw MRZ lines from text if validation succeeded
546
- const mrzText = parsedMRZData.valid ? mrzUtils.fixMRZ(text) : null;
575
+ // Raw MRZ lines: prefer the fused/corrected consensus when valid, else the
576
+ // lightweight per-frame clean. correctedMrz is the noise-free recovered MRZ.
577
+ const mrzText = parsedMRZData.valid ? mrzValidationResult.correctedMrz ?? mrzUtils.fixMRZ(text) : null;
547
578
 
548
579
  // CRITICAL: Only detect document type during initial scan step
549
580
  // For SCAN_HOLOGRAM and beyond, use the locked detectedDocumentType to avoid
@@ -644,7 +675,11 @@ const IdentityDocumentCamera = ({
644
675
  // else: Invalid/no MRZ - skip frame without resetting (allows temporary OCR noise)
645
676
 
646
677
  // Check if we have enough consistent valid reads
647
- const mrzStableAndValid = validMRZConsecutiveCount.current >= REQUIRED_CONSISTENT_MRZ_READS && parsedMRZData?.valid === true && areMRZFieldsEqual(lastValidMRZFields.current, parsedMRZData?.fields);
678
+ const mrzStableAndValid =
679
+ // Either: the multi-frame voted consensus has converged (valid + stable),
680
+ consensus.stable && parsedMRZData?.valid === true ||
681
+ // or the legacy N-identical-reads path is satisfied.
682
+ validMRZConsecutiveCount.current >= REQUIRED_CONSISTENT_MRZ_READS && parsedMRZData?.valid === true && areMRZFieldsEqual(lastValidMRZFields.current, parsedMRZData?.fields);
648
683
 
649
684
  // ============================================================================
650
685
  // SCAN_ID_BACK STEP - Validate MRZ + barcode on back of ID card
@@ -0,0 +1,112 @@
1
+ # MRZ Reading & Correction — Known Issues & Limitations
2
+
3
+ This documents the **inherent limitations** and **deliberate trade-offs** of the
4
+ MRZ camera scanner (`mrz.utils.ts`, `mrzFrameAggregator.ts`, and the native
5
+ `TrustchexCameraView` scan-frame OCR). These are not bugs — they are constraints
6
+ of the ICAO 9303 standard and OCR, and the design decisions made around them.
7
+
8
+ For the camera/OCR pipeline overview, see the doc comments in
9
+ `mrzFrameAggregator.ts` and `mrz.utils.ts`.
10
+
11
+ ---
12
+
13
+ ## 1. Check-digit MOD-10 collisions: `1↔L` and `6↔G` are indistinguishable
14
+
15
+ **Issue.** ICAO's 7-3-1 check digit weights letters by value (`A`=10 … `Z`=35),
16
+ then takes the result mod 10. Some OCR look-alikes are **mod-10 equal** to the
17
+ digit they resemble:
18
+
19
+ | Look-alike | Letter value | Digit | mod 10 |
20
+ | --- | --- | --- | --- |
21
+ | `L` ↔ `1` | 21 | 1 | both ≡ **1** |
22
+ | `G` ↔ `6` | 16 | 6 | both ≡ **6** |
23
+
24
+ So a document number read as `SPECL2345` produces the **exact same check digit**
25
+ as the correct `SPEC12345`. The MRZ **validates either way** — the check digit
26
+ cannot tell them apart.
27
+
28
+ **Impact.** If OCR reads a `1` as `L` (or `6` as `G`) in a *numeric* position of a
29
+ field that also accepts letters (e.g. an alphanumeric document number), the MRZ
30
+ will validate but may carry the letter form.
31
+
32
+ **Mitigation.**
33
+
34
+ - **Multi-frame voting** (`mrzFrameAggregator`) usually resolves it: across frames
35
+ the correct glyph wins the per-character vote.
36
+ - **NFC chip read (eID)** is authoritative — when available it overrides the
37
+ camera reading.
38
+ - Other digit↔letter confusions (`0↔O/Q/D`, `2↔Z`, `5↔S`, `8↔B`, `4↔A`, `1↔I`) are
39
+ **not** mod-10 collisions and *are* recovered to the correct digit.
40
+
41
+ Covered by `mrz.ambiguous.test.ts` ("documents the … MOD-10 collision").
42
+
43
+ ---
44
+
45
+ ## 2. Unprotected fields (TD1 optional / national-ID) can't be self-corrected
46
+
47
+ **Issue.** TD1's optional-data field (which on Turkish IDs holds the 11-digit
48
+ national ID / TC number) has **no per-field check digit** — it is only covered by
49
+ the composite check digit. If OCR corrupts the optional field **and** the
50
+ composite digit is also dropped/misread (common on glossy cards), there is no
51
+ independent anchor to detect the error.
52
+
53
+ **Mitigation.** High-confidence numeric normalisation (`B→8`, `S→5`, `O→0`, …) is
54
+ applied to the all-digit optional field, and **multi-frame voting** is the
55
+ primary safeguard. A single noisy frame is not trusted; the consensus is.
56
+
57
+ ---
58
+
59
+ ## 3. Filler-letter normalisation is conservative in name fields (by design)
60
+
61
+ **Issue.** The `<` filler is frequently OCR'd as a letter (`K`, and under glare
62
+ `C`/`E`/`G`). We normalise these back to `<` **only** in structurally
63
+ unambiguous positions — the trailing filler run, the TD1 issuing-state field, and
64
+ the document-code filler.
65
+
66
+ **Deliberate trade-off.** A `K`/`C`/`E`/`G` **inside the name field** is *not*
67
+ normalised, because it cannot be distinguished from a real name letter (surnames
68
+ like `AKTAS`, `KAYA`, `CETIN`, or a 2-letter `AK` start with these). Corrupting a
69
+ real name is worse than a cosmetic filler glyph, so:
70
+
71
+ - Names are **preserved verbatim** — `AKTAS` stays `AKTAS`.
72
+ - A genuine filler misread *in the middle of a name region* may remain as a
73
+ letter (cosmetic only; the name field has no check digit, so validity is
74
+ unaffected, and the document number / dates — the identity-critical fields —
75
+ are always cleaned).
76
+
77
+ Covered by the "real letters in names are not eaten" tests in `mrz.utils.test.ts`.
78
+
79
+ ---
80
+
81
+ ## 4. OCR throughput / image-quality dependence
82
+
83
+ - **Resolution.** ML Kit needs ~16 px per character. A full-frame capture leaves a
84
+ 30–44-char MRZ line below that floor, so the native pipeline **crops to the
85
+ on-screen scan frame and upscales + grayscale/contrast-enhances** before OCR.
86
+ The MRZ band must be reasonably filled within the on-screen guide.
87
+ - **Glare / lamination.** Heavy glare on a glossy laminated card still degrades
88
+ OCR; the enhancement helps but does not eliminate it. Steadying the card and
89
+ reducing direct glare improves convergence.
90
+ - **Throughput.** OCR runs at a few frames/sec; the JS handler is throttled
91
+ (~400 ms) and the recovery search is time-bounded so the UI stays responsive.
92
+ A pathological/garbage frame fails fast (recovery budget) rather than blocking.
93
+
94
+ ---
95
+
96
+ ## 5. Structurally broken frames are (correctly) rejected
97
+
98
+ Truncated or wrong-length line-sets that cannot be repaired by ambiguous-char
99
+ swaps or ±1 indel are reported **invalid** — the flow will not advance on a
100
+ guess. This is intentional: a wrong-but-plausible reconstruction must never leak
101
+ through. Multi-frame scanning accumulates until a check-digit-valid consensus is
102
+ reached.
103
+
104
+ ---
105
+
106
+ ## 6. Platform parity
107
+
108
+ The scan-frame ROI crop + grayscale/contrast enhancement exists on **both Android
109
+ (Kotlin) and iOS (Swift / Core Image)**. The JS recovery, voting, and field
110
+ mapping are shared, so behaviour is consistent across platforms. Debug builds
111
+ require Metro to be reachable; build a **Release** variant for untethered device
112
+ testing.