237 lines
6.2 KiB
TypeScript
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',
|
|
},
|
|
});
|