@hexar/biometric-identity-sdk-react-native 1.0.16 → 1.0.18
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/dist/components/BiometricIdentityFlow.d.ts.map +1 -1
- package/dist/components/BiometricIdentityFlow.js +40 -39
- package/dist/components/VideoRecorder.d.ts.map +1 -1
- package/dist/components/VideoRecorder.js +27 -35
- package/dist/hooks/useBiometricSDK.d.ts.map +1 -1
- package/dist/hooks/useBiometricSDK.js +18 -5
- package/package.json +1 -1
- package/src/components/BiometricIdentityFlow.tsx +41 -41
- package/src/components/VideoRecorder.tsx +28 -37
- package/src/hooks/useBiometricSDK.ts +17 -5
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BiometricIdentityFlow.d.ts","sourceRoot":"","sources":["../../src/components/BiometricIdentityFlow.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAmD,MAAM,OAAO,CAAC;AACxE,OAAO,EAOL,SAAS,EACV,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,gBAAgB,EAChB,WAAW,EACX,cAAc,EAKd,iBAAiB,EAElB,MAAM,oCAAoC,CAAC;AAU5C,MAAM,WAAW,0BAA0B;IACzC,oBAAoB,EAAE,CAAC,MAAM,EAAE,gBAAgB,KAAK,IAAI,CAAC;IACzD,OAAO,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,CAAC;IACzC,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IAC7B,kBAAkB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5C,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IACvB,MAAM,CAAC,EAAE;QACP,SAAS,CAAC,EAAE,SAAS,CAAC;QACtB,OAAO,CAAC,EAAE,SAAS,CAAC;KACrB,CAAC;CACH;AAED,eAAO,MAAM,qBAAqB,EAAE,KAAK,CAAC,EAAE,CAAC,0BAA0B,
|
|
1
|
+
{"version":3,"file":"BiometricIdentityFlow.d.ts","sourceRoot":"","sources":["../../src/components/BiometricIdentityFlow.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAmD,MAAM,OAAO,CAAC;AACxE,OAAO,EAOL,SAAS,EACV,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,gBAAgB,EAChB,WAAW,EACX,cAAc,EAKd,iBAAiB,EAElB,MAAM,oCAAoC,CAAC;AAU5C,MAAM,WAAW,0BAA0B;IACzC,oBAAoB,EAAE,CAAC,MAAM,EAAE,gBAAgB,KAAK,IAAI,CAAC;IACzD,OAAO,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,CAAC;IACzC,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IAC7B,kBAAkB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5C,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IACvB,MAAM,CAAC,EAAE;QACP,SAAS,CAAC,EAAE,SAAS,CAAC;QACtB,OAAO,CAAC,EAAE,SAAS,CAAC;KACrB,CAAC;CACH;AAED,eAAO,MAAM,qBAAqB,EAAE,KAAK,CAAC,EAAE,CAAC,0BAA0B,CAiWtE,CAAC;AA8NF,eAAe,qBAAqB,CAAC"}
|
|
@@ -55,49 +55,37 @@ const BiometricIdentityFlow = ({ onValidationComplete, onError, theme, language,
|
|
|
55
55
|
const [showInstructions, setShowInstructions] = (0, react_1.useState)(true);
|
|
56
56
|
const [currentChallenges, setCurrentChallenges] = (0, react_1.useState)([]);
|
|
57
57
|
const [isLoadingChallenges, setIsLoadingChallenges] = (0, react_1.useState)(false);
|
|
58
|
-
// Use refs to store callbacks to avoid dependency issues in useEffect
|
|
59
58
|
const onValidationCompleteRef = (0, react_1.useRef)(onValidationComplete);
|
|
60
59
|
const onErrorRef = (0, react_1.useRef)(onError);
|
|
61
60
|
const hasCalledValidationComplete = (0, react_1.useRef)(false);
|
|
62
|
-
// Update refs when callbacks change
|
|
63
61
|
(0, react_1.useEffect)(() => {
|
|
64
62
|
onValidationCompleteRef.current = onValidationComplete;
|
|
65
63
|
onErrorRef.current = onError;
|
|
66
64
|
}, [onValidationComplete, onError]);
|
|
67
|
-
// Set language early, before any components render
|
|
68
|
-
// Priority: language prop > SDK config language > default 'en'
|
|
69
|
-
// Run on mount and whenever language prop changes
|
|
70
65
|
(0, react_1.useEffect)(() => {
|
|
71
66
|
if (language) {
|
|
72
|
-
// If language prop is provided, override the config
|
|
73
67
|
(0, biometric_identity_sdk_core_1.setLanguage)(language);
|
|
74
68
|
}
|
|
75
|
-
// If no language prop, the language should already be set by BiometricIdentitySDK.configure()
|
|
76
|
-
// The global language state is set when configure() is called, so getStrings() will
|
|
77
|
-
// automatically return strings for the configured language (or 'en' as default)
|
|
78
69
|
}, [language]);
|
|
79
70
|
const strings = (0, biometric_identity_sdk_core_1.getStrings)();
|
|
80
71
|
const styles = createStyles(theme);
|
|
81
|
-
// Handle validation result - call callback when RESULT step is reached
|
|
82
72
|
(0, react_1.useEffect)(() => {
|
|
73
|
+
console.log('🔵 [BiometricIdentityFlow] useEffect triggered - currentStep:', state.currentStep, 'hasResult:', !!state.validationResult, 'hasCalled:', hasCalledValidationComplete.current);
|
|
83
74
|
if (state.currentStep === biometric_identity_sdk_core_1.SDKStep.RESULT && state.validationResult && !hasCalledValidationComplete.current) {
|
|
75
|
+
console.log('🟢 [BiometricIdentityFlow] Calling onValidationComplete callback');
|
|
84
76
|
biometric_identity_sdk_core_1.logger.info('Validation completed, calling onValidationComplete callback');
|
|
85
77
|
hasCalledValidationComplete.current = true;
|
|
86
|
-
// Use ref to avoid dependency issues
|
|
87
78
|
onValidationCompleteRef.current(state.validationResult);
|
|
88
79
|
}
|
|
89
80
|
}, [state.currentStep, state.validationResult]);
|
|
90
|
-
// Reset the flag when validation result changes or component resets
|
|
91
81
|
(0, react_1.useEffect)(() => {
|
|
92
82
|
if (state.currentStep !== biometric_identity_sdk_core_1.SDKStep.RESULT) {
|
|
93
83
|
hasCalledValidationComplete.current = false;
|
|
94
84
|
}
|
|
95
85
|
}, [state.currentStep]);
|
|
96
|
-
// Handle error
|
|
97
86
|
(0, react_1.useEffect)(() => {
|
|
98
87
|
if (state.error) {
|
|
99
88
|
biometric_identity_sdk_core_1.logger.error('SDK error detected:', state.error);
|
|
100
|
-
// Use ref to avoid dependency issues
|
|
101
89
|
onErrorRef.current(state.error);
|
|
102
90
|
}
|
|
103
91
|
}, [state.error]);
|
|
@@ -106,7 +94,6 @@ const BiometricIdentityFlow = ({ onValidationComplete, onError, theme, language,
|
|
|
106
94
|
*/
|
|
107
95
|
const handleCaptureStart = (0, react_1.useCallback)(async (mode) => {
|
|
108
96
|
setCameraMode(mode);
|
|
109
|
-
// If video mode, fetch challenges first
|
|
110
97
|
if (mode === 'video' && smartLivenessMode && isUsingBackend) {
|
|
111
98
|
setIsLoadingChallenges(true);
|
|
112
99
|
try {
|
|
@@ -120,7 +107,6 @@ const BiometricIdentityFlow = ({ onValidationComplete, onError, theme, language,
|
|
|
120
107
|
setIsLoadingChallenges(false);
|
|
121
108
|
}
|
|
122
109
|
else if (mode === 'video' && smartLivenessMode) {
|
|
123
|
-
// Use default challenges for offline mode
|
|
124
110
|
setCurrentChallenges(sdk.getDefaultChallenges());
|
|
125
111
|
}
|
|
126
112
|
setShowCamera(true);
|
|
@@ -147,29 +133,55 @@ const BiometricIdentityFlow = ({ onValidationComplete, onError, theme, language,
|
|
|
147
133
|
challengesCompleted: videoResult.challengesCompleted,
|
|
148
134
|
sessionId: videoResult.sessionId,
|
|
149
135
|
});
|
|
136
|
+
console.log('🔵 [BiometricIdentityFlow] Video stored, starting validation...');
|
|
150
137
|
biometric_identity_sdk_core_1.logger.info('Starting validation...');
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
138
|
+
let result = null;
|
|
139
|
+
try {
|
|
140
|
+
result = await validateIdentity();
|
|
141
|
+
console.log('🟢 [BiometricIdentityFlow] Validation completed:', result);
|
|
142
|
+
biometric_identity_sdk_core_1.logger.info('Validation completed successfully:', result);
|
|
143
|
+
}
|
|
144
|
+
catch (validationError) {
|
|
145
|
+
console.error('❌ [BiometricIdentityFlow] Validation error:', validationError);
|
|
146
|
+
biometric_identity_sdk_core_1.logger.error('Validation error:', validationError);
|
|
147
|
+
throw validationError; // Re-throw to be caught by outer catch
|
|
148
|
+
}
|
|
155
149
|
const finalState = sdk.getState();
|
|
150
|
+
console.log('🔵 [BiometricIdentityFlow] Final state after validation:', JSON.stringify({
|
|
151
|
+
currentStep: finalState.currentStep,
|
|
152
|
+
hasResult: !!finalState.validationResult,
|
|
153
|
+
progress: finalState.progress,
|
|
154
|
+
isLoading: finalState.isLoading
|
|
155
|
+
}));
|
|
156
156
|
biometric_identity_sdk_core_1.logger.info('Final state after validation:', finalState);
|
|
157
|
-
|
|
157
|
+
if (result && !hasCalledValidationComplete.current) {
|
|
158
|
+
console.log('🟢 [BiometricIdentityFlow] Calling onValidationComplete directly with result');
|
|
159
|
+
biometric_identity_sdk_core_1.logger.info('Calling onValidationComplete directly with validation result');
|
|
160
|
+
hasCalledValidationComplete.current = true;
|
|
161
|
+
onValidationCompleteRef.current(result);
|
|
162
|
+
}
|
|
158
163
|
if (finalState.currentStep === biometric_identity_sdk_core_1.SDKStep.RESULT && finalState.validationResult && !hasCalledValidationComplete.current) {
|
|
159
|
-
|
|
164
|
+
console.log('🟢 [BiometricIdentityFlow] Backup: Calling onValidationComplete from state');
|
|
165
|
+
biometric_identity_sdk_core_1.logger.info('Backup: Calling onValidationComplete from state');
|
|
160
166
|
hasCalledValidationComplete.current = true;
|
|
161
167
|
onValidationCompleteRef.current(finalState.validationResult);
|
|
162
168
|
}
|
|
163
169
|
}
|
|
164
170
|
}
|
|
165
171
|
catch (error) {
|
|
172
|
+
console.error('❌ [BiometricIdentityFlow] Capture/validation error:', error);
|
|
166
173
|
biometric_identity_sdk_core_1.logger.error('Capture/validation error:', error);
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
174
|
+
if (error && typeof error === 'object' && 'code' in error) {
|
|
175
|
+
onErrorRef.current(error);
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
const biometricError = {
|
|
179
|
+
name: 'BiometricError',
|
|
180
|
+
message: error instanceof Error ? error.message : 'Unknown error during capture',
|
|
181
|
+
code: 'LIVENESS_CHECK_FAILED',
|
|
182
|
+
};
|
|
183
|
+
onErrorRef.current(biometricError);
|
|
184
|
+
}
|
|
173
185
|
}
|
|
174
186
|
}, [cameraMode, uploadFrontID, uploadBackID, storeVideoRecording, validateIdentity, sdk]);
|
|
175
187
|
/**
|
|
@@ -181,18 +193,15 @@ const BiometricIdentityFlow = ({ onValidationComplete, onError, theme, language,
|
|
|
181
193
|
setCurrentChallenges([]);
|
|
182
194
|
setShowInstructions(true);
|
|
183
195
|
}, [reset]);
|
|
184
|
-
// Show loading while initializing
|
|
185
196
|
if (!isInitialized) {
|
|
186
197
|
return (react_1.default.createElement(react_native_1.SafeAreaView, { style: [styles.container, customStyles?.container] },
|
|
187
198
|
react_1.default.createElement(react_native_1.View, { style: styles.loadingFullScreen },
|
|
188
199
|
react_1.default.createElement(react_native_1.ActivityIndicator, { size: "large", color: theme?.primaryColor || '#6366F1' }),
|
|
189
200
|
react_1.default.createElement(react_native_1.Text, { style: styles.loadingText }, strings.initialization.initializing))));
|
|
190
201
|
}
|
|
191
|
-
// Show instructions on first load
|
|
192
202
|
if (showInstructions) {
|
|
193
203
|
return (react_1.default.createElement(InstructionsScreen_1.InstructionsScreen, { theme: theme, language: language, onStart: () => setShowInstructions(false), routeBack: routeBack, styles: customStyles }));
|
|
194
204
|
}
|
|
195
|
-
// Show camera/video recorder
|
|
196
205
|
if (showCamera) {
|
|
197
206
|
if (cameraMode === 'video') {
|
|
198
207
|
return (react_1.default.createElement(VideoRecorder_1.VideoRecorder, { theme: theme, language: language, challenges: currentChallenges, smartMode: smartLivenessMode, sessionId: sdk.getSessionId() || undefined, onComplete: handleCaptureComplete, onCancel: () => setShowCamera(false), onFetchChallenges: async () => {
|
|
@@ -202,23 +211,16 @@ const BiometricIdentityFlow = ({ onValidationComplete, onError, theme, language,
|
|
|
202
211
|
}
|
|
203
212
|
return (react_1.default.createElement(CameraCapture_1.CameraCapture, { mode: cameraMode, theme: theme, language: language, onCapture: handleCaptureComplete, onCancel: () => setShowCamera(false) }));
|
|
204
213
|
}
|
|
205
|
-
// Show result (check this first, even if step is still VALIDATING but result exists)
|
|
206
|
-
// This handles edge cases where state transition might be delayed
|
|
207
214
|
if (state.validationResult && (state.currentStep === biometric_identity_sdk_core_1.SDKStep.RESULT || state.progress >= 100)) {
|
|
208
|
-
// If we have a result but step is still VALIDATING, force transition to RESULT
|
|
209
215
|
if (state.currentStep === biometric_identity_sdk_core_1.SDKStep.VALIDATING && state.progress >= 100) {
|
|
210
216
|
biometric_identity_sdk_core_1.logger.info('Result available but step still VALIDATING, showing ResultScreen');
|
|
211
217
|
}
|
|
212
218
|
return (react_1.default.createElement(ResultScreen_1.ResultScreen, { result: state.validationResult, theme: theme, language: language, onClose: () => {
|
|
213
|
-
// Callback already called automatically when RESULT step was reached
|
|
214
|
-
// This onClose is just for UI cleanup/navigation
|
|
215
219
|
} }));
|
|
216
220
|
}
|
|
217
|
-
// Show validation progress
|
|
218
221
|
if (state.currentStep === biometric_identity_sdk_core_1.SDKStep.VALIDATING) {
|
|
219
222
|
return (react_1.default.createElement(ValidationProgress_1.ValidationProgress, { progress: state.progress, theme: theme, language: language }));
|
|
220
223
|
}
|
|
221
|
-
// Show error
|
|
222
224
|
if (state.currentStep === biometric_identity_sdk_core_1.SDKStep.ERROR && state.error) {
|
|
223
225
|
return (react_1.default.createElement(ErrorScreen_1.ErrorScreen, { error: state.error, theme: theme, language: language, onRetry: handleRetry, onClose: () => onError(state.error) }));
|
|
224
226
|
}
|
|
@@ -323,7 +325,6 @@ const buttonStyles = react_native_1.StyleSheet.create({
|
|
|
323
325
|
marginTop: 4,
|
|
324
326
|
},
|
|
325
327
|
});
|
|
326
|
-
// Create styles based on theme
|
|
327
328
|
const createStyles = (theme) => {
|
|
328
329
|
const backgroundColor = theme?.backgroundColor || '#FFFFFF';
|
|
329
330
|
const textColor = theme?.textColor || '#000000';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VideoRecorder.d.ts","sourceRoot":"","sources":["../../src/components/VideoRecorder.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAmD,MAAM,OAAO,CAAC;AAaxE,OAAO,EAAE,WAAW,EAAE,mBAAmB,EAAE,iBAAiB,EAAmC,MAAM,oCAAoC,CAAC;
|
|
1
|
+
{"version":3,"file":"VideoRecorder.d.ts","sourceRoot":"","sources":["../../src/components/VideoRecorder.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAmD,MAAM,OAAO,CAAC;AAaxE,OAAO,EAAE,WAAW,EAAE,mBAAmB,EAAE,iBAAiB,EAAmC,MAAM,oCAAoC,CAAC;AAE1I,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IAC7B,sEAAsE;IACtE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,sCAAsC;IACtC,YAAY,CAAC,EAAE,mBAAmB,EAAE,CAAC;IACrC,0CAA0C;IAC1C,UAAU,CAAC,EAAE,eAAe,EAAE,CAAC;IAC/B,wCAAwC;IACxC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,kDAAkD;IAClD,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,wCAAwC;IACxC,UAAU,EAAE,CAAC,SAAS,EAAE,oBAAoB,KAAK,IAAI,CAAC;IACtD,iCAAiC;IACjC,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,gDAAgD;IAChD,iBAAiB,CAAC,EAAE,MAAM,OAAO,CAAC,eAAe,EAAE,CAAC,CAAC;CACtD;AAED,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,oBAAoB,EAAE,OAAO,CAAC;IAC9B,YAAY,EAAE,MAAM,CAAC;IACrB,mBAAmB,EAAE,MAAM,EAAE,CAAC;IAC9B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AA8CD,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC,kBAAkB,CA2xBtD,CAAC;AA4OF,eAAe,aAAa,CAAC"}
|
|
@@ -43,32 +43,31 @@ const react_native_1 = require("react-native");
|
|
|
43
43
|
const react_native_vision_camera_1 = require("react-native-vision-camera");
|
|
44
44
|
const react_native_permissions_1 = require("react-native-permissions");
|
|
45
45
|
const biometric_identity_sdk_core_1 = require("@hexar/biometric-identity-sdk-core");
|
|
46
|
-
|
|
47
|
-
const DEFAULT_CHALLENGES = [
|
|
46
|
+
const getDefaultChallenges = (strings) => [
|
|
48
47
|
{
|
|
49
48
|
action: 'look_left',
|
|
50
|
-
instruction: 'Slowly turn your head to the LEFT',
|
|
49
|
+
instruction: strings.liveness.instructions.lookLeft || 'Slowly turn your head to the LEFT',
|
|
51
50
|
duration_ms: 2500,
|
|
52
51
|
order: 1,
|
|
53
52
|
icon: '←',
|
|
54
53
|
},
|
|
55
54
|
{
|
|
56
55
|
action: 'look_right',
|
|
57
|
-
instruction: 'Slowly turn your head to the RIGHT',
|
|
56
|
+
instruction: strings.liveness.instructions.lookRight || 'Slowly turn your head to the RIGHT',
|
|
58
57
|
duration_ms: 2500,
|
|
59
58
|
order: 2,
|
|
60
59
|
icon: '→',
|
|
61
60
|
},
|
|
62
61
|
{
|
|
63
62
|
action: 'blink',
|
|
64
|
-
instruction: 'Blink your eyes naturally',
|
|
63
|
+
instruction: strings.liveness.instructions.blink || 'Blink your eyes naturally',
|
|
65
64
|
duration_ms: 2000,
|
|
66
65
|
order: 3,
|
|
67
66
|
icon: '👁',
|
|
68
67
|
},
|
|
69
68
|
{
|
|
70
69
|
action: 'smile',
|
|
71
|
-
instruction: 'Smile 😊',
|
|
70
|
+
instruction: strings.liveness.instructions.smile || 'Smile 😊',
|
|
72
71
|
duration_ms: 2000,
|
|
73
72
|
order: 4,
|
|
74
73
|
icon: '😊',
|
|
@@ -87,11 +86,12 @@ const getInstructionMap = (strings) => ({
|
|
|
87
86
|
stay_still: { text: strings.liveness.instructions.stayStill || 'Look at the camera and follow the instructions', icon: '📷' },
|
|
88
87
|
});
|
|
89
88
|
const VideoRecorder = ({ theme, language, duration, instructions, challenges: propChallenges, sessionId, smartMode = true, onComplete, onCancel, onFetchChallenges, }) => {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
89
|
+
(0, react_1.useEffect)(() => {
|
|
90
|
+
if (language) {
|
|
91
|
+
(0, biometric_identity_sdk_core_1.setLanguage)(language);
|
|
92
|
+
}
|
|
93
|
+
}, [language]);
|
|
93
94
|
const strings = (0, biometric_identity_sdk_core_1.getStrings)();
|
|
94
|
-
// State
|
|
95
95
|
const [phase, setPhase] = (0, react_1.useState)('loading');
|
|
96
96
|
const [countdown, setCountdown] = (0, react_1.useState)(3);
|
|
97
97
|
const [challenges, setChallenges] = (0, react_1.useState)([]);
|
|
@@ -101,11 +101,9 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
|
|
|
101
101
|
const [completedChallenges, setCompletedChallenges] = (0, react_1.useState)([]);
|
|
102
102
|
const [frames, setFrames] = (0, react_1.useState)([]);
|
|
103
103
|
const [hasPermission, setHasPermission] = (0, react_1.useState)(false);
|
|
104
|
-
// Camera
|
|
105
104
|
const cameraRef = (0, react_1.useRef)(null);
|
|
106
105
|
const { hasPermission: cameraPermission, requestPermission } = (0, react_native_vision_camera_1.useCameraPermission)();
|
|
107
106
|
const device = (0, react_native_vision_camera_1.useCameraDevice)('front');
|
|
108
|
-
// Animations
|
|
109
107
|
const fadeAnim = (0, react_1.useRef)(new react_native_1.Animated.Value(0)).current;
|
|
110
108
|
const scaleAnim = (0, react_1.useRef)(new react_native_1.Animated.Value(1)).current;
|
|
111
109
|
const pulseAnim = (0, react_1.useRef)(new react_native_1.Animated.Value(1)).current;
|
|
@@ -119,19 +117,15 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
|
|
|
119
117
|
const recordingTimeoutRef = (0, react_1.useRef)(null);
|
|
120
118
|
const minDurationMs = 8000;
|
|
121
119
|
const totalDuration = duration || Math.max(minDurationMs, challenges.reduce((sum, c) => sum + c.duration_ms, 0) + 2000);
|
|
122
|
-
// Check camera permissions
|
|
123
120
|
(0, react_1.useEffect)(() => {
|
|
124
121
|
const checkPermissions = async () => {
|
|
125
122
|
try {
|
|
126
|
-
// First check if we already have permission from the hook
|
|
127
123
|
if (cameraPermission) {
|
|
128
124
|
setHasPermission(true);
|
|
129
125
|
return;
|
|
130
126
|
}
|
|
131
|
-
// Otherwise, request permission using the hook
|
|
132
127
|
const granted = await requestPermission();
|
|
133
128
|
setHasPermission(granted);
|
|
134
|
-
// If hook method didn't work, try platform-specific request
|
|
135
129
|
if (!granted) {
|
|
136
130
|
if (react_native_1.Platform.OS === 'ios') {
|
|
137
131
|
const result = await (0, react_native_permissions_1.request)(react_native_permissions_1.PERMISSIONS.IOS.CAMERA);
|
|
@@ -151,21 +145,28 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
|
|
|
151
145
|
};
|
|
152
146
|
checkPermissions();
|
|
153
147
|
}, [cameraPermission, requestPermission]);
|
|
154
|
-
// Initialize challenges
|
|
155
148
|
(0, react_1.useEffect)(() => {
|
|
156
149
|
const initChallenges = async () => {
|
|
157
150
|
try {
|
|
158
151
|
let challengeList;
|
|
152
|
+
const currentStrings = (0, biometric_identity_sdk_core_1.getStrings)();
|
|
153
|
+
const instructionMap = getInstructionMap(currentStrings);
|
|
159
154
|
if (propChallenges && propChallenges.length > 0) {
|
|
160
|
-
|
|
161
|
-
|
|
155
|
+
challengeList = propChallenges.map(challenge => ({
|
|
156
|
+
...challenge,
|
|
157
|
+
instruction: instructionMap[challenge.action]?.text || challenge.instruction,
|
|
158
|
+
icon: instructionMap[challenge.action]?.icon || challenge.icon,
|
|
159
|
+
}));
|
|
162
160
|
}
|
|
163
161
|
else if (onFetchChallenges) {
|
|
164
|
-
|
|
165
|
-
challengeList =
|
|
162
|
+
const fetchedChallenges = await onFetchChallenges();
|
|
163
|
+
challengeList = fetchedChallenges.map(challenge => ({
|
|
164
|
+
...challenge,
|
|
165
|
+
instruction: instructionMap[challenge.action]?.text || challenge.instruction,
|
|
166
|
+
icon: instructionMap[challenge.action]?.icon || challenge.icon,
|
|
167
|
+
}));
|
|
166
168
|
}
|
|
167
169
|
else if (instructions && instructions.length > 0) {
|
|
168
|
-
const instructionMap = getInstructionMap(strings);
|
|
169
170
|
challengeList = instructions.map((inst, idx) => ({
|
|
170
171
|
action: inst,
|
|
171
172
|
instruction: instructionMap[inst]?.text || inst,
|
|
@@ -175,11 +176,10 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
|
|
|
175
176
|
}));
|
|
176
177
|
}
|
|
177
178
|
else {
|
|
178
|
-
|
|
179
|
-
challengeList = smartMode ? DEFAULT_CHALLENGES : [
|
|
179
|
+
challengeList = smartMode ? getDefaultChallenges(currentStrings) : [
|
|
180
180
|
{
|
|
181
181
|
action: 'stay_still',
|
|
182
|
-
instruction: 'Look at the camera and stay still',
|
|
182
|
+
instruction: currentStrings.liveness.instructions.stayStill || 'Look at the camera and stay still',
|
|
183
183
|
duration_ms: duration || 5000,
|
|
184
184
|
order: 1,
|
|
185
185
|
icon: '📷',
|
|
@@ -191,19 +191,16 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
|
|
|
191
191
|
}
|
|
192
192
|
catch (error) {
|
|
193
193
|
biometric_identity_sdk_core_1.logger.error('Failed to fetch challenges:', error);
|
|
194
|
-
|
|
195
|
-
setChallenges(DEFAULT_CHALLENGES);
|
|
194
|
+
setChallenges(getDefaultChallenges((0, biometric_identity_sdk_core_1.getStrings)()));
|
|
196
195
|
setPhase('countdown');
|
|
197
196
|
}
|
|
198
197
|
};
|
|
199
198
|
initChallenges();
|
|
200
|
-
}, [propChallenges, instructions, onFetchChallenges, smartMode, duration]);
|
|
201
|
-
// Countdown phase
|
|
199
|
+
}, [propChallenges, instructions, onFetchChallenges, smartMode, duration, language]);
|
|
202
200
|
(0, react_1.useEffect)(() => {
|
|
203
201
|
if (phase !== 'countdown')
|
|
204
202
|
return;
|
|
205
203
|
if (countdown > 0) {
|
|
206
|
-
// Animate countdown number
|
|
207
204
|
react_native_1.Animated.sequence([
|
|
208
205
|
react_native_1.Animated.timing(scaleAnim, {
|
|
209
206
|
toValue: 1.3,
|
|
@@ -223,7 +220,6 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
|
|
|
223
220
|
startRecording();
|
|
224
221
|
}
|
|
225
222
|
}, [countdown, phase]);
|
|
226
|
-
// Start pulse animation for recording indicator
|
|
227
223
|
(0, react_1.useEffect)(() => {
|
|
228
224
|
if (phase === 'recording') {
|
|
229
225
|
react_native_1.Animated.loop(react_native_1.Animated.sequence([
|
|
@@ -242,7 +238,6 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
|
|
|
242
238
|
])).start();
|
|
243
239
|
}
|
|
244
240
|
}, [phase]);
|
|
245
|
-
// Animate arrow for directional challenges
|
|
246
241
|
const animateArrow = (0, react_1.useCallback)((direction) => {
|
|
247
242
|
const toValue = direction.includes('left') ? -20 : direction.includes('right') ? 20 : 0;
|
|
248
243
|
react_native_1.Animated.loop(react_native_1.Animated.sequence([
|
|
@@ -356,7 +351,6 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
|
|
|
356
351
|
}
|
|
357
352
|
}
|
|
358
353
|
catch (error) {
|
|
359
|
-
// Silent frame capture errors
|
|
360
354
|
}
|
|
361
355
|
}, 100);
|
|
362
356
|
}
|
|
@@ -546,9 +540,7 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
|
|
|
546
540
|
}
|
|
547
541
|
};
|
|
548
542
|
}, [device, totalDuration, minDurationMs, handleVideoComplete, handleRecordingError, runChallenge, stopRecording, startFrameCapture]);
|
|
549
|
-
// Current challenge
|
|
550
543
|
const currentChallenge = challenges[currentChallengeIndex];
|
|
551
|
-
// Get direction arrow for the current challenge
|
|
552
544
|
const getDirectionIndicator = () => {
|
|
553
545
|
if (!currentChallenge)
|
|
554
546
|
return null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useBiometricSDK.d.ts","sourceRoot":"","sources":["../../src/hooks/useBiometricSDK.ts"],"names":[],"mappings":"AACA,OAAO,EACL,oBAAoB,EACpB,QAAQ,EACR,WAAW,EAGZ,MAAM,oCAAoC,CAAC;AAE5C,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,qBAAqB;IACpC,GAAG,EAAE,oBAAoB,CAAC;IAC1B,KAAK,EAAE,QAAQ,CAAC;IAChB,aAAa,EAAE,OAAO,CAAC;IACvB,cAAc,EAAE,OAAO,CAAC;IACxB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,UAAU,EAAE,eAAe,EAAE,CAAC;IAC9B,aAAa,EAAE,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAClE,YAAY,EAAE,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACjE,mBAAmB,EAAE,CAAC,SAAS,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/D,eAAe,EAAE,CAAC,IAAI,CAAC,EAAE,QAAQ,GAAG,SAAS,KAAK,OAAO,CAAC,eAAe,EAAE,CAAC,CAAC;IAC7E,gBAAgB,EAAE,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC;IACrC,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,WAAW,EAAE,CAAC,SAAS,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACzD;AAED,eAAO,MAAM,eAAe,QAAO,
|
|
1
|
+
{"version":3,"file":"useBiometricSDK.d.ts","sourceRoot":"","sources":["../../src/hooks/useBiometricSDK.ts"],"names":[],"mappings":"AACA,OAAO,EACL,oBAAoB,EACpB,QAAQ,EACR,WAAW,EAGZ,MAAM,oCAAoC,CAAC;AAE5C,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,qBAAqB;IACpC,GAAG,EAAE,oBAAoB,CAAC;IAC1B,KAAK,EAAE,QAAQ,CAAC;IAChB,aAAa,EAAE,OAAO,CAAC;IACvB,cAAc,EAAE,OAAO,CAAC;IACxB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,UAAU,EAAE,eAAe,EAAE,CAAC;IAC9B,aAAa,EAAE,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAClE,YAAY,EAAE,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACjE,mBAAmB,EAAE,CAAC,SAAS,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/D,eAAe,EAAE,CAAC,IAAI,CAAC,EAAE,QAAQ,GAAG,SAAS,KAAK,OAAO,CAAC,eAAe,EAAE,CAAC,CAAC;IAC7E,gBAAgB,EAAE,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC;IACrC,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,WAAW,EAAE,CAAC,SAAS,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACzD;AAED,eAAO,MAAM,eAAe,QAAO,qBA4QlC,CAAC;AAEF,eAAe,eAAe,CAAC"}
|
|
@@ -137,6 +137,7 @@ const useBiometricSDK = () => {
|
|
|
137
137
|
* Validate identity with all collected data
|
|
138
138
|
*/
|
|
139
139
|
const validateIdentity = (0, react_1.useCallback)(async () => {
|
|
140
|
+
console.log('🔵 [useBiometricSDK] validateIdentity called');
|
|
140
141
|
biometric_identity_sdk_core_1.logger.info('validateIdentity called, current state:', sdk.getState());
|
|
141
142
|
setState(sdk.getState());
|
|
142
143
|
const pollInterval = setInterval(() => {
|
|
@@ -145,13 +146,16 @@ const useBiometricSDK = () => {
|
|
|
145
146
|
setState(currentState);
|
|
146
147
|
}
|
|
147
148
|
}, 200);
|
|
149
|
+
let finalPollInterval = null;
|
|
148
150
|
try {
|
|
151
|
+
console.log('🔵 [useBiometricSDK] Calling sdk.validateIdentity()...');
|
|
149
152
|
const result = await sdk.validateIdentity();
|
|
153
|
+
console.log('🟢 [useBiometricSDK] sdk.validateIdentity() completed, result:', result);
|
|
150
154
|
// Keep polling for a bit to ensure we capture the RESULT state transition
|
|
151
155
|
// The SDK updates state synchronously, but we want to make sure React sees it
|
|
152
156
|
let pollCount = 0;
|
|
153
157
|
const maxPolls = 10; // Poll for up to 2 seconds (10 * 200ms)
|
|
154
|
-
|
|
158
|
+
finalPollInterval = setInterval(() => {
|
|
155
159
|
pollCount++;
|
|
156
160
|
if (isMounted.current) {
|
|
157
161
|
const currentState = sdk.getState();
|
|
@@ -159,13 +163,16 @@ const useBiometricSDK = () => {
|
|
|
159
163
|
setState(currentState);
|
|
160
164
|
// Stop polling once we reach RESULT or ERROR state
|
|
161
165
|
if (currentState.currentStep === biometric_identity_sdk_core_1.SDKStep.RESULT || currentState.currentStep === biometric_identity_sdk_core_1.SDKStep.ERROR) {
|
|
162
|
-
|
|
166
|
+
console.log('🟢 [useBiometricSDK] Reached final state:', currentState.currentStep);
|
|
167
|
+
if (finalPollInterval)
|
|
168
|
+
clearInterval(finalPollInterval);
|
|
163
169
|
clearInterval(pollInterval);
|
|
164
170
|
biometric_identity_sdk_core_1.logger.info('Reached final state, stopped polling:', currentState);
|
|
165
171
|
}
|
|
166
172
|
else if (pollCount >= maxPolls) {
|
|
167
173
|
// Force stop after max polls
|
|
168
|
-
|
|
174
|
+
if (finalPollInterval)
|
|
175
|
+
clearInterval(finalPollInterval);
|
|
169
176
|
clearInterval(pollInterval);
|
|
170
177
|
const finalState = sdk.getState();
|
|
171
178
|
biometric_identity_sdk_core_1.logger.warn('Max polls reached, forcing final state:', finalState);
|
|
@@ -180,17 +187,23 @@ const useBiometricSDK = () => {
|
|
|
180
187
|
setState(immediateState);
|
|
181
188
|
// If we're already at RESULT, clear intervals immediately
|
|
182
189
|
if (immediateState.currentStep === biometric_identity_sdk_core_1.SDKStep.RESULT || immediateState.currentStep === biometric_identity_sdk_core_1.SDKStep.ERROR) {
|
|
183
|
-
|
|
190
|
+
if (finalPollInterval)
|
|
191
|
+
clearInterval(finalPollInterval);
|
|
184
192
|
clearInterval(pollInterval);
|
|
185
193
|
}
|
|
186
194
|
}
|
|
187
195
|
return result;
|
|
188
196
|
}
|
|
189
197
|
catch (error) {
|
|
198
|
+
// Clear all intervals on error
|
|
190
199
|
clearInterval(pollInterval);
|
|
200
|
+
if (finalPollInterval) {
|
|
201
|
+
clearInterval(finalPollInterval);
|
|
202
|
+
}
|
|
203
|
+
console.error('❌ [useBiometricSDK] Validation error:', error);
|
|
204
|
+
biometric_identity_sdk_core_1.logger.error('Validation error, state:', sdk.getState(), error);
|
|
191
205
|
if (isMounted.current) {
|
|
192
206
|
const errorState = sdk.getState();
|
|
193
|
-
biometric_identity_sdk_core_1.logger.error('Validation error, state:', errorState, error);
|
|
194
207
|
setState(errorState);
|
|
195
208
|
}
|
|
196
209
|
throw error;
|
package/package.json
CHANGED
|
@@ -77,55 +77,43 @@ export const BiometricIdentityFlow: React.FC<BiometricIdentityFlowProps> = ({
|
|
|
77
77
|
const [currentChallenges, setCurrentChallenges] = useState<ChallengeAction[]>([]);
|
|
78
78
|
const [isLoadingChallenges, setIsLoadingChallenges] = useState(false);
|
|
79
79
|
|
|
80
|
-
// Use refs to store callbacks to avoid dependency issues in useEffect
|
|
81
80
|
const onValidationCompleteRef = useRef(onValidationComplete);
|
|
82
81
|
const onErrorRef = useRef(onError);
|
|
83
82
|
const hasCalledValidationComplete = useRef(false);
|
|
84
83
|
|
|
85
|
-
// Update refs when callbacks change
|
|
86
84
|
useEffect(() => {
|
|
87
85
|
onValidationCompleteRef.current = onValidationComplete;
|
|
88
86
|
onErrorRef.current = onError;
|
|
89
87
|
}, [onValidationComplete, onError]);
|
|
90
88
|
|
|
91
|
-
// Set language early, before any components render
|
|
92
|
-
// Priority: language prop > SDK config language > default 'en'
|
|
93
|
-
// Run on mount and whenever language prop changes
|
|
94
89
|
useEffect(() => {
|
|
95
90
|
if (language) {
|
|
96
|
-
// If language prop is provided, override the config
|
|
97
91
|
setLanguage(language);
|
|
98
92
|
}
|
|
99
|
-
// If no language prop, the language should already be set by BiometricIdentitySDK.configure()
|
|
100
|
-
// The global language state is set when configure() is called, so getStrings() will
|
|
101
|
-
// automatically return strings for the configured language (or 'en' as default)
|
|
102
93
|
}, [language]);
|
|
103
94
|
|
|
104
95
|
const strings = getStrings();
|
|
105
96
|
const styles = createStyles(theme);
|
|
106
97
|
|
|
107
|
-
// Handle validation result - call callback when RESULT step is reached
|
|
108
98
|
useEffect(() => {
|
|
99
|
+
console.log('🔵 [BiometricIdentityFlow] useEffect triggered - currentStep:', state.currentStep, 'hasResult:', !!state.validationResult, 'hasCalled:', hasCalledValidationComplete.current);
|
|
109
100
|
if (state.currentStep === SDKStep.RESULT && state.validationResult && !hasCalledValidationComplete.current) {
|
|
101
|
+
console.log('🟢 [BiometricIdentityFlow] Calling onValidationComplete callback');
|
|
110
102
|
logger.info('Validation completed, calling onValidationComplete callback');
|
|
111
103
|
hasCalledValidationComplete.current = true;
|
|
112
|
-
// Use ref to avoid dependency issues
|
|
113
104
|
onValidationCompleteRef.current(state.validationResult);
|
|
114
105
|
}
|
|
115
106
|
}, [state.currentStep, state.validationResult]);
|
|
116
107
|
|
|
117
|
-
// Reset the flag when validation result changes or component resets
|
|
118
108
|
useEffect(() => {
|
|
119
109
|
if (state.currentStep !== SDKStep.RESULT) {
|
|
120
110
|
hasCalledValidationComplete.current = false;
|
|
121
111
|
}
|
|
122
112
|
}, [state.currentStep]);
|
|
123
113
|
|
|
124
|
-
// Handle error
|
|
125
114
|
useEffect(() => {
|
|
126
115
|
if (state.error) {
|
|
127
116
|
logger.error('SDK error detected:', state.error);
|
|
128
|
-
// Use ref to avoid dependency issues
|
|
129
117
|
onErrorRef.current(state.error);
|
|
130
118
|
}
|
|
131
119
|
}, [state.error]);
|
|
@@ -136,7 +124,6 @@ export const BiometricIdentityFlow: React.FC<BiometricIdentityFlowProps> = ({
|
|
|
136
124
|
const handleCaptureStart = useCallback(async (mode: 'front' | 'back' | 'video') => {
|
|
137
125
|
setCameraMode(mode);
|
|
138
126
|
|
|
139
|
-
// If video mode, fetch challenges first
|
|
140
127
|
if (mode === 'video' && smartLivenessMode && isUsingBackend) {
|
|
141
128
|
setIsLoadingChallenges(true);
|
|
142
129
|
try {
|
|
@@ -148,7 +135,6 @@ export const BiometricIdentityFlow: React.FC<BiometricIdentityFlowProps> = ({
|
|
|
148
135
|
}
|
|
149
136
|
setIsLoadingChallenges(false);
|
|
150
137
|
} else if (mode === 'video' && smartLivenessMode) {
|
|
151
|
-
// Use default challenges for offline mode
|
|
152
138
|
setCurrentChallenges(sdk.getDefaultChallenges());
|
|
153
139
|
}
|
|
154
140
|
|
|
@@ -178,30 +164,57 @@ export const BiometricIdentityFlow: React.FC<BiometricIdentityFlowProps> = ({
|
|
|
178
164
|
sessionId: videoResult.sessionId,
|
|
179
165
|
});
|
|
180
166
|
|
|
167
|
+
console.log('🔵 [BiometricIdentityFlow] Video stored, starting validation...');
|
|
181
168
|
logger.info('Starting validation...');
|
|
182
|
-
const result = await validateIdentity();
|
|
183
|
-
logger.info('Validation completed successfully:', result);
|
|
184
169
|
|
|
185
|
-
|
|
186
|
-
|
|
170
|
+
let result: ValidationResult | null = null;
|
|
171
|
+
try {
|
|
172
|
+
result = await validateIdentity();
|
|
173
|
+
console.log('🟢 [BiometricIdentityFlow] Validation completed:', result);
|
|
174
|
+
logger.info('Validation completed successfully:', result);
|
|
175
|
+
} catch (validationError) {
|
|
176
|
+
console.error('❌ [BiometricIdentityFlow] Validation error:', validationError);
|
|
177
|
+
logger.error('Validation error:', validationError);
|
|
178
|
+
throw validationError; // Re-throw to be caught by outer catch
|
|
179
|
+
}
|
|
180
|
+
|
|
187
181
|
const finalState = sdk.getState();
|
|
182
|
+
console.log('🔵 [BiometricIdentityFlow] Final state after validation:', JSON.stringify({
|
|
183
|
+
currentStep: finalState.currentStep,
|
|
184
|
+
hasResult: !!finalState.validationResult,
|
|
185
|
+
progress: finalState.progress,
|
|
186
|
+
isLoading: finalState.isLoading
|
|
187
|
+
}));
|
|
188
188
|
logger.info('Final state after validation:', finalState);
|
|
189
189
|
|
|
190
|
-
|
|
190
|
+
if (result && !hasCalledValidationComplete.current) {
|
|
191
|
+
console.log('🟢 [BiometricIdentityFlow] Calling onValidationComplete directly with result');
|
|
192
|
+
logger.info('Calling onValidationComplete directly with validation result');
|
|
193
|
+
hasCalledValidationComplete.current = true;
|
|
194
|
+
onValidationCompleteRef.current(result);
|
|
195
|
+
}
|
|
196
|
+
|
|
191
197
|
if (finalState.currentStep === SDKStep.RESULT && finalState.validationResult && !hasCalledValidationComplete.current) {
|
|
192
|
-
|
|
198
|
+
console.log('🟢 [BiometricIdentityFlow] Backup: Calling onValidationComplete from state');
|
|
199
|
+
logger.info('Backup: Calling onValidationComplete from state');
|
|
193
200
|
hasCalledValidationComplete.current = true;
|
|
194
201
|
onValidationCompleteRef.current(finalState.validationResult);
|
|
195
202
|
}
|
|
196
203
|
}
|
|
197
204
|
} catch (error) {
|
|
205
|
+
console.error('❌ [BiometricIdentityFlow] Capture/validation error:', error);
|
|
198
206
|
logger.error('Capture/validation error:', error);
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
207
|
+
|
|
208
|
+
if (error && typeof error === 'object' && 'code' in error) {
|
|
209
|
+
onErrorRef.current(error as BiometricError);
|
|
210
|
+
} else {
|
|
211
|
+
const biometricError: BiometricError = {
|
|
212
|
+
name: 'BiometricError',
|
|
213
|
+
message: error instanceof Error ? error.message : 'Unknown error during capture',
|
|
214
|
+
code: 'LIVENESS_CHECK_FAILED',
|
|
215
|
+
} as BiometricError;
|
|
216
|
+
onErrorRef.current(biometricError);
|
|
217
|
+
}
|
|
205
218
|
}
|
|
206
219
|
}, [cameraMode, uploadFrontID, uploadBackID, storeVideoRecording, validateIdentity, sdk]);
|
|
207
220
|
|
|
@@ -215,7 +228,6 @@ export const BiometricIdentityFlow: React.FC<BiometricIdentityFlowProps> = ({
|
|
|
215
228
|
setShowInstructions(true);
|
|
216
229
|
}, [reset]);
|
|
217
230
|
|
|
218
|
-
// Show loading while initializing
|
|
219
231
|
if (!isInitialized) {
|
|
220
232
|
return (
|
|
221
233
|
<SafeAreaView style={[styles.container, customStyles?.container]}>
|
|
@@ -227,7 +239,6 @@ export const BiometricIdentityFlow: React.FC<BiometricIdentityFlowProps> = ({
|
|
|
227
239
|
);
|
|
228
240
|
}
|
|
229
241
|
|
|
230
|
-
// Show instructions on first load
|
|
231
242
|
if (showInstructions) {
|
|
232
243
|
return (
|
|
233
244
|
<InstructionsScreen
|
|
@@ -240,7 +251,6 @@ export const BiometricIdentityFlow: React.FC<BiometricIdentityFlowProps> = ({
|
|
|
240
251
|
);
|
|
241
252
|
}
|
|
242
253
|
|
|
243
|
-
// Show camera/video recorder
|
|
244
254
|
if (showCamera) {
|
|
245
255
|
if (cameraMode === 'video') {
|
|
246
256
|
return (
|
|
@@ -271,10 +281,7 @@ export const BiometricIdentityFlow: React.FC<BiometricIdentityFlowProps> = ({
|
|
|
271
281
|
);
|
|
272
282
|
}
|
|
273
283
|
|
|
274
|
-
// Show result (check this first, even if step is still VALIDATING but result exists)
|
|
275
|
-
// This handles edge cases where state transition might be delayed
|
|
276
284
|
if (state.validationResult && (state.currentStep === SDKStep.RESULT || state.progress >= 100)) {
|
|
277
|
-
// If we have a result but step is still VALIDATING, force transition to RESULT
|
|
278
285
|
if (state.currentStep === SDKStep.VALIDATING && state.progress >= 100) {
|
|
279
286
|
logger.info('Result available but step still VALIDATING, showing ResultScreen');
|
|
280
287
|
}
|
|
@@ -284,14 +291,11 @@ export const BiometricIdentityFlow: React.FC<BiometricIdentityFlowProps> = ({
|
|
|
284
291
|
theme={theme}
|
|
285
292
|
language={language}
|
|
286
293
|
onClose={() => {
|
|
287
|
-
// Callback already called automatically when RESULT step was reached
|
|
288
|
-
// This onClose is just for UI cleanup/navigation
|
|
289
294
|
}}
|
|
290
295
|
/>
|
|
291
296
|
);
|
|
292
297
|
}
|
|
293
298
|
|
|
294
|
-
// Show validation progress
|
|
295
299
|
if (state.currentStep === SDKStep.VALIDATING) {
|
|
296
300
|
return (
|
|
297
301
|
<ValidationProgress
|
|
@@ -302,7 +306,6 @@ export const BiometricIdentityFlow: React.FC<BiometricIdentityFlowProps> = ({
|
|
|
302
306
|
);
|
|
303
307
|
}
|
|
304
308
|
|
|
305
|
-
// Show error
|
|
306
309
|
if (state.currentStep === SDKStep.ERROR && state.error) {
|
|
307
310
|
return (
|
|
308
311
|
<ErrorScreen
|
|
@@ -399,7 +402,6 @@ export const BiometricIdentityFlow: React.FC<BiometricIdentityFlowProps> = ({
|
|
|
399
402
|
);
|
|
400
403
|
};
|
|
401
404
|
|
|
402
|
-
// Step Indicator Component
|
|
403
405
|
interface StepIndicatorProps {
|
|
404
406
|
step: number;
|
|
405
407
|
active: boolean;
|
|
@@ -467,7 +469,6 @@ const stepStyles = StyleSheet.create({
|
|
|
467
469
|
},
|
|
468
470
|
});
|
|
469
471
|
|
|
470
|
-
// Action Button Component
|
|
471
472
|
interface ActionButtonProps {
|
|
472
473
|
title: string;
|
|
473
474
|
subtitle?: string;
|
|
@@ -531,7 +532,6 @@ const buttonStyles = StyleSheet.create({
|
|
|
531
532
|
},
|
|
532
533
|
});
|
|
533
534
|
|
|
534
|
-
// Create styles based on theme
|
|
535
535
|
const createStyles = (theme?: ThemeConfig) => {
|
|
536
536
|
const backgroundColor = theme?.backgroundColor || '#FFFFFF';
|
|
537
537
|
const textColor = theme?.textColor || '#000000';
|
|
@@ -18,7 +18,6 @@ import { Camera, useCameraDevice, useCameraPermission } from 'react-native-visio
|
|
|
18
18
|
import { request, PERMISSIONS, RESULTS } from 'react-native-permissions';
|
|
19
19
|
import { ThemeConfig, LivenessInstruction, SupportedLanguage, getStrings, setLanguage, logger } from '@hexar/biometric-identity-sdk-core';
|
|
20
20
|
|
|
21
|
-
// Challenge action configuration (matches backend response)
|
|
22
21
|
export interface ChallengeAction {
|
|
23
22
|
action: string;
|
|
24
23
|
instruction: string;
|
|
@@ -57,32 +56,31 @@ export interface VideoRecordingResult {
|
|
|
57
56
|
sessionId?: string;
|
|
58
57
|
}
|
|
59
58
|
|
|
60
|
-
|
|
61
|
-
const DEFAULT_CHALLENGES: ChallengeAction[] = [
|
|
59
|
+
const getDefaultChallenges = (strings: any): ChallengeAction[] => [
|
|
62
60
|
{
|
|
63
61
|
action: 'look_left',
|
|
64
|
-
instruction: 'Slowly turn your head to the LEFT',
|
|
62
|
+
instruction: strings.liveness.instructions.lookLeft || 'Slowly turn your head to the LEFT',
|
|
65
63
|
duration_ms: 2500,
|
|
66
64
|
order: 1,
|
|
67
65
|
icon: '←',
|
|
68
66
|
},
|
|
69
67
|
{
|
|
70
68
|
action: 'look_right',
|
|
71
|
-
instruction: 'Slowly turn your head to the RIGHT',
|
|
69
|
+
instruction: strings.liveness.instructions.lookRight || 'Slowly turn your head to the RIGHT',
|
|
72
70
|
duration_ms: 2500,
|
|
73
71
|
order: 2,
|
|
74
72
|
icon: '→',
|
|
75
73
|
},
|
|
76
74
|
{
|
|
77
75
|
action: 'blink',
|
|
78
|
-
instruction: 'Blink your eyes naturally',
|
|
76
|
+
instruction: strings.liveness.instructions.blink || 'Blink your eyes naturally',
|
|
79
77
|
duration_ms: 2000,
|
|
80
78
|
order: 3,
|
|
81
79
|
icon: '👁',
|
|
82
80
|
},
|
|
83
81
|
{
|
|
84
82
|
action: 'smile',
|
|
85
|
-
instruction: 'Smile 😊',
|
|
83
|
+
instruction: strings.liveness.instructions.smile || 'Smile 😊',
|
|
86
84
|
duration_ms: 2000,
|
|
87
85
|
order: 4,
|
|
88
86
|
icon: '😊',
|
|
@@ -114,12 +112,13 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
|
|
|
114
112
|
onCancel,
|
|
115
113
|
onFetchChallenges,
|
|
116
114
|
}) => {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
115
|
+
useEffect(() => {
|
|
116
|
+
if (language) {
|
|
117
|
+
setLanguage(language);
|
|
118
|
+
}
|
|
119
|
+
}, [language]);
|
|
121
120
|
|
|
122
|
-
|
|
121
|
+
const strings = getStrings();
|
|
123
122
|
const [phase, setPhase] = useState<'loading' | 'countdown' | 'recording' | 'processing'>('loading');
|
|
124
123
|
const [countdown, setCountdown] = useState(3);
|
|
125
124
|
const [challenges, setChallenges] = useState<ChallengeAction[]>([]);
|
|
@@ -130,12 +129,10 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
|
|
|
130
129
|
const [frames, setFrames] = useState<string[]>([]);
|
|
131
130
|
const [hasPermission, setHasPermission] = useState(false);
|
|
132
131
|
|
|
133
|
-
// Camera
|
|
134
132
|
const cameraRef = useRef<Camera>(null);
|
|
135
133
|
const { hasPermission: cameraPermission, requestPermission } = useCameraPermission();
|
|
136
134
|
const device = useCameraDevice('front');
|
|
137
135
|
|
|
138
|
-
// Animations
|
|
139
136
|
const fadeAnim = useRef(new Animated.Value(0)).current;
|
|
140
137
|
const scaleAnim = useRef(new Animated.Value(1)).current;
|
|
141
138
|
const pulseAnim = useRef(new Animated.Value(1)).current;
|
|
@@ -154,21 +151,17 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
|
|
|
154
151
|
challenges.reduce((sum, c) => sum + c.duration_ms, 0) + 2000
|
|
155
152
|
);
|
|
156
153
|
|
|
157
|
-
// Check camera permissions
|
|
158
154
|
useEffect(() => {
|
|
159
155
|
const checkPermissions = async () => {
|
|
160
156
|
try {
|
|
161
|
-
// First check if we already have permission from the hook
|
|
162
157
|
if (cameraPermission) {
|
|
163
158
|
setHasPermission(true);
|
|
164
159
|
return;
|
|
165
160
|
}
|
|
166
161
|
|
|
167
|
-
// Otherwise, request permission using the hook
|
|
168
162
|
const granted = await requestPermission();
|
|
169
163
|
setHasPermission(granted);
|
|
170
164
|
|
|
171
|
-
// If hook method didn't work, try platform-specific request
|
|
172
165
|
if (!granted) {
|
|
173
166
|
if (Platform.OS === 'ios') {
|
|
174
167
|
const result = await request(PERMISSIONS.IOS.CAMERA);
|
|
@@ -191,20 +184,27 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
|
|
|
191
184
|
checkPermissions();
|
|
192
185
|
}, [cameraPermission, requestPermission]);
|
|
193
186
|
|
|
194
|
-
// Initialize challenges
|
|
195
187
|
useEffect(() => {
|
|
196
188
|
const initChallenges = async () => {
|
|
197
189
|
try {
|
|
198
190
|
let challengeList: ChallengeAction[];
|
|
191
|
+
const currentStrings = getStrings();
|
|
192
|
+
const instructionMap = getInstructionMap(currentStrings);
|
|
199
193
|
|
|
200
194
|
if (propChallenges && propChallenges.length > 0) {
|
|
201
|
-
|
|
202
|
-
|
|
195
|
+
challengeList = propChallenges.map(challenge => ({
|
|
196
|
+
...challenge,
|
|
197
|
+
instruction: instructionMap[challenge.action]?.text || challenge.instruction,
|
|
198
|
+
icon: instructionMap[challenge.action]?.icon || challenge.icon,
|
|
199
|
+
}));
|
|
203
200
|
} else if (onFetchChallenges) {
|
|
204
|
-
|
|
205
|
-
challengeList =
|
|
201
|
+
const fetchedChallenges = await onFetchChallenges();
|
|
202
|
+
challengeList = fetchedChallenges.map(challenge => ({
|
|
203
|
+
...challenge,
|
|
204
|
+
instruction: instructionMap[challenge.action]?.text || challenge.instruction,
|
|
205
|
+
icon: instructionMap[challenge.action]?.icon || challenge.icon,
|
|
206
|
+
}));
|
|
206
207
|
} else if (instructions && instructions.length > 0) {
|
|
207
|
-
const instructionMap = getInstructionMap(strings);
|
|
208
208
|
challengeList = instructions.map((inst, idx) => ({
|
|
209
209
|
action: inst,
|
|
210
210
|
instruction: instructionMap[inst]?.text || inst,
|
|
@@ -213,11 +213,10 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
|
|
|
213
213
|
icon: instructionMap[inst]?.icon,
|
|
214
214
|
}));
|
|
215
215
|
} else {
|
|
216
|
-
|
|
217
|
-
challengeList = smartMode ? DEFAULT_CHALLENGES : [
|
|
216
|
+
challengeList = smartMode ? getDefaultChallenges(currentStrings) : [
|
|
218
217
|
{
|
|
219
218
|
action: 'stay_still',
|
|
220
|
-
instruction: 'Look at the camera and stay still',
|
|
219
|
+
instruction: currentStrings.liveness.instructions.stayStill || 'Look at the camera and stay still',
|
|
221
220
|
duration_ms: duration || 5000,
|
|
222
221
|
order: 1,
|
|
223
222
|
icon: '📷',
|
|
@@ -229,21 +228,18 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
|
|
|
229
228
|
setPhase('countdown');
|
|
230
229
|
} catch (error) {
|
|
231
230
|
logger.error('Failed to fetch challenges:', error);
|
|
232
|
-
|
|
233
|
-
setChallenges(DEFAULT_CHALLENGES);
|
|
231
|
+
setChallenges(getDefaultChallenges(getStrings()));
|
|
234
232
|
setPhase('countdown');
|
|
235
233
|
}
|
|
236
234
|
};
|
|
237
235
|
|
|
238
236
|
initChallenges();
|
|
239
|
-
}, [propChallenges, instructions, onFetchChallenges, smartMode, duration]);
|
|
237
|
+
}, [propChallenges, instructions, onFetchChallenges, smartMode, duration, language]);
|
|
240
238
|
|
|
241
|
-
// Countdown phase
|
|
242
239
|
useEffect(() => {
|
|
243
240
|
if (phase !== 'countdown') return;
|
|
244
241
|
|
|
245
242
|
if (countdown > 0) {
|
|
246
|
-
// Animate countdown number
|
|
247
243
|
Animated.sequence([
|
|
248
244
|
Animated.timing(scaleAnim, {
|
|
249
245
|
toValue: 1.3,
|
|
@@ -264,7 +260,6 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
|
|
|
264
260
|
}
|
|
265
261
|
}, [countdown, phase]);
|
|
266
262
|
|
|
267
|
-
// Start pulse animation for recording indicator
|
|
268
263
|
useEffect(() => {
|
|
269
264
|
if (phase === 'recording') {
|
|
270
265
|
Animated.loop(
|
|
@@ -286,7 +281,6 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
|
|
|
286
281
|
}
|
|
287
282
|
}, [phase]);
|
|
288
283
|
|
|
289
|
-
// Animate arrow for directional challenges
|
|
290
284
|
const animateArrow = useCallback((direction: string) => {
|
|
291
285
|
const toValue = direction.includes('left') ? -20 : direction.includes('right') ? 20 : 0;
|
|
292
286
|
|
|
@@ -422,7 +416,6 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
|
|
|
422
416
|
}
|
|
423
417
|
}
|
|
424
418
|
} catch (error) {
|
|
425
|
-
// Silent frame capture errors
|
|
426
419
|
}
|
|
427
420
|
}, 100);
|
|
428
421
|
}
|
|
@@ -641,10 +634,8 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
|
|
|
641
634
|
};
|
|
642
635
|
}, [device, totalDuration, minDurationMs, handleVideoComplete, handleRecordingError, runChallenge, stopRecording, startFrameCapture]);
|
|
643
636
|
|
|
644
|
-
// Current challenge
|
|
645
637
|
const currentChallenge = challenges[currentChallengeIndex];
|
|
646
638
|
|
|
647
|
-
// Get direction arrow for the current challenge
|
|
648
639
|
const getDirectionIndicator = () => {
|
|
649
640
|
if (!currentChallenge) return null;
|
|
650
641
|
|
|
@@ -194,6 +194,7 @@ export const useBiometricSDK = (): UseBiometricSDKResult => {
|
|
|
194
194
|
* Validate identity with all collected data
|
|
195
195
|
*/
|
|
196
196
|
const validateIdentity = useCallback(async () => {
|
|
197
|
+
console.log('🔵 [useBiometricSDK] validateIdentity called');
|
|
197
198
|
logger.info('validateIdentity called, current state:', sdk.getState());
|
|
198
199
|
setState(sdk.getState());
|
|
199
200
|
|
|
@@ -204,15 +205,19 @@ export const useBiometricSDK = (): UseBiometricSDKResult => {
|
|
|
204
205
|
}
|
|
205
206
|
}, 200);
|
|
206
207
|
|
|
208
|
+
let finalPollInterval: NodeJS.Timeout | null = null;
|
|
209
|
+
|
|
207
210
|
try {
|
|
211
|
+
console.log('🔵 [useBiometricSDK] Calling sdk.validateIdentity()...');
|
|
208
212
|
const result = await sdk.validateIdentity();
|
|
213
|
+
console.log('🟢 [useBiometricSDK] sdk.validateIdentity() completed, result:', result);
|
|
209
214
|
|
|
210
215
|
// Keep polling for a bit to ensure we capture the RESULT state transition
|
|
211
216
|
// The SDK updates state synchronously, but we want to make sure React sees it
|
|
212
217
|
let pollCount = 0;
|
|
213
218
|
const maxPolls = 10; // Poll for up to 2 seconds (10 * 200ms)
|
|
214
219
|
|
|
215
|
-
|
|
220
|
+
finalPollInterval = setInterval(() => {
|
|
216
221
|
pollCount++;
|
|
217
222
|
if (isMounted.current) {
|
|
218
223
|
const currentState = sdk.getState();
|
|
@@ -221,12 +226,13 @@ export const useBiometricSDK = (): UseBiometricSDKResult => {
|
|
|
221
226
|
|
|
222
227
|
// Stop polling once we reach RESULT or ERROR state
|
|
223
228
|
if (currentState.currentStep === SDKStep.RESULT || currentState.currentStep === SDKStep.ERROR) {
|
|
224
|
-
|
|
229
|
+
console.log('🟢 [useBiometricSDK] Reached final state:', currentState.currentStep);
|
|
230
|
+
if (finalPollInterval) clearInterval(finalPollInterval);
|
|
225
231
|
clearInterval(pollInterval);
|
|
226
232
|
logger.info('Reached final state, stopped polling:', currentState);
|
|
227
233
|
} else if (pollCount >= maxPolls) {
|
|
228
234
|
// Force stop after max polls
|
|
229
|
-
clearInterval(finalPollInterval);
|
|
235
|
+
if (finalPollInterval) clearInterval(finalPollInterval);
|
|
230
236
|
clearInterval(pollInterval);
|
|
231
237
|
const finalState = sdk.getState();
|
|
232
238
|
logger.warn('Max polls reached, forcing final state:', finalState);
|
|
@@ -243,17 +249,23 @@ export const useBiometricSDK = (): UseBiometricSDKResult => {
|
|
|
243
249
|
|
|
244
250
|
// If we're already at RESULT, clear intervals immediately
|
|
245
251
|
if (immediateState.currentStep === SDKStep.RESULT || immediateState.currentStep === SDKStep.ERROR) {
|
|
246
|
-
clearInterval(finalPollInterval);
|
|
252
|
+
if (finalPollInterval) clearInterval(finalPollInterval);
|
|
247
253
|
clearInterval(pollInterval);
|
|
248
254
|
}
|
|
249
255
|
}
|
|
250
256
|
|
|
251
257
|
return result;
|
|
252
258
|
} catch (error) {
|
|
259
|
+
// Clear all intervals on error
|
|
253
260
|
clearInterval(pollInterval);
|
|
261
|
+
if (finalPollInterval) {
|
|
262
|
+
clearInterval(finalPollInterval);
|
|
263
|
+
}
|
|
264
|
+
console.error('❌ [useBiometricSDK] Validation error:', error);
|
|
265
|
+
logger.error('Validation error, state:', sdk.getState(), error);
|
|
266
|
+
|
|
254
267
|
if (isMounted.current) {
|
|
255
268
|
const errorState = sdk.getState();
|
|
256
|
-
logger.error('Validation error, state:', errorState, error);
|
|
257
269
|
setState(errorState);
|
|
258
270
|
}
|
|
259
271
|
throw error;
|