@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.
- package/android/src/main/java/com/trustchex/reactnativesdk/camera/TrustchexCameraView.kt +1 -21
- package/android/src/main/java/com/trustchex/reactnativesdk/mlkit/MLKitModule.kt +1 -1
- package/android/src/main/java/com/trustchex/reactnativesdk/opencv/OpenCVModule.kt +636 -301
- package/ios/Camera/TrustchexCameraView.swift +9 -20
- package/ios/MLKit/MLKitModule.swift +1 -1
- package/ios/OpenCV/OpenCVHelper.h +0 -7
- package/ios/OpenCV/OpenCVHelper.mm +0 -60
- package/ios/OpenCV/OpenCVModule.h +0 -4
- package/ios/OpenCV/OpenCVModule.mm +440 -358
- package/lib/module/Screens/Debug/BarcodeTestScreen.js +308 -0
- package/lib/module/Screens/Debug/MRZTestScreen.js +105 -13
- package/lib/module/Screens/Dynamic/ContractAcceptanceScreen.js +49 -29
- package/lib/module/Screens/Dynamic/IdentityDocumentEIDScanningScreen.js +5 -0
- package/lib/module/Screens/Dynamic/IdentityDocumentScanningScreen.js +5 -0
- package/lib/module/Screens/Dynamic/LivenessDetectionScreen.js +26 -6
- package/lib/module/Screens/Dynamic/VideoCallScreen.js +676 -0
- package/lib/module/Screens/Static/OTPVerificationScreen.js +6 -0
- package/lib/module/Screens/Static/QrCodeScanningScreen.js +7 -1
- package/lib/module/Screens/Static/ResultScreen.js +27 -13
- package/lib/module/Screens/Static/VerificationSessionCheckScreen.js +51 -51
- package/lib/module/Shared/Animations/video-call.json +1 -0
- package/lib/module/Shared/Components/DebugNavigationPanel.js +180 -14
- package/lib/module/Shared/Components/DebugOverlay.js +541 -0
- package/lib/module/Shared/Components/EIDScanner.js +1 -4
- package/lib/module/Shared/Components/IdentityDocumentCamera.constants.js +44 -0
- package/lib/module/Shared/Components/IdentityDocumentCamera.flows.js +270 -0
- package/lib/module/Shared/Components/IdentityDocumentCamera.js +702 -1703
- package/lib/module/Shared/Components/IdentityDocumentCamera.types.js +3 -0
- package/lib/module/Shared/Components/IdentityDocumentCamera.utils.js +273 -0
- package/lib/module/Shared/Components/NavigationManager.js +15 -3
- package/lib/module/Shared/Contexts/AppContext.js +1 -0
- package/lib/module/Shared/Libs/SignalingClient.js +128 -0
- package/lib/module/Shared/Libs/analytics.utils.js +4 -0
- package/lib/module/Shared/Libs/deeplink.utils.js +9 -1
- package/lib/module/Shared/Libs/http-client.js +9 -0
- package/lib/module/Shared/Libs/promise.utils.js +16 -2
- package/lib/module/Shared/Libs/status-bar.utils.js +21 -0
- package/lib/module/Shared/Services/DataUploadService.js +294 -0
- package/lib/module/Shared/Services/VideoSessionService.js +156 -0
- package/lib/module/Shared/Services/WebRTCService.js +510 -0
- package/lib/module/Shared/Types/analytics.types.js +2 -0
- package/lib/module/Translation/Resources/en.js +20 -0
- package/lib/module/Translation/Resources/tr.js +20 -0
- package/lib/module/Trustchex.js +10 -0
- package/lib/module/version.js +1 -1
- package/lib/typescript/src/Screens/Debug/BarcodeTestScreen.d.ts +3 -0
- package/lib/typescript/src/Screens/Debug/BarcodeTestScreen.d.ts.map +1 -0
- package/lib/typescript/src/Screens/Debug/MRZTestScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Dynamic/ContractAcceptanceScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Dynamic/IdentityDocumentEIDScanningScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Dynamic/IdentityDocumentScanningScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Dynamic/LivenessDetectionScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Dynamic/VideoCallScreen.d.ts +3 -0
- package/lib/typescript/src/Screens/Dynamic/VideoCallScreen.d.ts.map +1 -0
- package/lib/typescript/src/Screens/Static/OTPVerificationScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Static/QrCodeScanningScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Static/ResultScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Static/VerificationSessionCheckScreen.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/DebugNavigationPanel.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/DebugOverlay.d.ts +30 -0
- package/lib/typescript/src/Shared/Components/DebugOverlay.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Components/EIDScanner.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.constants.d.ts +35 -0
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.constants.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.d.ts +3 -56
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.flows.d.ts +88 -0
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.flows.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.types.d.ts +116 -0
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.types.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.utils.d.ts +93 -0
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.utils.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Components/NavigationManager.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Contexts/AppContext.d.ts +1 -0
- package/lib/typescript/src/Shared/Contexts/AppContext.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/SignalingClient.d.ts +24 -0
- package/lib/typescript/src/Shared/Libs/SignalingClient.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Libs/analytics.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/deeplink.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/http-client.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/promise.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/status-bar.utils.d.ts +9 -0
- package/lib/typescript/src/Shared/Libs/status-bar.utils.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Services/DataUploadService.d.ts +25 -0
- package/lib/typescript/src/Shared/Services/DataUploadService.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Services/VideoSessionService.d.ts +33 -0
- package/lib/typescript/src/Shared/Services/VideoSessionService.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Services/WebRTCService.d.ts +58 -0
- package/lib/typescript/src/Shared/Services/WebRTCService.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Types/analytics.types.d.ts +2 -0
- package/lib/typescript/src/Shared/Types/analytics.types.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Types/identificationInfo.d.ts +4 -1
- package/lib/typescript/src/Shared/Types/identificationInfo.d.ts.map +1 -1
- package/lib/typescript/src/Translation/Resources/en.d.ts +20 -0
- package/lib/typescript/src/Translation/Resources/en.d.ts.map +1 -1
- package/lib/typescript/src/Translation/Resources/tr.d.ts +20 -0
- package/lib/typescript/src/Translation/Resources/tr.d.ts.map +1 -1
- package/lib/typescript/src/Trustchex.d.ts.map +1 -1
- package/lib/typescript/src/version.d.ts +1 -1
- package/package.json +29 -2
- package/src/Screens/Debug/BarcodeTestScreen.tsx +317 -0
- package/src/Screens/Debug/MRZTestScreen.tsx +107 -13
- package/src/Screens/Dynamic/ContractAcceptanceScreen.tsx +59 -33
- package/src/Screens/Dynamic/IdentityDocumentEIDScanningScreen.tsx +6 -0
- package/src/Screens/Dynamic/IdentityDocumentScanningScreen.tsx +6 -0
- package/src/Screens/Dynamic/LivenessDetectionScreen.tsx +34 -6
- package/src/Screens/Dynamic/VideoCallScreen.tsx +764 -0
- package/src/Screens/Static/OTPVerificationScreen.tsx +6 -0
- package/src/Screens/Static/QrCodeScanningScreen.tsx +7 -1
- package/src/Screens/Static/ResultScreen.tsx +58 -23
- package/src/Screens/Static/VerificationSessionCheckScreen.tsx +58 -72
- package/src/Shared/Animations/video-call.json +1 -0
- package/src/Shared/Components/DebugNavigationPanel.tsx +185 -9
- package/src/Shared/Components/DebugOverlay.tsx +656 -0
- package/src/Shared/Components/EIDScanner.tsx +1 -5
- package/src/Shared/Components/IdentityDocumentCamera.constants.ts +44 -0
- package/src/Shared/Components/IdentityDocumentCamera.flows.ts +342 -0
- package/src/Shared/Components/IdentityDocumentCamera.tsx +1089 -2465
- package/src/Shared/Components/IdentityDocumentCamera.types.ts +136 -0
- package/src/Shared/Components/IdentityDocumentCamera.utils.ts +364 -0
- package/src/Shared/Components/NavigationManager.tsx +14 -1
- package/src/Shared/Contexts/AppContext.ts +2 -0
- package/src/Shared/Libs/SignalingClient.ts +189 -0
- package/src/Shared/Libs/analytics.utils.ts +4 -0
- package/src/Shared/Libs/deeplink.utils.ts +12 -1
- package/src/Shared/Libs/http-client.ts +10 -0
- package/src/Shared/Libs/promise.utils.ts +16 -2
- package/src/Shared/Libs/status-bar.utils.ts +19 -0
- package/src/Shared/Services/DataUploadService.ts +395 -0
- package/src/Shared/Services/VideoSessionService.ts +190 -0
- package/src/Shared/Services/WebRTCService.ts +636 -0
- package/src/Shared/Types/analytics.types.ts +2 -0
- package/src/Shared/Types/identificationInfo.ts +5 -1
- package/src/Translation/Resources/en.ts +25 -0
- package/src/Translation/Resources/tr.ts +27 -0
- package/src/Trustchex.tsx +12 -2
- 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;
|