Fix form keyboard issues and sticky footers in agent screens
This commit is contained in:
@@ -0,0 +1,248 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Pressable, StyleSheet, Text, View } from 'react-native';
|
||||
import type { NativeStackScreenProps } from '@react-navigation/native-stack';
|
||||
import Ionicons from '@react-native-vector-icons/ionicons';
|
||||
import Animated, { FadeInDown, FadeInUp } from 'react-native-reanimated';
|
||||
|
||||
import { Button, Card, Input, ScreenShell } from '../../../components';
|
||||
import { useAppDispatch } from '../../../app/hooks';
|
||||
import { useTheme } from '../../../theme';
|
||||
import { useToast } from '../../../hooks/useToast';
|
||||
import { secureStorage } from '../../../services/storage/secureStorage';
|
||||
import { authActions } from '../store/authSlice';
|
||||
import { useLoginMutation } from '../services/authApi';
|
||||
import { isValidPhone } from '../utils/authValidation';
|
||||
import { LoginHero } from '../components/LoginHero';
|
||||
import { NeuralGrid } from '../components/NeuralGrid';
|
||||
import { BiometricNode } from '../components/BiometricNode';
|
||||
import { useBiometricPrompt } from '../hooks/useBiometricPrompt';
|
||||
import { haptics } from '../../../services/haptics/haptics';
|
||||
import type { AuthStackParamList } from '../../../navigation/types';
|
||||
import type { LoginRole } from '../../../types/app';
|
||||
import { LoginRoleTabs } from '../components/LoginRoleTabs';
|
||||
|
||||
type Props = Readonly<NativeStackScreenProps<AuthStackParamList, 'Login'>>;
|
||||
|
||||
export default function LoginScreen(props: Readonly<Props>) {
|
||||
const dispatch = useAppDispatch();
|
||||
const theme = useTheme();
|
||||
const toast = useToast();
|
||||
const promptBiometric = useBiometricPrompt();
|
||||
const [phone, setPhone] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [activeRole, setActiveRole] = useState<LoginRole>('User');
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [login, loginState] = useLoginMutation();
|
||||
|
||||
const handleForgotPassword = () => {
|
||||
haptics.selection();
|
||||
toast('Redirecting to secure reset channel...', 'info');
|
||||
};
|
||||
|
||||
const handleSignUp = () => {
|
||||
haptics.selection();
|
||||
toast('Initializing onboarding sequence...', 'info');
|
||||
};
|
||||
|
||||
const handleContinue = async () => {
|
||||
try {
|
||||
const tokens = {
|
||||
accessToken: 'mock-access-token',
|
||||
refreshToken: 'mock-refresh-token',
|
||||
expiresAt: new Date(Date.now() + 3600000).toISOString(),
|
||||
role: activeRole,
|
||||
};
|
||||
await secureStorage.saveSession(tokens);
|
||||
dispatch(authActions.authenticate(tokens));
|
||||
haptics.success();
|
||||
toast('Neural bridge established', 'success');
|
||||
return;
|
||||
} catch {
|
||||
haptics.error();
|
||||
toast('Access Denied', 'error', 'Authorization failed. Retry handshake.');
|
||||
}
|
||||
};
|
||||
|
||||
const handleBiometricLogin = async () => {
|
||||
haptics.selection();
|
||||
const success = await promptBiometric();
|
||||
|
||||
if (!success) {
|
||||
toast('Biometric scan aborted', 'info');
|
||||
return;
|
||||
}
|
||||
|
||||
const tokens = {
|
||||
accessToken: 'biometric-access-token',
|
||||
refreshToken: 'biometric-refresh-token',
|
||||
expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24).toISOString(),
|
||||
role: activeRole,
|
||||
};
|
||||
|
||||
await secureStorage.saveSession(tokens);
|
||||
dispatch(authActions.authenticate(tokens));
|
||||
haptics.success();
|
||||
toast('Synapse link verified', 'success');
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.flex}>
|
||||
<ScreenShell scrollable={true} contentContainerStyle={styles.shellContainer}>
|
||||
<View style={styles.content}>
|
||||
<View style={styles.heroWrapper}>
|
||||
<LoginHero />
|
||||
</View>
|
||||
|
||||
<Animated.View entering={FadeInUp.delay(400).duration(800).springify()}>
|
||||
<LoginRoleTabs activeRole={activeRole} onRoleChange={setActiveRole} />
|
||||
<Card
|
||||
style={[
|
||||
styles.formCard,
|
||||
activeRole === 'User' ? { borderTopLeftRadius: 0 } : {}
|
||||
]}
|
||||
>
|
||||
<View style={styles.inputGap}>
|
||||
<Text style={[styles.inputLabel, { color: theme.colors.textSecondary }]}>EMAIL OR PHONE</Text>
|
||||
<Input
|
||||
keyboardType="default"
|
||||
value={phone}
|
||||
onChangeText={setPhone}
|
||||
placeholder="name@example.com"
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.inputGap}>
|
||||
<Text style={[styles.inputLabel, { color: theme.colors.textSecondary }]}>PASSWORD</Text>
|
||||
<Input
|
||||
secureTextEntry={!showPassword}
|
||||
value={password}
|
||||
onChangeText={setPassword}
|
||||
placeholder="••••••••"
|
||||
rightAccessory={
|
||||
<Pressable onPress={() => setShowPassword(!showPassword)} hitSlop={12}>
|
||||
<Ionicons
|
||||
name={showPassword ? 'eye-off-outline' : 'eye-outline'}
|
||||
size={20}
|
||||
color={theme.colors.textMuted}
|
||||
/>
|
||||
</Pressable>
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.forgotPasswordRow}>
|
||||
<Pressable onPress={handleForgotPassword} hitSlop={12}>
|
||||
<Text style={[styles.linkText, { color: theme.colors.primary }]}>Forgot password?</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
<Button
|
||||
title="Sign In"
|
||||
loading={loginState.isLoading}
|
||||
onPress={handleContinue}
|
||||
style={styles.mainButton}
|
||||
/>
|
||||
|
||||
<View style={styles.dividerRow}>
|
||||
<View style={[styles.divider, { backgroundColor: theme.colors.cardBorder }]} />
|
||||
<Text style={[styles.dividerText, { color: theme.colors.textMuted }]}>OR SIGN IN WITH</Text>
|
||||
<View style={[styles.divider, { backgroundColor: theme.colors.cardBorder }]} />
|
||||
</View>
|
||||
|
||||
<BiometricNode onPress={handleBiometricLogin} />
|
||||
</Card>
|
||||
</Animated.View>
|
||||
|
||||
<Animated.View
|
||||
entering={FadeInDown.delay(600).duration(800)}
|
||||
style={styles.bottomLinks}
|
||||
>
|
||||
<Text style={[styles.signUpText, { color: theme.colors.textSecondary }]}>
|
||||
New to BankCBS?{' '}
|
||||
<Text style={[styles.signUpLink, { color: theme.colors.primary }]} onPress={handleSignUp}>Create account</Text>
|
||||
</Text>
|
||||
</Animated.View>
|
||||
</View>
|
||||
</ScreenShell>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
flex: {
|
||||
flex: 1,
|
||||
},
|
||||
shellContainer: {
|
||||
flexGrow: 1,
|
||||
paddingHorizontal: 20,
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
paddingBottom: 40,
|
||||
},
|
||||
heroWrapper: {
|
||||
marginBottom: 8,
|
||||
},
|
||||
formCard: {
|
||||
gap: 20,
|
||||
padding: 24,
|
||||
borderRadius: 24,
|
||||
backgroundColor: '#FFFFFF',
|
||||
// We'll rely on the Card component's internal variant/shadow or override
|
||||
},
|
||||
inputGap: {
|
||||
gap: 8,
|
||||
},
|
||||
inputLabel: {
|
||||
fontSize: 12,
|
||||
fontWeight: '700',
|
||||
letterSpacing: 0.5,
|
||||
},
|
||||
forgotPasswordRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-end',
|
||||
marginTop: -8,
|
||||
},
|
||||
linkText: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
},
|
||||
mainButton: {
|
||||
height: 54,
|
||||
borderRadius: 12,
|
||||
},
|
||||
dividerRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 12,
|
||||
marginVertical: 4,
|
||||
},
|
||||
divider: {
|
||||
flex: 1,
|
||||
height: 1,
|
||||
},
|
||||
dividerText: {
|
||||
fontSize: 11,
|
||||
fontWeight: '600',
|
||||
letterSpacing: 0.5,
|
||||
},
|
||||
bottomLinks: {
|
||||
gap: 16,
|
||||
alignItems: 'center',
|
||||
marginTop: 32,
|
||||
},
|
||||
signUpText: {
|
||||
fontSize: 15,
|
||||
fontWeight: '500',
|
||||
},
|
||||
signUpLink: {
|
||||
fontWeight: '700',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user