@sanctum-key/react-native-sdk 1.0.41 → 1.0.44

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.
@@ -1,5 +1,5 @@
1
1
  import React, { useEffect, useMemo, useRef, useState } from 'react';
2
- import { View, Text, StyleSheet, Image, ScrollView, Platform, ActivityIndicator, TouchableOpacity, useWindowDimensions } from 'react-native';
2
+ import { View, Text, StyleSheet, Image, ScrollView, Platform, ActivityIndicator, TouchableOpacity, useWindowDimensions, Alert } from 'react-native';
3
3
  import { showAlert } from '../../utils/platformAlert';
4
4
  import { EnhancedCameraView } from '../EnhancedCameraView';
5
5
  import { TemplateComponent, LocalizedText, GovernmentDocumentType, ISilentCaptureResult, IBbox, GovernmentDocumentTypeShorted, GovernmentDocumentTypeBackend } from '../../types/KYC.types';
@@ -15,7 +15,9 @@ import { cropByObb, getObbConfidence, OBB_CONFIDENCE_THRESHOLD } from '../../uti
15
15
  import REGION_MAPPING from '../../config/region_mapping.json';
16
16
  import QRCode from 'react-native-qrcode-svg';
17
17
  import { cropImageWithBBoxWithTolerance } from '../../utils/cropByObb';
18
- import kycService, { authentification } from '../../modules/api/KYCService';
18
+ import kycService, { authentification, clearAuthToken } from '../../modules/api/KYCService';
19
+ import { Linking } from 'react-native';
20
+ import { Camera } from 'react-native-vision-camera';
19
21
 
20
22
  const ISO_TO_COUNTRY_NAME: Record<string, string> = {
21
23
  'KE': 'Kenya', 'CM': 'Cameroon', 'NG': 'Nigeria', 'CA': 'Canada',
@@ -97,6 +99,7 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
97
99
  const { actions, state, env, apiKey } = useTemplateKYCFlowContext();
98
100
  const { width: screenWidth, height: screenHeight } = useWindowDimensions();
99
101
 
102
+
100
103
  const countrySelectionData = useMemo(() => {
101
104
  const countrySelectionComponent = state.template.components.find(c => c.type === 'country_selection');
102
105
  return countrySelectionComponent ? state.componentData[countrySelectionComponent.id] : null;
@@ -212,6 +215,58 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
212
215
  }
213
216
  }, [value, currentSide]);
214
217
 
218
+ const handleStartCamera = async () => {
219
+ // Web browsers handle permissions natively when the media stream is requested
220
+ if (Platform.OS === 'web') {
221
+ setShowCamera(true);
222
+ refreshCamera();
223
+ actions.showCustomStepper(false);
224
+ return;
225
+ }
226
+
227
+ try {
228
+ // 1. Get current status
229
+ let status = await Camera.getCameraPermissionStatus();
230
+
231
+ // 2. If it's anything other than granted, request it
232
+ if (status !== 'granted') {
233
+ status = await Camera.requestCameraPermission();
234
+ }
235
+
236
+ // 3. Check final status
237
+ if (status === 'granted') {
238
+ // Permission given, start camera
239
+ setShowCamera(true);
240
+ refreshCamera();
241
+ actions.showCustomStepper(false);
242
+ } else {
243
+ // Permission permanently denied or restricted
244
+ Alert.alert(
245
+ state.currentLanguage === 'en' ? 'Camera Access Required' : 'Accès à la caméra requis',
246
+ state.currentLanguage === 'en'
247
+ ? 'We need camera access to scan your ID. Please tap "Settings" to enable it.'
248
+ : 'Nous avons besoin de l\'accès à la caméra pour scanner votre pièce d\'identité. Veuillez appuyer sur "Paramètres" pour l\'activer.',
249
+ [
250
+ {
251
+ text: state.currentLanguage === 'en' ? 'Cancel' : 'Annuler',
252
+ style: 'cancel'
253
+ },
254
+ {
255
+ text: state.currentLanguage === 'en' ? 'Settings' : 'Paramètres',
256
+ onPress: () => Linking.openSettings()
257
+ }
258
+ ]
259
+ );
260
+ }
261
+ } catch (error) {
262
+ console.warn('Failed to request camera permission', error);
263
+ // Fallback
264
+ setShowCamera(true);
265
+ refreshCamera();
266
+ actions.showCustomStepper(false);
267
+ }
268
+ };
269
+
215
270
  const cameraConfig = useMemo(() => {
216
271
  let instructions = selectedDocumentType ? (locale === 'en' ? getDocumentTypeInfo(selectedDocumentType.type).instructions.en : getDocumentTypeInfo(selectedDocumentType.type).instructions.fr) : getLocalizedText(component.instructions as Record<string, LocalizedText>);
217
272
  if (scanPhase === 'barcode') instructions = '';
@@ -407,12 +462,37 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
407
462
  }
408
463
  };
