@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.
- package/build/package.json +1 -1
- package/build/src/components/KYCElements/IDCardCapture.d.ts.map +1 -1
- package/build/src/components/KYCElements/IDCardCapture.js +107 -24
- package/build/src/components/KYCElements/IDCardCapture.js.map +1 -1
- package/build/src/modules/api/CardAuthentification.d.ts.map +1 -1
- package/build/src/modules/api/CardAuthentification.js +12 -5
- package/build/src/modules/api/CardAuthentification.js.map +1 -1
- package/build/src/modules/api/KYCService.d.ts +1 -0
- package/build/src/modules/api/KYCService.d.ts.map +1 -1
- package/build/src/modules/api/KYCService.js +3 -0
- package/build/src/modules/api/KYCService.js.map +1 -1
- package/package.json +1 -1
- package/src/components/KYCElements/IDCardCapture.tsx +134 -30
- package/src/modules/api/CardAuthentification.ts +16 -6
- package/src/modules/api/KYCService.ts +4 -0
|
@@ -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
|
-
|
|
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
|
-
|
|
519
|
-
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
}
|