@trustchex/react-native-sdk 1.381.0 → 1.409.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.
Files changed (111) hide show
  1. package/android/src/main/java/com/trustchex/reactnativesdk/camera/TrustchexCameraView.kt +1 -12
  2. package/android/src/main/java/com/trustchex/reactnativesdk/mlkit/MLKitModule.kt +1 -1
  3. package/ios/Camera/TrustchexCameraView.swift +1 -12
  4. package/ios/MLKit/MLKitModule.swift +1 -1
  5. package/lib/module/Screens/Debug/BarcodeTestScreen.js +308 -0
  6. package/lib/module/Screens/Debug/MRZTestScreen.js +105 -13
  7. package/lib/module/Screens/Dynamic/ContractAcceptanceScreen.js +49 -29
  8. package/lib/module/Screens/Dynamic/IdentityDocumentEIDScanningScreen.js +5 -0
  9. package/lib/module/Screens/Dynamic/IdentityDocumentScanningScreen.js +5 -0
  10. package/lib/module/Screens/Dynamic/LivenessDetectionScreen.js +26 -6
  11. package/lib/module/Screens/Dynamic/VideoCallScreen.js +676 -0
  12. package/lib/module/Screens/Static/OTPVerificationScreen.js +6 -0
  13. package/lib/module/Screens/Static/QrCodeScanningScreen.js +7 -1
  14. package/lib/module/Screens/Static/ResultScreen.js +27 -13
  15. package/lib/module/Screens/Static/VerificationSessionCheckScreen.js +51 -51
  16. package/lib/module/Shared/Animations/video-call.json +1 -0
  17. package/lib/module/Shared/Components/DebugNavigationPanel.js +180 -14
  18. package/lib/module/Shared/Components/EIDScanner.js +1 -4
  19. package/lib/module/Shared/Components/IdentityDocumentCamera.js +29 -8
  20. package/lib/module/Shared/Components/NavigationManager.js +15 -3
  21. package/lib/module/Shared/Contexts/AppContext.js +1 -0
  22. package/lib/module/Shared/Libs/SignalingClient.js +128 -0
  23. package/lib/module/Shared/Libs/analytics.utils.js +4 -0
  24. package/lib/module/Shared/Libs/deeplink.utils.js +9 -1
  25. package/lib/module/Shared/Libs/http-client.js +9 -0
  26. package/lib/module/Shared/Libs/promise.utils.js +16 -2
  27. package/lib/module/Shared/Libs/status-bar.utils.js +21 -0
  28. package/lib/module/Shared/Services/DataUploadService.js +294 -0
  29. package/lib/module/Shared/Services/VideoSessionService.js +156 -0
  30. package/lib/module/Shared/Services/WebRTCService.js +510 -0
  31. package/lib/module/Shared/Types/analytics.types.js +2 -0
  32. package/lib/module/Translation/Resources/en.js +20 -0
  33. package/lib/module/Translation/Resources/tr.js +20 -0
  34. package/lib/module/Trustchex.js +10 -0
  35. package/lib/module/version.js +1 -1
  36. package/lib/typescript/src/Screens/Debug/BarcodeTestScreen.d.ts +3 -0
  37. package/lib/typescript/src/Screens/Debug/BarcodeTestScreen.d.ts.map +1 -0
  38. package/lib/typescript/src/Screens/Debug/MRZTestScreen.d.ts.map +1 -1
  39. package/lib/typescript/src/Screens/Dynamic/ContractAcceptanceScreen.d.ts.map +1 -1
  40. package/lib/typescript/src/Screens/Dynamic/IdentityDocumentEIDScanningScreen.d.ts.map +1 -1
  41. package/lib/typescript/src/Screens/Dynamic/IdentityDocumentScanningScreen.d.ts.map +1 -1
  42. package/lib/typescript/src/Screens/Dynamic/LivenessDetectionScreen.d.ts.map +1 -1
  43. package/lib/typescript/src/Screens/Dynamic/VideoCallScreen.d.ts +3 -0
  44. package/lib/typescript/src/Screens/Dynamic/VideoCallScreen.d.ts.map +1 -0
  45. package/lib/typescript/src/Screens/Static/OTPVerificationScreen.d.ts.map +1 -1
  46. package/lib/typescript/src/Screens/Static/QrCodeScanningScreen.d.ts.map +1 -1
  47. package/lib/typescript/src/Screens/Static/ResultScreen.d.ts.map +1 -1
  48. package/lib/typescript/src/Screens/Static/VerificationSessionCheckScreen.d.ts.map +1 -1
  49. package/lib/typescript/src/Shared/Components/DebugNavigationPanel.d.ts.map +1 -1
  50. package/lib/typescript/src/Shared/Components/EIDScanner.d.ts.map +1 -1
  51. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.d.ts.map +1 -1
  52. package/lib/typescript/src/Shared/Components/NavigationManager.d.ts.map +1 -1
  53. package/lib/typescript/src/Shared/Contexts/AppContext.d.ts +1 -0
  54. package/lib/typescript/src/Shared/Contexts/AppContext.d.ts.map +1 -1
  55. package/lib/typescript/src/Shared/Libs/SignalingClient.d.ts +24 -0
  56. package/lib/typescript/src/Shared/Libs/SignalingClient.d.ts.map +1 -0
  57. package/lib/typescript/src/Shared/Libs/analytics.utils.d.ts.map +1 -1
  58. package/lib/typescript/src/Shared/Libs/deeplink.utils.d.ts.map +1 -1
  59. package/lib/typescript/src/Shared/Libs/http-client.d.ts.map +1 -1
  60. package/lib/typescript/src/Shared/Libs/promise.utils.d.ts.map +1 -1
  61. package/lib/typescript/src/Shared/Libs/status-bar.utils.d.ts +9 -0
  62. package/lib/typescript/src/Shared/Libs/status-bar.utils.d.ts.map +1 -0
  63. package/lib/typescript/src/Shared/Services/DataUploadService.d.ts +25 -0
  64. package/lib/typescript/src/Shared/Services/DataUploadService.d.ts.map +1 -0
  65. package/lib/typescript/src/Shared/Services/VideoSessionService.d.ts +33 -0
  66. package/lib/typescript/src/Shared/Services/VideoSessionService.d.ts.map +1 -0
  67. package/lib/typescript/src/Shared/Services/WebRTCService.d.ts +58 -0
  68. package/lib/typescript/src/Shared/Services/WebRTCService.d.ts.map +1 -0
  69. package/lib/typescript/src/Shared/Types/analytics.types.d.ts +2 -0
  70. package/lib/typescript/src/Shared/Types/analytics.types.d.ts.map +1 -1
  71. package/lib/typescript/src/Shared/Types/identificationInfo.d.ts +4 -1
  72. package/lib/typescript/src/Shared/Types/identificationInfo.d.ts.map +1 -1
  73. package/lib/typescript/src/Translation/Resources/en.d.ts +20 -0
  74. package/lib/typescript/src/Translation/Resources/en.d.ts.map +1 -1
  75. package/lib/typescript/src/Translation/Resources/tr.d.ts +20 -0
  76. package/lib/typescript/src/Translation/Resources/tr.d.ts.map +1 -1
  77. package/lib/typescript/src/Trustchex.d.ts.map +1 -1
  78. package/lib/typescript/src/version.d.ts +1 -1
  79. package/package.json +29 -2
  80. package/src/Screens/Debug/BarcodeTestScreen.tsx +317 -0
  81. package/src/Screens/Debug/MRZTestScreen.tsx +107 -13
  82. package/src/Screens/Dynamic/ContractAcceptanceScreen.tsx +59 -33
  83. package/src/Screens/Dynamic/IdentityDocumentEIDScanningScreen.tsx +6 -0
  84. package/src/Screens/Dynamic/IdentityDocumentScanningScreen.tsx +6 -0
  85. package/src/Screens/Dynamic/LivenessDetectionScreen.tsx +34 -6
  86. package/src/Screens/Dynamic/VideoCallScreen.tsx +764 -0
  87. package/src/Screens/Static/OTPVerificationScreen.tsx +6 -0
  88. package/src/Screens/Static/QrCodeScanningScreen.tsx +7 -1
  89. package/src/Screens/Static/ResultScreen.tsx +58 -23
  90. package/src/Screens/Static/VerificationSessionCheckScreen.tsx +58 -72
  91. package/src/Shared/Animations/video-call.json +1 -0
  92. package/src/Shared/Components/DebugNavigationPanel.tsx +185 -9
  93. package/src/Shared/Components/EIDScanner.tsx +1 -5
  94. package/src/Shared/Components/IdentityDocumentCamera.tsx +29 -8
  95. package/src/Shared/Components/NavigationManager.tsx +14 -1
  96. package/src/Shared/Contexts/AppContext.ts +2 -0
  97. package/src/Shared/Libs/SignalingClient.ts +189 -0
  98. package/src/Shared/Libs/analytics.utils.ts +4 -0
  99. package/src/Shared/Libs/deeplink.utils.ts +12 -1
  100. package/src/Shared/Libs/http-client.ts +10 -0
  101. package/src/Shared/Libs/promise.utils.ts +16 -2
  102. package/src/Shared/Libs/status-bar.utils.ts +19 -0
  103. package/src/Shared/Services/DataUploadService.ts +395 -0
  104. package/src/Shared/Services/VideoSessionService.ts +190 -0
  105. package/src/Shared/Services/WebRTCService.ts +636 -0
  106. package/src/Shared/Types/analytics.types.ts +2 -0
  107. package/src/Shared/Types/identificationInfo.ts +5 -1
  108. package/src/Translation/Resources/en.ts +25 -0
  109. package/src/Translation/Resources/tr.ts +27 -0
  110. package/src/Trustchex.tsx +12 -2
  111. package/src/version.ts +1 -1
