react-native-biometric-verifier 0.0.28 → 0.0.30
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-biometric-verifier",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.30",
|
|
4
4
|
"description": "A React Native module for biometric verification with face recognition and QR code scanning",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"qr-code"
|
|
15
15
|
],
|
|
16
16
|
"author": "PRAFULDAS M M",
|
|
17
|
-
"license": "JESCON TECHNOLOGIES PVT",
|
|
17
|
+
"license": "JESCON TECHNOLOGIES PVT LTD",
|
|
18
18
|
"peerDependencies": {
|
|
19
19
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
|
|
20
20
|
"react-native": ">=0.60.0",
|
|
@@ -6,8 +6,8 @@ import {
|
|
|
6
6
|
StyleSheet,
|
|
7
7
|
ActivityIndicator,
|
|
8
8
|
Animated,
|
|
9
|
+
Dimensions,
|
|
9
10
|
} from 'react-native';
|
|
10
|
-
import Icon from 'react-native-vector-icons/MaterialIcons';
|
|
11
11
|
import {
|
|
12
12
|
Camera,
|
|
13
13
|
getCameraDevice,
|
|
@@ -24,7 +24,7 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
24
24
|
showCodeScanner = false,
|
|
25
25
|
isLoading = false,
|
|
26
26
|
frameProcessorFps = 1,
|
|
27
|
-
livenessLevel,
|
|
27
|
+
livenessLevel = 0, // 0 = anti-spoof only, 1 = anti-spoof + blinking
|
|
28
28
|
}) => {
|
|
29
29
|
const cameraRef = useRef(null);
|
|
30
30
|
const [cameraDevice, setCameraDevice] = useState(null);
|
|
@@ -38,11 +38,16 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
38
38
|
const [blinkCount, setBlinkCount] = useState(0);
|
|
39
39
|
const [progress, setProgress] = useState(0);
|
|
40
40
|
const [faceCount, setFaceCount] = useState(0);
|
|
41
|
+
const [isFaceLive, setIsFaceLive] = useState(false);
|
|
42
|
+
const [antiSpoofConfidence, setAntiSpoofConfidence] = useState(0);
|
|
43
|
+
const [isFaceCentered, setIsFaceCentered] = useState(false);
|
|
44
|
+
const [hasSingleFace, setHasSingleFace] = useState(false);
|
|
41
45
|
|
|
42
46
|
const captured = useRef(false);
|
|
43
47
|
const isMounted = useRef(true);
|
|
44
48
|
|
|
45
49
|
const instructionAnim = useRef(new Animated.Value(1)).current;
|
|
50
|
+
const liveIndicatorAnim = useRef(new Animated.Value(0)).current;
|
|
46
51
|
|
|
47
52
|
const resetCaptureState = useCallback(() => {
|
|
48
53
|
captured.current = false;
|
|
@@ -51,6 +56,10 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
51
56
|
setBlinkCount(0);
|
|
52
57
|
setProgress(0);
|
|
53
58
|
setFaceCount(0);
|
|
59
|
+
setIsFaceLive(false);
|
|
60
|
+
setAntiSpoofConfidence(0);
|
|
61
|
+
setIsFaceCentered(false);
|
|
62
|
+
setHasSingleFace(false);
|
|
54
63
|
}, []);
|
|
55
64
|
|
|
56
65
|
const codeScanner = useCodeScanner({
|
|
@@ -112,10 +121,18 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
112
121
|
const onFacesUpdate = useCallback((payload) => {
|
|
113
122
|
if (!isMounted.current) return;
|
|
114
123
|
try {
|
|
115
|
-
const { count, progress } = payload;
|
|
124
|
+
const { count, progress, antiSpoofState } = payload;
|
|
116
125
|
setFaceCount(count);
|
|
117
126
|
setProgress(progress);
|
|
118
127
|
|
|
128
|
+
// Update anti-spoof related states
|
|
129
|
+
if (antiSpoofState) {
|
|
130
|
+
setIsFaceLive(antiSpoofState.isLive || false);
|
|
131
|
+
setAntiSpoofConfidence(antiSpoofState.confidence || 0);
|
|
132
|
+
setIsFaceCentered(antiSpoofState.isFaceCentered || false);
|
|
133
|
+
setHasSingleFace(antiSpoofState.hasSingleFace || false);
|
|
134
|
+
}
|
|
135
|
+
|
|
119
136
|
if (count === 1) {
|
|
120
137
|
setFaces((prev) => {
|
|
121
138
|
if (prev.length === 1) return prev;
|
|
@@ -144,6 +161,33 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
144
161
|
[instructionAnim]
|
|
145
162
|
);
|
|
146
163
|
|
|
164
|
+
const onAntiSpoofUpdate = useCallback((result) => {
|
|
165
|
+
if (!isMounted.current) return;
|
|
166
|
+
try {
|
|
167
|
+
// Animate live indicator when face becomes live
|
|
168
|
+
if (result?.isLive && !isFaceLive) {
|
|
169
|
+
Animated.spring(liveIndicatorAnim, {
|
|
170
|
+
toValue: 1,
|
|
171
|
+
tension: 50,
|
|
172
|
+
friction: 7,
|
|
173
|
+
useNativeDriver: true,
|
|
174
|
+
}).start();
|
|
175
|
+
} else if (!result?.isLive && isFaceLive) {
|
|
176
|
+
Animated.timing(liveIndicatorAnim, {
|
|
177
|
+
toValue: 0,
|
|
178
|
+
duration: 200,
|
|
179
|
+
useNativeDriver: true,
|
|
180
|
+
}).start();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
setIsFaceLive(result?.isLive || false);
|
|
184
|
+
setAntiSpoofConfidence(result?.confidence || 0);
|
|
185
|
+
setIsFaceCentered(result?.isFaceCentered || false);
|
|
186
|
+
} catch (error) {
|
|
187
|
+
console.error('Error updating anti-spoof:', error);
|
|
188
|
+
}
|
|
189
|
+
}, [isFaceLive, liveIndicatorAnim]);
|
|
190
|
+
|
|
147
191
|
const {
|
|
148
192
|
frameProcessor,
|
|
149
193
|
forceResetCaptureState,
|
|
@@ -154,10 +198,11 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
154
198
|
onStableFaceDetected,
|
|
155
199
|
onFacesUpdate,
|
|
156
200
|
onLivenessUpdate,
|
|
201
|
+
onAntiSpoofUpdate,
|
|
157
202
|
showCodeScanner,
|
|
158
203
|
isLoading,
|
|
159
204
|
isActive: showCamera && cameraInitialized,
|
|
160
|
-
livenessLevel: livenessLevel
|
|
205
|
+
livenessLevel: livenessLevel,
|
|
161
206
|
});
|
|
162
207
|
|
|
163
208
|
useEffect(() => {
|
|
@@ -197,6 +242,7 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
197
242
|
setShowCamera(true);
|
|
198
243
|
console.log('Camera device set successfully');
|
|
199
244
|
} else {
|
|
245
|
+
console.warn('Camera permission not granted');
|
|
200
246
|
}
|
|
201
247
|
} catch (error) {
|
|
202
248
|
console.error('Camera permission error:', error);
|
|
@@ -208,7 +254,6 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
208
254
|
}
|
|
209
255
|
}, [currentCameraType]);
|
|
210
256
|
|
|
211
|
-
|
|
212
257
|
const initializeCamera = useCallback(async () => {
|
|
213
258
|
await getPermission();
|
|
214
259
|
}, [getPermission]);
|
|
@@ -245,7 +290,6 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
245
290
|
}, [cameraType, currentCameraType, initializeCamera]);
|
|
246
291
|
|
|
247
292
|
const format = useCameraFormat(cameraDevice, [
|
|
248
|
-
{ videoResolution: { width: 640, height: 640 } },
|
|
249
293
|
{ fps: 30 },
|
|
250
294
|
]);
|
|
251
295
|
|
|
@@ -283,54 +327,31 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
283
327
|
return 'Multiple faces detected';
|
|
284
328
|
}
|
|
285
329
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
if (currentLevel === 0) {
|
|
289
|
-
if (faces.length === 0) return 'Position your face in the frame';
|
|
290
|
-
if (progress < 100) return 'Hold still...';
|
|
291
|
-
return 'Perfect! Capturing...';
|
|
330
|
+
if (!hasSingleFace) {
|
|
331
|
+
return 'Position your face in the frame';
|
|
292
332
|
}
|
|
293
333
|
|
|
294
|
-
if (
|
|
295
|
-
|
|
296
|
-
case 0:
|
|
297
|
-
return 'Face the camera straight';
|
|
298
|
-
case 1:
|
|
299
|
-
return `Blink your eyes ${blinkCount} of 3 times`;
|
|
300
|
-
case 2:
|
|
301
|
-
return 'Perfect! Hold still — capturing...';
|
|
302
|
-
default:
|
|
303
|
-
return 'Align your face in frame';
|
|
304
|
-
}
|
|
334
|
+
if (!isFaceCentered) {
|
|
335
|
+
return 'Center your face in the frame';
|
|
305
336
|
}
|
|
306
337
|
|
|
307
|
-
if (
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
case 1:
|
|
312
|
-
return 'Turn your head to the RIGHT';
|
|
313
|
-
case 2:
|
|
314
|
-
return 'Turn your head to the LEFT';
|
|
315
|
-
case 3:
|
|
316
|
-
return 'Perfect! Hold still — capturing...';
|
|
317
|
-
default:
|
|
318
|
-
return 'Align your face in frame';
|
|
319
|
-
}
|
|
338
|
+
if (livenessLevel === 0) {
|
|
339
|
+
if (!isFaceLive) return 'Verifying liveness...';
|
|
340
|
+
if (progress < 100) return 'Hold still...';
|
|
341
|
+
return 'Perfect! Capturing...';
|
|
320
342
|
}
|
|
321
343
|
|
|
322
|
-
if (
|
|
344
|
+
if (livenessLevel === 1) {
|
|
323
345
|
switch (livenessStep) {
|
|
324
346
|
case 0:
|
|
325
347
|
return 'Face the camera straight';
|
|
326
348
|
case 1:
|
|
327
|
-
return '
|
|
328
|
-
case 2:
|
|
329
|
-
return 'Turn your head to the LEFT';
|
|
330
|
-
case 3:
|
|
349
|
+
if (!isFaceLive) return 'Verifying liveness...';
|
|
331
350
|
return `Blink your eyes ${blinkCount} of 3 times`;
|
|
332
|
-
case
|
|
333
|
-
|
|
351
|
+
case 2:
|
|
352
|
+
if (!isFaceLive) return 'Verifying liveness...';
|
|
353
|
+
if (progress < 100) return 'Hold still...';
|
|
354
|
+
return 'Perfect! Capturing...';
|
|
334
355
|
default:
|
|
335
356
|
return 'Align your face in frame';
|
|
336
357
|
}
|
|
@@ -341,23 +362,62 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
341
362
|
livenessLevel,
|
|
342
363
|
livenessStep,
|
|
343
364
|
blinkCount,
|
|
344
|
-
faces.length,
|
|
345
365
|
progress,
|
|
346
366
|
faceCount,
|
|
367
|
+
hasSingleFace,
|
|
368
|
+
isFaceCentered,
|
|
369
|
+
isFaceLive,
|
|
347
370
|
]);
|
|
348
371
|
|
|
349
|
-
const
|
|
350
|
-
const
|
|
372
|
+
const getInstructionContainerStyle = useCallback(() => {
|
|
373
|
+
const baseStyle = [
|
|
374
|
+
styles.instructionContainer,
|
|
375
|
+
{
|
|
376
|
+
opacity: instructionAnim,
|
|
377
|
+
transform: [
|
|
378
|
+
{
|
|
379
|
+
translateY: instructionAnim.interpolate({
|
|
380
|
+
inputRange: [0, 1],
|
|
381
|
+
outputRange: [10, 0],
|
|
382
|
+
}),
|
|
383
|
+
},
|
|
384
|
+
],
|
|
385
|
+
},
|
|
386
|
+
];
|
|
387
|
+
|
|
388
|
+
if (faceCount > 1) {
|
|
389
|
+
return [...baseStyle, styles.errorInstructionContainer];
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (isFaceLive) {
|
|
393
|
+
return [...baseStyle, styles.liveInstructionContainer];
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (hasSingleFace && isFaceCentered) {
|
|
397
|
+
return [...baseStyle, styles.verifyingInstructionContainer];
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return baseStyle;
|
|
401
|
+
}, [faceCount, isFaceLive, hasSingleFace, isFaceCentered, instructionAnim]);
|
|
402
|
+
|
|
403
|
+
const getInstructionStyle = useCallback(() => {
|
|
404
|
+
if (faceCount > 1) {
|
|
405
|
+
return [styles.instructionText, styles.errorInstructionText];
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (isFaceLive) {
|
|
409
|
+
return [styles.instructionText, styles.liveInstructionText];
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return styles.instructionText;
|
|
413
|
+
}, [faceCount, isFaceLive]);
|
|
351
414
|
|
|
352
|
-
|
|
415
|
+
const getStepConfig = useCallback(() => {
|
|
416
|
+
switch (livenessLevel) {
|
|
353
417
|
case 0:
|
|
354
418
|
return { totalSteps: 0, showSteps: false };
|
|
355
419
|
case 1:
|
|
356
420
|
return { totalSteps: 1, showSteps: true };
|
|
357
|
-
case 2:
|
|
358
|
-
return { totalSteps: 2, showSteps: true };
|
|
359
|
-
case 3:
|
|
360
|
-
return { totalSteps: 3, showSteps: true };
|
|
361
421
|
default:
|
|
362
422
|
return { totalSteps: 0, showSteps: false };
|
|
363
423
|
}
|
|
@@ -365,13 +425,6 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
365
425
|
|
|
366
426
|
const stepConfig = getStepConfig();
|
|
367
427
|
|
|
368
|
-
const getInstructionStyle = useCallback(() => {
|
|
369
|
-
if (faceCount > 1) {
|
|
370
|
-
return [styles.instructionText, { color: Global.AppTheme.error }];
|
|
371
|
-
}
|
|
372
|
-
return styles.instructionText;
|
|
373
|
-
}, [faceCount]);
|
|
374
|
-
|
|
375
428
|
return (
|
|
376
429
|
<View style={styles.container}>
|
|
377
430
|
<View style={styles.cameraContainer}>
|
|
@@ -420,41 +473,25 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
420
473
|
</View>
|
|
421
474
|
)}
|
|
422
475
|
|
|
423
|
-
{!showCodeScanner && showCamera && cameraDevice &&
|
|
476
|
+
{!showCodeScanner && showCamera && cameraDevice && livenessLevel === 1 && (
|
|
424
477
|
<View style={styles.livenessContainer}>
|
|
425
|
-
<Animated.View
|
|
426
|
-
style={[
|
|
427
|
-
styles.instructionContainer,
|
|
428
|
-
{
|
|
429
|
-
opacity: instructionAnim,
|
|
430
|
-
transform: [
|
|
431
|
-
{
|
|
432
|
-
translateY: instructionAnim.interpolate({
|
|
433
|
-
inputRange: [0, 1],
|
|
434
|
-
outputRange: [10, 0],
|
|
435
|
-
}),
|
|
436
|
-
},
|
|
437
|
-
],
|
|
438
|
-
},
|
|
439
|
-
]}
|
|
440
|
-
>
|
|
478
|
+
<Animated.View style={getInstructionContainerStyle()}>
|
|
441
479
|
<Text style={getInstructionStyle()}>{getInstruction()}</Text>
|
|
442
480
|
</Animated.View>
|
|
443
481
|
|
|
444
|
-
{
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
)}
|
|
482
|
+
{livenessStep === 1 && (
|
|
483
|
+
<View style={styles.blinkProgressContainer}>
|
|
484
|
+
{[1, 2, 3].map((i) => (
|
|
485
|
+
<View
|
|
486
|
+
key={i}
|
|
487
|
+
style={[
|
|
488
|
+
styles.blinkDot,
|
|
489
|
+
blinkCount >= i && styles.blinkDotActive,
|
|
490
|
+
]}
|
|
491
|
+
/>
|
|
492
|
+
))}
|
|
493
|
+
</View>
|
|
494
|
+
)}
|
|
458
495
|
|
|
459
496
|
{stepConfig.showSteps && faceCount <= 1 && (
|
|
460
497
|
<>
|
|
@@ -496,47 +533,34 @@ const CaptureImageWithoutEdit = React.memo(
|
|
|
496
533
|
<Text style={styles.stepLabel}>Blink</Text>
|
|
497
534
|
</>
|
|
498
535
|
)}
|
|
499
|
-
{livenessLevel === 2 && (
|
|
500
|
-
<>
|
|
501
|
-
<Text style={styles.stepLabel}>Center</Text>
|
|
502
|
-
<Text style={styles.stepLabel}>Right</Text>
|
|
503
|
-
<Text style={styles.stepLabel}>Left</Text>
|
|
504
|
-
</>
|
|
505
|
-
)}
|
|
506
|
-
{livenessLevel === 3 && (
|
|
507
|
-
<>
|
|
508
|
-
<Text style={styles.stepLabel}>Center</Text>
|
|
509
|
-
<Text style={styles.stepLabel}>Right</Text>
|
|
510
|
-
<Text style={styles.stepLabel}>Left</Text>
|
|
511
|
-
<Text style={styles.stepLabel}>Blink</Text>
|
|
512
|
-
</>
|
|
513
|
-
)}
|
|
514
536
|
</View>
|
|
515
537
|
</>
|
|
516
538
|
)}
|
|
517
539
|
</View>
|
|
518
540
|
)}
|
|
519
541
|
|
|
520
|
-
{!showCodeScanner && showCamera && cameraDevice &&
|
|
542
|
+
{!showCodeScanner && showCamera && cameraDevice && livenessLevel === 0 && (
|
|
521
543
|
<View style={styles.livenessContainer}>
|
|
522
|
-
<Animated.View
|
|
523
|
-
style={[
|
|
524
|
-
styles.instructionContainer,
|
|
525
|
-
{
|
|
526
|
-
opacity: instructionAnim,
|
|
527
|
-
},
|
|
528
|
-
]}
|
|
529
|
-
>
|
|
544
|
+
<Animated.View style={getInstructionContainerStyle()}>
|
|
530
545
|
<Text style={getInstructionStyle()}>{getInstruction()}</Text>
|
|
531
546
|
</Animated.View>
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
547
|
+
{isFaceCentered && (
|
|
548
|
+
<View style={styles.confidenceContainer}>
|
|
549
|
+
<Text style={styles.confidenceText}>
|
|
550
|
+
Confidence: {Math.round(antiSpoofConfidence * 100)}%
|
|
551
|
+
</Text>
|
|
552
|
+
<View style={styles.confidenceBar}>
|
|
536
553
|
<View
|
|
537
554
|
style={[
|
|
538
|
-
styles.
|
|
539
|
-
{
|
|
555
|
+
styles.confidenceProgress,
|
|
556
|
+
{
|
|
557
|
+
width: `${antiSpoofConfidence * 100}%`,
|
|
558
|
+
backgroundColor: antiSpoofConfidence > 0.7
|
|
559
|
+
? Global.AppTheme.success
|
|
560
|
+
: antiSpoofConfidence > 0.4
|
|
561
|
+
? Global.AppTheme.warning
|
|
562
|
+
: Global.AppTheme.error
|
|
563
|
+
}
|
|
540
564
|
]}
|
|
541
565
|
/>
|
|
542
566
|
</View>
|
|
@@ -613,12 +637,130 @@ const styles = StyleSheet.create({
|
|
|
613
637
|
borderRadius: 8,
|
|
614
638
|
marginBottom: 10,
|
|
615
639
|
},
|
|
640
|
+
liveInstructionContainer: {
|
|
641
|
+
backgroundColor: Global.AppTheme.success,
|
|
642
|
+
paddingHorizontal: 20,
|
|
643
|
+
paddingVertical: 12,
|
|
644
|
+
borderRadius: 8,
|
|
645
|
+
marginBottom: 10,
|
|
646
|
+
},
|
|
647
|
+
verifyingInstructionContainer: {
|
|
648
|
+
backgroundColor: Global.AppTheme.warning,
|
|
649
|
+
paddingHorizontal: 20,
|
|
650
|
+
paddingVertical: 12,
|
|
651
|
+
borderRadius: 8,
|
|
652
|
+
marginBottom: 10,
|
|
653
|
+
},
|
|
654
|
+
errorInstructionContainer: {
|
|
655
|
+
backgroundColor: Global.AppTheme.error,
|
|
656
|
+
paddingHorizontal: 20,
|
|
657
|
+
paddingVertical: 12,
|
|
658
|
+
borderRadius: 8,
|
|
659
|
+
marginBottom: 10,
|
|
660
|
+
},
|
|
616
661
|
instructionText: {
|
|
617
662
|
color: 'white',
|
|
618
663
|
fontSize: 16,
|
|
619
664
|
fontWeight: 'bold',
|
|
620
665
|
textAlign: 'center',
|
|
621
666
|
},
|
|
667
|
+
liveInstructionText: {
|
|
668
|
+
color: 'white',
|
|
669
|
+
fontWeight: 'bold',
|
|
670
|
+
},
|
|
671
|
+
errorInstructionText: {
|
|
672
|
+
color: 'white',
|
|
673
|
+
fontWeight: 'bold',
|
|
674
|
+
},
|
|
675
|
+
// Live Indicator
|
|
676
|
+
liveIndicator: {
|
|
677
|
+
position: 'absolute',
|
|
678
|
+
top: 20,
|
|
679
|
+
right: 20,
|
|
680
|
+
backgroundColor: Global.AppTheme.success,
|
|
681
|
+
paddingHorizontal: 12,
|
|
682
|
+
paddingVertical: 6,
|
|
683
|
+
borderRadius: 16,
|
|
684
|
+
shadowColor: '#000',
|
|
685
|
+
shadowOffset: {
|
|
686
|
+
width: 0,
|
|
687
|
+
height: 2,
|
|
688
|
+
},
|
|
689
|
+
shadowOpacity: 0.25,
|
|
690
|
+
shadowRadius: 3.84,
|
|
691
|
+
elevation: 5,
|
|
692
|
+
},
|
|
693
|
+
liveIndicatorInner: {
|
|
694
|
+
flexDirection: 'row',
|
|
695
|
+
alignItems: 'center',
|
|
696
|
+
},
|
|
697
|
+
livePulse: {
|
|
698
|
+
width: 8,
|
|
699
|
+
height: 8,
|
|
700
|
+
borderRadius: 4,
|
|
701
|
+
backgroundColor: 'white',
|
|
702
|
+
marginRight: 6,
|
|
703
|
+
},
|
|
704
|
+
liveIndicatorText: {
|
|
705
|
+
color: 'white',
|
|
706
|
+
fontSize: 12,
|
|
707
|
+
fontWeight: 'bold',
|
|
708
|
+
},
|
|
709
|
+
// Status Overview
|
|
710
|
+
statusOverview: {
|
|
711
|
+
position: 'absolute',
|
|
712
|
+
bottom: 20,
|
|
713
|
+
left: 20,
|
|
714
|
+
right: 20,
|
|
715
|
+
backgroundColor: 'rgba(0,0,0,0.8)',
|
|
716
|
+
borderRadius: 12,
|
|
717
|
+
padding: 16,
|
|
718
|
+
},
|
|
719
|
+
statusRow: {
|
|
720
|
+
flexDirection: 'row',
|
|
721
|
+
justifyContent: 'space-between',
|
|
722
|
+
marginBottom: 12,
|
|
723
|
+
},
|
|
724
|
+
statusItem: {
|
|
725
|
+
flexDirection: 'row',
|
|
726
|
+
alignItems: 'center',
|
|
727
|
+
},
|
|
728
|
+
statusDot: {
|
|
729
|
+
width: 10,
|
|
730
|
+
height: 10,
|
|
731
|
+
borderRadius: 5,
|
|
732
|
+
marginRight: 6,
|
|
733
|
+
},
|
|
734
|
+
statusGood: {
|
|
735
|
+
backgroundColor: Global.AppTheme.success,
|
|
736
|
+
},
|
|
737
|
+
statusPending: {
|
|
738
|
+
backgroundColor: '#666',
|
|
739
|
+
},
|
|
740
|
+
statusText: {
|
|
741
|
+
color: 'white',
|
|
742
|
+
fontSize: 12,
|
|
743
|
+
},
|
|
744
|
+
confidenceContainer: {
|
|
745
|
+
marginTop: 8,
|
|
746
|
+
},
|
|
747
|
+
confidenceText: {
|
|
748
|
+
color: 'white',
|
|
749
|
+
fontSize: 12,
|
|
750
|
+
marginBottom: 4,
|
|
751
|
+
textAlign: 'center',
|
|
752
|
+
},
|
|
753
|
+
confidenceBar: {
|
|
754
|
+
height: 4,
|
|
755
|
+
backgroundColor: 'rgba(255,255,255,0.3)',
|
|
756
|
+
borderRadius: 2,
|
|
757
|
+
overflow: 'hidden',
|
|
758
|
+
},
|
|
759
|
+
confidenceProgress: {
|
|
760
|
+
height: '100%',
|
|
761
|
+
borderRadius: 2,
|
|
762
|
+
},
|
|
763
|
+
// Existing styles
|
|
622
764
|
blinkProgressContainer: {
|
|
623
765
|
flexDirection: 'row',
|
|
624
766
|
marginVertical: 8,
|
|
@@ -631,7 +773,7 @@ const styles = StyleSheet.create({
|
|
|
631
773
|
marginHorizontal: 4,
|
|
632
774
|
},
|
|
633
775
|
blinkDotActive: {
|
|
634
|
-
backgroundColor:
|
|
776
|
+
backgroundColor: Global.AppTheme.success,
|
|
635
777
|
},
|
|
636
778
|
stepsContainer: {
|
|
637
779
|
flexDirection: 'row',
|