@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.
@@ -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;AA8B5C,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,CAuatE,CAAC;AA+NF,eAAe,qBAAqB,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' && smartLivenessMode && isUsingBackend) {
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;AAUxE,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,CAkUtE,CAAC;AAsEF,eAAe,qBAAqB,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,CA45BtD,CAAC;AA6OF,eAAe,aAAa,CAAC"}
1
+ {"version":3,"file":"VideoRecorder.d.ts","sourceRoot":"","sources":["../../src/components/VideoRecorder.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAmD,MAAM,OAAO,CAAC;AAaxE,OAAO,EAAE,WAAW,EAAE,mBAAmB,EAAE,iBAAiB,EAAmC,MAAM,oCAAoC,CAAC;AAE1I,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IAC7B,sEAAsE;IACtE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,sCAAsC;IACtC,YAAY,CAAC,EAAE,mBAAmB,EAAE,CAAC;IACrC,0CAA0C;IAC1C,UAAU,CAAC,EAAE,eAAe,EAAE,CAAC;IAC/B,wCAAwC;IACxC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,kDAAkD;IAClD,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,wCAAwC;IACxC,UAAU,EAAE,CAAC,SAAS,EAAE,oBAAoB,KAAK,IAAI,CAAC;IACtD,iCAAiC;IACjC,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,gDAAgD;IAChD,iBAAiB,CAAC,EAAE,MAAM,OAAO,CAAC,eAAe,EAAE,CAAC,CAAC;CACtD;AAED,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,oBAAoB,EAAE,OAAO,CAAC;IAC9B,YAAY,EAAE,MAAM,CAAC;IACrB,mBAAmB,EAAE,MAAM,EAAE,CAAC;IAC9B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;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 = 5;
356
- frameCaptureInterval.current = setInterval(async () => {
357
- if (!isRecordingRef.current) {
358
- if (frameCaptureInterval.current) {
359
- clearInterval(frameCaptureInterval.current);
360
- frameCaptureInterval.current = null;
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 < 100 ? [...prev, base64Data] : prev;
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
- if (frameCaptureInterval.current) {
415
- clearInterval(frameCaptureInterval.current);
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
- }, 100);
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
- clearInterval(frameCaptureInterval.current);
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
@@ -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;AAGrE,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAG1D,cAAc,oCAAoC,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.1.27",
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.1.6",
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' && smartLivenessMode && isUsingBackend) {
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 = 5;
424
-
425
- frameCaptureInterval.current = setInterval(async () => {
426
- if (!isRecordingRef.current) {
427
- if (frameCaptureInterval.current) {
428
- clearInterval(frameCaptureInterval.current);
429
- frameCaptureInterval.current = null;
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 < 100 ? [...prev, base64Data!] : prev;
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
- if (frameCaptureInterval.current) {
484
- clearInterval(frameCaptureInterval.current);
485
- frameCaptureInterval.current = null;
486
- }
484
+ logger.error('Too many consecutive capture errors, stopping');
485
+ return;
487
486
  }
488
487
  }
489
- }, 100);
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
- clearInterval(frameCaptureInterval.current);
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';