@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.
- package/android/build/intermediates/compiled_local_resources/debug/compileDebugLibraryResources/out/xml_file_paths.xml.flat +0 -0
- package/android/build/intermediates/incremental/debug/packageDebugResources/compile-file-map.properties +2 -2
- package/android/build/intermediates/incremental/debug/packageDebugResources/merger.xml +1 -1
- package/android/build/intermediates/incremental/mergeDebugJniLibFolders/merger.xml +1 -1
- package/android/build/intermediates/incremental/mergeDebugShaders/merger.xml +1 -1
- package/android/build/intermediates/incremental/packageDebugAssets/merger.xml +1 -1
- package/android/build/intermediates/manifest_merge_blame_file/debug/processDebugManifest/manifest-merger-blame-debug-report.txt +41 -41
- package/android/build/kotlin/compileDebugKotlin/cacheable/last-build.bin +0 -0
- package/android/build/kotlin/compileDebugKotlin/local-state/build-history.bin +0 -0
- package/android/build/outputs/logs/manifest-merger-debug-report.txt +52 -52
- package/build/package.json +1 -1
- package/build/src/components/KYCElements/IDCardCapture.d.ts.map +1 -1
- package/build/src/components/KYCElements/IDCardCapture.js +73 -66
- package/build/src/components/KYCElements/IDCardCapture.js.map +1 -1
- package/build/src/components/TemplateKYCExample.d.ts +2 -1
- package/build/src/components/TemplateKYCExample.d.ts.map +1 -1
- package/build/src/components/TemplateKYCExample.js +2 -2
- package/build/src/components/TemplateKYCExample.js.map +1 -1
- package/build/src/components/TemplateKYCFlowRefactored.d.ts +1 -0
- package/build/src/components/TemplateKYCFlowRefactored.d.ts.map +1 -1
- package/build/src/components/TemplateKYCFlowRefactored.js +20 -14
- package/build/src/components/TemplateKYCFlowRefactored.js.map +1 -1
- package/build/src/config/KYCConfig.d.ts.map +1 -1
- package/build/src/config/KYCConfig.js +4 -2
- package/build/src/config/KYCConfig.js.map +1 -1
- package/build/src/hooks/useTemplateKYCFlow.d.ts +1 -0
- package/build/src/hooks/useTemplateKYCFlow.d.ts.map +1 -1
- package/build/src/hooks/useTemplateKYCFlow.js.map +1 -1
- package/build/src/modules/api/CardAuthentification.d.ts +0 -23
- package/build/src/modules/api/CardAuthentification.d.ts.map +1 -1
- package/build/src/modules/api/CardAuthentification.js +46 -83
- package/build/src/modules/api/CardAuthentification.js.map +1 -1
- package/build/src/modules/api/KYCService.d.ts.map +1 -1
- package/build/src/modules/api/KYCService.js +96 -38
- package/build/src/modules/api/KYCService.js.map +1 -1
- package/build/src/modules/api/SelfieVerification.d.ts.map +1 -1
- package/build/src/modules/api/SelfieVerification.js +12 -19
- package/build/src/modules/api/SelfieVerification.js.map +1 -1
- package/package.json +1 -1
- package/src/components/KYCElements/IDCardCapture.tsx +192 -191
- package/src/components/TemplateKYCExample.tsx +3 -0
- package/src/components/TemplateKYCFlowRefactored.tsx +31 -25
- package/src/config/KYCConfig.ts +7 -5
- package/src/hooks/useTemplateKYCFlow.tsx +1 -0
- package/src/modules/api/CardAuthentification.ts +409 -445
- package/src/modules/api/KYCService.ts +599 -449
- 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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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 {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
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
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
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 {
|
|
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
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
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
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
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
|
-
|
|
653
|
+
return error.response?.data?.message || error.response?.data?.detail || error.response?.data?.error || error.response?.data || error.message;
|
|
504
654
|
}
|