@hexar/biometric-identity-sdk-react-native 1.0.17 → 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 +31 -41
- 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 +14 -5
- package/package.json +1 -1
- package/src/components/BiometricIdentityFlow.tsx +32 -43
- package/src/components/VideoRecorder.tsx +28 -37
- package/src/hooks/useBiometricSDK.ts +13 -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,51 +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)(() => {
|
|
83
73
|
console.log('🔵 [BiometricIdentityFlow] useEffect triggered - currentStep:', state.currentStep, 'hasResult:', !!state.validationResult, 'hasCalled:', hasCalledValidationComplete.current);
|
|
84
74
|
if (state.currentStep === biometric_identity_sdk_core_1.SDKStep.RESULT && state.validationResult && !hasCalledValidationComplete.current) {
|
|
85
75
|
console.log('🟢 [BiometricIdentityFlow] Calling onValidationComplete callback');
|
|
86
76
|
biometric_identity_sdk_core_1.logger.info('Validation completed, calling onValidationComplete callback');
|
|
87
77
|
hasCalledValidationComplete.current = true;
|
|
88
|
-
// Use ref to avoid dependency issues
|
|
89
78
|
onValidationCompleteRef.current(state.validationResult);
|
|
90
79
|
}
|
|
91
80
|
}, [state.currentStep, state.validationResult]);
|
|
92
|
-
// Reset the flag when validation result changes or component resets
|
|
93
81
|
(0, react_1.useEffect)(() => {
|
|
94
82
|
if (state.currentStep !== biometric_identity_sdk_core_1.SDKStep.RESULT) {
|
|
95
83
|
hasCalledValidationComplete.current = false;
|
|
96
84
|
}
|
|
97
85
|
}, [state.currentStep]);
|
|
98
|
-
// Handle error
|
|
99
86
|
(0, react_1.useEffect)(() => {
|
|
100
87
|
if (state.error) {
|
|
101
88
|
biometric_identity_sdk_core_1.logger.error('SDK error detected:', state.error);
|
|
102
|
-
// Use ref to avoid dependency issues
|
|
103
89
|
onErrorRef.current(state.error);
|
|
104
90
|
}
|
|
105
91
|
}, [state.error]);
|
|
@@ -108,7 +94,6 @@ const BiometricIdentityFlow = ({ onValidationComplete, onError, theme, language,
|
|
|
108
94
|
*/
|
|
109
95
|
const handleCaptureStart = (0, react_1.useCallback)(async (mode) => {
|
|
110
96
|
setCameraMode(mode);
|
|
111
|
-
// If video mode, fetch challenges first
|
|
112
97
|
if (mode === 'video' && smartLivenessMode && isUsingBackend) {
|
|
113
98
|
setIsLoadingChallenges(true);
|
|
114
99
|
try {
|
|
@@ -122,7 +107,6 @@ const BiometricIdentityFlow = ({ onValidationComplete, onError, theme, language,
|
|
|
122
107
|
setIsLoadingChallenges(false);
|
|
123
108
|
}
|
|
124
109
|
else if (mode === 'video' && smartLivenessMode) {
|
|
125
|
-
// Use default challenges for offline mode
|
|
126
110
|
setCurrentChallenges(sdk.getDefaultChallenges());
|
|
127
111
|
}
|
|
128
112
|
setShowCamera(true);
|
|
@@ -151,11 +135,17 @@ const BiometricIdentityFlow = ({ onValidationComplete, onError, theme, language,
|
|
|
151
135
|
});
|
|
152
136
|
console.log('🔵 [BiometricIdentityFlow] Video stored, starting validation...');
|
|
153
137
|
biometric_identity_sdk_core_1.logger.info('Starting validation...');
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
+
}
|
|
159
149
|
const finalState = sdk.getState();
|
|
160
150
|
console.log('🔵 [BiometricIdentityFlow] Final state after validation:', JSON.stringify({
|
|
161
151
|
currentStep: finalState.currentStep,
|
|
@@ -164,23 +154,34 @@ const BiometricIdentityFlow = ({ onValidationComplete, onError, theme, language,
|
|
|
164
154
|
isLoading: finalState.isLoading
|
|
165
155
|
}));
|
|
166
156
|
biometric_identity_sdk_core_1.logger.info('Final state after validation:', finalState);
|
|
167
|
-
|
|
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
|
+
}
|
|
168
163
|
if (finalState.currentStep === biometric_identity_sdk_core_1.SDKStep.RESULT && finalState.validationResult && !hasCalledValidationComplete.current) {
|
|
169
|
-
console.log('🟢 [BiometricIdentityFlow] Backup: Calling onValidationComplete
|
|
170
|
-
biometric_identity_sdk_core_1.logger.info('Backup: Calling onValidationComplete
|
|
164
|
+
console.log('🟢 [BiometricIdentityFlow] Backup: Calling onValidationComplete from state');
|
|
165
|
+
biometric_identity_sdk_core_1.logger.info('Backup: Calling onValidationComplete from state');
|
|
171
166
|
hasCalledValidationComplete.current = true;
|
|
172
167
|
onValidationCompleteRef.current(finalState.validationResult);
|
|
173
168
|
}
|
|
174
169
|
}
|
|
175
170
|
}
|
|
176
171
|
catch (error) {
|
|
172
|
+
console.error('❌ [BiometricIdentityFlow] Capture/validation error:', error);
|
|
177
173
|
biometric_identity_sdk_core_1.logger.error('Capture/validation error:', error);
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
+
}
|
|
184
185
|
}
|
|
185
186
|
}, [cameraMode, uploadFrontID, uploadBackID, storeVideoRecording, validateIdentity, sdk]);
|
|
186
187
|
/**
|
|
@@ -192,18 +193,15 @@ const BiometricIdentityFlow = ({ onValidationComplete, onError, theme, language,
|
|
|
192
193
|
setCurrentChallenges([]);
|
|
193
194
|
setShowInstructions(true);
|
|
194
195
|
}, [reset]);
|
|
195
|
-
// Show loading while initializing
|
|
196
196
|
if (!isInitialized) {
|
|
197
197
|
return (react_1.default.createElement(react_native_1.SafeAreaView, { style: [styles.container, customStyles?.container] },
|
|
198
198
|
react_1.default.createElement(react_native_1.View, { style: styles.loadingFullScreen },
|
|
199
199
|
react_1.default.createElement(react_native_1.ActivityIndicator, { size: "large", color: theme?.primaryColor || '#6366F1' }),
|
|
200
200
|
react_1.default.createElement(react_native_1.Text, { style: styles.loadingText }, strings.initialization.initializing))));
|
|
201
201
|
}
|
|
202
|
-
// Show instructions on first load
|
|
203
202
|
if (showInstructions) {
|
|
204
203
|
return (react_1.default.createElement(InstructionsScreen_1.InstructionsScreen, { theme: theme, language: language, onStart: () => setShowInstructions(false), routeBack: routeBack, styles: customStyles }));
|
|
205
204
|
}
|
|
206
|
-
// Show camera/video recorder
|
|
207
205
|
if (showCamera) {
|
|
208
206
|
if (cameraMode === 'video') {
|
|
209
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 () => {
|
|
@@ -213,23 +211,16 @@ const BiometricIdentityFlow = ({ onValidationComplete, onError, theme, language,
|
|
|
213
211
|
}
|
|
214
212
|
return (react_1.default.createElement(CameraCapture_1.CameraCapture, { mode: cameraMode, theme: theme, language: language, onCapture: handleCaptureComplete, onCancel: () => setShowCamera(false) }));
|
|
215
213
|
}
|
|
216
|
-
// Show result (check this first, even if step is still VALIDATING but result exists)
|
|
217
|
-
// This handles edge cases where state transition might be delayed
|
|
218
214
|
if (state.validationResult && (state.currentStep === biometric_identity_sdk_core_1.SDKStep.RESULT || state.progress >= 100)) {
|
|
219
|
-
// If we have a result but step is still VALIDATING, force transition to RESULT
|
|
220
215
|
if (state.currentStep === biometric_identity_sdk_core_1.SDKStep.VALIDATING && state.progress >= 100) {
|
|
221
216
|
biometric_identity_sdk_core_1.logger.info('Result available but step still VALIDATING, showing ResultScreen');
|
|
222
217
|
}
|
|
223
218
|
return (react_1.default.createElement(ResultScreen_1.ResultScreen, { result: state.validationResult, theme: theme, language: language, onClose: () => {
|
|
224
|
-
// Callback already called automatically when RESULT step was reached
|
|
225
|
-
// This onClose is just for UI cleanup/navigation
|
|
226
219
|
} }));
|
|
227
220
|
}
|
|
228
|
-
// Show validation progress
|
|
229
221
|
if (state.currentStep === biometric_identity_sdk_core_1.SDKStep.VALIDATING) {
|
|
230
222
|
return (react_1.default.createElement(ValidationProgress_1.ValidationProgress, { progress: state.progress, theme: theme, language: language }));
|
|
231
223
|
}
|
|
232
|
-
// Show error
|
|
233
224
|
if (state.currentStep === biometric_identity_sdk_core_1.SDKStep.ERROR && state.error) {
|
|
234
225
|
return (react_1.default.createElement(ErrorScreen_1.ErrorScreen, { error: state.error, theme: theme, language: language, onRetry: handleRetry, onClose: () => onError(state.error) }));
|
|
235
226
|
}
|
|
@@ -334,7 +325,6 @@ const buttonStyles = react_native_1.StyleSheet.create({
|
|
|
334
325
|
marginTop: 4,
|
|
335
326
|
},
|
|
336
327
|
});
|
|
337
|
-
// Create styles based on theme
|
|
338
328
|
const createStyles = (theme) => {
|
|
339
329
|
const backgroundColor = theme?.backgroundColor || '#FFFFFF';
|
|
340
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"}
|
|
@@ -146,6 +146,7 @@ const useBiometricSDK = () => {
|
|
|
146
146
|
setState(currentState);
|
|
147
147
|
}
|
|
148
148
|
}, 200);
|
|
149
|
+
let finalPollInterval = null;
|
|
149
150
|
try {
|
|
150
151
|
console.log('🔵 [useBiometricSDK] Calling sdk.validateIdentity()...');
|
|
151
152
|
const result = await sdk.validateIdentity();
|
|
@@ -154,7 +155,7 @@ const useBiometricSDK = () => {
|
|
|
154
155
|
// The SDK updates state synchronously, but we want to make sure React sees it
|
|
155
156
|
let pollCount = 0;
|
|
156
157
|
const maxPolls = 10; // Poll for up to 2 seconds (10 * 200ms)
|
|
157
|
-
|
|
158
|
+
finalPollInterval = setInterval(() => {
|
|
158
159
|
pollCount++;
|
|
159
160
|
if (isMounted.current) {
|
|
160
161
|
const currentState = sdk.getState();
|
|
@@ -163,13 +164,15 @@ const useBiometricSDK = () => {
|
|
|
163
164
|
// Stop polling once we reach RESULT or ERROR state
|
|
164
165
|
if (currentState.currentStep === biometric_identity_sdk_core_1.SDKStep.RESULT || currentState.currentStep === biometric_identity_sdk_core_1.SDKStep.ERROR) {
|
|
165
166
|
console.log('🟢 [useBiometricSDK] Reached final state:', currentState.currentStep);
|
|
166
|
-
|
|
167
|
+
if (finalPollInterval)
|
|
168
|
+
clearInterval(finalPollInterval);
|
|
167
169
|
clearInterval(pollInterval);
|
|
168
170
|
biometric_identity_sdk_core_1.logger.info('Reached final state, stopped polling:', currentState);
|
|
169
171
|
}
|
|
170
172
|
else if (pollCount >= maxPolls) {
|
|
171
173
|
// Force stop after max polls
|
|
172
|
-
|
|
174
|
+
if (finalPollInterval)
|
|
175
|
+
clearInterval(finalPollInterval);
|
|
173
176
|
clearInterval(pollInterval);
|
|
174
177
|
const finalState = sdk.getState();
|
|
175
178
|
biometric_identity_sdk_core_1.logger.warn('Max polls reached, forcing final state:', finalState);
|
|
@@ -184,17 +187,23 @@ const useBiometricSDK = () => {
|
|
|
184
187
|
setState(immediateState);
|
|
185
188
|
// If we're already at RESULT, clear intervals immediately
|
|
186
189
|
if (immediateState.currentStep === biometric_identity_sdk_core_1.SDKStep.RESULT || immediateState.currentStep === biometric_identity_sdk_core_1.SDKStep.ERROR) {
|
|
187
|
-
|
|
190
|
+
if (finalPollInterval)
|
|
191
|
+
clearInterval(finalPollInterval);
|
|
188
192
|
clearInterval(pollInterval);
|
|
189
193
|
}
|
|
190
194
|
}
|
|
191
195
|
return result;
|
|
192
196
|
}
|
|
193
197
|
catch (error) {
|
|
198
|
+
// Clear all intervals on error
|
|
194
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);
|
|
195
205
|
if (isMounted.current) {
|
|
196
206
|
const errorState = sdk.getState();
|
|
197
|
-
biometric_identity_sdk_core_1.logger.error('Validation error, state:', errorState, error);
|
|
198
207
|
setState(errorState);
|
|
199
208
|
}
|
|
200
209
|
throw error;
|
package/package.json
CHANGED
|
@@ -77,57 +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(() => {
|
|
109
99
|
console.log('🔵 [BiometricIdentityFlow] useEffect triggered - currentStep:', state.currentStep, 'hasResult:', !!state.validationResult, 'hasCalled:', hasCalledValidationComplete.current);
|
|
110
100
|
if (state.currentStep === SDKStep.RESULT && state.validationResult && !hasCalledValidationComplete.current) {
|
|
111
101
|
console.log('🟢 [BiometricIdentityFlow] Calling onValidationComplete callback');
|
|
112
102
|
logger.info('Validation completed, calling onValidationComplete callback');
|
|
113
103
|
hasCalledValidationComplete.current = true;
|
|
114
|
-
// Use ref to avoid dependency issues
|
|
115
104
|
onValidationCompleteRef.current(state.validationResult);
|
|
116
105
|
}
|
|
117
106
|
}, [state.currentStep, state.validationResult]);
|
|
118
107
|
|
|
119
|
-
// Reset the flag when validation result changes or component resets
|
|
120
108
|
useEffect(() => {
|
|
121
109
|
if (state.currentStep !== SDKStep.RESULT) {
|
|
122
110
|
hasCalledValidationComplete.current = false;
|
|
123
111
|
}
|
|
124
112
|
}, [state.currentStep]);
|
|
125
113
|
|
|
126
|
-
// Handle error
|
|
127
114
|
useEffect(() => {
|
|
128
115
|
if (state.error) {
|
|
129
116
|
logger.error('SDK error detected:', state.error);
|
|
130
|
-
// Use ref to avoid dependency issues
|
|
131
117
|
onErrorRef.current(state.error);
|
|
132
118
|
}
|
|
133
119
|
}, [state.error]);
|
|
@@ -138,7 +124,6 @@ export const BiometricIdentityFlow: React.FC<BiometricIdentityFlowProps> = ({
|
|
|
138
124
|
const handleCaptureStart = useCallback(async (mode: 'front' | 'back' | 'video') => {
|
|
139
125
|
setCameraMode(mode);
|
|
140
126
|
|
|
141
|
-
// If video mode, fetch challenges first
|
|
142
127
|
if (mode === 'video' && smartLivenessMode && isUsingBackend) {
|
|
143
128
|
setIsLoadingChallenges(true);
|
|
144
129
|
try {
|
|
@@ -150,7 +135,6 @@ export const BiometricIdentityFlow: React.FC<BiometricIdentityFlowProps> = ({
|
|
|
150
135
|
}
|
|
151
136
|
setIsLoadingChallenges(false);
|
|
152
137
|
} else if (mode === 'video' && smartLivenessMode) {
|
|
153
|
-
// Use default challenges for offline mode
|
|
154
138
|
setCurrentChallenges(sdk.getDefaultChallenges());
|
|
155
139
|
}
|
|
156
140
|
|
|
@@ -182,12 +166,18 @@ export const BiometricIdentityFlow: React.FC<BiometricIdentityFlowProps> = ({
|
|
|
182
166
|
|
|
183
167
|
console.log('🔵 [BiometricIdentityFlow] Video stored, starting validation...');
|
|
184
168
|
logger.info('Starting validation...');
|
|
185
|
-
const result = await validateIdentity();
|
|
186
|
-
console.log('🟢 [BiometricIdentityFlow] Validation completed:', result);
|
|
187
|
-
logger.info('Validation completed successfully:', result);
|
|
188
169
|
|
|
189
|
-
|
|
190
|
-
|
|
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
|
+
|
|
191
181
|
const finalState = sdk.getState();
|
|
192
182
|
console.log('🔵 [BiometricIdentityFlow] Final state after validation:', JSON.stringify({
|
|
193
183
|
currentStep: finalState.currentStep,
|
|
@@ -197,22 +187,34 @@ export const BiometricIdentityFlow: React.FC<BiometricIdentityFlowProps> = ({
|
|
|
197
187
|
}));
|
|
198
188
|
logger.info('Final state after validation:', finalState);
|
|
199
189
|
|
|
200
|
-
|
|
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
|
+
|
|
201
197
|
if (finalState.currentStep === SDKStep.RESULT && finalState.validationResult && !hasCalledValidationComplete.current) {
|
|
202
|
-
console.log('🟢 [BiometricIdentityFlow] Backup: Calling onValidationComplete
|
|
203
|
-
logger.info('Backup: Calling onValidationComplete
|
|
198
|
+
console.log('🟢 [BiometricIdentityFlow] Backup: Calling onValidationComplete from state');
|
|
199
|
+
logger.info('Backup: Calling onValidationComplete from state');
|
|
204
200
|
hasCalledValidationComplete.current = true;
|
|
205
201
|
onValidationCompleteRef.current(finalState.validationResult);
|
|
206
202
|
}
|
|
207
203
|
}
|
|
208
204
|
} catch (error) {
|
|
205
|
+
console.error('❌ [BiometricIdentityFlow] Capture/validation error:', error);
|
|
209
206
|
logger.error('Capture/validation error:', error);
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
+
}
|
|
216
218
|
}
|
|
217
219
|
}, [cameraMode, uploadFrontID, uploadBackID, storeVideoRecording, validateIdentity, sdk]);
|
|
218
220
|
|
|
@@ -226,7 +228,6 @@ export const BiometricIdentityFlow: React.FC<BiometricIdentityFlowProps> = ({
|
|
|
226
228
|
setShowInstructions(true);
|
|
227
229
|
}, [reset]);
|
|
228
230
|
|
|
229
|
-
// Show loading while initializing
|
|
230
231
|
if (!isInitialized) {
|
|
231
232
|
return (
|
|
232
233
|
<SafeAreaView style={[styles.container, customStyles?.container]}>
|
|
@@ -238,7 +239,6 @@ export const BiometricIdentityFlow: React.FC<BiometricIdentityFlowProps> = ({
|
|
|
238
239
|
);
|
|
239
240
|
}
|
|
240
241
|
|
|
241
|
-
// Show instructions on first load
|
|
242
242
|
if (showInstructions) {
|
|
243
243
|
return (
|
|
244
244
|
<InstructionsScreen
|
|
@@ -251,7 +251,6 @@ export const BiometricIdentityFlow: React.FC<BiometricIdentityFlowProps> = ({
|
|
|
251
251
|
);
|
|
252
252
|
}
|
|
253
253
|
|
|
254
|
-
// Show camera/video recorder
|
|
255
254
|
if (showCamera) {
|
|
256
255
|
if (cameraMode === 'video') {
|
|
257
256
|
return (
|
|
@@ -282,10 +281,7 @@ export const BiometricIdentityFlow: React.FC<BiometricIdentityFlowProps> = ({
|
|
|
282
281
|
);
|
|
283
282
|
}
|
|
284
283
|
|
|
285
|
-
// Show result (check this first, even if step is still VALIDATING but result exists)
|
|
286
|
-
// This handles edge cases where state transition might be delayed
|
|
287
284
|
if (state.validationResult && (state.currentStep === SDKStep.RESULT || state.progress >= 100)) {
|
|
288
|
-
// If we have a result but step is still VALIDATING, force transition to RESULT
|
|
289
285
|
if (state.currentStep === SDKStep.VALIDATING && state.progress >= 100) {
|
|
290
286
|
logger.info('Result available but step still VALIDATING, showing ResultScreen');
|
|
291
287
|
}
|
|
@@ -295,14 +291,11 @@ export const BiometricIdentityFlow: React.FC<BiometricIdentityFlowProps> = ({
|
|
|
295
291
|
theme={theme}
|
|
296
292
|
language={language}
|
|
297
293
|
onClose={() => {
|
|
298
|
-
// Callback already called automatically when RESULT step was reached
|
|
299
|
-
// This onClose is just for UI cleanup/navigation
|
|
300
294
|
}}
|
|
301
295
|
/>
|
|
302
296
|
);
|
|
303
297
|
}
|
|
304
298
|
|
|
305
|
-
// Show validation progress
|
|
306
299
|
if (state.currentStep === SDKStep.VALIDATING) {
|
|
307
300
|
return (
|
|
308
301
|
<ValidationProgress
|
|
@@ -313,7 +306,6 @@ export const BiometricIdentityFlow: React.FC<BiometricIdentityFlowProps> = ({
|
|
|
313
306
|
);
|
|
314
307
|
}
|
|
315
308
|
|
|
316
|
-
// Show error
|
|
317
309
|
if (state.currentStep === SDKStep.ERROR && state.error) {
|
|
318
310
|
return (
|
|
319
311
|
<ErrorScreen
|
|
@@ -410,7 +402,6 @@ export const BiometricIdentityFlow: React.FC<BiometricIdentityFlowProps> = ({
|
|
|
410
402
|
);
|
|
411
403
|
};
|
|
412
404
|
|
|
413
|
-
// Step Indicator Component
|
|
414
405
|
interface StepIndicatorProps {
|
|
415
406
|
step: number;
|
|
416
407
|
active: boolean;
|
|
@@ -478,7 +469,6 @@ const stepStyles = StyleSheet.create({
|
|
|
478
469
|
},
|
|
479
470
|
});
|
|
480
471
|
|
|
481
|
-
// Action Button Component
|
|
482
472
|
interface ActionButtonProps {
|
|
483
473
|
title: string;
|
|
484
474
|
subtitle?: string;
|
|
@@ -542,7 +532,6 @@ const buttonStyles = StyleSheet.create({
|
|
|
542
532
|
},
|
|
543
533
|
});
|
|
544
534
|
|
|
545
|
-
// Create styles based on theme
|
|
546
535
|
const createStyles = (theme?: ThemeConfig) => {
|
|
547
536
|
const backgroundColor = theme?.backgroundColor || '#FFFFFF';
|
|
548
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
|
|
|
@@ -205,6 +205,8 @@ export const useBiometricSDK = (): UseBiometricSDKResult => {
|
|
|
205
205
|
}
|
|
206
206
|
}, 200);
|
|
207
207
|
|
|
208
|
+
let finalPollInterval: NodeJS.Timeout | null = null;
|
|
209
|
+
|
|
208
210
|
try {
|
|
209
211
|
console.log('🔵 [useBiometricSDK] Calling sdk.validateIdentity()...');
|
|
210
212
|
const result = await sdk.validateIdentity();
|
|
@@ -215,7 +217,7 @@ export const useBiometricSDK = (): UseBiometricSDKResult => {
|
|
|
215
217
|
let pollCount = 0;
|
|
216
218
|
const maxPolls = 10; // Poll for up to 2 seconds (10 * 200ms)
|
|
217
219
|
|
|
218
|
-
|
|
220
|
+
finalPollInterval = setInterval(() => {
|
|
219
221
|
pollCount++;
|
|
220
222
|
if (isMounted.current) {
|
|
221
223
|
const currentState = sdk.getState();
|
|
@@ -225,12 +227,12 @@ export const useBiometricSDK = (): UseBiometricSDKResult => {
|
|
|
225
227
|
// Stop polling once we reach RESULT or ERROR state
|
|
226
228
|
if (currentState.currentStep === SDKStep.RESULT || currentState.currentStep === SDKStep.ERROR) {
|
|
227
229
|
console.log('🟢 [useBiometricSDK] Reached final state:', currentState.currentStep);
|
|
228
|
-
clearInterval(finalPollInterval);
|
|
230
|
+
if (finalPollInterval) clearInterval(finalPollInterval);
|
|
229
231
|
clearInterval(pollInterval);
|
|
230
232
|
logger.info('Reached final state, stopped polling:', currentState);
|
|
231
233
|
} else if (pollCount >= maxPolls) {
|
|
232
234
|
// Force stop after max polls
|
|
233
|
-
clearInterval(finalPollInterval);
|
|
235
|
+
if (finalPollInterval) clearInterval(finalPollInterval);
|
|
234
236
|
clearInterval(pollInterval);
|
|
235
237
|
const finalState = sdk.getState();
|
|
236
238
|
logger.warn('Max polls reached, forcing final state:', finalState);
|
|
@@ -247,17 +249,23 @@ export const useBiometricSDK = (): UseBiometricSDKResult => {
|
|
|
247
249
|
|
|
248
250
|
// If we're already at RESULT, clear intervals immediately
|
|
249
251
|
if (immediateState.currentStep === SDKStep.RESULT || immediateState.currentStep === SDKStep.ERROR) {
|
|
250
|
-
clearInterval(finalPollInterval);
|
|
252
|
+
if (finalPollInterval) clearInterval(finalPollInterval);
|
|
251
253
|
clearInterval(pollInterval);
|
|
252
254
|
}
|
|
253
255
|
}
|
|
254
256
|
|
|
255
257
|
return result;
|
|
256
258
|
} catch (error) {
|
|
259
|
+
// Clear all intervals on error
|
|
257
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
|
+
|
|
258
267
|
if (isMounted.current) {
|
|
259
268
|
const errorState = sdk.getState();
|
|
260
|
-
logger.error('Validation error, state:', errorState, error);
|
|
261
269
|
setState(errorState);
|
|
262
270
|
}
|
|
263
271
|
throw error;
|