@trustchex/react-native-sdk 1.381.0 → 1.409.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/src/main/java/com/trustchex/reactnativesdk/camera/TrustchexCameraView.kt +1 -12
- package/android/src/main/java/com/trustchex/reactnativesdk/mlkit/MLKitModule.kt +1 -1
- package/ios/Camera/TrustchexCameraView.swift +1 -12
- package/ios/MLKit/MLKitModule.swift +1 -1
- 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/EIDScanner.js +1 -4
- package/lib/module/Shared/Components/IdentityDocumentCamera.js +29 -8
- 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/EIDScanner.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.d.ts.map +1 -1
- 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/EIDScanner.tsx +1 -5
- package/src/Shared/Components/IdentityDocumentCamera.tsx +29 -8
- 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,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, {
|