@sanctum-key/react-native-sdk 1.0.33 → 1.0.34
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/build/package.json +1 -1
- package/build/src/components/EnhancedCameraView.d.ts.map +1 -1
- package/build/src/components/EnhancedCameraView.js +25 -18
- package/build/src/components/EnhancedCameraView.js.map +1 -1
- package/build/src/components/KYCElements/IDCardCapture.d.ts.map +1 -1
- package/build/src/components/KYCElements/IDCardCapture.js +82 -33
- package/build/src/components/KYCElements/IDCardCapture.js.map +1 -1
- package/build/src/modules/api/KYCService.d.ts +0 -7
- package/build/src/modules/api/KYCService.d.ts.map +1 -1
- package/build/src/modules/api/KYCService.js +48 -119
- package/build/src/modules/api/KYCService.js.map +1 -1
- package/package.json +1 -1
- package/src/components/EnhancedCameraView.tsx +29 -22
- package/src/components/KYCElements/IDCardCapture.tsx +113 -53
- package/src/modules/api/KYCService.ts +357 -534
|
@@ -23,19 +23,17 @@ const documentTypeMap: Record<string, string> = {
|
|
|
23
23
|
healthInsuranceCard: "health_insurance_card",
|
|
24
24
|
residencePermit: "permanent_residence_card",
|
|
25
25
|
identityCard: "identity_card",
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
driving_license: "driving_licence", // American spelling 'license'
|
|
33
|
-
permanent_residence: "permanent_residence_card", // Missing the word 'card'
|
|
26
|
+
nationalId: "identity_card",
|
|
27
|
+
national_id: "identity_card",
|
|
28
|
+
drivers_licence: "driving_licence",
|
|
29
|
+
drivers_license: "driving_licence",
|
|
30
|
+
driving_license: "driving_licence",
|
|
31
|
+
permanent_residence: "permanent_residence_card",
|
|
34
32
|
};
|
|
35
33
|
|
|
36
34
|
let cachedToken: string | null = null;
|
|
37
35
|
let tokenExpiryTime: number | null = null;
|
|
38
|
-
let activeAuthRequest: Promise<string> | null = null;
|
|
36
|
+
let activeAuthRequest: Promise<string> | null = null;
|
|
39
37
|
|
|
40
38
|
export interface KYCRequest {
|
|
41
39
|
userId: string;
|
|
@@ -71,11 +69,10 @@ export interface SelfieVideoResponse {
|
|
|
71
69
|
error: string;
|
|
72
70
|
}
|
|
73
71
|
|
|
74
|
-
|
|
75
72
|
export class KYCService {
|
|
76
73
|
private baseURL: string;
|
|
77
74
|
private apiKey: string;
|
|
78
|
-
|
|
75
|
+
|
|
79
76
|
private faceServiceURL = 'https://face-infera.sanctumkey.com';
|
|
80
77
|
private textExtractionServiceURL = 'https://text-infera.sanctumkey.com';
|
|
81
78
|
private mrzServiceURL = 'https://mrz-infera.sanctumkey.com';
|
|
@@ -150,7 +147,6 @@ export class KYCService {
|
|
|
150
147
|
}
|
|
151
148
|
|
|
152
149
|
async processSelfieOrientationPicture(videoFile: string, token: string): Promise<SelfieVideoResponse[]> {
|
|
153
|
-
|
|
154
150
|
try {
|
|
155
151
|
const formData = new FormData();
|
|
156
152
|
await appendFileToFormData(formData, 'file', videoFile, 'selfie_picture.jpg', 'image/jpeg');
|
|
@@ -160,7 +156,10 @@ export class KYCService {
|
|
|
160
156
|
formData,
|
|
161
157
|
{
|
|
162
158
|
timeout: 20000,
|
|
163
|
-
headers: {
|
|
159
|
+
headers: {
|
|
160
|
+
// 🚨 Removed explicit Content-Type to allow boundary injection
|
|
161
|
+
'Authorization': `Bearer ${token}`,
|
|
162
|
+
},
|
|
164
163
|
}
|
|
165
164
|
);
|
|
166
165
|
return response.data;
|
|
@@ -172,7 +171,6 @@ export class KYCService {
|
|
|
172
171
|
|
|
173
172
|
async processOrientationVideo(videoFile: string, env: KycEnvironment = 'PRODUCTION'): Promise<OrientationVideoResponse> {
|
|
174
173
|
try {
|
|
175
|
-
// SANDBOX mode: skip AI verification and return mock response
|
|
176
174
|
if (env === 'SANDBOX') {
|
|
177
175
|
console.log("SANDBOX mode: Skipping AI orientation video processing");
|
|
178
176
|
logger.log("SANDBOX mode: Returning mock orientation video response");
|
|
@@ -187,11 +185,9 @@ export class KYCService {
|
|
|
187
185
|
};
|
|
188
186
|
}
|
|
189
187
|
|
|
190
|
-
// Create FormData for multipart/form-data request
|
|
191
188
|
const formData = new FormData();
|
|
192
189
|
await appendFileToFormData(formData, 'file', videoFile, 'orientation_video.mp4', 'video/mp4');
|
|
193
190
|
|
|
194
|
-
|
|
195
191
|
const response = await axios.post(
|
|
196
192
|
`${this.orientationServiceURL}/process_orientation_video_stream/`,
|
|
197
193
|
formData,
|
|
@@ -208,8 +204,6 @@ export class KYCService {
|
|
|
208
204
|
};
|
|
209
205
|
} catch (error: any) {
|
|
210
206
|
logger.error('Error processing orientation video:', JSON.stringify(error, null, 2));
|
|
211
|
-
|
|
212
|
-
// Handle specific error cases
|
|
213
207
|
if (error.response?.status === 503) {
|
|
214
208
|
return {
|
|
215
209
|
success: false,
|
|
@@ -221,7 +215,6 @@ export class KYCService {
|
|
|
221
215
|
message: 'Service temporarily unavailable'
|
|
222
216
|
};
|
|
223
217
|
}
|
|
224
|
-
|
|
225
218
|
return {
|
|
226
219
|
success: false,
|
|
227
220
|
error: {
|
|
@@ -236,13 +229,11 @@ export class KYCService {
|
|
|
236
229
|
|
|
237
230
|
// STEP 1 - ID CARD VALIDATION
|
|
238
231
|
async detectFaceOnId(
|
|
239
|
-
idCardImageUri: string,
|
|
240
|
-
token: string,
|
|
241
|
-
docType: string,
|
|
232
|
+
idCardImageUri: string,
|
|
233
|
+
token: string,
|
|
234
|
+
docType: string,
|
|
242
235
|
env: KycEnvironment = 'PRODUCTION'
|
|
243
|
-
): Promise<{ result: boolean; detail: any[]; card_obb?: any }> {
|
|
244
|
-
|
|
245
|
-
// SANDBOX mode
|
|
236
|
+
): Promise<{ result: boolean; detail: any[]; card_obb?: any }> {
|
|
246
237
|
if (env === 'SANDBOX') {
|
|
247
238
|
logger.log("SANDBOX mode: Returning mock face detection response");
|
|
248
239
|
return {
|
|
@@ -251,39 +242,33 @@ export class KYCService {
|
|
|
251
242
|
card_obb: { x: 50, y: 50, width: 200, height: 200 }
|
|
252
243
|
};
|
|
253
244
|
}
|
|
254
|
-
|
|
245
|
+
|
|
255
246
|
const formData = new FormData();
|
|
256
247
|
await appendFileToFormData(formData, 'file', idCardImageUri, 'id_card_photo.jpg', 'image/jpeg');
|
|
257
|
-
|
|
258
|
-
const docTypeShorted = GovernmentDocumentTypeShorted[docType as GovernmentDocumentType] || docType;
|
|
259
248
|
|
|
260
|
-
|
|
249
|
+
const docTypeShorted = GovernmentDocumentTypeShorted[docType as GovernmentDocumentType] || docType;
|
|
261
250
|
logger.log('detectFaceOnId Request:', { docTypeShorted, imageUri: idCardImageUri });
|
|
262
|
-
|
|
251
|
+
|
|
263
252
|
try {
|
|
264
253
|
const res = await axios.post(
|
|
265
|
-
`${this.faceServiceURL}/detect_face/?doc_type=${docTypeShorted}`,
|
|
254
|
+
`${this.faceServiceURL}/detect_face/?doc_type=${docTypeShorted}`,
|
|
266
255
|
formData,
|
|
267
256
|
{
|
|
268
|
-
headers: {
|
|
269
|
-
|
|
270
|
-
'Authorization': `Bearer ${token}`
|
|
257
|
+
headers: {
|
|
258
|
+
// 🚨 Removed explicit Content-Type to allow boundary injection
|
|
259
|
+
'Authorization': `Bearer ${token}`
|
|
271
260
|
},
|
|
272
|
-
timeout: 20000
|
|
261
|
+
timeout: 20000
|
|
273
262
|
}
|
|
274
263
|
);
|
|
275
264
|
|
|
276
|
-
// It's safe to stringify res.data if the backend returns a clean JSON object
|
|
277
265
|
logger.log('detectFaceOnId res', JSON.stringify(res.data, null, 2));
|
|
278
|
-
|
|
266
|
+
|
|
279
267
|
if (res.data?.result) {
|
|
280
268
|
return res.data;
|
|
281
269
|
}
|
|
282
|
-
|
|
283
270
|
throw new Error(res.data?.detail || 'detect_face failed');
|
|
284
|
-
|
|
285
271
|
} catch (error: any) {
|
|
286
|
-
// Use your safe error extractor, avoid JSON.stringify on raw Axios errors
|
|
287
272
|
const safeError = errorMessage(error) || error.message;
|
|
288
273
|
logger.error('Error detecting face on id:', safeError);
|
|
289
274
|
throw error;
|
|
@@ -292,18 +277,16 @@ export class KYCService {
|
|
|
292
277
|
|
|
293
278
|
//check templatetemplate_type
|
|
294
279
|
async checkTemplateType(
|
|
295
|
-
params: {
|
|
296
|
-
fileUri: string;
|
|
297
|
-
docType: string;
|
|
298
|
-
docRegion: string;
|
|
299
|
-
token: string;
|
|
280
|
+
params: {
|
|
281
|
+
fileUri: string;
|
|
282
|
+
docType: string;
|
|
283
|
+
docRegion: string;
|
|
284
|
+
token: string;
|
|
300
285
|
postfix: string;
|
|
301
|
-
issuingSubdivision?: string;
|
|
302
|
-
},
|
|
286
|
+
issuingSubdivision?: string;
|
|
287
|
+
},
|
|
303
288
|
env: KycEnvironment = 'PRODUCTION'
|
|
304
289
|
): Promise<any> {
|
|
305
|
-
|
|
306
|
-
// SANDBOX mode
|
|
307
290
|
if (env === 'SANDBOX') {
|
|
308
291
|
console.log("SANDBOX mode: Skipping AI template type check");
|
|
309
292
|
logger.log("SANDBOX mode: Returning mock template type response");
|
|
@@ -315,26 +298,28 @@ export class KYCService {
|
|
|
315
298
|
}
|
|
316
299
|
|
|
317
300
|
const { fileUri, docType, docRegion, token, postfix, issuingSubdivision } = params;
|
|
301
|
+
|
|
318
302
|
const formData = new FormData();
|
|
319
|
-
|
|
320
303
|
await appendFileToFormData(formData, 'file', fileUri, `id_card_${postfix}.jpg`, 'image/jpeg');
|
|
321
304
|
|
|
322
305
|
const docTypeShorted = GovernmentDocumentTypeShorted[docType as GovernmentDocumentType];
|
|
323
306
|
|
|
324
|
-
// Build the base URL
|
|
325
307
|
let url = `${this.mrzServiceURL}/get_template_version/?doc_type=${encodeURIComponent(docTypeShorted)}&doc_region=${encodeURIComponent(docRegion)}&postfix=${postfix}`;
|
|
326
|
-
|
|
327
308
|
if (issuingSubdivision) {
|
|
328
309
|
url += `&issuing_subdivision=${encodeURIComponent(issuingSubdivision)}`;
|
|
329
310
|
}
|
|
330
311
|
|
|
331
312
|
logger.log('checkTemplateType params', this.mrzServiceURL, JSON.stringify({ fileUri, docTypeShorted, docRegion, issuingSubdivision, token, postfix, url }, null, 2));
|
|
332
|
-
|
|
313
|
+
|
|
333
314
|
try {
|
|
334
315
|
const res = await axios.post<CheckTemplateTypeResponse>(url, formData, {
|
|
335
|
-
headers: {
|
|
316
|
+
headers: {
|
|
317
|
+
// 🚨 Removed explicit Content-Type to allow boundary injection
|
|
318
|
+
'Authorization': `Bearer ${token}`,
|
|
319
|
+
},
|
|
336
320
|
timeout: 60000,
|
|
337
321
|
});
|
|
322
|
+
|
|
338
323
|
logger.log('checkTemplateType res', JSON.stringify(res.data, null, 2));
|
|
339
324
|
return res.data;
|
|
340
325
|
} catch (e: any) {
|
|
@@ -343,69 +328,62 @@ export class KYCService {
|
|
|
343
328
|
}
|
|
344
329
|
}
|
|
345
330
|
|
|
346
|
-
async extractDocumentInformation(params: {
|
|
347
|
-
fileUri: string;
|
|
348
|
-
docType: string;
|
|
349
|
-
docRegion: string;
|
|
350
|
-
templatePath: string;
|
|
331
|
+
async extractDocumentInformation(params: {
|
|
332
|
+
fileUri: string;
|
|
333
|
+
docType: string;
|
|
334
|
+
docRegion: string;
|
|
335
|
+
templatePath: string;
|
|
351
336
|
issuingSubdivision?: string;
|
|
352
|
-
token: string;
|
|
353
|
-
postfix?: string
|
|
337
|
+
token: string;
|
|
338
|
+
postfix?: string
|
|
354
339
|
}): Promise<any> {
|
|
355
340
|
const { fileUri, docType, docRegion, templatePath, issuingSubdivision, token, postfix = 'front' } = params;
|
|
356
|
-
|
|
357
|
-
// 1. Prepare Form Data
|
|
341
|
+
|
|
358
342
|
const formData = new FormData();
|
|
359
343
|
await appendFileToFormData(formData, 'file', fileUri, `id_card_${postfix}.jpg`, 'image/jpeg');
|
|
360
344
|
|
|
361
345
|
let queryParams = `doc_type=${encodeURIComponent(docType)}&doc_region=${encodeURIComponent(docRegion)}&template_path=${encodeURIComponent(templatePath)}&postfix=${postfix}&cleaned=true`;
|
|
362
|
-
|
|
363
346
|
if (issuingSubdivision) {
|
|
364
347
|
queryParams += `&issuing_subdivision=${encodeURIComponent(issuingSubdivision)}`;
|
|
365
348
|
}
|
|
349
|
+
|
|
366
350
|
logger.log(`URL PARAMS: ${queryParams}`)
|
|
367
|
-
|
|
368
351
|
const url = `${this.textExtractionServiceURL}/extract_doc_information/?${queryParams}`;
|
|
369
352
|
|
|
370
353
|
try {
|
|
371
354
|
const startRes = await axios.post(url, formData, {
|
|
372
|
-
headers: {
|
|
355
|
+
headers: {
|
|
356
|
+
// 🚨 Removed explicit Content-Type to allow boundary injection
|
|
357
|
+
'Authorization': `Bearer ${token}`
|
|
358
|
+
},
|
|
373
359
|
timeout: 60000,
|
|
374
360
|
});
|
|
375
|
-
|
|
361
|
+
|
|
376
362
|
const jobId = startRes.data?.job_id;
|
|
377
363
|
if (!jobId) throw new Error("Backend did not return a job_id");
|
|
378
|
-
|
|
379
364
|
logger.log(`Extraction job started: ${jobId}. Polling for results...`);
|
|
380
365
|
|
|
381
|
-
// 4. Poll the Status Endpoint
|
|
382
366
|
const statusUrl = `${this.textExtractionServiceURL}/extract_doc_information/status/${jobId}`;
|
|
383
|
-
|
|
384
|
-
// Poll every 2 seconds, up to 15 times (30 seconds max)
|
|
385
367
|
const maxRetries = 15;
|
|
368
|
+
|
|
386
369
|
for (let i = 0; i < maxRetries; i++) {
|
|
387
|
-
// Wait 2 seconds before checking
|
|
388
370
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
389
|
-
|
|
390
371
|
const statusRes = await axios.get(statusUrl, {
|
|
391
|
-
|
|
372
|
+
headers: {
|
|
373
|
+
'Authorization': `Bearer ${token}`
|
|
374
|
+
}
|
|
392
375
|
});
|
|
393
|
-
|
|
394
376
|
const statusData = statusRes.data;
|
|
395
377
|
|
|
396
378
|
if (statusData.status === 'completed') {
|
|
397
379
|
logger.log('Extraction completed successfully', JSON.stringify(truncateFields(statusData.result)));
|
|
398
|
-
return statusData.result;
|
|
380
|
+
return statusData.result;
|
|
399
381
|
}
|
|
400
|
-
|
|
401
382
|
if (statusData.status === 'failed') {
|
|
402
383
|
throw new Error(statusData.error || "Document extraction failed during processing.");
|
|
403
384
|
}
|
|
404
|
-
|
|
405
385
|
}
|
|
406
|
-
|
|
407
386
|
throw new Error("Document extraction timed out waiting for results.");
|
|
408
|
-
|
|
409
387
|
} catch (e: any) {
|
|
410
388
|
const errorMsg = e.response?.data?.detail || e.message;
|
|
411
389
|
logger.error('Error extracting document information:', errorMsg);
|
|
@@ -414,20 +392,16 @@ export class KYCService {
|
|
|
414
392
|
}
|
|
415
393
|
|
|
416
394
|
async extractBarcode(params: { fileUri: string; token: string; docType: string; postfix?: string }, env: KycEnvironment = 'PRODUCTION'): Promise<any> {
|
|
417
|
-
// SANDBOX mode
|
|
418
395
|
if (env === 'SANDBOX') {
|
|
419
396
|
console.log("SANDBOX mode: Skipping AI barcode extraction");
|
|
420
397
|
logger.log("SANDBOX mode: Returning mock barcode response");
|
|
421
|
-
return {
|
|
422
|
-
barcode_data: 'SANDBOX_MOCK_BARCODE_DATA',
|
|
423
|
-
card_obb: { x: 50, y: 50, width: 200, height: 200 }
|
|
424
|
-
};
|
|
398
|
+
return { barcode_data: 'SANDBOX_MOCK_BARCODE_DATA', card_obb: { x: 50, y: 50, width: 200, height: 200 } };
|
|
425
399
|
}
|
|
426
400
|
|
|
427
401
|
const { fileUri, token, docType, postfix = 'back' } = params;
|
|
428
402
|
logger.log(docType)
|
|
429
|
-
const formData = new FormData();
|
|
430
403
|
|
|
404
|
+
const formData = new FormData();
|
|
431
405
|
await appendFileToFormData(formData, 'file', fileUri, `id_${postfix}_photo.jpg`, 'image/jpeg');
|
|
432
406
|
|
|
433
407
|
const url = `${this.barcodeServiceURL}/decode_barcode/?doc_type=${encodeURIComponent(docType)}`;
|
|
@@ -435,13 +409,13 @@ export class KYCService {
|
|
|
435
409
|
const attempt = async () => {
|
|
436
410
|
try {
|
|
437
411
|
const res = await axios.post(url, formData, {
|
|
438
|
-
headers: {
|
|
412
|
+
headers: {
|
|
413
|
+
// 🚨 Removed explicit Content-Type to allow boundary injection
|
|
414
|
+
'Authorization': `Bearer ${token}`
|
|
415
|
+
},
|
|
439
416
|
timeout: 60000,
|
|
440
417
|
});
|
|
441
|
-
|
|
442
418
|
logger.log('📦 Raw extractBarcode response:', JSON.stringify(res.data, null, 2));
|
|
443
|
-
|
|
444
|
-
|
|
445
419
|
return res.data;
|
|
446
420
|
} catch (e: any) {
|
|
447
421
|
throw new Error(e?.message || 'Erreur de détection du code-barres');
|
|
@@ -457,20 +431,15 @@ export class KYCService {
|
|
|
457
431
|
}
|
|
458
432
|
|
|
459
433
|
async formatBarcodeData(params: { textInput: string; token: string }, env: KycEnvironment = 'PRODUCTION'): Promise<any> {
|
|
460
|
-
// SANDBOX mode
|
|
461
434
|
if (env === 'SANDBOX') {
|
|
462
435
|
console.log("SANDBOX mode: Skipping AI barcode formatting");
|
|
463
436
|
logger.log("SANDBOX mode: Returning mock formatted barcode response");
|
|
464
|
-
return {
|
|
465
|
-
barcode_type: "SANDBOX_MOCK",
|
|
466
|
-
first_name: "JOHN",
|
|
467
|
-
last_name: "DOE",
|
|
468
|
-
card_number: "000000000000"
|
|
469
|
-
};
|
|
437
|
+
return { barcode_type: "SANDBOX_MOCK", first_name: "JOHN", last_name: "DOE", card_number: "000000000000" };
|
|
470
438
|
}
|
|
471
439
|
|
|
472
440
|
const { textInput, token } = params;
|
|
473
441
|
|
|
442
|
+
// URLSearchParams requires urlencoded, this remains untouched
|
|
474
443
|
const formData = new URLSearchParams();
|
|
475
444
|
formData.append('text_input', textInput);
|
|
476
445
|
|
|
@@ -479,15 +448,13 @@ export class KYCService {
|
|
|
479
448
|
const attempt = async () => {
|
|
480
449
|
try {
|
|
481
450
|
const res = await axios.post(url, formData.toString(), {
|
|
482
|
-
headers: {
|
|
483
|
-
'Content-Type': 'application/x-www-form-urlencoded',
|
|
484
|
-
'Authorization': `Bearer ${token}`
|
|
451
|
+
headers: {
|
|
452
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
453
|
+
'Authorization': `Bearer ${token}`
|
|
485
454
|
},
|
|
486
455
|
timeout: 60000,
|
|
487
456
|
});
|
|
488
|
-
|
|
489
457
|
logger.log('📦 Raw formatBarcodeData response:', JSON.stringify(res.data, null, 2));
|
|
490
|
-
|
|
491
458
|
return res.data;
|
|
492
459
|
} catch (e: any) {
|
|
493
460
|
throw new Error(e?.message || 'Erreur de formatage du code-barres');
|
|
@@ -502,7 +469,6 @@ export class KYCService {
|
|
|
502
469
|
}
|
|
503
470
|
}
|
|
504
471
|
|
|
505
|
-
|
|
506
472
|
async extractMrzText(
|
|
507
473
|
params: {
|
|
508
474
|
fileUri: string;
|
|
@@ -514,125 +480,107 @@ export class KYCService {
|
|
|
514
480
|
mrz_type?: string;
|
|
515
481
|
},
|
|
516
482
|
env: KycEnvironment = 'PRODUCTION'
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
if (data?.success === false) {
|
|
612
|
-
const serverMessage = data.parsed_data?.status_message || data.parsed_data?.status || 'Échec de l\'extraction MRZ';
|
|
613
|
-
throw new Error(`Lecture MRZ refusée: ${serverMessage}`);
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
return data;
|
|
617
|
-
|
|
618
|
-
} catch (error: any) {
|
|
619
|
-
logger.error("MRZ Fetch Error: ", error?.message || error);
|
|
620
|
-
throw new Error(error?.message || 'Erreur de connexion lors de l\'extraction MRZ');
|
|
621
|
-
}
|
|
622
|
-
}
|
|
483
|
+
): Promise<any> {
|
|
484
|
+
if (env === 'SANDBOX') {
|
|
485
|
+
console.log("SANDBOX mode: Skipping AI MRZ extraction");
|
|
486
|
+
const { docType, docRegion, postfix = 'back', mrz_type } = params;
|
|
487
|
+
return {
|
|
488
|
+
success: true,
|
|
489
|
+
parsed_data: {
|
|
490
|
+
status: 'success',
|
|
491
|
+
document_type: docType,
|
|
492
|
+
mrz_type: mrz_type || 'TD1',
|
|
493
|
+
doc_region: docRegion,
|
|
494
|
+
postfix: postfix
|
|
495
|
+
},
|
|
496
|
+
card_obb: { x: 50, y: 50, width: 200, height: 200 }
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const { fileUri, docType, docRegion, postfix = 'back', token, template_path, mrz_type } = params;
|
|
501
|
+
|
|
502
|
+
const docTypeShorted = GovernmentDocumentTypeShorted[docType as GovernmentDocumentType] || docType;
|
|
503
|
+
const safeMrzType = mrz_type && mrz_type.trim() !== '' ? mrz_type : 'TD1';
|
|
504
|
+
|
|
505
|
+
logger.log("docTypeShorted", docTypeShorted, docRegion, postfix);
|
|
506
|
+
|
|
507
|
+
const queryParams = new URLSearchParams({
|
|
508
|
+
doc_type: docTypeShorted,
|
|
509
|
+
doc_region: docRegion,
|
|
510
|
+
postfix: postfix,
|
|
511
|
+
template_path: template_path,
|
|
512
|
+
mrz_type: safeMrzType,
|
|
513
|
+
reset_cache: 'true'
|
|
514
|
+
}).toString();
|
|
515
|
+
|
|
516
|
+
const url = `${this.mrzServiceURL}/extract_mrz_text/?${queryParams}`;
|
|
517
|
+
logger.log("url", url);
|
|
518
|
+
|
|
519
|
+
try {
|
|
520
|
+
let response;
|
|
521
|
+
|
|
522
|
+
if (Platform.OS === 'web') {
|
|
523
|
+
const nativeFetch = window.fetch;
|
|
524
|
+
const NativeFormData = window.FormData;
|
|
525
|
+
const formData = new NativeFormData();
|
|
526
|
+
const fileReq = await nativeFetch(fileUri);
|
|
527
|
+
const blob = await fileReq.blob();
|
|
528
|
+
formData.append('file', blob, `id_card_${postfix}.jpg`);
|
|
529
|
+
|
|
530
|
+
response = await nativeFetch(url, {
|
|
531
|
+
method: 'POST',
|
|
532
|
+
headers: {
|
|
533
|
+
'Authorization': `Bearer ${token}`,
|
|
534
|
+
'Accept': 'application/json'
|
|
535
|
+
},
|
|
536
|
+
body: formData,
|
|
537
|
+
});
|
|
538
|
+
} else {
|
|
539
|
+
// 📱 NATIVE FETCH APPROACH (Untouched, robust as is)
|
|
540
|
+
const formData = new FormData();
|
|
541
|
+
const filePayload = {
|
|
542
|
+
uri: fileUri,
|
|
543
|
+
type: 'image/jpeg',
|
|
544
|
+
name: `id_card_${postfix}.jpg`,
|
|
545
|
+
};
|
|
546
|
+
formData.append('file', filePayload as any);
|
|
547
|
+
|
|
548
|
+
response = await fetch(url, {
|
|
549
|
+
method: 'POST',
|
|
550
|
+
headers: {
|
|
551
|
+
'Authorization': `Bearer ${token}`,
|
|
552
|
+
'Accept': 'application/json'
|
|
553
|
+
},
|
|
554
|
+
body: formData,
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
if (!response.ok) {
|
|
559
|
+
const errorText = await response.text();
|
|
560
|
+
logger.error('Backend MRZ Error Details:', errorText);
|
|
561
|
+
throw new Error(`Erreur serveur: ${response.status}`);
|
|
562
|
+
}
|
|
563
|
+
const data = await response.json();
|
|
564
|
+
logger.log('extractMrzText res', JSON.stringify(data, null, 2));
|
|
565
|
+
|
|
566
|
+
if (Object.keys(data).length === 0) throw new Error('No data found');
|
|
567
|
+
if (data?.success === false) {
|
|
568
|
+
const serverMessage = data.parsed_data?.status_message || data.parsed_data?.status || 'Échec de l\'extraction MRZ';
|
|
569
|
+
throw new Error(`Lecture MRZ refusée: ${serverMessage}`);
|
|
570
|
+
}
|
|
571
|
+
return data;
|
|
572
|
+
} catch (error: any) {
|
|
573
|
+
logger.error("MRZ Fetch Error: ", error?.message || error);
|
|
574
|
+
throw new Error(error?.message || 'Erreur de connexion lors de l\'extraction MRZ');
|
|
575
|
+
}
|
|
576
|
+
}
|
|
623
577
|
|
|
624
578
|
// STEP 2 - SELFIE VALIDATION
|
|
625
579
|
async recognizeFace(params: { idPhotoUri: string; selfiePhotoUri: string }, env: KycEnvironment = 'PRODUCTION'): Promise<{ is_match: boolean; similarity: number; id_bbox?: number[]; selfie_bbox?: number[] }> {
|
|
626
|
-
// SANDBOX mode
|
|
627
580
|
if (env === 'SANDBOX') {
|
|
628
581
|
console.log("SANDBOX mode: Skipping AI face recognition");
|
|
629
582
|
logger.log("SANDBOX mode: Returning mock face recognition response");
|
|
630
|
-
return {
|
|
631
|
-
is_match: true,
|
|
632
|
-
similarity: 0.95,
|
|
633
|
-
id_bbox: [50, 50, 200, 200],
|
|
634
|
-
selfie_bbox: [50, 50, 200, 200]
|
|
635
|
-
};
|
|
583
|
+
return { is_match: true, similarity: 0.95, id_bbox: [50, 50, 200, 200], selfie_bbox: [50, 50, 200, 200] };
|
|
636
584
|
}
|
|
637
585
|
|
|
638
586
|
const { idPhotoUri, selfiePhotoUri } = params;
|
|
@@ -640,7 +588,11 @@ export class KYCService {
|
|
|
640
588
|
await appendFileToFormData(formData, 'id_photo', idPhotoUri, 'id_card_photo.jpg', 'image/jpeg');
|
|
641
589
|
await appendFileToFormData(formData, 'selfie_photo', selfiePhotoUri, 'selfie_final.jpg', 'image/jpeg');
|
|
642
590
|
|
|
643
|
-
const res = await axios.post(`${this.faceServiceURL}/recognize_face/`, formData, {
|
|
591
|
+
const res = await axios.post(`${this.faceServiceURL}/recognize_face/`, formData, {
|
|
592
|
+
// Let Axios determine Content-Type boundary natively
|
|
593
|
+
timeout: 45000
|
|
594
|
+
});
|
|
595
|
+
|
|
644
596
|
if (res.data?.detail) throw new Error(res.data.detail);
|
|
645
597
|
return res.data;
|
|
646
598
|
}
|
|
@@ -672,8 +624,7 @@ export class KYCService {
|
|
|
672
624
|
"status": "PENDING",
|
|
673
625
|
"metadata": {},
|
|
674
626
|
}
|
|
675
|
-
const res = await axios.post<SessionResponse>(`${KYCConfig.getBackendUrl()}/verification/sessions/`,
|
|
676
|
-
data, {
|
|
627
|
+
const res = await axios.post<SessionResponse>(`${KYCConfig.getBackendUrl()}/verification/sessions/`, data, {
|
|
677
628
|
headers: {
|
|
678
629
|
'Content-Type': 'application/json',
|
|
679
630
|
...(apiKey ? { 'Authorization': `ApiKey ${apiKey}` } : { 'Authorization': `Bearer ${token}` })
|
|
@@ -682,146 +633,112 @@ export class KYCService {
|
|
|
682
633
|
return res.data;
|
|
683
634
|
} catch (error: any) {
|
|
684
635
|
logger.error('Error creating session:', JSON.stringify(errorMessage(error), null, 2));
|
|
685
|
-
|
|
686
|
-
// Extract backend error message if available
|
|
687
636
|
const backendMessage = errorMessage(error);
|
|
688
|
-
|
|
689
637
|
if (backendMessage) {
|
|
690
638
|
logger.error('Backend error message:', JSON.stringify(truncateFields(backendMessage), null, 2));
|
|
691
639
|
throw new Error(`Backend error: ${JSON.stringify(truncateFields(backendMessage))}`);
|
|
692
640
|
}
|
|
693
|
-
|
|
694
641
|
throw error;
|
|
695
642
|
}
|
|
696
643
|
}
|
|
697
644
|
|
|
698
|
-
|
|
699
645
|
async verificationSession(payload: VerificationSessionRequest): Promise<any> {
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
// 1. Original Shallow Clone (Preserves file paths perfectly)
|
|
709
|
-
const normalizedData = { ...data };
|
|
710
|
-
|
|
711
|
-
logger.log("🔍 [DocType Check] Raw incoming data.metadata.document_type:", normalizedData?.metadata?.document_type);
|
|
712
|
-
logger.log("🔍 [DocType Check] Raw incoming data.documentType:", normalizedData?.documentType);
|
|
713
|
-
|
|
714
|
-
// Enhanced fallback to guarantee we find the document type
|
|
715
|
-
const rawDocType =
|
|
716
|
-
normalizedData?.metadata?.document_type ||
|
|
717
|
-
normalizedData?.documentType ||
|
|
718
|
-
normalizedData?.documents?.documentType;
|
|
719
|
-
|
|
720
|
-
if (rawDocType) {
|
|
721
|
-
const originalDocType = rawDocType;
|
|
722
|
-
const mappedDocType = documentTypeMap[originalDocType] ?? originalDocType;
|
|
723
|
-
|
|
724
|
-
normalizedData.metadata = {
|
|
725
|
-
...normalizedData.metadata,
|
|
726
|
-
document_type: mappedDocType,
|
|
727
|
-
};
|
|
646
|
+
try {
|
|
647
|
+
const { session_id, step, data, templateId, action, apiKey } = payload;
|
|
648
|
+
const token = apiKey ? undefined : await authentification();
|
|
649
|
+
if (!apiKey && !token) {
|
|
650
|
+
throw new Error("Authentication failed: No API key or Token available.");
|
|
651
|
+
}
|
|
728
652
|
|
|
729
|
-
normalizedData
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
653
|
+
const normalizedData = { ...data };
|
|
654
|
+
logger.log("🔍 [DocType Check] Raw incoming data.metadata.document_type:", normalizedData?.metadata?.document_type);
|
|
655
|
+
logger.log("🔍 [DocType Check] Raw incoming data.documentType:", normalizedData?.documentType);
|
|
656
|
+
|
|
657
|
+
const rawDocType = normalizedData?.metadata?.document_type || normalizedData?.documentType || normalizedData?.documents?.documentType;
|
|
658
|
+
|
|
659
|
+
if (rawDocType) {
|
|
660
|
+
const originalDocType = rawDocType;
|
|
661
|
+
const mappedDocType = documentTypeMap[originalDocType] ?? originalDocType;
|
|
662
|
+
normalizedData.metadata = {
|
|
663
|
+
...normalizedData.metadata,
|
|
664
|
+
document_type: mappedDocType,
|
|
735
665
|
};
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
if (normalizedData.documents.front?.mrz && typeof normalizedData.documents.front.mrz === 'string') {
|
|
743
|
-
try {
|
|
744
|
-
normalizedData.documents.front = {
|
|
745
|
-
...normalizedData.documents.front,
|
|
746
|
-
mrz: JSON.parse(normalizedData.documents.front.mrz)
|
|
747
|
-
};
|
|
748
|
-
} catch (e) {
|
|
749
|
-
logger.log("Warning: Could not parse front MRZ JSON");
|
|
666
|
+
normalizedData.documentType = mappedDocType;
|
|
667
|
+
if (normalizedData.documents) {
|
|
668
|
+
normalizedData.documents = { ...normalizedData.documents, documentType: mappedDocType };
|
|
669
|
+
const isSingleSided = ['passport', 'visa'].includes(mappedDocType.toLowerCase());
|
|
670
|
+
if (isSingleSided && !normalizedData.documents.back) {
|
|
671
|
+
normalizedData.documents.back = {};
|
|
750
672
|
}
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
673
|
+
if (normalizedData.documents.front?.mrz && typeof normalizedData.documents.front.mrz === 'string') {
|
|
674
|
+
try {
|
|
675
|
+
normalizedData.documents.front = { ...normalizedData.documents.front, mrz: JSON.parse(normalizedData.documents.front.mrz) };
|
|
676
|
+
} catch (e) {
|
|
677
|
+
logger.log("Warning: Could not parse front MRZ JSON");
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
if (normalizedData.documents.back?.mrz && typeof normalizedData.documents.back.mrz === 'string') {
|
|
681
|
+
try {
|
|
682
|
+
normalizedData.documents.back = { ...normalizedData.documents.back, mrz: JSON.parse(normalizedData.documents.back.mrz) };
|
|
683
|
+
} catch (e) {
|
|
684
|
+
logger.log("Warning: Could not parse back MRZ JSON");
|
|
685
|
+
}
|
|
761
686
|
}
|
|
762
687
|
}
|
|
763
688
|
}
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
const payloadData = {
|
|
767
|
-
action,
|
|
768
|
-
data: {
|
|
769
|
-
...normalizedData,
|
|
770
|
-
step,
|
|
771
|
-
templateId,
|
|
772
|
-
},
|
|
773
|
-
session_id,
|
|
774
|
-
timestamp: new Date().toISOString(),
|
|
775
|
-
};
|
|
776
689
|
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
{
|
|
784
|
-
url,
|
|
785
|
-
apiKey: apiKey ? "***HIDDEN***" : "None",
|
|
786
|
-
token: token ? "***HIDDEN***" : "None",
|
|
787
|
-
payload: logPayload,
|
|
690
|
+
const payloadData = {
|
|
691
|
+
action,
|
|
692
|
+
data: {
|
|
693
|
+
...normalizedData,
|
|
694
|
+
step,
|
|
695
|
+
templateId,
|
|
788
696
|
},
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
697
|
+
session_id,
|
|
698
|
+
timestamp: new Date().toISOString(),
|
|
699
|
+
};
|
|
700
|
+
const url = `${KYCConfig.getBackendUrl()}/verification/api/kyc/sessions/${session_id}/steps/${step}/`;
|
|
701
|
+
const logPayload = truncateFields({ payloadData, session_id, step });
|
|
702
|
+
|
|
703
|
+
logger.log(
|
|
704
|
+
"verificationSession payload",
|
|
705
|
+
JSON.stringify(
|
|
706
|
+
{
|
|
707
|
+
url,
|
|
708
|
+
apiKey: apiKey ? "***HIDDEN***" : "None",
|
|
709
|
+
token: token ? "***HIDDEN***" : "None",
|
|
710
|
+
payload: logPayload,
|
|
711
|
+
},
|
|
712
|
+
null,
|
|
713
|
+
2
|
|
714
|
+
)
|
|
715
|
+
);
|
|
801
716
|
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
717
|
+
const headers: Record<string, string> = {
|
|
718
|
+
"Content-Type": "application/json",
|
|
719
|
+
...(apiKey ? { Authorization: `ApiKey ${apiKey}` } : {}),
|
|
720
|
+
...(token && !apiKey ? { Authorization: `Bearer ${token}` } : {})
|
|
721
|
+
};
|
|
807
722
|
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
)
|
|
816
|
-
|
|
723
|
+
const res = await axios.post<SessionResponse>(url, payloadData, { headers });
|
|
724
|
+
|
|
725
|
+
logger.log(
|
|
726
|
+
"verificationSession res",
|
|
727
|
+
JSON.stringify(truncateFields(res.data), null, 2)
|
|
728
|
+
);
|
|
729
|
+
return res.data;
|
|
730
|
+
} catch (error: any) {
|
|
731
|
+
const parsedError = errorMessage(error);
|
|
732
|
+
const finalErrorMessage = parsedError || error?.message || "Unknown validation error";
|
|
733
|
+
logger.error(
|
|
734
|
+
"Error validating component:",
|
|
735
|
+
JSON.stringify(finalErrorMessage, null, 2)
|
|
736
|
+
);
|
|
737
|
+
throw new Error(typeof finalErrorMessage === 'string' ? finalErrorMessage : JSON.stringify(finalErrorMessage));
|
|
738
|
+
}
|
|
817
739
|
}
|
|
818
|
-
}
|
|
819
740
|
|
|
820
|
-
|
|
821
|
-
async sendEmailVerificationCode(
|
|
822
|
-
email: string,
|
|
823
|
-
auth?: { apiKey?: string; token?: string }
|
|
824
|
-
): Promise<unknown> {
|
|
741
|
+
async sendEmailVerificationCode(email: string, auth?: { apiKey?: string; token?: string }): Promise<unknown> {
|
|
825
742
|
const url = `${KYCConfig.getBackendUrl()}/accounts/email/send/`;
|
|
826
743
|
const token = auth?.apiKey ? undefined : (auth?.token ?? await authentification());
|
|
827
744
|
const res = await axios.post(
|
|
@@ -837,11 +754,7 @@ export class KYCService {
|
|
|
837
754
|
return res.data;
|
|
838
755
|
}
|
|
839
756
|
|
|
840
|
-
|
|
841
|
-
async verifyEmailCode(
|
|
842
|
-
otp: string,
|
|
843
|
-
auth?: { apiKey?: string; token?: string }
|
|
844
|
-
): Promise<unknown> {
|
|
757
|
+
async verifyEmailCode(otp: string, auth?: { apiKey?: string; token?: string }): Promise<unknown> {
|
|
845
758
|
const url = `${KYCConfig.getBackendUrl()}/accounts/email/verify/`;
|
|
846
759
|
const token = auth?.apiKey ? undefined : (auth?.token ?? await authentification());
|
|
847
760
|
const res = await axios.post(
|
|
@@ -859,231 +772,168 @@ export class KYCService {
|
|
|
859
772
|
|
|
860
773
|
async getVerificationResult(session_id: string, apiKey?: string): Promise<any> {
|
|
861
774
|
try {
|
|
862
|
-
|
|
863
775
|
const token = apiKey ? undefined : await authentification();
|
|
864
|
-
|
|
865
776
|
const url = `${KYCConfig.getBackendUrl()}/verification/api/kyc/result/?session_id=${session_id}`;
|
|
866
|
-
|
|
867
777
|
const controller = new AbortController();
|
|
868
778
|
const timeoutId = setTimeout(() => {
|
|
869
779
|
logger.log("⏳ 8 seconds passed. Force-killing hanging Axios request...");
|
|
870
780
|
controller.abort();
|
|
871
781
|
}, 8000);
|
|
872
|
-
|
|
782
|
+
|
|
873
783
|
const headers: Record<string, string> = {
|
|
874
784
|
'Content-Type': 'application/json',
|
|
875
785
|
};
|
|
876
|
-
|
|
877
786
|
if (apiKey) {
|
|
878
787
|
headers["Authorization"] = `ApiKey ${apiKey}`;
|
|
879
788
|
} else if (token) {
|
|
880
789
|
headers["Authorization"] = `Bearer ${token}`;
|
|
881
790
|
}
|
|
882
|
-
|
|
883
791
|
logger.log("3. Firing Axios GET to:", url);
|
|
884
|
-
|
|
885
|
-
const res = await axios.get(url, {
|
|
886
|
-
headers,
|
|
887
|
-
signal: controller.signal
|
|
888
|
-
});
|
|
889
|
-
|
|
890
|
-
// Clear the timeout if the request succeeds!
|
|
792
|
+
|
|
793
|
+
const res = await axios.get(url, { headers, signal: controller.signal });
|
|
891
794
|
clearTimeout(timeoutId);
|
|
892
795
|
|
|
893
796
|
return res.data;
|
|
894
|
-
|
|
895
797
|
} catch (error: any) {
|
|
896
|
-
// Catch the AbortController kill signal and format it as a timeout
|
|
897
798
|
if (axios.isCancel(error) || error.name === 'AbortError' || error.name === 'CanceledError') {
|
|
898
799
|
logger.error('Axios request was forcefully aborted due to timeout');
|
|
899
800
|
throw new Error('Timeout exceeded');
|
|
900
801
|
}
|
|
901
|
-
|
|
902
802
|
logger.error('Error getting verification result:', JSON.stringify(errorMessage(error) || error.message, null, 2));
|
|
903
803
|
throw error;
|
|
904
804
|
}
|
|
905
805
|
}
|
|
906
806
|
|
|
907
|
-
/** Send WhatsApp verification code. POST /api/v1/accounts/send-whatsapp-verification/ */
|
|
908
807
|
async sendWhatsAppVerificationCode(
|
|
909
808
|
sessionId: string,
|
|
910
809
|
phoneNumber: string,
|
|
911
810
|
auth?: { apiKey?: string; token?: string }
|
|
912
811
|
): Promise<unknown> {
|
|
913
812
|
const url = `${KYCConfig.getBackendUrl()}/accounts/send-whatsapp-verification/`;
|
|
914
|
-
|
|
915
|
-
// 1. Formatting (Ensure consistency with verification step)
|
|
916
813
|
const formattedPhoneNumber = phoneNumber.replace(/^\+/, '');
|
|
917
|
-
|
|
918
|
-
// 2. Prepare Payload
|
|
919
|
-
const payload = {
|
|
920
|
-
session_id: sessionId,
|
|
921
|
-
phone_number: formattedPhoneNumber
|
|
922
|
-
};
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
814
|
+
const payload = { session_id: sessionId, phone_number: formattedPhoneNumber };
|
|
926
815
|
const token = auth?.apiKey ? undefined : (auth?.token ?? await authentification());
|
|
816
|
+
|
|
927
817
|
const headers = {
|
|
928
|
-
|
|
929
|
-
|
|
818
|
+
'Content-Type': 'application/json',
|
|
819
|
+
...(auth?.apiKey ? { 'Authorization': `ApiKey ${auth.apiKey}` } : { 'Authorization': `Bearer ${token}` }),
|
|
930
820
|
};
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
821
|
try {
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
return res.data;
|
|
939
|
-
|
|
822
|
+
const res = await axios.post(url, payload, { headers });
|
|
823
|
+
logger.log("✅ WhatsApp Send Success:", res.data);
|
|
824
|
+
return res.data;
|
|
940
825
|
} catch (error: any) {
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
826
|
+
if (error.response) {
|
|
827
|
+
logger.error("🛑 WhatsApp Send Failed (Server Response):", {
|
|
828
|
+
status: error.response.status,
|
|
829
|
+
data: error.response.data,
|
|
830
|
+
headers: error.response.headers
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
throw error;
|
|
949
834
|
}
|
|
950
835
|
}
|
|
951
836
|
|
|
952
837
|
async verifyWhatsAppCode(
|
|
953
838
|
sessionId: string,
|
|
954
839
|
otp: string,
|
|
955
|
-
phoneNumber: string,
|
|
840
|
+
phoneNumber: string,
|
|
956
841
|
auth?: { apiKey?: string; token?: string }
|
|
957
842
|
): Promise<unknown> {
|
|
958
|
-
const url = `${KYCConfig.getBackendUrl()}/accounts/verify-whatsapp-code/`;
|
|
959
|
-
|
|
843
|
+
const url = `${KYCConfig.getBackendUrl()}/accounts/verify-whatsapp-code/`;
|
|
960
844
|
const formattedPhoneNumber = phoneNumber.replace(/^\+/, '');
|
|
961
|
-
|
|
962
|
-
const payload = {
|
|
963
|
-
session_id: sessionId,
|
|
964
|
-
verification_code: otp,
|
|
965
|
-
phone_number: formattedPhoneNumber
|
|
966
|
-
};
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
845
|
+
const payload = { session_id: sessionId, verification_code: otp, phone_number: formattedPhoneNumber };
|
|
970
846
|
const token = auth?.apiKey ? undefined : (auth?.token ?? await authentification());
|
|
847
|
+
|
|
971
848
|
const headers = {
|
|
972
|
-
|
|
973
|
-
|
|
849
|
+
'Content-Type': 'application/json',
|
|
850
|
+
...(auth?.apiKey ? { 'Authorization': `ApiKey ${auth.apiKey}` } : { 'Authorization': `Bearer ${token}` }),
|
|
974
851
|
};
|
|
975
|
-
|
|
976
|
-
|
|
977
852
|
try {
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
return res.data;
|
|
981
|
-
|
|
853
|
+
const res = await axios.post(url, payload, { headers });
|
|
854
|
+
return res.data;
|
|
982
855
|
} catch (error: any) {
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
856
|
+
if (error.response) {
|
|
857
|
+
logger.error("🛑 WhatsApp Verification Failed (Server Response):", {
|
|
858
|
+
status: error.response.status,
|
|
859
|
+
data: error.response.data,
|
|
860
|
+
headers: error.response.headers
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
throw error;
|
|
991
864
|
}
|
|
992
|
-
}
|
|
993
|
-
|
|
994
|
-
// Inside KYCService.ts
|
|
995
|
-
|
|
996
|
-
async validateBackTemplate(params: {
|
|
997
|
-
region: string;
|
|
998
|
-
docType: string;
|
|
999
|
-
frontTemplateName: string;
|
|
1000
|
-
backTemplateName: string;
|
|
1001
|
-
subRegion: string;
|
|
1002
|
-
token: string;
|
|
1003
|
-
}, env: KycEnvironment = 'PRODUCTION'): Promise<any> {
|
|
1004
|
-
if (env === 'SANDBOX') {
|
|
1005
|
-
logger.log("SANDBOX mode: Skipping back template validation");
|
|
1006
|
-
return { is_match: true, status_message: "SANDBOX: Valid Match" };
|
|
1007
865
|
}
|
|
1008
866
|
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
867
|
+
async validateBackTemplate(params: {
|
|
868
|
+
region: string;
|
|
869
|
+
docType: string;
|
|
870
|
+
frontTemplateName: string;
|
|
871
|
+
backTemplateName: string;
|
|
872
|
+
subRegion: string;
|
|
873
|
+
token: string;
|
|
874
|
+
}, env: KycEnvironment = 'PRODUCTION'): Promise<any> {
|
|
875
|
+
if (env === 'SANDBOX') {
|
|
876
|
+
logger.log("SANDBOX mode: Skipping back template validation");
|
|
877
|
+
return { is_match: true, status_message: "SANDBOX: Valid Match" };
|
|
878
|
+
}
|
|
879
|
+
const { region, docType, frontTemplateName, backTemplateName, subRegion, token } = params;
|
|
880
|
+
const queryParams = new URLSearchParams({
|
|
881
|
+
region: region,
|
|
882
|
+
doc_type: docType,
|
|
883
|
+
front_template_name: frontTemplateName,
|
|
884
|
+
back_template_name: backTemplateName,
|
|
885
|
+
sub_region: subRegion || ""
|
|
886
|
+
}).toString();
|
|
887
|
+
const url = `${this.mrzServiceURL}/validate_back_template/?${queryParams}`;
|
|
888
|
+
logger.log('validateBackTemplate URL', url);
|
|
889
|
+
|
|
890
|
+
try {
|
|
891
|
+
const res = await axios.post(url, null, {
|
|
892
|
+
headers: {
|
|
893
|
+
'Accept': 'application/json',
|
|
894
|
+
'Authorization': `Bearer ${token}`
|
|
895
|
+
},
|
|
896
|
+
timeout: 20000,
|
|
897
|
+
});
|
|
898
|
+
logger.log('validateBackTemplate res', JSON.stringify(res.data, null, 2));
|
|
899
|
+
return res.data;
|
|
900
|
+
} catch (e: any) {
|
|
901
|
+
const errorMsg = e.response?.data?.detail || e.response?.data?.Error || e.message;
|
|
902
|
+
logger.error('Error validating back template:', JSON.stringify(errorMsg));
|
|
903
|
+
throw new Error(typeof errorMsg === 'string' ? errorMsg : 'Template validation failed');
|
|
904
|
+
}
|
|
1038
905
|
}
|
|
1039
|
-
}
|
|
1040
906
|
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
907
|
+
async generateReferralCode(
|
|
908
|
+
payload: { session_id: string; [key: string]: any },
|
|
909
|
+
apiKey: string
|
|
910
|
+
): Promise<any> {
|
|
911
|
+
try {
|
|
912
|
+
logger.log(`Generating referral code for session: ${payload.session_id}`);
|
|
913
|
+
if (!apiKey) {
|
|
914
|
+
throw new Error("Authentication failed: API key is required.");
|
|
915
|
+
}
|
|
916
|
+
const url = `${KYCConfig.getBackendUrl()}/core/referral-code/generate/`;
|
|
917
|
+
const headers: Record<string, string> = {
|
|
918
|
+
"Content-Type": "application/json",
|
|
919
|
+
"Authorization": `ApiKey ${apiKey}`
|
|
920
|
+
};
|
|
921
|
+
|
|
922
|
+
const res = await axios.post(url, payload, { headers, timeout: 8000 });
|
|
923
|
+
logger.log("generateReferralCode SUCCESS:", JSON.stringify(res.data, null, 2));
|
|
924
|
+
return res.data;
|
|
925
|
+
} catch (error: any) {
|
|
926
|
+
const parsedError = errorMessage(error);
|
|
927
|
+
const finalErrorMessage = parsedError || error?.message || "Failed to generate referral code";
|
|
928
|
+
logger.error("Error generating referral code:", typeof finalErrorMessage === 'string' ? finalErrorMessage : JSON.stringify(finalErrorMessage));
|
|
929
|
+
throw new Error(typeof finalErrorMessage === 'string' ? finalErrorMessage : JSON.stringify(finalErrorMessage));
|
|
1054
930
|
}
|
|
1055
|
-
|
|
1056
|
-
const url = `${KYCConfig.getBackendUrl()}/core/referral-code/generate/`;
|
|
1057
|
-
|
|
1058
|
-
const headers: Record<string, string> = {
|
|
1059
|
-
"Content-Type": "application/json",
|
|
1060
|
-
"Authorization": `ApiKey ${apiKey}` // 🚨 Strictly using ApiKey
|
|
1061
|
-
};
|
|
1062
|
-
|
|
1063
|
-
const res = await axios.post(url, payload, {
|
|
1064
|
-
headers,
|
|
1065
|
-
timeout: 8000
|
|
1066
|
-
});
|
|
1067
|
-
|
|
1068
|
-
logger.log("generateReferralCode SUCCESS:", JSON.stringify(res.data, null, 2));
|
|
1069
|
-
return res.data;
|
|
1070
|
-
|
|
1071
|
-
} catch (error: any) {
|
|
1072
|
-
const parsedError = errorMessage(error);
|
|
1073
|
-
const finalErrorMessage = parsedError || error?.message || "Failed to generate referral code";
|
|
1074
|
-
logger.error("Error generating referral code:", typeof finalErrorMessage === 'string' ? finalErrorMessage : JSON.stringify(finalErrorMessage));
|
|
1075
|
-
throw new Error(typeof finalErrorMessage === 'string' ? finalErrorMessage : JSON.stringify(finalErrorMessage));
|
|
1076
931
|
}
|
|
1077
932
|
}
|
|
1078
933
|
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
}
|
|
1082
|
-
|
|
1083
934
|
const kycService = new KYCService("", "");
|
|
1084
935
|
export default kycService;
|
|
1085
936
|
|
|
1086
|
-
// Pour éviter d'afficher des champs trop longs, on tronque les valeurs longues dans le log
|
|
1087
937
|
export function truncateFields(obj: any, maxLength = 420): any {
|
|
1088
938
|
if (typeof obj !== 'object' || obj === null) return obj;
|
|
1089
939
|
if (Array.isArray(obj)) return obj.map(item => truncateFields(item, maxLength));
|
|
@@ -1102,28 +952,18 @@ export function truncateFields(obj: any, maxLength = 420): any {
|
|
|
1102
952
|
return truncated;
|
|
1103
953
|
}
|
|
1104
954
|
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
// Strictly define the return type as Promise<string>
|
|
1109
955
|
export const authentification = async (): Promise<string> => {
|
|
1110
956
|
try {
|
|
1111
|
-
// 1. If we have a valid token (with 10 seconds of breathing room), return it immediately
|
|
1112
957
|
if (cachedToken && tokenExpiryTime && Date.now() < tokenExpiryTime - 10000) {
|
|
1113
958
|
return cachedToken;
|
|
1114
959
|
}
|
|
1115
|
-
|
|
1116
|
-
// 2. RACE CONDITION PREVENTION
|
|
1117
960
|
if (activeAuthRequest) {
|
|
1118
961
|
return activeAuthRequest;
|
|
1119
962
|
}
|
|
1120
|
-
|
|
1121
|
-
// 3. Create the request and lock it
|
|
963
|
+
|
|
1122
964
|
activeAuthRequest = (async () => {
|
|
1123
965
|
logger.log('Authenticating: Fetching new Keycloak token...');
|
|
1124
|
-
|
|
1125
966
|
const params = 'client_id=kyc_frontend&client_secret=QhgAmvKgmwODzsEp98dnA4PeUEMMaFHd&grant_type=client_credentials';
|
|
1126
|
-
|
|
1127
967
|
const res = await axios.post(
|
|
1128
968
|
`https://idms.sanctumkey.com/realms/kyc/protocol/openid-connect/token`,
|
|
1129
969
|
params,
|
|
@@ -1133,44 +973,27 @@ export const authentification = async (): Promise<string> => {
|
|
|
1133
973
|
},
|
|
1134
974
|
}
|
|
1135
975
|
);
|
|
1136
|
-
|
|
1137
976
|
cachedToken = res.data.access_token;
|
|
1138
|
-
|
|
1139
977
|
if (!cachedToken) {
|
|
1140
978
|
throw new Error("Keycloak returned a success response but no access_token was found.");
|
|
1141
979
|
}
|
|
1142
|
-
|
|
1143
|
-
// Calculate exact expiry timestamp (expires_in is usually in seconds)
|
|
1144
|
-
const expiresInMs = (res.data.expires_in || 300) * 1000;
|
|
980
|
+
const expiresInMs = (res.data.expires_in || 300) * 1000;
|
|
1145
981
|
tokenExpiryTime = Date.now() + expiresInMs;
|
|
1146
|
-
|
|
1147
|
-
return cachedToken;
|
|
982
|
+
return cachedToken;
|
|
1148
983
|
})();
|
|
1149
|
-
|
|
984
|
+
|
|
1150
985
|
const finalToken = await activeAuthRequest;
|
|
1151
986
|
return finalToken;
|
|
1152
|
-
|
|
1153
987
|
} catch (error: any) {
|
|
1154
|
-
cachedToken = null;
|
|
1155
|
-
|
|
1156
|
-
const safeErrorMessage =
|
|
1157
|
-
error?.response?.data?.error_description ||
|
|
1158
|
-
error?.response?.data?.error ||
|
|
1159
|
-
error?.message ||
|
|
1160
|
-
'Unknown network error';
|
|
1161
|
-
|
|
988
|
+
cachedToken = null;
|
|
989
|
+
const safeErrorMessage = error?.response?.data?.error_description || error?.response?.data?.error || error?.message || 'Unknown network error';
|
|
1162
990
|
logger.error('Error authentifying:', safeErrorMessage);
|
|
1163
991
|
throw new Error(`Échec de l'authentification: ${safeErrorMessage}`);
|
|
1164
|
-
|
|
1165
992
|
} finally {
|
|
1166
|
-
// 4. Always clear the lock when the request finishes
|
|
1167
993
|
activeAuthRequest = null;
|
|
1168
994
|
}
|
|
1169
995
|
}
|
|
1170
996
|
|
|
1171
997
|
export const errorMessage = (error: any) => {
|
|
1172
|
-
return error.response?.data?.message ||
|
|
1173
|
-
error.response?.data?.detail ||
|
|
1174
|
-
error.response?.data?.error ||
|
|
1175
|
-
error.response?.data;
|
|
998
|
+
return error.response?.data?.message || error.response?.data?.detail || error.response?.data?.error || error.response?.data;
|
|
1176
999
|
}
|