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