Fix form keyboard issues and sticky footers in agent screens

This commit is contained in:
Abin Toppo
2026-05-26 16:34:05 +05:30
commit 9ac5e95d52
410 changed files with 86650 additions and 0 deletions
+248
View File
@@ -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',
},
});