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