@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.
Files changed (111) hide show
  1. package/android/src/main/java/com/trustchex/reactnativesdk/camera/TrustchexCameraView.kt +1 -12
  2. package/android/src/main/java/com/trustchex/reactnativesdk/mlkit/MLKitModule.kt +1 -1
  3. package/ios/Camera/TrustchexCameraView.swift +1 -12
  4. package/ios/MLKit/MLKitModule.swift +1 -1
  5. package/lib/module/Screens/Debug/BarcodeTestScreen.js +308 -0
  6. package/lib/module/Screens/Debug/MRZTestScreen.js +105 -13
  7. package/lib/module/Screens/Dynamic/ContractAcceptanceScreen.js +49 -29
  8. package/lib/module/Screens/Dynamic/IdentityDocumentEIDScanningScreen.js +5 -0
  9. package/lib/module/Screens/Dynamic/IdentityDocumentScanningScreen.js +5 -0
  10. package/lib/module/Screens/Dynamic/LivenessDetectionScreen.js +26 -6
  11. package/lib/module/Screens/Dynamic/VideoCallScreen.js +676 -0
  12. package/lib/module/Screens/Static/OTPVerificationScreen.js +6 -0
  13. package/lib/module/Screens/Static/QrCodeScanningScreen.js +7 -1
  14. package/lib/module/Screens/Static/ResultScreen.js +27 -13
  15. package/lib/module/Screens/Static/VerificationSessionCheckScreen.js +51 -51
  16. package/lib/module/Shared/Animations/video-call.json +1 -0
  17. package/lib/module/Shared/Components/DebugNavigationPanel.js +180 -14
  18. package/lib/module/Shared/Components/EIDScanner.js +1 -4
  19. package/lib/module/Shared/Components/IdentityDocumentCamera.js +29 -8
  20. package/lib/module/Shared/Components/NavigationManager.js +15 -3
  21. package/lib/module/Shared/Contexts/AppContext.js +1 -0
  22. package/lib/module/Shared/Libs/SignalingClient.js +128 -0
  23. package/lib/module/Shared/Libs/analytics.utils.js +4 -0
  24. package/lib/module/Shared/Libs/deeplink.utils.js +9 -1
  25. package/lib/module/Shared/Libs/http-client.js +9 -0
  26. package/lib/module/Shared/Libs/promise.utils.js +16 -2
  27. package/lib/module/Shared/Libs/status-bar.utils.js +21 -0
  28. package/lib/module/Shared/Services/DataUploadService.js +294 -0
  29. package/lib/module/Shared/Services/VideoSessionService.js +156 -0
  30. package/lib/module/Shared/Services/WebRTCService.js +510 -0
  31. package/lib/module/Shared/Types/analytics.types.js +2 -0
  32. package/lib/module/Translation/Resources/en.js +20 -0
  33. package/lib/module/Translation/Resources/tr.js +20 -0
  34. package/lib/module/Trustchex.js +10 -0
  35. package/lib/module/version.js +1 -1
  36. package/lib/typescript/src/Screens/Debug/BarcodeTestScreen.d.ts +3 -0
  37. package/lib/typescript/src/Screens/Debug/BarcodeTestScreen.d.ts.map +1 -0
  38. package/lib/typescript/src/Screens/Debug/MRZTestScreen.d.ts.map +1 -1
  39. package/lib/typescript/src/Screens/Dynamic/ContractAcceptanceScreen.d.ts.map +1 -1
  40. package/lib/typescript/src/Screens/Dynamic/IdentityDocumentEIDScanningScreen.d.ts.map +1 -1
  41. package/lib/typescript/src/Screens/Dynamic/IdentityDocumentScanningScreen.d.ts.map +1 -1
  42. package/lib/typescript/src/Screens/Dynamic/LivenessDetectionScreen.d.ts.map +1 -1
  43. package/lib/typescript/src/Screens/Dynamic/VideoCallScreen.d.ts +3 -0
  44. package/lib/typescript/src/Screens/Dynamic/VideoCallScreen.d.ts.map +1 -0
  45. package/lib/typescript/src/Screens/Static/OTPVerificationScreen.d.ts.map +1 -1
  46. package/lib/typescript/src/Screens/Static/QrCodeScanningScreen.d.ts.map +1 -1
  47. package/lib/typescript/src/Screens/Static/ResultScreen.d.ts.map +1 -1
  48. package/lib/typescript/src/Screens/Static/VerificationSessionCheckScreen.d.ts.map +1 -1
  49. package/lib/typescript/src/Shared/Components/DebugNavigationPanel.d.ts.map +1 -1
  50. package/lib/typescript/src/Shared/Components/EIDScanner.d.ts.map +1 -1
  51. package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.d.ts.map +1 -1
  52. package/lib/typescript/src/Shared/Components/NavigationManager.d.ts.map +1 -1
  53. package/lib/typescript/src/Shared/Contexts/AppContext.d.ts +1 -0
  54. package/lib/typescript/src/Shared/Contexts/AppContext.d.ts.map +1 -1
  55. package/lib/typescript/src/Shared/Libs/SignalingClient.d.ts +24 -0
  56. package/lib/typescript/src/Shared/Libs/SignalingClient.d.ts.map +1 -0
  57. package/lib/typescript/src/Shared/Libs/analytics.utils.d.ts.map +1 -1
  58. package/lib/typescript/src/Shared/Libs/deeplink.utils.d.ts.map +1 -1
  59. package/lib/typescript/src/Shared/Libs/http-client.d.ts.map +1 -1
  60. package/lib/typescript/src/Shared/Libs/promise.utils.d.ts.map +1 -1
  61. package/lib/typescript/src/Shared/Libs/status-bar.utils.d.ts +9 -0
  62. package/lib/typescript/src/Shared/Libs/status-bar.utils.d.ts.map +1 -0
  63. package/lib/typescript/src/Shared/Services/DataUploadService.d.ts +25 -0
  64. package/lib/typescript/src/Shared/Services/DataUploadService.d.ts.map +1 -0
  65. package/lib/typescript/src/Shared/Services/VideoSessionService.d.ts +33 -0
  66. package/lib/typescript/src/Shared/Services/VideoSessionService.d.ts.map +1 -0
  67. package/lib/typescript/src/Shared/Services/WebRTCService.d.ts +58 -0
  68. package/lib/typescript/src/Shared/Services/WebRTCService.d.ts.map +1 -0
  69. package/lib/typescript/src/Shared/Types/analytics.types.d.ts +2 -0
  70. package/lib/typescript/src/Shared/Types/analytics.types.d.ts.map +1 -1
  71. package/lib/typescript/src/Shared/Types/identificationInfo.d.ts +4 -1
  72. package/lib/typescript/src/Shared/Types/identificationInfo.d.ts.map +1 -1
  73. package/lib/typescript/src/Translation/Resources/en.d.ts +20 -0
  74. package/lib/typescript/src/Translation/Resources/en.d.ts.map +1 -1
  75. package/lib/typescript/src/Translation/Resources/tr.d.ts +20 -0
  76. package/lib/typescript/src/Translation/Resources/tr.d.ts.map +1 -1
  77. package/lib/typescript/src/Trustchex.d.ts.map +1 -1
  78. package/lib/typescript/src/version.d.ts +1 -1
  79. package/package.json +29 -2
  80. package/src/Screens/Debug/BarcodeTestScreen.tsx +317 -0
  81. package/src/Screens/Debug/MRZTestScreen.tsx +107 -13
  82. package/src/Screens/Dynamic/ContractAcceptanceScreen.tsx +59 -33
  83. package/src/Screens/Dynamic/IdentityDocumentEIDScanningScreen.tsx +6 -0
  84. package/src/Screens/Dynamic/IdentityDocumentScanningScreen.tsx +6 -0
  85. package/src/Screens/Dynamic/LivenessDetectionScreen.tsx +34 -6
  86. package/src/Screens/Dynamic/VideoCallScreen.tsx +764 -0
  87. package/src/Screens/Static/OTPVerificationScreen.tsx +6 -0
  88. package/src/Screens/Static/QrCodeScanningScreen.tsx +7 -1
  89. package/src/Screens/Static/ResultScreen.tsx +58 -23
  90. package/src/Screens/Static/VerificationSessionCheckScreen.tsx +58 -72
  91. package/src/Shared/Animations/video-call.json +1 -0
  92. package/src/Shared/Components/DebugNavigationPanel.tsx +185 -9
  93. package/src/Shared/Components/EIDScanner.tsx +1 -5
  94. package/src/Shared/Components/IdentityDocumentCamera.tsx +29 -8
  95. package/src/Shared/Components/NavigationManager.tsx +14 -1
  96. package/src/Shared/Contexts/AppContext.ts +2 -0
  97. package/src/Shared/Libs/SignalingClient.ts +189 -0
  98. package/src/Shared/Libs/analytics.utils.ts +4 -0
  99. package/src/Shared/Libs/deeplink.utils.ts +12 -1
  100. package/src/Shared/Libs/http-client.ts +10 -0
  101. package/src/Shared/Libs/promise.utils.ts +16 -2
  102. package/src/Shared/Libs/status-bar.utils.ts +19 -0
  103. package/src/Shared/Services/DataUploadService.ts +395 -0
  104. package/src/Shared/Services/VideoSessionService.ts +190 -0
  105. package/src/Shared/Services/WebRTCService.ts +636 -0
  106. package/src/Shared/Types/analytics.types.ts +2 -0
  107. package/src/Shared/Types/identificationInfo.ts +5 -1
  108. package/src/Translation/Resources/en.ts +25 -0
  109. package/src/Translation/Resources/tr.ts +27 -0
  110. package/src/Trustchex.tsx +12 -2
  111. package/src/version.ts +1 -1
