@sanctum-key/react-native-sdk 1.0.18 → 1.0.20

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 (48) hide show
  1. package/README.md +1 -1
  2. package/build/package.json +3 -2
  3. package/build/src/components/EnhancedCameraView.d.ts.map +1 -1
  4. package/build/src/components/EnhancedCameraView.js +19 -182
  5. package/build/src/components/EnhancedCameraView.js.map +1 -1
  6. package/build/src/components/KYCElements/CountrySelectionTemplate.d.ts.map +1 -1
  7. package/build/src/components/KYCElements/CountrySelectionTemplate.js +40 -128
  8. package/build/src/components/KYCElements/CountrySelectionTemplate.js.map +1 -1
  9. package/build/src/components/KYCElements/IDCardCapture.d.ts.map +1 -1
  10. package/build/src/components/KYCElements/IDCardCapture.js +224 -189
  11. package/build/src/components/KYCElements/IDCardCapture.js.map +1 -1
  12. package/build/src/components/KYCElements/PhoneVerificationTemplate.d.ts.map +1 -1
  13. package/build/src/components/KYCElements/PhoneVerificationTemplate.js +0 -2
  14. package/build/src/components/KYCElements/PhoneVerificationTemplate.js.map +1 -1
  15. package/build/src/components/KYCElements/VerificationProgressTemplate.d.ts.map +1 -1
  16. package/build/src/components/KYCElements/VerificationProgressTemplate.js +38 -33
  17. package/build/src/components/KYCElements/VerificationProgressTemplate.js.map +1 -1
  18. package/build/src/components/OverLay/IdCard.d.ts +6 -1
  19. package/build/src/components/OverLay/IdCard.d.ts.map +1 -1
  20. package/build/src/components/OverLay/IdCard.js +36 -34
  21. package/build/src/components/OverLay/IdCard.js.map +1 -1
  22. package/build/src/config/countriesData.d.ts.map +1 -1
  23. package/build/src/config/countriesData.js.map +1 -1
  24. package/build/src/hooks/useTemplateKYCFlow.js +1 -1
  25. package/build/src/hooks/useTemplateKYCFlow.js.map +1 -1
  26. package/build/src/modules/api/CardAuthentification.d.ts +5 -0
  27. package/build/src/modules/api/CardAuthentification.d.ts.map +1 -1
  28. package/build/src/modules/api/CardAuthentification.js +67 -14
  29. package/build/src/modules/api/CardAuthentification.js.map +1 -1
  30. package/build/src/modules/api/KYCService.d.ts +10 -0
  31. package/build/src/modules/api/KYCService.d.ts.map +1 -1
  32. package/build/src/modules/api/KYCService.js +135 -49
  33. package/build/src/modules/api/KYCService.js.map +1 -1
  34. package/build/src/web/WebKYCEntry.d.ts.map +1 -1
  35. package/build/src/web/WebKYCEntry.js +34 -6
  36. package/build/src/web/WebKYCEntry.js.map +1 -1
  37. package/package.json +3 -2
  38. package/src/components/EnhancedCameraView.tsx +28 -219
  39. package/src/components/KYCElements/CountrySelectionTemplate.tsx +53 -133
  40. package/src/components/KYCElements/IDCardCapture.tsx +606 -570
  41. package/src/components/KYCElements/PhoneVerificationTemplate.tsx +0 -2
  42. package/src/components/KYCElements/VerificationProgressTemplate.tsx +54 -66
  43. package/src/components/OverLay/IdCard.tsx +48 -36
  44. package/src/config/countriesData.ts +0 -4
  45. package/src/hooks/useTemplateKYCFlow.tsx +14 -14
  46. package/src/modules/api/CardAuthentification.ts +133 -67
  47. package/src/modules/api/KYCService.ts +189 -66
  48. package/src/web/WebKYCEntry.tsx +42 -7
@@ -281,7 +281,7 @@ export class KYCService {
281
281
  }
282
282
 
