@hexar/biometric-identity-sdk-react-native 1.1.27 → 1.3.0
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 +18 -2
- package/dist/components/FacePositioningGuide.d.ts +22 -0
- package/dist/components/FacePositioningGuide.d.ts.map +1 -0
- package/dist/components/FacePositioningGuide.js +159 -0
- package/dist/components/ProfilePictureCapture.d.ts.map +1 -1
- package/dist/components/ProfilePictureCapture.js +6 -1
- package/dist/components/VideoRecorder.d.ts.map +1 -1
- package/dist/components/VideoRecorder.js +20 -15
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/package.json +2 -2
- package/src/components/BiometricIdentityFlow.tsx +31 -4
- package/src/components/FacePositioningGuide.tsx +222 -0
- package/src/components/ProfilePictureCapture.tsx +14 -1
- package/src/components/VideoRecorder.tsx +27 -20
- package/src/index.ts +1 -0
|
@@ -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;
|
|
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;AA+B5C,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,CAictE,CAAC;AA+NF,eAAe,qBAAqB,CAAC"}
|
|
@@ -67,11 +67,13 @@ const ValidationProgress_1 = require("./ValidationProgress");
|
|
|
67
67
|
const ResultScreen_1 = require("./ResultScreen");
|
|
68
68
|
const ErrorScreen_1 = require("./ErrorScreen");
|
|
69
69
|
const InstructionsScreen_1 = require("./InstructionsScreen");
|
|
70
|
+
const FacePositioningGuide_1 = require("./FacePositioningGuide");
|
|
70
71
|
const BiometricIdentityFlow = ({ onValidationComplete, onError, theme, language, customTranslations, smartLivenessMode = true, routeBack, styles: customStyles, }) => {
|
|
71
72
|
const { sdk, state, isInitialized, isUsingBackend, challenges, uploadFrontID, uploadBackID, storeVideoRecording, fetchChallenges, validateIdentity, reset, } = (0, useBiometricSDK_1.useBiometricSDK)();
|
|
72
73
|
const [showCamera, setShowCamera] = (0, react_1.useState)(false);
|
|
73
74
|
const [cameraMode, setCameraMode] = (0, react_1.useState)('front');
|
|
74
75
|
const [showInstructions, setShowInstructions] = (0, react_1.useState)(true);
|
|
76
|
+
const [showFacePositioningGuide, setShowFacePositioningGuide] = (0, react_1.useState)(false);
|
|
75
77
|
const [currentChallenges, setCurrentChallenges] = (0, react_1.useState)([]);
|
|
76
78
|
const [isLoadingChallenges, setIsLoadingChallenges] = (0, react_1.useState)(false);
|
|
77
79
|
const onValidationCompleteRef = (0, react_1.useRef)(onValidationComplete);
|
|
@@ -117,7 +119,15 @@ const BiometricIdentityFlow = ({ onValidationComplete, onError, theme, language,
|
|
|
117
119
|
*/
|
|
118
120
|
const handleCaptureStart = (0, react_1.useCallback)(async (mode) => {
|
|
119
121
|
setCameraMode(mode);
|
|
120
|
-
if (mode === 'video'
|
|
122
|
+
if (mode === 'video') {
|
|
123
|
+
setShowFacePositioningGuide(true);
|
|
124
|
+
// Challenges will be loaded when guide is dismissed and we show camera
|
|
125
|
+
}
|
|
126
|
+
if (mode !== 'video') {
|
|
127
|
+
setShowCamera(true);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (smartLivenessMode && isUsingBackend) {
|
|
121
131
|
setIsLoadingChallenges(true);
|
|
122
132
|
try {
|
|
123
133
|
const challenges = await fetchChallenges('active');
|
|
@@ -186,8 +196,11 @@ const BiometricIdentityFlow = ({ onValidationComplete, onError, theme, language,
|
|
|
186
196
|
});
|
|
187
197
|
setCurrentChallenges(translatedChallenges);
|
|
188
198
|
}
|
|
189
|
-
setShowCamera(true);
|
|
190
199
|
}, [smartLivenessMode, isUsingBackend, fetchChallenges, sdk]);
|
|
200
|
+
const handleFacePositioningContinue = (0, react_1.useCallback)(() => {
|
|
201
|
+
setShowFacePositioningGuide(false);
|
|
202
|
+
setShowCamera(true);
|
|
203
|
+
}, []);
|
|
191
204
|
/**
|
|
192
205
|
* Handle capture completion
|
|
193
206
|
*/
|
|
@@ -287,6 +300,9 @@ const BiometricIdentityFlow = ({ onValidationComplete, onError, theme, language,
|
|
|
287
300
|
if (showInstructions) {
|
|
288
301
|
return (react_1.default.createElement(InstructionsScreen_1.InstructionsScreen, { theme: theme, language: language, onStart: () => setShowInstructions(false), routeBack: routeBack, styles: customStyles }));
|
|
289
302
|
}
|
|
303
|
+
if (showFacePositioningGuide) {
|
|
304
|
+
return (react_1.default.createElement(FacePositioningGuide_1.FacePositioningGuide, { theme: theme, language: language, onContinue: handleFacePositioningContinue, onCancel: () => setShowFacePositioningGuide(false), styles: customStyles }));
|
|
305
|
+
}
|
|
290
306
|
if (showCamera) {
|
|
291
307
|
if (cameraMode === 'video') {
|
|
292
308
|
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 () => {
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Face Positioning Guide Component
|
|
3
|
+
* Shared view that instructs users to keep their face inside the oval
|
|
4
|
+
* for better approval rate. Used before video capture in both
|
|
5
|
+
* BiometricIdentityFlow and ProfilePictureCapture.
|
|
6
|
+
*/
|
|
7
|
+
import React from 'react';
|
|
8
|
+
import { ViewStyle } from 'react-native';
|
|
9
|
+
import { ThemeConfig, SupportedLanguage } from '@hexar/biometric-identity-sdk-core';
|
|
10
|
+
export interface FacePositioningGuideProps {
|
|
11
|
+
theme?: ThemeConfig;
|
|
12
|
+
language?: SupportedLanguage;
|
|
13
|
+
onContinue: () => void;
|
|
14
|
+
onCancel?: () => void;
|
|
15
|
+
styles?: {
|
|
16
|
+
container?: ViewStyle;
|
|
17
|
+
content?: ViewStyle;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export declare const FacePositioningGuide: React.FC<FacePositioningGuideProps>;
|
|
21
|
+
export default FacePositioningGuide;
|
|
22
|
+
//# sourceMappingURL=FacePositioningGuide.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FacePositioningGuide.d.ts","sourceRoot":"","sources":["../../src/components/FacePositioningGuide.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAOL,SAAS,EACV,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAA2B,MAAM,oCAAoC,CAAC;AAE7G,MAAM,WAAW,yBAAyB;IACxC,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IAC7B,UAAU,EAAE,MAAM,IAAI,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,MAAM,CAAC,EAAE;QACP,SAAS,CAAC,EAAE,SAAS,CAAC;QACtB,OAAO,CAAC,EAAE,SAAS,CAAC;KACrB,CAAC;CACH;AAED,eAAO,MAAM,oBAAoB,EAAE,KAAK,CAAC,EAAE,CAAC,yBAAyB,CAqEpE,CAAC;AA0HF,eAAe,oBAAoB,CAAC"}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Face Positioning Guide Component
|
|
4
|
+
* Shared view that instructs users to keep their face inside the oval
|
|
5
|
+
* for better approval rate. Used before video capture in both
|
|
6
|
+
* BiometricIdentityFlow and ProfilePictureCapture.
|
|
7
|
+
*/
|
|
8
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
9
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.FacePositioningGuide = void 0;
|
|
13
|
+
const react_1 = __importDefault(require("react"));
|
|
14
|
+
const react_native_1 = require("react-native");
|
|
15
|
+
const biometric_identity_sdk_core_1 = require("@hexar/biometric-identity-sdk-core");
|
|
16
|
+
const FacePositioningGuide = ({ theme, language, onContinue, onCancel, styles: customStyles, }) => {
|
|
17
|
+
if (language) {
|
|
18
|
+
(0, biometric_identity_sdk_core_1.setLanguage)(language);
|
|
19
|
+
}
|
|
20
|
+
const strings = (0, biometric_identity_sdk_core_1.getStrings)();
|
|
21
|
+
const liveness = strings.liveness;
|
|
22
|
+
const guide = liveness?.facePositioningGuide;
|
|
23
|
+
const title = guide?.title ?? 'Position your face';
|
|
24
|
+
const description = guide?.description ?? 'Keep your face inside the oval for the best approval rate. Positioning your face outside the frame may reduce your chances of approval.';
|
|
25
|
+
const tip = guide?.tip ?? 'Face inside = better approval • Face outside = lower chances';
|
|
26
|
+
const continueLabel = guide?.continue ?? strings.common?.continue ?? 'Continue';
|
|
27
|
+
const primaryColor = theme?.primaryColor ?? '#6366F1';
|
|
28
|
+
const textColor = theme?.textColor ?? '#000000';
|
|
29
|
+
const secondaryTextColor = theme?.secondaryTextColor ?? '#6B7280';
|
|
30
|
+
const backgroundColor = theme?.backgroundColor ?? '#FFFFFF';
|
|
31
|
+
const styles = createStyles({
|
|
32
|
+
primaryColor,
|
|
33
|
+
textColor,
|
|
34
|
+
secondaryTextColor,
|
|
35
|
+
backgroundColor,
|
|
36
|
+
});
|
|
37
|
+
return (react_1.default.createElement(react_native_1.SafeAreaView, { style: [styles.container, customStyles?.container] },
|
|
38
|
+
onCancel && (react_1.default.createElement(react_native_1.TouchableOpacity, { style: styles.cancelButton, onPress: onCancel },
|
|
39
|
+
react_1.default.createElement(react_native_1.Text, { style: [styles.cancelText, { color: textColor }] }, strings.common?.cancel ?? 'Cancel'))),
|
|
40
|
+
react_1.default.createElement(react_native_1.ScrollView, { contentContainerStyle: [styles.scrollContent, customStyles?.content], showsVerticalScrollIndicator: false },
|
|
41
|
+
react_1.default.createElement(react_native_1.Text, { style: [styles.title, { color: textColor }] }, title),
|
|
42
|
+
react_1.default.createElement(react_native_1.Text, { style: [styles.description, { color: secondaryTextColor }] }, description),
|
|
43
|
+
react_1.default.createElement(react_native_1.View, { style: styles.ovalWrapper },
|
|
44
|
+
react_1.default.createElement(react_native_1.View, { style: [styles.oval, { borderColor: primaryColor }] },
|
|
45
|
+
react_1.default.createElement(FaceIcon, { color: secondaryTextColor }))),
|
|
46
|
+
react_1.default.createElement(react_native_1.View, { style: [styles.tipBox, { borderColor: primaryColor + '30', backgroundColor: primaryColor + '08' }] },
|
|
47
|
+
react_1.default.createElement(react_native_1.Text, { style: [styles.tipText, { color: secondaryTextColor }] }, tip)),
|
|
48
|
+
react_1.default.createElement(react_native_1.TouchableOpacity, { style: [styles.continueButton, { backgroundColor: primaryColor }], onPress: onContinue, activeOpacity: 0.85 },
|
|
49
|
+
react_1.default.createElement(react_native_1.Text, { style: styles.continueButtonText }, continueLabel)))));
|
|
50
|
+
};
|
|
51
|
+
exports.FacePositioningGuide = FacePositioningGuide;
|
|
52
|
+
/** Simple geometric face icon (head + eyes) when react-native-svg is not used */
|
|
53
|
+
const FaceIcon = ({ color }) => (react_1.default.createElement(react_native_1.View, { style: faceIconStyles.container },
|
|
54
|
+
react_1.default.createElement(react_native_1.View, { style: [faceIconStyles.head, { backgroundColor: color }] }),
|
|
55
|
+
react_1.default.createElement(react_native_1.View, { style: faceIconStyles.eyes },
|
|
56
|
+
react_1.default.createElement(react_native_1.View, { style: [faceIconStyles.eye, { backgroundColor: color }] }),
|
|
57
|
+
react_1.default.createElement(react_native_1.View, { style: [faceIconStyles.eye, { backgroundColor: color }] }))));
|
|
58
|
+
const faceIconStyles = react_native_1.StyleSheet.create({
|
|
59
|
+
container: {
|
|
60
|
+
alignItems: 'center',
|
|
61
|
+
justifyContent: 'center',
|
|
62
|
+
},
|
|
63
|
+
head: {
|
|
64
|
+
width: 80,
|
|
65
|
+
height: 100,
|
|
66
|
+
borderRadius: 50,
|
|
67
|
+
opacity: 0.4,
|
|
68
|
+
},
|
|
69
|
+
eyes: {
|
|
70
|
+
flexDirection: 'row',
|
|
71
|
+
justifyContent: 'space-between',
|
|
72
|
+
width: 44,
|
|
73
|
+
marginTop: -70,
|
|
74
|
+
opacity: 0.5,
|
|
75
|
+
},
|
|
76
|
+
eye: {
|
|
77
|
+
width: 12,
|
|
78
|
+
height: 12,
|
|
79
|
+
borderRadius: 6,
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
const createStyles = (vars) => react_native_1.StyleSheet.create({
|
|
83
|
+
container: {
|
|
84
|
+
flex: 1,
|
|
85
|
+
backgroundColor: vars.backgroundColor,
|
|
86
|
+
zIndex: 9999,
|
|
87
|
+
},
|
|
88
|
+
cancelButton: {
|
|
89
|
+
alignSelf: 'flex-start',
|
|
90
|
+
paddingVertical: 12,
|
|
91
|
+
paddingHorizontal: 16,
|
|
92
|
+
marginTop: 8,
|
|
93
|
+
},
|
|
94
|
+
cancelText: {
|
|
95
|
+
fontSize: 16,
|
|
96
|
+
fontWeight: '500',
|
|
97
|
+
},
|
|
98
|
+
scrollContent: {
|
|
99
|
+
paddingHorizontal: 24,
|
|
100
|
+
paddingBottom: 40,
|
|
101
|
+
alignItems: 'center',
|
|
102
|
+
},
|
|
103
|
+
title: {
|
|
104
|
+
fontSize: 24,
|
|
105
|
+
fontWeight: '700',
|
|
106
|
+
marginBottom: 12,
|
|
107
|
+
textAlign: 'center',
|
|
108
|
+
},
|
|
109
|
+
description: {
|
|
110
|
+
fontSize: 16,
|
|
111
|
+
lineHeight: 24,
|
|
112
|
+
textAlign: 'center',
|
|
113
|
+
marginBottom: 28,
|
|
114
|
+
paddingHorizontal: 8,
|
|
115
|
+
},
|
|
116
|
+
ovalWrapper: {
|
|
117
|
+
marginBottom: 24,
|
|
118
|
+
},
|
|
119
|
+
oval: {
|
|
120
|
+
width: 260,
|
|
121
|
+
height: 320,
|
|
122
|
+
borderRadius: 160,
|
|
123
|
+
borderWidth: 3,
|
|
124
|
+
alignItems: 'center',
|
|
125
|
+
justifyContent: 'center',
|
|
126
|
+
overflow: 'hidden',
|
|
127
|
+
},
|
|
128
|
+
tipBox: {
|
|
129
|
+
paddingVertical: 14,
|
|
130
|
+
paddingHorizontal: 20,
|
|
131
|
+
borderRadius: 12,
|
|
132
|
+
borderWidth: 1,
|
|
133
|
+
marginBottom: 32,
|
|
134
|
+
alignSelf: 'stretch',
|
|
135
|
+
},
|
|
136
|
+
tipText: {
|
|
137
|
+
fontSize: 14,
|
|
138
|
+
fontWeight: '600',
|
|
139
|
+
textAlign: 'center',
|
|
140
|
+
},
|
|
141
|
+
continueButton: {
|
|
142
|
+
paddingVertical: 16,
|
|
143
|
+
paddingHorizontal: 32,
|
|
144
|
+
borderRadius: 12,
|
|
145
|
+
alignItems: 'center',
|
|
146
|
+
alignSelf: 'stretch',
|
|
147
|
+
shadowColor: '#000',
|
|
148
|
+
shadowOffset: { width: 0, height: 2 },
|
|
149
|
+
shadowOpacity: 0.1,
|
|
150
|
+
shadowRadius: 4,
|
|
151
|
+
elevation: 3,
|
|
152
|
+
},
|
|
153
|
+
continueButtonText: {
|
|
154
|
+
color: '#FFFFFF',
|
|
155
|
+
fontSize: 18,
|
|
156
|
+
fontWeight: '700',
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
exports.default = exports.FacePositioningGuide;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ProfilePictureCapture.d.ts","sourceRoot":"","sources":["../../src/components/ProfilePictureCapture.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAmD,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"ProfilePictureCapture.d.ts","sourceRoot":"","sources":["../../src/components/ProfilePictureCapture.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAmD,MAAM,OAAO,CAAC;AAWxE,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAAmC,cAAc,EAAsB,MAAM,oCAAoC,CAAC;AAGzJ,MAAM,WAAW,8BAA8B;IAC7C,OAAO,EAAE,OAAO,CAAC;IACjB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,OAAO,CAAC;IACtB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,0BAA0B;IACzC,UAAU,EAAE,CAAC,MAAM,EAAE,8BAA8B,KAAK,IAAI,CAAC;IAC7D,OAAO,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,CAAC;IACzC,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,QAAQ,CAAC,EAAE,iBAAiB,CAAC;CAC9B;AAED,eAAO,MAAM,qBAAqB,EAAE,KAAK,CAAC,EAAE,CAAC,0BAA0B,CA8UtE,CAAC;AAsEF,eAAe,qBAAqB,CAAC"}
|
|
@@ -37,10 +37,12 @@ exports.ProfilePictureCapture = void 0;
|
|
|
37
37
|
const react_1 = __importStar(require("react"));
|
|
38
38
|
const react_native_1 = require("react-native");
|
|
39
39
|
const VideoRecorder_1 = require("./VideoRecorder");
|
|
40
|
+
const FacePositioningGuide_1 = require("./FacePositioningGuide");
|
|
40
41
|
const biometric_identity_sdk_core_1 = require("@hexar/biometric-identity-sdk-core");
|
|
41
42
|
const useBiometricSDK_1 = require("../hooks/useBiometricSDK");
|
|
42
43
|
const ProfilePictureCapture = ({ onComplete, onError, onCancel, theme, language, }) => {
|
|
43
44
|
const { sdk, isInitialized, isUsingBackend, sessionId, fetchChallenges, } = (0, useBiometricSDK_1.useBiometricSDK)();
|
|
45
|
+
const [showFacePositioningGuide, setShowFacePositioningGuide] = (0, react_1.useState)(true);
|
|
44
46
|
const [isValidating, setIsValidating] = (0, react_1.useState)(false);
|
|
45
47
|
const [currentChallenges, setCurrentChallenges] = (0, react_1.useState)([]);
|
|
46
48
|
const [isLoadingChallenges, setIsLoadingChallenges] = (0, react_1.useState)(false);
|
|
@@ -278,13 +280,16 @@ const ProfilePictureCapture = ({ onComplete, onError, onCancel, theme, language,
|
|
|
278
280
|
"%")),
|
|
279
281
|
react_1.default.createElement(react_native_1.Text, { style: [styles.progressBottomText, { color: theme?.secondaryTextColor || '#64748b' }] }, strings.validation.almostDone || strings.common.loading || 'Esto puede tardar unos segundos...'))));
|
|
280
282
|
}
|
|
281
|
-
// Wait for initialization and challenge loading before showing VideoRecorder
|
|
283
|
+
// Wait for initialization and challenge loading before showing guide or VideoRecorder
|
|
282
284
|
if (!isInitialized || (isUsingBackend && isLoadingChallenges)) {
|
|
283
285
|
return (react_1.default.createElement(react_native_1.SafeAreaView, { style: [styles.container, { backgroundColor: theme?.backgroundColor || '#FFFFFF' }] },
|
|
284
286
|
react_1.default.createElement(react_native_1.View, { style: styles.loadingContainer },
|
|
285
287
|
react_1.default.createElement(react_native_1.ActivityIndicator, { size: "large", color: theme?.primaryColor || '#4f46e5' }),
|
|
286
288
|
react_1.default.createElement(react_native_1.Text, { style: [styles.loadingText, { color: theme?.textColor || '#1e1b4b' }] }, strings.liveness.preparing || 'Preparing...'))));
|
|
287
289
|
}
|
|
290
|
+
if (showFacePositioningGuide) {
|
|
291
|
+
return (react_1.default.createElement(FacePositioningGuide_1.FacePositioningGuide, { theme: theme, language: language, onContinue: () => setShowFacePositioningGuide(false), onCancel: onCancel }));
|
|
292
|
+
}
|
|
288
293
|
return (react_1.default.createElement(VideoRecorder_1.VideoRecorder, { theme: theme, language: language, smartMode: true, challenges: currentChallenges, sessionId: sdk.getSessionId() || undefined, onComplete: handleVideoComplete, onCancel: handleVideoCancel, onFetchChallenges: handleFetchChallenges }));
|
|
289
294
|
};
|
|
290
295
|
exports.ProfilePictureCapture = ProfilePictureCapture;
|
|
@@ -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;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;AAiDD,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,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;AAiDD,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC,kBAAkB,CAm6BtD,CAAC;AA6OF,eAAe,aAAa,CAAC"}
|
|
@@ -349,16 +349,16 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
|
|
|
349
349
|
}, [frames, completedChallenges, challenges, sessionId, onComplete, resetAndRetry, handleRecordingError, strings, minDurationMs, phase]);
|
|
350
350
|
const startFrameCapture = (0, react_1.useCallback)(() => {
|
|
351
351
|
if (cameraRef.current && device) {
|
|
352
|
-
biometric_identity_sdk_core_1.logger.info('Starting frame capture');
|
|
352
|
+
biometric_identity_sdk_core_1.logger.info('Starting serial frame capture');
|
|
353
353
|
framesRef.current = [];
|
|
354
354
|
let consecutiveErrors = 0;
|
|
355
|
-
const maxConsecutiveErrors =
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
355
|
+
const maxConsecutiveErrors = 10;
|
|
356
|
+
const MAX_FRAMES = 80;
|
|
357
|
+
// Serial capture: each frame is captured only after the previous one
|
|
358
|
+
// finishes, preventing overlapping takePhoto() calls that cause the
|
|
359
|
+
// camera to throw "busy" errors and kill the capture loop.
|
|
360
|
+
const captureNextFrame = async () => {
|
|
361
|
+
if (!isRecordingRef.current || framesRef.current.length >= MAX_FRAMES) {
|
|
362
362
|
return;
|
|
363
363
|
}
|
|
364
364
|
try {
|
|
@@ -395,7 +395,7 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
|
|
|
395
395
|
}
|
|
396
396
|
if (base64Data) {
|
|
397
397
|
setFrames(prev => {
|
|
398
|
-
const newFrames = prev.length <
|
|
398
|
+
const newFrames = prev.length < MAX_FRAMES ? [...prev, base64Data] : prev;
|
|
399
399
|
framesRef.current = newFrames;
|
|
400
400
|
if (newFrames.length % 10 === 0) {
|
|
401
401
|
biometric_identity_sdk_core_1.logger.info('Captured frames:', newFrames.length);
|
|
@@ -410,14 +410,19 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
|
|
|
410
410
|
}
|
|
411
411
|
catch (error) {
|
|
412
412
|
consecutiveErrors++;
|
|
413
|
+
biometric_identity_sdk_core_1.logger.warn('Frame capture error', consecutiveErrors, '/', maxConsecutiveErrors, error?.message);
|
|
413
414
|
if (consecutiveErrors >= maxConsecutiveErrors) {
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
frameCaptureInterval.current = null;
|
|
417
|
-
}
|
|
415
|
+
biometric_identity_sdk_core_1.logger.error('Too many consecutive capture errors, stopping');
|
|
416
|
+
return;
|
|
418
417
|
}
|
|
419
418
|
}
|
|
420
|
-
|
|
419
|
+
// Schedule next capture only after this one completes (serial chain).
|
|
420
|
+
// Small delay lets the camera hardware reset between captures.
|
|
421
|
+
if (isRecordingRef.current && framesRef.current.length < MAX_FRAMES) {
|
|
422
|
+
frameCaptureInterval.current = setTimeout(captureNextFrame, 150);
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
captureNextFrame();
|
|
421
426
|
}
|
|
422
427
|
else {
|
|
423
428
|
biometric_identity_sdk_core_1.logger.warn('Cannot start frame capture: camera or device not available');
|
|
@@ -429,7 +434,7 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
|
|
|
429
434
|
recordingTimeoutRef.current = null;
|
|
430
435
|
}
|
|
431
436
|
if (frameCaptureInterval.current) {
|
|
432
|
-
|
|
437
|
+
clearTimeout(frameCaptureInterval.current);
|
|
433
438
|
frameCaptureInterval.current = null;
|
|
434
439
|
}
|
|
435
440
|
if (!isRecordingRef.current) {
|
package/dist/index.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ export { ValidationProgress } from './components/ValidationProgress';
|
|
|
12
12
|
export { ResultScreen } from './components/ResultScreen';
|
|
13
13
|
export { ErrorScreen } from './components/ErrorScreen';
|
|
14
14
|
export { InstructionsScreen } from './components/InstructionsScreen';
|
|
15
|
+
export { FacePositioningGuide } from './components/FacePositioningGuide';
|
|
15
16
|
export { useBiometricSDK } from './hooks/useBiometricSDK';
|
|
16
17
|
export * from '@hexar/biometric-identity-sdk-core';
|
|
17
18
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAE,qBAAqB,EAAE,MAAM,oCAAoC,CAAC;AAC3E,OAAO,EAAE,OAAO,EAAE,MAAM,oCAAoC,CAAC;AAG7D,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC3D,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC3D,OAAO,EAAE,qBAAqB,EAAE,MAAM,oCAAoC,CAAC;AAC3E,YAAY,EAAE,8BAA8B,EAAE,MAAM,oCAAoC,CAAC;AACzF,OAAO,EAAE,kBAAkB,EAAE,MAAM,iCAAiC,CAAC;AACrE,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AACzD,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AACvD,OAAO,EAAE,kBAAkB,EAAE,MAAM,iCAAiC,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAE,qBAAqB,EAAE,MAAM,oCAAoC,CAAC;AAC3E,OAAO,EAAE,OAAO,EAAE,MAAM,oCAAoC,CAAC;AAG7D,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC3D,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC3D,OAAO,EAAE,qBAAqB,EAAE,MAAM,oCAAoC,CAAC;AAC3E,YAAY,EAAE,8BAA8B,EAAE,MAAM,oCAAoC,CAAC;AACzF,OAAO,EAAE,kBAAkB,EAAE,MAAM,iCAAiC,CAAC;AACrE,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AACzD,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AACvD,OAAO,EAAE,kBAAkB,EAAE,MAAM,iCAAiC,CAAC;AACrE,OAAO,EAAE,oBAAoB,EAAE,MAAM,mCAAmC,CAAC;AAGzE,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAG1D,cAAc,oCAAoC,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -21,7 +21,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
21
21
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
22
22
|
};
|
|
23
23
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
|
-
exports.useBiometricSDK = exports.InstructionsScreen = exports.ErrorScreen = exports.ResultScreen = exports.ValidationProgress = exports.ProfilePictureCapture = exports.VideoRecorder = exports.CameraCapture = exports.default = exports.BiometricIdentityFlow = void 0;
|
|
24
|
+
exports.useBiometricSDK = exports.FacePositioningGuide = exports.InstructionsScreen = exports.ErrorScreen = exports.ResultScreen = exports.ValidationProgress = exports.ProfilePictureCapture = exports.VideoRecorder = exports.CameraCapture = exports.default = exports.BiometricIdentityFlow = void 0;
|
|
25
25
|
// Main component
|
|
26
26
|
var BiometricIdentityFlow_1 = require("./components/BiometricIdentityFlow");
|
|
27
27
|
Object.defineProperty(exports, "BiometricIdentityFlow", { enumerable: true, get: function () { return BiometricIdentityFlow_1.BiometricIdentityFlow; } });
|
|
@@ -42,6 +42,8 @@ var ErrorScreen_1 = require("./components/ErrorScreen");
|
|
|
42
42
|
Object.defineProperty(exports, "ErrorScreen", { enumerable: true, get: function () { return ErrorScreen_1.ErrorScreen; } });
|
|
43
43
|
var InstructionsScreen_1 = require("./components/InstructionsScreen");
|
|
44
44
|
Object.defineProperty(exports, "InstructionsScreen", { enumerable: true, get: function () { return InstructionsScreen_1.InstructionsScreen; } });
|
|
45
|
+
var FacePositioningGuide_1 = require("./components/FacePositioningGuide");
|
|
46
|
+
Object.defineProperty(exports, "FacePositioningGuide", { enumerable: true, get: function () { return FacePositioningGuide_1.FacePositioningGuide; } });
|
|
45
47
|
// Hooks
|
|
46
48
|
var useBiometricSDK_1 = require("./hooks/useBiometricSDK");
|
|
47
49
|
Object.defineProperty(exports, "useBiometricSDK", { enumerable: true, get: function () { return useBiometricSDK_1.useBiometricSDK; } });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hexar/biometric-identity-sdk-react-native",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "React Native wrapper for Biometric Identity SDK",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"clean": "rm -rf dist"
|
|
12
12
|
},
|
|
13
13
|
"peerDependencies": {
|
|
14
|
-
"@hexar/biometric-identity-sdk-core": ">=1.
|
|
14
|
+
"@hexar/biometric-identity-sdk-core": ">=1.2.0",
|
|
15
15
|
"react": ">=18.0.0",
|
|
16
16
|
"react-native": ">=0.70.0",
|
|
17
17
|
"react-native-permissions": ">=4.0.0",
|
|
@@ -52,6 +52,7 @@ import { ValidationProgress } from './ValidationProgress';
|
|
|
52
52
|
import { ResultScreen } from './ResultScreen';
|
|
53
53
|
import { ErrorScreen } from './ErrorScreen';
|
|
54
54
|
import { InstructionsScreen } from './InstructionsScreen';
|
|
55
|
+
import { FacePositioningGuide } from './FacePositioningGuide';
|
|
55
56
|
|
|
56
57
|
export interface BiometricIdentityFlowProps {
|
|
57
58
|
onValidationComplete: (result: ValidationResult) => void;
|
|
@@ -94,6 +95,7 @@ export const BiometricIdentityFlow: React.FC<BiometricIdentityFlowProps> = ({
|
|
|
94
95
|
const [showCamera, setShowCamera] = useState(false);
|
|
95
96
|
const [cameraMode, setCameraMode] = useState<'front' | 'back' | 'video'>('front');
|
|
96
97
|
const [showInstructions, setShowInstructions] = useState(true);
|
|
98
|
+
const [showFacePositioningGuide, setShowFacePositioningGuide] = useState(false);
|
|
97
99
|
const [currentChallenges, setCurrentChallenges] = useState<ChallengeAction[]>([]);
|
|
98
100
|
const [isLoadingChallenges, setIsLoadingChallenges] = useState(false);
|
|
99
101
|
|
|
@@ -147,8 +149,18 @@ export const BiometricIdentityFlow: React.FC<BiometricIdentityFlowProps> = ({
|
|
|
147
149
|
*/
|
|
148
150
|
const handleCaptureStart = useCallback(async (mode: 'front' | 'back' | 'video') => {
|
|
149
151
|
setCameraMode(mode);
|
|
150
|
-
|
|
151
|
-
if (mode === 'video'
|
|
152
|
+
|
|
153
|
+
if (mode === 'video') {
|
|
154
|
+
setShowFacePositioningGuide(true);
|
|
155
|
+
// Challenges will be loaded when guide is dismissed and we show camera
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (mode !== 'video') {
|
|
159
|
+
setShowCamera(true);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (smartLivenessMode && isUsingBackend) {
|
|
152
164
|
setIsLoadingChallenges(true);
|
|
153
165
|
try {
|
|
154
166
|
const challenges = await fetchChallenges('active');
|
|
@@ -215,10 +227,13 @@ export const BiometricIdentityFlow: React.FC<BiometricIdentityFlowProps> = ({
|
|
|
215
227
|
});
|
|
216
228
|
setCurrentChallenges(translatedChallenges);
|
|
217
229
|
}
|
|
218
|
-
|
|
219
|
-
setShowCamera(true);
|
|
220
230
|
}, [smartLivenessMode, isUsingBackend, fetchChallenges, sdk]);
|
|
221
231
|
|
|
232
|
+
const handleFacePositioningContinue = useCallback(() => {
|
|
233
|
+
setShowFacePositioningGuide(false);
|
|
234
|
+
setShowCamera(true);
|
|
235
|
+
}, []);
|
|
236
|
+
|
|
222
237
|
/**
|
|
223
238
|
* Handle capture completion
|
|
224
239
|
*/
|
|
@@ -341,6 +356,18 @@ export const BiometricIdentityFlow: React.FC<BiometricIdentityFlowProps> = ({
|
|
|
341
356
|
);
|
|
342
357
|
}
|
|
343
358
|
|
|
359
|
+
if (showFacePositioningGuide) {
|
|
360
|
+
return (
|
|
361
|
+
<FacePositioningGuide
|
|
362
|
+
theme={theme}
|
|
363
|
+
language={language}
|
|
364
|
+
onContinue={handleFacePositioningContinue}
|
|
365
|
+
onCancel={() => setShowFacePositioningGuide(false)}
|
|
366
|
+
styles={customStyles}
|
|
367
|
+
/>
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
|
|
344
371
|
if (showCamera) {
|
|
345
372
|
if (cameraMode === 'video') {
|
|
346
373
|
return (
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Face Positioning Guide Component
|
|
3
|
+
* Shared view that instructs users to keep their face inside the oval
|
|
4
|
+
* for better approval rate. Used before video capture in both
|
|
5
|
+
* BiometricIdentityFlow and ProfilePictureCapture.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React from 'react';
|
|
9
|
+
import {
|
|
10
|
+
View,
|
|
11
|
+
Text,
|
|
12
|
+
StyleSheet,
|
|
13
|
+
TouchableOpacity,
|
|
14
|
+
ScrollView,
|
|
15
|
+
SafeAreaView,
|
|
16
|
+
ViewStyle,
|
|
17
|
+
} from 'react-native';
|
|
18
|
+
import { ThemeConfig, SupportedLanguage, getStrings, setLanguage } from '@hexar/biometric-identity-sdk-core';
|
|
19
|
+
|
|
20
|
+
export interface FacePositioningGuideProps {
|
|
21
|
+
theme?: ThemeConfig;
|
|
22
|
+
language?: SupportedLanguage;
|
|
23
|
+
onContinue: () => void;
|
|
24
|
+
onCancel?: () => void;
|
|
25
|
+
styles?: {
|
|
26
|
+
container?: ViewStyle;
|
|
27
|
+
content?: ViewStyle;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const FacePositioningGuide: React.FC<FacePositioningGuideProps> = ({
|
|
32
|
+
theme,
|
|
33
|
+
language,
|
|
34
|
+
onContinue,
|
|
35
|
+
onCancel,
|
|
36
|
+
styles: customStyles,
|
|
37
|
+
}) => {
|
|
38
|
+
if (language) {
|
|
39
|
+
setLanguage(language);
|
|
40
|
+
}
|
|
41
|
+
const strings = getStrings();
|
|
42
|
+
const liveness = strings.liveness as Record<string, unknown> | undefined;
|
|
43
|
+
const guide = liveness?.facePositioningGuide as { title?: string; description?: string; tip?: string; continue?: string } | undefined;
|
|
44
|
+
const title = guide?.title ?? 'Position your face';
|
|
45
|
+
const description = guide?.description ?? 'Keep your face inside the oval for the best approval rate. Positioning your face outside the frame may reduce your chances of approval.';
|
|
46
|
+
const tip = guide?.tip ?? 'Face inside = better approval • Face outside = lower chances';
|
|
47
|
+
const continueLabel = guide?.continue ?? strings.common?.continue ?? 'Continue';
|
|
48
|
+
|
|
49
|
+
const primaryColor = theme?.primaryColor ?? '#6366F1';
|
|
50
|
+
const textColor = theme?.textColor ?? '#000000';
|
|
51
|
+
const secondaryTextColor = theme?.secondaryTextColor ?? '#6B7280';
|
|
52
|
+
const backgroundColor = theme?.backgroundColor ?? '#FFFFFF';
|
|
53
|
+
|
|
54
|
+
const styles = createStyles({
|
|
55
|
+
primaryColor,
|
|
56
|
+
textColor,
|
|
57
|
+
secondaryTextColor,
|
|
58
|
+
backgroundColor,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<SafeAreaView style={[styles.container, customStyles?.container]}>
|
|
63
|
+
{onCancel && (
|
|
64
|
+
<TouchableOpacity style={styles.cancelButton} onPress={onCancel}>
|
|
65
|
+
<Text style={[styles.cancelText, { color: textColor }]}>
|
|
66
|
+
{strings.common?.cancel ?? 'Cancel'}
|
|
67
|
+
</Text>
|
|
68
|
+
</TouchableOpacity>
|
|
69
|
+
)}
|
|
70
|
+
<ScrollView
|
|
71
|
+
contentContainerStyle={[styles.scrollContent, customStyles?.content]}
|
|
72
|
+
showsVerticalScrollIndicator={false}
|
|
73
|
+
>
|
|
74
|
+
<Text style={[styles.title, { color: textColor }]}>{title}</Text>
|
|
75
|
+
<Text style={[styles.description, { color: secondaryTextColor }]}>
|
|
76
|
+
{description}
|
|
77
|
+
</Text>
|
|
78
|
+
|
|
79
|
+
{/* Oval frame with face icon inside */}
|
|
80
|
+
<View style={styles.ovalWrapper}>
|
|
81
|
+
<View style={[styles.oval, { borderColor: primaryColor }]}>
|
|
82
|
+
<FaceIcon color={secondaryTextColor} />
|
|
83
|
+
</View>
|
|
84
|
+
</View>
|
|
85
|
+
|
|
86
|
+
<View style={[styles.tipBox, { borderColor: primaryColor + '30', backgroundColor: primaryColor + '08' }]}>
|
|
87
|
+
<Text style={[styles.tipText, { color: secondaryTextColor }]}>{tip}</Text>
|
|
88
|
+
</View>
|
|
89
|
+
|
|
90
|
+
<TouchableOpacity
|
|
91
|
+
style={[styles.continueButton, { backgroundColor: primaryColor }]}
|
|
92
|
+
onPress={onContinue}
|
|
93
|
+
activeOpacity={0.85}
|
|
94
|
+
>
|
|
95
|
+
<Text style={styles.continueButtonText}>{continueLabel}</Text>
|
|
96
|
+
</TouchableOpacity>
|
|
97
|
+
</ScrollView>
|
|
98
|
+
</SafeAreaView>
|
|
99
|
+
);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/** Simple geometric face icon (head + eyes) when react-native-svg is not used */
|
|
103
|
+
const FaceIcon: React.FC<{ color: string }> = ({ color }) => (
|
|
104
|
+
<View style={faceIconStyles.container}>
|
|
105
|
+
<View style={[faceIconStyles.head, { backgroundColor: color }]} />
|
|
106
|
+
<View style={faceIconStyles.eyes}>
|
|
107
|
+
<View style={[faceIconStyles.eye, { backgroundColor: color }]} />
|
|
108
|
+
<View style={[faceIconStyles.eye, { backgroundColor: color }]} />
|
|
109
|
+
</View>
|
|
110
|
+
</View>
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
const faceIconStyles = StyleSheet.create({
|
|
114
|
+
container: {
|
|
115
|
+
alignItems: 'center',
|
|
116
|
+
justifyContent: 'center',
|
|
117
|
+
},
|
|
118
|
+
head: {
|
|
119
|
+
width: 80,
|
|
120
|
+
height: 100,
|
|
121
|
+
borderRadius: 50,
|
|
122
|
+
opacity: 0.4,
|
|
123
|
+
},
|
|
124
|
+
eyes: {
|
|
125
|
+
flexDirection: 'row',
|
|
126
|
+
justifyContent: 'space-between',
|
|
127
|
+
width: 44,
|
|
128
|
+
marginTop: -70,
|
|
129
|
+
opacity: 0.5,
|
|
130
|
+
},
|
|
131
|
+
eye: {
|
|
132
|
+
width: 12,
|
|
133
|
+
height: 12,
|
|
134
|
+
borderRadius: 6,
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const createStyles = (vars: {
|
|
139
|
+
primaryColor: string;
|
|
140
|
+
textColor: string;
|
|
141
|
+
secondaryTextColor: string;
|
|
142
|
+
backgroundColor: string;
|
|
143
|
+
}) =>
|
|
144
|
+
StyleSheet.create({
|
|
145
|
+
container: {
|
|
146
|
+
flex: 1,
|
|
147
|
+
backgroundColor: vars.backgroundColor,
|
|
148
|
+
zIndex: 9999,
|
|
149
|
+
},
|
|
150
|
+
cancelButton: {
|
|
151
|
+
alignSelf: 'flex-start',
|
|
152
|
+
paddingVertical: 12,
|
|
153
|
+
paddingHorizontal: 16,
|
|
154
|
+
marginTop: 8,
|
|
155
|
+
},
|
|
156
|
+
cancelText: {
|
|
157
|
+
fontSize: 16,
|
|
158
|
+
fontWeight: '500',
|
|
159
|
+
},
|
|
160
|
+
scrollContent: {
|
|
161
|
+
paddingHorizontal: 24,
|
|
162
|
+
paddingBottom: 40,
|
|
163
|
+
alignItems: 'center',
|
|
164
|
+
},
|
|
165
|
+
title: {
|
|
166
|
+
fontSize: 24,
|
|
167
|
+
fontWeight: '700',
|
|
168
|
+
marginBottom: 12,
|
|
169
|
+
textAlign: 'center',
|
|
170
|
+
},
|
|
171
|
+
description: {
|
|
172
|
+
fontSize: 16,
|
|
173
|
+
lineHeight: 24,
|
|
174
|
+
textAlign: 'center',
|
|
175
|
+
marginBottom: 28,
|
|
176
|
+
paddingHorizontal: 8,
|
|
177
|
+
},
|
|
178
|
+
ovalWrapper: {
|
|
179
|
+
marginBottom: 24,
|
|
180
|
+
},
|
|
181
|
+
oval: {
|
|
182
|
+
width: 260,
|
|
183
|
+
height: 320,
|
|
184
|
+
borderRadius: 160,
|
|
185
|
+
borderWidth: 3,
|
|
186
|
+
alignItems: 'center',
|
|
187
|
+
justifyContent: 'center',
|
|
188
|
+
overflow: 'hidden',
|
|
189
|
+
},
|
|
190
|
+
tipBox: {
|
|
191
|
+
paddingVertical: 14,
|
|
192
|
+
paddingHorizontal: 20,
|
|
193
|
+
borderRadius: 12,
|
|
194
|
+
borderWidth: 1,
|
|
195
|
+
marginBottom: 32,
|
|
196
|
+
alignSelf: 'stretch',
|
|
197
|
+
},
|
|
198
|
+
tipText: {
|
|
199
|
+
fontSize: 14,
|
|
200
|
+
fontWeight: '600',
|
|
201
|
+
textAlign: 'center',
|
|
202
|
+
},
|
|
203
|
+
continueButton: {
|
|
204
|
+
paddingVertical: 16,
|
|
205
|
+
paddingHorizontal: 32,
|
|
206
|
+
borderRadius: 12,
|
|
207
|
+
alignItems: 'center',
|
|
208
|
+
alignSelf: 'stretch',
|
|
209
|
+
shadowColor: '#000',
|
|
210
|
+
shadowOffset: { width: 0, height: 2 },
|
|
211
|
+
shadowOpacity: 0.1,
|
|
212
|
+
shadowRadius: 4,
|
|
213
|
+
elevation: 3,
|
|
214
|
+
},
|
|
215
|
+
continueButtonText: {
|
|
216
|
+
color: '#FFFFFF',
|
|
217
|
+
fontSize: 18,
|
|
218
|
+
fontWeight: '700',
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
export default FacePositioningGuide;
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
Animated,
|
|
9
9
|
} from 'react-native';
|
|
10
10
|
import { VideoRecorder, VideoRecordingResult } from './VideoRecorder';
|
|
11
|
+
import { FacePositioningGuide } from './FacePositioningGuide';
|
|
11
12
|
import { ThemeConfig, SupportedLanguage, getStrings, setLanguage, logger, BiometricError, BiometricErrorCode } from '@hexar/biometric-identity-sdk-core';
|
|
12
13
|
import { useBiometricSDK } from '../hooks/useBiometricSDK';
|
|
13
14
|
|
|
@@ -42,6 +43,7 @@ export const ProfilePictureCapture: React.FC<ProfilePictureCaptureProps> = ({
|
|
|
42
43
|
fetchChallenges,
|
|
43
44
|
} = useBiometricSDK();
|
|
44
45
|
|
|
46
|
+
const [showFacePositioningGuide, setShowFacePositioningGuide] = useState(true);
|
|
45
47
|
const [isValidating, setIsValidating] = useState(false);
|
|
46
48
|
const [currentChallenges, setCurrentChallenges] = useState<any[]>([]);
|
|
47
49
|
const [isLoadingChallenges, setIsLoadingChallenges] = useState(false);
|
|
@@ -323,7 +325,7 @@ export const ProfilePictureCapture: React.FC<ProfilePictureCaptureProps> = ({
|
|
|
323
325
|
);
|
|
324
326
|
}
|
|
325
327
|
|
|
326
|
-
// Wait for initialization and challenge loading before showing VideoRecorder
|
|
328
|
+
// Wait for initialization and challenge loading before showing guide or VideoRecorder
|
|
327
329
|
if (!isInitialized || (isUsingBackend && isLoadingChallenges)) {
|
|
328
330
|
return (
|
|
329
331
|
<SafeAreaView style={[styles.container, { backgroundColor: theme?.backgroundColor || '#FFFFFF' }]}>
|
|
@@ -337,6 +339,17 @@ export const ProfilePictureCapture: React.FC<ProfilePictureCaptureProps> = ({
|
|
|
337
339
|
);
|
|
338
340
|
}
|
|
339
341
|
|
|
342
|
+
if (showFacePositioningGuide) {
|
|
343
|
+
return (
|
|
344
|
+
<FacePositioningGuide
|
|
345
|
+
theme={theme}
|
|
346
|
+
language={language}
|
|
347
|
+
onContinue={() => setShowFacePositioningGuide(false)}
|
|
348
|
+
onCancel={onCancel}
|
|
349
|
+
/>
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
|
|
340
353
|
return (
|
|
341
354
|
<VideoRecorder
|
|
342
355
|
theme={theme}
|
|
@@ -417,29 +417,29 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
|
|
|
417
417
|
|
|
418
418
|
const startFrameCapture = useCallback(() => {
|
|
419
419
|
if (cameraRef.current && device) {
|
|
420
|
-
logger.info('Starting frame capture');
|
|
420
|
+
logger.info('Starting serial frame capture');
|
|
421
421
|
framesRef.current = [];
|
|
422
422
|
let consecutiveErrors = 0;
|
|
423
|
-
const maxConsecutiveErrors =
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
423
|
+
const maxConsecutiveErrors = 10;
|
|
424
|
+
const MAX_FRAMES = 80;
|
|
425
|
+
|
|
426
|
+
// Serial capture: each frame is captured only after the previous one
|
|
427
|
+
// finishes, preventing overlapping takePhoto() calls that cause the
|
|
428
|
+
// camera to throw "busy" errors and kill the capture loop.
|
|
429
|
+
const captureNextFrame = async () => {
|
|
430
|
+
if (!isRecordingRef.current || framesRef.current.length >= MAX_FRAMES) {
|
|
431
431
|
return;
|
|
432
432
|
}
|
|
433
|
-
|
|
433
|
+
|
|
434
434
|
try {
|
|
435
435
|
const photo = await cameraRef.current?.takePhoto({
|
|
436
436
|
flash: 'off',
|
|
437
437
|
});
|
|
438
|
-
|
|
438
|
+
|
|
439
439
|
if (photo) {
|
|
440
440
|
consecutiveErrors = 0;
|
|
441
441
|
let base64Data: string | null = null;
|
|
442
|
-
|
|
442
|
+
|
|
443
443
|
try {
|
|
444
444
|
const RNFS = require('react-native-fs');
|
|
445
445
|
base64Data = await RNFS.readFile(photo.path, 'base64');
|
|
@@ -463,10 +463,10 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
|
|
|
463
463
|
base64Data = null;
|
|
464
464
|
}
|
|
465
465
|
}
|
|
466
|
-
|
|
466
|
+
|
|
467
467
|
if (base64Data) {
|
|
468
468
|
setFrames(prev => {
|
|
469
|
-
const newFrames = prev.length <
|
|
469
|
+
const newFrames = prev.length < MAX_FRAMES ? [...prev, base64Data!] : prev;
|
|
470
470
|
framesRef.current = newFrames;
|
|
471
471
|
if (newFrames.length % 10 === 0) {
|
|
472
472
|
logger.info('Captured frames:', newFrames.length);
|
|
@@ -479,14 +479,21 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
|
|
|
479
479
|
}
|
|
480
480
|
} catch (error: any) {
|
|
481
481
|
consecutiveErrors++;
|
|
482
|
+
logger.warn('Frame capture error', consecutiveErrors, '/', maxConsecutiveErrors, error?.message);
|
|
482
483
|
if (consecutiveErrors >= maxConsecutiveErrors) {
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
frameCaptureInterval.current = null;
|
|
486
|
-
}
|
|
484
|
+
logger.error('Too many consecutive capture errors, stopping');
|
|
485
|
+
return;
|
|
487
486
|
}
|
|
488
487
|
}
|
|
489
|
-
|
|
488
|
+
|
|
489
|
+
// Schedule next capture only after this one completes (serial chain).
|
|
490
|
+
// Small delay lets the camera hardware reset between captures.
|
|
491
|
+
if (isRecordingRef.current && framesRef.current.length < MAX_FRAMES) {
|
|
492
|
+
frameCaptureInterval.current = setTimeout(captureNextFrame, 150);
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
captureNextFrame();
|
|
490
497
|
} else {
|
|
491
498
|
logger.warn('Cannot start frame capture: camera or device not available');
|
|
492
499
|
}
|
|
@@ -499,7 +506,7 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
|
|
|
499
506
|
}
|
|
500
507
|
|
|
501
508
|
if (frameCaptureInterval.current) {
|
|
502
|
-
|
|
509
|
+
clearTimeout(frameCaptureInterval.current);
|
|
503
510
|
frameCaptureInterval.current = null;
|
|
504
511
|
}
|
|
505
512
|
|
package/src/index.ts
CHANGED
|
@@ -16,6 +16,7 @@ export { ValidationProgress } from './components/ValidationProgress';
|
|
|
16
16
|
export { ResultScreen } from './components/ResultScreen';
|
|
17
17
|
export { ErrorScreen } from './components/ErrorScreen';
|
|
18
18
|
export { InstructionsScreen } from './components/InstructionsScreen';
|
|
19
|
+
export { FacePositioningGuide } from './components/FacePositioningGuide';
|
|
19
20
|
|
|
20
21
|
// Hooks
|
|
21
22
|
export { useBiometricSDK } from './hooks/useBiometricSDK';
|