@@ -0,0 +1,636 @@
1
+ import {
2
+ RTCPeerConnection,
3
+ RTCIceCandidate,
4
+ RTCSessionDescription,
5
+ mediaDevices,
6
+ MediaStream,
7
+ } from 'react-native-webrtc';
8
+ import {
9
+ SignalingClient,
10
+ type SignalingMessage,
11
+ } from '../Libs/SignalingClient';
12
+
13
+ export interface WebRTCConfig {
14
+ baseUrl: string;
15
+ sessionId: string;
16
+ identificationId?: string;
17
+ onRemoteStream: (stream: MediaStream) => void;
18
+ onConnectionStateChange: (state: string) => void;
19
+ onCommand?: (command: any) => void;
20
+ onSessionEnded?: (state: string) => void;
21
+ onSnapshotRequest?: (requestId: string) => void;
22
+ onLocalStreamUpdate?: (stream: MediaStream) => void;
23
+ }
24
+
25
+ export class WebRTCService {
26
+ private peerConnection: RTCPeerConnection | null = null;
27
+ private localStream: MediaStream | null = null;
28
+ private remoteStream: MediaStream | null = null;
29
+ private readonly signalingClient: SignalingClient;
30
+ private readonly onRemoteStream: (stream: MediaStream) => void;
31
+ private readonly onConnectionStateChange: (state: string) => void;
32
+ private readonly onCommand?: (command: any) => void;
33
+ private readonly onSnapshotRequest?: (requestId: string) => void;
34
+ private readonly onLocalStreamUpdate?: (stream: MediaStream) => void;
35
+ private readonly baseUrl: string;
36
+ private readonly sessionId: string;
37
+ private readonly identificationId?: string;
38
+ private isFlashOn: boolean = false;
39
+ private pendingCandidates: RTCIceCandidate[] = [];
40
+ private hasRemoteDescription = false;
41
+ private isProcessingOffer = false;
42
+ private lastProcessedOfferSdp: string | null = null;
43
+ private tracksAdded = false;
44
+ private pendingOffer: SignalingMessage | null = null;
45
+ private isSwitchingCamera = false;
46
+ private lastSwitchTime = 0;
47
+ private facingMode: 'user' | 'environment' = 'user';
48
+ private videoTrackEndedHandler: (() => void) | null = null;
49
+
50
+ constructor(config: WebRTCConfig) {
51
+ this.onRemoteStream = config.onRemoteStream;
52
+ this.onConnectionStateChange = config.onConnectionStateChange;
53
+ this.onCommand = config.onCommand;
54
+ this.onSnapshotRequest = config.onSnapshotRequest;
55
+ this.onLocalStreamUpdate = config.onLocalStreamUpdate;
56
+ this.baseUrl = config.baseUrl;
57
+ this.sessionId = config.sessionId;
58
+ this.identificationId = config.identificationId;
59
+
60
+ this.signalingClient = new SignalingClient(
61
+ config.baseUrl,
62
+ config.sessionId,
63
+ this.handleSignalingMessage.bind(this),
64
+ config.identificationId,
65
+ config.onSessionEnded
66
+ );
67
+ }
68
+
69
+ private async fetchIceServers(): Promise<object[]> {
70
+ try {
71
+ const params = this.identificationId
72
+ ? `?identificationId=${this.identificationId}`
73
+ : '';
74
+ const response = await fetch(
75
+ `${this.baseUrl}/api/app/mobile/video-sessions/${this.sessionId}/ice-servers${params}`
76
+ );
77
+ if (!response.ok) return [];
78
+ const data = await response.json();
79
+ return Array.isArray(data.iceServers) ? data.iceServers : [];
80
+ } catch {
81
+ return [];
82
+ }
83
+ }
84
+
85
+ public async initialize(): Promise<MediaStream> {
86
+ console.log('[WebRTCService] Initializing...');
87
+
88
+ try {
89
+ // Fetch ICE servers before getUserMedia so peer connection is ready sooner
90
+ const iceServers = await this.fetchIceServers();
91
+
92
+ // Initialize PeerConnection before getUserMedia so signaling can start
93
+ // immediately — avoids missing the agent's offer while camera init blocks.
94
+ this.peerConnection = new RTCPeerConnection({
95
+ iceServers,
96
+ iceCandidatePoolSize: 2,
97
+ });
98
+
99
+ // Set up event handlers before signaling connects.
100
+ // Use addEventListener instead of property assignment for reliable
101
+ // delivery in react-native-webrtc (property may be overwritten).
102
+ (this.peerConnection as any).addEventListener('track', (event: any) => {
103
+ if (event.streams && event.streams[0]) {
104
+ this.remoteStream = event.streams[0];
105
+ this.onRemoteStream(this.remoteStream);
106
+ } else if (event.track) {
107
+ const existingTracks = this.remoteStream
108
+ ? this.remoteStream.getTracks()
109
+ : [];
110
+ this.remoteStream = new MediaStream([...existingTracks, event.track]);
111
+ this.onRemoteStream(this.remoteStream);
112
+ }
113
+ });
114
+
115
+ (this.peerConnection as any).addEventListener(
116
+ 'icecandidate',
117
+ (event: any) => {
118
+ if (event.candidate) {
119
+ this.signalingClient.send(
120
+ 'ice-candidate',
121
+ event.candidate.toJSON()
122
+ );
123
+ }
124
+ }
125
+ );
126
+
127
+ // react-native-webrtc often does not fire connectionstatechange or sets
128
+ // connectionState to undefined. Use iceconnectionstatechange which is
129
+ // reliably implemented across all versions.
130
+ (this.peerConnection as any).addEventListener(
131
+ 'iceconnectionstatechange',
132
+ () => {
133
+ if (!this.peerConnection) return;
134
+ const iceState: string =
135
+ (this.peerConnection as any).iceConnectionState || 'new';
136
+ // Map ICE states to the connection states the UI expects
137
+ const mapped =
138
+ iceState === 'connected' || iceState === 'completed'
139
+ ? 'connected'
140
+ : iceState === 'failed'
141
+ ? 'failed'
142
+ : iceState === 'disconnected'
143
+ ? 'disconnected'
144
+ : iceState === 'closed'
145
+ ? 'closed'
146
+ : iceState;
147
+ this.onConnectionStateChange(mapped);
148
+ if (mapped === 'connected') {
149
+ this.sendTorchAvailability();
150
+ }
151
+ }
152
+ );
153
+
154
+ // Connect signaling now — peer connection + handlers are ready.
155
+ // Any offer that arrives before local tracks are added is buffered in
156
+ // pendingOffer and processed once tracks are attached below.
157
+ this.signalingClient.connect();
158
+
159
+ // Get local stream. Avoid strict min constraints that hang on some Android
160
+ // devices when the camera cannot satisfy the combination.
161
+ const stream = await mediaDevices.getUserMedia({
162
+ audio: true,
163
+ video: {
164
+ facingMode: 'user',
165
+ frameRate: { ideal: 30 },
166
+ },
167
+ });
168
+
169
+ this.localStream = stream;
170
+
171
+ // Add local tracks
172
+ stream.getTracks().forEach((track) => {
173
+ this.peerConnection?.addTrack(track, stream);
174
+ });
175
+ this.tracksAdded = true;
176
+
177
+ // If the agent's offer arrived while getUserMedia was pending, process it now.
178
+ if (this.pendingOffer) {
179
+ const buffered = this.pendingOffer;
180
+ this.pendingOffer = null;
181
+ await this.handleSignalingMessage(buffered);
182
+ }
183
+
184
+ this.sendTorchAvailability();
185
+
186
+ return stream;
187
+ } catch (error) {
188
+ console.error('[WebRTCService] Initialization failed:', error);
189
+ throw error;
190
+ }
191
+ }
192
+
193
+ public async handleSignalingMessage(message: SignalingMessage) {
194
+ if (!this.peerConnection) return;
195
+
196
+ try {
197
+ switch (message.type) {
198
+ case 'offer': {
199
+ // Buffer the offer until local tracks have been added so the answer
200
+ // includes the mobile's audio/video tracks.
201
+ if (!this.tracksAdded) {
202
+ console.log(
203
+ '[WebRTCService] Offer received before tracks ready, buffering'
204
+ );
205
+ this.pendingOffer = message;
206
+ return;
207
+ }
208
+
209
+ // Skip if we're already processing an offer
210
+ if (this.isProcessingOffer) {
211
+ console.log(
212
+ '[WebRTCService] Skipping offer - already processing one'
213
+ );
214
+ return;
215
+ }
216
+
217
+ const offerSdp: string | undefined = message.payload?.sdp;
218
+ if (offerSdp && this.lastProcessedOfferSdp === offerSdp) {
219
+ console.log('[WebRTCService] Skipping offer - duplicate SDP');
220
+ return;
221
+ }
222
+
223
+ this.isProcessingOffer = true;
224
+ console.log('[WebRTCService] Received offer');
225
+
226
+ try {
227
+ await this.peerConnection.setRemoteDescription(
228
+ new RTCSessionDescription(message.payload)
229
+ );
230
+ this.hasRemoteDescription = true;
231
+ // Add any pending candidates
232
+ await this.addPendingCandidates();
233
+ const answer = await this.peerConnection.createAnswer();
234
+ await this.peerConnection.setLocalDescription(answer);
235
+ await this.signalingClient.send('answer', {
236
+ type: answer.type,
237
+ sdp: answer.sdp,
238
+ });
239
+ this.lastProcessedOfferSdp = offerSdp ?? null;
240
+ } finally {
241
+ this.isProcessingOffer = false;
242
+ }
243
+ break;
244
+ }
245
+
246
+ case 'answer':
247
+ console.log('[WebRTCService] Received answer');
248
+ await this.peerConnection.setRemoteDescription(
249
+ new RTCSessionDescription(message.payload)
250
+ );
251
+ this.hasRemoteDescription = true;
252
+ // Add any pending candidates
253
+ await this.addPendingCandidates();
254
+ break;
255
+
256
+ case 'ice-candidate':
257
+ if (message.payload) {
258
+ const candidate = new RTCIceCandidate(message.payload);
259
+ if (this.hasRemoteDescription) {
260
+ console.log('[WebRTCService] Adding ICE candidate');
261
+ await this.peerConnection.addIceCandidate(candidate);
262
+ } else {
263
+ this.pendingCandidates.push(candidate);
264
+ }
265
+ }
266
+ break;
267
+
268
+ case 'command':
269
+ this.handleCommand(message.payload);
270
+ break;
271
+ }
272
+ } catch (error) {
273
+ console.error('[WebRTCService] Error handling signaling:', error);
274
+ }
275
+ }
276
+
277
+ private handleCommand(payload: any) {
278
+ console.log('[WebRTCService] Received command:', payload);
279
+
280
+ if (this.onCommand) {
281
+ this.onCommand(payload);
282
+ }
283
+
284
+ switch (payload.type) {
285
+ case 'switchCamera':
286
+ this.switchCamera();
287
+ break;
288
+ case 'toggleFlash':
289
+ this.toggleFlash();
290
+ break;
291
+ case 'captureSnapshot':
292
+ this.captureSnapshot(payload.requestId);
293
+ break;
294
+ case 'takeSnapshot':
295
+ this.captureSnapshot(payload.requestId);
296
+ break;
297
+ }
298
+ }
299
+
300
+ private async addPendingCandidates() {
301
+ if (this.pendingCandidates.length > 0 && this.peerConnection) {
302
+ console.log(
303
+ `[WebRTCService] Adding ${this.pendingCandidates.length} pending ICE candidates`
304
+ );
305
+ for (const candidate of this.pendingCandidates) {
306
+ try {
307
+ await this.peerConnection.addIceCandidate(candidate);
308
+ } catch (error) {
309
+ console.error(
310
+ '[WebRTCService] Error adding pending candidate:',
311
+ error
312
+ );
313
+ }
314
+ }
315
+ this.pendingCandidates = [];
316
+ }
317
+ }
318
+
319
+ public cleanup() {
320
+ this.localStream?.getTracks().forEach((t) => t.stop());
321
+ this.peerConnection?.close();
322
+ this.signalingClient.disconnect();
323
+ this.localStream = null;
324
+ this.remoteStream = null;
325
+ this.peerConnection = null;
326
+ this.pendingCandidates = [];
327
+ this.hasRemoteDescription = false;
328
+ this.lastProcessedOfferSdp = null;
329
+ this.isProcessingOffer = false;
330
+ this.tracksAdded = false;
331
+ this.pendingOffer = null;
332
+ }
333
+
334
+ public async switchCamera() {
335
+ // Debounce: ignore if already switching or within 2s cooldown
336
+ const now = Date.now();
337
+ if (this.isSwitchingCamera || now - this.lastSwitchTime < 2000) {
338
+ console.warn('[WebRTCService] switchCamera ignored — cooldown');
339
+ return;
340
+ }
341
+
342
+ if (!this.localStream || !this.peerConnection) return;
343
+
344
+ const videoTrack = this.localStream.getVideoTracks()[0];
345
+ if (!videoTrack) return;
346
+
347
+ this.isSwitchingCamera = true;
348
+ this.lastSwitchTime = now;
349
+
350
+ try {
351
+ const newFacing = this.facingMode === 'user' ? 'environment' : 'user';
352
+
353
+ // First, remove the video track from the sender (set to null).
354
+ // This signals to the encoder to stop, which releases the camera HAL.
355
+ const senders = (this.peerConnection as any).getSenders?.() as any[];
356
+ let videoSender: any = null;
357
+ if (senders) {
358
+ for (const sender of senders) {
359
+ if (sender.track?.kind === 'video') {
360
+ videoSender = sender;
361
+ await sender.replaceTrack(null);
362
+ break;
363
+ }
364
+ }
365
+ }
366
+
367
+ // Stop the old track to fully release the camera hardware.
368
+ videoTrack.stop();
369
+
370
+ // Small delay to let the camera HAL fully release on Android.
371
+ await new Promise((res) => setTimeout(res, 200));
372
+
373
+ // Acquire a new stream from the target camera. Now that the old camera
374
+ // is fully released, there's no concurrent camera conflict.
375
+ const newStream = await mediaDevices.getUserMedia({
376
+ audio: false,
377
+ video: {
378
+ facingMode: newFacing,
379
+ frameRate: { ideal: 30 },
380
+ },
381
+ });
382
+
383
+ const newVideoTrack = newStream.getVideoTracks()[0];
384
+ if (!newVideoTrack) {
385
+ console.warn(
386
+ '[WebRTCService] switchCamera: no video track from getUserMedia'
387
+ );
388
+ return;
389
+ }
390
+
391
+ // Replace the null track in the sender with the new camera track.
392
+ // This properly notifies the WebRTC encoder to start encoding from
393
+ // the new camera — unlike _switchCamera() which only changes the
394
+ // hardware source without informing the encoder/remote side.
395
+ if (videoSender) {
396
+ await videoSender.replaceTrack(newVideoTrack);
397
+ }
398
+
399
+ // Build a new MediaStream with existing audio + new video.
400
+ const audioTracks = this.localStream.getAudioTracks();
401
+ this.localStream = new MediaStream([...audioTracks, newVideoTrack]);
402
+ this.facingMode = newFacing;
403
+
404
+ // Listen for the track ending unexpectedly (Android camera HAL power
405
+ // management can kill the rear camera mid-call). Re-acquire on ended.
406
+ if (this.videoTrackEndedHandler) {
407
+ (videoTrack as any).removeEventListener?.(
408
+ 'ended',
409
+ this.videoTrackEndedHandler
410
+ );
411
+ }
412
+ this.videoTrackEndedHandler = () => {
413
+ console.warn(
414
+ '[WebRTCService] video track ended unexpectedly — re-acquiring'
415
+ );
416
+ this.reacquireVideoTrack();
417
+ };
418
+ (newVideoTrack as any).addEventListener?.(
419
+ 'ended',
420
+ this.videoTrackEndedHandler
421
+ );
422
+
423
+ // Notify the screen to update its local stream reference and remount RTCView
424
+ this.onLocalStreamUpdate?.(this.localStream);
425
+ } catch (error) {
426
+ console.warn('[WebRTCService] switchCamera failed:', error);
427
+ } finally {
428
+ this.isSwitchingCamera = false;
429
+ }
430
+ }
431
+
432
+ private async reacquireVideoTrack() {
433
+ if (!this.localStream || !this.peerConnection) return;
434
+ try {
435
+ const newStream = await mediaDevices.getUserMedia({
436
+ audio: false,
437
+ video: { facingMode: this.facingMode, frameRate: { ideal: 30 } },
438
+ });
439
+ const newVideoTrack = newStream.getVideoTracks()[0];
440
+ if (!newVideoTrack) return;
441
+
442
+ const senders = (this.peerConnection as any).getSenders?.() as any[];
443
+ if (senders) {
444
+ for (const sender of senders) {
445
+ if (sender.track?.kind === 'video' || sender.track === null) {
446
+ await sender.replaceTrack(newVideoTrack);
447
+ break;
448
+ }
449
+ }
450
+ }
451
+
452
+ if (this.videoTrackEndedHandler) {
453
+ (newVideoTrack as any).addEventListener?.(
454
+ 'ended',
455
+ this.videoTrackEndedHandler
456
+ );
457
+ }
458
+
459
+ const audioTracks = this.localStream.getAudioTracks();
460
+ this.localStream = new MediaStream([...audioTracks, newVideoTrack]);
461
+ this.onLocalStreamUpdate?.(this.localStream);
462
+ } catch (error) {
463
+ console.warn('[WebRTCService] reacquireVideoTrack failed:', error);
464
+ }
465
+ }
466
+
467
+ public async toggleFlash() {
468
+ const videoTrack = this.localStream?.getVideoTracks()[0];
469
+ if (!videoTrack) {
470
+ console.warn('[WebRTCService] No video track available for flash toggle');
471
+ return;
472
+ }
473
+
474
+ this.isFlashOn = !this.isFlashOn;
475
+
476
+ // applyConstraints is the only reliable method for torch on Android.
477
+ // Retry up to 3 times with 300ms delay to handle "Camera switch already in progress" race.
478
+ let success = false;
479
+ for (let attempt = 0; attempt < 3; attempt++) {
480
+ try {
481
+ await (videoTrack as any).applyConstraints({
482
+ advanced: [{ torch: this.isFlashOn }],
483
+ });
484
+ success = true;
485
+ break;
486
+ } catch (error: any) {
487
+ const msg: string = error?.message || '';
488
+ if (msg.includes('Camera switch') && attempt < 2) {
489
+ await new Promise((res) => setTimeout(res, 300));
490
+ continue;
491
+ }
492
+ console.warn('[WebRTCService] Failed to toggle flash:', error);
493
+ this.isFlashOn = !this.isFlashOn; // revert
494
+ this.signalingClient.send('command', {
495
+ type: 'torch_availability',
496
+ available: false,
497
+ });
498
+ return;
499
+ }
500
+ }
501
+
502
+ if (!success) {
503
+ this.isFlashOn = !this.isFlashOn; // revert
504
+ this.signalingClient.send('command', {
505
+ type: 'torch_availability',
506
+ available: false,
507
+ });
508
+ return;
509
+ }
510
+
511
+ // Confirm flash is available now that we know it works
512
+ this.signalingClient.send('command', {
513
+ type: 'torch_availability',
514
+ available: true,
515
+ });
516
+
517
+ this.signalingClient.send('command', {
518
+ type: 'flash_state',
519
+ isFlashOn: this.isFlashOn,
520
+ });
521
+ }
522
+
523
+ private sendTorchAvailability() {
524
+ const videoTrack = this.localStream?.getVideoTracks()[0];
525
+ if (!videoTrack) {
526
+ this.signalingClient.send('command', {
527
+ type: 'torch_availability',
528
+ available: false,
529
+ });
530
+ return;
531
+ }
532
+
533
+ let capabilities: any;
534
+
535
+ try {
536
+ capabilities = (videoTrack as any).getCapabilities?.();
537
+ } catch {
538
+ capabilities = undefined;
539
+ }
540
+
541
+ // Only trust getCapabilities().torch — _setTorch exists on every Android
542
+ // track regardless of whether the camera actually has a flash.
543
+ const available = !!capabilities?.torch;
544
+
545
+ this.signalingClient.send('command', {
546
+ type: 'torch_availability',
547
+ available,
548
+ });
549
+ }
550
+
551
+ /**
552
+ * Handle snapshot capture request. Captures a frame from the local video track directly.
553
+ */
554
+ private async captureSnapshot(requestId?: string) {
555
+ console.log('[WebRTCService] Snapshot requested, requestId:', requestId);
556
+
557
+ if (this.onSnapshotRequest && requestId) {
558
+ // Delegate to parent component which has access to the RTCView
559
+ this.onSnapshotRequest(requestId);
560
+ return;
561
+ }
562
+
563
+ // Capture directly from local video track via react-native-webrtc captureFrame()
564
+ const videoTrack = this.localStream?.getVideoTracks()[0];
565
+ if (!videoTrack) {
566
+ console.error('[WebRTCService] No local video track for snapshot');
567
+ return;
568
+ }
569
+
570
+ try {
571
+ const frame = await (videoTrack as any).captureFrame();
572
+ const base64 = frame?.data;
573
+ if (!base64) {
574
+ console.error('[WebRTCService] captureFrame returned no data');
575
+ return;
576
+ }
577
+ await this.uploadSnapshot(requestId, base64);
578
+ } catch (error) {
579
+ console.error('[WebRTCService] captureFrame failed:', error);
580
+ }
581
+ }
582
+
583
+ /**
584
+ * Upload a captured snapshot image (called by parent component after capturing RTCView).
585
+ */
586
+ public async uploadSnapshot(
587
+ requestId: string,
588
+ base64Image: string
589
+ ): Promise<void> {
590
+ console.log('[WebRTCService] Uploading snapshot, requestId:', requestId);
591
+
592
+ try {
593
+ const formData = new FormData();
594
+ // Convert base64 to Blob via fetch data URL (works in React Native)
595
+ const dataResponse = await fetch(`data:image/jpeg;base64,${base64Image}`);
596
+ const blob = await dataResponse.blob();
597
+ formData.append('image', blob);
598
+
599
+ const uploadUrl = `${this.baseUrl}/api/app/mobile/video-sessions/${this.sessionId}/snapshots?identificationId=${this.identificationId}`;
600
+ const response = await fetch(uploadUrl, {
601
+ method: 'POST',
602
+ body: formData,
603
+ });
604
+
605
+ if (response.ok) {
606
+ const result = await response.json();
607
+ console.log('[WebRTCService] Snapshot uploaded:', result);
608
+ await this.signalingClient.send('command', {
609
+ type: 'snapshotReady',
610
+ snapshotId: result.snapshotId,
611
+ url: result.url,
612
+ createdAt: new Date().toISOString(),
613
+ });
614
+ } else {
615
+ const errorText = await response.text();
616
+ console.error(
617
+ '[WebRTCService] Snapshot upload failed:',
618
+ response.status,
619
+ errorText
620
+ );
621
+ await this.signalingClient.send('command', {
622
+ type: 'snapshotError',
623
+ requestId,
624
+ error: `Upload failed: ${response.status}`,
625
+ });
626
+ }
627
+ } catch (error) {
628
+ console.error('[WebRTCService] Snapshot upload error:', error);
629
+ await this.signalingClient.send('command', {
630
+ type: 'snapshotError',
631
+ requestId,
632
+ error: String(error),
633
+ });
634
+ }
635
+ }
636
+ }
@@ -82,6 +82,8 @@ export enum AnalyticsEventName {
82
82
  IDENTITY_DOCUMENT_EID_SCAN_COMPLETED = 'identity_document_eid_scan_completed',
83
83
  LIVENESS_CHECK_STARTED = 'liveness_check_started',
84
84
  LIVENESS_CHECK_COMPLETED = 'liveness_check_completed',
85
+ VIDEO_CALL_STARTED = 'video_call_started',
86
+ VIDEO_CALL_COMPLETED = 'video_call_completed',
85
87
 
86
88
  // NFC Scan Events (used by trackNFCScan* helpers)
87
89
  NFC_SCAN_STARTED = 'nfc_scan_started',
@@ -11,6 +11,9 @@ export interface IdentificationInfo {
11
11
  scannedDocument?: ScannedIdentityDocument;
12
12
  livenessDetection?: LivenessDetection;
13
13
  locale: string;
14
+ authToken?: string;
15
+ videoSessionId?: string;
16
+ mediaUploadedDuringVideoCall?: boolean;
14
17
  }
15
18
 
16
19
  export interface ScannedIdentityDocument {
@@ -39,7 +42,8 @@ export interface WorkflowStep {
39
42
  | 'IDENTITY_DOCUMENT_SCAN'
40
43
  | 'IDENTITY_DOCUMENT_EID_SCAN'
41
44
  | 'LIVENESS_CHECK'
42
- | 'AML_CHECK';
45
+ | 'AML_CHECK'
46
+ | 'VIDEO_CALL';
43
47
  data?: {
44
48
  contracts?: {
45
49
  en: {
@@ -158,6 +158,31 @@ export default {
158
158
  'identityDocumentCamera.documentTooLarge': 'Too close. Move back.',
159
159
  'identityDocumentCamera.holdSteady': 'Keep device steady',
160
160
  'identityDocumentCamera.centerDocument': 'Center document in the frame',
161
+ 'videoCallScreen.guideHeader': 'Get Ready for Video Call',
162
+ 'videoCallScreen.guideText': 'Before you begin, please note the following:',
163
+ 'videoCallScreen.guidePoint1':
164
+ 'Ensure you are in a quiet environment with good lighting',
165
+ 'videoCallScreen.guidePoint2':
166
+ 'Have your identity document ready if requested',
167
+ 'videoCallScreen.guidePoint3': 'Make sure your internet connection is stable',
168
+ 'videoCallScreen.guidePoint4':
169
+ 'The agent will guide you through the verification process',
170
+ 'videoCallScreen.waitingForAgent': 'Waiting for agent...',
171
+ 'videoCallScreen.queuePosition': 'Queue position: {position}',
172
+ 'videoCallScreen.new': 'Initializing connection...',
173
+ 'videoCallScreen.connecting': 'Connecting...',
174
+ 'videoCallScreen.connected': 'Connected',
175
+ 'videoCallScreen.disconnected': 'Disconnected',
176
+ 'videoCallScreen.failed': 'Connection failed',
177
+ 'videoCallScreen.closed': 'Connection closed',
178
+ 'videoCallScreen.permissions_denied': 'Camera permissions required',
179
+ 'videoCallScreen.leaveQueue': 'Leave Queue',
180
+ 'videoCallScreen.closeSession': 'Close Session',
181
+ 'videoCallScreen.closeSessionHint':
182
+ 'You can close this session and start a new verification',
183
+ 'videoCallScreen.agentInstructions': 'Instructions from Agent',
184
+ 'videoCallScreen.callNotCompleted':
185
+ 'Video call was not completed. This step is required to proceed.',
161
186
  'navigationManager.skipStepWarning':
162
187
  'Completing this step is recommended for successful verification. Skip anyway?',
163
188
  'navigationManager.skipStepLabel': 'Skip This Step',