@trustchex/react-native-sdk 1.464.0 → 1.472.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 (46) hide show
  1. package/android/src/main/java/com/trustchex/reactnativesdk/camera/TrustchexCameraView.kt +43 -3
  2. package/ios/Camera/TrustchexCameraView.swift +152 -27
  3. package/ios/Permission/PermissionModule.m +22 -0
  4. package/ios/Permission/PermissionModule.swift +67 -0
  5. package/lib/module/Screens/Dynamic/LivenessDetectionScreen.js +24 -3
  6. package/lib/module/Screens/Dynamic/VerbalConsentScreen.js +1 -1
  7. package/lib/module/Screens/Dynamic/VideoCallScreen.js +7 -12
  8. package/lib/module/Screens/Static/ResultScreen.js +4 -1
  9. package/lib/module/Shared/Components/EIDScanner.js +166 -163
  10. package/lib/module/Shared/Components/FaceCamera.js +14 -8
  11. package/lib/module/Shared/Components/IdentityDocumentCamera.js +7 -6
  12. package/lib/module/Shared/Libs/PERMISSIONS_MANAGER.md +268 -0
  13. package/lib/module/Shared/Libs/index.js +20 -0
  14. package/lib/module/Shared/Libs/permissions.utils.js +199 -0
  15. package/lib/module/Translation/Resources/en.js +1 -0
  16. package/lib/module/Translation/Resources/tr.js +1 -0
  17. package/lib/module/version.js +1 -1
  18. package/lib/typescript/src/Screens/Dynamic/LivenessDetectionScreen.d.ts.map +1 -1
  19. package/lib/typescript/src/Screens/Dynamic/VideoCallScreen.d.ts.map +1 -1
  20. package/lib/typescript/src/Screens/Static/ResultScreen.d.ts.map +1 -1
  21. package/lib/typescript/src/Shared/Components/EIDScanner.d.ts.map +1 -1
  22. package/lib/typescript/src/Shared/Components/FaceCamera.d.ts.map +1 -1
  23. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.d.ts.map +1 -1
  24. package/lib/typescript/src/Shared/Libs/index.d.ts +20 -0
  25. package/lib/typescript/src/Shared/Libs/index.d.ts.map +1 -0
  26. package/lib/typescript/src/Shared/Libs/permissions.utils.d.ts +58 -0
  27. package/lib/typescript/src/Shared/Libs/permissions.utils.d.ts.map +1 -0
  28. package/lib/typescript/src/Translation/Resources/en.d.ts +1 -0
  29. package/lib/typescript/src/Translation/Resources/en.d.ts.map +1 -1
  30. package/lib/typescript/src/Translation/Resources/tr.d.ts +1 -0
  31. package/lib/typescript/src/Translation/Resources/tr.d.ts.map +1 -1
  32. package/lib/typescript/src/version.d.ts +1 -1
  33. package/package.json +1 -1
  34. package/src/Screens/Dynamic/LivenessDetectionScreen.tsx +45 -5
  35. package/src/Screens/Dynamic/VerbalConsentScreen.tsx +1 -1
  36. package/src/Screens/Dynamic/VideoCallScreen.tsx +8 -19
  37. package/src/Screens/Static/ResultScreen.tsx +13 -9
  38. package/src/Shared/Components/EIDScanner.tsx +210 -160
  39. package/src/Shared/Components/FaceCamera.tsx +19 -16
  40. package/src/Shared/Components/IdentityDocumentCamera.tsx +7 -7
  41. package/src/Shared/Libs/PERMISSIONS_MANAGER.md +268 -0
  42. package/src/Shared/Libs/index.ts +63 -0
  43. package/src/Shared/Libs/permissions.utils.ts +251 -0
  44. package/src/Translation/Resources/en.ts +1 -0
  45. package/src/Translation/Resources/tr.ts +1 -0
  46. package/src/version.ts +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trustchex/react-native-sdk",
