@trustchex/react-native-sdk 1.381.0 → 1.409.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/src/main/java/com/trustchex/reactnativesdk/camera/TrustchexCameraView.kt +1 -12
- package/android/src/main/java/com/trustchex/reactnativesdk/mlkit/MLKitModule.kt +1 -1
- package/ios/Camera/TrustchexCameraView.swift +1 -12
- package/ios/MLKit/MLKitModule.swift +1 -1
- package/lib/module/Screens/Debug/BarcodeTestScreen.js +308 -0
- package/lib/module/Screens/Debug/MRZTestScreen.js +105 -13
- package/lib/module/Screens/Dynamic/ContractAcceptanceScreen.js +49 -29
- package/lib/module/Screens/Dynamic/IdentityDocumentEIDScanningScreen.js +5 -0
- package/lib/module/Screens/Dynamic/IdentityDocumentScanningScreen.js +5 -0
- package/lib/module/Screens/Dynamic/LivenessDetectionScreen.js +26 -6
- package/lib/module/Screens/Dynamic/VideoCallScreen.js +676 -0
- package/lib/module/Screens/Static/OTPVerificationScreen.js +6 -0
- package/lib/module/Screens/Static/QrCodeScanningScreen.js +7 -1
- package/lib/module/Screens/Static/ResultScreen.js +27 -13
- package/lib/module/Screens/Static/VerificationSessionCheckScreen.js +51 -51
- package/lib/module/Shared/Animations/video-call.json +1 -0
- package/lib/module/Shared/Components/DebugNavigationPanel.js +180 -14
- package/lib/module/Shared/Components/EIDScanner.js +1 -4
- package/lib/module/Shared/Components/IdentityDocumentCamera.js +29 -8
- package/lib/module/Shared/Components/NavigationManager.js +15 -3
- package/lib/module/Shared/Contexts/AppContext.js +1 -0
- package/lib/module/Shared/Libs/SignalingClient.js +128 -0
- package/lib/module/Shared/Libs/analytics.utils.js +4 -0
- package/lib/module/Shared/Libs/deeplink.utils.js +9 -1
- package/lib/module/Shared/Libs/http-client.js +9 -0
- package/lib/module/Shared/Libs/promise.utils.js +16 -2
- package/lib/module/Shared/Libs/status-bar.utils.js +21 -0
- package/lib/module/Shared/Services/DataUploadService.js +294 -0
- package/lib/module/Shared/Services/VideoSessionService.js +156 -0
- package/lib/module/Shared/Services/WebRTCService.js +510 -0
- package/lib/module/Shared/Types/analytics.types.js +2 -0
- package/lib/module/Translation/Resources/en.js +20 -0
- package/lib/module/Translation/Resources/tr.js +20 -0
- package/lib/module/Trustchex.js +10 -0
- package/lib/module/version.js +1 -1
- package/lib/typescript/src/Screens/Debug/BarcodeTestScreen.d.ts +3 -0
- package/lib/typescript/src/Screens/Debug/BarcodeTestScreen.d.ts.map +1 -0
- package/lib/typescript/src/Screens/Debug/MRZTestScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Dynamic/ContractAcceptanceScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Dynamic/IdentityDocumentEIDScanningScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Dynamic/IdentityDocumentScanningScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Dynamic/LivenessDetectionScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Dynamic/VideoCallScreen.d.ts +3 -0
- package/lib/typescript/src/Screens/Dynamic/VideoCallScreen.d.ts.map +1 -0
- package/lib/typescript/src/Screens/Static/OTPVerificationScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Static/QrCodeScanningScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Static/ResultScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Static/VerificationSessionCheckScreen.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/DebugNavigationPanel.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/EIDScanner.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/NavigationManager.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Contexts/AppContext.d.ts +1 -0
- package/lib/typescript/src/Shared/Contexts/AppContext.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/SignalingClient.d.ts +24 -0
- package/lib/typescript/src/Shared/Libs/SignalingClient.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Libs/analytics.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/deeplink.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/http-client.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/promise.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/status-bar.utils.d.ts +9 -0
- package/lib/typescript/src/Shared/Libs/status-bar.utils.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Services/DataUploadService.d.ts +25 -0
- package/lib/typescript/src/Shared/Services/DataUploadService.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Services/VideoSessionService.d.ts +33 -0
- package/lib/typescript/src/Shared/Services/VideoSessionService.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Services/WebRTCService.d.ts +58 -0
- package/lib/typescript/src/Shared/Services/WebRTCService.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Types/analytics.types.d.ts +2 -0
- package/lib/typescript/src/Shared/Types/analytics.types.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Types/identificationInfo.d.ts +4 -1
- package/lib/typescript/src/Shared/Types/identificationInfo.d.ts.map +1 -1
- package/lib/typescript/src/Translation/Resources/en.d.ts +20 -0
- package/lib/typescript/src/Translation/Resources/en.d.ts.map +1 -1
- package/lib/typescript/src/Translation/Resources/tr.d.ts +20 -0
- package/lib/typescript/src/Translation/Resources/tr.d.ts.map +1 -1
- package/lib/typescript/src/Trustchex.d.ts.map +1 -1
- package/lib/typescript/src/version.d.ts +1 -1
- package/package.json +29 -2
- package/src/Screens/Debug/BarcodeTestScreen.tsx +317 -0
- package/src/Screens/Debug/MRZTestScreen.tsx +107 -13
- package/src/Screens/Dynamic/ContractAcceptanceScreen.tsx +59 -33
- package/src/Screens/Dynamic/IdentityDocumentEIDScanningScreen.tsx +6 -0
- package/src/Screens/Dynamic/IdentityDocumentScanningScreen.tsx +6 -0
- package/src/Screens/Dynamic/LivenessDetectionScreen.tsx +34 -6
- package/src/Screens/Dynamic/VideoCallScreen.tsx +764 -0
- package/src/Screens/Static/OTPVerificationScreen.tsx +6 -0
- package/src/Screens/Static/QrCodeScanningScreen.tsx +7 -1
- package/src/Screens/Static/ResultScreen.tsx +58 -23
- package/src/Screens/Static/VerificationSessionCheckScreen.tsx +58 -72
- package/src/Shared/Animations/video-call.json +1 -0
- package/src/Shared/Components/DebugNavigationPanel.tsx +185 -9
- package/src/Shared/Components/EIDScanner.tsx +1 -5
- package/src/Shared/Components/IdentityDocumentCamera.tsx +29 -8
- package/src/Shared/Components/NavigationManager.tsx +14 -1
- package/src/Shared/Contexts/AppContext.ts +2 -0
- package/src/Shared/Libs/SignalingClient.ts +189 -0
- package/src/Shared/Libs/analytics.utils.ts +4 -0
- package/src/Shared/Libs/deeplink.utils.ts +12 -1
- package/src/Shared/Libs/http-client.ts +10 -0
- package/src/Shared/Libs/promise.utils.ts +16 -2
- package/src/Shared/Libs/status-bar.utils.ts +19 -0
- package/src/Shared/Services/DataUploadService.ts +395 -0
- package/src/Shared/Services/VideoSessionService.ts +190 -0
- package/src/Shared/Services/WebRTCService.ts +636 -0
- package/src/Shared/Types/analytics.types.ts +2 -0
- package/src/Shared/Types/identificationInfo.ts +5 -1
- package/src/Translation/Resources/en.ts +25 -0
- package/src/Translation/Resources/tr.ts +27 -0
- package/src/Trustchex.tsx +12 -2
- package/src/version.ts +1 -1
|
@@ -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
|
|
170
|
+
if (data?.action?.type === 'RESET') {
|
|
159
171
|
navigation.dispatch(data.action);
|
|
160
172
|
}
|
|
161
173
|
});
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
+
}
|