@@ -6,10 +6,16 @@ import AppContext from "../../Shared/Contexts/AppContext.js";
6
6
  import QrCodeScannerCamera from "../../Shared/Components/QrCodeScannerCamera.js";
7
7
  import { useNavigation } from '@react-navigation/native';
8
8
  import { handleDeepLink } from "../../Shared/Libs/deeplink.utils.js";
9
+ import { useKeepAwake } from "../../Shared/Libs/native-keep-awake.utils.js";
10
+ import { useStatusBarWhiteBackground } from "../../Shared/Libs/status-bar.utils.js";
9
11
  import { jsx as _jsx } from "react/jsx-runtime";
10
12
  const QrCodeScanningScreen = () => {
13
+ useKeepAwake();
11
14
  const appContext = React.useContext(AppContext);
12
15
  const navigation = useNavigation();
16
+
17
+ // Configure status bar for white background
18
+ useStatusBarWhiteBackground();
13
19
  const onQrCodeScanned = data => {
14
20
  const [bUrl, sId] = handleDeepLink({
15
21
  url: data
@@ -25,7 +31,7 @@ const QrCodeScanningScreen = () => {
25
31
  } else {
26
32
  appContext.identificationInfo.sessionId = sId;
27
33
  }
28
- navigation.navigate('VerificationSessionCheckScreen');
34
+ navigation.goBack();
29
35
  }
30
36
  };
31
37
  const onClose = () => {
@@ -9,6 +9,7 @@ import RNFS from 'react-native-fs';
9
9
  import NativeProgressBar from "../../Shared/Components/NativeProgressBar.js";
10
10
  import LottieView from 'lottie-react-native';
11
11
  import { runWithRetry } from "../../Shared/Libs/promise.utils.js";
12
+ import { useStatusBarWhiteBackground } from "../../Shared/Libs/status-bar.utils.js";
12
13
  import NavigationManager from "../../Shared/Components/NavigationManager.js";
13
14
  import mrzUtils from "../../Shared/Libs/mrz.utils.js";
14
15
  import { useTranslation } from 'react-i18next';
@@ -35,6 +36,9 @@ const ResultScreen = () => {
35
36
 
36
37
  // Track screen view and exit
37
38
  useScreenTracking('result_screen');
39
+
40
+ // Configure status bar for white background
41
+ useStatusBarWhiteBackground();
38
42
  const formatDate = useCallback(dateStr => {
39
43
  if (!dateStr) return '';
40
44
  try {
@@ -64,12 +68,12 @@ const ResultScreen = () => {
64
68
  }, []);
65
69
  const apiUrl = useMemo(() => `${appContext.baseUrl}/api/app/mobile`, [appContext.baseUrl]);
66
70
  useEffect(() => {
67
- if (appContext.isDemoSession) {
71
+ if (appContext.isDemoSession || appContext.isTestVideoSession) {
68
72
  setShouldShowDemoData(true);
69
73
  // Generate device identifier for demo
70
74
  NativeDeviceInfo.generateHumanReadableIdentifier().then(setDeviceIdentifier);
71
75
  }
72
- }, [appContext.identificationInfo.identificationId, appContext.isDemoSession]);
76
+ }, [appContext.identificationInfo.identificationId, appContext.isDemoSession, appContext.isTestVideoSession]);
73
77
  const createIdentification = useCallback(async identificationId => {
74
78
  await httpClient.post(`${apiUrl}/identifications/${identificationId}`, null);
75
79
  }, [apiUrl]);
@@ -141,7 +145,12 @@ const ResultScreen = () => {
141
145
  nonce
142
146
  });
143
147
  }, [apiUrl, getGenderEnumType]);
144
- const uploadIdentificationMedia = useCallback(async (identificationId, scannedIdentityDocument, livenessDetection) => {
148
+ const uploadIdentificationMedia = useCallback(async (identificationId, scannedIdentityDocument, livenessDetection, skipIfAlreadyUploaded) => {
149
+ // Skip media upload if already uploaded during video call
150
+ if (skipIfAlreadyUploaded) {
151
+ console.log('[ResultScreen] Media already uploaded during video call, skipping');
152
+ return;
153
+ }
145
154
  const uploadFileOptions = {
146
155
  toUrl: `${apiUrl}/identifications/${identificationId}/media`,
147
156
  method: 'POST',
@@ -271,18 +280,23 @@ const ResultScreen = () => {
271
280
  if (!identificationId) {
272
281
  throw new Error('IdentificationId not found');
273
282
  }
283
+ const alreadyUploaded = !!identificationInfo.mediaUploadedDuringVideoCall;
274
284
  const sessionKey = await runWithRetry(() => getSessionKey(apiUrl, appContext.identificationInfo.sessionId));
275
- await runWithRetry(() => createIdentification(identificationId));
285
+ if (!alreadyUploaded) {
286
+ await runWithRetry(() => createIdentification(identificationId));
287
+ }
276
288
  setProgress(20);
277
289
  await runWithRetry(() => submitIdentificationConsent(identificationId, sessionKey));
278
290
  setProgress(30);
279
- const scannedIdentityDocument = identificationInfo.scannedDocument;
280
- if (scannedIdentityDocument && scannedIdentityDocument.documentType !== 'UNKNOWN') {
281
- await runWithRetry(() => submitIdentificationDocument(identificationId, scannedIdentityDocument, sessionKey));
291
+ if (!alreadyUploaded) {
292
+ const scannedIdentityDocument = identificationInfo.scannedDocument;
293
+ if (scannedIdentityDocument && scannedIdentityDocument.documentType !== 'UNKNOWN') {
294
+ await runWithRetry(() => submitIdentificationDocument(identificationId, scannedIdentityDocument, sessionKey));
295
+ }
296
+ setProgress(40);
297
+ const livenessDetection = identificationInfo.livenessDetection;
298
+ await runWithRetry(() => uploadIdentificationMedia(identificationId, scannedIdentityDocument, livenessDetection, false));
282
299
  }
283
- setProgress(40);
284
- const livenessDetection = identificationInfo.livenessDetection;
285
- await runWithRetry(() => uploadIdentificationMedia(identificationId, scannedIdentityDocument, livenessDetection));
286
300
  setProgress(90);
287
301
  await runWithRetry(() => finishIdentification(identificationId, sessionKey));
288
302
  setProgress(100);
@@ -313,12 +327,12 @@ const ResultScreen = () => {
313
327
  }
314
328
  }, [createIdentification, apiUrl, uploadIdentificationMedia, finishIdentification, submitIdentificationDocument, submitIdentificationConsent, t, appContext]);
315
329
  useEffect(() => {
316
- if (appContext.identificationInfo && !appContext.isDemoSession) {
330
+ if (appContext.identificationInfo && !appContext.isDemoSession && !appContext.isTestVideoSession) {
317
331
  submit(appContext.identificationInfo);
318
332
  }
319
- }, [appContext.identificationInfo, appContext.isDemoSession, submit]);
333
+ }, [appContext.identificationInfo, appContext.isDemoSession, appContext.isTestVideoSession, submit]);
320
334
  useEffect(() => {
321
- if (progress === 100 && !appContext.isDemoSession) {
335
+ if (progress === 100 && !appContext.isDemoSession && !appContext.isTestVideoSession) {
322
336
  setTimeout(() => {
323
337
  appContext.onCompleted?.();
324
338
  navigationManagerRef.current?.reset();
@@ -14,27 +14,31 @@ import { analyticsService } from "../../Shared/Services/AnalyticsService.js";
14
14
  import { useTheme } from "../../Shared/Contexts/ThemeContext.js";
15
15
  import LottieView from 'lottie-react-native';
16
16
  import { getSimulatedDemoData, isDemoSession } from "../../Shared/Libs/demo.utils.js";
17
- import { useNavigation, useFocusEffect } from '@react-navigation/native';
17
+ import { useNavigation } from '@react-navigation/native';
18
18
  import { SESSION_CODE_CHARSET_PATTERN, SESSION_CODE_VALIDATION_PATTERN } from "../../Shared/Constants/validation.constants.js";
19
19
  import { trackError, trackFunnelStep, useScreenTracking } from "../../Shared/Libs/analytics.utils.js";
20
+ import { useStatusBarWhiteBackground } from "../../Shared/Libs/status-bar.utils.js";
20
21
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
21
22
  const VerificationSessionCheckScreen = () => {
22
23
  const [sessionCode, setSessionCode] = useState('');
23
24
  const [isCheckingSession, setIsCheckingSession] = useState(false);
24
25
  const isCheckingSessionRef = useRef(false);
26
+ const checkedSessionIdRef = useRef('');
25
27
  const appContext = useContext(AppContext);
26
28
  const {
27
29
  t
28
30
  } = useTranslation();
29
31
  const navigationManagerRef = React.useRef(null);
30
32
  const navigation = useNavigation();
31
- const initialized = useRef(false);
32
33
  const insets = useSafeAreaInsets();
33
34
  const theme = useTheme();
34
35
  const primaryColor = theme.colors.primary;
35
36
 
36
37
  // Track screen view and exit
37
38
  useScreenTracking('verification_session_check');
39
+
40
+ // Configure status bar for white background
41
+ useStatusBarWhiteBackground();
38
42
  const apiUrl = useMemo(() => `${appContext.baseUrl}/api/app/mobile`, [appContext.baseUrl]);
39
43
  const validateSessionCode = useCallback(value => {
40
44
  return SESSION_CODE_VALIDATION_PATTERN.test(value.toUpperCase());
@@ -45,17 +49,15 @@ const VerificationSessionCheckScreen = () => {
45
49
  const response = await httpClient.get(`${apiUrl}/verification-sessions/${id}`, isDemo ? getSimulatedDemoData('GET_SESSION') : undefined);
46
50
  return response;
47
51
  } catch (error) {
48
- if (error instanceof NotFoundError) {
49
- // User entered invalid session ID - expected user behavior, not actionable
50
- Alert.alert(t('general.error'), t('verificationSessionCheckScreen.noVerificationSessionFound'));
51
- } else {
52
+ if (!(error instanceof NotFoundError)) {
52
53
  trackError('SESSION_CHECK_ERROR', error instanceof Error ? error.message : 'Unknown error', 'verification_session_check', 'high', {
53
54
  recoverable: false,
54
55
  userAction: 'check_session'
55
56
  });
56
57
  }
58
+ throw error;
57
59
  }
58
- }, [apiUrl, appContext.isDemoSession, t]);
60
+ }, [apiUrl, appContext.isDemoSession]);
59
61
  const getSessionByCode = useCallback(async inputCode => {
60
62
  try {
61
63
  const isDemoMode = isDemoSession(inputCode);
@@ -86,52 +88,50 @@ const VerificationSessionCheckScreen = () => {
86
88
  sessionId
87
89
  });
88
90
  }, [navigation]);
89
-
90
- // Reset initialized flag when screen is focused and sessionId is empty (after reset)
91
- useFocusEffect(useCallback(() => {
92
- if (!appContext.identificationInfo.sessionId) {
93
- initialized.current = false;
94
- isCheckingSessionRef.current = false;
95
- }
96
- }, [appContext.identificationInfo.sessionId]));
91
+ const getSessionRef = useRef(getSession);
92
+ getSessionRef.current = getSession;
93
+ const navigateToOTPScreenRef = useRef(navigateToOTPScreen);
94
+ navigateToOTPScreenRef.current = navigateToOTPScreen;
95
+ const appContextRef = useRef(appContext);
96
+ appContextRef.current = appContext;
97
97
  useEffect(() => {
98
- // Only run this effect if sessionId exists AND we're not currently checking a session
99
- // This prevents race condition when session code is entered
100
- if (!initialized.current && appContext.identificationInfo.sessionId && !isCheckingSessionRef.current) {
101
- initialized.current = true;
102
- isCheckingSessionRef.current = true;
103
- setIsCheckingSession(true);
104
- getSession(appContext.identificationInfo.sessionId).then(session => {
105
- if (!session) {
106
- // Session not found - clear the sessionId and reset flags to allow retry
107
- Alert.alert(t('general.error'), t('verificationSessionCheckScreen.noVerificationSessionFound'));
108
- appContext.identificationInfo.sessionId = '';
109
- if (appContext.setSessionId) {
110
- appContext.setSessionId('');
111
- }
112
- initialized.current = false;
113
- return;
114
- }
115
- appContext.workflowSteps = session.workflowSteps;
116
- if (session.branding) {
117
- appContext.branding = appContext.branding || session.branding;
118
- }
119
- if (session.sendOTP) {
120
- navigateToOTPScreen(session.id);
121
- } else {
122
- if (session.identificationId) {
123
- appContext.identificationInfo.identificationId = session.identificationId;
124
- }
125
- navigationManagerRef.current?.navigateToNextStep();
126
- }
127
- }).finally(() => {
128
- setTimeout(() => {
129
- isCheckingSessionRef.current = false;
130
- setIsCheckingSession(false);
131
- }, 1000);
132
- });
98
+ const sid = appContext.identificationInfo.sessionId;
99
+ if (!sid || checkedSessionIdRef.current === sid) {
100
+ return;
133
101
  }
134
- }, [appContext, appContext.identificationInfo.sessionId, getSession, navigateToOTPScreen, t]);
102
+ checkedSessionIdRef.current = sid;
103
+ isCheckingSessionRef.current = true;
104
+ setIsCheckingSession(true);
105
+ getSessionRef.current(sid).then(session => {
106
+ const ctx = appContextRef.current;
107
+ ctx.workflowSteps = session.workflowSteps;
108
+ if (session.branding) {
109
+ ctx.branding = ctx.branding || session.branding;
110
+ }
111
+ if (session.sendOTP) {
112
+ navigateToOTPScreenRef.current(session.id);
113
+ } else {
114
+ if (session.identificationId) {
115
+ ctx.identificationInfo.identificationId = session.identificationId;
116
+ }
117
+ navigationManagerRef.current?.navigateToNextStep();
118
+ }
119
+ }).catch(() => {
120
+ Alert.alert(t('general.error'), t('verificationSessionCheckScreen.noVerificationSessionFound'));
121
+ checkedSessionIdRef.current = '';
122
+ const ctx = appContextRef.current;
123
+ if (ctx.setSessionId) {
124
+ ctx.setSessionId('');
125
+ }
126
+ }).finally(() => {
127
+ isCheckingSessionRef.current = false;
128
+ setIsCheckingSession(false);
129
+ });
130
+ // eslint-disable-next-line react-hooks/exhaustive-deps
131
+ }, [appContext.identificationInfo.sessionId]);
132
+ if (!appContext || !appContext.baseUrl) {
133
+ return null;
134
+ }
135
135
  return /*#__PURE__*/_jsx(SafeAreaView, {
136
136
  style: styles.safeAreaContainer,
137
137
  children: /*#__PURE__*/_jsx(KeyboardAvoidingView, {
@@ -0,0 +1 @@
1
+ {"v":"5.7.3","fr":30,"ip":0,"op":41,"w":600,"h":600,"nm":"Phone Call","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Call Icon","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[293.084,310.786,0],"ix":2},"a":{"a":0,"k":[1371.347,-543.458,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":0,"s":[28,28,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":20,"s":[25,25,100]},{"t":40,"s":[28,28,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-22.59,22.938],[0,0],[23.373,23.448],[0,0],[23.186,-22.195],[0,0],[25.1,60.946],[0,0],[22.137,23.256],[0.601,0.601],[23.496,-22.664],[0,0],[-157.804,5.059]],"o":[[0,0],[23.373,-23.448],[-0.601,-0.601],[-23.249,-22.13],[0,0],[-60.902,-25.206],[0,0],[22.213,-23.184],[0,0],[-23.009,-23.158],[0,0],[-131.678,152.765],[32.193,0.187]],"v":[[1568.371,-346.481],[1586.008,-366.813],[1586.008,-451.645],[1538.775,-488.121],[1455.803,-488.005],[1418.862,-456.878],[1284.51,-591.463],[1315.52,-628.288],[1315.656,-711.279],[1279.141,-758.453],[1195.297,-759.345],[1173.008,-739.964],[1482.685,-310.897]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,10.704],[64.224,0],[0,-10.704],[-10.704,0],[0,-42.816],[-10.704,0]],"o":[[0,-64.224],[-10.704,0],[0,10.704],[42.816,0],[0,10.704],[10.704,0]],"v":[[1526.061,-582.237],[1409.772,-698.526],[1390.391,-679.145],[1409.772,-659.763],[1487.298,-582.237],[1506.679,-562.856]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[10.704,0],[0,10.704],[85.597,0.085],[0,10.704],[-10.704,0],[-0.117,-106.992]],"o":[[-10.704,0],[-0.085,-85.597],[-10.704,0],[0,-10.704],[106.992,0.117],[0,10.704]],"v":[[1584.205,-562.856],[1564.824,-582.237],[1409.772,-737.289],[1390.391,-756.67],[1409.772,-776.052],[1603.587,-582.237]],"c":true},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":45,"st":-8,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Background Circle","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[300,300,0],"ix":2},"a":{"a":0,"k":[1397.949,-584.944,0],"ix":1},"s":{"a":0,"k":[27,27,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-262.123],[262.123,0],[0,262.123],[-262.123,0]],"o":[[0,262.123],[-262.123,0],[0,-262.123],[262.123,0]],"v":[[1872.564,-584.944],[1397.949,-110.329],[923.334,-584.944],[1397.949,-1059.559]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.117700554343,0.127342568192,0.138823505476,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":45,"st":-8,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Small Circle","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":210,"ix":10},"p":{"a":0,"k":[301,301,0],"ix":2},"a":{"a":0,"k":[-7,-71,0],"ix":1},"s":{"a":0,"k":[70,70,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[426,426],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":22.03,"s":[0]},{"t":39.5470890812688,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":10.769,"s":[0]},{"t":35.7941593937688,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[0.117647066303,0.125490196078,0.137254901961,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":8,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[-7,-71],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":10.7687687687688,"op":45,"st":10.7687687687688,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Medium Circle","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":119,"ix":10},"p":{"a":0,"k":[301,301,0],"ix":2},"a":{"a":0,"k":[-7,-71,0],"ix":1},"s":{"a":0,"k":[85,85,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[426,426],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":12.02,"s":[0]},{"t":29.5370790712588,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0.759,"s":[0]},{"t":25.7841493837588,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[0.117647066303,0.125490196078,0.137254901961,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":7,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[-7,-71],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0.75875875875876,"op":45,"st":0.75875875875876,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Large Circle","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[301,301,0],"ix":2},"a":{"a":0,"k":[-7,-71,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[426,426],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":15.774,"s":[0]},{"t":33.2908328250125,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":4.513,"s":[0]},{"t":29.5379031375125,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[0.117647066303,0.125490196078,0.137254901961,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":6,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[-7,-71],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":4.51251251251251,"op":45,"st":4.51251251251251,"bm":0}],"markers":[]}
@@ -1,12 +1,13 @@
1
1
  "use strict";
2
2
 
3
3
  import React, { useCallback, useContext, useState } from 'react';
4
- import { View, Text, TouchableOpacity, StyleSheet, Platform, Switch } from 'react-native';
4
+ import { View, Text, TouchableOpacity, StyleSheet, Platform, Switch, ActivityIndicator } from 'react-native';
5
5
  import { useNavigation } from '@react-navigation/native';
6
6
  import { useSafeAreaInsets } from 'react-native-safe-area-context';
7
7
  import AppContext from "../Contexts/AppContext.js";
8
8
  import { getSimulatedDemoData } from "../Libs/demo.utils.js";
9
9
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
10
+ const DEV_BASE_URL = 'https://192.168.0.171:3000';
10
11
  // Icon mapping matching Flutter SDK Material Icons
11
12
  const ICONS = {
12
13
  LOGIN: '�',
@@ -31,6 +32,8 @@ const DebugNavigationPanel = () => {
31
32
  const [voiceGuidanceEnabled, setVoiceGuidanceEnabled] = useState(true);
32
33
  const [allowIdCard, setAllowIdCard] = useState(true);
33
34
  const [allowPassport, setAllowPassport] = useState(true);
35
+ const [isConnectingVideoCall, setIsConnectingVideoCall] = useState(false);
36
+ const [videoCallError, setVideoCallError] = useState(null);
34
37
  const navigation = useNavigation();
35
38
  const appContext = useContext(AppContext);
36
39
  const insets = useSafeAreaInsets();
@@ -151,7 +154,13 @@ const DebugNavigationPanel = () => {
151
154
  }, {
152
155
  screen: 'MRZTestScreen',
153
156
  label: 'MRZ Test',
154
- icon: ICONS.BUG_REPORT
157
+ icon: ICONS.BUG_REPORT,
158
+ isDebug: true
159
+ }, {
160
+ screen: 'BarcodeTestScreen',
161
+ label: 'Barcode Test',
162
+ icon: ICONS.BUG_REPORT,
163
+ isDebug: true
155
164
  }];
156
165
  const selectScreen = useCallback(screen => {
157
166
  if (!screen.hasOptions) {
@@ -167,9 +176,63 @@ const DebugNavigationPanel = () => {
167
176
  setAllowIdCard(true);
168
177
  setAllowPassport(true);
169
178
  }, [navigation]);
170
- const jumpToScreen = useCallback(() => {
171
- if (!selectedScreen) return;
179
+ const handleVideoCallTest = useCallback(async () => {
180
+ setIsConnectingVideoCall(true);
181
+ setVideoCallError(null);
182
+ try {
183
+ const fallbackUrl = (appContext.baseUrl ?? '').replace(/\/$/, '');
184
+ let response;
185
+ let usedBaseUrl = DEV_BASE_URL;
186
+ try {
187
+ response = await fetch(`${DEV_BASE_URL}/api/test/video-sessions/latest`);
188
+ } catch {
189
+ usedBaseUrl = fallbackUrl;
190
+ response = await fetch(`${fallbackUrl}/api/test/video-sessions/latest`);
191
+ }
192
+ const text = await response.text();
193
+ let data;
194
+ try {
195
+ data = JSON.parse(text);
196
+ } catch {
197
+ throw new Error(`Server returned non-JSON (${response.status}): ${text.slice(0, 120)}`);
198
+ }
199
+ if (!response.ok || !data.success) {
200
+ throw new Error(data.message || 'No active test session found');
201
+ }
202
+ // Point SDK at the server where the test session lives.
203
+ // Do NOT call setBaseUrl (state setter) — it triggers a useMemo rebuild in
204
+ // Trustchex.tsx that resets identificationInfo to an empty object before the
205
+ // screen can render. Direct mutation is sufficient for the test flow.
206
+ appContext.baseUrl = usedBaseUrl;
207
+ appContext.isTestVideoSession = true;
208
+ appContext.identificationInfo.identificationId = data.identificationId;
209
+ appContext.identificationInfo.videoSessionId = data.videoSessionId;
210
+ appContext.identificationInfo.sessionId = data.verificationSessionId;
172
211
 
212
+ // Set up a mini workflow: eID scan → video call
213
+ const eidStep = {
214
+ type: 'IDENTITY_DOCUMENT_EID_SCAN',
215
+ required: false,
216
+ data: {
217
+ voiceGuidanceActive: false
218
+ }
219
+ };
220
+ const videoCallStep = {
221
+ type: 'VIDEO_CALL',
222
+ required: true,
223
+ data: {}
224
+ };
225
+ appContext.workflowSteps = [eidStep, videoCallStep];
226
+ appContext.currentWorkflowStep = eidStep;
227
+ navigation.navigate('IdentityDocumentEIDScanningScreen');
228
+ setIsExpanded(false);
229
+ } catch (err) {
230
+ setVideoCallError(err.message || 'Failed to connect');
231
+ } finally {
232
+ setIsConnectingVideoCall(false);
233
+ }
234
+ }, [appContext, navigation]);
235
+ const jumpToScreen = useCallback(() => {
173
236
  // Validate document type selection for document scanning screens
174
237
  if (selectedScreen.hasDocumentTypeOption && !allowIdCard && !allowPassport) {
175
238
  // Don't navigate if no document types are selected
@@ -272,17 +335,56 @@ const DebugNavigationPanel = () => {
272
335
  })
273
336
  })]
274
337
  })]
275
- }) : screens.map(screen => /*#__PURE__*/_jsxs(TouchableOpacity, {
276
- style: styles.screenButton,
277
- onPress: () => selectScreen(screen),
278
- children: [/*#__PURE__*/_jsx(Text, {
279
- style: styles.screenIcon,
280
- children: screen.icon
281
- }), /*#__PURE__*/_jsx(Text, {
282
- style: styles.screenLabel,
283
- children: screen.label
338
+ }) : /*#__PURE__*/_jsxs(_Fragment, {
339
+ children: [screens.filter(s => !s.isDebug).map(screen => /*#__PURE__*/_jsxs(TouchableOpacity, {
340
+ style: styles.screenButton,
341
+ onPress: () => selectScreen(screen),
342
+ children: [/*#__PURE__*/_jsx(Text, {
343
+ style: styles.screenIcon,
344
+ children: screen.icon
345
+ }), /*#__PURE__*/_jsx(Text, {
346
+ style: styles.screenLabel,
347
+ children: screen.label
348
+ })]
349
+ }, screen.screen)), /*#__PURE__*/_jsxs(View, {
350
+ style: styles.sectionDivider,
351
+ children: [/*#__PURE__*/_jsx(View, {
352
+ style: styles.sectionDividerLine
353
+ }), /*#__PURE__*/_jsx(Text, {
354
+ style: styles.sectionDividerLabel,
355
+ children: "DEBUG TOOLS"
356
+ }), /*#__PURE__*/_jsx(View, {
357
+ style: styles.sectionDividerLine
358
+ })]
359
+ }), screens.filter(s => s.isDebug).map(screen => /*#__PURE__*/_jsxs(TouchableOpacity, {
360
+ style: styles.debugScreenButton,
361
+ onPress: () => selectScreen(screen),
362
+ children: [/*#__PURE__*/_jsx(Text, {
363
+ style: styles.screenIcon,
364
+ children: screen.icon
365
+ }), /*#__PURE__*/_jsx(Text, {
366
+ style: styles.debugScreenLabel,
367
+ children: screen.label
368
+ })]
369
+ }, screen.screen)), /*#__PURE__*/_jsxs(TouchableOpacity, {
370
+ style: [styles.videoCallTestButton, isConnectingVideoCall && styles.videoCallTestButtonDisabled],
371
+ onPress: handleVideoCallTest,
372
+ disabled: isConnectingVideoCall,
373
+ children: [isConnectingVideoCall ? /*#__PURE__*/_jsx(ActivityIndicator, {
374
+ size: "small",
375
+ color: "#4CAF50"
376
+ }) : /*#__PURE__*/_jsx(Text, {
377
+ style: styles.screenIcon,
378
+ children: "\uD83D\uDCF9"
379
+ }), /*#__PURE__*/_jsx(Text, {
380
+ style: styles.videoCallTestLabel,
381
+ children: isConnectingVideoCall ? 'Connecting...' : 'Video Call Test'
382
+ })]
383
+ }), videoCallError && /*#__PURE__*/_jsx(Text, {
384
+ style: styles.videoCallTestError,
385
+ children: videoCallError
284
386
  })]
285
- }, screen.screen))]
387
+ })]
286
388
  }), /*#__PURE__*/_jsxs(TouchableOpacity, {
287
389
  style: styles.toggleButton,
288
390
  onPress: () => {
@@ -461,6 +563,70 @@ const styles = StyleSheet.create({
461
563
  },
462
564
  goButtonTextDisabled: {
463
565
  color: '#CCCCCC'
566
+ },
567
+ sectionDivider: {
568
+ flexDirection: 'row',
569
+ alignItems: 'center',
570
+ marginTop: 4,
571
+ marginBottom: 6,
572
+ gap: 6
573
+ },
574
+ sectionDividerLine: {
575
+ flex: 1,
576
+ height: 1,
577
+ backgroundColor: 'rgba(255, 167, 38, 0.4)'
578
+ },
579
+ sectionDividerLabel: {
580
+ color: '#FFA726',
581
+ fontSize: 9,
582
+ fontWeight: 'bold',
583
+ letterSpacing: 1
584
+ },
585
+ debugScreenButton: {
586
+ flexDirection: 'row',
587
+ alignItems: 'center',
588
+ justifyContent: 'flex-start',
589
+ backgroundColor: 'rgba(255, 167, 38, 0.15)',
590
+ borderWidth: 1,
591
+ borderColor: 'rgba(255, 167, 38, 0.4)',
592
+ paddingHorizontal: 12,
593
+ paddingVertical: 8,
594
+ borderRadius: 4,
595
+ marginBottom: 6
596
+ },
597
+ debugScreenLabel: {
598
+ color: '#FFA726',
599
+ fontSize: 12,
600
+ fontWeight: '500',
601
+ flex: 1
602
+ },
603
+ videoCallTestButton: {
604
+ flexDirection: 'row',
605
+ alignItems: 'center',
606
+ justifyContent: 'flex-start',
607
+ backgroundColor: 'rgba(76, 175, 80, 0.15)',
608
+ borderWidth: 1,
609
+ borderColor: 'rgba(76, 175, 80, 0.5)',
610
+ paddingHorizontal: 12,
611
+ paddingVertical: 8,
612
+ borderRadius: 4,
613
+ marginBottom: 6,
614
+ gap: 8
615
+ },
616
+ videoCallTestButtonDisabled: {
617
+ opacity: 0.5
618
+ },
619
+ videoCallTestLabel: {
620
+ color: '#4CAF50',
621
+ fontSize: 12,
622
+ fontWeight: '600',
623
+ flex: 1
624
+ },
625
+ videoCallTestError: {
626
+ color: '#FF5252',
627
+ fontSize: 10,
628
+ marginBottom: 6,
629
+ paddingHorizontal: 4
464
630
  }
465
631
  });
466
632
  export default DebugNavigationPanel;
@@ -73,7 +73,6 @@ const EIDScanner = ({
73
73
  const appContext = React.useContext(AppContext);
74
74
  const [voiceGuidanceMessage, setVoiceGuidanceMessage] = useState();
75
75
  const [attemptNumber, setAttemptNumber] = useState(0);
76
- const lastVoiceGuidanceMessage = useRef('');
77
76
  const readNFC = useCallback(async () => {
78
77
  const startTime = Date.now();
79
78
  const currentAttempt = attemptNumber + 1;
@@ -82,7 +81,6 @@ const EIDScanner = ({
82
81
  setIsScanned(false);
83
82
  setProgress(0);
84
83
  setProgressStage('');
85
- lastVoiceGuidanceMessage.current = '';
86
84
  resetLastMessage();
87
85
 
88
86
  // Track EID scan start using analytics helper
@@ -241,8 +239,7 @@ const EIDScanner = ({
241
239
  }
242
240
  }, [isScanning, progress, pulseAnim]);
243
241
  useEffect(() => {
244
- if (voiceGuidanceMessage && appContext.currentWorkflowStep?.data?.voiceGuidanceActive && voiceGuidanceMessage !== lastVoiceGuidanceMessage.current) {
245
- lastVoiceGuidanceMessage.current = voiceGuidanceMessage;
242
+ if (voiceGuidanceMessage && appContext.currentWorkflowStep?.data?.voiceGuidanceActive) {
246
243
  speak(voiceGuidanceMessage, true);
247
244
  }
248
245
  }, [voiceGuidanceMessage, appContext.currentWorkflowStep?.data?.voiceGuidanceActive]);
@@ -94,6 +94,7 @@ const IdentityDocumentCamera = ({
94
94
  const isHologramDetectionInProgress = useRef(false); // Prevent concurrent hologram detection calls
95
95
  const isCompletionCallbackInvoked = useRef(false); // Prevent multiple callback invocations when COMPLETED
96
96
  const isStepTransitionInProgress = useRef(false); // Prevent duplicate step transitions from rapid frames
97
+ const isCompletionAnnouncementSpoken = useRef(false); // Prevent duplicate "scan completed" voice guidance
97
98
 
98
99
  const faceDetectionErrorCount = useRef(0);
99
100
  const brightnessHistory = useRef([]);
@@ -101,7 +102,6 @@ const IdentityDocumentCamera = ({
101
102
  const faceImages = useRef([]);
102
103
  const hologramImageCountRef = useRef(0);
103
104
  const [hologramImageCount, setHologramImageCount] = useState(0);
104
- const lastVoiceGuidanceMessage = useRef('');
105
105
  const [latestHologramFaceImage, setLatestHologramFaceImage] = useState(undefined);
106
106
  const lastFacePosition = useRef(null);
107
107
  const [documentPlaneBounds, setDocumentPlaneBounds] = useState(null);
@@ -135,6 +135,7 @@ const IdentityDocumentCamera = ({
135
135
  setIsActive(true);
136
136
  isCompletionCallbackInvoked.current = false; // Reset callback flag when starting new scan
137
137
  isStepTransitionInProgress.current = false;
138
+ isCompletionAnnouncementSpoken.current = false;
138
139
  } else {
139
140
  setIsActive(false);
140
141
  faceImages.current = [];
@@ -148,9 +149,9 @@ const IdentityDocumentCamera = ({
148
149
  lastValidMRZFields.current = null;
149
150
  validMRZConsecutiveCount.current = 0;
150
151
  cachedBarcode.current = null;
151
- lastVoiceGuidanceMessage.current = '';
152
152
  isCompletionCallbackInvoked.current = false;
153
153
  isStepTransitionInProgress.current = false;
154
+ isCompletionAnnouncementSpoken.current = false;
154
155
  // Clear all captured image states from previous scan
155
156
  setCurrentFaceImage(undefined);
156
157
  setCurrentHologramImage(undefined);
@@ -163,9 +164,9 @@ const IdentityDocumentCamera = ({
163
164
  hologramImageCountRef.current = 0;
164
165
  setHologramImageCount(0);
165
166
  setLatestHologramFaceImage(undefined);
166
- lastVoiceGuidanceMessage.current = '';
167
167
  isCompletionCallbackInvoked.current = false; // Reset callback flag on unmount
168
168
  isStepTransitionInProgress.current = false;
169
+ isCompletionAnnouncementSpoken.current = false;
169
170
  // Clear all captured image states on unmount
170
171
  setCurrentFaceImage(undefined);
171
172
  setCurrentHologramImage(undefined);
@@ -176,9 +177,17 @@ const IdentityDocumentCamera = ({
176
177
  useEffect(() => {
177
178
  if (!hasGuideShown || !appContext.currentWorkflowStep?.data?.voiceGuidanceActive) return;
178
179
  const message = getStatusMessage(nextStep, status, detectedDocumentType, isBrightnessLow, isFrameBlurry, allElementsDetected, elementsOutsideScanArea, t);
179
- if (message && message !== lastVoiceGuidanceMessage.current) {
180
- lastVoiceGuidanceMessage.current = message;
181
- speak(message, true);
180
+ if (message) {
181
+ if (nextStep === 'COMPLETED') {
182
+ if (isCompletionAnnouncementSpoken.current) {
183
+ return;
184
+ }
185
+ isCompletionAnnouncementSpoken.current = true;
186
+ } else {
187
+ isCompletionAnnouncementSpoken.current = false;
188
+ }
189
+ const shouldBypassInterval = nextStep !== 'COMPLETED';
190
+ speak(message, shouldBypassInterval);
182
191
  }
183
192
  }, [appContext.currentWorkflowStep?.data?.voiceGuidanceActive, hasGuideShown, isBrightnessLow, isFrameBlurry, nextStep, status, detectedDocumentType, allElementsDetected, elementsOutsideScanArea, t]);
184
193
  useEffect(() => {
@@ -207,6 +216,18 @@ const IdentityDocumentCamera = ({
207
216
  isStepTransitionInProgress.current = false;
208
217
  }, [nextStep]);
209
218
 
219
+ // Update status bar based on guide visibility
220
+ useEffect(() => {
221
+ if (hasGuideShown) {
222
+ // Scanning camera view - use light icons on dark background
223
+ StatusBar.setBarStyle('light-content', true);
224
+ } else {
225
+ // Guide screen with white background - use dark icons
226
+ StatusBar.setBarStyle('dark-content', true);
227
+ StatusBar.setBackgroundColor('#ffffff', true);
228
+ }
229
+ }, [hasGuideShown]);
230
+
210
231
  // Error flash animation - flash red text when wrong side detected
211
232
  useEffect(() => {
212
233
  if (status === 'INCORRECT') {
@@ -1493,8 +1514,8 @@ const IdentityDocumentCamera = ({
1493
1514
  return /*#__PURE__*/_jsxs(View, {
1494
1515
  style: StyleSheet.absoluteFill,
1495
1516
  children: [/*#__PURE__*/_jsx(StatusBar, {
1496
- barStyle: "light-content",
1497
- backgroundColor: "transparent",
1517
+ barStyle: hasGuideShown ? 'light-content' : 'dark-content',
1518
+ backgroundColor: hasGuideShown ? 'transparent' : '#ffffff',
1498
1519
  translucent: true
1499
1520
  }), !hasGuideShown ? /*#__PURE__*/_jsxs(SafeAreaView, {
1500
1521
  style: styles.guide,