@hexar/biometric-identity-sdk-react-native 1.0.33 → 1.0.35

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.
@@ -0,0 +1,19 @@
1
+ import React from 'react';
2
+ import { ThemeConfig, SupportedLanguage, BiometricError } from '@hexar/biometric-identity-sdk-core';
3
+ export interface ProfilePictureValidationResult {
4
+ isValid: boolean;
5
+ profilePicture: string;
6
+ livenessScore: number;
7
+ faceDetected: boolean;
8
+ warnings: string[];
9
+ }
10
+ export interface ProfilePictureCaptureProps {
11
+ onComplete: (result: ProfilePictureValidationResult) => void;
12
+ onError: (error: BiometricError) => void;
13
+ onCancel?: () => void;
14
+ theme?: ThemeConfig;
15
+ language?: SupportedLanguage;
16
+ }
17
+ export declare const ProfilePictureCapture: React.FC<ProfilePictureCaptureProps>;
18
+ export default ProfilePictureCapture;
19
+ //# sourceMappingURL=ProfilePictureCapture.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ProfilePictureCapture.d.ts","sourceRoot":"","sources":["../../src/components/ProfilePictureCapture.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAwC,MAAM,OAAO,CAAC;AAS7D,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,CAmKtE,CAAC;AAuCF,eAAe,qBAAqB,CAAC"}
@@ -0,0 +1,194 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.ProfilePictureCapture = void 0;
37
+ const react_1 = __importStar(require("react"));
38
+ const react_native_1 = require("react-native");
39
+ const VideoRecorder_1 = require("./VideoRecorder");
40
+ const biometric_identity_sdk_core_1 = require("@hexar/biometric-identity-sdk-core");
41
+ const biometric_identity_sdk_core_2 = require("@hexar/biometric-identity-sdk-core");
42
+ const ProfilePictureCapture = ({ onComplete, onError, onCancel, theme, language, }) => {
43
+ const [isValidating, setIsValidating] = (0, react_1.useState)(false);
44
+ const [validationError, setValidationError] = (0, react_1.useState)(null);
45
+ if (language) {
46
+ (0, biometric_identity_sdk_core_1.setLanguage)(language);
47
+ }
48
+ const strings = (0, biometric_identity_sdk_core_1.getStrings)();
49
+ const validateWithBackend = (0, react_1.useCallback)(async (videoResult) => {
50
+ try {
51
+ const sdk = biometric_identity_sdk_core_2.BiometricIdentitySDK.getInstance();
52
+ if (!sdk.isUsingBackend()) {
53
+ throw new Error('Backend not available');
54
+ }
55
+ const sessionId = videoResult.sessionId;
56
+ if (!sessionId) {
57
+ throw new Error('Session ID not available');
58
+ }
59
+ const backendClient = sdk.backendClient;
60
+ const apiEndpoint = backendClient.config.apiEndpoint;
61
+ const apiKey = backendClient.config.apiKey;
62
+ const requestBody = {
63
+ session_id: sessionId,
64
+ video_frames: videoResult.frames,
65
+ video_duration_ms: videoResult.duration,
66
+ challenges_completed: videoResult.challengesCompleted || [],
67
+ };
68
+ biometric_identity_sdk_core_1.logger.info('Validating profile picture with backend', {
69
+ sessionId,
70
+ framesCount: videoResult.frames.length,
71
+ duration: videoResult.duration,
72
+ });
73
+ const response = await fetch(`${apiEndpoint}/profile/validate-face`, {
74
+ method: 'POST',
75
+ headers: {
76
+ 'Content-Type': 'application/json',
77
+ 'X-API-Key': apiKey,
78
+ },
79
+ body: JSON.stringify(requestBody),
80
+ });
81
+ if (!response.ok) {
82
+ const errorData = await response.json().catch(() => ({}));
83
+ throw new Error(errorData.detail || `Backend validation failed: ${response.status}`);
84
+ }
85
+ const data = await response.json();
86
+ biometric_identity_sdk_core_1.logger.info('Profile picture validation result', {
87
+ isValid: data.is_valid,
88
+ livenessScore: data.liveness_score,
89
+ faceDetected: data.face_detected,
90
+ });
91
+ return {
92
+ isValid: data.is_valid,
93
+ profilePicture: data.profile_picture,
94
+ livenessScore: data.liveness_score,
95
+ faceDetected: data.face_detected,
96
+ warnings: data.warnings || [],
97
+ };
98
+ }
99
+ catch (error) {
100
+ biometric_identity_sdk_core_1.logger.error('Profile picture validation error', error);
101
+ throw error;
102
+ }
103
+ }, []);
104
+ const handleVideoComplete = (0, react_1.useCallback)(async (videoResult) => {
105
+ setIsValidating(true);
106
+ setValidationError(null);
107
+ try {
108
+ const result = await validateWithBackend(videoResult);
109
+ if (!result.isValid) {
110
+ setValidationError('La validación de liveness falló. Por favor, intenta nuevamente.');
111
+ setIsValidating(false);
112
+ return;
113
+ }
114
+ setIsValidating(false);
115
+ onComplete(result);
116
+ }
117
+ catch (error) {
118
+ setIsValidating(false);
119
+ let errorCode = biometric_identity_sdk_core_1.BiometricErrorCode.UNKNOWN_ERROR;
120
+ if (error.message && error.message.toLowerCase().includes('network')) {
121
+ errorCode = biometric_identity_sdk_core_1.BiometricErrorCode.NETWORK_ERROR;
122
+ }
123
+ else if (error.message && error.message.toLowerCase().includes('timeout')) {
124
+ errorCode = biometric_identity_sdk_core_1.BiometricErrorCode.VALIDATION_TIMEOUT;
125
+ }
126
+ else if (error.message && error.message.toLowerCase().includes('liveness')) {
127
+ errorCode = biometric_identity_sdk_core_1.BiometricErrorCode.LIVENESS_CHECK_FAILED;
128
+ }
129
+ const biometricError = {
130
+ name: 'BiometricError',
131
+ message: error.message || 'Error al validar la foto de perfil',
132
+ code: errorCode,
133
+ };
134
+ onError(biometricError);
135
+ }
136
+ }, [validateWithBackend, onComplete, onError]);
137
+ const handleVideoCancel = (0, react_1.useCallback)(() => {
138
+ if (onCancel) {
139
+ onCancel();
140
+ }
141
+ }, [onCancel]);
142
+ if (isValidating) {
143
+ return (react_1.default.createElement(react_native_1.SafeAreaView, { style: [styles.container, { backgroundColor: theme?.backgroundColor || '#FFFFFF' }] },
144
+ react_1.default.createElement(react_native_1.View, { style: styles.loadingContainer },
145
+ react_1.default.createElement(react_native_1.ActivityIndicator, { size: "large", color: theme?.primaryColor || '#4f46e5' }),
146
+ react_1.default.createElement(react_native_1.Text, { style: [styles.loadingText, { color: theme?.textColor || '#1e1b4b' }] }, strings.liveness.processing || 'Validando foto de perfil...'),
147
+ react_1.default.createElement(react_native_1.Text, { style: [styles.loadingSubtext, { color: theme?.secondaryTextColor || '#64748b' }] }, "Esto puede tardar unos segundos"))));
148
+ }
149
+ if (validationError) {
150
+ return (react_1.default.createElement(react_native_1.SafeAreaView, { style: [styles.container, { backgroundColor: theme?.backgroundColor || '#FFFFFF' }] },
151
+ react_1.default.createElement(react_native_1.View, { style: styles.errorContainer },
152
+ react_1.default.createElement(react_native_1.Text, { style: [styles.errorTitle, { color: theme?.errorColor || '#EF4444' }] }, "Error de validaci\u00F3n"),
153
+ react_1.default.createElement(react_native_1.Text, { style: [styles.errorText, { color: theme?.textColor || '#1e1b4b' }] }, validationError))));
154
+ }
155
+ return (react_1.default.createElement(VideoRecorder_1.VideoRecorder, { theme: theme, language: language, duration: 8000, smartMode: true, onComplete: handleVideoComplete, onCancel: handleVideoCancel }));
156
+ };
157
+ exports.ProfilePictureCapture = ProfilePictureCapture;
158
+ const styles = react_native_1.StyleSheet.create({
159
+ container: {
160
+ flex: 1,
161
+ },
162
+ loadingContainer: {
163
+ flex: 1,
164
+ justifyContent: 'center',
165
+ alignItems: 'center',
166
+ padding: 24,
167
+ },
168
+ loadingText: {
169
+ fontSize: 18,
170
+ fontWeight: '600',
171
+ marginTop: 24,
172
+ },
173
+ loadingSubtext: {
174
+ fontSize: 14,
175
+ marginTop: 8,
176
+ },
177
+ errorContainer: {
178
+ flex: 1,
179
+ justifyContent: 'center',
180
+ alignItems: 'center',
181
+ padding: 24,
182
+ },
183
+ errorTitle: {
184
+ fontSize: 20,
185
+ fontWeight: '600',
186
+ marginBottom: 12,
187
+ },
188
+ errorText: {
189
+ fontSize: 16,
190
+ textAlign: 'center',
191
+ lineHeight: 24,
192
+ },
193
+ });
194
+ exports.default = exports.ProfilePictureCapture;
@@ -1 +1 @@
1
- {"version":3,"file":"ValidationProgress.d.ts","sourceRoot":"","sources":["../../src/components/ValidationProgress.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAsC,MAAM,OAAO,CAAC;AAQ3D,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAA2B,MAAM,oCAAoC,CAAC;AAE7G,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,QAAQ,CAAC,EAAE,iBAAiB,CAAC;CAC9B;AAED,eAAO,MAAM,kBAAkB,EAAE,KAAK,CAAC,EAAE,CAAC,uBAAuB,CA8GhE,CAAC;AAqDF,eAAe,kBAAkB,CAAC"}
1
+ {"version":3,"file":"ValidationProgress.d.ts","sourceRoot":"","sources":["../../src/components/ValidationProgress.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAsC,MAAM,OAAO,CAAC;AAQ3D,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAA2B,MAAM,oCAAoC,CAAC;AAE7G,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,QAAQ,CAAC,EAAE,iBAAiB,CAAC;CAC9B;AAED,eAAO,MAAM,kBAAkB,EAAE,KAAK,CAAC,EAAE,CAAC,uBAAuB,CAqGhE,CAAC;AAqDF,eAAe,kBAAkB,CAAC"}
@@ -45,38 +45,29 @@ const ValidationProgress = ({ progress, theme, language = 'en', }) => {
45
45
  const animatedProgress = (0, react_1.useRef)(new react_native_1.Animated.Value(0)).current;
46
46
  const [displayProgress, setDisplayProgress] = (0, react_1.useState)(0);
47
47
  const animationRef = (0, react_1.useRef)(null);
48
+ const hasStartedAnimation = (0, react_1.useRef)(false);
48
49
  (0, react_1.useEffect)(() => {
49
50
  if (language) {
50
51
  (0, biometric_identity_sdk_core_1.setLanguage)(language);
51
52
  }
52
53
  }, [language]);
53
54
  (0, react_1.useEffect)(() => {
54
- // Start animation from 0 to 90% over 60 seconds (1 minute)
55
- if (progress < 90) {
56
- // If we have real progress from backend, use it; otherwise animate to 90% over 60s
57
- if (progress > 0 && progress < 90) {
58
- // Use actual progress
59
- react_native_1.Animated.timing(animatedProgress, {
60
- toValue: progress,
61
- duration: 500,
62
- useNativeDriver: false,
63
- }).start();
64
- }
65
- else if (progress === 0) {
66
- // Start animation to 90% over 60 seconds
67
- if (animationRef.current) {
68
- animationRef.current.stop();
69
- }
70
- animationRef.current = react_native_1.Animated.timing(animatedProgress, {
71
- toValue: 90,
72
- duration: 60000, // 60 seconds
73
- useNativeDriver: false,
74
- });
75
- animationRef.current.start();
76
- }
55
+ // Start animation from 0 to 90% over 60 seconds (1 minute) - only once
56
+ if (!hasStartedAnimation.current) {
57
+ hasStartedAnimation.current = true;
58
+ // Start animation to 90% over 60 seconds, regardless of backend progress
59
+ animationRef.current = react_native_1.Animated.timing(animatedProgress, {
60
+ toValue: 90,
61
+ duration: 60000, // 60 seconds
62
+ useNativeDriver: false,
63
+ });
64
+ animationRef.current.start();
77
65
  }
78
- else {
79
- // If progress is 90% or more, animate to actual progress
66
+ // Only update to actual progress if it's 90% or more (validation complete)
67
+ if (progress >= 90) {
68
+ if (animationRef.current) {
69
+ animationRef.current.stop();
70
+ }
80
71
  react_native_1.Animated.timing(animatedProgress, {
81
72
  toValue: progress,
82
73
  duration: 500,
@@ -89,9 +80,6 @@ const ValidationProgress = ({ progress, theme, language = 'en', }) => {
89
80
  });
90
81
  return () => {
91
82
  animatedProgress.removeListener(listener);
92
- if (animationRef.current) {
93
- animationRef.current.stop();
94
- }
95
83
  };
96
84
  }, [progress, animatedProgress]);
97
85
  const strings = (0, biometric_identity_sdk_core_1.getStrings)();
package/dist/index.d.ts CHANGED
@@ -6,6 +6,8 @@ export { BiometricIdentityFlow } from './components/BiometricIdentityFlow';
6
6
  export { default } from './components/BiometricIdentityFlow';
7
7
  export { CameraCapture } from './components/CameraCapture';
8
8
  export { VideoRecorder } from './components/VideoRecorder';
9
+ export { ProfilePictureCapture } from './components/ProfilePictureCapture';
10
+ export type { ProfilePictureValidationResult } from './components/ProfilePictureCapture';
9
11
  export { ValidationProgress } from './components/ValidationProgress';
10
12
  export { ResultScreen } from './components/ResultScreen';
11
13
  export { ErrorScreen } from './components/ErrorScreen';
@@ -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,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;AAGrE,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.VideoRecorder = exports.CameraCapture = exports.default = exports.BiometricIdentityFlow = void 0;
24
+ exports.useBiometricSDK = 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; } });
@@ -32,6 +32,8 @@ var CameraCapture_1 = require("./components/CameraCapture");
32
32
  Object.defineProperty(exports, "CameraCapture", { enumerable: true, get: function () { return CameraCapture_1.CameraCapture; } });
33
33
  var VideoRecorder_1 = require("./components/VideoRecorder");
34
34
  Object.defineProperty(exports, "VideoRecorder", { enumerable: true, get: function () { return VideoRecorder_1.VideoRecorder; } });
35
+ var ProfilePictureCapture_1 = require("./components/ProfilePictureCapture");
36
+ Object.defineProperty(exports, "ProfilePictureCapture", { enumerable: true, get: function () { return ProfilePictureCapture_1.ProfilePictureCapture; } });
35
37
  var ValidationProgress_1 = require("./components/ValidationProgress");
36
38
  Object.defineProperty(exports, "ValidationProgress", { enumerable: true, get: function () { return ValidationProgress_1.ValidationProgress; } });
37
39
  var ResultScreen_1 = require("./components/ResultScreen");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hexar/biometric-identity-sdk-react-native",
3
- "version": "1.0.33",
3
+ "version": "1.0.35",
4
4
  "description": "React Native wrapper for Biometric Identity SDK",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -0,0 +1,231 @@
1
+ import React, { useState, useCallback, useRef } from 'react';
2
+ import {
3
+ View,
4
+ Text,
5
+ StyleSheet,
6
+ ActivityIndicator,
7
+ SafeAreaView,
8
+ } from 'react-native';
9
+ import { VideoRecorder, VideoRecordingResult } from './VideoRecorder';
10
+ import { ThemeConfig, SupportedLanguage, getStrings, setLanguage, logger, BiometricError, BiometricErrorCode } from '@hexar/biometric-identity-sdk-core';
11
+ import { BiometricIdentitySDK } from '@hexar/biometric-identity-sdk-core';
12
+
13
+ export interface ProfilePictureValidationResult {
14
+ isValid: boolean;
15
+ profilePicture: string;
16
+ livenessScore: number;
17
+ faceDetected: boolean;
18
+ warnings: string[];
19
+ }
20
+
21
+ export interface ProfilePictureCaptureProps {
22
+ onComplete: (result: ProfilePictureValidationResult) => void;
23
+ onError: (error: BiometricError) => void;
24
+ onCancel?: () => void;
25
+ theme?: ThemeConfig;
26
+ language?: SupportedLanguage;
27
+ }
28
+
29
+ export const ProfilePictureCapture: React.FC<ProfilePictureCaptureProps> = ({
30
+ onComplete,
31
+ onError,
32
+ onCancel,
33
+ theme,
34
+ language,
35
+ }) => {
36
+ const [isValidating, setIsValidating] = useState(false);
37
+ const [validationError, setValidationError] = useState<string | null>(null);
38
+
39
+ if (language) {
40
+ setLanguage(language);
41
+ }
42
+ const strings = getStrings();
43
+
44
+ const validateWithBackend = useCallback(async (videoResult: VideoRecordingResult): Promise<ProfilePictureValidationResult> => {
45
+ try {
46
+ const sdk = BiometricIdentitySDK.getInstance();
47
+
48
+ if (!sdk.isUsingBackend()) {
49
+ throw new Error('Backend not available');
50
+ }
51
+
52
+ const sessionId = videoResult.sessionId;
53
+ if (!sessionId) {
54
+ throw new Error('Session ID not available');
55
+ }
56
+
57
+ const backendClient = (sdk as any).backendClient;
58
+ const apiEndpoint = backendClient.config.apiEndpoint;
59
+ const apiKey = backendClient.config.apiKey;
60
+
61
+ const requestBody = {
62
+ session_id: sessionId,
63
+ video_frames: videoResult.frames,
64
+ video_duration_ms: videoResult.duration,
65
+ challenges_completed: videoResult.challengesCompleted || [],
66
+ };
67
+
68
+ logger.info('Validating profile picture with backend', {
69
+ sessionId,
70
+ framesCount: videoResult.frames.length,
71
+ duration: videoResult.duration,
72
+ });
73
+
74
+ const response = await fetch(`${apiEndpoint}/profile/validate-face`, {
75
+ method: 'POST',
76
+ headers: {
77
+ 'Content-Type': 'application/json',
78
+ 'X-API-Key': apiKey,
79
+ },
80
+ body: JSON.stringify(requestBody),
81
+ });
82
+
83
+ if (!response.ok) {
84
+ const errorData = await response.json().catch(() => ({}));
85
+ throw new Error(errorData.detail || `Backend validation failed: ${response.status}`);
86
+ }
87
+
88
+ const data = await response.json();
89
+
90
+ logger.info('Profile picture validation result', {
91
+ isValid: data.is_valid,
92
+ livenessScore: data.liveness_score,
93
+ faceDetected: data.face_detected,
94
+ });
95
+
96
+ return {
97
+ isValid: data.is_valid,
98
+ profilePicture: data.profile_picture,
99
+ livenessScore: data.liveness_score,
100
+ faceDetected: data.face_detected,
101
+ warnings: data.warnings || [],
102
+ };
103
+ } catch (error: any) {
104
+ logger.error('Profile picture validation error', error);
105
+ throw error;
106
+ }
107
+ }, []);
108
+
109
+ const handleVideoComplete = useCallback(async (videoResult: VideoRecordingResult) => {
110
+ setIsValidating(true);
111
+ setValidationError(null);
112
+
113
+ try {
114
+ const result = await validateWithBackend(videoResult);
115
+
116
+ if (!result.isValid) {
117
+ setValidationError('La validación de liveness falló. Por favor, intenta nuevamente.');
118
+ setIsValidating(false);
119
+ return;
120
+ }
121
+
122
+ setIsValidating(false);
123
+ onComplete(result);
124
+ } catch (error: any) {
125
+ setIsValidating(false);
126
+ let errorCode = BiometricErrorCode.UNKNOWN_ERROR;
127
+
128
+ if (error.message && error.message.toLowerCase().includes('network')) {
129
+ errorCode = BiometricErrorCode.NETWORK_ERROR;
130
+ } else if (error.message && error.message.toLowerCase().includes('timeout')) {
131
+ errorCode = BiometricErrorCode.VALIDATION_TIMEOUT;
132
+ } else if (error.message && error.message.toLowerCase().includes('liveness')) {
133
+ errorCode = BiometricErrorCode.LIVENESS_CHECK_FAILED;
134
+ }
135
+
136
+ const biometricError: BiometricError = {
137
+ name: 'BiometricError',
138
+ message: error.message || 'Error al validar la foto de perfil',
139
+ code: errorCode,
140
+ } as BiometricError;
141
+ onError(biometricError);
142
+ }
143
+ }, [validateWithBackend, onComplete, onError]);
144
+
145
+ const handleVideoCancel = useCallback(() => {
146
+ if (onCancel) {
147
+ onCancel();
148
+ }
149
+ }, [onCancel]);
150
+
151
+ if (isValidating) {
152
+ return (
153
+ <SafeAreaView style={[styles.container, { backgroundColor: theme?.backgroundColor || '#FFFFFF' }]}>
154
+ <View style={styles.loadingContainer}>
155
+ <ActivityIndicator size="large" color={theme?.primaryColor || '#4f46e5'} />
156
+ <Text style={[styles.loadingText, { color: theme?.textColor || '#1e1b4b' }]}>
157
+ {strings.liveness.processing || 'Validando foto de perfil...'}
158
+ </Text>
159
+ <Text style={[styles.loadingSubtext, { color: theme?.secondaryTextColor || '#64748b' }]}>
160
+ Esto puede tardar unos segundos
161
+ </Text>
162
+ </View>
163
+ </SafeAreaView>
164
+ );
165
+ }
166
+
167
+ if (validationError) {
168
+ return (
169
+ <SafeAreaView style={[styles.container, { backgroundColor: theme?.backgroundColor || '#FFFFFF' }]}>
170
+ <View style={styles.errorContainer}>
171
+ <Text style={[styles.errorTitle, { color: theme?.errorColor || '#EF4444' }]}>
172
+ Error de validación
173
+ </Text>
174
+ <Text style={[styles.errorText, { color: theme?.textColor || '#1e1b4b' }]}>
175
+ {validationError}
176
+ </Text>
177
+ </View>
178
+ </SafeAreaView>
179
+ );
180
+ }
181
+
182
+ return (
183
+ <VideoRecorder
184
+ theme={theme}
185
+ language={language}
186
+ duration={8000}
187
+ smartMode={true}
188
+ onComplete={handleVideoComplete}
189
+ onCancel={handleVideoCancel}
190
+ />
191
+ );
192
+ };
193
+
194
+ const styles = StyleSheet.create({
195
+ container: {
196
+ flex: 1,
197
+ },
198
+ loadingContainer: {
199
+ flex: 1,
200
+ justifyContent: 'center',
201
+ alignItems: 'center',
202
+ padding: 24,
203
+ },
204
+ loadingText: {
205
+ fontSize: 18,
206
+ fontWeight: '600',
207
+ marginTop: 24,
208
+ },
209
+ loadingSubtext: {
210
+ fontSize: 14,
211
+ marginTop: 8,
212
+ },
213
+ errorContainer: {
214
+ flex: 1,
215
+ justifyContent: 'center',
216
+ alignItems: 'center',
217
+ padding: 24,
218
+ },
219
+ errorTitle: {
220
+ fontSize: 20,
221
+ fontWeight: '600',
222
+ marginBottom: 12,
223
+ },
224
+ errorText: {
225
+ fontSize: 16,
226
+ textAlign: 'center',
227
+ lineHeight: 24,
228
+ },
229
+ });
230
+
231
+ export default ProfilePictureCapture;
@@ -27,6 +27,7 @@ export const ValidationProgress: React.FC<ValidationProgressProps> = ({
27
27
  const animatedProgress = useRef(new Animated.Value(0)).current;
28
28
  const [displayProgress, setDisplayProgress] = useState(0);
29
29
  const animationRef = useRef<Animated.CompositeAnimation | null>(null);
30
+ const hasStartedAnimation = useRef(false);
30
31
 
31
32
  useEffect(() => {
32
33
  if (language) {
@@ -35,32 +36,25 @@ export const ValidationProgress: React.FC<ValidationProgressProps> = ({
35
36
  }, [language]);
36
37
 
37
38
  useEffect(() => {
38
- // Start animation from 0 to 90% over 60 seconds (1 minute)
39
- if (progress < 90) {
40
- // If we have real progress from backend, use it; otherwise animate to 90% over 60s
41
- if (progress > 0 && progress < 90) {
42
- // Use actual progress
43
- Animated.timing(animatedProgress, {
44
- toValue: progress,
45
- duration: 500,
46
- useNativeDriver: false,
47
- }).start();
48
- } else if (progress === 0) {
49
- // Start animation to 90% over 60 seconds
50
- if (animationRef.current) {
51
- animationRef.current.stop();
52
- }
53
-
54
- animationRef.current = Animated.timing(animatedProgress, {
55
- toValue: 90,
56
- duration: 60000, // 60 seconds
57
- useNativeDriver: false,
58
- });
59
-
60
- animationRef.current.start();
39
+ // Start animation from 0 to 90% over 60 seconds (1 minute) - only once
40
+ if (!hasStartedAnimation.current) {
41
+ hasStartedAnimation.current = true;
42
+
43
+ // Start animation to 90% over 60 seconds, regardless of backend progress
44
+ animationRef.current = Animated.timing(animatedProgress, {
45
+ toValue: 90,
46
+ duration: 60000, // 60 seconds
47
+ useNativeDriver: false,
48
+ });
49
+
50
+ animationRef.current.start();
51
+ }
52
+
53
+ // Only update to actual progress if it's 90% or more (validation complete)
54
+ if (progress >= 90) {
55
+ if (animationRef.current) {
56
+ animationRef.current.stop();
61
57
  }
62
- } else {
63
- // If progress is 90% or more, animate to actual progress
64
58
  Animated.timing(animatedProgress, {
65
59
  toValue: progress,
66
60
  duration: 500,
@@ -75,9 +69,6 @@ export const ValidationProgress: React.FC<ValidationProgressProps> = ({
75
69
 
76
70
  return () => {
77
71
  animatedProgress.removeListener(listener);
78
- if (animationRef.current) {
79
- animationRef.current.stop();
80
- }
81
72
  };
82
73
  }, [progress, animatedProgress]);
83
74
 
package/src/index.ts CHANGED
@@ -10,6 +10,8 @@ export { default } from './components/BiometricIdentityFlow';
10
10
  // Individual components (for custom implementations)
11
11
  export { CameraCapture } from './components/CameraCapture';
12
12
  export { VideoRecorder } from './components/VideoRecorder';
13
+ export { ProfilePictureCapture } from './components/ProfilePictureCapture';
14
+ export type { ProfilePictureValidationResult } from './components/ProfilePictureCapture';
13
15
  export { ValidationProgress } from './components/ValidationProgress';
14
16
  export { ResultScreen } from './components/ResultScreen';
15
17
  export { ErrorScreen } from './components/ErrorScreen';