@hexar/biometric-identity-sdk-react-native 1.9.0 → 1.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/CameraCapture.d.ts.map +1 -1
- package/dist/components/CameraCapture.js +10 -27
- package/dist/components/ProfilePictureCapture.d.ts.map +1 -1
- package/dist/components/ProfilePictureCapture.js +25 -2
- package/dist/components/ValidationProgress.js +2 -2
- package/dist/components/VideoRecorder.d.ts.map +1 -1
- package/dist/components/VideoRecorder.js +11 -31
- package/package.json +2 -2
- package/src/components/CameraCapture.tsx +9 -27
- package/src/components/ProfilePictureCapture.tsx +42 -2
- package/src/components/ValidationProgress.tsx +2 -2
- package/src/components/VideoRecorder.tsx +10 -29
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CameraCapture.d.ts","sourceRoot":"","sources":["../../src/components/CameraCapture.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAsC,MAAM,OAAO,CAAC;AAY3D,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAAmC,MAAM,oCAAoC,CAAC;AAIrH,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,OAAO,GAAG,MAAM,CAAC;IACvB,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IAC7B,SAAS,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,QAAQ,EAAE,MAAM,IAAI,CAAC;CACtB;AAED,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC,kBAAkB,
|
|
1
|
+
{"version":3,"file":"CameraCapture.d.ts","sourceRoot":"","sources":["../../src/components/CameraCapture.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAsC,MAAM,OAAO,CAAC;AAY3D,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAAmC,MAAM,oCAAoC,CAAC;AAIrH,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,OAAO,GAAG,MAAM,CAAC;IACvB,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IAC7B,SAAS,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,QAAQ,EAAE,MAAM,IAAI,CAAC;CACtB;AAED,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC,kBAAkB,CA8OtD,CAAC;AA0JF,eAAe,aAAa,CAAC"}
|
|
@@ -129,36 +129,19 @@ const CameraCapture = ({ mode, theme, language, onCapture, onCancel, }) => {
|
|
|
129
129
|
// Convert to base64
|
|
130
130
|
try {
|
|
131
131
|
const RNFS = require('react-native-fs');
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
biometric_identity_sdk_core_1.logger.info('Image converted to base64:', { sizeKB, path: processedPath });
|
|
135
|
-
onCapture(base64);
|
|
136
|
-
}
|
|
137
|
-
catch (fsError) {
|
|
138
|
-
// Fallback: try fetch method
|
|
132
|
+
// Try with original path first, then strip file:// prefix
|
|
133
|
+
let base64;
|
|
139
134
|
try {
|
|
140
|
-
|
|
141
|
-
const response = await fetch(fileUri);
|
|
142
|
-
const blob = await response.blob();
|
|
143
|
-
const reader = new FileReader();
|
|
144
|
-
reader.onloadend = () => {
|
|
145
|
-
const base64data = reader.result;
|
|
146
|
-
const base64 = base64data.includes(',')
|
|
147
|
-
? base64data.split(',')[1]
|
|
148
|
-
: base64data;
|
|
149
|
-
onCapture(base64);
|
|
150
|
-
setIsCapturing(false);
|
|
151
|
-
};
|
|
152
|
-
reader.onerror = () => {
|
|
153
|
-
throw new Error('Failed to read photo file');
|
|
154
|
-
};
|
|
155
|
-
reader.readAsDataURL(blob);
|
|
156
|
-
return;
|
|
135
|
+
base64 = await RNFS.readFile(processedPath, 'base64');
|
|
157
136
|
}
|
|
158
|
-
catch
|
|
159
|
-
|
|
160
|
-
onCapture(processedPath);
|
|
137
|
+
catch {
|
|
138
|
+
base64 = await RNFS.readFile(processedPath.replace('file://', ''), 'base64');
|
|
161
139
|
}
|
|
140
|
+
onCapture(base64);
|
|
141
|
+
}
|
|
142
|
+
catch (fsError) {
|
|
143
|
+
biometric_identity_sdk_core_1.logger.warn('Could not convert to base64, using file path:', processedPath);
|
|
144
|
+
onCapture(processedPath);
|
|
162
145
|
}
|
|
163
146
|
}
|
|
164
147
|
catch (error) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ProfilePictureCapture.d.ts","sourceRoot":"","sources":["../../src/components/ProfilePictureCapture.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAmD,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"ProfilePictureCapture.d.ts","sourceRoot":"","sources":["../../src/components/ProfilePictureCapture.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAmD,MAAM,OAAO,CAAC;AAYxE,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,CAyYtE,CAAC;AA4EF,eAAe,qBAAqB,CAAC"}
|
|
@@ -46,6 +46,7 @@ const ProfilePictureCapture = ({ onComplete, onError, onCancel, theme, language,
|
|
|
46
46
|
const [isValidating, setIsValidating] = (0, react_1.useState)(false);
|
|
47
47
|
const [currentChallenges, setCurrentChallenges] = (0, react_1.useState)([]);
|
|
48
48
|
const [isLoadingChallenges, setIsLoadingChallenges] = (0, react_1.useState)(false);
|
|
49
|
+
const [loadingTimedOut, setLoadingTimedOut] = (0, react_1.useState)(false);
|
|
49
50
|
const animatedProgress = (0, react_1.useRef)(new react_native_1.Animated.Value(0)).current;
|
|
50
51
|
const [displayProgress, setDisplayProgress] = (0, react_1.useState)(0);
|
|
51
52
|
const animationRef = (0, react_1.useRef)(null);
|
|
@@ -58,6 +59,19 @@ const ProfilePictureCapture = ({ onComplete, onError, onCancel, theme, language,
|
|
|
58
59
|
(0, biometric_identity_sdk_core_1.setLanguage)(language);
|
|
59
60
|
}
|
|
60
61
|
}, [language]);
|
|
62
|
+
// 15-second timeout on initial loading (challenge fetch / SDK init)
|
|
63
|
+
(0, react_1.useEffect)(() => {
|
|
64
|
+
const isLoading = !isInitialized || (isUsingBackend && isLoadingChallenges);
|
|
65
|
+
if (isLoading) {
|
|
66
|
+
const timer = setTimeout(() => {
|
|
67
|
+
setLoadingTimedOut(true);
|
|
68
|
+
}, 15000);
|
|
69
|
+
return () => clearTimeout(timer);
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
setLoadingTimedOut(false);
|
|
73
|
+
}
|
|
74
|
+
}, [isInitialized, isUsingBackend, isLoadingChallenges]);
|
|
61
75
|
(0, react_1.useEffect)(() => {
|
|
62
76
|
if (isInitialized && isUsingBackend) {
|
|
63
77
|
const loadChallenges = async () => {
|
|
@@ -151,10 +165,10 @@ const ProfilePictureCapture = ({ onComplete, onError, onCancel, theme, language,
|
|
|
151
165
|
hasStartedAnimation.current = true;
|
|
152
166
|
animatedProgress.setValue(0);
|
|
153
167
|
setDisplayProgress(0);
|
|
154
|
-
// Start animation to 90% over
|
|
168
|
+
// Start animation to 90% over 25 seconds
|
|
155
169
|
animationRef.current = react_native_1.Animated.timing(animatedProgress, {
|
|
156
170
|
toValue: 90,
|
|
157
|
-
duration:
|
|
171
|
+
duration: 25000, // 25 seconds
|
|
158
172
|
useNativeDriver: false,
|
|
159
173
|
});
|
|
160
174
|
animationRef.current.start();
|
|
@@ -301,6 +315,15 @@ const ProfilePictureCapture = ({ onComplete, onError, onCancel, theme, language,
|
|
|
301
315
|
}
|
|
302
316
|
// Wait for initialization and challenge loading before showing guide or VideoRecorder
|
|
303
317
|
if (!isInitialized || (isUsingBackend && isLoadingChallenges)) {
|
|
318
|
+
if (loadingTimedOut) {
|
|
319
|
+
return (react_1.default.createElement(react_native_1.SafeAreaView, { style: [styles.container, { backgroundColor: theme?.backgroundColor || '#FFFFFF' }] },
|
|
320
|
+
react_1.default.createElement(react_native_1.View, { style: styles.loadingContainer },
|
|
321
|
+
react_1.default.createElement(react_native_1.Text, { style: { fontSize: 48, marginBottom: 16 } }, "\u23F3"),
|
|
322
|
+
react_1.default.createElement(react_native_1.Text, { style: [styles.loadingText, { color: theme?.textColor || '#1e1b4b', fontSize: 20, fontWeight: 'bold' }] }, strings.errors.systemBusy),
|
|
323
|
+
react_1.default.createElement(react_native_1.Text, { style: { fontSize: 15, color: theme?.secondaryTextColor || '#64748b', textAlign: 'center', marginTop: 12, paddingHorizontal: 24, lineHeight: 22 } }, strings.errors.systemBusyMessage),
|
|
324
|
+
onCancel && (react_1.default.createElement(react_native_1.TouchableOpacity, { style: { backgroundColor: theme?.primaryColor || '#4f46e5', paddingVertical: 14, paddingHorizontal: 32, borderRadius: 12, marginTop: 32 }, onPress: onCancel },
|
|
325
|
+
react_1.default.createElement(react_native_1.Text, { style: { color: '#FFFFFF', fontSize: 16, fontWeight: '600' } }, strings.common?.back || 'Go back'))))));
|
|
326
|
+
}
|
|
304
327
|
return (react_1.default.createElement(react_native_1.SafeAreaView, { style: [styles.container, { backgroundColor: theme?.backgroundColor || '#FFFFFF' }] },
|
|
305
328
|
react_1.default.createElement(react_native_1.View, { style: styles.loadingContainer },
|
|
306
329
|
react_1.default.createElement(react_native_1.ActivityIndicator, { size: "large", color: theme?.primaryColor || '#4f46e5' }),
|
|
@@ -55,10 +55,10 @@ const ValidationProgress = ({ progress, theme, language = 'en', }) => {
|
|
|
55
55
|
// Start animation from 0 to 90% over 60 seconds (1 minute) - only once
|
|
56
56
|
if (!hasStartedAnimation.current) {
|
|
57
57
|
hasStartedAnimation.current = true;
|
|
58
|
-
// Start animation to 90% over
|
|
58
|
+
// Start animation to 90% over 35 seconds, regardless of backend progress
|
|
59
59
|
animationRef.current = react_native_1.Animated.timing(animatedProgress, {
|
|
60
60
|
toValue: 90,
|
|
61
|
-
duration:
|
|
61
|
+
duration: 35000, // 35 seconds
|
|
62
62
|
useNativeDriver: false,
|
|
63
63
|
});
|
|
64
64
|
animationRef.current.start();
|
|
@@ -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;AA2CD,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC,kBAAkB,
|
|
1
|
+
{"version":3,"file":"VideoRecorder.d.ts","sourceRoot":"","sources":["../../src/components/VideoRecorder.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAmD,MAAM,OAAO,CAAC;AAaxE,OAAO,EAAE,WAAW,EAAE,mBAAmB,EAAE,iBAAiB,EAAmC,MAAM,oCAAoC,CAAC;AAE1I,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IAC7B,sEAAsE;IACtE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,sCAAsC;IACtC,YAAY,CAAC,EAAE,mBAAmB,EAAE,CAAC;IACrC,0CAA0C;IAC1C,UAAU,CAAC,EAAE,eAAe,EAAE,CAAC;IAC/B,wCAAwC;IACxC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,kDAAkD;IAClD,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,wCAAwC;IACxC,UAAU,EAAE,CAAC,SAAS,EAAE,oBAAoB,KAAK,IAAI,CAAC;IACtD,iCAAiC;IACjC,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,gDAAgD;IAChD,iBAAiB,CAAC,EAAE,MAAM,OAAO,CAAC,eAAe,EAAE,CAAC,CAAC;CACtD;AAED,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,oBAAoB,EAAE,OAAO,CAAC;IAC9B,YAAY,EAAE,MAAM,CAAC;IACrB,mBAAmB,EAAE,MAAM,EAAE,CAAC;IAC9B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AA2CD,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC,kBAAkB,CAg5BtD,CAAC;AA6OF,eAAe,aAAa,CAAC"}
|
|
@@ -347,7 +347,7 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
|
|
|
347
347
|
framesRef.current = [];
|
|
348
348
|
let consecutiveErrors = 0;
|
|
349
349
|
const maxConsecutiveErrors = 10;
|
|
350
|
-
const MAX_FRAMES =
|
|
350
|
+
const MAX_FRAMES = 30;
|
|
351
351
|
// Serial capture: each frame is captured only after the previous one
|
|
352
352
|
// finishes, preventing overlapping takePhoto() calls that cause the
|
|
353
353
|
// camera to throw "busy" errors and kill the capture loop.
|
|
@@ -364,41 +364,21 @@ const VideoRecorder = ({ theme, language, duration, instructions, challenges: pr
|
|
|
364
364
|
let base64Data = null;
|
|
365
365
|
try {
|
|
366
366
|
const RNFS = require('react-native-fs');
|
|
367
|
-
|
|
368
|
-
}
|
|
369
|
-
catch (fsError) {
|
|
367
|
+
// Try with original path first, then strip file:// prefix
|
|
370
368
|
try {
|
|
371
|
-
|
|
372
|
-
const response = await fetch(fileUri);
|
|
373
|
-
const blob = await response.blob();
|
|
374
|
-
base64Data = await new Promise((resolve, reject) => {
|
|
375
|
-
const reader = new FileReader();
|
|
376
|
-
reader.onloadend = () => {
|
|
377
|
-
const result = reader.result;
|
|
378
|
-
const base64 = result.includes(',') ? result.split(',')[1] : result;
|
|
379
|
-
resolve(base64);
|
|
380
|
-
};
|
|
381
|
-
reader.onerror = reject;
|
|
382
|
-
reader.readAsDataURL(blob);
|
|
383
|
-
});
|
|
369
|
+
base64Data = await RNFS.readFile(photo.path, 'base64');
|
|
384
370
|
}
|
|
385
|
-
catch
|
|
386
|
-
|
|
387
|
-
base64Data = null;
|
|
371
|
+
catch {
|
|
372
|
+
base64Data = await RNFS.readFile(photo.path.replace('file://', ''), 'base64');
|
|
388
373
|
}
|
|
389
374
|
}
|
|
390
|
-
|
|
391
|
-
setFrames(prev => {
|
|
392
|
-
const newFrames = prev.length < MAX_FRAMES ? [...prev, base64Data] : prev;
|
|
393
|
-
framesRef.current = newFrames;
|
|
394
|
-
if (newFrames.length % 10 === 0) {
|
|
395
|
-
biometric_identity_sdk_core_1.logger.info('Captured frames:', newFrames.length);
|
|
396
|
-
}
|
|
397
|
-
return newFrames;
|
|
398
|
-
});
|
|
399
|
-
}
|
|
400
|
-
else {
|
|
375
|
+
catch (fsError) {
|
|
401
376
|
biometric_identity_sdk_core_1.logger.warn('Could not convert photo to base64, skipping frame');
|
|
377
|
+
base64Data = null;
|
|
378
|
+
}
|
|
379
|
+
if (base64Data) {
|
|
380
|
+
framesRef.current = [...framesRef.current, base64Data];
|
|
381
|
+
setFrames(framesRef.current);
|
|
402
382
|
}
|
|
403
383
|
}
|
|
404
384
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hexar/biometric-identity-sdk-react-native",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.11.0",
|
|
4
4
|
"description": "React Native wrapper for Biometric Identity SDK",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"clean": "rm -rf dist"
|
|
12
12
|
},
|
|
13
13
|
"peerDependencies": {
|
|
14
|
-
"@hexar/biometric-identity-sdk-core": ">=1.
|
|
14
|
+
"@hexar/biometric-identity-sdk-core": ">=1.7.0",
|
|
15
15
|
"react": ">=18.0.0",
|
|
16
16
|
"react-native": ">=0.70.0",
|
|
17
17
|
"react-native-permissions": ">=4.0.0",
|
|
@@ -133,35 +133,17 @@ export const CameraCapture: React.FC<CameraCaptureProps> = ({
|
|
|
133
133
|
// Convert to base64
|
|
134
134
|
try {
|
|
135
135
|
const RNFS = require('react-native-fs');
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
logger.info('Image converted to base64:', { sizeKB, path: processedPath });
|
|
139
|
-
onCapture(base64);
|
|
140
|
-
} catch (fsError) {
|
|
141
|
-
// Fallback: try fetch method
|
|
136
|
+
// Try with original path first, then strip file:// prefix
|
|
137
|
+
let base64: string;
|
|
142
138
|
try {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
const reader = new FileReader();
|
|
148
|
-
reader.onloadend = () => {
|
|
149
|
-
const base64data = reader.result as string;
|
|
150
|
-
const base64 = base64data.includes(',')
|
|
151
|
-
? base64data.split(',')[1]
|
|
152
|
-
: base64data;
|
|
153
|
-
onCapture(base64);
|
|
154
|
-
setIsCapturing(false);
|
|
155
|
-
};
|
|
156
|
-
reader.onerror = () => {
|
|
157
|
-
throw new Error('Failed to read photo file');
|
|
158
|
-
};
|
|
159
|
-
reader.readAsDataURL(blob);
|
|
160
|
-
return;
|
|
161
|
-
} catch (fetchError) {
|
|
162
|
-
logger.warn('Could not convert to base64, using file path:', processedPath);
|
|
163
|
-
onCapture(processedPath);
|
|
139
|
+
base64 = await RNFS.readFile(processedPath, 'base64');
|
|
140
|
+
} catch {
|
|
141
|
+
base64 = await RNFS.readFile(processedPath.replace('file://', ''), 'base64');
|
|
164
142
|
}
|
|
143
|
+
onCapture(base64);
|
|
144
|
+
} catch (fsError) {
|
|
145
|
+
logger.warn('Could not convert to base64, using file path:', processedPath);
|
|
146
|
+
onCapture(processedPath);
|
|
165
147
|
}
|
|
166
148
|
} catch (error) {
|
|
167
149
|
logger.error('Error processing image:', error);
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
ActivityIndicator,
|
|
7
7
|
SafeAreaView,
|
|
8
8
|
Animated,
|
|
9
|
+
TouchableOpacity,
|
|
9
10
|
} from 'react-native';
|
|
10
11
|
import { VideoRecorder, VideoRecordingResult } from './VideoRecorder';
|
|
11
12
|
import { FacePositioningGuide } from './FacePositioningGuide';
|
|
@@ -47,6 +48,7 @@ export const ProfilePictureCapture: React.FC<ProfilePictureCaptureProps> = ({
|
|
|
47
48
|
const [isValidating, setIsValidating] = useState(false);
|
|
48
49
|
const [currentChallenges, setCurrentChallenges] = useState<any[]>([]);
|
|
49
50
|
const [isLoadingChallenges, setIsLoadingChallenges] = useState(false);
|
|
51
|
+
const [loadingTimedOut, setLoadingTimedOut] = useState(false);
|
|
50
52
|
const animatedProgress = useRef(new Animated.Value(0)).current;
|
|
51
53
|
const [displayProgress, setDisplayProgress] = useState(0);
|
|
52
54
|
const animationRef = useRef<Animated.CompositeAnimation | null>(null);
|
|
@@ -62,6 +64,19 @@ export const ProfilePictureCapture: React.FC<ProfilePictureCaptureProps> = ({
|
|
|
62
64
|
}
|
|
63
65
|
}, [language]);
|
|
64
66
|
|
|
67
|
+
// 15-second timeout on initial loading (challenge fetch / SDK init)
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
const isLoading = !isInitialized || (isUsingBackend && isLoadingChallenges);
|
|
70
|
+
if (isLoading) {
|
|
71
|
+
const timer = setTimeout(() => {
|
|
72
|
+
setLoadingTimedOut(true);
|
|
73
|
+
}, 15000);
|
|
74
|
+
return () => clearTimeout(timer);
|
|
75
|
+
} else {
|
|
76
|
+
setLoadingTimedOut(false);
|
|
77
|
+
}
|
|
78
|
+
}, [isInitialized, isUsingBackend, isLoadingChallenges]);
|
|
79
|
+
|
|
65
80
|
useEffect(() => {
|
|
66
81
|
if (isInitialized && isUsingBackend) {
|
|
67
82
|
const loadChallenges = async () => {
|
|
@@ -163,10 +178,10 @@ export const ProfilePictureCapture: React.FC<ProfilePictureCaptureProps> = ({
|
|
|
163
178
|
animatedProgress.setValue(0);
|
|
164
179
|
setDisplayProgress(0);
|
|
165
180
|
|
|
166
|
-
// Start animation to 90% over
|
|
181
|
+
// Start animation to 90% over 25 seconds
|
|
167
182
|
animationRef.current = Animated.timing(animatedProgress, {
|
|
168
183
|
toValue: 90,
|
|
169
|
-
duration:
|
|
184
|
+
duration: 25000, // 25 seconds
|
|
170
185
|
useNativeDriver: false,
|
|
171
186
|
});
|
|
172
187
|
|
|
@@ -347,6 +362,31 @@ export const ProfilePictureCapture: React.FC<ProfilePictureCaptureProps> = ({
|
|
|
347
362
|
|
|
348
363
|
// Wait for initialization and challenge loading before showing guide or VideoRecorder
|
|
349
364
|
if (!isInitialized || (isUsingBackend && isLoadingChallenges)) {
|
|
365
|
+
if (loadingTimedOut) {
|
|
366
|
+
return (
|
|
367
|
+
<SafeAreaView style={[styles.container, { backgroundColor: theme?.backgroundColor || '#FFFFFF' }]}>
|
|
368
|
+
<View style={styles.loadingContainer}>
|
|
369
|
+
<Text style={{ fontSize: 48, marginBottom: 16 }}>⏳</Text>
|
|
370
|
+
<Text style={[styles.loadingText, { color: theme?.textColor || '#1e1b4b', fontSize: 20, fontWeight: 'bold' }]}>
|
|
371
|
+
{strings.errors.systemBusy}
|
|
372
|
+
</Text>
|
|
373
|
+
<Text style={{ fontSize: 15, color: theme?.secondaryTextColor || '#64748b', textAlign: 'center', marginTop: 12, paddingHorizontal: 24, lineHeight: 22 }}>
|
|
374
|
+
{strings.errors.systemBusyMessage}
|
|
375
|
+
</Text>
|
|
376
|
+
{onCancel && (
|
|
377
|
+
<TouchableOpacity
|
|
378
|
+
style={{ backgroundColor: theme?.primaryColor || '#4f46e5', paddingVertical: 14, paddingHorizontal: 32, borderRadius: 12, marginTop: 32 }}
|
|
379
|
+
onPress={onCancel}
|
|
380
|
+
>
|
|
381
|
+
<Text style={{ color: '#FFFFFF', fontSize: 16, fontWeight: '600' }}>
|
|
382
|
+
{strings.common?.back || 'Go back'}
|
|
383
|
+
</Text>
|
|
384
|
+
</TouchableOpacity>
|
|
385
|
+
)}
|
|
386
|
+
</View>
|
|
387
|
+
</SafeAreaView>
|
|
388
|
+
);
|
|
389
|
+
}
|
|
350
390
|
return (
|
|
351
391
|
<SafeAreaView style={[styles.container, { backgroundColor: theme?.backgroundColor || '#FFFFFF' }]}>
|
|
352
392
|
<View style={styles.loadingContainer}>
|
|
@@ -40,10 +40,10 @@ export const ValidationProgress: React.FC<ValidationProgressProps> = ({
|
|
|
40
40
|
if (!hasStartedAnimation.current) {
|
|
41
41
|
hasStartedAnimation.current = true;
|
|
42
42
|
|
|
43
|
-
// Start animation to 90% over
|
|
43
|
+
// Start animation to 90% over 35 seconds, regardless of backend progress
|
|
44
44
|
animationRef.current = Animated.timing(animatedProgress, {
|
|
45
45
|
toValue: 90,
|
|
46
|
-
duration:
|
|
46
|
+
duration: 35000, // 35 seconds
|
|
47
47
|
useNativeDriver: false,
|
|
48
48
|
});
|
|
49
49
|
|
|
@@ -415,7 +415,7 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
|
|
|
415
415
|
framesRef.current = [];
|
|
416
416
|
let consecutiveErrors = 0;
|
|
417
417
|
const maxConsecutiveErrors = 10;
|
|
418
|
-
const MAX_FRAMES =
|
|
418
|
+
const MAX_FRAMES = 30;
|
|
419
419
|
|
|
420
420
|
// Serial capture: each frame is captured only after the previous one
|
|
421
421
|
// finishes, preventing overlapping takePhoto() calls that cause the
|
|
@@ -436,39 +436,20 @@ export const VideoRecorder: React.FC<VideoRecorderProps> = ({
|
|
|
436
436
|
|
|
437
437
|
try {
|
|
438
438
|
const RNFS = require('react-native-fs');
|
|
439
|
-
|
|
440
|
-
} catch (fsError) {
|
|
439
|
+
// Try with original path first, then strip file:// prefix
|
|
441
440
|
try {
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
base64Data = await new Promise<string>((resolve, reject) => {
|
|
446
|
-
const reader = new FileReader();
|
|
447
|
-
reader.onloadend = () => {
|
|
448
|
-
const result = reader.result as string;
|
|
449
|
-
const base64 = result.includes(',') ? result.split(',')[1] : result;
|
|
450
|
-
resolve(base64);
|
|
451
|
-
};
|
|
452
|
-
reader.onerror = reject;
|
|
453
|
-
reader.readAsDataURL(blob);
|
|
454
|
-
});
|
|
455
|
-
} catch (fetchError) {
|
|
456
|
-
logger.error('Failed to read photo file as base64:', fetchError);
|
|
457
|
-
base64Data = null;
|
|
441
|
+
base64Data = await RNFS.readFile(photo.path, 'base64');
|
|
442
|
+
} catch {
|
|
443
|
+
base64Data = await RNFS.readFile(photo.path.replace('file://', ''), 'base64');
|
|
458
444
|
}
|
|
445
|
+
} catch (fsError) {
|
|
446
|
+
logger.warn('Could not convert photo to base64, skipping frame');
|
|
447
|
+
base64Data = null;
|
|
459
448
|
}
|
|
460
449
|
|
|
461
450
|
if (base64Data) {
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
framesRef.current = newFrames;
|
|
465
|
-
if (newFrames.length % 10 === 0) {
|
|
466
|
-
logger.info('Captured frames:', newFrames.length);
|
|
467
|
-
}
|
|
468
|
-
return newFrames;
|
|
469
|
-
});
|
|
470
|
-
} else {
|
|
471
|
-
logger.warn('Could not convert photo to base64, skipping frame');
|
|
451
|
+
framesRef.current = [...framesRef.current, base64Data];
|
|
452
|
+
setFrames(framesRef.current);
|
|
472
453
|
}
|
|
473
454
|
}
|
|
474
455
|
} catch (error: any) {
|