Files
pwap/mobile/src/screens/VerifyHumanScreen.tsx
T

342 lines
8.8 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState, useEffect } from 'react';
import {
View,
Text,
StyleSheet,
TouchableOpacity,
SafeAreaView,
StatusBar,
Animated,
Platform,
Alert,
} from 'react-native';
import { LinearGradient } from 'expo-linear-gradient';
import { KurdistanColors } from '../theme/colors';
import AsyncStorage from '@react-native-async-storage/async-storage';
import * as LocalAuthentication from 'expo-local-authentication';
const HUMAN_VERIFIED_KEY = '@pezkuwi_human_verified';
interface VerifyHumanScreenProps {
onVerified: () => void;
}
const VerifyHumanScreen: React.FC<VerifyHumanScreenProps> = ({ onVerified }) => {
const [isChecked, setIsChecked] = useState(false);
const [isBiometricSupported, setIsBiometricSupported] = useState(false);
const [scaleValue] = useState(new Animated.Value(1));
// Check biometric support on mount
useEffect(() => {
checkBiometricSupport();
}, []);
const checkBiometricSupport = async () => {
try {
const compatible = await LocalAuthentication.hasHardwareAsync();
const enrolled = await LocalAuthentication.isEnrolledAsync();
setIsBiometricSupported(compatible && enrolled);
} catch (error) {
console.warn('Biometric check failed:', error);
}
};
const handleBiometricAuth = async () => {
try {
const result = await LocalAuthentication.authenticateAsync({
promptMessage: 'Verify you are human',
fallbackLabel: 'Use Passcode',
cancelLabel: 'Cancel',
disableDeviceFallback: false,
});
if (result.success) {
completeVerification();
} else {
// Only show alert if it wasn't a user cancellation
if (result.error !== 'user_cancel') {
Alert.alert('Verification Failed', 'Could not verify identity. Please try again.');
}
}
} catch (error) {
console.error('Biometric auth error:', error);
Alert.alert('Error', 'Biometric authentication unavailable');
}
};
const handleManualVerify = () => {
if (!isChecked) return;
completeVerification();
};
const completeVerification = async () => {
// Save verification status
try {
await AsyncStorage.setItem(HUMAN_VERIFIED_KEY, 'true');
} catch (_error) {
if (__DEV__) console.error('Failed to save verification:', _error);
}
// Animate and navigate
Animated.sequence([
Animated.timing(scaleValue, {
toValue: 1.1,
duration: 100,
useNativeDriver: true,
}),
Animated.timing(scaleValue, {
toValue: 1,
duration: 100,
useNativeDriver: true,
}),
]).start(() => {
setTimeout(() => onVerified(), 200);
});
};
const toggleCheck = () => {
setIsChecked(!isChecked);
};
return (
<SafeAreaView style={styles.container}>
<StatusBar barStyle="light-content" />
<LinearGradient
colors={[KurdistanColors.kesk, KurdistanColors.zer, KurdistanColors.sor]}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={styles.gradient}
>
<View style={styles.content}>
{/* Security Icon */}
<View style={styles.iconContainer}>
<Text style={styles.iconText}>🛡</Text>
</View>
{/* Title */}
<Text style={styles.title}>Identity Verification</Text>
<Text style={styles.subtitle}>
Please confirm your identity to continue
</Text>
{/* Biometric Button (Primary) */}
{isBiometricSupported ? (
<TouchableOpacity
style={styles.biometricButton}
onPress={handleBiometricAuth}
activeOpacity={0.8}
>
<Text style={styles.biometricIcon}>👆</Text>
<Text style={styles.biometricText}>Verify with Biometrics</Text>
</TouchableOpacity>
) : (
/* Fallback to Manual Checkbox */
<TouchableOpacity
style={styles.verificationBox}
onPress={toggleCheck}
activeOpacity={0.8}
>
<View style={[styles.checkbox, isChecked && styles.checkboxChecked]}>
{isChecked && <Text style={styles.checkmark}></Text>}
</View>
<Text style={styles.verificationText}>
I&apos;m not a robot
</Text>
</TouchableOpacity>
)}
{/* Info Text */}
<Text style={styles.infoText}>
{isBiometricSupported
? "Secure hardware-backed verification"
: "This helps protect the Pezkuwi network from automated attacks"
}
</Text>
{/* Continue Button (Only for manual fallback) */}
{!isBiometricSupported && (
<TouchableOpacity
style={[styles.continueButton, !isChecked && styles.continueButtonDisabled]}
onPress={handleManualVerify}
disabled={!isChecked}
activeOpacity={0.8}
>
<Animated.View style={{ transform: [{ scale: scaleValue }] }}>
<Text
style={[
styles.continueButtonText,
!isChecked && styles.continueButtonTextDisabled,
]}
>
Continue
</Text>
</Animated.View>
</TouchableOpacity>
)}
{/* Footer */}
<View style={styles.footer}>
<Text style={styles.footerText}>
Secure & Private
</Text>
</View>
</View>
</LinearGradient>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: KurdistanColors.kesk,
},
gradient: {
flex: 1,
},
content: {
flex: 1,
padding: 20,
justifyContent: 'center',
alignItems: 'center',
},
iconContainer: {
width: 100,
height: 100,
borderRadius: 50,
backgroundColor: KurdistanColors.spi,
justifyContent: 'center',
alignItems: 'center',
marginBottom: 30,
boxShadow: '0 4px 8px rgba(0, 0, 0, 0.3)',
elevation: 8,
},
iconText: {
fontSize: 50,
},
title: {
fontSize: 28,
fontWeight: 'bold',
color: KurdistanColors.spi,
textAlign: 'center',
marginBottom: 10,
},
subtitle: {
fontSize: 16,
color: KurdistanColors.spi,
textAlign: 'center',
opacity: 0.9,
marginBottom: 40,
},
biometricButton: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: KurdistanColors.spi,
borderRadius: 16,
paddingVertical: 20,
paddingHorizontal: 32,
marginBottom: 20,
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.2)',
elevation: 8,
},
biometricIcon: {
fontSize: 24,
marginRight: 12,
},
biometricText: {
fontSize: 18,
fontWeight: 'bold',
color: KurdistanColors.kesk,
},
verificationBox: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: KurdistanColors.spi,
borderRadius: 12,
padding: 20,
marginBottom: 20,
boxShadow: '0 2px 4px rgba(0, 0, 0, 0.2)',
elevation: 4,
width: '100%',
maxWidth: 400,
},
checkbox: {
width: 32,
height: 32,
borderRadius: 6,
borderWidth: 2,
borderColor: KurdistanColors.kesk,
marginRight: 15,
justifyContent: 'center',
alignItems: 'center',
},
checkboxChecked: {
backgroundColor: KurdistanColors.kesk,
borderColor: KurdistanColors.kesk,
},
checkmark: {
fontSize: 20,
color: KurdistanColors.spi,
fontWeight: 'bold',
},
verificationText: {
fontSize: 18,
color: KurdistanColors.reş,
fontWeight: '500',
},
infoText: {
fontSize: 13,
color: KurdistanColors.spi,
textAlign: 'center',
opacity: 0.8,
marginBottom: 40,
paddingHorizontal: 20,
},
continueButton: {
backgroundColor: KurdistanColors.spi,
borderRadius: 12,
padding: 16,
alignItems: 'center',
width: '100%',
maxWidth: 400,
boxShadow: '0 4px 6px rgba(0, 0, 0, 0.3)',
elevation: 6,
},
continueButtonDisabled: {
backgroundColor: 'rgba(255, 255, 255, 0.3)',
boxShadow: 'none',
elevation: 0,
},
continueButtonText: {
fontSize: 18,
fontWeight: 'bold',
color: KurdistanColors.kesk,
},
continueButtonTextDisabled: {
color: 'rgba(0, 0, 0, 0.3)',
},
footer: {
position: 'absolute',
bottom: 30,
alignItems: 'center',
},
footerText: {
fontSize: 14,
color: KurdistanColors.spi,
opacity: 0.8,
},
});
export default VerifyHumanScreen;
// Export helper to check verification status
export const checkHumanVerification = async (): Promise<boolean> => {
try {
const verified = await AsyncStorage.getItem(HUMAN_VERIFIED_KEY);
return verified === 'true';
} catch (_error) {
if (__DEV__) console.error('Failed to check verification:', _error);
return false;
}
};