proposal-system/mobile/src/screens/auth/LoginScreen.tsx
Adam Moussa da00d27049 Add email/password login, fix Cognito config, enable mobile auto-deploy
Apple review requires a test account login path that doesn't depend on
Google OAuth. Add amazon-cognito-identity-js for direct SRP auth with a
native email/password form on the login screen. Fill in the empty Cognito
client ID and pool ID, fix the Cognito domain prefix, and align CDK
callback URLs with the app's actual URL scheme. Enable push-triggered
mobile deploys, add CDK outputs for client IDs, fix stale README
references, and add mobile/README.md.
2026-05-20 11:36:51 -04:00

244 lines
6 KiB
TypeScript

import React, { useState } from 'react';
import {
View,
StyleSheet,
KeyboardAvoidingView,
Platform,
ScrollView,
} from 'react-native';
import {
Button,
Text,
Surface,
TextInput,
Divider,
HelperText,
} from 'react-native-paper';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useDispatch } from 'react-redux';
import { authApi } from '../../lib/api/auth';
import { setUser, setError, clearError } from '../../store/slices/authSlice';
export function LoginScreen() {
const dispatch = useDispatch();
const [loading, setLoading] = useState(false);
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [showPassword, setShowPassword] = useState(false);
const [localError, setLocalError] = useState('');
const handleGoogleLogin = async () => {
setLoading(true);
setLocalError('');
dispatch(clearError());
try {
const user = await authApi.login();
dispatch(setUser(user));
} catch (err) {
const message =
err instanceof Error ? err.message : 'Login failed. Please try again.';
setLocalError(message);
dispatch(setError(message));
} finally {
setLoading(false);
}
};
const handleCredentialLogin = async () => {
if (!email.trim() || !password) {
setLocalError('Email and password are required.');
return;
}
setLoading(true);
setLocalError('');
dispatch(clearError());
try {
const user = await authApi.loginWithCredentials(
email.trim().toLowerCase(),
password,
);
dispatch(setUser(user));
} catch (err) {
const message =
err instanceof Error ? err.message : 'Login failed. Please try again.';
setLocalError(message);
dispatch(setError(message));
} finally {
setLoading(false);
}
};
return (
<SafeAreaView style={styles.container}>
<KeyboardAvoidingView
style={styles.flex}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
>
<ScrollView
contentContainerStyle={styles.content}
keyboardShouldPersistTaps="handled"
>
<Surface style={styles.logoContainer} elevation={0}>
<Text variant="headlineLarge" style={styles.brand}>
Sea Haven
</Text>
<Text variant="titleMedium" style={styles.brandSub}>
Industries
</Text>
</Surface>
<Text variant="headlineSmall" style={styles.title}>
Proposal System
</Text>
<Text variant="bodyLarge" style={styles.subtitle}>
Submit and manage proposals from anywhere
</Text>
<View style={styles.form}>
<TextInput
label="Email"
value={email}
onChangeText={setEmail}
autoCapitalize="none"
autoComplete="email"
keyboardType="email-address"
textContentType="emailAddress"
mode="outlined"
disabled={loading}
style={styles.input}
/>
<TextInput
label="Password"
value={password}
onChangeText={setPassword}
secureTextEntry={!showPassword}
autoCapitalize="none"
autoComplete="password"
textContentType="password"
mode="outlined"
disabled={loading}
style={styles.input}
right={
<TextInput.Icon
icon={showPassword ? 'eye-off' : 'eye'}
onPress={() => setShowPassword(!showPassword)}
/>
}
/>
{localError ? (
<HelperText type="error" visible>
{localError}
</HelperText>
) : null}
<Button
mode="contained"
onPress={handleCredentialLogin}
loading={loading}
disabled={loading}
contentStyle={styles.buttonContent}
style={styles.button}
>
Sign In
</Button>
</View>
<View style={styles.dividerRow}>
<Divider style={styles.dividerLine} />
<Text variant="bodySmall" style={styles.dividerText}>
OR
</Text>
<Divider style={styles.dividerLine} />
</View>
<Button
mode="outlined"
icon="google"
onPress={handleGoogleLogin}
loading={loading}
disabled={loading}
contentStyle={styles.buttonContent}
style={styles.button}
>
Sign in with Google
</Button>
<Text variant="bodySmall" style={styles.footer}>
Use your Sea Haven email to sign in
</Text>
</ScrollView>
</KeyboardAvoidingView>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#FAFAFA',
},
flex: {
flex: 1,
},
content: {
flexGrow: 1,
justifyContent: 'center',
alignItems: 'center',
paddingHorizontal: 32,
paddingVertical: 24,
},
logoContainer: {
alignItems: 'center',
marginBottom: 32,
backgroundColor: 'transparent',
},
brand: {
fontWeight: '800',
color: '#1565C0',
},
brandSub: {
color: '#757575',
marginTop: -4,
},
title: {
fontWeight: '700',
color: '#212121',
marginBottom: 8,
},
subtitle: {
color: '#757575',
textAlign: 'center',
marginBottom: 32,
},
form: {
width: '100%',
},
input: {
marginBottom: 12,
},
buttonContent: {
paddingVertical: 8,
},
button: {
borderRadius: 8,
width: '100%',
},
dividerRow: {
flexDirection: 'row',
alignItems: 'center',
width: '100%',
marginVertical: 20,
},
dividerLine: {
flex: 1,
},
dividerText: {
color: '#9E9E9E',
marginHorizontal: 16,
},
footer: {
color: '#9E9E9E',
marginTop: 24,
},
});