@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.
- package/ios/ImageDecoder/ImageDecoderModule.m +12 -0
- package/ios/ImageDecoder/ImageDecoderModule.swift +43 -0
- package/lib/module/Shared/Components/EIDScanner.js +100 -37
- package/lib/module/Shared/Components/IdentityDocumentCamera.constants.js +9 -1
- package/lib/module/Shared/Components/IdentityDocumentCamera.flows.js +22 -2
- package/lib/module/Shared/Components/IdentityDocumentCamera.js +1 -1
- package/lib/module/Shared/Components/IdentityDocumentCamera.utils.js +7 -7
- package/lib/module/Shared/EIDReader/cardFileInputStream.js +25 -2
- package/lib/module/Shared/EIDReader/defaultFileSystem.js +56 -6
- package/lib/module/Shared/EIDReader/eidReader.js +138 -16
- package/lib/module/Shared/EIDReader/lds/icao/dg11File.js +23 -6
- package/lib/module/Shared/EIDReader/nfcManagerCardService.js +79 -9
- package/lib/module/Shared/EIDReader/protocol/paceDH.js +101 -0
- package/lib/module/Shared/EIDReader/protocol/paceErrors.js +34 -0
- package/lib/module/Shared/EIDReader/protocol/paceProtocol.js +53 -4
- package/lib/module/Shared/EIDReader/protocol/readBinaryAPDUSender.js +7 -3
- package/lib/module/Shared/Libs/country-display.utils.js +13 -1
- package/lib/module/Shared/Libs/diagnosticReport.js +24 -1
- package/lib/module/Shared/Libs/diagnostics.js +30 -8
- package/lib/module/Shared/Libs/jp2Decode.js +43 -0
- package/lib/module/Shared/Libs/mrz.utils.js +22 -5
- package/lib/module/Shared/Libs/mrzTransliteration.js +14 -1
- package/lib/module/Translation/Resources/en.js +12 -0
- package/lib/module/Translation/Resources/tr.js +12 -0
- package/lib/module/version.js +1 -1
- package/lib/typescript/src/Shared/Components/EIDScanner.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.constants.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.flows.d.ts +11 -0
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.flows.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.utils.d.ts +1 -1
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/EIDReader/cardFileInputStream.d.ts +3 -0
- package/lib/typescript/src/Shared/EIDReader/cardFileInputStream.d.ts.map +1 -1
- package/lib/typescript/src/Shared/EIDReader/defaultFileSystem.d.ts +4 -0
- package/lib/typescript/src/Shared/EIDReader/defaultFileSystem.d.ts.map +1 -1
- package/lib/typescript/src/Shared/EIDReader/eidReader.d.ts.map +1 -1
- package/lib/typescript/src/Shared/EIDReader/lds/icao/dg11File.d.ts.map +1 -1
- package/lib/typescript/src/Shared/EIDReader/nfcManagerCardService.d.ts +9 -1
- package/lib/typescript/src/Shared/EIDReader/nfcManagerCardService.d.ts.map +1 -1
- package/lib/typescript/src/Shared/EIDReader/protocol/paceDH.d.ts +56 -0
- package/lib/typescript/src/Shared/EIDReader/protocol/paceDH.d.ts.map +1 -0
- package/lib/typescript/src/Shared/EIDReader/protocol/paceErrors.d.ts +26 -0
- package/lib/typescript/src/Shared/EIDReader/protocol/paceErrors.d.ts.map +1 -0
- package/lib/typescript/src/Shared/EIDReader/protocol/paceProtocol.d.ts.map +1 -1
- package/lib/typescript/src/Shared/EIDReader/protocol/readBinaryAPDUSender.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/country-display.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/diagnosticReport.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/diagnostics.d.ts +9 -1
- package/lib/typescript/src/Shared/Libs/diagnostics.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/jp2Decode.d.ts +13 -0
- package/lib/typescript/src/Shared/Libs/jp2Decode.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Libs/mrz.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/mrzTransliteration.d.ts.map +1 -1
- package/lib/typescript/src/Translation/Resources/en.d.ts +12 -0
- package/lib/typescript/src/Translation/Resources/en.d.ts.map +1 -1
- package/lib/typescript/src/Translation/Resources/tr.d.ts +12 -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 +1 -1
- package/src/Shared/Components/EIDScanner.tsx +153 -69
- package/src/Shared/Components/IdentityDocumentCamera.constants.ts +9 -1
- package/src/Shared/Components/IdentityDocumentCamera.flows.ts +22 -2
- package/src/Shared/Components/IdentityDocumentCamera.tsx +1 -2
- package/src/Shared/Components/IdentityDocumentCamera.utils.ts +6 -8
- package/src/Shared/EIDReader/cardFileInputStream.ts +30 -6
- package/src/Shared/EIDReader/defaultFileSystem.ts +75 -17
- package/src/Shared/EIDReader/eidReader.ts +172 -24
- package/src/Shared/EIDReader/lds/icao/dg11File.ts +28 -13
- package/src/Shared/EIDReader/nfcManagerCardService.ts +91 -13
- package/src/Shared/EIDReader/protocol/paceDH.ts +183 -0
- package/src/Shared/EIDReader/protocol/paceErrors.ts +41 -0
- package/src/Shared/EIDReader/protocol/paceProtocol.ts +72 -6
- package/src/Shared/EIDReader/protocol/readBinaryAPDUSender.ts +7 -4
- package/src/Shared/Libs/country-display.utils.ts +12 -1
- package/src/Shared/Libs/diagnosticReport.ts +32 -1
- package/src/Shared/Libs/diagnostics.ts +38 -14
- package/src/Shared/Libs/jp2Decode.ts +48 -0
- package/src/Shared/Libs/mrz.utils.ts +27 -6
- package/src/Shared/Libs/mrzTransliteration.ts +16 -3
- package/src/Translation/Resources/en.ts +19 -0
- package/src/Translation/Resources/tr.ts +19 -0
- 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
|
|
112
|
-
lastName
|
|
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
|
-
|
|
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'
|
|
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__*/
|
|
439
|
+
}), /*#__PURE__*/_jsx(View, {
|
|
392
440
|
style: styles.idCardFields,
|
|
393
|
-
children:
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
53
|
-
|
|
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
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|