3
- "version": "1.464.0",
3
+ "version": "1.472.0",
4
4
  "description": "Trustchex mobile app react native SDK for android or ios devices",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",
@@ -181,7 +181,6 @@ const LivenessDetectionScreen = () => {
181
181
  },
182
182
  FINISH: { instruction: t('livenessDetectionScreen.finish') },
183
183
  });
184
- const [instructionList, setInstructionList] = useState<string[]>([]);
185
184
  const [hasGuideShown, setHasGuideShown] = useState(false);
186
185
  const stoppingRecordingRef = useRef(false); // Track if we're already stopping to prevent multiple calls
187
186
 
@@ -220,7 +219,6 @@ const LivenessDetectionScreen = () => {
220
219
  .sort(() => Math.random() - 0.5);
221
220
  il.unshift('START');
222
221
  il.push('FINISH');
223
- setInstructionList(il);
224
222
  setInitialState({
225
223
  brightnessLow: false,
226
224
  faceDetected: false,
@@ -598,9 +596,51 @@ const LivenessDetectionScreen = () => {
598
596
  const circleR = previewSizeInFrame / 2;
599
597
  const faceDx = faceCenterX - circleCX;
600
598
  const faceDy = faceCenterY - circleCY;
601
- const previewContainsFace =
602
- faceDx * faceDx + faceDy * faceDy <=
603
- (circleR - faceCoreRadius) * (circleR - faceCoreRadius);
599
+
600
+ const yawAbs = Math.abs(face.yawAngle || 0);
601
+ const yawFactor = Math.min(1, yawAbs / TURN_ANGLE_LIMIT);
602
+
603
+ // General bound tolerance (instruction-agnostic): adapt to head pose and
604
+ // face box deformation so containment remains stable across all actions.
605
+ const faceWidthGrowth = Math.max(
606
+ 0,
607
+ face.bounds.width - face.bounds.height
608
+ );
609
+
610
+ // On iOS, close faces tend to appear larger in-frame and lateral head
611
+ // turns shift center more aggressively. Apply an iOS-only proximity boost
612
+ // to horizontal tolerance to avoid false outside-circle resets.
613
+ const faceRadiusRatio = Math.min(
614
+ 1,
615
+ faceCoreRadius / Math.max(1, circleR)
616
+ );
617
+ const iosProximityBoost =
618
+ Platform.OS === 'ios' ? Math.max(0, faceRadiusRatio - 0.5) : 0;
619
+
620
+ const baseToleranceX = previewSizeInFrame * 0.05;
621
+ const yawToleranceX = face.bounds.width * 0.12 * yawFactor;
622
+ const shapeToleranceX = faceWidthGrowth * 0.1;
623
+ const iosToleranceX = face.bounds.width * 0.2 * iosProximityBoost;
624
+ const toleranceX = Math.max(
625
+ 0,
626
+ baseToleranceX + yawToleranceX + shapeToleranceX + iosToleranceX,
627
+ face.bounds.width * 0.05
628
+ );
629
+
630
+ const pitchAbs = Math.abs(face.pitchAngle || 0);
631
+ const pitchFactor = Math.min(1, pitchAbs / TURN_ANGLE_LIMIT);
632
+ const toleranceY = Math.max(
633
+ 0,
634
+ previewSizeInFrame * 0.03 + face.bounds.height * 0.03 * pitchFactor,
635
+ face.bounds.height * 0.04
636
+ );
637
+
638
+ const allowedRadiusX = Math.max(1, circleR - faceCoreRadius + toleranceX);
639
+ const allowedRadiusY = Math.max(1, circleR - faceCoreRadius + toleranceY);
640
+ const normalizedDistance =
641
+ (faceDx * faceDx) / (allowedRadiusX * allowedRadiusX) +
642
+ (faceDy * faceDy) / (allowedRadiusY * allowedRadiusY);
643
+ const previewContainsFace = normalizedDistance <= 1;
604
644
  const multipleFacesDetected = faces.length > 1;
605
645
 
606
646
  if (!isImageBright) {
@@ -801,7 +801,7 @@ const VerbalConsentScreen = () => {
801
801
  if (!currentInstructionKey) {
802
802
  return;
803
803
  }
804
- speak(t(currentInstructionKey));
804
+ speak(t(currentInstructionKey)).catch(() => {});
805
805
  }, [
806
806
  appContext.currentWorkflowStep?.data?.voiceGuidanceActive,
807
807
  currentInstructionKey,
@@ -7,13 +7,13 @@ import {
7
7
  TouchableOpacity,
8
8
  View,
9
9
  ActivityIndicator,
10
- PermissionsAndroid,
11
10
  StatusBar,
12
11
  } from 'react-native';
13
12
  import { RTCView, MediaStream } from 'react-native-webrtc';
14
13
  import InCallManager from 'react-native-incall-manager';
15
14
  import LottieView from 'lottie-react-native';
16
15
  import { useTranslation } from 'react-i18next';
16
+ import PermissionManager from '../../Shared/Libs/permissions.utils';
17
17
  import { useStatusBarWhiteBackground } from '../../Shared/Libs/status-bar.utils';
18
18
  import { WebRTCService } from '../../Shared/Services/WebRTCService';
19
19
  import { VideoSessionService } from '../../Shared/Services/VideoSessionService';
@@ -373,25 +373,14 @@ const VideoCallScreen = ({ navigation }: any) => {
373
373
  });
374
374
  serviceRef.current = service;
375
375
 
376
- // Request Permissions
377
- if (Platform.OS === 'android') {
378
- const granted = await PermissionsAndroid.requestMultiple([
379
- PermissionsAndroid.PERMISSIONS.CAMERA,
380
- PermissionsAndroid.PERMISSIONS.RECORD_AUDIO,
381
- ]);
382
- if (
383
- granted['android.permission.CAMERA'] !==
384
- PermissionsAndroid.RESULTS.GRANTED ||
385
- granted['android.permission.RECORD_AUDIO'] !==
386
- PermissionsAndroid.RESULTS.GRANTED
387
- ) {
388
- if (mounted) setConnectionState('permissions_denied');
389
- return;
390
- }
376
+ // Request Camera and Microphone Permissions
377
+ const hasBothPermissions =
378
+ await PermissionManager.requestCameraAndMicrophone();
379
+
380
+ if (!hasBothPermissions) {
381
+ if (mounted) setConnectionState('permissions_denied');
382
+ return;
391
383
  }
392
- // For iOS, react-native-webrtc or VisionCamera usually triggers it,
393
- // but explicit request via library is better if we had one.
394
- // Assuming automatic or previously granted for now.
395
384
 
396
385
  try {
397
386
  const stream = await service.initialize();
@@ -1055,15 +1055,19 @@ const ResultScreen = () => {
1055
1055
  <Text style={styles.sectionText}>
1056
1056
  {t('resultScreen.demoVideo')}
1057
1057
  </Text>
1058
- <Video
1059
- source={{
1060
- uri: consentVideo.videoPath,
1061
- }}
1062
- resizeMode="contain"
1063
- style={styles.video}
1064
- controls={true}
1065
- muted={false}
1066
- />
1058
+ {consentVideo.videoPath ? (
1059
+ <Video
1060
+ source={{
1061
+ uri: consentVideo.videoPath,
1062
+ }}
1063
+ resizeMode="contain"
1064
+ style={styles.video}
1065
+ controls={true}
1066
+ muted={false}
1067
+ />
1068
+ ) : (
1069
+ <Text style={styles.sectionText}>N/A</Text>
1070
+ )}
1067
1071
  </View>
1068
1072
  )
1069
1073
  )}
@@ -3,6 +3,8 @@ import { Alert, View, Text, Image, StyleSheet, Animated } from 'react-native';
3
3
  import { useSafeAreaInsets } from 'react-native-safe-area-context';
4
4
  import NFCManager from 'react-native-nfc-manager';
5
5
  import DeviceInfo from 'react-native-device-info';
6
+ import PermissionManager from '../Libs/permissions.utils';
7
+ import { debugError } from '../Libs/debug.utils';
6
8
  import { MRZInfo } from '../EIDReader/lds/icao/mrzInfo';
7
9
  import { eidReader } from '../EIDReader/eidReader';
8
10
  import NativeProgressBar from './NativeProgressBar';
@@ -142,6 +144,21 @@ const EIDScanner = ({
142
144
  setProgressStage('');
143
145
  resetLastMessage();
144
146
 
147
+ // Check NFC permissions before attempting to scan
148
+ const hasNFCPermission = await PermissionManager.requestPermission('nfc');
149
+ if (!hasNFCPermission) {
150
+ debugError('EIDScanner', 'NFC permission denied');
151
+ setIsScanning(false);
152
+ Alert.alert(
153
+ t('eidScannerScreen.error'),
154
+ t(
155
+ 'eidScannerScreen.nfcPermissionDenied',
156
+ 'NFC permission is required to scan your document'
157
+ )
158
+ );
159
+ return;
160
+ }
161
+
145
162
  // Track EID scan start using analytics helper
146
163
  const docType =
147
164
  (documentType as 'ID' | 'PASSPORT' | 'UNKNOWN') || 'UNKNOWN';
@@ -473,116 +490,124 @@ const EIDScanner = ({
473
490
  </View>
474
491
  ) : (
475
492
  <>
476
- {isScanned && (
477
- <View style={styles.resultCard}>
478
- <View style={styles.resultHeader}>
479
- {documentFaceImage &&
480
- (documentFaceImageMimeType === 'image/jpeg' ||
481
- documentFaceImageMimeType === 'image/png') ? (
482
- <Image
483
- source={{
484
- uri: `data:${documentFaceImageMimeType};base64,${documentFaceImage}`,
485
- }}
486
- style={styles.faceImage}
487
- resizeMode="cover"
488
- />
489
- ) : (
490
- <View style={styles.faceImagePlaceholder}>
491
- <Text style={styles.faceImagePlaceholderText}>?</Text>
493
+ {isScanned && documentMRZInfo && (
494
+ <View style={styles.idCardWrapper}>
495
+ <View style={styles.idCard}>
496
+ {/* ── Card header strip: doc type + issuing state ── */}
497
+ <View
498
+ style={[
499
+ styles.idCardStrip,
500
+ { backgroundColor: appContext.branding.primaryColor },
501
+ ]}
502
+ >
503
+ <Text style={styles.idCardStripDocType}>
504
+ {formatDocumentType(documentMRZInfo.getDocumentCode())}
505
+ </Text>
506
+ </View>
507
+
508
+ {/* ── Photo (left) + fields (right) ── */}
509
+ <View style={styles.idCardBody}>
510
+ <View style={styles.idCardPhotoContainer}>
511
+ <View style={styles.idCardPhotoFrame}>
512
+ {documentFaceImage &&
513
+ (documentFaceImageMimeType === 'image/jpeg' ||
514
+ documentFaceImageMimeType === 'image/png') ? (
515
+ <Image
516
+ source={{
517
+ uri: `data:${documentFaceImageMimeType};base64,${documentFaceImage}`,
518
+ }}
519
+ style={styles.idCardPhoto}
520
+ resizeMode="cover"
521
+ />
522
+ ) : (
523
+ <View style={styles.idCardPhotoPlaceholder}>
524
+ <Text style={styles.idCardPhotoPlaceholderText}>
525
+ ?
526
+ </Text>
527
+ </View>
528
+ )}
529
+ </View>
492
530
  </View>
493
- )}
494
- {documentMRZInfo && (
495
- <View style={styles.resultHeaderInfo}>
496
- <Text style={styles.fullName} numberOfLines={2}>
497
- {documentMRZInfo.getSecondaryIdentifier()}{' '}
498
- {documentMRZInfo.getPrimaryIdentifier()}
499
- </Text>
500
- <Text style={styles.subInfo}>
501
- {formatNationality(documentMRZInfo.getNationality())} ·{' '}
502
- {formatDocumentType(documentMRZInfo.getDocumentCode())}
503
- </Text>
504
- <Text style={styles.subInfo}>
505
- {formatGender(documentMRZInfo.getGender().getStrCode())} ·{' '}
506
- {formatDate(documentMRZInfo.getDateOfBirth())}
507
- </Text>
531
+
532
+ {/* Right column: all fields as grid */}
533
+ <View style={styles.idCardFields}>
534
+ <IDCardField
535
+ label={t('eidScannerScreen.surname')}
536
+ value={documentMRZInfo.getPrimaryIdentifier()}
537
+ />
538
+ <IDCardField
539
+ label={t('eidScannerScreen.name')}
540
+ value={documentMRZInfo.getSecondaryIdentifier()}
541
+ />
542
+ <IDCardField
543
+ label={t('eidScannerScreen.documentNumber')}
544
+ value={documentMRZInfo.getDocumentNumber()}
545
+ />
546
+ <IDCardField
547
+ label={t('eidScannerScreen.birthDate')}
548
+ value={formatDate(documentMRZInfo.getDateOfBirth())}
549
+ />
550
+ <IDCardField
551
+ label={t('eidScannerScreen.expirationDate')}
552
+ value={formatDate(documentMRZInfo.getDateOfExpiry())}
553
+ />
554
+ <IDCardField
555
+ label={t('eidScannerScreen.gender')}
556
+ value={formatGender(
557
+ documentMRZInfo.getGender().getStrCode()
558
+ )}
559
+ />
560
+ <IDCardField
561
+ label={t('eidScannerScreen.nationality')}
562
+ value={formatNationality(
563
+ documentMRZInfo.getNationality()
564
+ )}
565
+ />
566
+ <IDCardField
567
+ label={t('eidScannerScreen.issuingState')}
568
+ value={formatNationality(
569
+ documentMRZInfo.getIssuingState()
570
+ )}
571
+ />
572
+ {!!(
573
+ documentMRZInfo.getPersonalNumber() ||
574
+ documentMRZInfo.getOptionalData1()
575
+ ) && (
576
+ <IDCardField
577
+ label={t('eidScannerScreen.personalNumber')}
578
+ value={
579
+ documentMRZInfo.getPersonalNumber() ||
580
+ documentMRZInfo.getOptionalData1() ||
581
+ ''
582
+ }
583
+ />
584
+ )}
508
585
  </View>
509
- )}
586
+ </View>
510
587
  </View>
511
588
 
512
- {documentMRZInfo && (
513
- <View style={styles.mrzInfo}>
514
- <View style={styles.mrzInfoItem}>
515
- <Text style={styles.mrzInfoLabel}>
516
- {t('eidScannerScreen.name')}
517
- </Text>
518
- <Text style={styles.mrzInfoText}>
519
- {documentMRZInfo.getSecondaryIdentifier()}
520
- </Text>
521
- </View>
522
- <View style={styles.mrzInfoItem}>
523
- <Text style={styles.mrzInfoLabel}>
524
- {t('eidScannerScreen.surname')}
525
- </Text>
526
- <Text style={styles.mrzInfoText}>
527
- {documentMRZInfo.getPrimaryIdentifier()}
528
- </Text>
529
- </View>
530
- <View style={styles.mrzInfoItem}>
531
- <Text style={styles.mrzInfoLabel}>
532
- {t('eidScannerScreen.documentCode')}
533
- </Text>
534
- <Text style={styles.mrzInfoText}>
535
- {formatDocumentType(documentMRZInfo.getDocumentCode())}
536
- </Text>
537
- </View>
538
- <View style={styles.mrzInfoItem}>
539
- <Text style={styles.mrzInfoLabel}>
540
- {t('eidScannerScreen.documentNumber')}
541
- </Text>
542
- <Text style={styles.mrzInfoText}>
543
- {documentMRZInfo.getDocumentNumber()}
544
- </Text>
545
- </View>
546
- <View style={styles.mrzInfoItem}>
547
- <Text style={styles.mrzInfoLabel}>
548
- {t('eidScannerScreen.personalNumber')}
549
- </Text>
550
- <Text style={styles.mrzInfoText}>
551
- {documentMRZInfo.getOptionalData1()}
552
- </Text>
553
- </View>
554
- <View style={styles.mrzInfoItem}>
555
- <Text style={styles.mrzInfoLabel}>
556
- {t('eidScannerScreen.nationality')}
557
- </Text>
558
- <Text style={styles.mrzInfoText}>
559
- {formatNationality(documentMRZInfo.getNationality())}
560
- </Text>
561
- </View>
562
- <View style={styles.mrzInfoItem}>
563
- <Text style={styles.mrzInfoLabel}>
564
- {t('eidScannerScreen.gender')}
565
- </Text>
566
- <Text style={styles.mrzInfoText}>
567
- {formatGender(documentMRZInfo.getGender().getStrCode())}
568
- </Text>
569
- </View>
570
- <View style={styles.mrzInfoItem}>
571
- <Text style={styles.mrzInfoLabel}>
572
- {t('eidScannerScreen.birthDate')}
573
- </Text>
574
- <Text style={styles.mrzInfoText}>
575
- {formatDate(documentMRZInfo.getDateOfBirth())}
576
- </Text>
577
- </View>
578
- <View style={styles.mrzInfoItem}>
579
- <Text style={styles.mrzInfoLabel}>
580
- {t('eidScannerScreen.expirationDate')}
581
- </Text>
582
- <Text style={styles.mrzInfoText}>
583
- {formatDate(documentMRZInfo.getDateOfExpiry())}
584
- </Text>
585
- </View>
589
+ {hasNfc && isEnabled && isScanned && (
590
+ <View style={styles.buttonsContainer}>
591
+ <StyledButton
592
+ mode="contained"
593
+ disabled={!documentFaceImage || !documentMRZInfo}
594
+ onPress={() => {
595
+ if (
596
+ onScanSuccess &&
597
+ documentMRZInfo &&
598
+ documentFaceImage &&
599
+ documentFaceImageMimeType
600
+ ) {
601
+ onScanSuccess(
602
+ getFieldsFromMRZ(documentMRZInfo),
603
+ documentFaceImage,
604
+ documentFaceImageMimeType
605
+ );
606
+ }
607
+ }}
608
+ >
609
+ {t('eidScannerScreen.approveAndContinue')}
610
+ </StyledButton>
586
611
  </View>
587
612
  )}
588
613
  </View>
@@ -648,7 +673,7 @@ const EIDScanner = ({
648
673
  </View>
649
674
  )}
650
675
 
651
- {hasNfc && isEnabled && isScanned && (
676
+ {hasNfc && isEnabled && isScanned && !documentMRZInfo && (
652
677
  <View style={styles.buttonsContainer}>
653
678
  <StyledButton
654
679
  mode="contained"
@@ -747,83 +772,99 @@ const styles = StyleSheet.create({
747
772
  color: 'black',
748
773
  textAlign: 'center',
749
774
  },
750
- resultCard: {
775
+ // ── ID Card result layout ──────────────────────────────────────────────
776
+ idCardWrapper: {
777
+ gap: 10,
778
+ },
779
+ idCard: {
751
780
  backgroundColor: '#fff',
752
- borderRadius: 16,
753
- padding: 16,
754
- gap: 16,
781
+ borderRadius: 14,
782
+ overflow: 'hidden',
755
783
  shadowColor: '#000',
756
- shadowOffset: { width: 0, height: 2 },
757
- shadowOpacity: 0.08,
758
- shadowRadius: 8,
759
- elevation: 3,
784
+ shadowOffset: { width: 0, height: 3 },
785
+ shadowOpacity: 0.12,
786
+ shadowRadius: 10,
787
+ elevation: 4,
760
788
  borderWidth: 1,
761
- borderColor: '#f0f0f0',
789
+ borderColor: '#e8e8e8',
762
790
  },
763
- resultHeader: {
791
+ idCardStrip: {
764
792
  flexDirection: 'row',
765
- gap: 14,
766
- alignItems: 'flex-start',
767
- },
768
- resultHeaderInfo: {
769
- flex: 1,
770
- gap: 4,
771
- justifyContent: 'center',
793
+ justifyContent: 'space-between',
794
+ alignItems: 'center',
795
+ paddingHorizontal: 12,
796
+ paddingVertical: 6,
772
797
  },
773
- fullName: {
774
- fontSize: 17,
798
+ idCardStripDocType: {
799
+ color: '#fff',
800
+ fontSize: 12,
775
801
  fontWeight: '700',
776
- color: '#111',
777
- letterSpacing: 0.2,
802
+ letterSpacing: 0.5,
803
+ textTransform: 'uppercase',
778
804
  },
779
- subInfo: {
780
- fontSize: 13,
781
- color: '#666',
782
- fontWeight: '500',
805
+ idCardStripIssuer: {
806
+ color: 'rgba(255,255,255,0.85)',
807
+ fontSize: 12,
808
+ fontWeight: '600',
809
+ letterSpacing: 0.3,
810
+ },
811
+ idCardBody: {
812
+ flexDirection: 'row',
813
+ alignItems: 'stretch',
783
814
  },
784
- faceImage: {
785
- width: 90,
786
- height: 115,
815
+ idCardPhotoContainer: {
816
+ width: 110,
817
+ padding: 8,
818
+ alignSelf: 'stretch',
819
+ },
820
+ idCardPhotoFrame: {
821
+ flex: 1,
787
822
  borderRadius: 10,
788
823
  overflow: 'hidden',
824
+ borderWidth: 1,
825
+ borderColor: 'rgba(0,0,0,0.12)',
789
826
  },
790
- faceImagePlaceholder: {
791
- width: 90,
792
- height: 115,
827
+ idCardPhoto: {
828
+ flex: 1,
829
+ },
830
+ idCardPhotoPlaceholder: {
831
+ flex: 1,
793
832
  borderRadius: 10,
794
833
  backgroundColor: '#f5f5f5',
795
834
  alignItems: 'center',
796
835
  justifyContent: 'center',
797
836
  },
798
- faceImagePlaceholderText: {
799
- fontSize: 32,
837
+ idCardPhotoPlaceholderText: {
838
+ fontSize: 30,
800
839
  color: '#ccc',
801
840
  },
802
- mrzInfo: {
803
- flexDirection: 'column',
804
- width: '100%',
805
- gap: 2,
806
- },
807
- mrzInfoItem: {
841
+ idCardFields: {
842
+ flex: 1,
808
843
  flexDirection: 'row',
809
- justifyContent: 'space-between',
810
- alignItems: 'center',
811
- paddingVertical: 8,
812
- borderBottomColor: '#f0f0f0',
813
- borderBottomWidth: 1,
844
+ flexWrap: 'wrap',
845
+ gap: 6,
846
+ alignContent: 'flex-start',
847
+ padding: 10,
814
848
  },
815
- mrzInfoLabel: {
816
- color: '#888',
817
- fontSize: 13,
818
- fontWeight: '500',
849
+ idCardFieldItem: {
850
+ flexGrow: 1,
851
+ flexShrink: 1,
852
+ flexBasis: '45%',
853
+ minWidth: 0,
819
854
  },
820
- mrzInfoText: {
821
- color: '#111',
822
- fontSize: 13,
855
+ idCardFieldLabel: {
856
+ fontSize: 8,
857
+ fontWeight: '600',
858
+ color: '#999',
859
+ letterSpacing: 0.3,
860
+ marginBottom: 1,
861
+ },
862
+ idCardFieldValue: {
863
+ fontSize: 11,
823
864
  fontWeight: '600',
824
- textAlign: 'right',
865
+ color: '#111',
825
866
  flexShrink: 1,
826
- marginLeft: 8,
867
+ flexWrap: 'wrap',
827
868
  },
828
869
  guide: {
829
870
  flex: 1,
@@ -855,4 +896,13 @@ const styles = StyleSheet.create({
855
896
  },
856
897
  });
857
898
 
899
+ const IDCardField = ({ label, value }: { label: string; value: string }) => (
900
+ <View style={styles.idCardFieldItem}>
901
+ <Text style={styles.idCardFieldLabel} numberOfLines={1}>
902
+ {label}
903
+ </Text>
904
+ <Text style={styles.idCardFieldValue}>{value || '—'}</Text>
905
+ </View>
906
+ );
907
+
858
908
  export default EIDScanner;
@@ -1,16 +1,15 @@
1
1
  import { useIsFocused } from '@react-navigation/native';
2
2
  import { useKeepAwake } from '../Libs/native-keep-awake.utils';
3
3
  import { debugLog, debugWarn, debugError, logError } from '../Libs/debug.utils';
4
+ import PermissionManager from '../Libs/permissions.utils';
4
5
  import React, { useEffect, useState, useRef, useCallback } from 'react';
5
6
  import {
6
7
  StyleSheet,
7
8
  Text,
8
9
  View,
9
- Platform,
10
10
  Linking,
11
11
  ActivityIndicator,
12
12
  NativeModules,
13
- PermissionsAndroid,
14
13
  type NativeSyntheticEvent,
15
14
  } from 'react-native';
16
15
  import {
@@ -81,20 +80,24 @@ const FaceCamera = ({
81
80
 
82
81
  useEffect(() => {
83
82
  const requestPermissions = async () => {
84
- if (Platform.OS === 'android') {
85
- const cameraGranted = await PermissionsAndroid.request(
86
- PermissionsAndroid.PERMISSIONS.CAMERA
87
- );
88
- const audioGranted = await PermissionsAndroid.request(
89
- PermissionsAndroid.PERMISSIONS.RECORD_AUDIO
90
- );
91
- setHasPermission(
92
- cameraGranted === PermissionsAndroid.RESULTS.GRANTED &&
93
- audioGranted === PermissionsAndroid.RESULTS.GRANTED
94
- );
95
- } else {
96
- // iOS permissions handled by native camera
97
- setHasPermission(true);
83
+ try {
84
+ const permissions = await PermissionManager.requestPermissions([
85
+ 'camera',
86
+ 'microphone',
87
+ ]);
88
+
89
+ const hasCamera = permissions.get('camera') ?? false;
90
+ const hasMicrophone = permissions.get('microphone') ?? false;
91
+
92
+ debugLog('FaceCamera', 'Permissions requested:', {
93
+ camera: hasCamera,
94
+ microphone: hasMicrophone,
95
+ });
96
+
97
+ setHasPermission(hasCamera && hasMicrophone);
98
+ } catch (error) {
99
+ debugError('FaceCamera', 'Permission request failed:', error);
100
+ setHasPermission(false);
98
101
  }
99
102
  setPermissionsRequested(true);
100
103
  };