@hexar/biometric-identity-sdk-react-native 1.0.8 → 1.0.10
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 +1 -0
- package/dist/components/BiometricIdentityFlow.d.ts.map +1 -1
- package/dist/components/BiometricIdentityFlow.js +18 -7
- package/dist/components/InstructionsScreen.d.ts +1 -1
- package/dist/components/InstructionsScreen.d.ts.map +1 -1
- package/dist/components/InstructionsScreen.js +20 -18
- package/dist/components/VideoRecorder.d.ts.map +1 -1
- package/dist/components/VideoRecorder.js +75 -22
- package/dist/hooks/useBiometricSDK.d.ts.map +1 -1
- package/dist/hooks/useBiometricSDK.js +27 -4
- package/package.json +1 -1
- package/src/components/BiometricIdentityFlow.tsx +20 -6
- package/src/components/InstructionsScreen.tsx +31 -28
- package/src/components/VideoRecorder.tsx +84 -25
- package/src/hooks/useBiometricSDK.ts +30 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BiometricIdentityFlow.d.ts","sourceRoot":"","sources":["../../src/components/BiometricIdentityFlow.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAA2C,MAAM,OAAO,CAAC;AAChE,OAAO,EAOL,SAAS,EACV,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,gBAAgB,EAChB,WAAW,EACX,cAAc,EAKd,iBAAiB,EAClB,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,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,KAA2C,MAAM,OAAO,CAAC;AAChE,OAAO,EAOL,SAAS,EACV,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,gBAAgB,EAChB,WAAW,EACX,cAAc,EAKd,iBAAiB,EAClB,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,CAiUtE,CAAC;AAiOF,eAAe,qBAAqB,CAAC"}
|
|
@@ -48,7 +48,7 @@ const ValidationProgress_1 = require("./ValidationProgress");
|
|
|
48
48
|
const ResultScreen_1 = require("./ResultScreen");
|
|
49
49
|
const ErrorScreen_1 = require("./ErrorScreen");
|
|
50
50
|
const InstructionsScreen_1 = require("./InstructionsScreen");
|
|
51
|
-
const BiometricIdentityFlow = ({ onValidationComplete, onError, theme, language, customTranslations, smartLivenessMode = true, styles: customStyles, }) => {
|
|
51
|
+
const BiometricIdentityFlow = ({ onValidationComplete, onError, theme, language, customTranslations, smartLivenessMode = true, routeBack, styles: customStyles, }) => {
|
|
52
52
|
const { sdk, state, isInitialized, isUsingBackend, challenges, uploadFrontID, uploadBackID, storeVideoRecording, fetchChallenges, validateIdentity, reset, } = (0, useBiometricSDK_1.useBiometricSDK)();
|
|
53
53
|
const [showCamera, setShowCamera] = (0, react_1.useState)(false);
|
|
54
54
|
const [cameraMode, setCameraMode] = (0, react_1.useState)('front');
|
|
@@ -109,17 +109,28 @@ const BiometricIdentityFlow = ({ onValidationComplete, onError, theme, language,
|
|
|
109
109
|
* Handle capture completion
|
|
110
110
|
*/
|
|
111
111
|
const handleCaptureComplete = (0, react_1.useCallback)(async (data) => {
|
|
112
|
+
console.log('handleCaptureComplete called, cameraMode:', cameraMode);
|
|
112
113
|
setShowCamera(false);
|
|
113
114
|
try {
|
|
114
115
|
if (cameraMode === 'front') {
|
|
116
|
+
console.log('Uploading front ID');
|
|
115
117
|
await uploadFrontID(data);
|
|
118
|
+
console.log('Front ID uploaded successfully');
|
|
116
119
|
}
|
|
117
120
|
else if (cameraMode === 'back') {
|
|
121
|
+
console.log('Uploading back ID');
|
|
118
122
|
await uploadBackID(data);
|
|
123
|
+
console.log('Back ID uploaded successfully');
|
|
119
124
|
}
|
|
120
125
|
else if (cameraMode === 'video') {
|
|
126
|
+
console.log('Processing video recording result');
|
|
121
127
|
// Handle video recording result
|
|
122
128
|
const videoResult = data;
|
|
129
|
+
console.log('Storing video recording:', {
|
|
130
|
+
frames: videoResult.frames.length,
|
|
131
|
+
duration: videoResult.duration,
|
|
132
|
+
challengesCompleted: videoResult.challengesCompleted.length
|
|
133
|
+
});
|
|
123
134
|
await storeVideoRecording({
|
|
124
135
|
frames: videoResult.frames,
|
|
125
136
|
duration: videoResult.duration,
|
|
@@ -128,8 +139,10 @@ const BiometricIdentityFlow = ({ onValidationComplete, onError, theme, language,
|
|
|
128
139
|
challengesCompleted: videoResult.challengesCompleted,
|
|
129
140
|
sessionId: videoResult.sessionId,
|
|
130
141
|
});
|
|
142
|
+
console.log('Video recording stored, starting validation...');
|
|
131
143
|
// Automatically start validation after video
|
|
132
|
-
await validateIdentity();
|
|
144
|
+
const result = await validateIdentity();
|
|
145
|
+
console.log('Validation complete, result:', result);
|
|
133
146
|
}
|
|
134
147
|
}
|
|
135
148
|
catch (error) {
|
|
@@ -158,11 +171,7 @@ const BiometricIdentityFlow = ({ onValidationComplete, onError, theme, language,
|
|
|
158
171
|
}
|
|
159
172
|
// Show instructions on first load
|
|
160
173
|
if (showInstructions) {
|
|
161
|
-
return (react_1.default.createElement(InstructionsScreen_1.InstructionsScreen, { theme: theme, language: language, onStart: () => setShowInstructions(false),
|
|
162
|
-
name: 'BiometricError',
|
|
163
|
-
message: 'User cancelled',
|
|
164
|
-
code: biometric_identity_sdk_core_1.BiometricErrorCode.USER_CANCELLED,
|
|
165
|
-
}) : undefined, styles: customStyles }));
|
|
174
|
+
return (react_1.default.createElement(InstructionsScreen_1.InstructionsScreen, { theme: theme, language: language, onStart: () => setShowInstructions(false), routeBack: routeBack, styles: customStyles }));
|
|
166
175
|
}
|
|
167
176
|
// Show camera/video recorder
|
|
168
177
|
if (showCamera) {
|
|
@@ -176,10 +185,12 @@ const BiometricIdentityFlow = ({ onValidationComplete, onError, theme, language,
|
|
|
176
185
|
}
|
|
177
186
|
// Show validation progress
|
|
178
187
|
if (state.currentStep === biometric_identity_sdk_core_1.SDKStep.VALIDATING) {
|
|
188
|
+
console.log('Rendering ValidationProgress, progress:', state.progress);
|
|
179
189
|
return (react_1.default.createElement(ValidationProgress_1.ValidationProgress, { progress: state.progress, theme: theme, language: language }));
|
|
180
190
|
}
|
|
181
191
|
// Show result
|
|
182
192
|
if (state.currentStep === biometric_identity_sdk_core_1.SDKStep.RESULT && state.validationResult) {
|
|
193
|
+
console.log('Rendering ResultScreen, result:', state.validationResult);
|
|
183
194
|
return (react_1.default.createElement(ResultScreen_1.ResultScreen, { result: state.validationResult, theme: theme, language: language, onClose: () => onValidationComplete(state.validationResult) }));
|
|
184
195
|
}
|
|
185
196
|
// Show error
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"InstructionsScreen.d.ts","sourceRoot":"","sources":["../../src/components/InstructionsScreen.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAA8B,MAAM,OAAO,CAAC;AACnD,OAAO,EAML,SAAS,EACV,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAA2B,MAAM,oCAAoC,CAAC;AAE7G,MAAM,WAAW,uBAAuB;IACtC,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IAC7B,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,
|
|
1
|
+
{"version":3,"file":"InstructionsScreen.d.ts","sourceRoot":"","sources":["../../src/components/InstructionsScreen.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAA8B,MAAM,OAAO,CAAC;AACnD,OAAO,EAML,SAAS,EACV,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAA2B,MAAM,oCAAoC,CAAC;AAE7G,MAAM,WAAW,uBAAuB;IACtC,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IAC7B,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,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,kBAAkB,EAAE,KAAK,CAAC,EAAE,CAAC,uBAAuB,CAyGhE,CAAC;AAuMF,eAAe,kBAAkB,CAAC"}
|
|
@@ -41,7 +41,7 @@ exports.InstructionsScreen = void 0;
|
|
|
41
41
|
const react_1 = __importStar(require("react"));
|
|
42
42
|
const react_native_1 = require("react-native");
|
|
43
43
|
const biometric_identity_sdk_core_1 = require("@hexar/biometric-identity-sdk-core");
|
|
44
|
-
const InstructionsScreen = ({ theme, language = 'en', onStart,
|
|
44
|
+
const InstructionsScreen = ({ theme, language = 'en', onStart, routeBack, styles: customStyles, }) => {
|
|
45
45
|
const [strings, setStrings] = (0, react_1.useState)(() => {
|
|
46
46
|
// Set initial language
|
|
47
47
|
if (language) {
|
|
@@ -61,6 +61,9 @@ const InstructionsScreen = ({ theme, language = 'en', onStart, onCancel, styles:
|
|
|
61
61
|
const tipsContent = getTipsContent(language);
|
|
62
62
|
const privacyContent = getPrivacyContent(language);
|
|
63
63
|
return (react_1.default.createElement(react_native_1.View, { style: [styles.container, customStyles?.container] },
|
|
64
|
+
routeBack && (react_1.default.createElement(react_native_1.View, { style: styles.header },
|
|
65
|
+
react_1.default.createElement(react_native_1.TouchableOpacity, { style: styles.backButton, onPress: routeBack },
|
|
66
|
+
react_1.default.createElement(react_native_1.Text, { style: [styles.backButtonText, { color: theme?.textColor || '#000000' }] }, strings.common.back || 'Volver')))),
|
|
64
67
|
react_1.default.createElement(react_native_1.ScrollView, { contentContainerStyle: [styles.content, customStyles?.content] },
|
|
65
68
|
react_1.default.createElement(react_native_1.Text, { style: [styles.title, { color: theme?.textColor || '#000000' }] }, strings.instructions.title),
|
|
66
69
|
react_1.default.createElement(react_native_1.Text, { style: [styles.subtitle, { color: theme?.secondaryTextColor || '#6B7280' }] }, strings.instructions.subtitle),
|
|
@@ -76,16 +79,9 @@ const InstructionsScreen = ({ theme, language = 'en', onStart, onCancel, styles:
|
|
|
76
79
|
react_1.default.createElement(react_native_1.View, { style: styles.privacyContainer },
|
|
77
80
|
react_1.default.createElement(react_native_1.Text, { style: styles.privacyText }, privacyContent))),
|
|
78
81
|
react_1.default.createElement(react_native_1.View, { style: styles.footer },
|
|
79
|
-
onCancel && (react_1.default.createElement(react_native_1.TouchableOpacity, { style: [
|
|
80
|
-
styles.button,
|
|
81
|
-
styles.cancelButton,
|
|
82
|
-
{ borderColor: theme?.errorColor || '#EF4444' },
|
|
83
|
-
], onPress: onCancel },
|
|
84
|
-
react_1.default.createElement(react_native_1.Text, { style: [styles.buttonText, { color: theme?.errorColor || '#EF4444' }] }, strings.common.cancel || 'Cancel'))),
|
|
85
82
|
react_1.default.createElement(react_native_1.TouchableOpacity, { style: [
|
|
86
83
|
styles.button,
|
|
87
84
|
{ backgroundColor: theme?.primaryColor || '#6366F1' },
|
|
88
|
-
onCancel && styles.startButton,
|
|
89
85
|
], onPress: onStart },
|
|
90
86
|
react_1.default.createElement(react_native_1.Text, { style: styles.buttonText }, strings.instructions.startButton)))));
|
|
91
87
|
};
|
|
@@ -135,6 +131,22 @@ const styles = react_native_1.StyleSheet.create({
|
|
|
135
131
|
flex: 1,
|
|
136
132
|
backgroundColor: '#FFFFFF',
|
|
137
133
|
},
|
|
134
|
+
header: {
|
|
135
|
+
paddingTop: 16,
|
|
136
|
+
paddingHorizontal: 16,
|
|
137
|
+
paddingBottom: 8,
|
|
138
|
+
borderBottomWidth: 1,
|
|
139
|
+
borderBottomColor: '#E5E7EB',
|
|
140
|
+
},
|
|
141
|
+
backButton: {
|
|
142
|
+
paddingVertical: 8,
|
|
143
|
+
paddingHorizontal: 12,
|
|
144
|
+
alignSelf: 'flex-start',
|
|
145
|
+
},
|
|
146
|
+
backButtonText: {
|
|
147
|
+
fontSize: 16,
|
|
148
|
+
fontWeight: '600',
|
|
149
|
+
},
|
|
138
150
|
content: {
|
|
139
151
|
padding: 24,
|
|
140
152
|
},
|
|
@@ -182,22 +194,12 @@ const styles = react_native_1.StyleSheet.create({
|
|
|
182
194
|
padding: 24,
|
|
183
195
|
borderTopWidth: 1,
|
|
184
196
|
borderTopColor: '#E5E7EB',
|
|
185
|
-
flexDirection: 'row',
|
|
186
|
-
gap: 12,
|
|
187
197
|
},
|
|
188
198
|
button: {
|
|
189
199
|
paddingVertical: 16,
|
|
190
200
|
paddingHorizontal: 32,
|
|
191
201
|
borderRadius: 8,
|
|
192
202
|
alignItems: 'center',
|
|
193
|
-
flex: 1,
|
|
194
|
-
},
|
|
195
|
-
cancelButton: {
|
|
196
|
-
borderWidth: 2,
|
|
197
|
-
backgroundColor: 'transparent',
|
|
198
|
-
},
|
|
199
|
-
startButton: {
|
|
200
|
-
flex: 1,
|
|
201
203
|
},
|
|
202
204
|
buttonText: {
|
|
203
205
|
color: '#FFFFFF',
|
|
@@ -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,EAA2B,MAAM,oCAAoC,CAAC;AAGlI,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;AA+CD,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC,kBAAkB,
|
|
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,EAA2B,MAAM,oCAAoC,CAAC;AAGlI,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;AA+CD,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC,kBAAkB,CA8vBtD,CAAC;AA4OF,eAAe,aAAa,CAAC"}
|
|
@@ -275,12 +275,18 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
|
|
|
275
275
|
}, [onCancel]);
|
|
276
276
|
const handleVideoComplete = (0, react_1.useCallback)(async (video) => {
|
|
277
277
|
console.log('handleVideoComplete called with video:', video?.path);
|
|
278
|
+
if (!isRecordingRef.current && phase !== 'processing') {
|
|
279
|
+
console.log('Video already processed, ignoring duplicate callback');
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
278
282
|
try {
|
|
279
283
|
setPhase('processing');
|
|
284
|
+
setOverallProgress(100);
|
|
280
285
|
const actualDuration = Date.now() - recordingStartTime.current;
|
|
281
|
-
console.log('Video
|
|
286
|
+
console.log('Video Processing - Duration:', (actualDuration / 1000).toFixed(1), 's, Min required:', (minDurationMs / 1000).toFixed(1), 's, Frames:', frames.length);
|
|
282
287
|
if (actualDuration < minDurationMs) {
|
|
283
288
|
setPhase('recording');
|
|
289
|
+
setOverallProgress(0);
|
|
284
290
|
react_native_1.Alert.alert(strings.errors.videoTooShort?.title || 'Recording Too Short', strings.errors.videoTooShort?.message || `Video must be at least ${minDurationMs / 1000} seconds. Please try again.`, [{ text: strings.common.retry || 'OK', onPress: resetAndRetry }]);
|
|
285
291
|
return;
|
|
286
292
|
}
|
|
@@ -289,36 +295,47 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
|
|
|
289
295
|
try {
|
|
290
296
|
const RNFS = require('react-native-fs');
|
|
291
297
|
videoBase64 = await RNFS.readFile(video.path, 'base64');
|
|
292
|
-
console.log('Video file read successfully, size:', videoBase64.length);
|
|
298
|
+
console.log('Video file read successfully, size:', videoBase64.length, 'bytes');
|
|
293
299
|
}
|
|
294
300
|
catch (fsError) {
|
|
295
301
|
console.warn('Could not read video file, using captured frames:', fsError);
|
|
296
302
|
}
|
|
297
303
|
}
|
|
304
|
+
const finalFrames = frames.length > 0 ? frames : (videoBase64 ? [videoBase64] : []);
|
|
305
|
+
if (finalFrames.length === 0) {
|
|
306
|
+
console.error('No frames available, cannot complete');
|
|
307
|
+
setPhase('recording');
|
|
308
|
+
handleRecordingError(new Error('No video frames captured'));
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
298
311
|
const result = {
|
|
299
|
-
frames:
|
|
312
|
+
frames: finalFrames,
|
|
300
313
|
duration: actualDuration,
|
|
301
314
|
instructionsFollowed: completedChallenges.length === challenges.length,
|
|
302
|
-
qualityScore:
|
|
315
|
+
qualityScore: finalFrames.length > 0 ? Math.min(100, (finalFrames.length / 30) * 100) : 85,
|
|
303
316
|
challengesCompleted: completedChallenges,
|
|
304
317
|
sessionId,
|
|
305
318
|
};
|
|
306
319
|
console.log('Video recording completed successfully:', {
|
|
307
|
-
duration: actualDuration,
|
|
320
|
+
duration: (actualDuration / 1000).toFixed(1) + 's',
|
|
308
321
|
frames: result.frames.length,
|
|
309
|
-
|
|
310
|
-
instructionsFollowed: result.instructionsFollowed
|
|
322
|
+
challenges: `${completedChallenges.length}/${challenges.length}`,
|
|
323
|
+
instructionsFollowed: result.instructionsFollowed,
|
|
324
|
+
quality: result.qualityScore.toFixed(0) + '%'
|
|
311
325
|
});
|
|
326
|
+
isRecordingRef.current = false;
|
|
312
327
|
onComplete(result);
|
|
313
328
|
}
|
|
314
329
|
catch (error) {
|
|
315
330
|
console.error('Error processing video:', error);
|
|
316
331
|
setPhase('recording');
|
|
332
|
+
setOverallProgress(0);
|
|
317
333
|
handleRecordingError(error);
|
|
318
334
|
}
|
|
319
|
-
}, [frames, completedChallenges, challenges, sessionId, onComplete, resetAndRetry, handleRecordingError, strings, minDurationMs]);
|
|
335
|
+
}, [frames, completedChallenges, challenges, sessionId, onComplete, resetAndRetry, handleRecordingError, strings, minDurationMs, phase]);
|
|
320
336
|
const startFrameCapture = (0, react_1.useCallback)(() => {
|
|
321
337
|
if (cameraRef.current && device) {
|
|
338
|
+
console.log('Starting frame capture mode');
|
|
322
339
|
frameCaptureInterval.current = setInterval(async () => {
|
|
323
340
|
try {
|
|
324
341
|
const photo = await cameraRef.current?.takePhoto({
|
|
@@ -353,37 +370,75 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
|
|
|
353
370
|
}, [device]);
|
|
354
371
|
const stopRecording = (0, react_1.useCallback)(async () => {
|
|
355
372
|
console.log('Stopping recording...');
|
|
356
|
-
|
|
373
|
+
if (frameCaptureInterval.current) {
|
|
374
|
+
clearInterval(frameCaptureInterval.current);
|
|
375
|
+
frameCaptureInterval.current = null;
|
|
376
|
+
}
|
|
357
377
|
if (videoRecordingRef.current) {
|
|
358
378
|
try {
|
|
359
379
|
console.log('Stopping video recording');
|
|
360
|
-
|
|
361
|
-
|
|
380
|
+
const recording = videoRecordingRef.current;
|
|
381
|
+
videoRecordingRef.current = null;
|
|
382
|
+
isRecordingRef.current = false;
|
|
383
|
+
await recording.stop();
|
|
384
|
+
console.log('Video recording stopped - waiting for onRecordingFinished callback');
|
|
362
385
|
}
|
|
363
386
|
catch (error) {
|
|
364
387
|
console.error('Error stopping video recording:', error);
|
|
388
|
+
isRecordingRef.current = false;
|
|
389
|
+
const actualDuration = Date.now() - recordingStartTime.current;
|
|
390
|
+
if (actualDuration >= minDurationMs && frames.length > 0) {
|
|
391
|
+
console.log('Video stopped with error, using captured frames');
|
|
392
|
+
const result = {
|
|
393
|
+
frames,
|
|
394
|
+
duration: actualDuration,
|
|
395
|
+
instructionsFollowed: completedChallenges.length === challenges.length,
|
|
396
|
+
qualityScore: Math.min(100, (frames.length / 30) * 100),
|
|
397
|
+
challengesCompleted: completedChallenges,
|
|
398
|
+
sessionId,
|
|
399
|
+
};
|
|
400
|
+
onComplete(result);
|
|
401
|
+
}
|
|
365
402
|
}
|
|
366
|
-
videoRecordingRef.current = null;
|
|
367
403
|
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
frameCaptureInterval.current = null;
|
|
404
|
+
else {
|
|
405
|
+
isRecordingRef.current = false;
|
|
371
406
|
}
|
|
372
|
-
}, []);
|
|
407
|
+
}, [frames, completedChallenges, challenges, sessionId, onComplete, minDurationMs]);
|
|
373
408
|
const runChallenge = (0, react_1.useCallback)((index) => {
|
|
374
409
|
if (index >= challenges.length) {
|
|
410
|
+
console.log('All challenges completed, stopping recording');
|
|
411
|
+
setOverallProgress(100);
|
|
375
412
|
if (isRecordingRef.current) {
|
|
376
413
|
const elapsed = Date.now() - recordingStartTime.current;
|
|
414
|
+
console.log('Checking duration - Elapsed:', (elapsed / 1000).toFixed(1), 's, Min required:', (minDurationMs / 1000).toFixed(1), 's');
|
|
377
415
|
if (elapsed < minDurationMs) {
|
|
416
|
+
const remaining = minDurationMs - elapsed;
|
|
417
|
+
console.log('Waiting additional', (remaining / 1000).toFixed(1), 's to meet minimum duration');
|
|
378
418
|
setTimeout(() => {
|
|
379
419
|
if (isRecordingRef.current) {
|
|
380
420
|
stopRecording();
|
|
381
421
|
}
|
|
382
|
-
},
|
|
422
|
+
}, remaining);
|
|
383
423
|
return;
|
|
384
424
|
}
|
|
385
425
|
stopRecording();
|
|
386
426
|
}
|
|
427
|
+
else {
|
|
428
|
+
const actualDuration = Date.now() - recordingStartTime.current;
|
|
429
|
+
if (actualDuration >= minDurationMs && frames.length > 0) {
|
|
430
|
+
console.log('Recording already stopped, completing with frames');
|
|
431
|
+
const result = {
|
|
432
|
+
frames,
|
|
433
|
+
duration: actualDuration,
|
|
434
|
+
instructionsFollowed: completedChallenges.length === challenges.length,
|
|
435
|
+
qualityScore: Math.min(100, (frames.length / 30) * 100),
|
|
436
|
+
challengesCompleted: completedChallenges,
|
|
437
|
+
sessionId,
|
|
438
|
+
};
|
|
439
|
+
onComplete(result);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
387
442
|
return;
|
|
388
443
|
}
|
|
389
444
|
const challenge = challenges[index];
|
|
@@ -428,16 +483,14 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
|
|
|
428
483
|
setPhase('recording');
|
|
429
484
|
recordingStartTime.current = Date.now();
|
|
430
485
|
isRecordingRef.current = true;
|
|
431
|
-
console.log('Starting video recording, total duration:', totalDuration);
|
|
486
|
+
console.log('Starting video recording, total duration:', (totalDuration / 1000).toFixed(1), 's');
|
|
432
487
|
if (cameraRef.current && device) {
|
|
433
488
|
try {
|
|
434
489
|
videoRecordingRef.current = await cameraRef.current.startRecording({
|
|
435
490
|
flash: 'off',
|
|
436
491
|
onRecordingFinished: (video) => {
|
|
437
|
-
console.log('Video recording finished callback called', video);
|
|
438
|
-
|
|
439
|
-
handleVideoComplete(video);
|
|
440
|
-
}
|
|
492
|
+
console.log('Video recording finished callback called, path:', video?.path || 'N/A');
|
|
493
|
+
handleVideoComplete(video);
|
|
441
494
|
},
|
|
442
495
|
onRecordingError: (error) => {
|
|
443
496
|
console.error('Recording error:', error);
|
|
@@ -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,EACZ,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,EACZ,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,qBA+NlC,CAAC;AAEF,eAAe,eAAe,CAAC"}
|
|
@@ -137,11 +137,34 @@ const useBiometricSDK = () => {
|
|
|
137
137
|
* Validate identity with all collected data
|
|
138
138
|
*/
|
|
139
139
|
const validateIdentity = (0, react_1.useCallback)(async () => {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
140
|
+
console.log('validateIdentity called, current state:', sdk.getState().currentStep);
|
|
141
|
+
// Update state immediately to show validation screen
|
|
142
|
+
setState(sdk.getState());
|
|
143
|
+
// Poll state during validation to catch intermediate updates
|
|
144
|
+
const pollInterval = setInterval(() => {
|
|
145
|
+
if (isMounted.current) {
|
|
146
|
+
const currentState = sdk.getState();
|
|
147
|
+
setState(currentState);
|
|
148
|
+
console.log('State polled - step:', currentState.currentStep, 'progress:', currentState.progress);
|
|
149
|
+
}
|
|
150
|
+
}, 200);
|
|
151
|
+
try {
|
|
152
|
+
const result = await sdk.validateIdentity();
|
|
153
|
+
clearInterval(pollInterval);
|
|
154
|
+
if (isMounted.current) {
|
|
155
|
+
const finalState = sdk.getState();
|
|
156
|
+
setState(finalState);
|
|
157
|
+
console.log('Validation complete, final state:', finalState.currentStep, 'result:', result);
|
|
158
|
+
}
|
|
159
|
+
return result;
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
clearInterval(pollInterval);
|
|
163
|
+
if (isMounted.current) {
|
|
164
|
+
setState(sdk.getState());
|
|
165
|
+
}
|
|
166
|
+
throw error;
|
|
143
167
|
}
|
|
144
|
-
return result;
|
|
145
168
|
}, [sdk]);
|
|
146
169
|
/**
|
|
147
170
|
* Reset SDK state
|
package/package.json
CHANGED
|
@@ -39,6 +39,7 @@ export interface BiometricIdentityFlowProps {
|
|
|
39
39
|
language?: SupportedLanguage;
|
|
40
40
|
customTranslations?: Record<string, string>;
|
|
41
41
|
smartLivenessMode?: boolean;
|
|
42
|
+
routeBack?: () => void;
|
|
42
43
|
styles?: {
|
|
43
44
|
container?: ViewStyle;
|
|
44
45
|
content?: ViewStyle;
|
|
@@ -52,6 +53,7 @@ export const BiometricIdentityFlow: React.FC<BiometricIdentityFlowProps> = ({
|
|
|
52
53
|
language,
|
|
53
54
|
customTranslations,
|
|
54
55
|
smartLivenessMode = true,
|
|
56
|
+
routeBack,
|
|
55
57
|
styles: customStyles,
|
|
56
58
|
}) => {
|
|
57
59
|
const {
|
|
@@ -133,17 +135,29 @@ export const BiometricIdentityFlow: React.FC<BiometricIdentityFlowProps> = ({
|
|
|
133
135
|
* Handle capture completion
|
|
134
136
|
*/
|
|
135
137
|
const handleCaptureComplete = useCallback(async (data: any) => {
|
|
138
|
+
console.log('handleCaptureComplete called, cameraMode:', cameraMode);
|
|
136
139
|
setShowCamera(false);
|
|
137
140
|
|
|
138
141
|
try {
|
|
139
142
|
if (cameraMode === 'front') {
|
|
143
|
+
console.log('Uploading front ID');
|
|
140
144
|
await uploadFrontID(data);
|
|
145
|
+
console.log('Front ID uploaded successfully');
|
|
141
146
|
} else if (cameraMode === 'back') {
|
|
147
|
+
console.log('Uploading back ID');
|
|
142
148
|
await uploadBackID(data);
|
|
149
|
+
console.log('Back ID uploaded successfully');
|
|
143
150
|
} else if (cameraMode === 'video') {
|
|
151
|
+
console.log('Processing video recording result');
|
|
144
152
|
// Handle video recording result
|
|
145
153
|
const videoResult: VideoRecordingResult = data;
|
|
146
154
|
|
|
155
|
+
console.log('Storing video recording:', {
|
|
156
|
+
frames: videoResult.frames.length,
|
|
157
|
+
duration: videoResult.duration,
|
|
158
|
+
challengesCompleted: videoResult.challengesCompleted.length
|
|
159
|
+
});
|
|
160
|
+
|
|
147
161
|
await storeVideoRecording({
|
|
148
162
|
frames: videoResult.frames,
|
|
149
163
|
duration: videoResult.duration,
|
|
@@ -153,8 +167,10 @@ export const BiometricIdentityFlow: React.FC<BiometricIdentityFlowProps> = ({
|
|
|
153
167
|
sessionId: videoResult.sessionId,
|
|
154
168
|
});
|
|
155
169
|
|
|
170
|
+
console.log('Video recording stored, starting validation...');
|
|
156
171
|
// Automatically start validation after video
|
|
157
|
-
await validateIdentity();
|
|
172
|
+
const result = await validateIdentity();
|
|
173
|
+
console.log('Validation complete, result:', result);
|
|
158
174
|
}
|
|
159
175
|
} catch (error) {
|
|
160
176
|
console.error('Capture error:', error);
|
|
@@ -194,11 +210,7 @@ export const BiometricIdentityFlow: React.FC<BiometricIdentityFlowProps> = ({
|
|
|
194
210
|
theme={theme}
|
|
195
211
|
language={language}
|
|
196
212
|
onStart={() => setShowInstructions(false)}
|
|
197
|
-
|
|
198
|
-
name: 'BiometricError',
|
|
199
|
-
message: 'User cancelled',
|
|
200
|
-
code: BiometricErrorCode.USER_CANCELLED,
|
|
201
|
-
} as BiometricError) : undefined}
|
|
213
|
+
routeBack={routeBack}
|
|
202
214
|
styles={customStyles}
|
|
203
215
|
/>
|
|
204
216
|
);
|
|
@@ -237,6 +249,7 @@ export const BiometricIdentityFlow: React.FC<BiometricIdentityFlowProps> = ({
|
|
|
237
249
|
|
|
238
250
|
// Show validation progress
|
|
239
251
|
if (state.currentStep === SDKStep.VALIDATING) {
|
|
252
|
+
console.log('Rendering ValidationProgress, progress:', state.progress);
|
|
240
253
|
return (
|
|
241
254
|
<ValidationProgress
|
|
242
255
|
progress={state.progress}
|
|
@@ -248,6 +261,7 @@ export const BiometricIdentityFlow: React.FC<BiometricIdentityFlowProps> = ({
|
|
|
248
261
|
|
|
249
262
|
// Show result
|
|
250
263
|
if (state.currentStep === SDKStep.RESULT && state.validationResult) {
|
|
264
|
+
console.log('Rendering ResultScreen, result:', state.validationResult);
|
|
251
265
|
return (
|
|
252
266
|
<ResultScreen
|
|
253
267
|
result={state.validationResult}
|
|
@@ -18,7 +18,7 @@ export interface InstructionsScreenProps {
|
|
|
18
18
|
theme?: ThemeConfig;
|
|
19
19
|
language?: SupportedLanguage;
|
|
20
20
|
onStart: () => void;
|
|
21
|
-
|
|
21
|
+
routeBack?: () => void;
|
|
22
22
|
styles?: {
|
|
23
23
|
container?: ViewStyle;
|
|
24
24
|
content?: ViewStyle;
|
|
@@ -29,7 +29,7 @@ export const InstructionsScreen: React.FC<InstructionsScreenProps> = ({
|
|
|
29
29
|
theme,
|
|
30
30
|
language = 'en',
|
|
31
31
|
onStart,
|
|
32
|
-
|
|
32
|
+
routeBack,
|
|
33
33
|
styles: customStyles,
|
|
34
34
|
}) => {
|
|
35
35
|
const [strings, setStrings] = useState(() => {
|
|
@@ -55,6 +55,18 @@ export const InstructionsScreen: React.FC<InstructionsScreenProps> = ({
|
|
|
55
55
|
|
|
56
56
|
return (
|
|
57
57
|
<View style={[styles.container, customStyles?.container]}>
|
|
58
|
+
{routeBack && (
|
|
59
|
+
<View style={styles.header}>
|
|
60
|
+
<TouchableOpacity
|
|
61
|
+
style={styles.backButton}
|
|
62
|
+
onPress={routeBack}
|
|
63
|
+
>
|
|
64
|
+
<Text style={[styles.backButtonText, { color: theme?.textColor || '#000000' }]}>
|
|
65
|
+
{strings.common.back || 'Volver'}
|
|
66
|
+
</Text>
|
|
67
|
+
</TouchableOpacity>
|
|
68
|
+
</View>
|
|
69
|
+
)}
|
|
58
70
|
<ScrollView contentContainerStyle={[styles.content, customStyles?.content]}>
|
|
59
71
|
<Text style={[styles.title, { color: theme?.textColor || '#000000' }]}>
|
|
60
72
|
{strings.instructions.title}
|
|
@@ -104,27 +116,12 @@ export const InstructionsScreen: React.FC<InstructionsScreenProps> = ({
|
|
|
104
116
|
</View>
|
|
105
117
|
</ScrollView>
|
|
106
118
|
|
|
107
|
-
{/*
|
|
119
|
+
{/* Start Button */}
|
|
108
120
|
<View style={styles.footer}>
|
|
109
|
-
{onCancel && (
|
|
110
|
-
<TouchableOpacity
|
|
111
|
-
style={[
|
|
112
|
-
styles.button,
|
|
113
|
-
styles.cancelButton,
|
|
114
|
-
{ borderColor: theme?.errorColor || '#EF4444' },
|
|
115
|
-
]}
|
|
116
|
-
onPress={onCancel}
|
|
117
|
-
>
|
|
118
|
-
<Text style={[styles.buttonText, { color: theme?.errorColor || '#EF4444' }]}>
|
|
119
|
-
{strings.common.cancel || 'Cancel'}
|
|
120
|
-
</Text>
|
|
121
|
-
</TouchableOpacity>
|
|
122
|
-
)}
|
|
123
121
|
<TouchableOpacity
|
|
124
122
|
style={[
|
|
125
123
|
styles.button,
|
|
126
124
|
{ backgroundColor: theme?.primaryColor || '#6366F1' },
|
|
127
|
-
onCancel && styles.startButton,
|
|
128
125
|
]}
|
|
129
126
|
onPress={onStart}
|
|
130
127
|
>
|
|
@@ -205,6 +202,22 @@ const styles = StyleSheet.create({
|
|
|
205
202
|
flex: 1,
|
|
206
203
|
backgroundColor: '#FFFFFF',
|
|
207
204
|
},
|
|
205
|
+
header: {
|
|
206
|
+
paddingTop: 16,
|
|
207
|
+
paddingHorizontal: 16,
|
|
208
|
+
paddingBottom: 8,
|
|
209
|
+
borderBottomWidth: 1,
|
|
210
|
+
borderBottomColor: '#E5E7EB',
|
|
211
|
+
},
|
|
212
|
+
backButton: {
|
|
213
|
+
paddingVertical: 8,
|
|
214
|
+
paddingHorizontal: 12,
|
|
215
|
+
alignSelf: 'flex-start',
|
|
216
|
+
},
|
|
217
|
+
backButtonText: {
|
|
218
|
+
fontSize: 16,
|
|
219
|
+
fontWeight: '600',
|
|
220
|
+
},
|
|
208
221
|
content: {
|
|
209
222
|
padding: 24,
|
|
210
223
|
},
|
|
@@ -252,22 +265,12 @@ const styles = StyleSheet.create({
|
|
|
252
265
|
padding: 24,
|
|
253
266
|
borderTopWidth: 1,
|
|
254
267
|
borderTopColor: '#E5E7EB',
|
|
255
|
-
flexDirection: 'row',
|
|
256
|
-
gap: 12,
|
|
257
268
|
},
|
|
258
269
|
button: {
|
|
259
270
|
paddingVertical: 16,
|
|
260
271
|
paddingHorizontal: 32,
|
|
261
272
|
borderRadius: 8,
|
|
262
273
|
alignItems: 'center',
|
|
263
|
-
flex: 1,
|
|
264
|
-
},
|
|
265
|
-
cancelButton: {
|
|
266
|
-
borderWidth: 2,
|
|
267
|
-
backgroundColor: 'transparent',
|
|
268
|
-
},
|
|
269
|
-
startButton: {
|
|
270
|
-
flex: 1,
|
|
271
274
|
},
|
|
272
275
|
buttonText: {
|
|
273
276
|
color: '#FFFFFF',
|
|
@@ -330,14 +330,21 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
|
|
|
330
330
|
const handleVideoComplete = useCallback(async (video: any) => {
|
|
331
331
|
console.log('handleVideoComplete called with video:', video?.path);
|
|
332
332
|
|
|
333
|
+
if (!isRecordingRef.current && phase !== 'processing') {
|
|
334
|
+
console.log('Video already processed, ignoring duplicate callback');
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
|
|
333
338
|
try {
|
|
334
339
|
setPhase('processing');
|
|
335
|
-
|
|
340
|
+
setOverallProgress(100);
|
|
336
341
|
|
|
337
|
-
|
|
342
|
+
const actualDuration = Date.now() - recordingStartTime.current;
|
|
343
|
+
console.log('Video Processing - Duration:', (actualDuration / 1000).toFixed(1), 's, Min required:', (minDurationMs / 1000).toFixed(1), 's, Frames:', frames.length);
|
|
338
344
|
|
|
339
345
|
if (actualDuration < minDurationMs) {
|
|
340
346
|
setPhase('recording');
|
|
347
|
+
setOverallProgress(0);
|
|
341
348
|
Alert.alert(
|
|
342
349
|
strings.errors.videoTooShort?.title || 'Recording Too Short',
|
|
343
350
|
strings.errors.videoTooShort?.message || `Video must be at least ${minDurationMs / 1000} seconds. Please try again.`,
|
|
@@ -351,38 +358,51 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
|
|
|
351
358
|
try {
|
|
352
359
|
const RNFS = require('react-native-fs');
|
|
353
360
|
videoBase64 = await RNFS.readFile(video.path, 'base64');
|
|
354
|
-
console.log('Video file read successfully, size:', videoBase64.length);
|
|
361
|
+
console.log('Video file read successfully, size:', videoBase64.length, 'bytes');
|
|
355
362
|
} catch (fsError) {
|
|
356
363
|
console.warn('Could not read video file, using captured frames:', fsError);
|
|
357
364
|
}
|
|
358
365
|
}
|
|
359
366
|
|
|
367
|
+
const finalFrames = frames.length > 0 ? frames : (videoBase64 ? [videoBase64] : []);
|
|
368
|
+
|
|
369
|
+
if (finalFrames.length === 0) {
|
|
370
|
+
console.error('No frames available, cannot complete');
|
|
371
|
+
setPhase('recording');
|
|
372
|
+
handleRecordingError(new Error('No video frames captured'));
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
|
|
360
376
|
const result: VideoRecordingResult = {
|
|
361
|
-
frames:
|
|
377
|
+
frames: finalFrames,
|
|
362
378
|
duration: actualDuration,
|
|
363
379
|
instructionsFollowed: completedChallenges.length === challenges.length,
|
|
364
|
-
qualityScore:
|
|
380
|
+
qualityScore: finalFrames.length > 0 ? Math.min(100, (finalFrames.length / 30) * 100) : 85,
|
|
365
381
|
challengesCompleted: completedChallenges,
|
|
366
382
|
sessionId,
|
|
367
383
|
};
|
|
368
384
|
|
|
369
385
|
console.log('Video recording completed successfully:', {
|
|
370
|
-
duration: actualDuration,
|
|
386
|
+
duration: (actualDuration / 1000).toFixed(1) + 's',
|
|
371
387
|
frames: result.frames.length,
|
|
372
|
-
|
|
373
|
-
instructionsFollowed: result.instructionsFollowed
|
|
388
|
+
challenges: `${completedChallenges.length}/${challenges.length}`,
|
|
389
|
+
instructionsFollowed: result.instructionsFollowed,
|
|
390
|
+
quality: result.qualityScore.toFixed(0) + '%'
|
|
374
391
|
});
|
|
375
392
|
|
|
393
|
+
isRecordingRef.current = false;
|
|
376
394
|
onComplete(result);
|
|
377
395
|
} catch (error) {
|
|
378
396
|
console.error('Error processing video:', error);
|
|
379
397
|
setPhase('recording');
|
|
398
|
+
setOverallProgress(0);
|
|
380
399
|
handleRecordingError(error);
|
|
381
400
|
}
|
|
382
|
-
}, [frames, completedChallenges, challenges, sessionId, onComplete, resetAndRetry, handleRecordingError, strings, minDurationMs]);
|
|
401
|
+
}, [frames, completedChallenges, challenges, sessionId, onComplete, resetAndRetry, handleRecordingError, strings, minDurationMs, phase]);
|
|
383
402
|
|
|
384
403
|
const startFrameCapture = useCallback(() => {
|
|
385
404
|
if (cameraRef.current && device) {
|
|
405
|
+
console.log('Starting frame capture mode');
|
|
386
406
|
frameCaptureInterval.current = setInterval(async () => {
|
|
387
407
|
try {
|
|
388
408
|
const photo = await cameraRef.current?.takePhoto({
|
|
@@ -417,38 +437,79 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
|
|
|
417
437
|
|
|
418
438
|
const stopRecording = useCallback(async () => {
|
|
419
439
|
console.log('Stopping recording...');
|
|
420
|
-
|
|
440
|
+
|
|
441
|
+
if (frameCaptureInterval.current) {
|
|
442
|
+
clearInterval(frameCaptureInterval.current);
|
|
443
|
+
frameCaptureInterval.current = null;
|
|
444
|
+
}
|
|
421
445
|
|
|
422
446
|
if (videoRecordingRef.current) {
|
|
423
447
|
try {
|
|
424
448
|
console.log('Stopping video recording');
|
|
425
|
-
|
|
426
|
-
|
|
449
|
+
const recording = videoRecordingRef.current;
|
|
450
|
+
videoRecordingRef.current = null;
|
|
451
|
+
isRecordingRef.current = false;
|
|
452
|
+
|
|
453
|
+
await recording.stop();
|
|
454
|
+
console.log('Video recording stopped - waiting for onRecordingFinished callback');
|
|
427
455
|
} catch (error) {
|
|
428
456
|
console.error('Error stopping video recording:', error);
|
|
457
|
+
isRecordingRef.current = false;
|
|
458
|
+
|
|
459
|
+
const actualDuration = Date.now() - recordingStartTime.current;
|
|
460
|
+
if (actualDuration >= minDurationMs && frames.length > 0) {
|
|
461
|
+
console.log('Video stopped with error, using captured frames');
|
|
462
|
+
const result: VideoRecordingResult = {
|
|
463
|
+
frames,
|
|
464
|
+
duration: actualDuration,
|
|
465
|
+
instructionsFollowed: completedChallenges.length === challenges.length,
|
|
466
|
+
qualityScore: Math.min(100, (frames.length / 30) * 100),
|
|
467
|
+
challengesCompleted: completedChallenges,
|
|
468
|
+
sessionId,
|
|
469
|
+
};
|
|
470
|
+
onComplete(result);
|
|
471
|
+
}
|
|
429
472
|
}
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
if (frameCaptureInterval.current) {
|
|
434
|
-
clearInterval(frameCaptureInterval.current);
|
|
435
|
-
frameCaptureInterval.current = null;
|
|
473
|
+
} else {
|
|
474
|
+
isRecordingRef.current = false;
|
|
436
475
|
}
|
|
437
|
-
}, []);
|
|
476
|
+
}, [frames, completedChallenges, challenges, sessionId, onComplete, minDurationMs]);
|
|
438
477
|
|
|
439
478
|
const runChallenge = useCallback((index: number) => {
|
|
440
479
|
if (index >= challenges.length) {
|
|
480
|
+
console.log('All challenges completed, stopping recording');
|
|
481
|
+
setOverallProgress(100);
|
|
482
|
+
|
|
441
483
|
if (isRecordingRef.current) {
|
|
442
484
|
const elapsed = Date.now() - recordingStartTime.current;
|
|
485
|
+
console.log('Checking duration - Elapsed:', (elapsed / 1000).toFixed(1), 's, Min required:', (minDurationMs / 1000).toFixed(1), 's');
|
|
486
|
+
|
|
443
487
|
if (elapsed < minDurationMs) {
|
|
488
|
+
const remaining = minDurationMs - elapsed;
|
|
489
|
+
console.log('Waiting additional', (remaining / 1000).toFixed(1), 's to meet minimum duration');
|
|
444
490
|
setTimeout(() => {
|
|
445
491
|
if (isRecordingRef.current) {
|
|
446
492
|
stopRecording();
|
|
447
493
|
}
|
|
448
|
-
},
|
|
494
|
+
}, remaining);
|
|
449
495
|
return;
|
|
450
496
|
}
|
|
497
|
+
|
|
451
498
|
stopRecording();
|
|
499
|
+
} else {
|
|
500
|
+
const actualDuration = Date.now() - recordingStartTime.current;
|
|
501
|
+
if (actualDuration >= minDurationMs && frames.length > 0) {
|
|
502
|
+
console.log('Recording already stopped, completing with frames');
|
|
503
|
+
const result: VideoRecordingResult = {
|
|
504
|
+
frames,
|
|
505
|
+
duration: actualDuration,
|
|
506
|
+
instructionsFollowed: completedChallenges.length === challenges.length,
|
|
507
|
+
qualityScore: Math.min(100, (frames.length / 30) * 100),
|
|
508
|
+
challengesCompleted: completedChallenges,
|
|
509
|
+
sessionId,
|
|
510
|
+
};
|
|
511
|
+
onComplete(result);
|
|
512
|
+
}
|
|
452
513
|
}
|
|
453
514
|
return;
|
|
454
515
|
}
|
|
@@ -505,17 +566,15 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
|
|
|
505
566
|
recordingStartTime.current = Date.now();
|
|
506
567
|
isRecordingRef.current = true;
|
|
507
568
|
|
|
508
|
-
console.log('Starting video recording, total duration:', totalDuration);
|
|
569
|
+
console.log('Starting video recording, total duration:', (totalDuration / 1000).toFixed(1), 's');
|
|
509
570
|
|
|
510
571
|
if (cameraRef.current && device) {
|
|
511
572
|
try {
|
|
512
573
|
videoRecordingRef.current = await cameraRef.current.startRecording({
|
|
513
574
|
flash: 'off',
|
|
514
575
|
onRecordingFinished: (video: any) => {
|
|
515
|
-
console.log('Video recording finished callback called', video);
|
|
516
|
-
|
|
517
|
-
handleVideoComplete(video);
|
|
518
|
-
}
|
|
576
|
+
console.log('Video recording finished callback called, path:', video?.path || 'N/A');
|
|
577
|
+
handleVideoComplete(video);
|
|
519
578
|
},
|
|
520
579
|
onRecordingError: (error: any) => {
|
|
521
580
|
console.error('Recording error:', error);
|
|
@@ -192,11 +192,37 @@ export const useBiometricSDK = (): UseBiometricSDKResult => {
|
|
|
192
192
|
* Validate identity with all collected data
|
|
193
193
|
*/
|
|
194
194
|
const validateIdentity = useCallback(async () => {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
195
|
+
console.log('validateIdentity called, current state:', sdk.getState().currentStep);
|
|
196
|
+
|
|
197
|
+
// Update state immediately to show validation screen
|
|
198
|
+
setState(sdk.getState());
|
|
199
|
+
|
|
200
|
+
// Poll state during validation to catch intermediate updates
|
|
201
|
+
const pollInterval = setInterval(() => {
|
|
202
|
+
if (isMounted.current) {
|
|
203
|
+
const currentState = sdk.getState();
|
|
204
|
+
setState(currentState);
|
|
205
|
+
console.log('State polled - step:', currentState.currentStep, 'progress:', currentState.progress);
|
|
206
|
+
}
|
|
207
|
+
}, 200);
|
|
208
|
+
|
|
209
|
+
try {
|
|
210
|
+
const result = await sdk.validateIdentity();
|
|
211
|
+
clearInterval(pollInterval);
|
|
212
|
+
|
|
213
|
+
if (isMounted.current) {
|
|
214
|
+
const finalState = sdk.getState();
|
|
215
|
+
setState(finalState);
|
|
216
|
+
console.log('Validation complete, final state:', finalState.currentStep, 'result:', result);
|
|
217
|
+
}
|
|
218
|
+
return result;
|
|
219
|
+
} catch (error) {
|
|
220
|
+
clearInterval(pollInterval);
|
|
221
|
+
if (isMounted.current) {
|
|
222
|
+
setState(sdk.getState());
|
|
223
|
+
}
|
|
224
|
+
throw error;
|
|
198
225
|
}
|
|
199
|
-
return result;
|
|
200
226
|
}, [sdk]);
|
|
201
227
|
|
|
202
228
|
/**
|