mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-06-13 15:51:02 +00:00
feat: Add biometric verification and restore onboarding flow
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
@@ -7,10 +7,13 @@ import {
|
|||||||
SafeAreaView,
|
SafeAreaView,
|
||||||
StatusBar,
|
StatusBar,
|
||||||
Animated,
|
Animated,
|
||||||
|
Platform,
|
||||||
|
Alert,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { LinearGradient } from 'expo-linear-gradient';
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
import { KurdistanColors } from '../theme/colors';
|
import { KurdistanColors } from '../theme/colors';
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
|
import * as LocalAuthentication from 'expo-local-authentication';
|
||||||
|
|
||||||
const HUMAN_VERIFIED_KEY = '@pezkuwi_human_verified';
|
const HUMAN_VERIFIED_KEY = '@pezkuwi_human_verified';
|
||||||
|
|
||||||
@@ -20,11 +23,53 @@ interface VerifyHumanScreenProps {
|
|||||||
|
|
||||||
const VerifyHumanScreen: React.FC<VerifyHumanScreenProps> = ({ onVerified }) => {
|
const VerifyHumanScreen: React.FC<VerifyHumanScreenProps> = ({ onVerified }) => {
|
||||||
const [isChecked, setIsChecked] = useState(false);
|
const [isChecked, setIsChecked] = useState(false);
|
||||||
|
const [isBiometricSupported, setIsBiometricSupported] = useState(false);
|
||||||
const [scaleValue] = useState(new Animated.Value(1));
|
const [scaleValue] = useState(new Animated.Value(1));
|
||||||
|
|
||||||
const handleVerify = async () => {
|
// Check biometric support on mount
|
||||||
if (!isChecked) return;
|
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
|
// Save verification status
|
||||||
try {
|
try {
|
||||||
await AsyncStorage.setItem(HUMAN_VERIFIED_KEY, 'true');
|
await AsyncStorage.setItem(HUMAN_VERIFIED_KEY, 'true');
|
||||||
@@ -69,48 +114,65 @@ const VerifyHumanScreen: React.FC<VerifyHumanScreenProps> = ({ onVerified }) =>
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Title */}
|
{/* Title */}
|
||||||
<Text style={styles.title}>Security Verification</Text>
|
<Text style={styles.title}>Identity Verification</Text>
|
||||||
<Text style={styles.subtitle}>
|
<Text style={styles.subtitle}>
|
||||||
Please confirm you are human to continue
|
Please confirm your identity to continue
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
{/* Verification Box */}
|
{/* Biometric Button (Primary) */}
|
||||||
<TouchableOpacity
|
{isBiometricSupported ? (
|
||||||
style={styles.verificationBox}
|
<TouchableOpacity
|
||||||
onPress={toggleCheck}
|
style={styles.biometricButton}
|
||||||
activeOpacity={0.8}
|
onPress={handleBiometricAuth}
|
||||||
>
|
activeOpacity={0.8}
|
||||||
<View style={[styles.checkbox, isChecked && styles.checkboxChecked]}>
|
>
|
||||||
{isChecked && <Text style={styles.checkmark}>✓</Text>}
|
<Text style={styles.biometricIcon}>👆</Text>
|
||||||
</View>
|
<Text style={styles.biometricText}>Verify with Biometrics</Text>
|
||||||
<Text style={styles.verificationText}>
|
</TouchableOpacity>
|
||||||
I'm not a robot
|
) : (
|
||||||
</Text>
|
/* Fallback to Manual Checkbox */
|
||||||
</TouchableOpacity>
|
<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'm not a robot
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Info Text */}
|
{/* Info Text */}
|
||||||
<Text style={styles.infoText}>
|
<Text style={styles.infoText}>
|
||||||
This helps protect the Pezkuwi network from automated attacks
|
{isBiometricSupported
|
||||||
|
? "Secure hardware-backed verification"
|
||||||
|
: "This helps protect the Pezkuwi network from automated attacks"
|
||||||
|
}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
{/* Continue Button */}
|
{/* Continue Button (Only for manual fallback) */}
|
||||||
<TouchableOpacity
|
{!isBiometricSupported && (
|
||||||
style={[styles.continueButton, !isChecked && styles.continueButtonDisabled]}
|
<TouchableOpacity
|
||||||
onPress={handleVerify}
|
style={[styles.continueButton, !isChecked && styles.continueButtonDisabled]}
|
||||||
disabled={!isChecked}
|
onPress={handleManualVerify}
|
||||||
activeOpacity={0.8}
|
disabled={!isChecked}
|
||||||
>
|
activeOpacity={0.8}
|
||||||
<Animated.View style={{ transform: [{ scale: scaleValue }] }}>
|
>
|
||||||
<Text
|
<Animated.View style={{ transform: [{ scale: scaleValue }] }}>
|
||||||
style={[
|
<Text
|
||||||
styles.continueButtonText,
|
style={[
|
||||||
!isChecked && styles.continueButtonTextDisabled,
|
styles.continueButtonText,
|
||||||
]}
|
!isChecked && styles.continueButtonTextDisabled,
|
||||||
>
|
]}
|
||||||
Continue
|
>
|
||||||
</Text>
|
Continue
|
||||||
</Animated.View>
|
</Text>
|
||||||
</TouchableOpacity>
|
</Animated.View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<View style={styles.footer}>
|
<View style={styles.footer}>
|
||||||
@@ -166,6 +228,26 @@ const styles = StyleSheet.create({
|
|||||||
opacity: 0.9,
|
opacity: 0.9,
|
||||||
marginBottom: 40,
|
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: {
|
verificationBox: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
|||||||
Reference in New Issue
Block a user