409
464
 
465
+ const handleExitKYCFlow = () => {
466
+ clearAuthToken();
467
+
468
+ // 2. Clear all local camera/capture states
469
+ setCapturedImages({});
470
+ if (onValueChange) onValueChange({}); // Clear the parent's form state
471
+ setCurrentSide('front');
472
+ setScanPhase('image');
473
+ setSilentCaptureResult({ success: false, isAnalyzing: false, path: '', error: '' });
474
+ scannedBarcodeRef.current = null;
475
+ setPendingCAImage(null);
476
+ setIsProcessingCapture(false);
477
+ setProcessingImagePath(null);
478
+
479
+ // 3. Restart the flow
480
+ if (actions.resetTemplate) {
481
+ // This is the best method: it wipes the context and rebuilds the initial state
482
+ actions.resetTemplate();
483
+ } else if (actions.goToComponent && state.template?.components?.length > 0) {
484
+ actions.goToComponent(state.template.components[0].id);
485
+ } else {
486
+ console.warn("Could not restart flow. Check your KYC context actions.");
487
+ }
488
+ };
489
+
410
490
  const handleSilentCapture = async (result: { success: boolean; path?: string; error?: string; barcode?: unknown }) => {
411
491
  if (isProcessingCaptureRef.current) return;
412
492
  if (scanPhaseRef.current === 'barcode') return;
413
493
  if (isAnalyzingRef.current) return;
414
494
 
415
- // USE REFS TO AVOID IOS NATIVE STALE CLOSURE
495
+
416
496
  const side = currentSideRef.current;
417
497
  const captured = capturedImagesRef.current;
418
498
  const docType = selectedDocTypeRef.current;
@@ -510,41 +590,59 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
510
590
  } catch (error: any) {
511
591
  clearTimeout(watchdog);
512
592
 
513
- const rawMessage = (error?.message || '').toLowerCase();
593
+ const rawMessage = (typeof error === 'string' ? error : (error?.message || '')).toLowerCase();
594
+ const statusCode = error?.response?.status || error?.status;
595
+
596
+ if (
597
+ statusCode === 401 ||
598
+ statusCode === 403 ||
599
+ rawMessage.includes('401') ||
600
+ rawMessage.includes('unauthorized') ||
601
+ rawMessage.includes('token') ||
602
+ rawMessage.includes('unauthenticated')
603
+ ) {
604
+ setShowCamera(false);
605
+ setIsProcessingCapture(false);
606
+ setSilentCaptureResult(prev => ({ ...prev, isAnalyzing: false, success: false, error: '' }));
607
+
608
+ // Use React Native's native Alert to wait for the user's tap
609
+ Alert.alert(
610
+ state.currentLanguage === 'en' ? 'Session Expired' : 'Session Expirée',
611
+ state.currentLanguage === 'en'
612
+ ? 'Your secure session has expired. Please restart the process.'
613
+ : 'Votre session sécurisée a expiré. Veuillez recommencer.',
614
+ [
615
+ {
616
+ text: state.currentLanguage === 'en' ? 'Restart' : 'Recommencer',
617
+ onPress: () => handleExitKYCFlow() // Redirects ONLY when they click the button
618
+ }
619
+ ],
620
+ { cancelable: false } // Forces them to click the button
621
+ );
622
+
623
+ return;
624
+ }
625
+
514
626
  let hintMessage = '';
515
627
 
628
+ // 2. Business Logic Errors
516
629
  if (rawMessage.includes('pays sélectionné') || rawMessage.includes('country mismatch')) {
517
- hintMessage = state.currentLanguage === 'en'
518
- ? 'Does not match the selected country.'
519
- : 'Ne correspond pas au pays sélectionné.';
520
- }
521
- else if (rawMessage.includes('impossible') || rawMessage.includes('unreadable')) {
522
- hintMessage = state.currentLanguage === 'en'
523
- ? 'Move to better lighting.'
524
- : 'Améliorez l\'éclairage.';
525
- }
526
- else if (rawMessage.includes('card_not_fully_in_frame') || rawMessage.includes('frame')) {
527
- hintMessage = state.currentLanguage === 'en'
528
- ? 'Move closer to the document.'
529
- : 'Rapprochez-vous du document.';
530
- }
531
- else if (rawMessage.includes('ne correspond pas au recto')) {
532
- hintMessage = state.currentLanguage === 'en'
533
- ? 'Please scan the same ID.'
534
- : 'Veuillez scanner la même pièce.';
630
+ hintMessage = state.currentLanguage === 'en' ? 'Does not match the selected country.' : 'Ne correspond pas au pays sélectionné.';
631
+ } else if (rawMessage.includes('impossible') || rawMessage.includes('unreadable')) {
632
+ hintMessage = state.currentLanguage === 'en' ? 'Move to better lighting and ensure card is in frame.' : 'Améliorez l\'éclairage et assurez-vous que la carte est bien cadrée.';
633
+ } else if (rawMessage.includes('card_not_fully_in_frame') || rawMessage.includes('frame')) {
634
+ hintMessage = state.currentLanguage === 'en' ? 'Move closer to the document.' : 'Rapprochez-vous du document.';
635
+ } else if (rawMessage.includes('ne correspond pas au recto')) {
636
+ hintMessage = state.currentLanguage === 'en' ? 'Please scan the same ID.' : 'Veuillez scanner la même pièce.';
535
637
  }
536
- // 2. If it's not a known logical error, assume it's a network/API failure
638
+ // 3. Fallback to Network/API failure
537
639
  else {
538
640
  const isNetworkError = error.isAxiosError || error.message === 'Network Error' || error.code === 'ECONNABORTED' || rawMessage.includes('network') || !error.response;
539
641
 
540
642
  if (isNetworkError) {
541
- hintMessage = state.currentLanguage === 'en'
542
- ? 'Poor connection. Retrying...'
543
- : 'Connexion faible. Nouvelle tentative...';
643
+ hintMessage = state.currentLanguage === 'en' ? 'Poor connection. Retrying...' : 'Connexion faible. Nouvelle tentative...';
544
644
  } else {
545
- hintMessage = state.currentLanguage === 'en'
546
- ? 'Analyzing...' // Generic fallback if an unknown API error occurs
547
- : 'Analyse en cours...';
645
+ hintMessage = state.currentLanguage === 'en' ? 'Analyzing...' : 'Analyse en cours...';
548
646
  }
549
647
  }
550
648
 
@@ -724,7 +822,13 @@ export const IDCardCapture: React.FC<IDCardCaptureProps> = ({ component, value =
724
822
  </>
725
823
  ) : (
726
824
  <>
727
- <Button title={state.currentLanguage === "en" ? "Start Scanning" : "Commencer la numérisation"} onPress={() => { setShowCamera(true); refreshCamera(); actions.showCustomStepper(false); }} variant="primary" size="large" fullWidth />
825
+ <Button
826
+ title={state.currentLanguage === "en" ? "Start Scanning" : "Commencer la numérisation"}
827
+ onPress={handleStartCamera}
828
+ variant="primary"
829
+ size="large"
830
+ fullWidth
831
+ />
728
832
  {Platform.OS === 'web' && ( <Button title={state.currentLanguage === "en" ? "Continue on Phone" : "Continuer sur le téléphone"} onPress={() => setShowQRModal(true)} variant="outline" size="large" fullWidth /> )}
729
833
  </>
730
834
  )}
@@ -3,7 +3,7 @@ import { cropByObb, getObbConfidence, OBB_CONFIDENCE_THRESHOLD } from "../../uti
3
3
  import { GovernmentDocumentType, IBbox } from "../../types/KYC.types";
4
4
  import { KycEnvironment } from "../../types/env.types";
5
5
  import { logger } from "../../utils/logger";
6
- // import { countryData } from "../../config/countriesData";
6
+ import { countryData } from "../../config/countriesData";
7
7
 
8
8
  interface CardObbData {
9
9
  obb: number[][];
@@ -301,11 +301,18 @@ export async function backVerification(
301
301
  const activeTemplatePath = templateResponse.template_path || result.templatePath || '';
302
302
 
303
303
  if (activeTemplatePath) {
304
- // Note: Make sure ISO_TO_COUNTRY_NAME is properly defined or imported if needed here
305
- const hasCodeMatch = activeTemplatePath.includes(`_${result.code}_`);
306
- // Assuming you have countryData available, otherwise you can rely just on CodeMatch
307
- if (!hasCodeMatch && !activeTemplatePath.toLowerCase().includes(result.code.toLowerCase())) {
308
- logger.log(`Template mismatch! Expected region: ${result.code}, Detected: ${activeTemplatePath}`);
304
+ const countryInfo = countryData[result.code];
305
+
306
+ const countryName = countryInfo?.name_en || countryInfo?.name || '';
307
+
308
+ const hasCodeMatch = activeTemplatePath.toUpperCase().includes(`_${result.code.toUpperCase()}_`) ||
309
+ activeTemplatePath.toUpperCase().includes(`${result.code.toUpperCase()}/`);
310
+
311
+ const hasNameMatch = countryName && activeTemplatePath.toLowerCase().includes(countryName.toLowerCase());
312
+
313
+ // 5. If NEITHER match, throw the error
314
+ if (!hasCodeMatch && !hasNameMatch) {
315
+ logger.log(`Template mismatch! Expected region: ${result.code} (${countryName}), Detected: ${activeTemplatePath}`);
309
316
  throw new Error(`Le document ne correspond pas au pays sélectionné (${result.code}).`);
310
317
  }
311
318
  }
@@ -499,6 +506,9 @@ export async function checkTemplateType(
499
506
 
500
507
  } catch (e: any) {
501
508
  logger.log('Error checking template type:', JSON.stringify(errorMessage(e), null, 2));
509
+ if (e?.response?.status === 401 || e?.response?.status === 403 || e?.isAxiosError) {
510
+ throw e;
511
+ }
502
512
  if (e?.message === 'CARD_NOT_FULLY_IN_FRAME' || e?.message?.includes('ne correspond pas')) throw e;
503
513
  throw new Error(e?.message || 'Erreur de vérification du template');
504
514
  }
@@ -488,6 +488,10 @@ export const authentification = async (): Promise<string> => {
488
488
  }
489
489
  }
490
490
 
491
+ export const clearAuthToken = () => {
492
+ cachedToken = null; // Clear memory cache
493
+ };
494
+
491
495
  export const errorMessage = (error: any) => {
492
496
  return error.response?.data?.message || error.response?.data?.detail || error.response?.data?.error || error.response?.data || error.message;
493
497
  }