@hexar/biometric-identity-sdk-react-native 1.2.0 → 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;
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.2.0",
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.7",
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}
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';