@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
@@ -29,7 +29,8 @@ const NavigationManager = /*#__PURE__*/forwardRef(({
29
29
  CONTRACT_ACCEPTANCE: 'ContractAcceptanceScreen',
30
30
  IDENTITY_DOCUMENT_SCAN: 'IdentityDocumentScanningScreen',
31
31
  IDENTITY_DOCUMENT_EID_SCAN: 'IdentityDocumentEIDScanningScreen',
32
- LIVENESS_CHECK: 'LivenessDetectionScreen'
32
+ LIVENESS_CHECK: 'LivenessDetectionScreen',
33
+ VIDEO_CALL: 'VideoCallScreen'
33
34
  },
34
35
  RESULT: 'ResultScreen'
35
36
  };
@@ -69,6 +70,9 @@ const NavigationManager = /*#__PURE__*/forwardRef(({
69
70
  if (nextStep.type === 'LIVENESS_CHECK') {
70
71
  return routes.DYNAMIC_ROUTES.LIVENESS_CHECK;
71
72
  }
73
+ if (nextStep.type === 'VIDEO_CALL') {
74
+ return routes.DYNAMIC_ROUTES.VIDEO_CALL;
75
+ }
72
76
  return routes.VERIFICATION_SESSION_CHECK;
73
77
  }, [appContext, routes.VERIFICATION_SESSION_CHECK, routes.DYNAMIC_ROUTES.CONTRACT_ACCEPTANCE, routes.DYNAMIC_ROUTES.IDENTITY_DOCUMENT_EID_SCAN, routes.DYNAMIC_ROUTES.IDENTITY_DOCUMENT_SCAN, routes.DYNAMIC_ROUTES.LIVENESS_CHECK, routes.RESULT]);
74
78
  const goToNextRoute = useCallback(() => {
@@ -124,8 +128,15 @@ const NavigationManager = /*#__PURE__*/forwardRef(({
124
128
  contractIds: [],
125
129
  deviceInfo: ''
126
130
  },
127
- locale: appContext.locale || i18n.language
131
+ locale: appContext.locale || i18n.language,
132
+ // Explicitly reset collected data fields
133
+ scannedDocument: undefined,
134
+ livenessDetection: undefined,
135
+ authToken: undefined,
136
+ videoSessionId: undefined
128
137
  };
138
+
139
+ // Reset branding to defaults while preserving any custom values
129
140
  appContext.branding = {
130
141
  logoUrl: appContext.branding?.logoUrl || '',
131
142
  primaryColor: appContext.branding?.primaryColor || '#000000',
@@ -138,6 +149,7 @@ const NavigationManager = /*#__PURE__*/forwardRef(({
138
149
  appContext.setIsDemoSession?.(false);
139
150
  analyticsService.setDemoSession(false);
140
151
  }
152
+ appContext.isTestVideoSession = false;
141
153
  navigation.dispatch(CommonActions.reset({
142
154
  index: 0,
143
155
  routes: [{
@@ -155,7 +167,7 @@ const NavigationManager = /*#__PURE__*/forwardRef(({
155
167
  usePreventRemove(true, ({
156
168
  data
157
169
  }) => {
158
- if (data.action.type === 'RESET') {
170
+ if (data?.action?.type === 'RESET') {
159
171
  navigation.dispatch(data.action);
160
172
  }
161
173
  });
@@ -23,6 +23,7 @@ export default /*#__PURE__*/createContext({
23
23
  workflowSteps: [],
24
24
  currentWorkflowStep: undefined,
25
25
  isDebugNavigated: false,
26
+ isTestVideoSession: false,
26
27
  onCompleted: undefined,
27
28
  onError: undefined,
28
29
  setSessionId: undefined,
@@ -0,0 +1,128 @@
1
+ "use strict";
2
+
3
+ import EventSource from 'react-native-sse';
4
+ export class SignalingClient {
5
+ eventSource = null;
6
+ constructor(baseUrl, sessionId, onMessage, identificationId, onSessionEnded) {
7
+ this.baseUrl = baseUrl;
8
+ this.sessionId = sessionId;
9
+ this.onMessage = onMessage;
10
+ this.identificationId = identificationId;
11
+ this.onSessionEnded = onSessionEnded;
12
+ }
13
+ connect() {
14
+ if (this.eventSource) {
15
+ this.eventSource.close();
16
+ }
17
+ const urlParams = new URLSearchParams();
18
+ if (this.identificationId) {
19
+ urlParams.append('identificationId', this.identificationId);
20
+ }
21
+ const url = `${this.baseUrl}/api/app/mobile/video-sessions/${this.sessionId}/signaling/stream?${urlParams.toString()}`;
22
+ console.log('[SignalingClient] Connecting to SSE:', url);
23
+ this.eventSource = new EventSource(url, {
24
+ pollingInterval: 0 // 0 means default/no polling if SSE is real
25
+ });
26
+ const listener = event => {
27
+ if (event.type === 'open') {
28
+ console.log('[SignalingClient] Connected');
29
+ this.onConnected?.();
30
+ } else if (event.type === 'error') {
31
+ console.error('[SignalingClient] Connection error:', JSON.stringify(event, null, 2));
32
+ this.onDisconnected?.();
33
+ } else if (event.type === 'message') {
34
+ try {
35
+ const data = JSON.parse(event.data || '{}');
36
+ if (['offer', 'answer', 'ice-candidate', 'command'].includes(data.type)) {
37
+ this.onMessage({
38
+ id: data.id,
39
+ type: data.type,
40
+ payload: data.payload,
41
+ createdAt: data.createdAt
42
+ });
43
+ }
44
+ } catch (e) {
45
+ console.error('[SignalingClient] Error parsing message:', e);
46
+ }
47
+ }
48
+ };
49
+ const customEventListener = event => {
50
+ try {
51
+ const data = JSON.parse(event.data || '{}');
52
+ const eventType = event.type;
53
+ if (['offer', 'answer', 'ice-candidate', 'command'].includes(eventType)) {
54
+ this.onMessage({
55
+ id: data.id,
56
+ type: eventType,
57
+ payload: data.payload,
58
+ createdAt: data.createdAt
59
+ });
60
+ }
61
+ } catch (e) {
62
+ console.error('[SignalingClient] Error parsing custom event:', e);
63
+ }
64
+ };
65
+
66
+ // Ping handler - server sends pings to keep connection alive
67
+ const pingListener = event => {
68
+ console.log('[SignalingClient] Received ping');
69
+ // Just acknowledge by doing nothing, server-side heartbeat keeps connection alive
70
+ };
71
+
72
+ // Session-ended event listener
73
+ const sessionEndedListener = event => {
74
+ try {
75
+ const data = JSON.parse(event.data || '{}');
76
+ console.log('[SignalingClient] Session ended:', data.state);
77
+ if (this.onSessionEnded) {
78
+ this.onSessionEnded(data.state);
79
+ }
80
+ } catch (e) {
81
+ console.error('[SignalingClient] Error parsing session-ended event:', e);
82
+ }
83
+ };
84
+ this.eventSource.addEventListener('open', listener);
85
+ this.eventSource.addEventListener('message', listener);
86
+ this.eventSource.addEventListener('error', listener);
87
+ this.eventSource.addEventListener('ping', pingListener);
88
+ this.eventSource.addEventListener('offer', customEventListener);
89
+ this.eventSource.addEventListener('answer', customEventListener);
90
+ this.eventSource.addEventListener('ice-candidate', customEventListener);
91
+ this.eventSource.addEventListener('command', customEventListener);
92
+ this.eventSource.addEventListener('session-ended', sessionEndedListener);
93
+ }
94
+ async send(type, payload) {
95
+ const urlParams = new URLSearchParams();
96
+ if (this.identificationId) {
97
+ urlParams.append('identificationId', this.identificationId);
98
+ }
99
+ const url = `${this.baseUrl}/api/app/mobile/video-sessions/${this.sessionId}/signaling?${urlParams.toString()}`;
100
+ try {
101
+ const body = {
102
+ type,
103
+ data: payload
104
+ };
105
+ const response = await fetch(url, {
106
+ method: 'POST',
107
+ headers: {
108
+ 'Content-Type': 'application/json'
109
+ },
110
+ body: JSON.stringify(body)
111
+ });
112
+ if (!response.ok) {
113
+ throw new Error(`Failed to send signaling message: ${response.statusText}`);
114
+ }
115
+ } catch (error) {
116
+ console.error('[SignalingClient] Error sending message:', error);
117
+ throw error;
118
+ }
119
+ }
120
+ disconnect() {
121
+ if (this.eventSource) {
122
+ this.eventSource.removeAllEventListeners();
123
+ this.eventSource.close();
124
+ this.eventSource = null;
125
+ this.onDisconnected?.();
126
+ }
127
+ }
128
+ }
@@ -188,6 +188,10 @@ const STEP_EVENT_MAP = {
188
188
  liveness_check: {
189
189
  started: AnalyticsEventName.LIVENESS_CHECK_STARTED,
190
190
  completed: AnalyticsEventName.LIVENESS_CHECK_COMPLETED
191
+ },
192
+ video_call: {
193
+ started: AnalyticsEventName.VIDEO_CALL_STARTED,
194
+ completed: AnalyticsEventName.VIDEO_CALL_COMPLETED
191
195
  }
192
196
  };
193
197
  function getStepEventName(stepType, suffix) {
@@ -12,7 +12,7 @@ const handleDeepLink = ({
12
12
  let baseUrl = '';
13
13
  let sessionId = '';
14
14
  for (let i = 0; i < segments.length; i++) {
15
- if (segments[i] === 'verification-session') {
15
+ if (segments[i] === 'verification-session' || segments[i] === 'verification-sessions') {
16
16
  sessionId = segments[i + 1] ?? '';
17
17
  debugLog('handleDeepLink', 'Found sessionId:', sessionId);
18
18
  } else if (segments[i] === 'app-url') {
@@ -20,6 +20,14 @@ const handleDeepLink = ({
20
20
  debugLog('handleDeepLink', 'Found baseUrl:', baseUrl);
21
21
  }
22
22
  }
23
+
24
+ // If no app-url segment found, derive baseUrl from the URL itself
25
+ if (!baseUrl && sessionId) {
26
+ const match = url.match(/^(https?:\/\/[^/]+)/);
27
+ if (match) {
28
+ baseUrl = match[1];
29
+ }
30
+ }
23
31
  debugLog('handleDeepLink', 'Returning:', {
24
32
  baseUrl,
25
33
  sessionId
@@ -47,6 +47,10 @@ const request = async (httpMethod, url, body, simulatedResponse) => {
47
47
  const startTime = Date.now();
48
48
  let statusCode = 0;
49
49
  let success = false;
50
+ console.log(`[HTTP] ${httpMethod} ${url}`);
51
+ if (body) {
52
+ console.log('[HTTP] Request body:', JSON.stringify(body).substring(0, 200) + '...');
53
+ }
50
54
  try {
51
55
  const response = await fetch(url, {
52
56
  method: httpMethod,
@@ -57,11 +61,16 @@ const request = async (httpMethod, url, body, simulatedResponse) => {
57
61
  });
58
62
  statusCode = response.status;
59
63
  success = response.ok;
64
+ console.log(`[HTTP] Response status: ${statusCode} ${response.ok ? '✓' : '✗'}`);
60
65
  let responseJson = null;
61
66
  try {
62
67
  responseJson = await response.json();
68
+ if (responseJson) {
69
+ console.log('[HTTP] Response body:', JSON.stringify(responseJson).substring(0, 200) + '...');
70
+ }
63
71
  } catch (error) {
64
72
  // Invalid JSON response
73
+ console.log('[HTTP] Response body: (non-JSON)');
65
74
  }
66
75
 
67
76
  // Track API call performance (non-blocking)
@@ -2,16 +2,30 @@
2
2
 
3
3
  const runWithRetry = async (fn, maxRetries = 3, delay = 1000) => {
4
4
  let retries = 0;
5
+ let lastError;
5
6
  let result;
6
7
  while (retries < maxRetries) {
7
8
  try {
9
+ if (retries > 0) {
10
+ console.log(`[Retry] Attempt ${retries + 1}/${maxRetries}...`);
11
+ }
8
12
  result = await fn();
13
+ if (retries > 0) {
14
+ console.log(`[Retry] ✓ Success on attempt ${retries + 1}`);
15
+ }
9
16
  return result;
10
17
  } catch (error) {
18
+ lastError = error;
11
19
  retries++;
12
- await new Promise(resolve => setTimeout(resolve, delay * retries));
20
+ console.error(`[Retry] Attempt ${retries}/${maxRetries} failed:`, error instanceof Error ? error.message : error);
21
+ if (retries < maxRetries) {
22
+ const waitTime = delay * retries;
23
+ console.log(`[Retry] Waiting ${waitTime}ms before retry...`);
24
+ await new Promise(resolve => setTimeout(resolve, waitTime));
25
+ }
13
26
  }
14
27
  }
15
- throw new Error('Max retries exceeded');
28
+ console.error('[Retry] ✗ All retries exhausted. Last error:', lastError);
29
+ throw new Error(`Max retries (${maxRetries}) exceeded. Last error: ${lastError instanceof Error ? lastError.message : String(lastError)}`);
16
30
  };
17
31
  export { runWithRetry };
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+
3
+ import { StatusBar } from 'react-native';
4
+ import { useEffect } from 'react';
5
+
6
+ /**
7
+ * Configure status bar for white background with dark content for better contrast
8
+ */
9
+ export const configureStatusBarForWhiteBackground = () => {
10
+ StatusBar.setBarStyle('dark-content', true);
11
+ StatusBar.setBackgroundColor('#ffffff', true);
12
+ };
13
+
14
+ /**
15
+ * Hook to configure status bar for white background on mount
16
+ */
17
+ export const useStatusBarWhiteBackground = () => {
18
+ useEffect(() => {
19
+ configureStatusBarForWhiteBackground();
20
+ }, []);
21
+ };
@@ -0,0 +1,294 @@
1
+ "use strict";
2
+
3
+ import RNFS from 'react-native-fs';
4
+ import { Platform } from 'react-native';
5
+ import { getSessionKey, encryptWithAes } from "../Libs/crypto.utils.js";
6
+ import mrzUtils from "../Libs/mrz.utils.js";
7
+ import httpClient from "../Libs/http-client.js";
8
+ import { NotFoundError } from "../Libs/http-client.js";
9
+ import { runWithRetry } from "../Libs/promise.utils.js";
10
+
11
+ /**
12
+ * Service to upload collected identification data to the backend.
13
+ * This is used to send data before/during video call so agents can see it.
14
+ */
15
+ export class DataUploadService {
16
+ constructor(baseUrl) {
17
+ this.baseUrl = baseUrl;
18
+ this.apiUrl = `${baseUrl}/api/app/mobile`;
19
+ }
20
+ async ensureIdentificationExists(identificationId) {
21
+ console.log('[DataUploadService] POST', `${this.apiUrl}/identifications/${identificationId}`);
22
+ await httpClient.post(`${this.apiUrl}/identifications/${identificationId}`, {});
23
+ console.log('[DataUploadService] ✓ Identification created/verified');
24
+ }
25
+
26
+ /**
27
+ * Submit document data to the backend (same approach as ResultScreen)
28
+ */
29
+ async submitDocumentData(identificationId, scannedDocument, sessionKey) {
30
+ if (!scannedDocument || scannedDocument.documentType === 'UNKNOWN') {
31
+ console.log('[DataUploadService] No document data to submit');
32
+ return;
33
+ }
34
+ const mrzFields = scannedDocument.mrzFields;
35
+ if (!mrzFields) {
36
+ console.log('[DataUploadService] No MRZ fields to submit');
37
+ return;
38
+ }
39
+ const identificationDocument = {
40
+ type: mrzFields.documentCode,
41
+ name: mrzFields.firstName,
42
+ surname: mrzFields.lastName,
43
+ gender: this.getGenderEnumType(mrzFields.sex),
44
+ number: mrzFields.documentNumber,
45
+ country: mrzFields.issuingState,
46
+ barcodeValue: scannedDocument.barcodeValue,
47
+ personalNumber: mrzFields.personalNumber || mrzFields.optional1,
48
+ birthDate: mrzUtils.convertMRZDateToISODate(mrzFields.birthDate),
49
+ expiryDate: mrzUtils.convertMRZDateToISODate(mrzFields.expirationDate),
50
+ dataSource: scannedDocument.dataSource,
51
+ mrzText: scannedDocument.mrzText
52
+ };
53
+ console.log('[DataUploadService] Submitting document data:', identificationDocument.type, identificationDocument.number);
54
+ const {
55
+ encryptedData,
56
+ nonce
57
+ } = encryptWithAes(JSON.stringify(identificationDocument), sessionKey);
58
+ console.log('[DataUploadService] POST', `${this.apiUrl}/identifications/${identificationId}/documents`);
59
+ await runWithRetry(() => httpClient.post(`${this.apiUrl}/identifications/${identificationId}/documents`, {
60
+ encryptedData,
61
+ nonce
62
+ }));
63
+ console.log('[DataUploadService] ✓ Document data submitted');
64
+ }
65
+
66
+ /**
67
+ * Upload media files (document images, selfies, etc.)
68
+ */
69
+ async uploadMedia(identificationId, scannedDocument, livenessDetection, onProgress) {
70
+ const uploadFileOptions = {
71
+ toUrl: `${this.apiUrl}/identifications/${identificationId}/media`,
72
+ method: 'POST',
73
+ headers: {
74
+ Accept: 'application/json'
75
+ },
76
+ files: [],
77
+ progress: res => {
78
+ const progress = res.totalBytesSent / res.totalBytesExpectedToSend;
79
+ onProgress?.(progress);
80
+ }
81
+ };
82
+
83
+ // Add document front image
84
+ const frontImage = scannedDocument?.frontImage;
85
+ if (frontImage && frontImage !== '') {
86
+ const filePath = `${RNFS.TemporaryDirectoryPath}/DOCUMENT_FRONT_IMAGE.jpg`;
87
+ await RNFS.writeFile(decodeURIComponent(filePath), frontImage, 'base64');
88
+ uploadFileOptions.files.push({
89
+ name: 'files',
90
+ filename: 'DOCUMENT_FRONT_IMAGE.jpg',
91
+ filepath: decodeURIComponent(filePath),
92
+ filetype: 'image/jpeg'
93
+ });
94
+ }
95
+
96
+ // Add document back image
97
+ const backImage = scannedDocument?.backImage;
98
+ if (backImage && backImage !== '') {
99
+ const filePath = `${RNFS.TemporaryDirectoryPath}/DOCUMENT_BACK_IMAGE.jpg`;
100
+ await RNFS.writeFile(decodeURIComponent(filePath), backImage, 'base64');
101
+ uploadFileOptions.files.push({
102
+ name: 'files',
103
+ filename: 'DOCUMENT_BACK_IMAGE.jpg',
104
+ filepath: decodeURIComponent(filePath),
105
+ filetype: 'image/jpeg'
106
+ });
107
+ }
108
+
109
+ // Add face image from document
110
+ const faceImage = scannedDocument?.faceImage;
111
+ if (faceImage && faceImage !== '') {
112
+ const filePath = `${RNFS.TemporaryDirectoryPath}/FACE_IMAGE.jpg`;
113
+ await RNFS.writeFile(decodeURIComponent(filePath), faceImage, 'base64');
114
+ uploadFileOptions.files.push({
115
+ name: 'files',
116
+ filename: 'FACE_IMAGE.jpg',
117
+ filepath: decodeURIComponent(filePath),
118
+ filetype: 'image/jpeg'
119
+ });
120
+ }
121
+
122
+ // Add secondary face image from document (optional)
123
+ const secondaryFaceImage = scannedDocument?.secondaryFaceImage;
124
+ if (secondaryFaceImage && secondaryFaceImage !== '') {
125
+ const filePath = `${RNFS.TemporaryDirectoryPath}/DOCUMENT_SECONDARY_FACE_IMAGE.jpg`;
126
+ await RNFS.writeFile(decodeURIComponent(filePath), secondaryFaceImage, 'base64');
127
+ uploadFileOptions.files.push({
128
+ name: 'files',
129
+ filename: 'DOCUMENT_SECONDARY_FACE_IMAGE.jpg',
130
+ filepath: decodeURIComponent(filePath),
131
+ filetype: 'image/jpeg'
132
+ });
133
+ }
134
+
135
+ // Add hologram image from document (optional)
136
+ const hologramImage = scannedDocument?.hologramImage;
137
+ if (hologramImage && hologramImage !== '') {
138
+ const filePath = `${RNFS.TemporaryDirectoryPath}/DOCUMENT_HOLOGRAM_IMAGE.jpg`;
139
+ await RNFS.writeFile(decodeURIComponent(filePath), hologramImage, 'base64');
140
+ uploadFileOptions.files.push({
141
+ name: 'files',
142
+ filename: 'DOCUMENT_HOLOGRAM_IMAGE.jpg',
143
+ filepath: decodeURIComponent(filePath),
144
+ filetype: 'image/jpeg'
145
+ });
146
+ }
147
+
148
+ // Add liveness images and selfie from liveness detection
149
+ if (livenessDetection?.instructions) {
150
+ for (const instruction of livenessDetection.instructions) {
151
+ if (instruction?.photo) {
152
+ const filePath = `${RNFS.TemporaryDirectoryPath}/LIVENESS_${instruction.instruction}_IMAGE.jpg`;
153
+ await RNFS.writeFile(decodeURIComponent(filePath), instruction.photo, 'base64');
154
+ uploadFileOptions.files.push({
155
+ name: 'files',
156
+ filename: `LIVENESS_${instruction.instruction}_IMAGE.jpg`,
157
+ filepath: decodeURIComponent(filePath),
158
+ filetype: 'image/jpeg'
159
+ });
160
+ if (instruction.instruction === 'LOOK_STRAIGHT_AND_BLINK') {
161
+ uploadFileOptions.files.push({
162
+ name: 'files',
163
+ filename: 'SELFIE_IMAGE.jpg',
164
+ filepath: decodeURIComponent(filePath),
165
+ filetype: 'image/jpeg'
166
+ });
167
+ }
168
+ }
169
+ }
170
+ }
171
+
172
+ // Add liveness video (optional)
173
+ if (livenessDetection?.videoPath) {
174
+ let videoFilePath;
175
+ if (Platform.OS === 'ios') {
176
+ const tempDir = `${RNFS.TemporaryDirectoryPath}/${new Date().getTime()}`;
177
+ await RNFS.mkdir(tempDir);
178
+ videoFilePath = `${tempDir}/LIVENESS_VIDEO.mp4`;
179
+ } else {
180
+ videoFilePath = `${RNFS.TemporaryDirectoryPath}/LIVENESS_VIDEO.mp4`;
181
+ }
182
+ await RNFS.copyFile(livenessDetection.videoPath, videoFilePath);
183
+ uploadFileOptions.files.push({
184
+ name: 'files',
185
+ filename: 'LIVENESS_VIDEO.mp4',
186
+ filepath: decodeURIComponent(videoFilePath),
187
+ filetype: 'video/mp4'
188
+ });
189
+ }
190
+
191
+ // Skip upload if no files
192
+ if (uploadFileOptions.files.length === 0) {
193
+ console.log('[DataUploadService] No media files to upload');
194
+ return;
195
+ }
196
+ console.log('[DataUploadService] Uploading', uploadFileOptions.files.length, 'media files to', uploadFileOptions.toUrl);
197
+ const response = await runWithRetry(() => RNFS.uploadFiles(uploadFileOptions).promise);
198
+ console.log('[DataUploadService] Upload response status:', response.statusCode);
199
+ if (![200, 201, 204].includes(response.statusCode)) {
200
+ console.error('[DataUploadService] Media upload failed:', response.statusCode, response.body);
201
+ throw new Error(`Media upload failed: ${response.statusCode}`);
202
+ }
203
+ console.log('[DataUploadService] ✓ Media uploaded successfully');
204
+ }
205
+
206
+ /**
207
+ * Upload all collected data (document + media) before video call
208
+ */
209
+ async uploadCollectedData(identificationInfo, onProgress) {
210
+ const {
211
+ identificationId,
212
+ sessionId,
213
+ scannedDocument,
214
+ livenessDetection
215
+ } = identificationInfo;
216
+ let {
217
+ authToken
218
+ } = identificationInfo;
219
+ if (!identificationId) {
220
+ console.log('[DataUploadService] No identification ID, skipping upload');
221
+ return false;
222
+ }
223
+ console.log('[DataUploadService] ========== UPLOADING COLLECTED DATA ==========');
224
+ console.log('[DataUploadService] Identification ID:', identificationId);
225
+ console.log('[DataUploadService] Has document:', !!scannedDocument);
226
+ console.log('[DataUploadService] Has liveness:', !!livenessDetection);
227
+ console.log('[DataUploadService] Has auth token:', !!authToken);
228
+ try {
229
+ await runWithRetry(() => this.ensureIdentificationExists(identificationId));
230
+
231
+ // Always refresh session key for current session (required for encrypted submission)
232
+ if (sessionId) {
233
+ const existingAuthToken = authToken;
234
+ console.log('[DataUploadService] Getting session key...');
235
+ try {
236
+ authToken = await runWithRetry(() => getSessionKey(this.apiUrl, sessionId));
237
+ console.log('[DataUploadService] ✓ Session key obtained');
238
+ } catch (error) {
239
+ if (existingAuthToken) {
240
+ console.warn('[DataUploadService] Session key refresh failed, using existing token');
241
+ authToken = existingAuthToken;
242
+ } else {
243
+ throw error;
244
+ }
245
+ }
246
+ }
247
+ if (!authToken) {
248
+ console.log('[DataUploadService] No session key available, skipping upload');
249
+ return false;
250
+ }
251
+
252
+ // Step 1: Submit document data (using same encryption as ResultScreen)
253
+ if (scannedDocument) {
254
+ onProgress?.(0.1);
255
+ await runWithRetry(() => this.submitDocumentData(identificationId, scannedDocument, authToken));
256
+ onProgress?.(0.3);
257
+ }
258
+
259
+ // Step 2: Upload media files (images only, skip video for now - too slow)
260
+ onProgress?.(0.4);
261
+ await runWithRetry(() => this.uploadMedia(identificationId, scannedDocument, livenessDetection, p => {
262
+ onProgress?.(0.4 + p * 0.5);
263
+ }));
264
+ onProgress?.(1.0);
265
+ console.log('[DataUploadService] ✓ All collected data uploaded');
266
+
267
+ // Mark media as uploaded during video call
268
+ identificationInfo.mediaUploadedDuringVideoCall = true;
269
+
270
+ // Store the auth token back for future use
271
+ identificationInfo.authToken = authToken;
272
+ return true;
273
+ } catch (error) {
274
+ if (error instanceof NotFoundError) {
275
+ console.warn('[DataUploadService] Upload skipped: identification is not active anymore');
276
+ return false;
277
+ }
278
+ console.error('[DataUploadService] Failed to upload collected data:', error);
279
+ return false;
280
+ }
281
+ }
282
+ getGenderEnumType(sex) {
283
+ switch (sex?.toLowerCase()) {
284
+ case 'male':
285
+ case 'm':
286
+ return 'M';
287
+ case 'female':
288
+ case 'f':
289
+ return 'F';
290
+ default:
291
+ return 'X';
292
+ }
293
+ }
294
+ }