@trustchex/react-native-sdk 1.381.0 → 1.409.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/src/main/java/com/trustchex/reactnativesdk/camera/TrustchexCameraView.kt +1 -12
- package/android/src/main/java/com/trustchex/reactnativesdk/mlkit/MLKitModule.kt +1 -1
- package/ios/Camera/TrustchexCameraView.swift +1 -12
- package/ios/MLKit/MLKitModule.swift +1 -1
- package/lib/module/Screens/Debug/BarcodeTestScreen.js +308 -0
- package/lib/module/Screens/Debug/MRZTestScreen.js +105 -13
- package/lib/module/Screens/Dynamic/ContractAcceptanceScreen.js +49 -29
- package/lib/module/Screens/Dynamic/IdentityDocumentEIDScanningScreen.js +5 -0
- package/lib/module/Screens/Dynamic/IdentityDocumentScanningScreen.js +5 -0
- package/lib/module/Screens/Dynamic/LivenessDetectionScreen.js +26 -6
- package/lib/module/Screens/Dynamic/VideoCallScreen.js +676 -0
- package/lib/module/Screens/Static/OTPVerificationScreen.js +6 -0
- package/lib/module/Screens/Static/QrCodeScanningScreen.js +7 -1
- package/lib/module/Screens/Static/ResultScreen.js +27 -13
- package/lib/module/Screens/Static/VerificationSessionCheckScreen.js +51 -51
- package/lib/module/Shared/Animations/video-call.json +1 -0
- package/lib/module/Shared/Components/DebugNavigationPanel.js +180 -14
- package/lib/module/Shared/Components/EIDScanner.js +1 -4
- package/lib/module/Shared/Components/IdentityDocumentCamera.js +29 -8
- package/lib/module/Shared/Components/NavigationManager.js +15 -3
- package/lib/module/Shared/Contexts/AppContext.js +1 -0
- package/lib/module/Shared/Libs/SignalingClient.js +128 -0
- package/lib/module/Shared/Libs/analytics.utils.js +4 -0
- package/lib/module/Shared/Libs/deeplink.utils.js +9 -1
- package/lib/module/Shared/Libs/http-client.js +9 -0
- package/lib/module/Shared/Libs/promise.utils.js +16 -2
- package/lib/module/Shared/Libs/status-bar.utils.js +21 -0
- package/lib/module/Shared/Services/DataUploadService.js +294 -0
- package/lib/module/Shared/Services/VideoSessionService.js +156 -0
- package/lib/module/Shared/Services/WebRTCService.js +510 -0
- package/lib/module/Shared/Types/analytics.types.js +2 -0
- package/lib/module/Translation/Resources/en.js +20 -0
- package/lib/module/Translation/Resources/tr.js +20 -0
- package/lib/module/Trustchex.js +10 -0
- package/lib/module/version.js +1 -1
- package/lib/typescript/src/Screens/Debug/BarcodeTestScreen.d.ts +3 -0
- package/lib/typescript/src/Screens/Debug/BarcodeTestScreen.d.ts.map +1 -0
- package/lib/typescript/src/Screens/Debug/MRZTestScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Dynamic/ContractAcceptanceScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Dynamic/IdentityDocumentEIDScanningScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Dynamic/IdentityDocumentScanningScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Dynamic/LivenessDetectionScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Dynamic/VideoCallScreen.d.ts +3 -0
- package/lib/typescript/src/Screens/Dynamic/VideoCallScreen.d.ts.map +1 -0
- package/lib/typescript/src/Screens/Static/OTPVerificationScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Static/QrCodeScanningScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Static/ResultScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Static/VerificationSessionCheckScreen.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/DebugNavigationPanel.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/EIDScanner.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/NavigationManager.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Contexts/AppContext.d.ts +1 -0
- package/lib/typescript/src/Shared/Contexts/AppContext.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/SignalingClient.d.ts +24 -0
- package/lib/typescript/src/Shared/Libs/SignalingClient.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Libs/analytics.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/deeplink.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/http-client.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/promise.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/status-bar.utils.d.ts +9 -0
- package/lib/typescript/src/Shared/Libs/status-bar.utils.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Services/DataUploadService.d.ts +25 -0
- package/lib/typescript/src/Shared/Services/DataUploadService.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Services/VideoSessionService.d.ts +33 -0
- package/lib/typescript/src/Shared/Services/VideoSessionService.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Services/WebRTCService.d.ts +58 -0
- package/lib/typescript/src/Shared/Services/WebRTCService.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Types/analytics.types.d.ts +2 -0
- package/lib/typescript/src/Shared/Types/analytics.types.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Types/identificationInfo.d.ts +4 -1
- package/lib/typescript/src/Shared/Types/identificationInfo.d.ts.map +1 -1
- package/lib/typescript/src/Translation/Resources/en.d.ts +20 -0
- package/lib/typescript/src/Translation/Resources/en.d.ts.map +1 -1
- package/lib/typescript/src/Translation/Resources/tr.d.ts +20 -0
- package/lib/typescript/src/Translation/Resources/tr.d.ts.map +1 -1
- package/lib/typescript/src/Trustchex.d.ts.map +1 -1
- package/lib/typescript/src/version.d.ts +1 -1
- package/package.json +29 -2
- package/src/Screens/Debug/BarcodeTestScreen.tsx +317 -0
- package/src/Screens/Debug/MRZTestScreen.tsx +107 -13
- package/src/Screens/Dynamic/ContractAcceptanceScreen.tsx +59 -33
- package/src/Screens/Dynamic/IdentityDocumentEIDScanningScreen.tsx +6 -0
- package/src/Screens/Dynamic/IdentityDocumentScanningScreen.tsx +6 -0
- package/src/Screens/Dynamic/LivenessDetectionScreen.tsx +34 -6
- package/src/Screens/Dynamic/VideoCallScreen.tsx +764 -0
- package/src/Screens/Static/OTPVerificationScreen.tsx +6 -0
- package/src/Screens/Static/QrCodeScanningScreen.tsx +7 -1
- package/src/Screens/Static/ResultScreen.tsx +58 -23
- package/src/Screens/Static/VerificationSessionCheckScreen.tsx +58 -72
- package/src/Shared/Animations/video-call.json +1 -0
- package/src/Shared/Components/DebugNavigationPanel.tsx +185 -9
- package/src/Shared/Components/EIDScanner.tsx +1 -5
- package/src/Shared/Components/IdentityDocumentCamera.tsx +29 -8
- package/src/Shared/Components/NavigationManager.tsx +14 -1
- package/src/Shared/Contexts/AppContext.ts +2 -0
- package/src/Shared/Libs/SignalingClient.ts +189 -0
- package/src/Shared/Libs/analytics.utils.ts +4 -0
- package/src/Shared/Libs/deeplink.utils.ts +12 -1
- package/src/Shared/Libs/http-client.ts +10 -0
- package/src/Shared/Libs/promise.utils.ts +16 -2
- package/src/Shared/Libs/status-bar.utils.ts +19 -0
- package/src/Shared/Services/DataUploadService.ts +395 -0
- package/src/Shared/Services/VideoSessionService.ts +190 -0
- package/src/Shared/Services/WebRTCService.ts +636 -0
- package/src/Shared/Types/analytics.types.ts +2 -0
- package/src/Shared/Types/identificationInfo.ts +5 -1
- package/src/Translation/Resources/en.ts +25 -0
- package/src/Translation/Resources/tr.ts +27 -0
- package/src/Trustchex.tsx +12 -2
- package/src/version.ts +1 -1
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
import RNFS from 'react-native-fs';
|
|
2
|
+
import { Platform } from 'react-native';
|
|
3
|
+
import type {
|
|
4
|
+
IdentificationInfo,
|
|
5
|
+
ScannedIdentityDocument,
|
|
6
|
+
LivenessDetection,
|
|
7
|
+
} from '../Types/identificationInfo';
|
|
8
|
+
import { getSessionKey, encryptWithAes } from '../Libs/crypto.utils';
|
|
9
|
+
import mrzUtils from '../Libs/mrz.utils';
|
|
10
|
+
import httpClient from '../Libs/http-client';
|
|
11
|
+
import { NotFoundError } from '../Libs/http-client';
|
|
12
|
+
import { runWithRetry } from '../Libs/promise.utils';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Service to upload collected identification data to the backend.
|
|
16
|
+
* This is used to send data before/during video call so agents can see it.
|
|
17
|
+
*/
|
|
18
|
+
export class DataUploadService {
|
|
19
|
+
private baseUrl: string;
|
|
20
|
+
private apiUrl: string;
|
|
21
|
+
|
|
22
|
+
constructor(baseUrl: string) {
|
|
23
|
+
this.baseUrl = baseUrl;
|
|
24
|
+
this.apiUrl = `${baseUrl}/api/app/mobile`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
private async ensureIdentificationExists(
|
|
28
|
+
identificationId: string
|
|
29
|
+
): Promise<void> {
|
|
30
|
+
console.log(
|
|
31
|
+
'[DataUploadService] POST',
|
|
32
|
+
`${this.apiUrl}/identifications/${identificationId}`
|
|
33
|
+
);
|
|
34
|
+
await httpClient.post(
|
|
35
|
+
`${this.apiUrl}/identifications/${identificationId}`,
|
|
36
|
+
{}
|
|
37
|
+
);
|
|
38
|
+
console.log('[DataUploadService] ✓ Identification created/verified');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Submit document data to the backend (same approach as ResultScreen)
|
|
43
|
+
*/
|
|
44
|
+
async submitDocumentData(
|
|
45
|
+
identificationId: string,
|
|
46
|
+
scannedDocument: ScannedIdentityDocument,
|
|
47
|
+
sessionKey: string
|
|
48
|
+
): Promise<void> {
|
|
49
|
+
if (!scannedDocument || scannedDocument.documentType === 'UNKNOWN') {
|
|
50
|
+
console.log('[DataUploadService] No document data to submit');
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const mrzFields = scannedDocument.mrzFields;
|
|
55
|
+
if (!mrzFields) {
|
|
56
|
+
console.log('[DataUploadService] No MRZ fields to submit');
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const identificationDocument = {
|
|
61
|
+
type: mrzFields.documentCode,
|
|
62
|
+
name: mrzFields.firstName,
|
|
63
|
+
surname: mrzFields.lastName,
|
|
64
|
+
gender: this.getGenderEnumType(mrzFields.sex),
|
|
65
|
+
number: mrzFields.documentNumber,
|
|
66
|
+
country: mrzFields.issuingState,
|
|
67
|
+
barcodeValue: scannedDocument.barcodeValue,
|
|
68
|
+
personalNumber: mrzFields.personalNumber || mrzFields.optional1,
|
|
69
|
+
birthDate: mrzUtils.convertMRZDateToISODate(mrzFields.birthDate),
|
|
70
|
+
expiryDate: mrzUtils.convertMRZDateToISODate(mrzFields.expirationDate),
|
|
71
|
+
dataSource: scannedDocument.dataSource,
|
|
72
|
+
mrzText: scannedDocument.mrzText,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
console.log(
|
|
76
|
+
'[DataUploadService] Submitting document data:',
|
|
77
|
+
identificationDocument.type,
|
|
78
|
+
identificationDocument.number
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const { encryptedData, nonce } = encryptWithAes(
|
|
82
|
+
JSON.stringify(identificationDocument),
|
|
83
|
+
sessionKey
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
console.log(
|
|
87
|
+
'[DataUploadService] POST',
|
|
88
|
+
`${this.apiUrl}/identifications/${identificationId}/documents`
|
|
89
|
+
);
|
|
90
|
+
await runWithRetry(() =>
|
|
91
|
+
httpClient.post(
|
|
92
|
+
`${this.apiUrl}/identifications/${identificationId}/documents`,
|
|
93
|
+
{
|
|
94
|
+
encryptedData,
|
|
95
|
+
nonce,
|
|
96
|
+
}
|
|
97
|
+
)
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
console.log('[DataUploadService] ✓ Document data submitted');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Upload media files (document images, selfies, etc.)
|
|
105
|
+
*/
|
|
106
|
+
async uploadMedia(
|
|
107
|
+
identificationId: string,
|
|
108
|
+
scannedDocument?: ScannedIdentityDocument,
|
|
109
|
+
livenessDetection?: LivenessDetection,
|
|
110
|
+
onProgress?: (progress: number) => void
|
|
111
|
+
): Promise<void> {
|
|
112
|
+
const uploadFileOptions: RNFS.UploadFileOptions = {
|
|
113
|
+
toUrl: `${this.apiUrl}/identifications/${identificationId}/media`,
|
|
114
|
+
method: 'POST',
|
|
115
|
+
headers: {
|
|
116
|
+
Accept: 'application/json',
|
|
117
|
+
},
|
|
118
|
+
files: [],
|
|
119
|
+
progress: (res) => {
|
|
120
|
+
const progress = res.totalBytesSent / res.totalBytesExpectedToSend;
|
|
121
|
+
onProgress?.(progress);
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// Add document front image
|
|
126
|
+
const frontImage = scannedDocument?.frontImage;
|
|
127
|
+
if (frontImage && frontImage !== '') {
|
|
128
|
+
const filePath = `${RNFS.TemporaryDirectoryPath}/DOCUMENT_FRONT_IMAGE.jpg`;
|
|
129
|
+
await RNFS.writeFile(decodeURIComponent(filePath), frontImage, 'base64');
|
|
130
|
+
uploadFileOptions.files.push({
|
|
131
|
+
name: 'files',
|
|
132
|
+
filename: 'DOCUMENT_FRONT_IMAGE.jpg',
|
|
133
|
+
filepath: decodeURIComponent(filePath),
|
|
134
|
+
filetype: 'image/jpeg',
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Add document back image
|
|
139
|
+
const backImage = scannedDocument?.backImage;
|
|
140
|
+
if (backImage && backImage !== '') {
|
|
141
|
+
const filePath = `${RNFS.TemporaryDirectoryPath}/DOCUMENT_BACK_IMAGE.jpg`;
|
|
142
|
+
await RNFS.writeFile(decodeURIComponent(filePath), backImage, 'base64');
|
|
143
|
+
uploadFileOptions.files.push({
|
|
144
|
+
name: 'files',
|
|
145
|
+
filename: 'DOCUMENT_BACK_IMAGE.jpg',
|
|
146
|
+
filepath: decodeURIComponent(filePath),
|
|
147
|
+
filetype: 'image/jpeg',
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Add face image from document
|
|
152
|
+
const faceImage = scannedDocument?.faceImage;
|
|
153
|
+
if (faceImage && faceImage !== '') {
|
|
154
|
+
const filePath = `${RNFS.TemporaryDirectoryPath}/FACE_IMAGE.jpg`;
|
|
155
|
+
await RNFS.writeFile(decodeURIComponent(filePath), faceImage, 'base64');
|
|
156
|
+
uploadFileOptions.files.push({
|
|
157
|
+
name: 'files',
|
|
158
|
+
filename: 'FACE_IMAGE.jpg',
|
|
159
|
+
filepath: decodeURIComponent(filePath),
|
|
160
|
+
filetype: 'image/jpeg',
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Add secondary face image from document (optional)
|
|
165
|
+
const secondaryFaceImage = scannedDocument?.secondaryFaceImage;
|
|
166
|
+
if (secondaryFaceImage && secondaryFaceImage !== '') {
|
|
167
|
+
const filePath = `${RNFS.TemporaryDirectoryPath}/DOCUMENT_SECONDARY_FACE_IMAGE.jpg`;
|
|
168
|
+
await RNFS.writeFile(
|
|
169
|
+
decodeURIComponent(filePath),
|
|
170
|
+
secondaryFaceImage,
|
|
171
|
+
'base64'
|
|
172
|
+
);
|
|
173
|
+
uploadFileOptions.files.push({
|
|
174
|
+
name: 'files',
|
|
175
|
+
filename: 'DOCUMENT_SECONDARY_FACE_IMAGE.jpg',
|
|
176
|
+
filepath: decodeURIComponent(filePath),
|
|
177
|
+
filetype: 'image/jpeg',
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Add hologram image from document (optional)
|
|
182
|
+
const hologramImage = scannedDocument?.hologramImage;
|
|
183
|
+
if (hologramImage && hologramImage !== '') {
|
|
184
|
+
const filePath = `${RNFS.TemporaryDirectoryPath}/DOCUMENT_HOLOGRAM_IMAGE.jpg`;
|
|
185
|
+
await RNFS.writeFile(
|
|
186
|
+
decodeURIComponent(filePath),
|
|
187
|
+
hologramImage,
|
|
188
|
+
'base64'
|
|
189
|
+
);
|
|
190
|
+
uploadFileOptions.files.push({
|
|
191
|
+
name: 'files',
|
|
192
|
+
filename: 'DOCUMENT_HOLOGRAM_IMAGE.jpg',
|
|
193
|
+
filepath: decodeURIComponent(filePath),
|
|
194
|
+
filetype: 'image/jpeg',
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Add liveness images and selfie from liveness detection
|
|
199
|
+
if (livenessDetection?.instructions) {
|
|
200
|
+
for (const instruction of livenessDetection.instructions) {
|
|
201
|
+
if (instruction?.photo) {
|
|
202
|
+
const filePath = `${RNFS.TemporaryDirectoryPath}/LIVENESS_${instruction.instruction}_IMAGE.jpg`;
|
|
203
|
+
await RNFS.writeFile(
|
|
204
|
+
decodeURIComponent(filePath),
|
|
205
|
+
instruction.photo,
|
|
206
|
+
'base64'
|
|
207
|
+
);
|
|
208
|
+
uploadFileOptions.files.push({
|
|
209
|
+
name: 'files',
|
|
210
|
+
filename: `LIVENESS_${instruction.instruction}_IMAGE.jpg`,
|
|
211
|
+
filepath: decodeURIComponent(filePath),
|
|
212
|
+
filetype: 'image/jpeg',
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
if (instruction.instruction === 'LOOK_STRAIGHT_AND_BLINK') {
|
|
216
|
+
uploadFileOptions.files.push({
|
|
217
|
+
name: 'files',
|
|
218
|
+
filename: 'SELFIE_IMAGE.jpg',
|
|
219
|
+
filepath: decodeURIComponent(filePath),
|
|
220
|
+
filetype: 'image/jpeg',
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Add liveness video (optional)
|
|
228
|
+
if (livenessDetection?.videoPath) {
|
|
229
|
+
let videoFilePath: string;
|
|
230
|
+
if (Platform.OS === 'ios') {
|
|
231
|
+
const tempDir = `${RNFS.TemporaryDirectoryPath}/${new Date().getTime()}`;
|
|
232
|
+
await RNFS.mkdir(tempDir);
|
|
233
|
+
videoFilePath = `${tempDir}/LIVENESS_VIDEO.mp4`;
|
|
234
|
+
} else {
|
|
235
|
+
videoFilePath = `${RNFS.TemporaryDirectoryPath}/LIVENESS_VIDEO.mp4`;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
await RNFS.copyFile(livenessDetection.videoPath, videoFilePath);
|
|
239
|
+
|
|
240
|
+
uploadFileOptions.files.push({
|
|
241
|
+
name: 'files',
|
|
242
|
+
filename: 'LIVENESS_VIDEO.mp4',
|
|
243
|
+
filepath: decodeURIComponent(videoFilePath),
|
|
244
|
+
filetype: 'video/mp4',
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Skip upload if no files
|
|
249
|
+
if (uploadFileOptions.files.length === 0) {
|
|
250
|
+
console.log('[DataUploadService] No media files to upload');
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
console.log(
|
|
255
|
+
'[DataUploadService] Uploading',
|
|
256
|
+
uploadFileOptions.files.length,
|
|
257
|
+
'media files to',
|
|
258
|
+
uploadFileOptions.toUrl
|
|
259
|
+
);
|
|
260
|
+
const response = await runWithRetry(
|
|
261
|
+
() => RNFS.uploadFiles(uploadFileOptions).promise
|
|
262
|
+
);
|
|
263
|
+
console.log(
|
|
264
|
+
'[DataUploadService] Upload response status:',
|
|
265
|
+
response.statusCode
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
if (![200, 201, 204].includes(response.statusCode)) {
|
|
269
|
+
console.error(
|
|
270
|
+
'[DataUploadService] Media upload failed:',
|
|
271
|
+
response.statusCode,
|
|
272
|
+
response.body
|
|
273
|
+
);
|
|
274
|
+
throw new Error(`Media upload failed: ${response.statusCode}`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
console.log('[DataUploadService] ✓ Media uploaded successfully');
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Upload all collected data (document + media) before video call
|
|
282
|
+
*/
|
|
283
|
+
async uploadCollectedData(
|
|
284
|
+
identificationInfo: IdentificationInfo,
|
|
285
|
+
onProgress?: (progress: number) => void
|
|
286
|
+
): Promise<boolean> {
|
|
287
|
+
const { identificationId, sessionId, scannedDocument, livenessDetection } =
|
|
288
|
+
identificationInfo;
|
|
289
|
+
let { authToken } = identificationInfo;
|
|
290
|
+
|
|
291
|
+
if (!identificationId) {
|
|
292
|
+
console.log('[DataUploadService] No identification ID, skipping upload');
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
console.log(
|
|
297
|
+
'[DataUploadService] ========== UPLOADING COLLECTED DATA =========='
|
|
298
|
+
);
|
|
299
|
+
console.log('[DataUploadService] Identification ID:', identificationId);
|
|
300
|
+
console.log('[DataUploadService] Has document:', !!scannedDocument);
|
|
301
|
+
console.log('[DataUploadService] Has liveness:', !!livenessDetection);
|
|
302
|
+
console.log('[DataUploadService] Has auth token:', !!authToken);
|
|
303
|
+
|
|
304
|
+
try {
|
|
305
|
+
await runWithRetry(() =>
|
|
306
|
+
this.ensureIdentificationExists(identificationId)
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
// Always refresh session key for current session (required for encrypted submission)
|
|
310
|
+
if (sessionId) {
|
|
311
|
+
const existingAuthToken = authToken;
|
|
312
|
+
console.log('[DataUploadService] Getting session key...');
|
|
313
|
+
try {
|
|
314
|
+
authToken = await runWithRetry(() =>
|
|
315
|
+
getSessionKey(this.apiUrl, sessionId)
|
|
316
|
+
);
|
|
317
|
+
console.log('[DataUploadService] ✓ Session key obtained');
|
|
318
|
+
} catch (error) {
|
|
319
|
+
if (existingAuthToken) {
|
|
320
|
+
console.warn(
|
|
321
|
+
'[DataUploadService] Session key refresh failed, using existing token'
|
|
322
|
+
);
|
|
323
|
+
authToken = existingAuthToken;
|
|
324
|
+
} else {
|
|
325
|
+
throw error;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (!authToken) {
|
|
331
|
+
console.log(
|
|
332
|
+
'[DataUploadService] No session key available, skipping upload'
|
|
333
|
+
);
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Step 1: Submit document data (using same encryption as ResultScreen)
|
|
338
|
+
if (scannedDocument) {
|
|
339
|
+
onProgress?.(0.1);
|
|
340
|
+
await runWithRetry(() =>
|
|
341
|
+
this.submitDocumentData(identificationId, scannedDocument, authToken)
|
|
342
|
+
);
|
|
343
|
+
onProgress?.(0.3);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Step 2: Upload media files (images only, skip video for now - too slow)
|
|
347
|
+
onProgress?.(0.4);
|
|
348
|
+
await runWithRetry(() =>
|
|
349
|
+
this.uploadMedia(
|
|
350
|
+
identificationId,
|
|
351
|
+
scannedDocument,
|
|
352
|
+
livenessDetection,
|
|
353
|
+
(p) => {
|
|
354
|
+
onProgress?.(0.4 + p * 0.5);
|
|
355
|
+
}
|
|
356
|
+
)
|
|
357
|
+
);
|
|
358
|
+
onProgress?.(1.0);
|
|
359
|
+
|
|
360
|
+
console.log('[DataUploadService] ✓ All collected data uploaded');
|
|
361
|
+
|
|
362
|
+
// Mark media as uploaded during video call
|
|
363
|
+
identificationInfo.mediaUploadedDuringVideoCall = true;
|
|
364
|
+
|
|
365
|
+
// Store the auth token back for future use
|
|
366
|
+
identificationInfo.authToken = authToken;
|
|
367
|
+
return true;
|
|
368
|
+
} catch (error) {
|
|
369
|
+
if (error instanceof NotFoundError) {
|
|
370
|
+
console.warn(
|
|
371
|
+
'[DataUploadService] Upload skipped: identification is not active anymore'
|
|
372
|
+
);
|
|
373
|
+
return false;
|
|
374
|
+
}
|
|
375
|
+
console.error(
|
|
376
|
+
'[DataUploadService] Failed to upload collected data:',
|
|
377
|
+
error
|
|
378
|
+
);
|
|
379
|
+
return false;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
private getGenderEnumType(sex: string | undefined): string {
|
|
384
|
+
switch (sex?.toLowerCase()) {
|
|
385
|
+
case 'male':
|
|
386
|
+
case 'm':
|
|
387
|
+
return 'M';
|
|
388
|
+
case 'female':
|
|
389
|
+
case 'f':
|
|
390
|
+
return 'F';
|
|
391
|
+
default:
|
|
392
|
+
return 'X';
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
export interface VideoSessionInfo {
|
|
2
|
+
sessionId: string;
|
|
3
|
+
identificationId: string;
|
|
4
|
+
callState: 'WAITING' | 'CONNECTING' | 'ACTIVE' | 'COMPLETED' | 'FAILED';
|
|
5
|
+
queuePosition?: number;
|
|
6
|
+
connectedAt?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class VideoSessionService {
|
|
10
|
+
private baseUrl: string;
|
|
11
|
+
private identificationId?: string;
|
|
12
|
+
|
|
13
|
+
constructor(baseUrl: string, identificationId?: string) {
|
|
14
|
+
this.baseUrl = baseUrl;
|
|
15
|
+
this.identificationId = identificationId;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Get current active video session for customer
|
|
20
|
+
*/
|
|
21
|
+
public async getCurrentSession(): Promise<VideoSessionInfo> {
|
|
22
|
+
const params = new URLSearchParams();
|
|
23
|
+
if (this.identificationId) {
|
|
24
|
+
params.append('identificationId', this.identificationId);
|
|
25
|
+
}
|
|
26
|
+
const url = `${this.baseUrl}/api/app/mobile/video-sessions/current${params.toString() ? '?' + params.toString() : ''}`;
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const response = await fetch(url, {
|
|
30
|
+
method: 'GET',
|
|
31
|
+
headers: {
|
|
32
|
+
'Content-Type': 'application/json',
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
if (!response.ok) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
`Failed to get current session: ${response.statusText}`
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return await response.json();
|
|
43
|
+
} catch (error) {
|
|
44
|
+
console.error(
|
|
45
|
+
'[VideoSessionService] Error getting current session:',
|
|
46
|
+
error
|
|
47
|
+
);
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Join a video session
|
|
54
|
+
*/
|
|
55
|
+
public async joinSession(sessionId: string): Promise<VideoSessionInfo> {
|
|
56
|
+
const params = new URLSearchParams();
|
|
57
|
+
if (this.identificationId) {
|
|
58
|
+
params.append('identificationId', this.identificationId);
|
|
59
|
+
}
|
|
60
|
+
const url = `${this.baseUrl}/api/app/mobile/video-sessions/${sessionId}/join${params.toString() ? '?' + params.toString() : ''}`;
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const response = await fetch(url, {
|
|
64
|
+
method: 'POST',
|
|
65
|
+
headers: {
|
|
66
|
+
'Content-Type': 'application/json',
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
if (!response.ok) {
|
|
71
|
+
throw new Error(`Failed to join session: ${response.statusText}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return await response.json();
|
|
75
|
+
} catch (error) {
|
|
76
|
+
console.error('[VideoSessionService] Error joining session:', error);
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Send a heartbeat to keep the queue-status SSE watchdog alive
|
|
83
|
+
*/
|
|
84
|
+
public async sendHeartbeat(sessionId: string): Promise<void> {
|
|
85
|
+
const params = new URLSearchParams();
|
|
86
|
+
if (this.identificationId) {
|
|
87
|
+
params.append('identificationId', this.identificationId);
|
|
88
|
+
}
|
|
89
|
+
const url = `${this.baseUrl}/api/app/mobile/video-sessions/${sessionId}/heartbeat${params.toString() ? '?' + params.toString() : ''}`;
|
|
90
|
+
try {
|
|
91
|
+
await fetch(url, { method: 'POST' });
|
|
92
|
+
} catch {
|
|
93
|
+
// best-effort, ignore errors
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Leave the session queue (drops customer from WAITING state)
|
|
99
|
+
*/
|
|
100
|
+
public async leaveSession(sessionId: string): Promise<void> {
|
|
101
|
+
const params = new URLSearchParams();
|
|
102
|
+
if (this.identificationId) {
|
|
103
|
+
params.append('identificationId', this.identificationId);
|
|
104
|
+
}
|
|
105
|
+
const url = `${this.baseUrl}/api/app/mobile/video-sessions/${sessionId}/join${params.toString() ? '?' + params.toString() : ''}`;
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
await fetch(url, { method: 'DELETE' });
|
|
109
|
+
} catch (error) {
|
|
110
|
+
console.error('[VideoSessionService] Error leaving session:', error);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Subscribe to queue status updates via Server-Sent Events (SSE)
|
|
116
|
+
*/
|
|
117
|
+
public subscribeToQueueUpdates(
|
|
118
|
+
sessionId: string,
|
|
119
|
+
onUpdate: (position: number) => void,
|
|
120
|
+
onError: (error: Error) => void
|
|
121
|
+
): () => void {
|
|
122
|
+
try {
|
|
123
|
+
// Use react-native-sse for real SSE support
|
|
124
|
+
const EventSource = require('react-native-sse').default;
|
|
125
|
+
|
|
126
|
+
const url = `${this.baseUrl}/api/app/mobile/video-sessions/${sessionId}/queue-status?identificationId=${this.identificationId}`;
|
|
127
|
+
console.log('[VideoSessionService] Creating SSE connection to:', url);
|
|
128
|
+
|
|
129
|
+
const es = new EventSource(url);
|
|
130
|
+
let heartbeatInterval: NodeJS.Timeout | null = null;
|
|
131
|
+
|
|
132
|
+
es.addEventListener('open', () => {
|
|
133
|
+
console.log('[VideoSessionService] Queue SSE connected');
|
|
134
|
+
// Send initial heartbeat
|
|
135
|
+
this.sendHeartbeat(sessionId).catch(() => {});
|
|
136
|
+
// Set up periodic heartbeat (every 20s to stay under 30s watchdog)
|
|
137
|
+
heartbeatInterval = setInterval(() => {
|
|
138
|
+
this.sendHeartbeat(sessionId).catch(() => {});
|
|
139
|
+
}, 20000);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
es.addEventListener('ping', () => {
|
|
143
|
+
// Server sent a ping, respond with heartbeat
|
|
144
|
+
this.sendHeartbeat(sessionId).catch(() => {});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
es.addEventListener('message', (event: any) => {
|
|
148
|
+
try {
|
|
149
|
+
const data = JSON.parse(event.data);
|
|
150
|
+
console.log('[VideoSessionService] Queue update:', data);
|
|
151
|
+
if (data.queuePosition !== undefined) {
|
|
152
|
+
onUpdate(data.queuePosition);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Close connection if no longer waiting
|
|
156
|
+
if (data.callState !== 'WAITING') {
|
|
157
|
+
if (heartbeatInterval) clearInterval(heartbeatInterval);
|
|
158
|
+
es.close();
|
|
159
|
+
}
|
|
160
|
+
} catch (error) {
|
|
161
|
+
console.error('[SSE] Failed to parse queue update:', error);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
es.addEventListener('error', (event: any) => {
|
|
166
|
+
console.error('[SSE] Connection error:', event);
|
|
167
|
+
if (heartbeatInterval) clearInterval(heartbeatInterval);
|
|
168
|
+
onError(new Error('SSE connection failed'));
|
|
169
|
+
es.close();
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// Return cleanup function
|
|
173
|
+
return () => {
|
|
174
|
+
if (heartbeatInterval) clearInterval(heartbeatInterval);
|
|
175
|
+
es.close();
|
|
176
|
+
};
|
|
177
|
+
} catch (error) {
|
|
178
|
+
console.error(
|
|
179
|
+
'[VideoSessionService] Failed to create SSE connection:',
|
|
180
|
+
error
|
|
181
|
+
);
|
|
182
|
+
onError(
|
|
183
|
+
error instanceof Error
|
|
184
|
+
? error
|
|
185
|
+
: new Error('Failed to create SSE connection')
|
|
186
|
+
);
|
|
187
|
+
return () => {};
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|