@trustchex/react-native-sdk 1.496.0 → 1.500.1
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/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/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/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
|
@@ -6,7 +6,6 @@ import React, {
|
|
|
6
6
|
useState,
|
|
7
7
|
} from 'react';
|
|
8
8
|
import {
|
|
9
|
-
Alert,
|
|
10
9
|
Image,
|
|
11
10
|
Platform,
|
|
12
11
|
SafeAreaView,
|
|
@@ -80,8 +79,22 @@ const ResultScreen = () => {
|
|
|
80
79
|
const appContext = useContext(AppContext);
|
|
81
80
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
82
81
|
const [progress, setProgress] = useState(0);
|
|
82
|
+
// Set when submit() fails — drives a recoverable error UI with a "Retry"
|
|
83
|
+
// button instead of wiping the whole flow back to the start (which forced a
|
|
84
|
+
// full re-scan + re-liveness on any transient final-step failure).
|
|
85
|
+
const [submitFailed, setSubmitFailed] = useState(false);
|
|
83
86
|
const navigationManagerRef = React.useRef<NavigationManagerRef>(null);
|
|
84
87
|
const hasSubmittedRef = React.useRef(false);
|
|
88
|
+
// Tracks which submit steps already succeeded so a retry RESUMES from the
|
|
89
|
+
// failed step instead of re-running earlier ones. submit() is a chain of
|
|
90
|
+
// non-idempotent POSTs (consent/document/media/videos), so a naive full
|
|
91
|
+
// retry after a partial success could create duplicate server-side records.
|
|
92
|
+
const completedSteps = React.useRef({
|
|
93
|
+
consent: false,
|
|
94
|
+
document: false,
|
|
95
|
+
media: false,
|
|
96
|
+
verbalConsentVideos: false,
|
|
97
|
+
});
|
|
85
98
|
const [shouldShowDemoData, setShouldShowDemoData] = useState(false);
|
|
86
99
|
const [deviceIdentifier, setDeviceIdentifier] = useState<string>('');
|
|
87
100
|
const { t, i18n } = useTranslation();
|
|
@@ -266,8 +279,10 @@ const ResultScreen = () => {
|
|
|
266
279
|
type: rawMrzFields.documentCode,
|
|
267
280
|
name: rawMrzFields.firstName,
|
|
268
281
|
surname: rawMrzFields.lastName,
|
|
269
|
-
|
|
270
|
-
|
|
282
|
+
displayName:
|
|
283
|
+
docName?.source === 'dg11' ? docName.displayFirst : undefined,
|
|
284
|
+
displaySurname:
|
|
285
|
+
docName?.source === 'dg11' ? docName.displayLast : undefined,
|
|
271
286
|
nameSource: docName?.source === 'dg11' ? docName.source : undefined,
|
|
272
287
|
gender: getGenderEnumType(rawMrzFields.sex),
|
|
273
288
|
number: rawMrzFields.documentNumber,
|
|
@@ -542,6 +557,7 @@ const ResultScreen = () => {
|
|
|
542
557
|
async (identificationInfo: IdentificationInfo) => {
|
|
543
558
|
try {
|
|
544
559
|
setIsSubmitting(true);
|
|
560
|
+
setSubmitFailed(false);
|
|
545
561
|
const identificationId = identificationInfo.identificationId;
|
|
546
562
|
if (!identificationId) {
|
|
547
563
|
throw new Error('IdentificationId not found');
|
|
@@ -554,19 +570,27 @@ const ResultScreen = () => {
|
|
|
554
570
|
getSessionKey(apiUrl, appContext.identificationInfo.sessionId)
|
|
555
571
|
);
|
|
556
572
|
|
|
573
|
+
// createIdentification is effectively idempotent (creates/verifies the
|
|
574
|
+
// record), so it's safe to re-run on retry without a guard.
|
|
557
575
|
if (!alreadyUploaded) {
|
|
558
576
|
await runWithRetry(() => createIdentification(identificationId));
|
|
559
577
|
}
|
|
560
578
|
setProgress(20);
|
|
561
579
|
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
)
|
|
580
|
+
// Each POST below is guarded by completedSteps so a retry skips work
|
|
581
|
+
// that already committed server-side (avoiding duplicate records).
|
|
582
|
+
if (!completedSteps.current.consent) {
|
|
583
|
+
await runWithRetry(() =>
|
|
584
|
+
submitIdentificationConsent(identificationId, sessionKey)
|
|
585
|
+
);
|
|
586
|
+
completedSteps.current.consent = true;
|
|
587
|
+
}
|
|
565
588
|
setProgress(30);
|
|
566
589
|
|
|
567
590
|
if (!alreadyUploaded) {
|
|
568
591
|
const scannedIdentityDocument = identificationInfo.scannedDocument;
|
|
569
592
|
if (
|
|
593
|
+
!completedSteps.current.document &&
|
|
570
594
|
scannedIdentityDocument &&
|
|
571
595
|
scannedIdentityDocument.documentType !== 'UNKNOWN'
|
|
572
596
|
) {
|
|
@@ -577,28 +601,36 @@ const ResultScreen = () => {
|
|
|
577
601
|
sessionKey
|
|
578
602
|
)
|
|
579
603
|
);
|
|
604
|
+
completedSteps.current.document = true;
|
|
580
605
|
}
|
|
581
606
|
setProgress(40);
|
|
582
607
|
|
|
583
|
-
|
|
608
|
+
if (!completedSteps.current.media) {
|
|
609
|
+
const livenessDetection = identificationInfo.livenessDetection;
|
|
610
|
+
await runWithRetry(() =>
|
|
611
|
+
uploadIdentificationMedia(
|
|
612
|
+
identificationId,
|
|
613
|
+
scannedIdentityDocument,
|
|
614
|
+
livenessDetection,
|
|
615
|
+
false
|
|
616
|
+
)
|
|
617
|
+
);
|
|
618
|
+
completedSteps.current.media = true;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
if (!completedSteps.current.verbalConsentVideos) {
|
|
584
623
|
await runWithRetry(() =>
|
|
585
|
-
|
|
624
|
+
uploadVerbalConsentVideos(
|
|
586
625
|
identificationId,
|
|
587
|
-
|
|
588
|
-
livenessDetection,
|
|
589
|
-
false
|
|
626
|
+
identificationInfo.verbalConsentVideos
|
|
590
627
|
)
|
|
591
628
|
);
|
|
629
|
+
completedSteps.current.verbalConsentVideos = true;
|
|
592
630
|
}
|
|
593
|
-
|
|
594
|
-
await runWithRetry(() =>
|
|
595
|
-
uploadVerbalConsentVideos(
|
|
596
|
-
identificationId,
|
|
597
|
-
identificationInfo.verbalConsentVideos
|
|
598
|
-
)
|
|
599
|
-
);
|
|
600
631
|
setProgress(90);
|
|
601
632
|
|
|
633
|
+
// finishIdentification is a PUT (idempotent) — always safe to re-run.
|
|
602
634
|
await runWithRetry(() =>
|
|
603
635
|
finishIdentification(identificationId, sessionKey)
|
|
604
636
|
);
|
|
@@ -632,7 +664,7 @@ const ResultScreen = () => {
|
|
|
632
664
|
errorMessage,
|
|
633
665
|
'result_screen',
|
|
634
666
|
'critical',
|
|
635
|
-
{ recoverable:
|
|
667
|
+
{ recoverable: true, userAction: 'submit_verification' }
|
|
636
668
|
).catch(() => {});
|
|
637
669
|
analyticsService
|
|
638
670
|
.trackEvent(
|
|
@@ -645,9 +677,13 @@ const ResultScreen = () => {
|
|
|
645
677
|
)
|
|
646
678
|
.catch(() => {});
|
|
647
679
|
|
|
648
|
-
|
|
680
|
+
// Show a recoverable error UI with a Retry button instead of resetting
|
|
681
|
+
// the whole flow. The collected data is still intact in AppContext, and
|
|
682
|
+
// completedSteps ensures a retry resumes from the failed step. We notify
|
|
683
|
+
// the host via onError but do NOT tear the flow down — the user can
|
|
684
|
+
// retry without re-scanning their document or redoing liveness.
|
|
649
685
|
appContext.onError?.(t('resultScreen.submissionFailed'));
|
|
650
|
-
|
|
686
|
+
setSubmitFailed(true);
|
|
651
687
|
}
|
|
652
688
|
},
|
|
653
689
|
[
|
|
@@ -680,7 +716,8 @@ const ResultScreen = () => {
|
|
|
680
716
|
submit,
|
|
681
717
|
]);
|
|
682
718
|
|
|
683
|
-
const { isDemoSession, isTestVideoSession, skipSuccessScreen, onCompleted } =
|
|
719
|
+
const { isDemoSession, isTestVideoSession, skipSuccessScreen, onCompleted } =
|
|
720
|
+
appContext;
|
|
684
721
|
useEffect(() => {
|
|
685
722
|
if (progress === 100 && !isDemoSession && !isTestVideoSession) {
|
|
686
723
|
if (skipSuccessScreen) {
|
|
@@ -694,12 +731,60 @@ const ResultScreen = () => {
|
|
|
694
731
|
return () => clearTimeout(timer);
|
|
695
732
|
}
|
|
696
733
|
}
|
|
697
|
-
}, [
|
|
734
|
+
}, [
|
|
735
|
+
progress,
|
|
736
|
+
isDemoSession,
|
|
737
|
+
isTestVideoSession,
|
|
738
|
+
skipSuccessScreen,
|
|
739
|
+
onCompleted,
|
|
740
|
+
]);
|
|
698
741
|
|
|
699
742
|
return (
|
|
700
743
|
<SafeAreaView style={styles.container}>
|
|
701
744
|
<View style={styles.container}>
|
|
702
|
-
{isSubmitting ? (
|
|
745
|
+
{submitFailed && !isSubmitting ? (
|
|
746
|
+
<View style={styles.submitErrorContainer}>
|
|
747
|
+
<Text style={styles.submitErrorTitle}>
|
|
748
|
+
{t('resultScreen.submissionFailedTitle')}
|
|
749
|
+
</Text>
|
|
750
|
+
<Text style={styles.submitErrorMessage}>
|
|
751
|
+
{t('resultScreen.submissionFailedRetryHint')}
|
|
752
|
+
</Text>
|
|
753
|
+
<StyledButton
|
|
754
|
+
mode="contained"
|
|
755
|
+
onPress={() => submit(appContext.identificationInfo)}
|
|
756
|
+
style={styles.submitRetryButton}
|
|
757
|
+
>
|
|
758
|
+
{t('resultScreen.retrySubmission')}
|
|
759
|
+
</StyledButton>
|
|
760
|
+
{/* Give a stuck user a support channel — the diagnostic report is
|
|
761
|
+
PoW-authenticated (no session key needed) and opt-in (it asks
|
|
762
|
+
before sending). Auto-hides without baseUrl/sessionId. */}
|
|
763
|
+
<View style={styles.submitReportButton}>
|
|
764
|
+
<DiagnosticReportButton
|
|
765
|
+
baseUrl={appContext.baseUrl}
|
|
766
|
+
sessionId={appContext.identificationInfo?.sessionId}
|
|
767
|
+
scan={{
|
|
768
|
+
documentType:
|
|
769
|
+
appContext.identificationInfo?.scannedDocument
|
|
770
|
+
?.documentType,
|
|
771
|
+
dataSource:
|
|
772
|
+
appContext.identificationInfo?.scannedDocument?.dataSource,
|
|
773
|
+
mrzText:
|
|
774
|
+
appContext.identificationInfo?.scannedDocument?.mrzText,
|
|
775
|
+
mrzFields:
|
|
776
|
+
appContext.identificationInfo?.scannedDocument?.mrzFields,
|
|
777
|
+
barcodeValue:
|
|
778
|
+
appContext.identificationInfo?.scannedDocument
|
|
779
|
+
?.barcodeValue,
|
|
780
|
+
}}
|
|
781
|
+
images={collectDiagnosticImages(
|
|
782
|
+
appContext.identificationInfo?.scannedDocument
|
|
783
|
+
)}
|
|
784
|
+
/>
|
|
785
|
+
</View>
|
|
786
|
+
</View>
|
|
787
|
+
) : isSubmitting ? (
|
|
703
788
|
<>
|
|
704
789
|
<Text style={styles.sectionHeader}>
|
|
705
790
|
{t('resultScreen.submitting')}
|
|
@@ -796,8 +881,9 @@ const ResultScreen = () => {
|
|
|
796
881
|
{t('eidScannerScreen.name')}:
|
|
797
882
|
</Text>
|
|
798
883
|
<Text style={styles.mrzInfoText}>
|
|
799
|
-
{appContext.lastDocumentRead?.name.displayFirst
|
|
800
|
-
|
|
884
|
+
{appContext.lastDocumentRead?.name.displayFirst ??
|
|
885
|
+
appContext.identificationInfo.scannedDocument
|
|
886
|
+
.mrzFields?.firstName}
|
|
801
887
|
</Text>
|
|
802
888
|
</View>
|
|
803
889
|
<View style={styles.mrzInfoItem}>
|
|
@@ -805,8 +891,9 @@ const ResultScreen = () => {
|
|
|
805
891
|
{t('eidScannerScreen.surname')}:
|
|
806
892
|
</Text>
|
|
807
893
|
<Text style={styles.mrzInfoText}>
|
|
808
|
-
{appContext.lastDocumentRead?.name.displayLast
|
|
809
|
-
|
|
894
|
+
{appContext.lastDocumentRead?.name.displayLast ??
|
|
895
|
+
appContext.identificationInfo.scannedDocument
|
|
896
|
+
.mrzFields?.lastName}
|
|
810
897
|
</Text>
|
|
811
898
|
</View>
|
|
812
899
|
<View style={styles.mrzInfoItem}>
|
|
@@ -1071,7 +1158,9 @@ const ResultScreen = () => {
|
|
|
1071
1158
|
)}
|
|
1072
1159
|
</View>
|
|
1073
1160
|
</ScrollView>
|
|
1074
|
-
<View
|
|
1161
|
+
<View
|
|
1162
|
+
style={[styles.footer, { paddingBottom: insets.bottom + 20 }]}
|
|
1163
|
+
>
|
|
1075
1164
|
<StyledButton
|
|
1076
1165
|
mode="contained"
|
|
1077
1166
|
onPress={() => {
|
|
@@ -1154,6 +1243,33 @@ const styles = StyleSheet.create({
|
|
|
1154
1243
|
textAlign: 'center',
|
|
1155
1244
|
textTransform: 'capitalize',
|
|
1156
1245
|
},
|
|
1246
|
+
submitErrorContainer: {
|
|
1247
|
+
flex: 1,
|
|
1248
|
+
justifyContent: 'center',
|
|
1249
|
+
alignItems: 'center',
|
|
1250
|
+
paddingHorizontal: 24,
|
|
1251
|
+
},
|
|
1252
|
+
submitErrorTitle: {
|
|
1253
|
+
fontSize: 20,
|
|
1254
|
+
fontWeight: 'bold',
|
|
1255
|
+
color: 'black',
|
|
1256
|
+
textAlign: 'center',
|
|
1257
|
+
marginBottom: 12,
|
|
1258
|
+
},
|
|
1259
|
+
submitErrorMessage: {
|
|
1260
|
+
fontSize: 16,
|
|
1261
|
+
color: '#666',
|
|
1262
|
+
textAlign: 'center',
|
|
1263
|
+
lineHeight: 22,
|
|
1264
|
+
marginBottom: 28,
|
|
1265
|
+
},
|
|
1266
|
+
submitRetryButton: {
|
|
1267
|
+
minWidth: 220,
|
|
1268
|
+
},
|
|
1269
|
+
submitReportButton: {
|
|
1270
|
+
marginTop: 16,
|
|
1271
|
+
minWidth: 220,
|
|
1272
|
+
},
|
|
1157
1273
|
sectionText: {
|
|
1158
1274
|
fontSize: 18,
|
|
1159
1275
|
color: 'black',
|
|
@@ -24,6 +24,7 @@ import { normalizeFromMRZInfo } from '../Libs/documentDataNormalizer';
|
|
|
24
24
|
import { useTranslation } from 'react-i18next';
|
|
25
25
|
import AppContext from '../Contexts/AppContext';
|
|
26
26
|
import StyledButton from './StyledButton';
|
|
27
|
+
import DiagnosticReportButton from './DiagnosticReportButton';
|
|
27
28
|
import LottieView from 'lottie-react-native';
|
|
28
29
|
import { getLocalizedCountryName } from '../Libs/country-display.utils';
|
|
29
30
|
import { useKeepAwake } from '../Libs/native-keep-awake.utils';
|
|
@@ -116,6 +117,10 @@ const EIDScanner = ({
|
|
|
116
117
|
}
|
|
117
118
|
}, []);
|
|
118
119
|
const [isScanned, setIsScanned] = React.useState(false);
|
|
120
|
+
// Set when an NFC read fails — surfaces a "report issue" support channel next
|
|
121
|
+
// to the retry button (a failed chip read is the most common point a user
|
|
122
|
+
// genuinely needs help). Cleared when a new read starts.
|
|
123
|
+
const [hasScanFailed, setHasScanFailed] = React.useState(false);
|
|
119
124
|
const [hasGuideShown, setHasGuideShown] = React.useState(false);
|
|
120
125
|
const { t, i18n } = useTranslation();
|
|
121
126
|
const appContext = React.useContext(AppContext);
|
|
@@ -192,6 +197,7 @@ const EIDScanner = ({
|
|
|
192
197
|
|
|
193
198
|
setIsScanning(true);
|
|
194
199
|
setIsScanned(false);
|
|
200
|
+
setHasScanFailed(false);
|
|
195
201
|
setProgress(0);
|
|
196
202
|
setProgressStage('');
|
|
197
203
|
resetLastMessage();
|
|
@@ -354,6 +360,9 @@ const EIDScanner = ({
|
|
|
354
360
|
setDocumentName(builtName);
|
|
355
361
|
setIsScanned(true);
|
|
356
362
|
}
|
|
363
|
+
// Defensive: clear any prior-failure flag on a successful read so the
|
|
364
|
+
// report affordance can't linger if the UI returns to this state.
|
|
365
|
+
setHasScanFailed(false);
|
|
357
366
|
await trackEIDScanComplete(
|
|
358
367
|
docType,
|
|
359
368
|
scanDuration,
|
|
@@ -385,6 +394,7 @@ const EIDScanner = ({
|
|
|
385
394
|
Alert.alert(t('general.error'), t('eidScannerScreen.invalidMRZFields'));
|
|
386
395
|
}
|
|
387
396
|
} catch (error) {
|
|
397
|
+
setHasScanFailed(true);
|
|
388
398
|
const scanDuration = Date.now() - startTime;
|
|
389
399
|
const errorMessage =
|
|
390
400
|
error instanceof Error ? error.message : 'Unknown error';
|
|
@@ -876,6 +886,31 @@ const EIDScanner = ({
|
|
|
876
886
|
<StyledButton mode="contained" onPress={readNFC}>
|
|
877
887
|
{t('eidScannerScreen.startScanning')}
|
|
878
888
|
</StyledButton>
|
|
889
|
+
{/* After a failed chip read, offer a support channel. The
|
|
890
|
+
diagnostics singleton captured this read's NFC step/error code;
|
|
891
|
+
the report is PoW-authenticated and opt-in. Auto-hides without
|
|
892
|
+
baseUrl/session. */}
|
|
893
|
+
{hasScanFailed && (
|
|
894
|
+
<View style={styles.reportButtonWrapper}>
|
|
895
|
+
<DiagnosticReportButton
|
|
896
|
+
baseUrl={appContext.baseUrl}
|
|
897
|
+
sessionId={appContext.identificationInfo?.sessionId}
|
|
898
|
+
scan={{
|
|
899
|
+
documentType:
|
|
900
|
+
appContext.identificationInfo?.scannedDocument
|
|
901
|
+
?.documentType,
|
|
902
|
+
dataSource:
|
|
903
|
+
appContext.identificationInfo?.scannedDocument
|
|
904
|
+
?.dataSource,
|
|
905
|
+
mrzText:
|
|
906
|
+
appContext.identificationInfo?.scannedDocument?.mrzText,
|
|
907
|
+
mrzFields:
|
|
908
|
+
appContext.identificationInfo?.scannedDocument
|
|
909
|
+
?.mrzFields,
|
|
910
|
+
}}
|
|
911
|
+
/>
|
|
912
|
+
</View>
|
|
913
|
+
)}
|
|
879
914
|
</View>
|
|
880
915
|
)}
|
|
881
916
|
|
|
@@ -972,6 +1007,9 @@ const styles = StyleSheet.create({
|
|
|
972
1007
|
flexDirection: 'column',
|
|
973
1008
|
gap: 10,
|
|
974
1009
|
},
|
|
1010
|
+
reportButtonWrapper: {
|
|
1011
|
+
marginTop: 4,
|
|
1012
|
+
},
|
|
975
1013
|
mainText: {
|
|
976
1014
|
fontSize: 20,
|
|
977
1015
|
fontWeight: 'bold',
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import React, { useContext } from 'react';
|
|
2
|
+
import { View, Text, StyleSheet } from 'react-native';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import { useTheme } from '../Contexts/ThemeContext';
|
|
5
|
+
import AppContext from '../Contexts/AppContext';
|
|
6
|
+
import StyledButton from './StyledButton';
|
|
7
|
+
import { trackError } from '../Libs/analytics.utils';
|
|
8
|
+
import { logError } from '../Libs/debug.utils';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Themed fallback UI for an unrecoverable render error. A functional child so it
|
|
12
|
+
* can use the theme + translation hooks (the boundary itself must be a class).
|
|
13
|
+
*/
|
|
14
|
+
function ErrorFallback({ onRetry }: { onRetry: () => void }) {
|
|
15
|
+
const { colors } = useTheme();
|
|
16
|
+
const { t } = useTranslation();
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<View style={[styles.container, { backgroundColor: colors.background }]}>
|
|
20
|
+
<View style={styles.content}>
|
|
21
|
+
<Text style={[styles.title, { color: colors.text }]}>
|
|
22
|
+
{t('errorBoundary.title')}
|
|
23
|
+
</Text>
|
|
24
|
+
<Text style={[styles.message, { color: colors.textSecondary }]}>
|
|
25
|
+
{t('errorBoundary.message')}
|
|
26
|
+
</Text>
|
|
27
|
+
<StyledButton mode="contained" onPress={onRetry} style={styles.button}>
|
|
28
|
+
{t('errorBoundary.tryAgain')}
|
|
29
|
+
</StyledButton>
|
|
30
|
+
</View>
|
|
31
|
+
</View>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface ErrorBoundaryProps {
|
|
36
|
+
children: React.ReactNode;
|
|
37
|
+
// The host's onError, injected as a PROP by the wrapper below. We deliberately
|
|
38
|
+
// avoid `static contextType` + `this.context` here: typing `this.context`
|
|
39
|
+
// needs a `declare context` field, which Metro's Babel flow-strip transform
|
|
40
|
+
// rejects (only allowed under allowDeclareFields) — it would crash the bundle.
|
|
41
|
+
onError?: (error: string) => void;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface ErrorBoundaryState {
|
|
45
|
+
hasError: boolean;
|
|
46
|
+
// Bumped on every retry so the recovered subtree gets a fresh `key` and
|
|
47
|
+
// genuinely re-mounts (a plain hasError:false would re-render the SAME tree
|
|
48
|
+
// and, for a deterministic error, immediately re-throw into a flicker loop).
|
|
49
|
+
resetKey: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
class ErrorBoundaryInner extends React.Component<
|
|
53
|
+
ErrorBoundaryProps,
|
|
54
|
+
ErrorBoundaryState
|
|
55
|
+
> {
|
|
56
|
+
state: ErrorBoundaryState = { hasError: false, resetKey: 0 };
|
|
57
|
+
|
|
58
|
+
static getDerivedStateFromError(): Partial<ErrorBoundaryState> {
|
|
59
|
+
return { hasError: true };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
componentDidCatch(error: Error, info: React.ErrorInfo) {
|
|
63
|
+
logError('ErrorBoundary', 'Caught render error', error);
|
|
64
|
+
// Telemetry — best-effort, never let reporting throw.
|
|
65
|
+
trackError(
|
|
66
|
+
'SDK_RENDER_ERROR',
|
|
67
|
+
error.message,
|
|
68
|
+
'error_boundary',
|
|
69
|
+
'critical',
|
|
70
|
+
{
|
|
71
|
+
error,
|
|
72
|
+
component: 'error_boundary',
|
|
73
|
+
recoverable: true,
|
|
74
|
+
additionalData: {
|
|
75
|
+
componentStack: info.componentStack ?? '',
|
|
76
|
+
},
|
|
77
|
+
}
|
|
78
|
+
).catch(() => {});
|
|
79
|
+
// Notify the host integration that the SDK hit an unrecoverable error.
|
|
80
|
+
this.props.onError?.(error.message);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
handleRetry = () => {
|
|
84
|
+
// Bump resetKey so the children below re-mount with a fresh key — a real
|
|
85
|
+
// recovery, not just a re-render of the same (possibly still-broken) tree.
|
|
86
|
+
this.setState((prev) => ({
|
|
87
|
+
hasError: false,
|
|
88
|
+
resetKey: prev.resetKey + 1,
|
|
89
|
+
}));
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
render() {
|
|
93
|
+
if (this.state.hasError) {
|
|
94
|
+
return <ErrorFallback onRetry={this.handleRetry} />;
|
|
95
|
+
}
|
|
96
|
+
return (
|
|
97
|
+
<React.Fragment key={this.state.resetKey}>
|
|
98
|
+
{this.props.children}
|
|
99
|
+
</React.Fragment>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Catches unexpected render/runtime errors anywhere in the SDK's screen tree so
|
|
106
|
+
* an unhandled throw shows a recoverable fallback instead of unmounting to a
|
|
107
|
+
* white screen / crashing the host app. The SDK was otherwise entirely
|
|
108
|
+
* functional with no crash protection.
|
|
109
|
+
*
|
|
110
|
+
* Placed inside ThemeProvider + AppContext.Provider so the fallback can theme
|
|
111
|
+
* itself and notify the host via `onError`. This thin functional wrapper reads
|
|
112
|
+
* `onError` from context (a hook, which the class can't use) and passes it to
|
|
113
|
+
* the class as a prop. "Try Again" re-mounts the subtree (the navigator returns
|
|
114
|
+
* to its initial route).
|
|
115
|
+
*/
|
|
116
|
+
export default function ErrorBoundary({
|
|
117
|
+
children,
|
|
118
|
+
}: {
|
|
119
|
+
children: React.ReactNode;
|
|
120
|
+
}) {
|
|
121
|
+
const { onError } = useContext(AppContext);
|
|
122
|
+
return <ErrorBoundaryInner onError={onError}>{children}</ErrorBoundaryInner>;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const styles = StyleSheet.create({
|
|
126
|
+
container: {
|
|
127
|
+
flex: 1,
|
|
128
|
+
justifyContent: 'center',
|
|
129
|
+
alignItems: 'center',
|
|
130
|
+
padding: 24,
|
|
131
|
+
},
|
|
132
|
+
content: {
|
|
133
|
+
alignItems: 'center',
|
|
134
|
+
maxWidth: 360,
|
|
135
|
+
},
|
|
136
|
+
title: {
|
|
137
|
+
fontSize: 22,
|
|
138
|
+
fontWeight: 'bold',
|
|
139
|
+
textAlign: 'center',
|
|
140
|
+
marginBottom: 12,
|
|
141
|
+
},
|
|
142
|
+
message: {
|
|
143
|
+
fontSize: 16,
|
|
144
|
+
textAlign: 'center',
|
|
145
|
+
lineHeight: 22,
|
|
146
|
+
marginBottom: 28,
|
|
147
|
+
},
|
|
148
|
+
button: {
|
|
149
|
+
minWidth: 200,
|
|
150
|
+
},
|
|
151
|
+
});
|
|
@@ -14,6 +14,11 @@ export const MIN_MLI_FACE_SIZE_PERCENT = 0.01; // MLI (Multi Layer Image): min 1
|
|
|
14
14
|
export const REQUIRED_CONSISTENT_MRZ_READS = 2;
|
|
15
15
|
export const REQUIRED_CONSISTENT_DOCTYPE_DETECTIONS = 3;
|
|
16
16
|
|
|
17
|
+
// After this long on a single scan step without completing it, surface the
|
|
18
|
+
// "Having trouble?" prompt so the user isn't stranded on a live camera with no
|
|
19
|
+
// timeout, retry, or help. Per-step (re-armed on each step transition).
|
|
20
|
+
export const HELP_PROMPT_DELAY_MS = 25000;
|
|
21
|
+
|
|
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;
|