@trustchex/react-native-sdk 1.374.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 (137) hide show
  1. package/android/src/main/java/com/trustchex/reactnativesdk/camera/TrustchexCameraView.kt +1 -21
  2. package/android/src/main/java/com/trustchex/reactnativesdk/mlkit/MLKitModule.kt +1 -1
  3. package/android/src/main/java/com/trustchex/reactnativesdk/opencv/OpenCVModule.kt +636 -301
  4. package/ios/Camera/TrustchexCameraView.swift +9 -20
  5. package/ios/MLKit/MLKitModule.swift +1 -1
  6. package/ios/OpenCV/OpenCVHelper.h +0 -7
  7. package/ios/OpenCV/OpenCVHelper.mm +0 -60
  8. package/ios/OpenCV/OpenCVModule.h +0 -4
  9. package/ios/OpenCV/OpenCVModule.mm +440 -358
  10. package/lib/module/Screens/Debug/BarcodeTestScreen.js +308 -0
  11. package/lib/module/Screens/Debug/MRZTestScreen.js +105 -13
  12. package/lib/module/Screens/Dynamic/ContractAcceptanceScreen.js +49 -29
  13. package/lib/module/Screens/Dynamic/IdentityDocumentEIDScanningScreen.js +5 -0
  14. package/lib/module/Screens/Dynamic/IdentityDocumentScanningScreen.js +5 -0
  15. package/lib/module/Screens/Dynamic/LivenessDetectionScreen.js +26 -6
  16. package/lib/module/Screens/Dynamic/VideoCallScreen.js +676 -0
  17. package/lib/module/Screens/Static/OTPVerificationScreen.js +6 -0
  18. package/lib/module/Screens/Static/QrCodeScanningScreen.js +7 -1
  19. package/lib/module/Screens/Static/ResultScreen.js +27 -13
  20. package/lib/module/Screens/Static/VerificationSessionCheckScreen.js +51 -51
  21. package/lib/module/Shared/Animations/video-call.json +1 -0
  22. package/lib/module/Shared/Components/DebugNavigationPanel.js +180 -14
  23. package/lib/module/Shared/Components/DebugOverlay.js +541 -0
  24. package/lib/module/Shared/Components/EIDScanner.js +1 -4
  25. package/lib/module/Shared/Components/IdentityDocumentCamera.constants.js +44 -0
  26. package/lib/module/Shared/Components/IdentityDocumentCamera.flows.js +270 -0
  27. package/lib/module/Shared/Components/IdentityDocumentCamera.js +702 -1703
  28. package/lib/module/Shared/Components/IdentityDocumentCamera.types.js +3 -0
  29. package/lib/module/Shared/Components/IdentityDocumentCamera.utils.js +273 -0
  30. package/lib/module/Shared/Components/NavigationManager.js +15 -3
  31. package/lib/module/Shared/Contexts/AppContext.js +1 -0
  32. package/lib/module/Shared/Libs/SignalingClient.js +128 -0
  33. package/lib/module/Shared/Libs/analytics.utils.js +4 -0
  34. package/lib/module/Shared/Libs/deeplink.utils.js +9 -1
  35. package/lib/module/Shared/Libs/http-client.js +9 -0
  36. package/lib/module/Shared/Libs/promise.utils.js +16 -2
  37. package/lib/module/Shared/Libs/status-bar.utils.js +21 -0
  38. package/lib/module/Shared/Services/DataUploadService.js +294 -0
  39. package/lib/module/Shared/Services/VideoSessionService.js +156 -0
  40. package/lib/module/Shared/Services/WebRTCService.js +510 -0
  41. package/lib/module/Shared/Types/analytics.types.js +2 -0
  42. package/lib/module/Translation/Resources/en.js +20 -0
  43. package/lib/module/Translation/Resources/tr.js +20 -0
  44. package/lib/module/Trustchex.js +10 -0
  45. package/lib/module/version.js +1 -1
  46. package/lib/typescript/src/Screens/Debug/BarcodeTestScreen.d.ts +3 -0
  47. package/lib/typescript/src/Screens/Debug/BarcodeTestScreen.d.ts.map +1 -0
  48. package/lib/typescript/src/Screens/Debug/MRZTestScreen.d.ts.map +1 -1
  49. package/lib/typescript/src/Screens/Dynamic/ContractAcceptanceScreen.d.ts.map +1 -1
  50. package/lib/typescript/src/Screens/Dynamic/IdentityDocumentEIDScanningScreen.d.ts.map +1 -1
  51. package/lib/typescript/src/Screens/Dynamic/IdentityDocumentScanningScreen.d.ts.map +1 -1
  52. package/lib/typescript/src/Screens/Dynamic/LivenessDetectionScreen.d.ts.map +1 -1
  53. package/lib/typescript/src/Screens/Dynamic/VideoCallScreen.d.ts +3 -0
  54. package/lib/typescript/src/Screens/Dynamic/VideoCallScreen.d.ts.map +1 -0
  55. package/lib/typescript/src/Screens/Static/OTPVerificationScreen.d.ts.map +1 -1
  56. package/lib/typescript/src/Screens/Static/QrCodeScanningScreen.d.ts.map +1 -1
  57. package/lib/typescript/src/Screens/Static/ResultScreen.d.ts.map +1 -1
  58. package/lib/typescript/src/Screens/Static/VerificationSessionCheckScreen.d.ts.map +1 -1
  59. package/lib/typescript/src/Shared/Components/DebugNavigationPanel.d.ts.map +1 -1
  60. package/lib/typescript/src/Shared/Components/DebugOverlay.d.ts +30 -0
  61. package/lib/typescript/src/Shared/Components/DebugOverlay.d.ts.map +1 -0
  62. package/lib/typescript/src/Shared/Components/EIDScanner.d.ts.map +1 -1
  63. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.constants.d.ts +35 -0
  64. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.constants.d.ts.map +1 -0
  65. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.d.ts +3 -56
  66. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.d.ts.map +1 -1
  67. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.flows.d.ts +88 -0
  68. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.flows.d.ts.map +1 -0
  69. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.types.d.ts +116 -0
  70. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.types.d.ts.map +1 -0
  71. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.utils.d.ts +93 -0
  72. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.utils.d.ts.map +1 -0
  73. package/lib/typescript/src/Shared/Components/NavigationManager.d.ts.map +1 -1
  74. package/lib/typescript/src/Shared/Contexts/AppContext.d.ts +1 -0
  75. package/lib/typescript/src/Shared/Contexts/AppContext.d.ts.map +1 -1
  76. package/lib/typescript/src/Shared/Libs/SignalingClient.d.ts +24 -0
  77. package/lib/typescript/src/Shared/Libs/SignalingClient.d.ts.map +1 -0
  78. package/lib/typescript/src/Shared/Libs/analytics.utils.d.ts.map +1 -1
  79. package/lib/typescript/src/Shared/Libs/deeplink.utils.d.ts.map +1 -1
  80. package/lib/typescript/src/Shared/Libs/http-client.d.ts.map +1 -1
  81. package/lib/typescript/src/Shared/Libs/promise.utils.d.ts.map +1 -1
  82. package/lib/typescript/src/Shared/Libs/status-bar.utils.d.ts +9 -0
  83. package/lib/typescript/src/Shared/Libs/status-bar.utils.d.ts.map +1 -0
  84. package/lib/typescript/src/Shared/Services/DataUploadService.d.ts +25 -0
  85. package/lib/typescript/src/Shared/Services/DataUploadService.d.ts.map +1 -0
  86. package/lib/typescript/src/Shared/Services/VideoSessionService.d.ts +33 -0
  87. package/lib/typescript/src/Shared/Services/VideoSessionService.d.ts.map +1 -0
  88. package/lib/typescript/src/Shared/Services/WebRTCService.d.ts +58 -0
  89. package/lib/typescript/src/Shared/Services/WebRTCService.d.ts.map +1 -0
  90. package/lib/typescript/src/Shared/Types/analytics.types.d.ts +2 -0
  91. package/lib/typescript/src/Shared/Types/analytics.types.d.ts.map +1 -1
  92. package/lib/typescript/src/Shared/Types/identificationInfo.d.ts +4 -1
  93. package/lib/typescript/src/Shared/Types/identificationInfo.d.ts.map +1 -1
  94. package/lib/typescript/src/Translation/Resources/en.d.ts +20 -0
  95. package/lib/typescript/src/Translation/Resources/en.d.ts.map +1 -1
  96. package/lib/typescript/src/Translation/Resources/tr.d.ts +20 -0
  97. package/lib/typescript/src/Translation/Resources/tr.d.ts.map +1 -1
  98. package/lib/typescript/src/Trustchex.d.ts.map +1 -1
  99. package/lib/typescript/src/version.d.ts +1 -1
  100. package/package.json +29 -2
  101. package/src/Screens/Debug/BarcodeTestScreen.tsx +317 -0
  102. package/src/Screens/Debug/MRZTestScreen.tsx +107 -13
  103. package/src/Screens/Dynamic/ContractAcceptanceScreen.tsx +59 -33
  104. package/src/Screens/Dynamic/IdentityDocumentEIDScanningScreen.tsx +6 -0
  105. package/src/Screens/Dynamic/IdentityDocumentScanningScreen.tsx +6 -0
  106. package/src/Screens/Dynamic/LivenessDetectionScreen.tsx +34 -6
  107. package/src/Screens/Dynamic/VideoCallScreen.tsx +764 -0
  108. package/src/Screens/Static/OTPVerificationScreen.tsx +6 -0
  109. package/src/Screens/Static/QrCodeScanningScreen.tsx +7 -1
  110. package/src/Screens/Static/ResultScreen.tsx +58 -23
  111. package/src/Screens/Static/VerificationSessionCheckScreen.tsx +58 -72
  112. package/src/Shared/Animations/video-call.json +1 -0
  113. package/src/Shared/Components/DebugNavigationPanel.tsx +185 -9
  114. package/src/Shared/Components/DebugOverlay.tsx +656 -0
  115. package/src/Shared/Components/EIDScanner.tsx +1 -5
  116. package/src/Shared/Components/IdentityDocumentCamera.constants.ts +44 -0
  117. package/src/Shared/Components/IdentityDocumentCamera.flows.ts +342 -0
  118. package/src/Shared/Components/IdentityDocumentCamera.tsx +1089 -2465
  119. package/src/Shared/Components/IdentityDocumentCamera.types.ts +136 -0
  120. package/src/Shared/Components/IdentityDocumentCamera.utils.ts +364 -0
  121. package/src/Shared/Components/NavigationManager.tsx +14 -1
  122. package/src/Shared/Contexts/AppContext.ts +2 -0
  123. package/src/Shared/Libs/SignalingClient.ts +189 -0
  124. package/src/Shared/Libs/analytics.utils.ts +4 -0
  125. package/src/Shared/Libs/deeplink.utils.ts +12 -1
  126. package/src/Shared/Libs/http-client.ts +10 -0
  127. package/src/Shared/Libs/promise.utils.ts +16 -2
  128. package/src/Shared/Libs/status-bar.utils.ts +19 -0
  129. package/src/Shared/Services/DataUploadService.ts +395 -0
  130. package/src/Shared/Services/VideoSessionService.ts +190 -0
  131. package/src/Shared/Services/WebRTCService.ts +636 -0
  132. package/src/Shared/Types/analytics.types.ts +2 -0
  133. package/src/Shared/Types/identificationInfo.ts +5 -1
  134. package/src/Translation/Resources/en.ts +25 -0
  135. package/src/Translation/Resources/tr.ts +27 -0
  136. package/src/Trustchex.tsx +12 -2
  137. package/src/version.ts +1 -1
