@sanctum-key/react-native-sdk 1.0.51 → 1.0.52

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/android/build/intermediates/compiled_local_resources/debug/compileDebugLibraryResources/out/xml_file_paths.xml.flat +0 -0
  2. package/android/build/intermediates/incremental/debug/packageDebugResources/compile-file-map.properties +2 -2
  3. package/android/build/intermediates/incremental/debug/packageDebugResources/merger.xml +1 -1
  4. package/android/build/intermediates/incremental/mergeDebugJniLibFolders/merger.xml +1 -1
  5. package/android/build/intermediates/incremental/mergeDebugShaders/merger.xml +1 -1
  6. package/android/build/intermediates/incremental/packageDebugAssets/merger.xml +1 -1
  7. package/android/build/intermediates/manifest_merge_blame_file/debug/processDebugManifest/manifest-merger-blame-debug-report.txt +41 -41
  8. package/android/build/kotlin/compileDebugKotlin/cacheable/last-build.bin +0 -0
  9. package/android/build/kotlin/compileDebugKotlin/local-state/build-history.bin +0 -0
  10. package/android/build/outputs/logs/manifest-merger-debug-report.txt +52 -52
  11. package/build/package.json +1 -1
  12. package/build/src/components/KYCElements/IDCardCapture.d.ts.map +1 -1
  13. package/build/src/components/KYCElements/IDCardCapture.js +73 -66
  14. package/build/src/components/KYCElements/IDCardCapture.js.map +1 -1
  15. package/build/src/components/TemplateKYCExample.d.ts +2 -1
  16. package/build/src/components/TemplateKYCExample.d.ts.map +1 -1
  17. package/build/src/components/TemplateKYCExample.js +2 -2
  18. package/build/src/components/TemplateKYCExample.js.map +1 -1
  19. package/build/src/components/TemplateKYCFlowRefactored.d.ts +1 -0
  20. package/build/src/components/TemplateKYCFlowRefactored.d.ts.map +1 -1
  21. package/build/src/components/TemplateKYCFlowRefactored.js +20 -14
  22. package/build/src/components/TemplateKYCFlowRefactored.js.map +1 -1
  23. package/build/src/config/KYCConfig.d.ts.map +1 -1
  24. package/build/src/config/KYCConfig.js +4 -2
  25. package/build/src/config/KYCConfig.js.map +1 -1
  26. package/build/src/hooks/useTemplateKYCFlow.d.ts +1 -0
  27. package/build/src/hooks/useTemplateKYCFlow.d.ts.map +1 -1
  28. package/build/src/hooks/useTemplateKYCFlow.js.map +1 -1
  29. package/build/src/modules/api/CardAuthentification.d.ts +0 -23
  30. package/build/src/modules/api/CardAuthentification.d.ts.map +1 -1
  31. package/build/src/modules/api/CardAuthentification.js +46 -83
  32. package/build/src/modules/api/CardAuthentification.js.map +1 -1
  33. package/build/src/modules/api/KYCService.d.ts.map +1 -1
  34. package/build/src/modules/api/KYCService.js +96 -38
  35. package/build/src/modules/api/KYCService.js.map +1 -1
  36. package/build/src/modules/api/SelfieVerification.d.ts.map +1 -1
  37. package/build/src/modules/api/SelfieVerification.js +12 -19
  38. package/build/src/modules/api/SelfieVerification.js.map +1 -1
  39. package/package.json +1 -1
  40. package/src/components/KYCElements/IDCardCapture.tsx +192 -191
  41. package/src/components/TemplateKYCExample.tsx +3 -0
  42. package/src/components/TemplateKYCFlowRefactored.tsx +31 -25
  43. package/src/config/KYCConfig.ts +7 -5
  44. package/src/hooks/useTemplateKYCFlow.tsx +1 -0
  45. package/src/modules/api/CardAuthentification.ts +409 -445
  46. package/src/modules/api/KYCService.ts +599 -449
  47. package/src/modules/api/SelfieVerification.ts +22 -29
@@ -8,497 +8,647 @@ import { Platform } from 'react-native';
8
8
  import KYCConfig from '../../config/KYCConfig';
9
9
 
10
10
  const appendFileToFormData = async (formData: FormData, key: string, uri: string, name: string = 'file.jpg', type: string = 'image/jpeg') => {
11
- if (Platform.OS === 'web') {
12
- const response = await fetch(uri);
13
- const blob = await response.blob();
14
- formData.append(key, blob, name);
15
- } else {
16
- // React Native Native structure
17
- formData.append(key, { uri, type, name } as any);
18
- }
11
+ if (Platform.OS === 'web') {
12
+ const response = await fetch(uri);
13
+ const blob = await response.blob();
14
+ formData.append(key, blob, name);
15
+ } else {
16
+ // React Native Native structure
17
+ formData.append(key, { uri, type, name } as any);
18
+ }
19
19
  };
20
20
 
21
21
  const documentTypeMap: Record<string, string> = {
22
- passport: "passport",
23
- driversLicense: "driving_licence",
24
- healthInsuranceCard: "health_insurance_card",
25
- residencePermit: "permanent_residence_card",
26
- identityCard: "identity_card",
27
- nationalId: "identity_card",
28
- national_id: "identity_card",
29
- drivers_licence: "driving_licence",
30
- drivers_license: "driving_licence",
31
- driving_license: "driving_licence",
32
- permanent_residence: "permanent_residence_card",
22
+ passport: "passport",
23
+ driversLicense: "driving_licence",
24
+ healthInsuranceCard: "health_insurance_card",
25
+ residencePermit: "permanent_residence_card",
26
+ identityCard: "identity_card",
27
+ nationalId: "identity_card",
28
+ national_id: "identity_card",
29
+ drivers_licence: "driving_licence",
30
+ drivers_license: "driving_licence",
31
+ driving_license: "driving_licence",
32
+ permanent_residence: "permanent_residence_card",
33
33
  };
34
34
 
35
35
  let cachedToken: string | null = null;
36
36
  let tokenExpiryTime: number | null = null;
37
37
  let activeAuthRequest: Promise<string> | null = null;
38
38
 
39
- export interface KYCRequest { userId: string; documentImage: string; selfieImage: string; documentType: 'passport' | 'id_card' | 'drivers_license'; }
40
- export interface KYCResponse { success: boolean; verificationId: string; status: 'pending' | 'approved' | 'rejected'; message?: string; }
41
- export interface FaceDetectionRequest { selfieImage: string; documentImage: string; }
42
- export interface FaceDetectionResponse { match: boolean; confidence: number; message: string; }
43
- export interface SelfieVideoResponse { orientation_direction: "center" | "left" | "right", turn_score: number; bbox: number[]; capture: boolean; instruction: string; error: string; }
39
+ export interface KYCRequest {
40
+ userId: string;
41
+ documentImage: string;
42
+ selfieImage: string;
43
+ documentType: 'passport' | 'id_card' | 'drivers_license';
44
+ }
45
+
46
+ export interface KYCResponse {
47
+ success: boolean;
48
+ verificationId: string;
49
+ status: 'pending' | 'approved' | 'rejected';
50
+ message?: string;
51
+ }
52
+
53
+ export interface FaceDetectionRequest {
54
+ selfieImage: string;
55
+ documentImage: string;
56
+ }
57
+
58
+ export interface FaceDetectionResponse {
59
+ match: boolean;
60
+ confidence: number;
61
+ message: string;
62
+ }
63
+
64
+ export interface SelfieVideoResponse {
65
+ orientation_direction: "center" | "left" | "right",
66
+ turn_score: number;
67
+ bbox: number[];
68
+ capture: boolean;
69
+ instruction: string;
70
+ error: string;
71
+ }
44
72
 
