@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.
- package/android/build.gradle +3 -3
- package/android/src/main/java/com/trustchex/reactnativesdk/camera/TrustchexCameraView.kt +190 -12
- package/ios/Camera/TrustchexCameraView.swift +244 -46
- package/ios/Permission/PermissionModule.m +22 -0
- package/ios/Permission/PermissionModule.swift +67 -0
- package/lib/module/Screens/Debug/MRZTestScreen.js +121 -147
- package/lib/module/Screens/Dynamic/LivenessDetectionScreen.js +24 -3
- package/lib/module/Screens/Dynamic/VerbalConsentScreen.js +1 -1
- package/lib/module/Screens/Dynamic/VideoCallScreen.js +7 -12
- package/lib/module/Screens/Static/ResultScreen.js +9 -1
- package/lib/module/Shared/Components/EIDScanner.js +166 -163
- package/lib/module/Shared/Components/FaceCamera.js +14 -8
- package/lib/module/Shared/Components/IdentityDocumentCamera.js +45 -10
- package/lib/module/Shared/Libs/MRZ_KNOWN_ISSUES.md +112 -0
- package/lib/module/Shared/Libs/PERMISSIONS_MANAGER.md +268 -0
- package/lib/module/Shared/Libs/index.js +20 -0
- package/lib/module/Shared/Libs/mrz.utils.js +570 -16
- package/lib/module/Shared/Libs/mrzFrameAggregator.js +301 -0
- package/lib/module/Shared/Libs/mrzOcrIntegration.js +109 -0
- package/lib/module/Shared/Libs/permissions.utils.js +199 -0
- package/lib/module/Translation/Resources/en.js +1 -0
- package/lib/module/Translation/Resources/tr.js +1 -0
- package/lib/module/version.js +1 -1
- package/lib/typescript/src/Screens/Debug/MRZTestScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Dynamic/LivenessDetectionScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Dynamic/VideoCallScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Static/ResultScreen.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/EIDScanner.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/FaceCamera.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/index.d.ts +20 -0
- package/lib/typescript/src/Shared/Libs/index.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Libs/mrz.utils.d.ts +8 -0
- package/lib/typescript/src/Shared/Libs/mrz.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/mrzFrameAggregator.d.ts +76 -0
- package/lib/typescript/src/Shared/Libs/mrzFrameAggregator.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Libs/mrzOcrIntegration.d.ts +73 -0
- package/lib/typescript/src/Shared/Libs/mrzOcrIntegration.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Libs/permissions.utils.d.ts +58 -0
- package/lib/typescript/src/Shared/Libs/permissions.utils.d.ts.map +1 -0
- package/lib/typescript/src/Translation/Resources/en.d.ts +1 -0
- package/lib/typescript/src/Translation/Resources/en.d.ts.map +1 -1
- package/lib/typescript/src/Translation/Resources/tr.d.ts +1 -0
- package/lib/typescript/src/Translation/Resources/tr.d.ts.map +1 -1
- package/lib/typescript/src/version.d.ts +1 -1
- package/package.json +14 -10
- package/src/Screens/Debug/MRZTestScreen.tsx +137 -166
- package/src/Screens/Dynamic/LivenessDetectionScreen.tsx +45 -5
- package/src/Screens/Dynamic/VerbalConsentScreen.tsx +1 -1
- package/src/Screens/Dynamic/VideoCallScreen.tsx +8 -19
- package/src/Screens/Static/ResultScreen.tsx +18 -9
- package/src/Shared/Components/EIDScanner.tsx +210 -160
- package/src/Shared/Components/FaceCamera.tsx +19 -16
- package/src/Shared/Components/IdentityDocumentCamera.tsx +53 -13
- package/src/Shared/Libs/MRZ_KNOWN_ISSUES.md +112 -0
- package/src/Shared/Libs/PERMISSIONS_MANAGER.md +268 -0
- package/src/Shared/Libs/index.ts +63 -0
- package/src/Shared/Libs/mrz.utils.ts +639 -11
- package/src/Shared/Libs/mrzFrameAggregator.ts +370 -0
- package/src/Shared/Libs/mrzOcrIntegration.ts +175 -0
- package/src/Shared/Libs/permissions.utils.ts +251 -0
- package/src/Translation/Resources/en.ts +1 -0
- package/src/Translation/Resources/tr.ts +1 -0
- 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.
|
|
334
|
+
children: [isScanned && documentMRZInfo && /*#__PURE__*/_jsxs(View, {
|
|
335
|
+
style: styles.idCardWrapper,
|
|
325
336
|
children: [/*#__PURE__*/_jsxs(View, {
|
|
326
|
-
style: styles.
|
|
327
|
-
children: [
|
|
328
|
-
|
|
329
|
-
|
|
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.
|
|
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.
|
|
429
|
-
children: [/*#__PURE__*/_jsx(
|
|
430
|
-
style: styles.
|
|
431
|
-
children:
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
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
|
-
|
|
533
|
+
// ── ID Card result layout ──────────────────────────────────────────────
|
|
534
|
+
idCardWrapper: {
|
|
535
|
+
gap: 10
|
|
536
|
+
},
|
|
537
|
+
idCard: {
|
|
561
538
|
backgroundColor: '#fff',
|
|
562
|
-
borderRadius:
|
|
563
|
-
|
|
564
|
-
gap: 16,
|
|
539
|
+
borderRadius: 14,
|
|
540
|
+
overflow: 'hidden',
|
|
565
541
|
shadowColor: '#000',
|
|
566
542
|
shadowOffset: {
|
|
567
543
|
width: 0,
|
|
568
|
-
height:
|
|
544
|
+
height: 3
|
|
569
545
|
},
|
|
570
|
-
shadowOpacity: 0.
|
|
571
|
-
shadowRadius:
|
|
572
|
-
elevation:
|
|
546
|
+
shadowOpacity: 0.12,
|
|
547
|
+
shadowRadius: 10,
|
|
548
|
+
elevation: 4,
|
|
573
549
|
borderWidth: 1,
|
|
574
|
-
borderColor: '#
|
|
550
|
+
borderColor: '#e8e8e8'
|
|
575
551
|
},
|
|
576
|
-
|
|
552
|
+
idCardStrip: {
|
|
577
553
|
flexDirection: 'row',
|
|
578
|
-
|
|
579
|
-
alignItems: '
|
|
580
|
-
|
|
581
|
-
|
|
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
|
-
|
|
587
|
-
|
|
559
|
+
idCardStripDocType: {
|
|
560
|
+
color: '#fff',
|
|
561
|
+
fontSize: 12,
|
|
588
562
|
fontWeight: '700',
|
|
589
|
-
|
|
590
|
-
|
|
563
|
+
letterSpacing: 0.5,
|
|
564
|
+
textTransform: 'uppercase'
|
|
591
565
|
},
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
fontWeight: '
|
|
566
|
+
idCardStripIssuer: {
|
|
567
|
+
color: 'rgba(255,255,255,0.85)',
|
|
568
|
+
fontSize: 12,
|
|
569
|
+
fontWeight: '600',
|
|
570
|
+
letterSpacing: 0.3
|
|
596
571
|
},
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
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
|
-
|
|
604
|
-
|
|
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
|
-
|
|
612
|
-
fontSize:
|
|
598
|
+
idCardPhotoPlaceholderText: {
|
|
599
|
+
fontSize: 30,
|
|
613
600
|
color: '#ccc'
|
|
614
601
|
},
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
width: '100%',
|
|
618
|
-
gap: 2
|
|
619
|
-
},
|
|
620
|
-
mrzInfoItem: {
|
|
602
|
+
idCardFields: {
|
|
603
|
+
flex: 1,
|
|
621
604
|
flexDirection: 'row',
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
borderBottomWidth: 1
|
|
605
|
+
flexWrap: 'wrap',
|
|
606
|
+
gap: 6,
|
|
607
|
+
alignContent: 'flex-start',
|
|
608
|
+
padding: 10
|
|
627
609
|
},
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
610
|
+
idCardFieldItem: {
|
|
611
|
+
flexGrow: 1,
|
|
612
|
+
flexShrink: 1,
|
|
613
|
+
flexBasis: '45%',
|
|
614
|
+
minWidth: 0
|
|
632
615
|
},
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
fontSize: 13,
|
|
616
|
+
idCardFieldLabel: {
|
|
617
|
+
fontSize: 8,
|
|
636
618
|
fontWeight: '600',
|
|
637
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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,
|
|
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
|
-
|
|
124
|
-
const
|
|
125
|
-
setHasPermission(
|
|
126
|
-
}
|
|
127
|
-
setHasPermission(
|
|
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
|
-
|
|
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
|
-
//
|
|
546
|
-
|
|
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 =
|
|
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.
|