@trustchex/react-native-sdk 1.497.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.
Files changed (32) hide show
  1. package/lib/module/Screens/Static/ResultScreen.js +104 -11
  2. package/lib/module/Shared/Components/EIDScanner.js +28 -3
  3. package/lib/module/Shared/Components/ErrorBoundary.js +144 -0
  4. package/lib/module/Shared/Components/IdentityDocumentCamera.constants.js +5 -0
  5. package/lib/module/Shared/Components/IdentityDocumentCamera.js +163 -4
  6. package/lib/module/Translation/Resources/en.js +13 -0
  7. package/lib/module/Translation/Resources/tr.js +13 -0
  8. package/lib/module/Trustchex.js +52 -49
  9. package/lib/module/version.js +1 -1
  10. package/lib/typescript/src/Screens/Static/ResultScreen.d.ts.map +1 -1
  11. package/lib/typescript/src/Shared/Components/EIDScanner.d.ts.map +1 -1
  12. package/lib/typescript/src/Shared/Components/ErrorBoundary.d.ts +17 -0
  13. package/lib/typescript/src/Shared/Components/ErrorBoundary.d.ts.map +1 -0
  14. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.constants.d.ts +1 -0
  15. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.constants.d.ts.map +1 -1
  16. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.d.ts.map +1 -1
  17. package/lib/typescript/src/Translation/Resources/en.d.ts +13 -0
  18. package/lib/typescript/src/Translation/Resources/en.d.ts.map +1 -1
  19. package/lib/typescript/src/Translation/Resources/tr.d.ts +13 -0
  20. package/lib/typescript/src/Translation/Resources/tr.d.ts.map +1 -1
  21. package/lib/typescript/src/Trustchex.d.ts.map +1 -1
  22. package/lib/typescript/src/version.d.ts +1 -1
  23. package/package.json +1 -1
  24. package/src/Screens/Static/ResultScreen.tsx +145 -29
  25. package/src/Shared/Components/EIDScanner.tsx +38 -0
  26. package/src/Shared/Components/ErrorBoundary.tsx +151 -0
  27. package/src/Shared/Components/IdentityDocumentCamera.constants.ts +5 -0
  28. package/src/Shared/Components/IdentityDocumentCamera.tsx +220 -27
  29. package/src/Translation/Resources/en.ts +16 -0
  30. package/src/Translation/Resources/tr.ts +16 -0
  31. package/src/Trustchex.tsx +64 -59
  32. 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
- displayName: docName?.source === 'dg11' ? docName.displayFirst : undefined,
270
- displaySurname: docName?.source === 'dg11' ? docName.displayLast : undefined,
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
- await runWithRetry(() =>
563
- submitIdentificationConsent(identificationId, sessionKey)
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
- const livenessDetection = identificationInfo.livenessDetection;
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
- uploadIdentificationMedia(
624
+ uploadVerbalConsentVideos(
586
625
  identificationId,
587
- scannedIdentityDocument,
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: false, userAction: 'submit_verification' }
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
- Alert.alert(t('general.error'), t('resultScreen.submissionFailed'));
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
- navigationManagerRef.current?.reset();
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 } = appContext;
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
- }, [progress, isDemoSession, isTestVideoSession, skipSuccessScreen, onCompleted]);
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
- ?? appContext.identificationInfo.scannedDocument.mrzFields?.firstName}
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
- ?? appContext.identificationInfo.scannedDocument.mrzFields?.lastName}
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 style={[styles.footer, { paddingBottom: insets.bottom + 20 }]}>
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;