@@ -0,0 +1,676 @@
1
+ "use strict";
2
+
3
+ import React, { useEffect, useState, useRef, useContext } from 'react';
4
+ import { Dimensions, Platform, StyleSheet, Text, TouchableOpacity, View, ActivityIndicator, PermissionsAndroid, StatusBar } from 'react-native';
5
+ import { RTCView } from 'react-native-webrtc';
6
+ import InCallManager from 'react-native-incall-manager';
7
+ import LottieView from 'lottie-react-native';
8
+ import { useTranslation } from 'react-i18next';
9
+ import { useStatusBarWhiteBackground } from "../../Shared/Libs/status-bar.utils.js";
10
+ import { WebRTCService } from "../../Shared/Services/WebRTCService.js";
11
+ import { VideoSessionService } from "../../Shared/Services/VideoSessionService.js";
12
+ import { DataUploadService } from "../../Shared/Services/DataUploadService.js";
13
+ import AppContext from "../../Shared/Contexts/AppContext.js";
14
+ import NavigationManager from "../../Shared/Components/NavigationManager.js";
15
+ import StyledButton from "../../Shared/Components/StyledButton.js";
16
+ import { trackVerificationStart, trackVerificationComplete } from "../../Shared/Libs/analytics.utils.js";
17
+ import { useKeepAwake } from "../../Shared/Libs/native-keep-awake.utils.js";
18
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
19
+ const {
20
+ width,
21
+ height
22
+ } = Dimensions.get('window');
23
+ const VideoCallScreen = ({
24
+ navigation
25
+ }) => {
26
+ useKeepAwake();
27
+ const appContext = useContext(AppContext);
28
+ const {
29
+ baseUrl,
30
+ identificationInfo,
31
+ onError
32
+ } = appContext;
33
+ const {
34
+ t
35
+ } = useTranslation();
36
+
37
+ // Configure status bar for white background
38
+ useStatusBarWhiteBackground();
39
+ const [hasGuideShown, setHasGuideShown] = useState(false);
40
+ const [localStream, setLocalStream] = useState(null);
41
+ const [remoteStream, setRemoteStream] = useState(null);
42
+ const [connectionState, setConnectionState] = useState('connecting');
43
+ const [queuePosition, setQueuePosition] = useState(null);
44
+ const [errorMessage, setErrorMessage] = useState(null);
45
+ const [showLeaveButton, setShowLeaveButton] = useState(false);
46
+ const [uploadStatus, setUploadStatus] = useState('pending');
47
+ const [agentInstructions, setAgentInstructions] = useState([]);
48
+ const [localStreamKey, setLocalStreamKey] = useState(0);
49
+
50
+ // Update status bar when guide visibility changes
51
+ useEffect(() => {
52
+ if (hasGuideShown) {
53
+ // Video call view - use light icons
54
+ StatusBar.setBarStyle('light-content', true);
55
+ } else {
56
+ // Guide screen with white background - use dark icons
57
+ StatusBar.setBarStyle('dark-content', true);
58
+ StatusBar.setBackgroundColor('#ffffff', true);
59
+ }
60
+ }, [hasGuideShown]);
61
+ const serviceRef = useRef(null);
62
+ const videoSessionServiceRef = useRef(null);
63
+ const dataUploadServiceRef = useRef(null);
64
+ const uploadPromiseMapRef = useRef(new Map());
65
+ const navigationManagerRef = useRef(null);
66
+ const joinedSessionIdRef = useRef(null);
67
+ const callConnectedRef = useRef(false);
68
+ const sessionEndedRef = useRef(false);
69
+ const handleRemoteCommand = command => {
70
+ switch (command.type) {
71
+ case 'toggleFlash':
72
+ serviceRef.current?.toggleFlash();
73
+ break;
74
+ case 'displayInstruction':
75
+ setAgentInstructions(prev => [...prev, {
76
+ id: command.itemId || Date.now().toString(),
77
+ text: command.text,
78
+ timestamp: Date.now()
79
+ }].slice(-3)); // Keep only last 3 instructions
80
+ break;
81
+ }
82
+ };
83
+ useEffect(() => {
84
+ // Don't initialize until guide is dismissed
85
+ if (!hasGuideShown) return;
86
+ const identificationId = identificationInfo.identificationId;
87
+ const verificationSessionId = identificationInfo.sessionId;
88
+ const existingVideoSessionId = identificationInfo.videoSessionId;
89
+ const hasCollectedData = !!identificationInfo.scannedDocument || !!identificationInfo.livenessDetection;
90
+ let mounted = true;
91
+ let queueUnsubscribe = null;
92
+ let callStartTime = null;
93
+ let heartbeatInterval = null;
94
+ let leaveButtonTimeout = null;
95
+
96
+ // Track video call started
97
+ trackVerificationStart('VIDEO_CALL');
98
+ const init = async () => {
99
+ if (!baseUrl) {
100
+ console.error('Missing base URL');
101
+ return;
102
+ }
103
+ if (!identificationId) {
104
+ console.error('Missing identification ID');
105
+ return;
106
+ }
107
+
108
+ // Upload collected data in background (don't block video call initialization)
109
+ // This allows the agent to see the data during the video call
110
+ const uploadCollectedData = async () => {
111
+ const uploadKey = `${identificationId}:${verificationSessionId || ''}`;
112
+
113
+ // If upload already in progress, return the same promise
114
+ const existingPromise = uploadPromiseMapRef.current.get(uploadKey);
115
+ if (existingPromise) {
116
+ return existingPromise;
117
+ }
118
+
119
+ // Create the upload promise
120
+ const uploadPromise = (async () => {
121
+ if (hasCollectedData) {
122
+ setUploadStatus('uploading');
123
+ try {
124
+ const uploadService = new DataUploadService(baseUrl);
125
+ dataUploadServiceRef.current = uploadService;
126
+ const uploaded = await uploadService.uploadCollectedData(identificationInfo);
127
+ if (!uploaded) {
128
+ console.warn('[VideoCallScreen] Upload service returned false');
129
+ if (mounted) {
130
+ setUploadStatus('failed');
131
+ }
132
+ return false;
133
+ }
134
+ if (mounted) {
135
+ setUploadStatus('done');
136
+ }
137
+ return true;
138
+ } catch (error) {
139
+ console.error('[VideoCallScreen] Failed to upload collected data:', error);
140
+ if (mounted) {
141
+ setUploadStatus('failed');
142
+ }
143
+ return false;
144
+ } finally {
145
+ uploadPromiseMapRef.current.delete(uploadKey);
146
+ }
147
+ } else {
148
+ setUploadStatus('done');
149
+ return true;
150
+ }
151
+ })();
152
+
153
+ // Store the promise before awaiting
154
+ uploadPromiseMapRef.current.set(uploadKey, uploadPromise);
155
+ return uploadPromise;
156
+ };
157
+ const uploadedBeforeCall = await uploadCollectedData();
158
+ if (hasCollectedData && !uploadedBeforeCall) {
159
+ if (mounted) {
160
+ setErrorMessage('Failed to upload verification data before video call.');
161
+ setConnectionState('failed');
162
+ }
163
+ return;
164
+ }
165
+
166
+ // Fetch or create video session if videoSessionId is not available
167
+ let videoSessionId = existingVideoSessionId;
168
+ if (!videoSessionId) {
169
+ try {
170
+ // Build URL with both identificationId and verificationSessionId for better lookup.
171
+ // Avoid new URL() + searchParams.set — not implemented on Android Hermes.
172
+ let sessionUrl = `${baseUrl}/api/app/mobile/identifications/${identificationId}/video-session`;
173
+ if (verificationSessionId) {
174
+ sessionUrl += `?verificationSessionId=${encodeURIComponent(verificationSessionId)}`;
175
+ }
176
+ const response = await fetch(sessionUrl, {
177
+ method: 'GET',
178
+ headers: {
179
+ 'Content-Type': 'application/json'
180
+ }
181
+ });
182
+ if (!response.ok) {
183
+ let errorMessage = 'Unknown error';
184
+ try {
185
+ const errorBody = await response.json();
186
+ errorMessage = errorBody.message || errorBody.error || response.statusText;
187
+ if (errorBody.details) {
188
+ errorMessage = `${errorMessage}: ${errorBody.details}`;
189
+ }
190
+ } catch {
191
+ errorMessage = response.statusText;
192
+ }
193
+ console.error('[VideoCallScreen] Server error response:', {
194
+ status: response.status,
195
+ message: errorMessage,
196
+ url: sessionUrl
197
+ });
198
+ throw new Error(`Failed to get video session: ${response.status} ${errorMessage}`);
199
+ }
200
+ const data = await response.json();
201
+ videoSessionId = data.videoSessionId;
202
+ } catch (error) {
203
+ console.error('[VideoCallScreen] Failed to get video session:', error);
204
+ if (mounted) {
205
+ setErrorMessage('Failed to initialize video session. Please try again.');
206
+ setConnectionState('failed');
207
+ }
208
+ return;
209
+ }
210
+ }
211
+ if (!videoSessionId) {
212
+ console.error('Missing video session ID');
213
+ if (mounted) {
214
+ setErrorMessage('Unable to start video call. Please try again.');
215
+ setConnectionState('failed');
216
+ }
217
+ return;
218
+ }
219
+
220
+ // Initialize video session service (mobile API is public, no auth token needed)
221
+ const videoSessionService = new VideoSessionService(baseUrl, identificationId);
222
+ videoSessionServiceRef.current = videoSessionService;
223
+
224
+ // Try to join the session
225
+ try {
226
+ await videoSessionService.joinSession(videoSessionId);
227
+ joinedSessionIdRef.current = videoSessionId;
228
+
229
+ // Send heartbeat every 10s while in queue
230
+ heartbeatInterval = setInterval(() => {
231
+ if (!callConnectedRef.current) {
232
+ videoSessionService.sendHeartbeat(videoSessionId);
233
+ } else {
234
+ clearInterval(heartbeatInterval);
235
+ heartbeatInterval = null;
236
+ }
237
+ }, 10_000);
238
+
239
+ // Show leave button after 60s if still waiting
240
+ leaveButtonTimeout = setTimeout(() => {
241
+ if (mounted && !callConnectedRef.current) {
242
+ setShowLeaveButton(true);
243
+ }
244
+ }, 60_000);
245
+ } catch (error) {
246
+ console.error('[VideoCallScreen] Failed to join session:', error);
247
+ if (mounted) {
248
+ setErrorMessage(error?.message || 'Failed to join video session');
249
+ setConnectionState('failed');
250
+ }
251
+ return;
252
+ }
253
+
254
+ // Subscribe to queue updates
255
+ queueUnsubscribe = videoSessionService.subscribeToQueueUpdates(videoSessionId, position => {
256
+ if (mounted) {
257
+ setQueuePosition(position);
258
+ }
259
+ }, error => {
260
+ console.error('[VideoCallScreen] Queue update error:', error);
261
+ });
262
+
263
+ // Initialize WebRTC service
264
+ const service = new WebRTCService({
265
+ baseUrl,
266
+ sessionId: videoSessionId,
267
+ identificationId,
268
+ onRemoteStream: stream => {
269
+ if (mounted) {
270
+ setRemoteStream(stream);
271
+ }
272
+ },
273
+ onConnectionStateChange: state => {
274
+ if (mounted) setConnectionState(state);
275
+ if (state === 'connected') {
276
+ callConnectedRef.current = true;
277
+ setShowLeaveButton(false);
278
+ if (leaveButtonTimeout) {
279
+ clearTimeout(leaveButtonTimeout);
280
+ leaveButtonTimeout = null;
281
+ }
282
+ }
283
+ if ((state === 'failed' || state === 'closed') && callConnectedRef.current && !sessionEndedRef.current) {
284
+ const durationMs = callStartTime ? Date.now() - callStartTime : 0;
285
+ trackVerificationComplete('VIDEO_CALL', false, durationMs / 1000);
286
+ service.cleanup();
287
+ if (mounted) {
288
+ if (appContext.currentWorkflowStep?.required) {
289
+ setErrorMessage(t('videoCallScreen.callNotCompleted'));
290
+ setConnectionState('failed');
291
+ } else {
292
+ navigation.navigate('ResultScreen');
293
+ }
294
+ }
295
+ }
296
+ },
297
+ onCommand: handleRemoteCommand,
298
+ onLocalStreamUpdate: newStream => {
299
+ setLocalStream(newStream);
300
+ setLocalStreamKey(k => k + 1);
301
+ },
302
+ onSessionEnded: state => {
303
+ sessionEndedRef.current = true;
304
+ // Track video call completed
305
+ const durationMs = callStartTime ? Date.now() - callStartTime : 0;
306
+ trackVerificationComplete('VIDEO_CALL', state === 'COMPLETED', durationMs / 1000);
307
+ // Always navigate to ResultScreen so data is submitted regardless of
308
+ // whether the agent ended the session as COMPLETED or FAILED
309
+ service.cleanup();
310
+ if (mounted) {
311
+ navigation.navigate('ResultScreen');
312
+ }
313
+ }
314
+ });
315
+ serviceRef.current = service;
316
+
317
+ // Request Permissions
318
+ if (Platform.OS === 'android') {
319
+ const granted = await PermissionsAndroid.requestMultiple([PermissionsAndroid.PERMISSIONS.CAMERA, PermissionsAndroid.PERMISSIONS.RECORD_AUDIO]);
320
+ if (granted['android.permission.CAMERA'] !== PermissionsAndroid.RESULTS.GRANTED || granted['android.permission.RECORD_AUDIO'] !== PermissionsAndroid.RESULTS.GRANTED) {
321
+ if (mounted) setConnectionState('permissions_denied');
322
+ return;
323
+ }
324
+ }
325
+ // For iOS, react-native-webrtc or VisionCamera usually triggers it,
326
+ // but explicit request via library is better if we had one.
327
+ // Assuming automatic or previously granted for now.
328
+
329
+ try {
330
+ const stream = await service.initialize();
331
+ if (mounted) {
332
+ setLocalStream(stream);
333
+ callStartTime = Date.now();
334
+ }
335
+
336
+ // Start InCallManager to route audio through speaker
337
+ InCallManager.start({
338
+ media: 'video'
339
+ });
340
+ InCallManager.setSpeakerphoneOn(true);
341
+ } catch (e) {
342
+ console.error('[VideoCallScreen] Failed to start call', e);
343
+ if (mounted) setConnectionState('failed');
344
+ }
345
+ };
346
+ init();
347
+ return () => {
348
+ mounted = false;
349
+ if (heartbeatInterval) clearInterval(heartbeatInterval);
350
+ if (leaveButtonTimeout) clearTimeout(leaveButtonTimeout);
351
+ queueUnsubscribe?.();
352
+ // If joined but call never connected, explicitly leave to drop from queue
353
+ if (joinedSessionIdRef.current && !callConnectedRef.current) {
354
+ videoSessionServiceRef.current?.leaveSession(joinedSessionIdRef.current);
355
+ }
356
+ serviceRef.current?.cleanup();
357
+ InCallManager.stop();
358
+ };
359
+ }, [baseUrl, identificationInfo.identificationId, identificationInfo.sessionId, identificationInfo.videoSessionId, hasGuideShown, appContext.currentWorkflowStep?.required, t, navigation, identificationInfo]);
360
+ const handleCloseSession = () => {
361
+ serviceRef.current?.cleanup();
362
+ // Call onError to notify parent and reset navigation to start over
363
+ onError?.('Video call session closed by user');
364
+ navigationManagerRef.current?.reset();
365
+ };
366
+ if (!identificationInfo || !identificationInfo.identificationId) {
367
+ return null;
368
+ }
369
+
370
+ // Show guide screen first
371
+ if (!hasGuideShown) {
372
+ return /*#__PURE__*/_jsxs(View, {
373
+ style: styles.guide,
374
+ children: [/*#__PURE__*/_jsx(LottieView, {
375
+ source: require('../../Shared/Animations/video-call.json'),
376
+ style: styles.guideAnimation,
377
+ loop: true,
378
+ autoPlay: true
379
+ }), /*#__PURE__*/_jsx(Text, {
380
+ style: styles.guideHeader,
381
+ children: t('videoCallScreen.guideHeader')
382
+ }), /*#__PURE__*/_jsxs(View, {
383
+ style: styles.guidePoints,
384
+ children: [/*#__PURE__*/_jsx(Text, {
385
+ style: styles.guideText,
386
+ children: t('videoCallScreen.guideText')
387
+ }), /*#__PURE__*/_jsxs(Text, {
388
+ style: styles.guideText,
389
+ children: ["\u2022 ", t('videoCallScreen.guidePoint1')]
390
+ }), /*#__PURE__*/_jsxs(Text, {
391
+ style: styles.guideText,
392
+ children: ["\u2022 ", t('videoCallScreen.guidePoint2')]
393
+ }), /*#__PURE__*/_jsxs(Text, {
394
+ style: styles.guideText,
395
+ children: ["\u2022 ", t('videoCallScreen.guidePoint3')]
396
+ }), /*#__PURE__*/_jsxs(Text, {
397
+ style: styles.guideText,
398
+ children: ["\u2022 ", t('videoCallScreen.guidePoint4')]
399
+ })]
400
+ }), /*#__PURE__*/_jsx(StyledButton, {
401
+ mode: "contained",
402
+ onPress: () => {
403
+ setHasGuideShown(true);
404
+ },
405
+ children: t('general.letsGo')
406
+ })]
407
+ });
408
+ }
409
+ return /*#__PURE__*/_jsxs(View, {
410
+ style: styles.container,
411
+ children: [errorMessage ? /*#__PURE__*/_jsxs(View, {
412
+ style: styles.errorContainer,
413
+ children: [/*#__PURE__*/_jsx(Text, {
414
+ style: styles.errorText,
415
+ children: errorMessage
416
+ }), /*#__PURE__*/_jsx(TouchableOpacity, {
417
+ style: styles.closeSessionButton,
418
+ onPress: handleCloseSession,
419
+ children: /*#__PURE__*/_jsx(Text, {
420
+ style: styles.buttonText,
421
+ children: t('videoCallScreen.closeSession')
422
+ })
423
+ }), /*#__PURE__*/_jsx(Text, {
424
+ style: styles.closeSessionHint,
425
+ children: t('videoCallScreen.closeSessionHint')
426
+ })]
427
+ }) : remoteStream ? /*#__PURE__*/_jsx(RTCView, {
428
+ streamURL: remoteStream.toURL(),
429
+ style: styles.remoteVideo,
430
+ objectFit: "contain",
431
+ mirror: false
432
+ }) : connectionState === 'connected' ?
433
+ /*#__PURE__*/
434
+ // WebRTC connected but remote stream not yet received — show black background
435
+ // so the PIP local video and controls are visible without "waiting" overlay
436
+ _jsx(View, {
437
+ style: styles.remoteVideo
438
+ }) : /*#__PURE__*/_jsxs(View, {
439
+ style: styles.waitingContainer,
440
+ children: [/*#__PURE__*/_jsx(ActivityIndicator, {
441
+ size: "large",
442
+ color: "#fff"
443
+ }), /*#__PURE__*/_jsx(Text, {
444
+ style: styles.waitingText,
445
+ children: t('videoCallScreen.waitingForAgent')
446
+ }), queuePosition !== null && /*#__PURE__*/_jsx(Text, {
447
+ style: styles.queueText,
448
+ children: t('videoCallScreen.queuePosition', {
449
+ position: queuePosition
450
+ })
451
+ }), /*#__PURE__*/_jsx(Text, {
452
+ style: styles.statusText,
453
+ children: t(`videoCallScreen.${connectionState}`)
454
+ }), showLeaveButton && /*#__PURE__*/_jsx(TouchableOpacity, {
455
+ style: styles.leaveQueueButton,
456
+ onPress: handleCloseSession,
457
+ children: /*#__PURE__*/_jsx(Text, {
458
+ style: styles.leaveQueueButtonText,
459
+ children: t('videoCallScreen.leaveQueue')
460
+ })
461
+ })]
462
+ }), localStream && /*#__PURE__*/_jsx(View, {
463
+ style: styles.localVideoContainer,
464
+ children: /*#__PURE__*/_jsx(RTCView, {
465
+ streamURL: localStream.toURL(),
466
+ style: styles.localVideo,
467
+ objectFit: "cover",
468
+ mirror: true,
469
+ zOrder: 1
470
+ }, localStreamKey)
471
+ }), agentInstructions.length > 0 && /*#__PURE__*/_jsxs(View, {
472
+ style: styles.instructionsContainer,
473
+ children: [/*#__PURE__*/_jsx(View, {
474
+ style: styles.instructionsHeader,
475
+ children: /*#__PURE__*/_jsx(Text, {
476
+ style: styles.instructionsTitle,
477
+ children: t('videoCallScreen.agentInstructions')
478
+ })
479
+ }), agentInstructions.map(instruction => /*#__PURE__*/_jsxs(View, {
480
+ style: styles.instructionItem,
481
+ children: [/*#__PURE__*/_jsx(Text, {
482
+ style: styles.instructionIcon,
483
+ children: "\uD83D\uDCCB"
484
+ }), /*#__PURE__*/_jsx(Text, {
485
+ style: styles.instructionText,
486
+ children: instruction.text
487
+ })]
488
+ }, instruction.id))]
489
+ }), /*#__PURE__*/_jsx(NavigationManager, {
490
+ ref: navigationManagerRef
491
+ })]
492
+ });
493
+ };
494
+ const styles = StyleSheet.create({
495
+ container: {
496
+ flex: 1,
497
+ backgroundColor: '#000'
498
+ },
499
+ remoteVideo: {
500
+ width: width,
501
+ height: height,
502
+ backgroundColor: '#000'
503
+ },
504
+ errorContainer: {
505
+ flex: 1,
506
+ justifyContent: 'center',
507
+ alignItems: 'center',
508
+ padding: 20
509
+ },
510
+ errorText: {
511
+ color: '#fff',
512
+ fontSize: 18,
513
+ textAlign: 'center',
514
+ marginBottom: 30
515
+ },
516
+ retryButton: {
517
+ backgroundColor: '#d32f2f',
518
+ padding: 15,
519
+ borderRadius: 8,
520
+ minWidth: 120,
521
+ alignItems: 'center'
522
+ },
523
+ closeSessionButton: {
524
+ backgroundColor: '#d32f2f',
525
+ paddingVertical: 16,
526
+ paddingHorizontal: 32,
527
+ borderRadius: 8,
528
+ minWidth: 200,
529
+ alignItems: 'center'
530
+ },
531
+ closeSessionHint: {
532
+ color: '#aaa',
533
+ fontSize: 14,
534
+ textAlign: 'center',
535
+ marginTop: 16,
536
+ paddingHorizontal: 40
537
+ },
538
+ waitingContainer: {
539
+ flex: 1,
540
+ justifyContent: 'center',
541
+ alignItems: 'center'
542
+ },
543
+ waitingText: {
544
+ color: '#fff',
545
+ marginTop: 20,
546
+ fontSize: 18
547
+ },
548
+ queueText: {
549
+ color: '#fff',
550
+ marginTop: 10,
551
+ fontSize: 16,
552
+ fontWeight: 'bold'
553
+ },
554
+ statusText: {
555
+ color: '#aaa',
556
+ marginTop: 10,
557
+ fontSize: 14
558
+ },
559
+ localVideoContainer: {
560
+ position: 'absolute',
561
+ top: 50,
562
+ right: 20,
563
+ width: 100,
564
+ height: 150,
565
+ borderRadius: 10,
566
+ overflow: 'hidden',
567
+ borderWidth: 1,
568
+ borderColor: '#fff',
569
+ backgroundColor: '#333'
570
+ },
571
+ localVideo: {
572
+ flex: 1
573
+ },
574
+ instructionsContainer: {
575
+ position: 'absolute',
576
+ bottom: 40,
577
+ left: 20,
578
+ right: 20,
579
+ backgroundColor: 'rgba(59, 130, 246, 0.95)',
580
+ borderRadius: 12,
581
+ padding: 16,
582
+ maxHeight: 200
583
+ },
584
+ instructionsHeader: {
585
+ marginBottom: 12,
586
+ paddingBottom: 8,
587
+ borderBottomWidth: 1,
588
+ borderBottomColor: 'rgba(255, 255, 255, 0.3)'
589
+ },
590
+ instructionsTitle: {
591
+ color: '#fff',
592
+ fontSize: 16,
593
+ fontWeight: 'bold'
594
+ },
595
+ instructionItem: {
596
+ flexDirection: 'row',
597
+ alignItems: 'flex-start',
598
+ backgroundColor: 'rgba(255, 255, 255, 0.15)',
599
+ borderRadius: 8,
600
+ padding: 12,
601
+ marginBottom: 8
602
+ },
603
+ instructionIcon: {
604
+ fontSize: 18,
605
+ marginRight: 10
606
+ },
607
+ instructionText: {
608
+ flex: 1,
609
+ color: '#fff',
610
+ fontSize: 14,
611
+ lineHeight: 20
612
+ },
613
+ controlsContainer: {
614
+ position: 'absolute',
615
+ bottom: 40,
616
+ left: 0,
617
+ right: 0,
618
+ flexDirection: 'row',
619
+ justifyContent: 'space-evenly',
620
+ alignItems: 'center'
621
+ },
622
+ controlButton: {
623
+ width: 60,
624
+ height: 60,
625
+ borderRadius: 30,
626
+ backgroundColor: 'rgba(255,255,255,0.3)',
627
+ justifyContent: 'center',
628
+ alignItems: 'center'
629
+ },
630
+ endCallButton: {
631
+ backgroundColor: '#ff4444'
632
+ },
633
+ buttonText: {
634
+ color: '#fff',
635
+ fontSize: 12
636
+ },
637
+ leaveQueueButton: {
638
+ marginTop: 24,
639
+ paddingVertical: 12,
640
+ paddingHorizontal: 32,
641
+ borderRadius: 8,
642
+ borderWidth: 1,
643
+ borderColor: '#fff'
644
+ },
645
+ leaveQueueButtonText: {
646
+ color: '#fff',
647
+ fontSize: 15
648
+ },
649
+ guide: {
650
+ flex: 1,
651
+ justifyContent: 'center',
652
+ paddingHorizontal: 20,
653
+ gap: 10,
654
+ backgroundColor: 'white'
655
+ },
656
+ guideAnimation: {
657
+ width: 250,
658
+ height: 250,
659
+ alignSelf: 'center'
660
+ },
661
+ guideHeader: {
662
+ color: 'black',
663
+ fontSize: 18,
664
+ textAlign: 'center',
665
+ fontWeight: 'bold'
666
+ },
667
+ guidePoints: {
668
+ gap: 10,
669
+ padding: 10
670
+ },
671
+ guideText: {
672
+ color: 'black',
673
+ fontSize: 14
674
+ }
675
+ });
676
+ export default VideoCallScreen;
@@ -15,8 +15,11 @@ import { useTheme } from "../../Shared/Contexts/ThemeContext.js";
15
15
  import LottieView from 'lottie-react-native';
16
16
  import { getSimulatedDemoData } from "../../Shared/Libs/demo.utils.js";
17
17
  import { trackError, useScreenTracking } from "../../Shared/Libs/analytics.utils.js";
18
+ import { useKeepAwake } from "../../Shared/Libs/native-keep-awake.utils.js";
19
+ import { useStatusBarWhiteBackground } from "../../Shared/Libs/status-bar.utils.js";
18
20
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
19
21
  const OTPVerificationScreen = () => {
22
+ useKeepAwake();
20
23
  const route = useRoute();
21
24
  const sessionId = route.params?.sessionId;
22
25
  const [code, setCode] = useState('');
@@ -33,6 +36,9 @@ const OTPVerificationScreen = () => {
33
36
  const primaryColor = theme.colors.primary;
34
37
  useScreenTracking('otp_verification');
35
38
 
39
+ // Configure status bar for white background
40
+ useStatusBarWhiteBackground();
41
+
36
42
  // Guard: If sessionId is not provided, show error
37
43
  if (!sessionId) {
38
44
  return /*#__PURE__*/_jsx(SafeAreaView, {