P0 fixes: - Replace crypto.randomUUID() with uuid v4 (not available in RN) - Replace Platform.Version with expo-device osVersion - Fix auth navigation types, remove unused App route P1 fixes: - Push notification handler respects user preferences (useRef pattern) - Fix stale closure: use zustand subscribe + useRef for live preferences - Add retry logging for device registration failures - Replace emoji tab icons with @expo/vector-icons Ionicons - Document API integration TODOs in all local-only stores P2 fixes: - Add __DEV__ global declaration (global.d.ts) - Fix package.json main field to expo/AppEntry.js - Add retry logging for push device registration - Add z-index/elevation to LoadingOverlay - Add visual indicator to EmptyState icon P3 fixes: - Type navigation with NavigationProp<RootStackParamList> - Move getSeverityColor to theme.ts (single source of truth) - Add useMemo for SpamShield filter computations - Verified usesNonExemptEncryption: false is correct for expo-secure-store Co-Authored-By: Paperclip <noreply@paperclip.ing>
143 lines
3.7 KiB
TypeScript
143 lines
3.7 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { StyleSheet, Text, View, SafeAreaView, KeyboardAvoidingView, Platform, ScrollView, Alert } from 'react-native';
|
|
import { useNavigation, NavigationProp } from '@react-navigation/native';
|
|
import { useAuthStore } from '@/store/authStore';
|
|
import { Button, Input } from '@/components';
|
|
import { COLORS, FONT_SIZES, SPACING } from '@/constants/theme';
|
|
|
|
type RootStackParamList = {
|
|
Login: undefined;
|
|
Register: undefined;
|
|
};
|
|
|
|
export function LoginScreen() {
|
|
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
|
const { login, isLoading, clearError } = useAuthStore();
|
|
|
|
const [email, setEmail] = useState('');
|
|
const [password, setPassword] = useState('');
|
|
const [formError, setFormError] = useState('');
|
|
|
|
const handleLogin = async () => {
|
|
setFormError('');
|
|
clearError();
|
|
|
|
if (!email || !password) {
|
|
setFormError('Please fill in all fields');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await login(email, password);
|
|
} catch (err: any) {
|
|
setFormError(err.message || 'Login failed. Please try again.');
|
|
Alert.alert('Login Failed', err.message || 'Please check your credentials and try again.');
|
|
}
|
|
};
|
|
|
|
return (
|
|
<SafeAreaView style={styles.container}>
|
|
<KeyboardAvoidingView
|
|
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
|
style={styles.keyboardView}
|
|
>
|
|
<ScrollView contentContainerStyle={styles.scrollContent}>
|
|
<View style={styles.header}>
|
|
<Text style={styles.logo}>ShieldAI</Text>
|
|
<Text style={styles.tagline}>Your digital protection suite</Text>
|
|
</View>
|
|
|
|
<View style={styles.form}>
|
|
{formError && <Text style={styles.error}>{formError}</Text>}
|
|
|
|
<Input
|
|
label="Email"
|
|
placeholder="you@example.com"
|
|
value={email}
|
|
onChangeText={setEmail}
|
|
autoCapitalize="none"
|
|
keyboardType="email-address"
|
|
autoComplete="email"
|
|
/>
|
|
|
|
<Input
|
|
label="Password"
|
|
placeholder="••••••••"
|
|
value={password}
|
|
onChangeText={setPassword}
|
|
secureTextEntry
|
|
autoComplete="password"
|
|
/>
|
|
|
|
<Button
|
|
title={isLoading ? 'Signing in...' : 'Sign In'}
|
|
onPress={handleLogin}
|
|
disabled={isLoading}
|
|
fullWidth
|
|
/>
|
|
|
|
<View style={styles.footer}>
|
|
<Text style={styles.footerText}>Don't have an account? </Text>
|
|
<Text style={styles.link} onPress={() => navigation.navigate('Register')}>
|
|
Sign Up
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
</ScrollView>
|
|
</KeyboardAvoidingView>
|
|
</SafeAreaView>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
flex: 1,
|
|
backgroundColor: COLORS.background,
|
|
},
|
|
keyboardView: {
|
|
flex: 1,
|
|
},
|
|
scrollContent: {
|
|
flexGrow: 1,
|
|
justifyContent: 'center',
|
|
padding: SPACING.lg,
|
|
},
|
|
header: {
|
|
alignItems: 'center',
|
|
marginBottom: SPACING.xxl,
|
|
},
|
|
logo: {
|
|
color: COLORS.primary,
|
|
fontSize: FONT_SIZES.xxxl,
|
|
fontWeight: 'bold',
|
|
},
|
|
tagline: {
|
|
color: COLORS.textSecondary,
|
|
fontSize: FONT_SIZES.md,
|
|
marginTop: SPACING.sm,
|
|
},
|
|
form: {
|
|
width: '100%',
|
|
},
|
|
error: {
|
|
color: COLORS.danger,
|
|
fontSize: FONT_SIZES.sm,
|
|
marginBottom: SPACING.md,
|
|
textAlign: 'center',
|
|
},
|
|
footer: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'center',
|
|
marginTop: SPACING.lg,
|
|
},
|
|
footerText: {
|
|
color: COLORS.textSecondary,
|
|
fontSize: FONT_SIZES.sm,
|
|
},
|
|
link: {
|
|
color: COLORS.primary,
|
|
fontSize: FONT_SIZES.sm,
|
|
fontWeight: '600',
|
|
},
|
|
});
|