mirror of
https://github.com/Sea-Haven-Industries/proposal-system.git
synced 2026-10-07 16:18:57 +00:00
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.
244 lines
6 KiB
TypeScript
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,
|
|
},
|
|
});
|