45
73
  export class KYCService {
46
- private baseURL: string;
47
- private apiKey: string;
48
- private faceServiceURL = 'https://face-infera.sanctumkey.com';
49
- private textExtractionServiceURL = 'https://text-infera.sanctumkey.com';
50
- private mrzServiceURL = 'https://mrz-infera.sanctumkey.com';
51
- private barcodeServiceURL = 'https://face-infera.sanctumkey.com';
52
- private orientationServiceURL = 'http://18.188.180.154:8080';
53
-
54
- constructor(baseURL: string, apiKey: string) {
55
- this.baseURL = baseURL;
56
- this.apiKey = apiKey;
57
- }
58
-
59
- private getHeaders() {
60
- return {
61
- 'Authorization': `Bearer ${this.apiKey}`,
62
- 'Content-Type': 'application/json',
63
- };
64
- }
65
-
66
- // 🚨 BULLETPROOF MULTIPART FETCHER
67
- // Replaces Axios for file uploads to guarantee iOS compatibility
68
- private async crossPlatformMultipartUpload<T>(url: string, formData: FormData, token?: string, timeoutMs: number = 60000): Promise<T> {
69
- const controller = new AbortController();
70
- const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
71
-
72
- const headers: Record<string, string> = {
73
- 'Accept': 'application/json',
74
- };
75
- if (token) headers['Authorization'] = `Bearer ${token}`;
74
+ private baseURL: string;
75
+ private apiKey: string;
76
+ private faceServiceURL = 'https://face-infera.sanctumkey.com';
77
+ private textExtractionServiceURL = 'https://text-infera.sanctumkey.com';
78
+ private mrzServiceURL = 'https://mrz-infera.sanctumkey.com';
79
+ private barcodeServiceURL = 'https://face-infera.sanctumkey.com';
80
+ private orientationServiceURL = 'http://18.188.180.154:8080';
81
+
82
+ constructor(baseURL: string, apiKey: string) {
83
+ this.baseURL = baseURL;
84
+ this.apiKey = apiKey;
85
+ }
76
86
 
77
- try {
78
- const response = await fetch(url, {
79
- method: 'POST',
80
- headers, // Do NOT set Content-Type, native fetch sets the boundary
81
- body: formData,
82
- signal: controller.signal
83
- });
84
-
85
- clearTimeout(timeoutId);
86
-
87
- const responseText = await response.text();
88
- let responseData;
89
- try {
90
- responseData = responseText ? JSON.parse(responseText) : {};
91
- } catch (e) {
92
- responseData = { detail: responseText };
93
- }
94
-
95
- if (!response.ok) {
96
- // Mock Axios error structure so your `errorMessage` helper still works!
97
- throw { response: { status: response.status, data: responseData } };
98
- }
99
-
100
- return responseData as T;
101
- } catch (error: any) {
102
- clearTimeout(timeoutId);
103
- if (error.name === 'AbortError') {
104
- throw { response: { data: { message: 'Request timed out' } } };
105
- }
106
- throw error;
87
+ private getHeaders() {
88
+ return {
89
+ 'Authorization': `Bearer ${this.apiKey}`,
90
+ 'Content-Type': 'application/json',
91
+ };
107
92
  }
108
- }
109
93
 
110
- async submitKYC(data: KYCRequest): Promise<KYCResponse> {
111
- try {
112
- const response = await axios.post(`${this.baseURL}/kyc/submit`, data, { headers: this.getHeaders() });
113
- return response.data;
114
- } catch (error) { throw error; }
115
- }
94
+ // 🚨 BULLETPROOF MULTIPART FETCHER
95
+ // Replaces Axios for file uploads to guarantee iOS compatibility
96
+ private async crossPlatformMultipartUpload<T>(url: string, formData: FormData, token?: string, timeoutMs: number = 60000): Promise<T> {
97
+ const controller = new AbortController();
98
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
99
+
100
+ const headers: Record<string, string> = {
101
+ 'Accept': 'application/json',
102
+ };
103
+ if (token) headers['Authorization'] = `Bearer ${token}`;
104
+
105
+ try {
106
+ const response = await fetch(url, {
107
+ method: 'POST',
108
+ headers, // Do NOT set Content-Type, native fetch sets the boundary
109
+ body: formData,
110
+ signal: controller.signal
111
+ });
112
+
113
+ clearTimeout(timeoutId);
114
+ const responseText = await response.text();
115
+ let responseData;
116
+ try {
117
+ responseData = responseText ? JSON.parse(responseText) : {};
118
+ } catch (e) {
119
+ responseData = { detail: responseText };
120
+ }
121
+
122
+ if (!response.ok) {
123
+ // Mock Axios error structure so your `errorMessage` helper still works!
124
+ throw { response: { status: response.status, data: responseData } };
125
+ }
126
+ return responseData as T;
127
+ } catch (error: any) {
128
+ clearTimeout(timeoutId);
129
+ if (error.name === 'AbortError') {
130
+ throw { response: { data: { message: 'Request timed out' } } };
131
+ }
132
+ throw error;
133
+ }
134
+ }
116
135
 
117
- async detectFace(data: FaceDetectionRequest): Promise<FaceDetectionResponse> {
118
- try {
119
- const response = await axios.post(`${this.baseURL}/ai/face-detection`, data, { headers: this.getHeaders() });
120
- return response.data;
121
- } catch (error) { throw error; }
122
- }
136
+ async submitKYC(data: KYCRequest): Promise<KYCResponse> {
137
+ try {
138
+ const response = await axios.post(`${this.baseURL}/kyc/submit`, data, {
139
+ headers: this.getHeaders()
140
+ });
141
+ return response.data;
142
+ } catch (error) {
143
+ throw error;
144
+ }
145
+ }
123
146
 
124
- async processSelfieOrientationPicture(videoFile: string, token: string): Promise<SelfieVideoResponse[]> {
125
- try {
126
- const formData = new FormData();
127
- await appendFileToFormData(formData, 'file', videoFile, 'selfie_picture.jpg', 'image/jpeg');
128
-
129
- const data = await this.crossPlatformMultipartUpload<SelfieVideoResponse[]>(
130
- `${this.faceServiceURL}/detect_face_orientation/`, formData, token, 20000
131
- );
132
- return data;
133
- } catch (error) { throw error; }
134
- }
135
-
136
- async processOrientationVideo(videoFile: string, env: KycEnvironment = 'PRODUCTION'): Promise<OrientationVideoResponse> {
137
- try {
138
- if (env === 'SANDBOX') {
139
- return { success: true, data: { center: { captured: true, frame: 10 }, left: { captured: true, frame: 30 }, right: { captured: true, frame: 50 } }, message: 'SANDBOX Mock' };
140
- }
141
- const formData = new FormData();
142
- await appendFileToFormData(formData, 'file', videoFile, 'orientation_video.mp4', 'video/mp4');
143
-
144
- const data = await this.crossPlatformMultipartUpload<any>(
145
- `${this.orientationServiceURL}/process_orientation_video_stream/`, formData, undefined, 90000
146
- );
147
-
148
- return { success: true, data: data, message: 'Orientation video processed successfully' };
149
- } catch (error: any) {
150
- if (error.response?.status === 503) {
151
- return { success: false, error: { code: 'SERVICE_UNAVAILABLE', message: 'Service not ready', details: error.response.data }, message: 'Service temporarily unavailable' };
152
- }
153
- return { success: false, error: { code: 'PROCESSING_ERROR', message: error.response?.data?.detail || 'Unknown error occurred' }, message: 'Failed to process orientation video' };
147
+ async detectFace(data: FaceDetectionRequest): Promise<FaceDetectionResponse> {
148
+ try {
149
+ const response = await axios.post(`${this.baseURL}/ai/face-detection`, data, {
150
+ headers: this.getHeaders()
151
+ });
152
+ return response.data;
153
+ } catch (error) {
154
+ throw error;
155
+ }
154
156
  }
155
- }
156
157
 
157
- async detectFaceOnId(idCardImageUri: string, token: string, docType: string, env: KycEnvironment = 'PRODUCTION'): Promise<{ result: boolean; detail: any[]; card_obb?: any }> {
158
- if (env === 'SANDBOX') {
159
- return { result: true, detail: [{ confidence: 0.95, bbox: [50, 50, 200, 200] }], card_obb: { x: 50, y: 50, width: 200, height: 200 } };
158
+ async processSelfieOrientationPicture(videoFile: string, token: string): Promise<SelfieVideoResponse[]> {
159
+ try {
160
+ const formData = new FormData();
161
+ await appendFileToFormData(formData, 'file', videoFile, 'selfie_picture.jpg', 'image/jpeg');
162
+
163
+ const data = await this.crossPlatformMultipartUpload<SelfieVideoResponse[]>(
164
+ `${this.faceServiceURL}/detect_face_orientation/`,
165
+ formData,
166
+ token,
167
+ 20000
168
+ );
169
+
170
+ return data;
171
+ } catch (error) {
172
+ throw error;
173
+ }
160
174
  }
161
- const formData = new FormData();
162
- await appendFileToFormData(formData, 'file', idCardImageUri, 'id_card_photo.jpg', 'image/jpeg');
163
- const docTypeShorted = GovernmentDocumentTypeShorted[docType as GovernmentDocumentType] || docType;
164
-
165
- try {
166
- const data = await this.crossPlatformMultipartUpload<any>(
167
- `${this.faceServiceURL}/detect_face/?doc_type=${docTypeShorted}`, formData, token, 20000
168
- );
169
- if (data?.result) return data;
170
- throw new Error(data?.detail || 'detect_face failed');
171
- } catch (error: any) {
172
- throw error;
175
+
176
+ async processOrientationVideo(videoFile: string, env: KycEnvironment = 'PRODUCTION'): Promise<OrientationVideoResponse> {
177
+ try {
178
+ const formData = new FormData();
179
+ await appendFileToFormData(formData, 'file', videoFile, 'orientation_video.mp4', 'video/mp4');
180
+
181
+ const data = await this.crossPlatformMultipartUpload<any>(
182
+ `${this.orientationServiceURL}/process_orientation_video_stream/`,
183
+ formData,
184
+ undefined,
185
+ 90000
186
+ );
187
+
188
+ return { success: true, data: data, message: 'Orientation video processed successfully' };
189
+ } catch (error: any) {
190
+ if (error.response?.status === 503) {
191
+ return {
192
+ success: false,
193
+ error: {
194
+ code: 'SERVICE_UNAVAILABLE',
195
+ message: 'Service not ready',
196
+ details: error.response.data
197
+ },
198
+ message: 'Service temporarily unavailable'
199
+ };
200
+ }
201
+ return {
202
+ success: false,
203
+ error: {
204
+ code: 'PROCESSING_ERROR',
205
+ message: error.response?.data?.detail || 'Unknown error occurred'
206
+ },
207
+ message: 'Failed to process orientation video'
208
+ };
209
+ }
173
210
  }
174
- }
175
211
 
176
- async checkTemplateType(params: { fileUri: string; docType: string; docRegion: string; token: string; postfix: string; issuingSubdivision?: string; }, env: KycEnvironment = 'PRODUCTION'): Promise<any> {
177
- if (env === 'SANDBOX') {
178
- return { template_path: `templates/${params.docType}_${params.docRegion}_${params.postfix}.jpg`, card_obb: { x: 50, y: 50, width: 200, height: 200 } };
212
+ async detectFaceOnId(idCardImageUri: string, token: string, docType: string, env: KycEnvironment = 'PRODUCTION'): Promise<{ result: boolean; detail: any[]; card_obb?: any }> {
213
+ const formData = new FormData();
214
+ await appendFileToFormData(formData, 'file', idCardImageUri, 'id_card_photo.jpg', 'image/jpeg');
215
+
216
+ const docTypeShorted = GovernmentDocumentTypeShorted[docType as GovernmentDocumentType] || docType;
217
+
218
+ try {
219
+ const data = await this.crossPlatformMultipartUpload<any>(
220
+ `${this.faceServiceURL}/detect_face/?doc_type=${docTypeShorted}`,
221
+ formData,
222
+ token,
223
+ 20000
224
+ );
225
+
226
+ if (data?.result) return data;
227
+ throw new Error(data?.detail || 'detect_face failed');
228
+ } catch (error: any) {
229
+ throw error;
230
+ }
179
231
  }
180
- const formData = new FormData();
181
- await appendFileToFormData(formData, 'file', params.fileUri, `id_card_${params.postfix}.jpg`, 'image/jpeg');
182
- const docTypeShorted = GovernmentDocumentTypeShorted[params.docType as GovernmentDocumentType];
183
-
184
- let url = `${this.mrzServiceURL}/get_template_version/?doc_type=${encodeURIComponent(docTypeShorted)}&doc_region=${encodeURIComponent(params.docRegion)}&postfix=${params.postfix}`;
185
- if (params.issuingSubdivision) url += `&issuing_subdivision=${encodeURIComponent(params.issuingSubdivision)}`;
186
-
187
- try {
188
- return await this.crossPlatformMultipartUpload<CheckTemplateTypeResponse>(url, formData, params.token, 60000);
189
- } catch (e: any) { throw e; }
190
- }
191
232
 
192
- async extractDocumentInformation(params: { fileUri: string; docType: string; docRegion: string; templatePath: string; issuingSubdivision?: string; token: string; postfix?: string }): Promise<any> {
193
- const formData = new FormData();
194
- await appendFileToFormData(formData, 'file', params.fileUri, `id_card_${params.postfix || 'front'}.jpg`, 'image/jpeg');
195
-
196
- let queryParams = `doc_type=${encodeURIComponent(params.docType)}&doc_region=${encodeURIComponent(params.docRegion)}&template_path=${encodeURIComponent(params.templatePath)}&postfix=${params.postfix || 'front'}&cleaned=true`;
197
- if (params.issuingSubdivision) queryParams += `&issuing_subdivision=${encodeURIComponent(params.issuingSubdivision)}`;
198
-
199
- const url = `${this.textExtractionServiceURL}/extract_doc_information/?${queryParams}`;
200
- try {
201
- const startResData = await this.crossPlatformMultipartUpload<any>(url, formData, params.token, 60000);
202
- const jobId = startResData?.job_id;
203
- if (!jobId) throw new Error("Backend did not return a job_id");
204
-
205
- const statusUrl = `${this.textExtractionServiceURL}/extract_doc_information/status/${jobId}`;
206
- for (let i = 0; i < 15; i++) {
207
- await new Promise(resolve => setTimeout(resolve, 2000));
208
- // Status polling can remain Axios as it contains no file uploads
209
- const statusRes = await axios.get(statusUrl, { headers: { 'Authorization': `Bearer ${params.token}` } });
210
- if (statusRes.data.status === 'completed') return statusRes.data.result;
211
- if (statusRes.data.status === 'failed') throw new Error(statusRes.data.error || "Document extraction failed.");
212
- }
213
- throw new Error("Document extraction timed out waiting for results.");
214
- } catch (e: any) { throw e; }
215
- }
216
-
217
- async extractBarcode(params: { fileUri: string; token: string; docType: string; postfix?: string }, env: KycEnvironment = 'PRODUCTION'): Promise<any> {
218
- if (env === 'SANDBOX') return { barcode_data: 'SANDBOX_MOCK_BARCODE_DATA' };
219
-
220
- const formData = new FormData();
221
- await appendFileToFormData(formData, 'file', params.fileUri, `id_${params.postfix || 'back'}_photo.jpg`, 'image/jpeg');
222
- const url = `${this.barcodeServiceURL}/decode_barcode/?doc_type=${encodeURIComponent(params.docType)}`;
223
-
224
- try {
225
- return await this.crossPlatformMultipartUpload<any>(url, formData, params.token, 60000);
226
- } catch (e: any) { throw e; }
227
- }
228
-
229
- // URL-Encoded data is completely fine with Axios, no multipart files here.
230
- async formatBarcodeData(params: { textInput: string; token: string }, env: KycEnvironment = 'PRODUCTION'): Promise<any> {
231
- if (env === 'SANDBOX') return { barcode_type: "SANDBOX_MOCK", first_name: "JOHN", last_name: "DOE" };
232
- const formData = new URLSearchParams();
233
- formData.append('text_input', params.textInput);
234
-
235
- try {
236
- const res = await axios.post(`${this.barcodeServiceURL}/format_barcode_data/`, formData.toString(), {
237
- headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Authorization': `Bearer ${params.token}` },
238
- timeout: 60000,
239
- });
240
- return res.data;
241
- } catch (e: any) { throw e; }
242
- }
243
-
244
- async extractMrzText(params: { fileUri: string; docType: string; docRegion: string; postfix?: string; token: string; template_path: string; mrz_type?: string; }, env: KycEnvironment = 'PRODUCTION'): Promise<any> {
245
- if (env === 'SANDBOX') return { success: true, parsed_data: { status: 'success', document_type: params.docType } };
246
-
247
- const formData = new FormData();
248
- await appendFileToFormData(formData, 'file', params.fileUri, `id_card_${params.postfix || 'back'}.jpg`, 'image/jpeg');
249
-
250
- const safeMrzType = params.mrz_type && params.mrz_type.trim() !== '' ? params.mrz_type : 'TD1';
251
- const queryParams = new URLSearchParams({
252
- doc_type: GovernmentDocumentTypeShorted[params.docType as GovernmentDocumentType] || params.docType,
253
- doc_region: params.docRegion, postfix: params.postfix || 'back', template_path: params.template_path, mrz_type: safeMrzType, reset_cache: 'true'
254
- }).toString();
255
-
256
- const url = `${this.mrzServiceURL}/extract_mrz_text/?${queryParams}`;
257
-
258
- try {
259
- const data = await this.crossPlatformMultipartUpload<any>(url, formData, params.token, 60000);
260
- if (Object.keys(data).length === 0) throw new Error('No data found');
261
- if (data?.success === false) throw new Error(`Lecture MRZ refusée: ${data.parsed_data?.status_message}`);
262
- return data;
263
- } catch (error: any) { throw error; }
264
- }
265
-
266
- async recognizeFace(params: { idPhotoUri: string; selfiePhotoUri: string }, env: KycEnvironment = 'PRODUCTION'): Promise<{ is_match: boolean; similarity: number; id_bbox?: number[]; selfie_bbox?: number[] }> {
267
- if (env === 'SANDBOX') return { is_match: true, similarity: 0.95 };
268
- const formData = new FormData();
269
- await appendFileToFormData(formData, 'id_photo', params.idPhotoUri, 'id_card_photo.jpg', 'image/jpeg');
270
- await appendFileToFormData(formData, 'selfie_photo', params.selfiePhotoUri, 'selfie_final.jpg', 'image/jpeg');
233
+ async checkTemplateType(params: { fileUri: string; docType: string; docRegion: string; token: string; postfix: string; issuingSubdivision?: string; }, env: KycEnvironment = 'PRODUCTION'): Promise<any> {
234
+ const formData = new FormData();
235
+ await appendFileToFormData(formData, 'file', params.fileUri, `id_card_${params.postfix}.jpg`, 'image/jpeg');
236
+
237
+ const docTypeShorted = GovernmentDocumentTypeShorted[params.docType as GovernmentDocumentType];
238
+
239
+ let url = `${this.mrzServiceURL}/get_template_version/?doc_type=${encodeURIComponent(docTypeShorted)}&doc_region=${encodeURIComponent(params.docRegion)}&postfix=${params.postfix}`;
240
+ if (params.issuingSubdivision) url += `&issuing_subdivision=${encodeURIComponent(params.issuingSubdivision)}`;
241
+
242
+ try {
243
+ return await this.crossPlatformMultipartUpload<CheckTemplateTypeResponse>(url, formData, params.token, 60000);
244
+ } catch (e: any) {
245
+ throw e;
246
+ }
247
+ }
248
+
249
+ async extractDocumentInformation(params: { fileUri: string; docType: string; docRegion: string; templatePath: string; issuingSubdivision?: string; token: string; postfix?: string }): Promise<any> {
250
+ const formData = new FormData();
251
+ await appendFileToFormData(formData, 'file', params.fileUri, `id_card_${params.postfix || 'front'}.jpg`, 'image/jpeg');
252
+
253
+ let queryParams = `doc_type=${encodeURIComponent(params.docType)}&doc_region=${encodeURIComponent(params.docRegion)}&template_path=${encodeURIComponent(params.templatePath)}&postfix=${params.postfix || 'front'}&cleaned=true`;
254
+
255
+ if (params.issuingSubdivision) queryParams += `&issuing_subdivision=${encodeURIComponent(params.issuingSubdivision)}`;
256
+
257
+ const url = `${this.textExtractionServiceURL}/extract_doc_information/?${queryParams}`;
258
+
259
+ try {
260
+ const startResData = await this.crossPlatformMultipartUpload<any>(url, formData, params.token, 60000);
261
+
262
+ const jobId = startResData?.job_id;
263
+ if (!jobId) throw new Error("Backend did not return a job_id");
264
+
265
+ const statusUrl = `${this.textExtractionServiceURL}/extract_doc_information/status/${jobId}`;
266
+
267
+ for (let i = 0; i < 15; i++) {
268
+ await new Promise(resolve => setTimeout(resolve, 2000));
269
+ // Status polling can remain Axios as it contains no file uploads
270
+ const statusRes = await axios.get(statusUrl, { headers: { 'Authorization': `Bearer ${params.token}` } });
271
+
272
+ if (statusRes.data.status === 'completed') return statusRes.data.result;
273
+ if (statusRes.data.status === 'failed') throw new Error(statusRes.data.error || "Document extraction failed.");
274
+ }
275
+
276
+ throw new Error("Document extraction timed out waiting for results.");
277
+ } catch (e: any) {
278
+ throw e;
279
+ }
280
+ }
281
+
282
+ async extractBarcode(params: { fileUri: string; token: string; docType: string; postfix?: string }, env: KycEnvironment = 'PRODUCTION'): Promise<any> {
283
+ const formData = new FormData();
284
+ await appendFileToFormData(formData, 'file', params.fileUri, `id_${params.postfix || 'back'}_photo.jpg`, 'image/jpeg');
285
+
286
+ const url = `${this.barcodeServiceURL}/decode_barcode/?doc_type=${encodeURIComponent(params.docType)}`;
287
+
288
+ try {
289
+ return await this.crossPlatformMultipartUpload<any>(url, formData, params.token, 60000);
290
+ } catch (e: any) {
291
+ throw e;
292
+ }
293
+ }
294
+
295
+ // URL-Encoded data is completely fine with Axios, no multipart files here.
296
+ async formatBarcodeData(params: { textInput: string; token: string }, env: KycEnvironment = 'PRODUCTION'): Promise<any> {
297
+ const formData = new URLSearchParams();
298
+ formData.append('text_input', params.textInput);
299
+
300
+ try {
301
+ const res = await axios.post(`${this.barcodeServiceURL}/format_barcode_data/`, formData.toString(), {
302
+ headers: {
303
+ 'Content-Type': 'application/x-www-form-urlencoded',
304
+ 'Authorization': `Bearer ${params.token}`
305
+ },
306
+ timeout: 60000,
307
+ });
308
+ return res.data;
309
+ } catch (e: any) {
310
+ throw e;
311
+ }
312
+ }
313
+
314
+ async extractMrzText(params: { fileUri: string; docType: string; docRegion: string; postfix?: string; token: string; template_path: string; mrz_type?: string; }, env: KycEnvironment = 'PRODUCTION'): Promise<any> {
315
+ const formData = new FormData();
316
+ await appendFileToFormData(formData, 'file', params.fileUri, `id_card_${params.postfix || 'back'}.jpg`, 'image/jpeg');
317
+
318
+ const safeMrzType = params.mrz_type && params.mrz_type.trim() !== '' ? params.mrz_type : 'TD1';
319
+
320
+ const queryParams = new URLSearchParams({
321
+ doc_type: GovernmentDocumentTypeShorted[params.docType as GovernmentDocumentType] || params.docType,
322
+ doc_region: params.docRegion,
323
+ postfix: params.postfix || 'back',
324
+ template_path: params.template_path,
325
+ mrz_type: safeMrzType,
326
+ reset_cache: 'true'
327
+ }).toString();
328
+
329
+ const url = `${this.mrzServiceURL}/extract_mrz_text/?${queryParams}`;
330
+
331
+ try {
332
+ const data = await this.crossPlatformMultipartUpload<any>(url, formData, params.token, 60000);
333
+ if (Object.keys(data).length === 0) throw new Error('No data found');
334
+ if (data?.success === false) throw new Error(`Lecture MRZ refusée: ${data.parsed_data?.status_message}`);
335
+ return data;
336
+ } catch (error: any) {
337
+ throw error;
338
+ }
339
+ }
340
+
341
+ async recognizeFace(params: { idPhotoUri: string; selfiePhotoUri: string }, env: KycEnvironment = 'PRODUCTION'): Promise<{ is_match: boolean; similarity: number; id_bbox?: number[]; selfie_bbox?: number[] }> {
342
+ const formData = new FormData();
343
+ await appendFileToFormData(formData, 'id_photo', params.idPhotoUri, 'id_card_photo.jpg', 'image/jpeg');
344
+ await appendFileToFormData(formData, 'selfie_photo', params.selfiePhotoUri, 'selfie_final.jpg', 'image/jpeg');
345
+
346
+ try {
347
+ const data = await this.crossPlatformMultipartUpload<any>(`${this.faceServiceURL}/recognize_face/`, formData, undefined, 45000);
348
+ if (data?.detail) throw new Error(data.detail);
349
+ return data;
350
+ } catch (error: any) {
351
+ throw error;
352
+ }
353
+ }
354
+
355
+ // --- STANDARD AXIOS REQUESTS BELOW (UNCHANGED, SAFE) ---
271
356
 
272
- try {
273
- const data = await this.crossPlatformMultipartUpload<any>(`${this.faceServiceURL}/recognize_face/`, formData, undefined, 45000);
274
- if (data?.detail) throw new Error(data.detail);
275
- return data;
276
- } catch (error: any) { throw error; }
277
- }
278
-
279
- // --- STANDARD AXIOS REQUESTS BELOW (UNCHANGED, SAFE) ---
280
-
281
- async healthFaceDetection(): Promise<any> { return (await axios.get(`${this.faceServiceURL}/health`)).data; }
282
- async healthTextExtraction(): Promise<any> { return (await axios.get(`${this.textExtractionServiceURL}/health`)).data; }
283
- async healthMrzService(): Promise<any> { return (await axios.get(`${this.mrzServiceURL}/health`)).data; }
284
- async healthOrientationService(): Promise<any> { return (await axios.get(`${this.orientationServiceURL}/health`)).data; }
285
-
286
- async newSession({ token, apiKey }: { token?: string, apiKey?: string }): Promise<SessionResponse> {
287
- try {
288
- const res = await axios.post<SessionResponse>(`${KYCConfig.getBackendUrl()}/verification/sessions/`, { "status": "PENDING", "metadata": {} }, {
289
- headers: { 'Content-Type': 'application/json', ...(apiKey ? { 'Authorization': `ApiKey ${apiKey}` } : { 'Authorization': `Bearer ${token}` }) }
290
- });
291
- return res.data;
292
- } catch (error: any) { throw error; }
293
- }
294
-
295
- async verificationSession(payload: VerificationSessionRequest): Promise<any> {
296
- try {
297
- const { session_id, step, data, templateId, action, apiKey } = payload;
298
- const token = apiKey ? undefined : await authentification();
299
- const normalizedData = { ...data };
300
- const rawDocType = normalizedData?.metadata?.document_type || normalizedData?.documentType || normalizedData?.documents?.documentType;
301
-
302
- if (rawDocType) {
303
- const mappedDocType = documentTypeMap[rawDocType] ?? rawDocType;
304
- normalizedData.metadata = { ...normalizedData.metadata, document_type: mappedDocType };
305
- normalizedData.documentType = mappedDocType;
306
- if (normalizedData.documents) {
307
- normalizedData.documents = { ...normalizedData.documents, documentType: mappedDocType };
308
- if (['passport', 'visa'].includes(mappedDocType.toLowerCase()) && !normalizedData.documents.back) normalizedData.documents.back = {};
357
+ async healthFaceDetection(): Promise<any> {
358
+ return (await axios.get(`${this.faceServiceURL}/health`)).data;
359
+ }
360
+
361
+ async healthTextExtraction(): Promise<any> {
362
+ return (await axios.get(`${this.textExtractionServiceURL}/health`)).data;
363
+ }
364
+
365
+ async healthMrzService(): Promise<any> {
366
+ return (await axios.get(`${this.mrzServiceURL}/health`)).data;
367
+ }
368
+
369
+ async healthOrientationService(): Promise<any> {
370
+ return (await axios.get(`${this.orientationServiceURL}/health`)).data;
371
+ }
372
+
373
+ async newSession({ token, apiKey }: { token?: string, apiKey?: string }): Promise<SessionResponse> {
374
+ try {
375
+ const res = await axios.post<SessionResponse>(`${KYCConfig.getBackendUrl()}/verification/sessions/`, {
376
+ "status": "PENDING",
377
+ "metadata": {}
378
+ }, {
379
+ headers: {
380
+ 'Content-Type': 'application/json',
381
+ ...(apiKey ? { 'Authorization': `ApiKey ${apiKey}` } : { 'Authorization': `Bearer ${token}` })
382
+ }
383
+ });
384
+ return res.data;
385
+ } catch (error: any) {
386
+ throw error;
309
387
  }
310
- }
388
+ }
311
389
 
312
- const payloadData = { action, data: { ...normalizedData, step, templateId }, session_id, timestamp: new Date().toISOString() };
313
- const res = await axios.post<SessionResponse>(`${KYCConfig.getBackendUrl()}/verification/api/kyc/sessions/${session_id}/steps/${step}/`, payloadData, {
314
- headers: { "Content-Type": "application/json", ...(apiKey ? { Authorization: `ApiKey ${apiKey}` } : {}), ...(token && !apiKey ? { Authorization: `Bearer ${token}` } : {}) }
315
- });
316
- return res.data;
317
- } catch (error: any) { throw error; }
318
- }
390
+ async verificationSession(payload: VerificationSessionRequest): Promise<any> {
391
+ try {
392
+ const { session_id, step, data, templateId, action, apiKey } = payload;
393
+ const token = apiKey ? undefined : await authentification();
394
+ const normalizedData = { ...data };
395
+
396
+ const rawDocType = normalizedData?.metadata?.document_type || normalizedData?.documentType || normalizedData?.documents?.documentType;
397
+
398
+ if (rawDocType) {
399
+ const mappedDocType = documentTypeMap[rawDocType] ?? rawDocType;
400
+ normalizedData.metadata = { ...normalizedData.metadata, document_type: mappedDocType };
401
+ normalizedData.documentType = mappedDocType;
402
+
403
+ if (normalizedData.documents) {
404
+ normalizedData.documents = { ...normalizedData.documents, documentType: mappedDocType };
405
+ if (['passport', 'visa'].includes(mappedDocType.toLowerCase()) && !normalizedData.documents.back)
406
+ normalizedData.documents.back = {};
407
+ }
408
+ }
409
+
410
+ const payloadData = {
411
+ action,
412
+ data: { ...normalizedData, step, templateId },
413
+ session_id,
414
+ timestamp: new Date().toISOString()
415
+ };
416
+
417
+ const res = await axios.post<SessionResponse>(`${KYCConfig.getBackendUrl()}/verification/api/kyc/sessions/${session_id}/steps/${step}/`, payloadData, {
418
+ headers: {
419
+ "Content-Type": "application/json",
420
+ ...(apiKey ? { Authorization: `ApiKey ${apiKey}` } : {}),
421
+ ...(token && !apiKey ? { Authorization: `Bearer ${token}` } : {})
422
+ }
423
+ });
424
+
425
+ return res.data;
426
+ } catch (error: any) {
427
+ throw error;
428
+ }
429
+ }
319
430
 
320
- async getVerificationResult(session_id: string, apiKey?: string): Promise<any> {
321
- try {
322
- const token = apiKey ? undefined : await authentification();
323
- const controller = new AbortController();
324
- const timeoutId = setTimeout(() => controller.abort(), 8000);
325
- const headers: Record<string, string> = { 'Content-Type': 'application/json' };
326
- if (apiKey) headers["Authorization"] = `ApiKey ${apiKey}`;
327
- else if (token) headers["Authorization"] = `Bearer ${token}`;
328
-
329
- const res = await axios.get(`${KYCConfig.getBackendUrl()}/verification/api/kyc/result/?session_id=${session_id}`, { headers, signal: controller.signal });
330
- clearTimeout(timeoutId);
331
- return res.data;
332
- } catch (error: any) { throw error; }
333
- }
334
-
335
- async validateBackTemplate(params: { region: string; docType: string; frontTemplateName: string; backTemplateName: string; subRegion: string; token: string; }, env: KycEnvironment = 'PRODUCTION'): Promise<any> {
336
- const queryParams = new URLSearchParams({ region: params.region, doc_type: params.docType, front_template_name: params.frontTemplateName, back_template_name: params.backTemplateName, sub_region: params.subRegion || "" }).toString();
337
- try {
338
- const res = await axios.post(`${this.mrzServiceURL}/validate_back_template/?${queryParams}`, null, {
339
- headers: { 'Accept': 'application/json', 'Authorization': `Bearer ${params.token}` }, timeout: 20000,
340
- });
341
- return res.data;
342
- } catch (e: any) { throw e; }
343
- }
344
-
345
- async sendEmailVerificationCode(email: string, auth?: { apiKey?: string; token?: string }): Promise<unknown> {
346
- const url = `${KYCConfig.getBackendUrl()}/accounts/email/send/`;
347
- const token = auth?.apiKey ? undefined : (auth?.token ?? await authentification());
348
- const res = await axios.post(
349
- url,
350
- { email },
351
- {
352
- headers: {
353
- 'Content-Type': 'application/json',
354
- ...(auth?.apiKey ? { 'Authorization': `ApiKey ${auth.apiKey}` } : { 'Authorization': `Bearer ${token}` }),
355
- },
356
- }
357
- );
358
- return res.data;
359
- }
360
-
361
- async verifyEmailCode(otp: string, auth?: { apiKey?: string; token?: string }): Promise<unknown> {
362
- const url = `${KYCConfig.getBackendUrl()}/accounts/email/verify/`;
363
- const token = auth?.apiKey ? undefined : (auth?.token ?? await authentification());
364
- const res = await axios.post(
365
- url,
366
- { otp },
367
- {
368
- headers: {
369
- 'Content-Type': 'application/json',
370
- ...(auth?.apiKey ? { 'Authorization': `ApiKey ${auth.apiKey}` } : { 'Authorization': `Bearer ${token}` }),
371
- },
372
- }
373
- );
374
- return res.data;
375
- }
376
-
377
- async sendWhatsAppVerificationCode(
378
- sessionId: string,
379
- phoneNumber: string,
380
- auth?: { apiKey?: string; token?: string }
381
- ): Promise<unknown> {
382
- const url = `${KYCConfig.getBackendUrl()}/accounts/send-whatsapp-verification/`;
383
- const formattedPhoneNumber = phoneNumber.replace(/^\+/, '');
384
- const payload = { session_id: sessionId, phone_number: formattedPhoneNumber };
385
- const token = auth?.apiKey ? undefined : (auth?.token ?? await authentification());
386
- const headers = {
387
- 'Content-Type': 'application/json',
388
- ...(auth?.apiKey ? { 'Authorization': `ApiKey ${auth.apiKey}` } : { 'Authorization': `Bearer ${token}` }),
389
- };
390
- try {
391
- const res = await axios.post(url, payload, { headers });
392
- logger.log("✅ WhatsApp Send Success:", res.data);
393
- return res.data;
394
- } catch (error: any) {
395
- if (error.response) {
396
- logger.error("🛑 WhatsApp Send Failed (Server Response):", { status: error.response.status, data: error.response.data, headers: error.response.headers });
397
- }
398
- throw error;
431
+ async getVerificationResult(session_id: string, apiKey?: string): Promise<any> {
432
+ try {
433
+ const token = apiKey ? undefined : await authentification();
434
+ const controller = new AbortController();
435
+ const timeoutId = setTimeout(() => controller.abort(), 8000);
436
+
437
+ const headers: Record<string, string> = {
438
+ 'Content-Type': 'application/json'
439
+ };
440
+
441
+ if (apiKey) headers["Authorization"] = `ApiKey ${apiKey}`;
442
+ else if (token) headers["Authorization"] = `Bearer ${token}`;
443
+
444
+ const res = await axios.get(`${KYCConfig.getBackendUrl()}/verification/api/kyc/result/?session_id=${session_id}`, {
445
+ headers,
446
+ signal: controller.signal
447
+ });
448
+
449
+ clearTimeout(timeoutId);
450
+ return res.data;
451
+ } catch (error: any) {
452
+ throw error;
453
+ }
399
454
  }
400
- }
401
-
402
- async verifyWhatsAppCode(
403
- sessionId: string,
404
- otp: string,
405
- phoneNumber: string,
406
- auth?: { apiKey?: string; token?: string }
407
- ): Promise<unknown> {
408
- const url = `${KYCConfig.getBackendUrl()}/accounts/verify-whatsapp-code/`;
409
- const formattedPhoneNumber = phoneNumber.replace(/^\+/, '');
410
- const payload = { session_id: sessionId, verification_code: otp, phone_number: formattedPhoneNumber };
411
- const token = auth?.apiKey ? undefined : (auth?.token ?? await authentification());
412
- const headers = {
413
- 'Content-Type': 'application/json',
414
- ...(auth?.apiKey ? { 'Authorization': `ApiKey ${auth.apiKey}` } : { 'Authorization': `Bearer ${token}` }),
415
- };
416
- try {
417
- const res = await axios.post(url, payload, { headers });
418
- return res.data;
419
- } catch (error: any) {
420
- if (error.response) {
421
- logger.error("🛑 WhatsApp Verification Failed (Server Response):", { status: error.response.status, data: error.response.data, headers: error.response.headers });
422
- }
423
- throw error;
455
+
456
+ async validateBackTemplate(params: { region: string; docType: string; frontTemplateName: string; backTemplateName: string; subRegion: string; token: string; }, env: KycEnvironment = 'PRODUCTION'): Promise<any> {
457
+ const queryParams = new URLSearchParams({
458
+ region: params.region,
459
+ doc_type: params.docType,
460
+ front_template_name: params.frontTemplateName,
461
+ back_template_name: params.backTemplateName,
462
+ sub_region: params.subRegion || ""
463
+ }).toString();
464
+
465
+ try {
466
+ const res = await axios.post(`${this.mrzServiceURL}/validate_back_template/?${queryParams}`, null, {
467
+ headers: {
468
+ 'Accept': 'application/json',
469
+ 'Authorization': `Bearer ${params.token}`
470
+ },
471
+ timeout: 20000,
472
+ });
473
+ return res.data;
474
+ } catch (e: any) {
475
+ throw e;
476
+ }
424
477
  }
425
- }
426
478
 
427
- async generateReferralCode(
428
- payload: { session_id: string; [key: string]: any },
429
- apiKey: string
430
- ): Promise<any> {
431
- try {
432
- logger.log(`Generating referral code for session: ${payload.session_id}`);
433
- if (!apiKey) {
434
- throw new Error("Authentication failed: API key is required.");
435
- }
436
- const url = `${KYCConfig.getBackendUrl()}/core/referral-code/generate/`;
437
- const headers: Record<string, string> = {
438
- "Content-Type": "application/json",
439
- "Authorization": `ApiKey ${apiKey}`
440
- };
441
- const res = await axios.post(url, payload, { headers, timeout: 8000 });
442
- logger.log("generateReferralCode SUCCESS:", JSON.stringify(res.data, null, 2));
443
- return res.data;
444
- } catch (error: any) {
445
- const parsedError = errorMessage(error);
446
- const finalErrorMessage = parsedError || error?.message || "Failed to generate referral code";
447
- logger.error("Error generating referral code:", typeof finalErrorMessage === 'string' ? finalErrorMessage : JSON.stringify(finalErrorMessage));
448
- throw new Error(typeof finalErrorMessage === 'string' ? finalErrorMessage : JSON.stringify(finalErrorMessage));
479
+ async sendEmailVerificationCode(email: string, auth?: { apiKey?: string; token?: string }): Promise<unknown> {
480
+ const url = `${KYCConfig.getBackendUrl()}/accounts/email/send/`;
481
+ const token = auth?.apiKey ? undefined : (auth?.token ?? await authentification());
482
+
483
+ const res = await axios.post(
484
+ url,
485
+ { email },
486
+ {
487
+ headers: {
488
+ 'Content-Type': 'application/json',
489
+ ...(auth?.apiKey ? { 'Authorization': `ApiKey ${auth.apiKey}` } : { 'Authorization': `Bearer ${token}` }),
490
+ },
491
+ }
492
+ );
493
+ return res.data;
494
+ }
495
+
496
+ async verifyEmailCode(otp: string, auth?: { apiKey?: string; token?: string }): Promise<unknown> {
497
+ const url = `${KYCConfig.getBackendUrl()}/accounts/email/verify/`;
498
+ const token = auth?.apiKey ? undefined : (auth?.token ?? await authentification());
499
+
500
+ const res = await axios.post(
501
+ url,
502
+ { otp },
503
+ {
504
+ headers: {
505
+ 'Content-Type': 'application/json',
506
+ ...(auth?.apiKey ? { 'Authorization': `ApiKey ${auth.apiKey}` } : { 'Authorization': `Bearer ${token}` }),
507
+ },
508
+ }
509
+ );
510
+ return res.data;
511
+ }
512
+
513
+ async sendWhatsAppVerificationCode(
514
+ sessionId: string,
515
+ phoneNumber: string,
516
+ auth?: { apiKey?: string; token?: string }
517
+ ): Promise<unknown> {
518
+ const url = `${KYCConfig.getBackendUrl()}/accounts/send-whatsapp-verification/`;
519
+ const formattedPhoneNumber = phoneNumber.replace(/^\+/, '');
520
+ const payload = { session_id: sessionId, phone_number: formattedPhoneNumber };
521
+ const token = auth?.apiKey ? undefined : (auth?.token ?? await authentification());
522
+
523
+ const headers = {
524
+ 'Content-Type': 'application/json',
525
+ ...(auth?.apiKey ? { 'Authorization': `ApiKey ${auth.apiKey}` } : { 'Authorization': `Bearer ${token}` }),
526
+ };
527
+
528
+ try {
529
+ const res = await axios.post(url, payload, { headers });
530
+ logger.log("✅ WhatsApp Send Success:", res.data);
531
+ return res.data;
532
+ } catch (error: any) {
533
+ if (error.response) {
534
+ logger.error("🛑 WhatsApp Send Failed (Server Response):", {
535
+ status: error.response.status,
536
+ data: error.response.data,
537
+ headers: error.response.headers
538
+ });
539
+ }
540
+ throw error;
541
+ }
542
+ }
543
+
544
+ async verifyWhatsAppCode(
545
+ sessionId: string,
546
+ otp: string,
547
+ phoneNumber: string,
548
+ auth?: { apiKey?: string; token?: string }
549
+ ): Promise<unknown> {
550
+ const url = `${KYCConfig.getBackendUrl()}/accounts/verify-whatsapp-code/`;
551
+ const formattedPhoneNumber = phoneNumber.replace(/^\+/, '');
552
+ const payload = { session_id: sessionId, verification_code: otp, phone_number: formattedPhoneNumber };
553
+ const token = auth?.apiKey ? undefined : (auth?.token ?? await authentification());
554
+
555
+ const headers = {
556
+ 'Content-Type': 'application/json',
557
+ ...(auth?.apiKey ? { 'Authorization': `ApiKey ${auth.apiKey}` } : { 'Authorization': `Bearer ${token}` }),
558
+ };
559
+
560
+ try {
561
+ const res = await axios.post(url, payload, { headers });
562
+ return res.data;
563
+ } catch (error: any) {
564
+ if (error.response) {
565
+ logger.error("🛑 WhatsApp Verification Failed (Server Response):", {
566
+ status: error.response.status,
567
+ data: error.response.data,
568
+ headers: error.response.headers
569
+ });
570
+ }
571
+ throw error;
572
+ }
573
+ }
574
+
575
+ async generateReferralCode(
576
+ payload: { session_id: string;[key: string]: any },
577
+ apiKey: string
578
+ ): Promise<any> {
579
+ try {
580
+ logger.log(`Generating referral code for session: ${payload.session_id}`);
581
+ if (!apiKey) {
582
+ throw new Error("Authentication failed: API key is required.");
583
+ }
584
+
585
+ const url = `${KYCConfig.getBackendUrl()}/core/referral-code/generate/`;
586
+ const headers: Record<string, string> = {
587
+ "Content-Type": "application/json",
588
+ "Authorization": `ApiKey ${apiKey}`
589
+ };
590
+
591
+ const res = await axios.post(url, payload, { headers, timeout: 8000 });
592
+ logger.log("generateReferralCode SUCCESS:", JSON.stringify(res.data, null, 2));
593
+ return res.data;
594
+ } catch (error: any) {
595
+ const parsedError = errorMessage(error);
596
+ const finalErrorMessage = parsedError || error?.message || "Failed to generate referral code";
597
+ logger.error("Error generating referral code:", typeof finalErrorMessage === 'string' ? finalErrorMessage : JSON.stringify(finalErrorMessage));
598
+ throw new Error(typeof finalErrorMessage === 'string' ? finalErrorMessage : JSON.stringify(finalErrorMessage));
599
+ }
449
600
  }
450
- }
451
601
  }
452
602
 
453
603
  const kycService = new KYCService("", "");
454
604
  export default kycService;
455
605
 
456
- export function truncateFields(obj: any, maxLength = 420): any { /* logic unchanged */ return obj; }
606
+ export function truncateFields(obj: any, maxLength = 420): any {
607
+ /* logic unchanged */
608
+ return obj;
609
+ }
457
610
 
458
611
  export const authentification = async (): Promise<string> => {
459
- try {
460
- if (cachedToken && tokenExpiryTime && Date.now() < tokenExpiryTime - 10000) return cachedToken;
461
- if (activeAuthRequest) return activeAuthRequest;
462
-
463
- activeAuthRequest = (async () => {
464
- const res = await axios.post(
465
- `https://idms.sanctumkey.com/realms/kyc/protocol/openid-connect/token`,
466
- 'client_id=kyc_frontend&client_secret=QhgAmvKgmwODzsEp98dnA4PeUEMMaFHd&grant_type=client_credentials',
467
- { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }
468
- );
469
-
470
- const token = res.data.access_token;
471
- if (!token) {
472
- throw new Error("Keycloak returned a success response but no access_token was found.");
473
- }
474
-
475
- cachedToken = token;
476
- tokenExpiryTime = Date.now() + ((res.data.expires_in || 300) * 1000);
477
- activeAuthRequest = null; // 🚨 FIX: Clear the promise once resolved
478
-
479
- return cachedToken as string;
480
- })();
481
-
482
- return await activeAuthRequest;
483
- } catch (error: any) {
484
- cachedToken = null;
485
- tokenExpiryTime = null;
486
- activeAuthRequest = null;
487
-
488
- const safeErrorMessage = error?.response?.data?.error_description || error?.message || 'Unknown error';
489
- logger.error('Error authentifying:', safeErrorMessage);
490
- throw new Error(`Échec de l'authentification: ${safeErrorMessage}`);
491
- }
612
+ try {
613
+ if (cachedToken && tokenExpiryTime && Date.now() < tokenExpiryTime - 10000) return cachedToken;
614
+ if (activeAuthRequest) return activeAuthRequest;
615
+
616
+ activeAuthRequest = (async () => {
617
+ const res = await axios.post(
618
+ `https://idms.sanctumkey.com/realms/kyc/protocol/openid-connect/token`,
619
+ 'client_id=kyc_frontend&client_secret=QhgAmvKgmwODzsEp98dnA4PeUEMMaFHd&grant_type=client_credentials',
620
+ { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }
621
+ );
622
+
623
+ const token = res.data.access_token;
624
+ if (!token) {
625
+ throw new Error("Keycloak returned a success response but no access_token was found.");
626
+ }
627
+
628
+ cachedToken = token;
629
+ tokenExpiryTime = Date.now() + ((res.data.expires_in || 300) * 1000);
630
+ activeAuthRequest = null; // 🚨 FIX: Clear the promise once resolved
631
+ return cachedToken as string;
632
+ })();
633
+
634
+ return await activeAuthRequest;
635
+ } catch (error: any) {
636
+ cachedToken = null;
637
+ tokenExpiryTime = null;
638
+ activeAuthRequest = null;
639
+ const safeErrorMessage = error?.response?.data?.error_description || error?.message || 'Unknown error';
640
+ logger.error('Error authentifying:', safeErrorMessage);
641
+ throw new Error(`Échec de l'authentification: ${safeErrorMessage}`);
642
+ }
492
643
  }
493
644
 
494
645
  export const clearAuthToken = () => {
495
- logger.log("Wiping AI services auth token cache...");
496
- cachedToken = null;
497
- tokenExpiryTime = null;
498
- activeAuthRequest = null;
646
+ logger.log("Wiping AI services auth token cache...");
647
+ cachedToken = null;
648
+ tokenExpiryTime = null;
649
+ activeAuthRequest = null;
499
650
  };
500
651
 
501
-
502
652
  export const errorMessage = (error: any) => {
503
- return error.response?.data?.message || error.response?.data?.detail || error.response?.data?.error || error.response?.data || error.message;
653
+ return error.response?.data?.message || error.response?.data?.detail || error.response?.data?.error || error.response?.data || error.message;
504
654
  }