frontend first version
This commit is contained in:
238
src/components/common/BiometricModal.tsx
Normal file
238
src/components/common/BiometricModal.tsx
Normal file
@@ -0,0 +1,238 @@
|
||||
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: 800,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(scanAnimation, {
|
||||
toValue: 0,
|
||||
duration: 800,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
]),
|
||||
{ iterations: 2 }
|
||||
).start(() => {
|
||||
setTimeout(() => {
|
||||
onSuccess();
|
||||
}, 300);
|
||||
});
|
||||
};
|
||||
|
||||
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',
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user