@trustchex/react-native-sdk 1.497.0 → 1.500.2
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/lib/module/Screens/Static/ResultScreen.js +104 -11
- package/lib/module/Shared/Components/EIDScanner.js +28 -3
- package/lib/module/Shared/Components/ErrorBoundary.js +144 -0
- package/lib/module/Shared/Components/IdentityDocumentCamera.constants.js +5 -0
- package/lib/module/Shared/Components/IdentityDocumentCamera.js +163 -4
- package/lib/module/Shared/Libs/mrz.utils.js +24 -1
- package/lib/module/Translation/Resources/en.js +13 -0
- package/lib/module/Translation/Resources/tr.js +13 -0
- package/lib/module/Trustchex.js +52 -49
- package/lib/module/version.js +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/ErrorBoundary.d.ts +17 -0
- package/lib/typescript/src/Shared/Components/ErrorBoundary.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.constants.d.ts +1 -0
- 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/Libs/mrz.utils.d.ts.map +1 -1
- package/lib/typescript/src/Translation/Resources/en.d.ts +13 -0
- package/lib/typescript/src/Translation/Resources/en.d.ts.map +1 -1
- package/lib/typescript/src/Translation/Resources/tr.d.ts +13 -0
- package/lib/typescript/src/Translation/Resources/tr.d.ts.map +1 -1
- package/lib/typescript/src/Trustchex.d.ts.map +1 -1
- package/lib/typescript/src/version.d.ts +1 -1
- package/package.json +1 -1
- package/src/Screens/Static/ResultScreen.tsx +145 -29
- package/src/Shared/Components/EIDScanner.tsx +38 -0
- package/src/Shared/Components/ErrorBoundary.tsx +151 -0
- package/src/Shared/Components/IdentityDocumentCamera.constants.ts +5 -0
- package/src/Shared/Components/IdentityDocumentCamera.tsx +220 -27
- package/src/Shared/Libs/mrz.utils.ts +27 -1
- package/src/Translation/Resources/en.ts +16 -0
- package/src/Translation/Resources/tr.ts +16 -0
- package/src/Trustchex.tsx +64 -59
- package/src/version.ts +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
|
4
|
-
import {
|
|
4
|
+
import { Image, Platform, SafeAreaView, ScrollView, StyleSheet, Text, View } from 'react-native';
|
|
5
5
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
6
6
|
import AppContext from "../../Shared/Contexts/AppContext.js";
|
|
7
7
|
import httpClient, { getSessionToken } from "../../Shared/Libs/http-client.js";
|
|
@@ -48,13 +48,27 @@ const collectDiagnosticImages = doc => {
|
|
|
48
48
|
import { trackError, trackFunnelStep, useScreenTracking } from "../../Shared/Libs/analytics.utils.js";
|
|
49
49
|
import { analyticsService } from "../../Shared/Services/AnalyticsService.js";
|
|
50
50
|
import { AnalyticsEventName, AnalyticsEventCategory } from "../../Shared/Types/analytics.types.js";
|
|
51
|
-
import { jsx as _jsx,
|
|
51
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
52
52
|
const ResultScreen = () => {
|
|
53
53
|
const appContext = useContext(AppContext);
|
|
54
54
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
55
55
|
const [progress, setProgress] = useState(0);
|
|
56
|
+
// Set when submit() fails — drives a recoverable error UI with a "Retry"
|
|
57
|
+
// button instead of wiping the whole flow back to the start (which forced a
|
|
58
|
+
// full re-scan + re-liveness on any transient final-step failure).
|
|
59
|
+
const [submitFailed, setSubmitFailed] = useState(false);
|
|
56
60
|
const navigationManagerRef = React.useRef(null);
|
|
57
61
|
const hasSubmittedRef = React.useRef(false);
|
|
62
|
+
// Tracks which submit steps already succeeded so a retry RESUMES from the
|
|
63
|
+
// failed step instead of re-running earlier ones. submit() is a chain of
|
|
64
|
+
// non-idempotent POSTs (consent/document/media/videos), so a naive full
|
|
65
|
+
// retry after a partial success could create duplicate server-side records.
|
|
66
|
+
const completedSteps = React.useRef({
|
|
67
|
+
consent: false,
|
|
68
|
+
document: false,
|
|
69
|
+
media: false,
|
|
70
|
+
verbalConsentVideos: false
|
|
71
|
+
});
|
|
58
72
|
const [shouldShowDemoData, setShouldShowDemoData] = useState(false);
|
|
59
73
|
const [deviceIdentifier, setDeviceIdentifier] = useState('');
|
|
60
74
|
const {
|
|
@@ -383,29 +397,48 @@ const ResultScreen = () => {
|
|
|
383
397
|
const submit = useCallback(async identificationInfo => {
|
|
384
398
|
try {
|
|
385
399
|
setIsSubmitting(true);
|
|
400
|
+
setSubmitFailed(false);
|
|
386
401
|
const identificationId = identificationInfo.identificationId;
|
|
387
402
|
if (!identificationId) {
|
|
388
403
|
throw new Error('IdentificationId not found');
|
|
389
404
|
}
|
|
390
405
|
const alreadyUploaded = !!identificationInfo.mediaUploadedDuringVideoCall;
|
|
391
406
|
const sessionKey = await runWithRetry(() => getSessionKey(apiUrl, appContext.identificationInfo.sessionId));
|
|
407
|
+
|
|
408
|
+
// createIdentification is effectively idempotent (creates/verifies the
|
|
409
|
+
// record), so it's safe to re-run on retry without a guard.
|
|
392
410
|
if (!alreadyUploaded) {
|
|
393
411
|
await runWithRetry(() => createIdentification(identificationId));
|
|
394
412
|
}
|
|
395
413
|
setProgress(20);
|
|
396
|
-
|
|
414
|
+
|
|
415
|
+
// Each POST below is guarded by completedSteps so a retry skips work
|
|
416
|
+
// that already committed server-side (avoiding duplicate records).
|
|
417
|
+
if (!completedSteps.current.consent) {
|
|
418
|
+
await runWithRetry(() => submitIdentificationConsent(identificationId, sessionKey));
|
|
419
|
+
completedSteps.current.consent = true;
|
|
420
|
+
}
|
|
397
421
|
setProgress(30);
|
|
398
422
|
if (!alreadyUploaded) {
|
|
399
423
|
const scannedIdentityDocument = identificationInfo.scannedDocument;
|
|
400
|
-
if (scannedIdentityDocument && scannedIdentityDocument.documentType !== 'UNKNOWN') {
|
|
424
|
+
if (!completedSteps.current.document && scannedIdentityDocument && scannedIdentityDocument.documentType !== 'UNKNOWN') {
|
|
401
425
|
await runWithRetry(() => submitIdentificationDocument(identificationId, scannedIdentityDocument, sessionKey));
|
|
426
|
+
completedSteps.current.document = true;
|
|
402
427
|
}
|
|
403
428
|
setProgress(40);
|
|
404
|
-
|
|
405
|
-
|
|
429
|
+
if (!completedSteps.current.media) {
|
|
430
|
+
const livenessDetection = identificationInfo.livenessDetection;
|
|
431
|
+
await runWithRetry(() => uploadIdentificationMedia(identificationId, scannedIdentityDocument, livenessDetection, false));
|
|
432
|
+
completedSteps.current.media = true;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
if (!completedSteps.current.verbalConsentVideos) {
|
|
436
|
+
await runWithRetry(() => uploadVerbalConsentVideos(identificationId, identificationInfo.verbalConsentVideos));
|
|
437
|
+
completedSteps.current.verbalConsentVideos = true;
|
|
406
438
|
}
|
|
407
|
-
await runWithRetry(() => uploadVerbalConsentVideos(identificationId, identificationInfo.verbalConsentVideos));
|
|
408
439
|
setProgress(90);
|
|
440
|
+
|
|
441
|
+
// finishIdentification is a PUT (idempotent) — always safe to re-run.
|
|
409
442
|
await runWithRetry(() => finishIdentification(identificationId, sessionKey));
|
|
410
443
|
setProgress(100);
|
|
411
444
|
setIsSubmitting(false);
|
|
@@ -422,16 +455,21 @@ const ResultScreen = () => {
|
|
|
422
455
|
// Track verification failure
|
|
423
456
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
424
457
|
trackError('VERIFICATION_SUBMISSION_FAILED', errorMessage, 'result_screen', 'critical', {
|
|
425
|
-
recoverable:
|
|
458
|
+
recoverable: true,
|
|
426
459
|
userAction: 'submit_verification'
|
|
427
460
|
}).catch(() => {});
|
|
428
461
|
analyticsService.trackEvent(AnalyticsEventName.VERIFICATION_FAILED, AnalyticsEventCategory.VERIFICATION, {
|
|
429
462
|
sessionId: appContext.identificationInfo.sessionId || '',
|
|
430
463
|
errorMessage
|
|
431
464
|
}).catch(() => {});
|
|
432
|
-
|
|
465
|
+
|
|
466
|
+
// Show a recoverable error UI with a Retry button instead of resetting
|
|
467
|
+
// the whole flow. The collected data is still intact in AppContext, and
|
|
468
|
+
// completedSteps ensures a retry resumes from the failed step. We notify
|
|
469
|
+
// the host via onError but do NOT tear the flow down — the user can
|
|
470
|
+
// retry without re-scanning their document or redoing liveness.
|
|
433
471
|
appContext.onError?.(t('resultScreen.submissionFailed'));
|
|
434
|
-
|
|
472
|
+
setSubmitFailed(true);
|
|
435
473
|
}
|
|
436
474
|
}, [createIdentification, apiUrl, uploadIdentificationMedia, uploadVerbalConsentVideos, finishIdentification, submitIdentificationDocument, submitIdentificationConsent, t, appContext]);
|
|
437
475
|
useEffect(() => {
|
|
@@ -464,7 +502,35 @@ const ResultScreen = () => {
|
|
|
464
502
|
style: styles.container,
|
|
465
503
|
children: /*#__PURE__*/_jsxs(View, {
|
|
466
504
|
style: styles.container,
|
|
467
|
-
children: [isSubmitting ? /*#__PURE__*/_jsxs(
|
|
505
|
+
children: [submitFailed && !isSubmitting ? /*#__PURE__*/_jsxs(View, {
|
|
506
|
+
style: styles.submitErrorContainer,
|
|
507
|
+
children: [/*#__PURE__*/_jsx(Text, {
|
|
508
|
+
style: styles.submitErrorTitle,
|
|
509
|
+
children: t('resultScreen.submissionFailedTitle')
|
|
510
|
+
}), /*#__PURE__*/_jsx(Text, {
|
|
511
|
+
style: styles.submitErrorMessage,
|
|
512
|
+
children: t('resultScreen.submissionFailedRetryHint')
|
|
513
|
+
}), /*#__PURE__*/_jsx(StyledButton, {
|
|
514
|
+
mode: "contained",
|
|
515
|
+
onPress: () => submit(appContext.identificationInfo),
|
|
516
|
+
style: styles.submitRetryButton,
|
|
517
|
+
children: t('resultScreen.retrySubmission')
|
|
518
|
+
}), /*#__PURE__*/_jsx(View, {
|
|
519
|
+
style: styles.submitReportButton,
|
|
520
|
+
children: /*#__PURE__*/_jsx(DiagnosticReportButton, {
|
|
521
|
+
baseUrl: appContext.baseUrl,
|
|
522
|
+
sessionId: appContext.identificationInfo?.sessionId,
|
|
523
|
+
scan: {
|
|
524
|
+
documentType: appContext.identificationInfo?.scannedDocument?.documentType,
|
|
525
|
+
dataSource: appContext.identificationInfo?.scannedDocument?.dataSource,
|
|
526
|
+
mrzText: appContext.identificationInfo?.scannedDocument?.mrzText,
|
|
527
|
+
mrzFields: appContext.identificationInfo?.scannedDocument?.mrzFields,
|
|
528
|
+
barcodeValue: appContext.identificationInfo?.scannedDocument?.barcodeValue
|
|
529
|
+
},
|
|
530
|
+
images: collectDiagnosticImages(appContext.identificationInfo?.scannedDocument)
|
|
531
|
+
})
|
|
532
|
+
})]
|
|
533
|
+
}) : isSubmitting ? /*#__PURE__*/_jsxs(_Fragment, {
|
|
468
534
|
children: [/*#__PURE__*/_jsx(Text, {
|
|
469
535
|
style: styles.sectionHeader,
|
|
470
536
|
children: t('resultScreen.submitting')
|
|
@@ -819,6 +885,33 @@ const styles = StyleSheet.create({
|
|
|
819
885
|
textAlign: 'center',
|
|
820
886
|
textTransform: 'capitalize'
|
|
821
887
|
},
|
|
888
|
+
submitErrorContainer: {
|
|
889
|
+
flex: 1,
|
|
890
|
+
justifyContent: 'center',
|
|
891
|
+
alignItems: 'center',
|
|
892
|
+
paddingHorizontal: 24
|
|
893
|
+
},
|
|
894
|
+
submitErrorTitle: {
|
|
895
|
+
fontSize: 20,
|
|
896
|
+
fontWeight: 'bold',
|
|
897
|
+
color: 'black',
|
|
898
|
+
textAlign: 'center',
|
|
899
|
+
marginBottom: 12
|
|
900
|
+
},
|
|
901
|
+
submitErrorMessage: {
|
|
902
|
+
fontSize: 16,
|
|
903
|
+
color: '#666',
|
|
904
|
+
textAlign: 'center',
|
|
905
|
+
lineHeight: 22,
|
|
906
|
+
marginBottom: 28
|
|
907
|
+
},
|
|
908
|
+
submitRetryButton: {
|
|
909
|
+
minWidth: 220
|
|
910
|
+
},
|
|
911
|
+
submitReportButton: {
|
|
912
|
+
marginTop: 16,
|
|
913
|
+
minWidth: 220
|
|
914
|
+
},
|
|
822
915
|
sectionText: {
|
|
823
916
|
fontSize: 18,
|
|
824
917
|
color: 'black',
|
|
@@ -15,6 +15,7 @@ import { normalizeFromMRZInfo } from "../Libs/documentDataNormalizer.js";
|
|
|
15
15
|
import { useTranslation } from 'react-i18next';
|
|
16
16
|
import AppContext from "../Contexts/AppContext.js";
|
|
17
17
|
import StyledButton from "./StyledButton.js";
|
|
18
|
+
import DiagnosticReportButton from "./DiagnosticReportButton.js";
|
|
18
19
|
import LottieView from 'lottie-react-native';
|
|
19
20
|
import { getLocalizedCountryName } from "../Libs/country-display.utils.js";
|
|
20
21
|
import { useKeepAwake } from "../Libs/native-keep-awake.utils.js";
|
|
@@ -87,6 +88,10 @@ const EIDScanner = ({
|
|
|
87
88
|
}
|
|
88
89
|
}, []);
|
|
89
90
|
const [isScanned, setIsScanned] = React.useState(false);
|
|
91
|
+
// Set when an NFC read fails — surfaces a "report issue" support channel next
|
|
92
|
+
// to the retry button (a failed chip read is the most common point a user
|
|
93
|
+
// genuinely needs help). Cleared when a new read starts.
|
|
94
|
+
const [hasScanFailed, setHasScanFailed] = React.useState(false);
|
|
90
95
|
const [hasGuideShown, setHasGuideShown] = React.useState(false);
|
|
91
96
|
const {
|
|
92
97
|
t,
|
|
@@ -144,6 +149,7 @@ const EIDScanner = ({
|
|
|
144
149
|
setAttemptNumber(currentAttempt);
|
|
145
150
|
setIsScanning(true);
|
|
146
151
|
setIsScanned(false);
|
|
152
|
+
setHasScanFailed(false);
|
|
147
153
|
setProgress(0);
|
|
148
154
|
setProgressStage('');
|
|
149
155
|
resetLastMessage();
|
|
@@ -260,6 +266,9 @@ const EIDScanner = ({
|
|
|
260
266
|
setDocumentName(builtName);
|
|
261
267
|
setIsScanned(true);
|
|
262
268
|
}
|
|
269
|
+
// Defensive: clear any prior-failure flag on a successful read so the
|
|
270
|
+
// report affordance can't linger if the UI returns to this state.
|
|
271
|
+
setHasScanFailed(false);
|
|
263
272
|
await trackEIDScanComplete(docType, scanDuration, currentAttempt).catch(() => {});
|
|
264
273
|
} else {
|
|
265
274
|
// eidReader resolved without throwing but produced no data. Without
|
|
@@ -279,6 +288,7 @@ const EIDScanner = ({
|
|
|
279
288
|
Alert.alert(t('general.error'), t('eidScannerScreen.invalidMRZFields'));
|
|
280
289
|
}
|
|
281
290
|
} catch (error) {
|
|
291
|
+
setHasScanFailed(true);
|
|
282
292
|
const scanDuration = Date.now() - startTime;
|
|
283
293
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
284
294
|
|
|
@@ -593,13 +603,25 @@ const EIDScanner = ({
|
|
|
593
603
|
})), hasNfc && isEnabled && isScanning && progress === 0 && /*#__PURE__*/_jsx(Text, {
|
|
594
604
|
style: styles.keepStillHint,
|
|
595
605
|
children: t('eidScannerScreen.keepStill')
|
|
596
|
-
}), hasNfc && isEnabled && !isScanning && !isScanned && /*#__PURE__*/
|
|
606
|
+
}), hasNfc && isEnabled && !isScanning && !isScanned && /*#__PURE__*/_jsxs(View, {
|
|
597
607
|
style: styles.buttonsContainer,
|
|
598
|
-
children: /*#__PURE__*/_jsx(StyledButton, {
|
|
608
|
+
children: [/*#__PURE__*/_jsx(StyledButton, {
|
|
599
609
|
mode: "contained",
|
|
600
610
|
onPress: readNFC,
|
|
601
611
|
children: t('eidScannerScreen.startScanning')
|
|
602
|
-
})
|
|
612
|
+
}), hasScanFailed && /*#__PURE__*/_jsx(View, {
|
|
613
|
+
style: styles.reportButtonWrapper,
|
|
614
|
+
children: /*#__PURE__*/_jsx(DiagnosticReportButton, {
|
|
615
|
+
baseUrl: appContext.baseUrl,
|
|
616
|
+
sessionId: appContext.identificationInfo?.sessionId,
|
|
617
|
+
scan: {
|
|
618
|
+
documentType: appContext.identificationInfo?.scannedDocument?.documentType,
|
|
619
|
+
dataSource: appContext.identificationInfo?.scannedDocument?.dataSource,
|
|
620
|
+
mrzText: appContext.identificationInfo?.scannedDocument?.mrzText,
|
|
621
|
+
mrzFields: appContext.identificationInfo?.scannedDocument?.mrzFields
|
|
622
|
+
}
|
|
623
|
+
})
|
|
624
|
+
})]
|
|
603
625
|
}), hasNfc && isEnabled && isScanned && !documentMRZInfo && /*#__PURE__*/_jsx(View, {
|
|
604
626
|
style: styles.buttonsContainer,
|
|
605
627
|
children: /*#__PURE__*/_jsx(StyledButton, {
|
|
@@ -679,6 +701,9 @@ const styles = StyleSheet.create({
|
|
|
679
701
|
flexDirection: 'column',
|
|
680
702
|
gap: 10
|
|
681
703
|
},
|
|
704
|
+
reportButtonWrapper: {
|
|
705
|
+
marginTop: 4
|
|
706
|
+
},
|
|
682
707
|
mainText: {
|
|
683
708
|
fontSize: 20,
|
|
684
709
|
fontWeight: 'bold',
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import React, { useContext } from 'react';
|
|
4
|
+
import { View, Text, StyleSheet } from 'react-native';
|
|
5
|
+
import { useTranslation } from 'react-i18next';
|
|
6
|
+
import { useTheme } from "../Contexts/ThemeContext.js";
|
|
7
|
+
import AppContext from "../Contexts/AppContext.js";
|
|
8
|
+
import StyledButton from "./StyledButton.js";
|
|
9
|
+
import { trackError } from "../Libs/analytics.utils.js";
|
|
10
|
+
import { logError } from "../Libs/debug.utils.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Themed fallback UI for an unrecoverable render error. A functional child so it
|
|
14
|
+
* can use the theme + translation hooks (the boundary itself must be a class).
|
|
15
|
+
*/
|
|
16
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
17
|
+
function ErrorFallback({
|
|
18
|
+
onRetry
|
|
19
|
+
}) {
|
|
20
|
+
const {
|
|
21
|
+
colors
|
|
22
|
+
} = useTheme();
|
|
23
|
+
const {
|
|
24
|
+
t
|
|
25
|
+
} = useTranslation();
|
|
26
|
+
return /*#__PURE__*/_jsx(View, {
|
|
27
|
+
style: [styles.container, {
|
|
28
|
+
backgroundColor: colors.background
|
|
29
|
+
}],
|
|
30
|
+
children: /*#__PURE__*/_jsxs(View, {
|
|
31
|
+
style: styles.content,
|
|
32
|
+
children: [/*#__PURE__*/_jsx(Text, {
|
|
33
|
+
style: [styles.title, {
|
|
34
|
+
color: colors.text
|
|
35
|
+
}],
|
|
36
|
+
children: t('errorBoundary.title')
|
|
37
|
+
}), /*#__PURE__*/_jsx(Text, {
|
|
38
|
+
style: [styles.message, {
|
|
39
|
+
color: colors.textSecondary
|
|
40
|
+
}],
|
|
41
|
+
children: t('errorBoundary.message')
|
|
42
|
+
}), /*#__PURE__*/_jsx(StyledButton, {
|
|
43
|
+
mode: "contained",
|
|
44
|
+
onPress: onRetry,
|
|
45
|
+
style: styles.button,
|
|
46
|
+
children: t('errorBoundary.tryAgain')
|
|
47
|
+
})]
|
|
48
|
+
})
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
class ErrorBoundaryInner extends React.Component {
|
|
52
|
+
state = {
|
|
53
|
+
hasError: false,
|
|
54
|
+
resetKey: 0
|
|
55
|
+
};
|
|
56
|
+
static getDerivedStateFromError() {
|
|
57
|
+
return {
|
|
58
|
+
hasError: true
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
componentDidCatch(error, info) {
|
|
62
|
+
logError('ErrorBoundary', 'Caught render error', error);
|
|
63
|
+
// Telemetry — best-effort, never let reporting throw.
|
|
64
|
+
trackError('SDK_RENDER_ERROR', error.message, 'error_boundary', 'critical', {
|
|
65
|
+
error,
|
|
66
|
+
component: 'error_boundary',
|
|
67
|
+
recoverable: true,
|
|
68
|
+
additionalData: {
|
|
69
|
+
componentStack: info.componentStack ?? ''
|
|
70
|
+
}
|
|
71
|
+
}).catch(() => {});
|
|
72
|
+
// Notify the host integration that the SDK hit an unrecoverable error.
|
|
73
|
+
this.props.onError?.(error.message);
|
|
74
|
+
}
|
|
75
|
+
handleRetry = () => {
|
|
76
|
+
// Bump resetKey so the children below re-mount with a fresh key — a real
|
|
77
|
+
// recovery, not just a re-render of the same (possibly still-broken) tree.
|
|
78
|
+
this.setState(prev => ({
|
|
79
|
+
hasError: false,
|
|
80
|
+
resetKey: prev.resetKey + 1
|
|
81
|
+
}));
|
|
82
|
+
};
|
|
83
|
+
render() {
|
|
84
|
+
if (this.state.hasError) {
|
|
85
|
+
return /*#__PURE__*/_jsx(ErrorFallback, {
|
|
86
|
+
onRetry: this.handleRetry
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
return /*#__PURE__*/_jsx(React.Fragment, {
|
|
90
|
+
children: this.props.children
|
|
91
|
+
}, this.state.resetKey);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Catches unexpected render/runtime errors anywhere in the SDK's screen tree so
|
|
97
|
+
* an unhandled throw shows a recoverable fallback instead of unmounting to a
|
|
98
|
+
* white screen / crashing the host app. The SDK was otherwise entirely
|
|
99
|
+
* functional with no crash protection.
|
|
100
|
+
*
|
|
101
|
+
* Placed inside ThemeProvider + AppContext.Provider so the fallback can theme
|
|
102
|
+
* itself and notify the host via `onError`. This thin functional wrapper reads
|
|
103
|
+
* `onError` from context (a hook, which the class can't use) and passes it to
|
|
104
|
+
* the class as a prop. "Try Again" re-mounts the subtree (the navigator returns
|
|
105
|
+
* to its initial route).
|
|
106
|
+
*/
|
|
107
|
+
export default function ErrorBoundary({
|
|
108
|
+
children
|
|
109
|
+
}) {
|
|
110
|
+
const {
|
|
111
|
+
onError
|
|
112
|
+
} = useContext(AppContext);
|
|
113
|
+
return /*#__PURE__*/_jsx(ErrorBoundaryInner, {
|
|
114
|
+
onError: onError,
|
|
115
|
+
children: children
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
const styles = StyleSheet.create({
|
|
119
|
+
container: {
|
|
120
|
+
flex: 1,
|
|
121
|
+
justifyContent: 'center',
|
|
122
|
+
alignItems: 'center',
|
|
123
|
+
padding: 24
|
|
124
|
+
},
|
|
125
|
+
content: {
|
|
126
|
+
alignItems: 'center',
|
|
127
|
+
maxWidth: 360
|
|
128
|
+
},
|
|
129
|
+
title: {
|
|
130
|
+
fontSize: 22,
|
|
131
|
+
fontWeight: 'bold',
|
|
132
|
+
textAlign: 'center',
|
|
133
|
+
marginBottom: 12
|
|
134
|
+
},
|
|
135
|
+
message: {
|
|
136
|
+
fontSize: 16,
|
|
137
|
+
textAlign: 'center',
|
|
138
|
+
lineHeight: 22,
|
|
139
|
+
marginBottom: 28
|
|
140
|
+
},
|
|
141
|
+
button: {
|
|
142
|
+
minWidth: 200
|
|
143
|
+
}
|
|
144
|
+
});
|
|
@@ -14,6 +14,11 @@ export const MIN_MLI_FACE_SIZE_PERCENT = 0.01; // MLI (Multi Layer Image): min 1
|
|
|
14
14
|
|
|
15
15
|
export const REQUIRED_CONSISTENT_MRZ_READS = 2;
|
|
16
16
|
export const REQUIRED_CONSISTENT_DOCTYPE_DETECTIONS = 3;
|
|
17
|
+
|
|
18
|
+
// After this long on a single scan step without completing it, surface the
|
|
19
|
+
// "Having trouble?" prompt so the user isn't stranded on a live camera with no
|
|
20
|
+
// timeout, retry, or help. Per-step (re-armed on each step transition).
|
|
21
|
+
export const HELP_PROMPT_DELAY_MS = 25000;
|
|
17
22
|
export const SIGNATURE_REGEX = /s[gi]g?n[au]?t[u]?r|imz[a]s?/i;
|
|
18
23
|
export const SIGNATURE_TEXT_REGEX = /signature|imza|İmza/i;
|
|
19
24
|
export const MRZ_BLOCK_PATTERN = /[A-Z0-9<]{8,}.*</i;
|
|
@@ -2,7 +2,7 @@
|
|
|
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, Animated } from 'react-native';
|
|
5
|
+
import { View, StyleSheet, Text as TextView, Platform, StatusBar, Vibration, Linking, ActivityIndicator, Animated, TouchableOpacity } 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';
|
|
@@ -16,6 +16,7 @@ import { debugLog, logError, isDebugEnabled } from "../Libs/debug.utils.js";
|
|
|
16
16
|
import { diagnostics } from "../Libs/diagnostics.js";
|
|
17
17
|
import LottieView from 'lottie-react-native';
|
|
18
18
|
import StyledButton from "./StyledButton.js";
|
|
19
|
+
import DiagnosticReportButton from "./DiagnosticReportButton.js";
|
|
19
20
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
20
21
|
import { speak, resetLastMessage } from "../Libs/tts.utils.js";
|
|
21
22
|
import AppContext from "../Contexts/AppContext.js";
|
|
@@ -23,7 +24,7 @@ import { useTheme } from "../Contexts/ThemeContext.js";
|
|
|
23
24
|
import DebugOverlay, { TestModePanel } from "./DebugOverlay.js";
|
|
24
25
|
import { getStatusMessage, getFrameToScreenTransform, transformBoundsToScreen, getScanAreaBounds, angleBetweenPoints, detectDocumentType, determineDocumentTypeToSet, areMRZFieldsEqual, hasRequiredMRZFields, validateFacePosition, isPassportDocumentCode } from "./IdentityDocumentCamera.utils.js";
|
|
25
26
|
import { handlePassportFlow, handleIDFrontFlow, handleIDBackFlow, getNextStepAfterHologram } from "./IdentityDocumentCamera.flows.js";
|
|
26
|
-
import { HOLOGRAM_IMAGE_COUNT, HOLOGRAM_DETECTION_THRESHOLD, HOLOGRAM_DETECTION_RETRY_COUNT, HOLOGRAM_CAPTURE_INTERVAL, HOLOGRAM_MAX_FRAMES_WITHOUT_FACE, MIN_BRIGHTNESS_THRESHOLD, MAX_BRIGHTNESS_THRESHOLD, FACE_EDGE_MARGIN_PERCENT, MAX_CONSECUTIVE_QUALITY_FAILURES, REQUIRED_CONSISTENT_DOCTYPE_DETECTIONS, SIGNATURE_TEXT_REGEX, MRZ_BLOCK_PATTERN, MIN_CARD_FACE_SIZE_PERCENT } from "./IdentityDocumentCamera.constants.js";
|
|
27
|
+
import { HOLOGRAM_IMAGE_COUNT, HOLOGRAM_DETECTION_THRESHOLD, HOLOGRAM_DETECTION_RETRY_COUNT, HOLOGRAM_CAPTURE_INTERVAL, HOLOGRAM_MAX_FRAMES_WITHOUT_FACE, MIN_BRIGHTNESS_THRESHOLD, MAX_BRIGHTNESS_THRESHOLD, FACE_EDGE_MARGIN_PERCENT, MAX_CONSECUTIVE_QUALITY_FAILURES, REQUIRED_CONSISTENT_DOCTYPE_DETECTIONS, HELP_PROMPT_DELAY_MS, SIGNATURE_TEXT_REGEX, MRZ_BLOCK_PATTERN, MIN_CARD_FACE_SIZE_PERCENT } from "./IdentityDocumentCamera.constants.js";
|
|
27
28
|
|
|
28
29
|
// Re-export types for backward compatibility
|
|
29
30
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
@@ -77,6 +78,13 @@ const IdentityDocumentCamera = ({
|
|
|
77
78
|
// cleared when a frame passes the brightness check (a fresh, glare-free frame).
|
|
78
79
|
const [isGlareDetected, setIsGlareDetected] = useState(false);
|
|
79
80
|
const [hasGuideShown, setHasGuideShown] = useState(false);
|
|
81
|
+
// "Having trouble?" escape: if a scan step doesn't complete within
|
|
82
|
+
// HELP_PROMPT_DELAY_MS the user is otherwise stuck on a live camera forever
|
|
83
|
+
// (no timeout/retry/help). We surface an unobtrusive prompt, then an opt-in
|
|
84
|
+
// help sheet with tips / retry / torch.
|
|
85
|
+
const [showHelpPrompt, setShowHelpPrompt] = useState(false);
|
|
86
|
+
const [helpSheetVisible, setHelpSheetVisible] = useState(false);
|
|
87
|
+
const stepStartedAt = useRef(Date.now());
|
|
80
88
|
const [status, setStatus] = useState('SEARCHING');
|
|
81
89
|
// True once the multi-frame voted MRZ consensus is STABLE + valid (the same
|
|
82
90
|
// reading matched across `stabilityTarget` consecutive frames, with a clear
|
|
@@ -285,6 +293,25 @@ const IdentityDocumentCamera = ({
|
|
|
285
293
|
}
|
|
286
294
|
}, [status]);
|
|
287
295
|
|
|
296
|
+
// "Having trouble?" stuck-detection. The scan is frame-driven with no
|
|
297
|
+
// wall-clock timeout, so a document that never reads leaves the user stranded.
|
|
298
|
+
// Re-arm per step (keyed on nextStep) and per scan-start (hasGuideShown); show
|
|
299
|
+
// the prompt once the current step has been open longer than the threshold.
|
|
300
|
+
// Never arm on the terminal COMPLETED step.
|
|
301
|
+
useEffect(() => {
|
|
302
|
+
setShowHelpPrompt(false);
|
|
303
|
+
stepStartedAt.current = Date.now();
|
|
304
|
+
if (!hasGuideShown || nextStep === 'COMPLETED') {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
const interval = setInterval(() => {
|
|
308
|
+
if (Date.now() - stepStartedAt.current >= HELP_PROMPT_DELAY_MS) {
|
|
309
|
+
setShowHelpPrompt(true);
|
|
310
|
+
}
|
|
311
|
+
}, 1000);
|
|
312
|
+
return () => clearInterval(interval);
|
|
313
|
+
}, [nextStep, hasGuideShown]);
|
|
314
|
+
|
|
288
315
|
// Disable face detection when scanning back side (no face expected, avoids false positives)
|
|
289
316
|
useEffect(() => {
|
|
290
317
|
if (nextStep === 'SCAN_ID_BACK') {
|
|
@@ -1852,9 +1879,16 @@ const IdentityDocumentCamera = ({
|
|
|
1852
1879
|
style: [styles.topZone, {
|
|
1853
1880
|
paddingTop: insets.top
|
|
1854
1881
|
}],
|
|
1855
|
-
children: [nextStep !== 'COMPLETED' && status !== 'SCANNED' &&
|
|
1882
|
+
children: [nextStep !== 'COMPLETED' && status !== 'SCANNED' && /*#__PURE__*/_jsx(TextView, {
|
|
1856
1883
|
style: styles.stepIndicator,
|
|
1857
|
-
children: nextStep === 'SCAN_ID_FRONT_OR_PASSPORT' ?
|
|
1884
|
+
children: nextStep === 'SCAN_ID_FRONT_OR_PASSPORT' ?
|
|
1885
|
+
// Show "Front side • 1/N" from the START — even before the
|
|
1886
|
+
// document type is recognized — so the user immediately knows
|
|
1887
|
+
// to present the photo side and how many steps remain. Until
|
|
1888
|
+
// the type is known we assume the ID-card path (the common
|
|
1889
|
+
// case); the total corrects to the passport count once a
|
|
1890
|
+
// passport is detected.
|
|
1891
|
+
`${t('identityDocumentCamera.frontSide')} • ${t('identityDocumentCamera.stepProgress', {
|
|
1858
1892
|
current: 1,
|
|
1859
1893
|
total: onlyMRZScan ? detectedDocumentType === 'PASSPORT' ? 1 : 2 : detectedDocumentType === 'PASSPORT' ? 2 : 3
|
|
1860
1894
|
})}` : nextStep === 'SCAN_HOLOGRAM' ? `${t('identityDocumentCamera.hologramCheck')} • ${t('identityDocumentCamera.stepProgress', {
|
|
@@ -1864,6 +1898,12 @@ const IdentityDocumentCamera = ({
|
|
|
1864
1898
|
current: 3,
|
|
1865
1899
|
total: 3
|
|
1866
1900
|
})}` : ''
|
|
1901
|
+
}), nextStep === 'SCAN_HOLOGRAM' && status !== 'SCANNED' && /*#__PURE__*/_jsx(TextView, {
|
|
1902
|
+
style: styles.hologramProgress,
|
|
1903
|
+
children: t('identityDocumentCamera.hologramProgress', {
|
|
1904
|
+
current: hologramImageCount,
|
|
1905
|
+
total: HOLOGRAM_IMAGE_COUNT
|
|
1906
|
+
})
|
|
1867
1907
|
}), /*#__PURE__*/_jsx(AnimatedText, {
|
|
1868
1908
|
style: [styles.topZoneText,
|
|
1869
1909
|
// Priority order for coloring (later styles override earlier ones)
|
|
@@ -1935,6 +1975,68 @@ const IdentityDocumentCamera = ({
|
|
|
1935
1975
|
elementsOutsideScanArea: elementsOutsideScanArea
|
|
1936
1976
|
}), testMode && testModeData && /*#__PURE__*/_jsx(TestModePanel, {
|
|
1937
1977
|
mrzText: testModeData.mrzText
|
|
1978
|
+
}), showHelpPrompt && !helpSheetVisible && nextStep !== 'COMPLETED' && /*#__PURE__*/_jsx(TouchableOpacity, {
|
|
1979
|
+
style: styles.helpPrompt,
|
|
1980
|
+
accessibilityRole: "button",
|
|
1981
|
+
onPress: () => setHelpSheetVisible(true),
|
|
1982
|
+
children: /*#__PURE__*/_jsx(TextView, {
|
|
1983
|
+
style: styles.helpPromptText,
|
|
1984
|
+
children: t('identityDocumentCamera.havingTrouble')
|
|
1985
|
+
})
|
|
1986
|
+
}), helpSheetVisible && /*#__PURE__*/_jsx(View, {
|
|
1987
|
+
style: styles.helpBackdrop,
|
|
1988
|
+
children: /*#__PURE__*/_jsxs(View, {
|
|
1989
|
+
style: styles.helpCard,
|
|
1990
|
+
children: [/*#__PURE__*/_jsx(TextView, {
|
|
1991
|
+
style: styles.helpTitle,
|
|
1992
|
+
children: t('identityDocumentCamera.troubleTitle')
|
|
1993
|
+
}), /*#__PURE__*/_jsx(TextView, {
|
|
1994
|
+
style: styles.helpTip,
|
|
1995
|
+
children: `• ${t('identityDocumentCamera.guidePoint1')}`
|
|
1996
|
+
}), /*#__PURE__*/_jsx(TextView, {
|
|
1997
|
+
style: styles.helpTip,
|
|
1998
|
+
children: `• ${t('identityDocumentCamera.guidePoint2')}`
|
|
1999
|
+
}), /*#__PURE__*/_jsx(TextView, {
|
|
2000
|
+
style: styles.helpTip,
|
|
2001
|
+
children: `• ${t('identityDocumentCamera.guidePoint3')}`
|
|
2002
|
+
}), isBrightnessLow && nextStep !== 'SCAN_HOLOGRAM' && /*#__PURE__*/_jsx(StyledButton, {
|
|
2003
|
+
mode: "outlined",
|
|
2004
|
+
onPress: () => {
|
|
2005
|
+
setIsTorchOn(!isTorchOnRef.current);
|
|
2006
|
+
},
|
|
2007
|
+
style: styles.helpButton,
|
|
2008
|
+
children: isTorchOn ? t('identityDocumentCamera.turnOffTorch') : t('identityDocumentCamera.turnOnTorch')
|
|
2009
|
+
}), /*#__PURE__*/_jsx(StyledButton, {
|
|
2010
|
+
mode: "contained",
|
|
2011
|
+
onPress: () => {
|
|
2012
|
+
// Full restart: send the flow back to the first step (so a
|
|
2013
|
+
// stuck back/hologram step truly restarts, matching the label)
|
|
2014
|
+
// and re-show the guide, which deactivates + resets scan state
|
|
2015
|
+
// via the focus effect.
|
|
2016
|
+
setHelpSheetVisible(false);
|
|
2017
|
+
setShowHelpPrompt(false);
|
|
2018
|
+
setNextStep('SCAN_ID_FRONT_OR_PASSPORT');
|
|
2019
|
+
setDetectedDocumentType('UNKNOWN');
|
|
2020
|
+
setHasGuideShown(false);
|
|
2021
|
+
},
|
|
2022
|
+
style: styles.helpButton,
|
|
2023
|
+
children: t('identityDocumentCamera.troubleRetry')
|
|
2024
|
+
}), /*#__PURE__*/_jsx(View, {
|
|
2025
|
+
style: styles.helpButton,
|
|
2026
|
+
children: /*#__PURE__*/_jsx(DiagnosticReportButton, {
|
|
2027
|
+
baseUrl: appContext.baseUrl,
|
|
2028
|
+
sessionId: appContext.identificationInfo?.sessionId,
|
|
2029
|
+
scan: {
|
|
2030
|
+
documentType: detectedDocumentType
|
|
2031
|
+
}
|
|
2032
|
+
})
|
|
2033
|
+
}), /*#__PURE__*/_jsx(StyledButton, {
|
|
2034
|
+
mode: "text",
|
|
2035
|
+
onPress: () => setHelpSheetVisible(false),
|
|
2036
|
+
style: styles.helpButton,
|
|
2037
|
+
children: t('identityDocumentCamera.troubleDismiss')
|
|
2038
|
+
})]
|
|
2039
|
+
})
|
|
1938
2040
|
})]
|
|
1939
2041
|
})]
|
|
1940
2042
|
});
|
|
@@ -1988,6 +2090,13 @@ const styles = StyleSheet.create({
|
|
|
1988
2090
|
fontWeight: '500',
|
|
1989
2091
|
marginBottom: 4
|
|
1990
2092
|
},
|
|
2093
|
+
hologramProgress: {
|
|
2094
|
+
color: '#4CAF50',
|
|
2095
|
+
fontSize: 14,
|
|
2096
|
+
textAlign: 'center',
|
|
2097
|
+
fontWeight: '600',
|
|
2098
|
+
marginBottom: 4
|
|
2099
|
+
},
|
|
1991
2100
|
topZoneText: {
|
|
1992
2101
|
color: 'white',
|
|
1993
2102
|
fontSize: 20,
|
|
@@ -2023,6 +2132,56 @@ const styles = StyleSheet.create({
|
|
|
2023
2132
|
bottom: '36%',
|
|
2024
2133
|
backgroundColor: '#00000099'
|
|
2025
2134
|
},
|
|
2135
|
+
helpPrompt: {
|
|
2136
|
+
position: 'absolute',
|
|
2137
|
+
bottom: 40,
|
|
2138
|
+
alignSelf: 'center',
|
|
2139
|
+
backgroundColor: 'rgba(0,0,0,0.65)',
|
|
2140
|
+
paddingVertical: 10,
|
|
2141
|
+
paddingHorizontal: 20,
|
|
2142
|
+
borderRadius: 22
|
|
2143
|
+
},
|
|
2144
|
+
helpPromptText: {
|
|
2145
|
+
color: 'white',
|
|
2146
|
+
fontSize: 15,
|
|
2147
|
+
fontWeight: '600'
|
|
2148
|
+
},
|
|
2149
|
+
helpBackdrop: {
|
|
2150
|
+
position: 'absolute',
|
|
2151
|
+
top: 0,
|
|
2152
|
+
left: 0,
|
|
2153
|
+
right: 0,
|
|
2154
|
+
bottom: 0,
|
|
2155
|
+
backgroundColor: 'rgba(0,0,0,0.55)',
|
|
2156
|
+
alignItems: 'center',
|
|
2157
|
+
justifyContent: 'center',
|
|
2158
|
+
padding: 24,
|
|
2159
|
+
zIndex: 10
|
|
2160
|
+
},
|
|
2161
|
+
helpCard: {
|
|
2162
|
+
width: '100%',
|
|
2163
|
+
maxWidth: 340,
|
|
2164
|
+
backgroundColor: 'white',
|
|
2165
|
+
borderRadius: 16,
|
|
2166
|
+
paddingVertical: 24,
|
|
2167
|
+
paddingHorizontal: 20
|
|
2168
|
+
},
|
|
2169
|
+
helpTitle: {
|
|
2170
|
+
fontSize: 18,
|
|
2171
|
+
fontWeight: 'bold',
|
|
2172
|
+
color: '#111827',
|
|
2173
|
+
textAlign: 'center',
|
|
2174
|
+
marginBottom: 16
|
|
2175
|
+
},
|
|
2176
|
+
helpTip: {
|
|
2177
|
+
fontSize: 15,
|
|
2178
|
+
color: '#374151',
|
|
2179
|
+
lineHeight: 22,
|
|
2180
|
+
marginBottom: 8
|
|
2181
|
+
},
|
|
2182
|
+
helpButton: {
|
|
2183
|
+
marginTop: 12
|
|
2184
|
+
},
|
|
2026
2185
|
bottomZone: {
|
|
2027
2186
|
position: 'absolute',
|
|
2028
2187
|
top: '64%',
|