@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
package/build/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"IDCardCapture.d.ts","sourceRoot":"","sources":["../../../../src/components/KYCElements/IDCardCapture.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA+C,MAAM,OAAO,CAAC;AAIpE,OAAO,EAAE,iBAAiB,EAAoI,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"IDCardCapture.d.ts","sourceRoot":"","sources":["../../../../src/components/KYCElements/IDCardCapture.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA+C,MAAM,OAAO,CAAC;AAIpE,OAAO,EAAE,iBAAiB,EAAoI,MAAM,uBAAuB,CAAC;AAuB5L,UAAU,cAAc;IACtB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,UAAU,kBAAkB;IAC1B,SAAS,EAAE,iBAAiB,CAAC;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;IACvC,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,GAAG,MAAM,CAAC,KAAK,IAAI,CAAC;IACxE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAkCD,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC,kBAAkB,CAo2BtD,CAAC"}
|
|
@@ -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 { 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
|
const ISO_TO_COUNTRY_NAME = {
|
|
20
22
|
'KE': 'Kenya', 'CM': 'Cameroon', 'NG': 'Nigeria', 'CA': 'Canada',
|
|
21
23
|
'FR': 'France', 'GH': 'Ghana', 'ZA': 'South Africa', 'GB': 'Britain',
|
|
@@ -189,6 +191,52 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
189
191
|
}
|
|
190
192
|
}
|
|
191
193
|
}, [value, currentSide]);
|
|
194
|
+
const handleStartCamera = async () => {
|
|
195
|
+
// Web browsers handle permissions natively when the media stream is requested
|
|
196
|
+
if (Platform.OS === 'web') {
|
|
197
|
+
setShowCamera(true);
|
|
198
|
+
refreshCamera();
|
|
199
|
+
actions.showCustomStepper(false);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
try {
|
|
203
|
+
// 1. Get current status
|
|
204
|
+
let status = await Camera.getCameraPermissionStatus();
|
|
205
|
+
// 2. If it's anything other than granted, request it
|
|
206
|
+
if (status !== 'granted') {
|
|
207
|
+
status = await Camera.requestCameraPermission();
|
|
208
|
+
}
|
|
209
|
+
// 3. Check final status
|
|
210
|
+
if (status === 'granted') {
|
|
211
|
+
// Permission given, start camera
|
|
212
|
+
setShowCamera(true);
|
|
213
|
+
refreshCamera();
|
|
214
|
+
actions.showCustomStepper(false);
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
// Permission permanently denied or restricted
|
|
218
|
+
Alert.alert(state.currentLanguage === 'en' ? 'Camera Access Required' : 'Accès à la caméra requis', state.currentLanguage === 'en'
|
|
219
|
+
? 'We need camera access to scan your ID. Please tap "Settings" to enable it.'
|
|
220
|
+
: '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.', [
|
|
221
|
+
{
|
|
222
|
+
text: state.currentLanguage === 'en' ? 'Cancel' : 'Annuler',
|
|
223
|
+
style: 'cancel'
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
text: state.currentLanguage === 'en' ? 'Settings' : 'Paramètres',
|
|
227
|
+
onPress: () => Linking.openSettings()
|
|
228
|
+
}
|
|
229
|
+
]);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
catch (error) {
|
|
233
|
+
console.warn('Failed to request camera permission', error);
|
|
234
|
+
// Fallback
|
|
235
|
+
setShowCamera(true);
|
|
236
|
+
refreshCamera();
|
|
237
|
+
actions.showCustomStepper(false);
|
|
238
|
+
}
|
|
239
|
+
};
|
|
192
240
|
const cameraConfig = useMemo(() => {
|
|
193
241
|
let instructions = selectedDocumentType ? (locale === 'en' ? getDocumentTypeInfo(selectedDocumentType.type).instructions.en : getDocumentTypeInfo(selectedDocumentType.type).instructions.fr) : getLocalizedText(component.instructions);
|
|
194
242
|
if (scanPhase === 'barcode')
|
|
@@ -397,6 +445,31 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
397
445
|
setSilentCaptureResult(prev => ({ ...prev, isAnalyzing: false, success: false, error: 'Failed to process barcode. Please retake the photo.' }));
|
|
398
446
|
}
|
|
399
447
|
};
|
|
448
|
+
const handleExitKYCFlow = () => {
|
|
449
|
+
clearAuthToken();
|
|
450
|
+
// 2. Clear all local camera/capture states
|
|
451
|
+
setCapturedImages({});
|
|
452
|
+
if (onValueChange)
|
|
453
|
+
onValueChange({}); // Clear the parent's form state
|
|
454
|
+
setCurrentSide('front');
|
|
455
|
+
setScanPhase('image');
|
|
456
|
+
setSilentCaptureResult({ success: false, isAnalyzing: false, path: '', error: '' });
|
|
457
|
+
scannedBarcodeRef.current = null;
|
|
458
|
+
setPendingCAImage(null);
|
|
459
|
+
setIsProcessingCapture(false);
|
|
460
|
+
setProcessingImagePath(null);
|
|
461
|
+
// 3. Restart the flow
|
|
462
|
+
if (actions.resetTemplate) {
|
|
463
|
+
// This is the best method: it wipes the context and rebuilds the initial state
|
|
464
|
+
actions.resetTemplate();
|
|
465
|
+
}
|
|
466
|
+
else if (actions.goToComponent && state.template?.components?.length > 0) {
|
|
467
|
+
actions.goToComponent(state.template.components[0].id);
|
|
468
|
+
}
|
|
469
|
+
else {
|
|
470
|
+
console.warn("Could not restart flow. Check your KYC context actions.");
|
|
471
|
+
}
|
|
472
|
+
};
|
|
400
473
|
const handleSilentCapture = async (result) => {
|
|
401
474
|
if (isProcessingCaptureRef.current)
|
|
402
475
|
return;
|
|
@@ -404,7 +477,6 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
404
477
|
return;
|
|
405
478
|
if (isAnalyzingRef.current)
|
|
406
479
|
return;
|
|
407
|
-
// USE REFS TO AVOID IOS NATIVE STALE CLOSURE
|
|
408
480
|
const side = currentSideRef.current;
|
|
409
481
|
const captured = capturedImagesRef.current;
|
|
410
482
|
const docType = selectedDocTypeRef.current;
|
|
@@ -497,40 +569,51 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
497
569
|
}
|
|
498
570
|
catch (error) {
|
|
499
571
|
clearTimeout(watchdog);
|
|
500
|
-
const rawMessage = (error?.message || '').toLowerCase();
|
|
572
|
+
const rawMessage = (typeof error === 'string' ? error : (error?.message || '')).toLowerCase();
|
|
573
|
+
const statusCode = error?.response?.status || error?.status;
|
|
574
|
+
if (statusCode === 401 ||
|
|
575
|
+
statusCode === 403 ||
|
|
576
|
+
rawMessage.includes('401') ||
|
|
577
|
+
rawMessage.includes('unauthorized') ||
|
|
578
|
+
rawMessage.includes('token') ||
|
|
579
|
+
rawMessage.includes('unauthenticated')) {
|
|
580
|
+
setShowCamera(false);
|
|
581
|
+
setIsProcessingCapture(false);
|
|
582
|
+
setSilentCaptureResult(prev => ({ ...prev, isAnalyzing: false, success: false, error: '' }));
|
|
583
|
+
// Use React Native's native Alert to wait for the user's tap
|
|
584
|
+
Alert.alert(state.currentLanguage === 'en' ? 'Session Expired' : 'Session Expirée', state.currentLanguage === 'en'
|
|
585
|
+
? 'Your secure session has expired. Please restart the process.'
|
|
586
|
+
: 'Votre session sécurisée a expiré. Veuillez recommencer.', [
|
|
587
|
+
{
|
|
588
|
+
text: state.currentLanguage === 'en' ? 'Restart' : 'Recommencer',
|
|
589
|
+
onPress: () => handleExitKYCFlow() // Redirects ONLY when they click the button
|
|
590
|
+
}
|
|
591
|
+
], { cancelable: false } // Forces them to click the button
|
|
592
|
+
);
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
501
595
|
let hintMessage = '';
|
|
596
|
+
// 2. Business Logic Errors
|
|
502
597
|
if (rawMessage.includes('pays sélectionné') || rawMessage.includes('country mismatch')) {
|
|
503
|
-
hintMessage = state.currentLanguage === 'en'
|
|
504
|
-
? 'Does not match the selected country.'
|
|
505
|
-
: 'Ne correspond pas au pays sélectionné.';
|
|
598
|
+
hintMessage = state.currentLanguage === 'en' ? 'Does not match the selected country.' : 'Ne correspond pas au pays sélectionné.';
|
|
506
599
|
}
|
|
507
600
|
else if (rawMessage.includes('impossible') || rawMessage.includes('unreadable')) {
|
|
508
|
-
hintMessage = state.currentLanguage === 'en'
|
|
509
|
-
? 'Move to better lighting.'
|
|
510
|
-
: 'Améliorez l\'éclairage.';
|
|
601
|
+
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.';
|
|
511
602
|
}
|
|
512
603
|
else if (rawMessage.includes('card_not_fully_in_frame') || rawMessage.includes('frame')) {
|
|
513
|
-
hintMessage = state.currentLanguage === 'en'
|
|
514
|
-
? 'Move closer to the document.'
|
|
515
|
-
: 'Rapprochez-vous du document.';
|
|
604
|
+
hintMessage = state.currentLanguage === 'en' ? 'Move closer to the document.' : 'Rapprochez-vous du document.';
|
|
516
605
|
}
|
|
517
606
|
else if (rawMessage.includes('ne correspond pas au recto')) {
|
|
518
|
-
hintMessage = state.currentLanguage === 'en'
|
|
519
|
-
? 'Please scan the same ID.'
|
|
520
|
-
: 'Veuillez scanner la même pièce.';
|
|
607
|
+
hintMessage = state.currentLanguage === 'en' ? 'Please scan the same ID.' : 'Veuillez scanner la même pièce.';
|
|
521
608
|
}
|
|
522
|
-
//
|
|
609
|
+
// 3. Fallback to Network/API failure
|
|
523
610
|
else {
|
|
524
611
|
const isNetworkError = error.isAxiosError || error.message === 'Network Error' || error.code === 'ECONNABORTED' || rawMessage.includes('network') || !error.response;
|
|
525
612
|
if (isNetworkError) {
|
|
526
|
-
hintMessage = state.currentLanguage === 'en'
|
|
527
|
-
? 'Poor connection. Retrying...'
|
|
528
|
-
: 'Connexion faible. Nouvelle tentative...';
|
|
613
|
+
hintMessage = state.currentLanguage === 'en' ? 'Poor connection. Retrying...' : 'Connexion faible. Nouvelle tentative...';
|
|
529
614
|
}
|
|
530
615
|
else {
|
|
531
|
-
hintMessage = state.currentLanguage === 'en'
|
|
532
|
-
? 'Analyzing...' // Generic fallback if an unknown API error occurs
|
|
533
|
-
: 'Analyse en cours...';
|
|
616
|
+
hintMessage = state.currentLanguage === 'en' ? 'Analyzing...' : 'Analyse en cours...';
|
|
534
617
|
}
|
|
535
618
|
}
|
|
536
619
|
setSilentCaptureResult(prev => ({ ...prev, isAnalyzing: false, success: false, error: hintMessage }));
|
|
@@ -684,7 +767,7 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
684
767
|
</View>
|
|
685
768
|
<Button title={state.currentLanguage === "en" ? "Continue on Phone" : "Continuer sur le téléphone"} onPress={() => setShowQRModal(true)} variant="primary" size="large" fullWidth/>
|
|
686
769
|
</>) : (<>
|
|
687
|
-
|
|
770
|
+
<Button title={state.currentLanguage === "en" ? "Start Scanning" : "Commencer la numérisation"} onPress={handleStartCamera} variant="primary" size="large" fullWidth/>
|
|
688
771
|
{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/>)}
|
|
689
772
|
</>)}
|
|
690
773
|
</View>)}
|