Files
frontend/src/components/common/BiometricModal.tsx
2026-02-01 21:13:15 -08:00

237 lines
6.2 KiB
TypeScript

import React, { useEffect, useState } from 'react';
import {
View,
Text,
Modal,
StyleSheet,
TouchableOpacity,
Animated,
} from 'react-native';
import { LinearGradient } from 'expo-linear-gradient';
import { Ionicons, MaterialCommunityIcons } from '@expo/vector-icons';
import { colors, borderRadius, spacing, typography, shadows } from '../../theme/colors';
interface BiometricModalProps {
visible: boolean;
onSuccess: () => void;
onCancel: () => void;
title?: string;
message?: string;
isDark?: boolean;
}
export default function BiometricModal({
visible,
onSuccess,
onCancel,
title = 'Captain\'s Verification',
message = 'Verify your identity to continue',
isDark = false,
}: BiometricModalProps) {
const [scanAnimation] = useState(new Animated.Value(0));
const [pulseAnimation] = useState(new Animated.Value(1));
const [isScanning, setIsScanning] = useState(false);
useEffect(() => {
if (visible) {
setIsScanning(false);
scanAnimation.setValue(0);
// Pulse animation
Animated.loop(
Animated.sequence([
Animated.timing(pulseAnimation, {
toValue: 1.08,
duration: 1000,
useNativeDriver: true,
}),
Animated.timing(pulseAnimation, {
toValue: 1,
duration: 1000,
useNativeDriver: true,
}),
])
).start();
}
}, [visible]);
const handleScan = () => {
setIsScanning(true);
Animated.loop(
Animated.sequence([
Animated.timing(scanAnimation, {
toValue: 1,
duration: 400,
useNativeDriver: true,
}),
Animated.timing(scanAnimation, {
toValue: 0,
duration: 400,
useNativeDriver: true,
}),
]),
{ iterations: 1 }
).start(() => {
onSuccess();
});
};
const backgroundColor = isDark ? colors.vault.cardBackground : colors.white;
const textColor = isDark ? colors.vault.text : colors.nautical.navy;
const accentColor = isDark ? colors.vault.primary : colors.nautical.teal;
const accentGradient: [string, string] = isDark
? [colors.vault.primary, colors.vault.secondary]
: [colors.nautical.teal, colors.nautical.seafoam];
return (
<Modal
visible={visible}
transparent
animationType="fade"
onRequestClose={onCancel}
>
<View style={styles.overlay}>
<View style={[styles.container, { backgroundColor }, shadows.medium]}>
{/* Ship wheel watermark */}
<View style={styles.watermark}>
<MaterialCommunityIcons
name="ship-wheel"
size={150}
color={isDark ? colors.vault.primary : colors.nautical.lightMint}
style={{ opacity: 0.15 }}
/>
</View>
<Text style={[styles.title, { color: textColor }]}>{title}</Text>
<Text style={[styles.message, { color: isDark ? colors.vault.textSecondary : colors.nautical.sage }]}>
{message}
</Text>
<TouchableOpacity
style={styles.fingerprintButton}
onPress={handleScan}
activeOpacity={0.8}
>
<Animated.View
style={[
styles.fingerprintOuter,
{
backgroundColor: `${accentColor}15`,
transform: [{ scale: pulseAnimation }],
},
]}
/>
<Animated.View
style={[
styles.fingerprintContainer,
{
opacity: scanAnimation.interpolate({
inputRange: [0, 1],
outputRange: [1, 0.7],
}),
transform: [
{
scale: scanAnimation.interpolate({
inputRange: [0, 1],
outputRange: [1, 1.05],
}),
},
],
},
]}
>
<LinearGradient
colors={accentGradient}
style={styles.fingerprintGradient}
>
<Ionicons
name={isScanning ? "finger-print" : "finger-print-outline"}
size={48}
color="#fff"
/>
</LinearGradient>
</Animated.View>
<Text style={[styles.scanText, { color: accentColor }]}>
{isScanning ? 'Verifying...' : 'Tap to Verify'}
</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.cancelButton} onPress={onCancel}>
<Text style={[styles.cancelText, { color: isDark ? colors.vault.textSecondary : colors.nautical.sage }]}>
Cancel
</Text>
</TouchableOpacity>
</View>
</View>
</Modal>
);
}
const styles = StyleSheet.create({
overlay: {
flex: 1,
backgroundColor: 'rgba(26, 58, 74, 0.7)',
justifyContent: 'center',
alignItems: 'center',
padding: spacing.lg,
},
container: {
width: '100%',
borderRadius: borderRadius.xxl,
padding: spacing.xl,
alignItems: 'center',
overflow: 'hidden',
},
watermark: {
position: 'absolute',
top: -30,
right: -30,
},
title: {
fontSize: typography.fontSize.lg,
fontWeight: '600',
marginBottom: spacing.sm,
textAlign: 'center',
fontFamily: typography.fontFamily.serif,
},
message: {
fontSize: typography.fontSize.base,
textAlign: 'center',
marginBottom: spacing.xl,
lineHeight: typography.fontSize.base * 1.5,
},
fingerprintButton: {
alignItems: 'center',
marginBottom: spacing.lg,
},
fingerprintOuter: {
position: 'absolute',
width: 150,
height: 150,
borderRadius: 75,
},
fingerprintContainer: {
marginBottom: spacing.md,
},
fingerprintGradient: {
width: 110,
height: 110,
borderRadius: 55,
justifyContent: 'center',
alignItems: 'center',
},
scanText: {
fontSize: typography.fontSize.base,
fontWeight: '600',
},
cancelButton: {
paddingVertical: spacing.md,
paddingHorizontal: spacing.xl,
},
cancelText: {
fontSize: typography.fontSize.base,
fontWeight: '500',
},
});