283
283
  //check templatetemplate_type
284
- async checkTemplateType(params: { fileUri: string; docType: string; docRegion: string; token: string; postfix: string }, env: KycEnvironment = 'PRODUCTION'): Promise<any> {
284
+ async checkTemplateType(params: { fileUri: string; docType: string; docRegion: string; token: string; postfix: string}, env: KycEnvironment = 'PRODUCTION'): Promise<any> {
285
285
  // SANDBOX mode
286
286
  if (env === 'SANDBOX') {
287
287
  console.log("SANDBOX mode: Skipping AI template type check");
@@ -296,7 +296,6 @@ export class KYCService {
296
296
  const { fileUri, docType, docRegion, token, postfix } = params;
297
297
  const formData = new FormData();
298
298
 
299
- // ✅ FIX: Dynamically assign the postfix to the filename
300
299
  await appendFileToFormData(formData, 'file', fileUri, `id_card_${postfix}.jpg`, 'image/jpeg');
301
300
 
302
301
  const docTypeShorted = GovernmentDocumentTypeShorted[docType as GovernmentDocumentType];
@@ -317,29 +316,75 @@ export class KYCService {
317
316
 
318
317
  }
319
318
 
320
- async extractDocumentInformation(params: { fileUri: string; docType: string; docRegion: string; token: string; postfix?: string }): Promise<any> {
321
- const { fileUri, docType, docRegion, token, postfix = 'front' } = params;
322
- const formData = new FormData();
319
+ async extractDocumentInformation(params: {
320
+ fileUri: string;
321
+ docType: string;
322
+ docRegion: string;
323
+ templatePath: string; // 🚨 Required by API
324
+ issuingSubdivision?: string; // 🚨 Required for non-passports
325
+ token: string;
326
+ postfix?: string
327
+ }): Promise<any> {
328
+ const { fileUri, docType, docRegion, templatePath, issuingSubdivision, token, postfix = 'front' } = params;
323
329
 
324
- // ✅ FIX: Dynamic filename
330
+ // 1. Prepare Form Data
331
+ const formData = new FormData();
332
+ // Prefixing with 'front' or 'back' as strictly required by the docs
325
333
  await appendFileToFormData(formData, 'file', fileUri, `id_card_${postfix}.jpg`, 'image/jpeg');
326
334
 
327
- const url = `${this.textExtractionServiceURL}/extract_doc_information/?doc_type=${encodeURIComponent(docType)}&doc_region=${encodeURIComponent(docRegion)}`;
328
- const attempt = async () => {
329
- const res = await axios.post(url, formData, {
330
- headers: { 'Content-Type': 'multipart/form-data', 'Authorization': `Bearer ${token}`, },
335
+ // 2. Build the URL with ALL required parameters
336
+ let queryParams = `doc_type=${encodeURIComponent(docType)}&doc_region=${encodeURIComponent(docRegion)}&template_path=${encodeURIComponent(templatePath)}`;
337
+ if (issuingSubdivision) {
338
+ queryParams += `&issuing_subdivision=${encodeURIComponent(issuingSubdivision)}`;
339
+ }
340
+
341
+ const url = `${this.textExtractionServiceURL}/extract_doc_information/?${queryParams}`;
342
+
343
+ try {
344
+ // 3. Start the Job
345
+ const startRes = await axios.post(url, formData, {
346
+ headers: { 'Content-Type': 'multipart/form-data', 'Authorization': `Bearer ${token}` },
331
347
  timeout: 60000,
332
348
  });
333
- logger.log('extractDocumentInformation res', JSON.stringify(truncateFields(res)));
349
+
350
+ const jobId = startRes.data?.job_id;
351
+ if (!jobId) throw new Error("Backend did not return a job_id");
334
352
 
335
- if (res.data?.detail) throw new Error(res.data.detail);
336
- return res.data;
337
- };
338
- try {
339
- return await attempt();
340
- } catch (e) {
341
- logger.error('Error extracting document information:', JSON.stringify(truncateFields(e)));
342
- throw e;
353
+ logger.log(`Extraction job started: ${jobId}. Polling for results...`);
354
+
355
+ // 4. Poll the Status Endpoint
356
+ const statusUrl = `${this.textExtractionServiceURL}/extract_doc_information/status/${jobId}`;
357
+
358
+ // Poll every 2 seconds, up to 15 times (30 seconds max)
359
+ const maxRetries = 15;
360
+ for (let i = 0; i < maxRetries; i++) {
361
+ // Wait 2 seconds before checking
362
+ await new Promise(resolve => setTimeout(resolve, 2000));
363
+
364
+ const statusRes = await axios.get(statusUrl, {
365
+ headers: { 'Authorization': `Bearer ${token}` }
366
+ });
367
+
368
+ const statusData = statusRes.data;
369
+
370
+ if (statusData.status === 'completed') {
371
+ logger.log('Extraction completed successfully', JSON.stringify(truncateFields(statusData.result)));
372
+ return statusData.result; // 👈 Returns the actual extracted data
373
+ }
374
+
375
+ if (statusData.status === 'failed') {
376
+ throw new Error(statusData.error || "Document extraction failed during processing.");
377
+ }
378
+
379
+ // If 'processing', loop continues...
380
+ }
381
+
382
+ throw new Error("Document extraction timed out waiting for results.");
383
+
384
+ } catch (e: any) {
385
+ const errorMsg = e.response?.data?.detail || e.message;
386
+ logger.error('Error extracting document information:', errorMsg);
387
+ throw new Error(errorMsg);
343
388
  }
344
389
  }
345
390
 
@@ -412,24 +457,14 @@ export class KYCService {
412
457
  card_obb: { x: 50, y: 50, width: 200, height: 200 }
413
458
  };
414
459
  }
415
-
416
460
  const { fileUri, docType, docRegion, postfix = 'back', token, template_path, mrz_type } = params;
417
-
418
- // 1. Build the FormData ONLY for the file
419
- const formData = new FormData();
420
- const filePayload = {
421
- uri: fileUri,
422
- type: 'image/jpeg',
423
- name: `id_card_${postfix}.jpg`,
424
- };
425
- formData.append('file', filePayload as any);
426
461
 
462
+ // ... top of extractMrzText (params extraction, etc) ...
427
463
  const docTypeShorted = GovernmentDocumentTypeShorted[docType as GovernmentDocumentType] || docType;
428
464
  const safeMrzType = mrz_type && mrz_type.trim() !== '' ? mrz_type : 'TD1';
429
465
 
430
466
  logger.log("docTypeShorted", docTypeShorted, docRegion, postfix);
431
467
 
432
- // 🚨 THE FIX: Pass all required text parameters in the URL query string for FastAPI
433
468
  const queryParams = new URLSearchParams({
434
469
  doc_type: docTypeShorted,
435
470
  doc_region: docRegion,
@@ -443,14 +478,52 @@ export class KYCService {
443
478
  logger.log("url", url);
444
479
 
445
480
  try {
446
- const response = await fetch(url, {
447
- method: 'POST',
448
- headers: {
449
- 'Authorization': `Bearer ${token}`,
450
- 'Accept': 'application/json'
451
- },
452
- body: formData, // Only the file goes in the body
453
- });
481
+ let response;
482
+
483
+ if (Platform.OS === 'web') {
484
+ // 🚨 THE FIX: Completely bypass React Native Web's networking polyfills.
485
+ // We explicitly grab the browser's native APIs so the bundler cannot interfere.
486
+ const nativeFetch = window.fetch;
487
+ const NativeFormData = window.FormData;
488
+
489
+ const formData = new NativeFormData();
490
+
491
+ // Fetch the Blob natively
492
+ const fileReq = await nativeFetch(fileUri);
493
+ const blob = await fileReq.blob();
494
+
495
+ // Append the Blob. The native browser will perfectly handle the multipart boundary.
496
+ formData.append('file', blob, `id_card_${postfix}.jpg`);
497
+
498
+ // Execute using Native Fetch!
499
+ response = await nativeFetch(url, {
500
+ method: 'POST',
501
+ headers: {
502
+ 'Authorization': `Bearer ${token}`,
503
+ 'Accept': 'application/json'
504
+ },
505
+ body: formData,
506
+ });
507
+
508
+ } else {
509
+ // 📱 MOBILE NATIVE: React Native's Native Bridge expects this exact object
510
+ const formData = new FormData();
511
+ const filePayload = {
512
+ uri: fileUri,
513
+ type: 'image/jpeg',
514
+ name: `id_card_${postfix}.jpg`,
515
+ };
516
+ formData.append('file', filePayload as any);
517
+
518
+ response = await fetch(url, {
519
+ method: 'POST',
520
+ headers: {
521
+ 'Authorization': `Bearer ${token}`,
522
+ 'Accept': 'application/json'
523
+ },
524
+ body: formData,
525
+ });
526
+ }
454
527
 
455
528
  if (!response.ok) {
456
529
  const errorText = await response.text();
@@ -458,19 +531,11 @@ export class KYCService {
458
531
  throw new Error(`Erreur serveur: ${response.status}`);
459
532
  }
460
533
 
461
- // const data = await response.json();
462
- // logger.log('extractMrzText res', JSON.stringify(data, null, 2));
463
-
464
- // if (Object.keys(data).length === 0) throw new Error('No data found');
465
- // if (data?.success === false) throw new Error(data.parsed_data?.status || 'Échec de l\'extraction MRZ');
466
-
467
- // return data;
468
- const data = await response.json();
534
+ const data = await response.json();
469
535
  logger.log('extractMrzText res', JSON.stringify(data, null, 2));
470
536
 
471
537
  if (Object.keys(data).length === 0) throw new Error('No data found');
472
538
 
473
- // 🚨 UPDATE THIS LINE to grab the actual status_message:
474
539
  if (data?.success === false) {
475
540
  const serverMessage = data.parsed_data?.status_message || data.parsed_data?.status || 'Échec de l\'extraction MRZ';
476
541
  throw new Error(`Lecture MRZ refusée: ${serverMessage}`);
@@ -559,41 +624,53 @@ export class KYCService {
559
624
  }
560
625
 
561
626
  async verificationSession(payload: VerificationSessionRequest): Promise<any> {
562
- console.log('apiKey in verificationSession', payload.apiKey);
563
627
  try {
564
628
  const { session_id, step, data, templateId, action, apiKey } = payload;
629
+
630
+ // 1. Safe Auth Resolution
565
631
  const token = apiKey ? undefined : await authentification();
632
+
633
+ if (!apiKey && !token) {
634
+ throw new Error("Authentication failed: No API key or Token available.");
635
+ }
636
+
637
+
566
638
  const payloadData = {
567
639
  action: action,
568
640
  data: {
569
- step,
570
- templateId,
571
- ...(action === "location_permission" ? { permissionGranted: true, } : {}),
572
- ...data,
641
+ ...data,
642
+ step: step,
643
+ templateId: templateId,
644
+ ...(action === "location_permission" ? { permissionGranted: true } : {}),
573
645
  },
574
- ...({ session_id: session_id }),
575
- timestamp: new Date().toISOString()
576
- }
646
+ session_id: session_id,
647
+ timestamp: new Date().toISOString(),
648
+ };
649
+
577
650
  const url = `${KYCConfig.getBackendUrl()}/verification/api/kyc/sessions/${session_id}/steps/${step}/`;
578
651
 
652
+ // 3. Clean Logging
579
653
  const logPayload = truncateFields({ payloadData, session_id, step });
580
- logger.log('verificationSession payload',
581
- JSON.stringify(truncateFields({ logPayload, token: token ?? "-", path: url, apiKey }),
582
- null, 2))
583
-
584
- const res = await axios.post<SessionResponse>(url,
585
- payloadData,
586
- {
654
+ logger.log('verificationSession payload', JSON.stringify({
655
+ url,
656
+ apiKey: apiKey ? '***HIDDEN***' : 'None', // Don't log the raw API key!
657
+ token: token ? '***HIDDEN***' : 'None',
658
+ payload: logPayload
659
+ }, null, 2));
660
+
661
+ // 4. API Execution
662
+ const res = await axios.post<SessionResponse>(url, payloadData, {
587
663
  headers: {
588
664
  'Content-Type': 'application/json',
589
665
  ...(apiKey ? { 'Authorization': `ApiKey ${apiKey}` } : { 'Authorization': `Bearer ${token}` })
590
666
  }
591
- });
667
+ });
668
+
592
669
  logger.log('verificationSession res', JSON.stringify(truncateFields(res.data), null, 2));
593
670
  return res.data;
594
671
 
595
672
  } catch (error) {
596
- logger.error('Error validating component:', JSON.stringify(error, null, 2));
673
+ logger.error('Error validating component:', JSON.stringify(errorMessage(error), null, 2));
597
674
  throw new Error(errorMessage(error));
598
675
  }
599
676
  }
@@ -738,6 +815,54 @@ export class KYCService {
738
815
  throw error;
739
816
  }
740
817
  }
818
+
819
+ // Inside KYCService.ts
820
+
821
+ async validateBackTemplate(params: {
822
+ region: string;
823
+ docType: string;
824
+ frontTemplateName: string;
825
+ backTemplateName: string;
826
+ subRegion: string;
827
+ token: string;
828
+ }, env: KycEnvironment = 'PRODUCTION'): Promise<any> {
829
+ if (env === 'SANDBOX') {
830
+ logger.log("SANDBOX mode: Skipping back template validation");
831
+ return { is_match: true, status_message: "SANDBOX: Valid Match" };
832
+ }
833
+
834
+ const { region, docType, frontTemplateName, backTemplateName, subRegion, token } = params;
835
+
836
+ // 🚨 THE FIX: Format the payload as URL Query Parameters
837
+ const queryParams = new URLSearchParams({
838
+ region: region,
839
+ doc_type: docType,
840
+ front_template_name: frontTemplateName,
841
+ back_template_name: backTemplateName,
842
+ sub_region: subRegion || ""
843
+ }).toString();
844
+
845
+ const url = `${this.mrzServiceURL}/validate_back_template/?${queryParams}`;
846
+
847
+ logger.log('validateBackTemplate URL', url);
848
+
849
+ try {
850
+ // Pass `null` for the body since the data is now in the URL
851
+ const res = await axios.post(url, null, {
852
+ headers: {
853
+ 'Accept': 'application/json',
854
+ 'Authorization': `Bearer ${token}`
855
+ },
856
+ timeout: 20000,
857
+ });
858
+ logger.log('validateBackTemplate res', JSON.stringify(res.data, null, 2));
859
+ return res.data;
860
+ } catch (e: any) {
861
+ const errorMsg = e.response?.data?.detail || e.response?.data?.Error || e.message;
862
+ logger.error('Error validating back template:', JSON.stringify(errorMsg));
863
+ throw new Error(typeof errorMsg === 'string' ? errorMsg : 'Template validation failed');
864
+ }
865
+ }
741
866
  }
742
867
 
743
868
  const kycService = new KYCService("", "");
@@ -782,8 +907,6 @@ export const authentification = async (): Promise<string> => {
782
907
  activeAuthRequest = (async () => {
783
908
  logger.log('Authenticating: Fetching new Keycloak token...');
784
909
 
785
- // 🚨 FIX: Do NOT use URLSearchParams in React Native with Axios.
786
- // Use a raw URL-encoded string to guarantee Keycloak understands the payload.
787
910
  const params = 'client_id=kyc_frontend&client_secret=QhgAmvKgmwODzsEp98dnA4PeUEMMaFHd&grant_type=client_credentials';
788
911
 
789
912
  const res = await axios.post(
@@ -111,6 +111,7 @@ export function WebKYCEntry({
111
111
  onCancel,
112
112
  onSessionSave,
113
113
  }: WebKYCEntryProps): JSX.Element {
114
+ console.log('first')
114
115
  const { setLocale } = useI18n();
115
116
  const [urlParams, setUrlParams] = useState<URLParams>({});
116
117
  const [isLoading, setIsLoading] = useState(true);
@@ -204,7 +205,6 @@ export function WebKYCEntry({
204
205
  }
205
206
  }, [urlParams]);
206
207
 
207
- // Handle KYC completion
208
208
  const handleComplete = useCallback(async (data: any) => {
209
209
  console.log('KYC completed:', data);
210
210
 
@@ -214,15 +214,48 @@ export function WebKYCEntry({
214
214
  // Handle Push URL webhook if provided
215
215
  if (urlParams.push_url) {
216
216
  try {
217
+ const innerResult = data?.result?.result || data?.result || data;
218
+ const nestedDataMap = innerResult?.data;
219
+
220
+ let kycSessionId = data?.session_id || urlParams.kyc_id || '';
221
+ let parsedKycData: any = null;
222
+
223
+ if (nestedDataMap) {
224
+ const mapKeys = Object.keys(nestedDataMap);
225
+ if (mapKeys.length > 0) {
226
+ const detectedSessionId = mapKeys.find(key => key.startsWith('kyc-')) || mapKeys[0];
227
+ if (detectedSessionId) {
228
+ kycSessionId = detectedSessionId;
229
+ }
230
+
231
+ const rawData = nestedDataMap[kycSessionId]?.data;
232
+
233
+ if (typeof rawData === 'string') {
234
+ try {
235
+ parsedKycData = JSON.parse(rawData);
236
+ } catch (parseErr) {
237
+ console.warn('Failed parsing inner stringified JSON string:', parseErr);
238
+ parsedKycData = rawData;
239
+ }
240
+ } else {
241
+ parsedKycData = rawData;
242
+ }
243
+ }
244
+ }
245
+
246
+ const finalSessionId = kycSessionId || parsedKycData?.session_id || urlParams.kyc_id || 'unknown';
247
+
248
+ // 3. Build your exact un-nested payload signature
217
249
  const verificationState = {
218
- status: 'success', // verification completed successfully
219
- result: data, // pass complete data object as result
220
- reference_id: urlParams.reference_id,
250
+ session_id: finalSessionId,
251
+ status: 'success',
252
+ message: innerResult?.message || 'Verification data retrieved successfully.',
253
+ data: parsedKycData || {}
221
254
  };
255
+
222
256
  await postWebhook(urlParams.push_url, verificationState, 5000);
223
- console.log('Successfully pushed KYC data to webhook');
257
+ console.log('Successfully pushed beautifully clean KYC data payload to webhook');
224
258
  } catch (err) {
225
- // Fail open - redirect anyway even if webhook fails
226
259
  console.warn('Failed to push data to webhook URL:', err);
227
260
  }
228
261
  }
@@ -238,6 +271,7 @@ export function WebKYCEntry({
238
271
  liveness_checked: data.livenessCheckComplete || false,
239
272
  },
240
273
  });
274
+
241
275
  onComplete?.(data);
242
276
  }, [redirectToReturnUrl, urlParams, onComplete]);
243
277
 
@@ -311,6 +345,7 @@ export function WebKYCEntry({
311
345
  useEffect(() => {
312
346
  try {
313
347
  const params = parseUrlParams();
348
+
314
349
  setUrlParams(params);
315
350
 
316
351
  // Set language if provided
@@ -447,4 +482,4 @@ const styles = StyleSheet.create({
447
482
  },
448
483
  });
449
484
 
450
- export default WebKYCEntry;
485
+ export default WebKYCEntry;