diff options
Diffstat (limited to 'src/screens/Login.tsx')
| -rw-r--r-- | src/screens/Login.tsx | 325 |
1 files changed, 185 insertions, 140 deletions
diff --git a/src/screens/Login.tsx b/src/screens/Login.tsx index 5291b643..193ef767 100644 --- a/src/screens/Login.tsx +++ b/src/screens/Login.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, {useRef} from 'react'; import {RouteProp} from '@react-navigation/native'; import {StackNavigationProp} from '@react-navigation/stack'; import { @@ -9,70 +9,74 @@ import { Image, TouchableOpacity, StyleSheet, - Keyboard, - TouchableWithoutFeedback, } from 'react-native'; -import {RootStackParams} from '../routes'; -import LinearGradient from 'react-native-linear-gradient'; - -import LoginInput from '../components/common/LoginInput'; - -import * as Constants from '../constants'; - -type LoginScreenRouteProp = RouteProp<RootStackParams, 'Login'>; -type LoginScreenNavigationProp = StackNavigationProp<RootStackParams, 'Login'>; +import {RootStackParamList} from '../routes'; +import {Background, TaggInput, CenteredView} from '../components'; +import {usernameRegex, LOGIN_ENDPOINT} from '../constants'; +type LoginScreenRouteProp = RouteProp<RootStackParamList, 'Login'>; +type LoginScreenNavigationProp = StackNavigationProp< + RootStackParamList, + 'Login' +>; interface LoginProps { route: LoginScreenRouteProp; navigation: LoginScreenNavigationProp; } - -const Login = ({navigation}: LoginProps) => { - const input_ref = React.createRef(); - const [data, setData] = React.useState({ +/** + * Login screen. + * @param navigation react-navigation navigation object. + */ +const Login: React.FC<LoginProps> = ({navigation}: LoginProps) => { + // ref for focusing on input fields + const inputRef = useRef(); + // login form state + const [form, setForm] = React.useState({ username: '', password: '', - isValidUser: true, - isValidPassword: true, + isValidUser: false, + isValidPassword: false, + attemptedSubmit: false, }); - /* - Updates the state of username. Also verifies the input of the Username field. - */ + /** + * Updates the state of username. Also verifies the input of the username field by ensuring proper length and characters. + */ const handleUsernameUpdate = (val: string) => { - let validLength: boolean = val.trim().length >= 6; + let validLength: boolean = val.length >= 6; + let validChars: boolean = usernameRegex.test(val); - if (validLength) { - setData({ - ...data, + if (validLength && validChars) { + setForm({ + ...form, username: val, isValidUser: true, }); } else { - setData({ - ...data, + setForm({ + ...form, username: val, isValidUser: false, }); } }; - /* - Updates the state of password. Also verifies the input of the Password field. - */ + /** + * Updates the state of password. Also verifies the input of the password field by ensuring proper length. + */ const handlePasswordUpdate = (val: string) => { let validLength: boolean = val.trim().length >= 8; if (validLength) { - setData({ - ...data, + setForm({ + ...form, password: val, isValidPassword: true, }); } else { - setData({ - ...data, + setForm({ + ...form, password: val, isValidPassword: false, }); @@ -80,23 +84,39 @@ const Login = ({navigation}: LoginProps) => { }; /* - Handler for the Let's Start button or the Go button on the keyboard. + * Handles tap on username keyboard's "Next" button by focusing on password field. + */ + const handleUsernameSubmit = () => { + const passwordField: any = inputRef.current; + if (passwordField) { + passwordField.focus(); + } + }; + + /** + * Handler for the Let's Start button or the Go button on the keyboard. Makes a POST request to the Django login API and presents Alerts based on the status codes that the backend returns. - */ + */ const handleLogin = async () => { + if (!form.attemptedSubmit) { + setForm({ + ...form, + attemptedSubmit: true, + }); + } try { - if (data.isValidUser && data.isValidPassword) { - let response = await fetch(Constants.LOGIN_ENDPOINT, { + if (form.isValidUser && form.isValidPassword) { + let response = await fetch(LOGIN_ENDPOINT, { method: 'POST', body: JSON.stringify({ - username: data.username, - password: data.password, + username: form.username, + password: form.password, }), }); let statusCode = response.status; if (statusCode === 200) { - Alert.alert('Successfully logged in! 🥳', `Welcome ${data.username}`); + Alert.alert('Successfully logged in! 🥳', `Welcome ${form.username}`); } else if (statusCode === 401) { Alert.alert( 'Login failed 😔', @@ -108,6 +128,9 @@ const Login = ({navigation}: LoginProps) => { "Would you believe me if I told you that I don't know what happened?", ); } + } else { + setForm({...form, attemptedSubmit: false}); + setTimeout(() => setForm({...form, attemptedSubmit: true})); } } catch (error) { Alert.alert( @@ -122,135 +145,157 @@ const Login = ({navigation}: LoginProps) => { }; /* - Handler for the submit button on the Username keyboard - */ - const handleUsernameSubmit = () => { - input_ref.current.focus(); - }; - - const handleRegistration = () => { + * Handles tap on "Get Started" text by resetting fields & navigating to the registration page. + */ + const goToRegistration = () => { navigation.navigate('Registration'); + setForm({...form, attemptedSubmit: false}); }; + /** + * Login screen forgot password button. + */ + const ForgotPassword = () => ( + <TouchableOpacity + accessibilityLabel="Forgot password button" + accessibilityHint="Select this if you forgot your tagg password" + style={styles.forgotPassword} + onPress={() => Alert.alert("tagg! You're it!")}> + <Text style={styles.forgotPasswordText}>Forgot password</Text> + </TouchableOpacity> + ); + + /** + * Login screen login button. + */ + const LoginButton = () => ( + <TouchableOpacity + accessibilityLabel="Let's Start!" + accessibilityHint="Select this after entering your tagg username and password" + onPress={handleLogin} + style={styles.start}> + <Text style={styles.startText}>Let's Start!</Text> + </TouchableOpacity> + ); + + /** + * Login screen registration prompt. + */ + const RegistrationPrompt = () => ( + <View style={styles.newUserContainer}> + <Text + accessible={true} + accessibilityLabel="New to tagg?" + style={styles.newUser}> + New to tagg?{' '} + </Text> + <TouchableOpacity + accessibilityLabel="Get started." + accessibilityHint="Select this if you do not have a tagg account"> + <Text + accessible={true} + accessibilityLabel="Get started" + style={styles.getStarted} + onPress={goToRegistration}> + Get started! + </Text> + </TouchableOpacity> + </View> + ); + return ( - <> + <Background> <StatusBar barStyle="light-content" /> - <TouchableWithoutFeedback - onPress={() => { - Keyboard.dismiss(); - }}> - <View style={styles.container}> - <LinearGradient - colors={['#8F00FF', '#6EE7E7']} - style={styles.linearGradient} - useAngle={true} - angle={154.72} - angleCenter={{x: 0.5, y: 0.5}}> - <Image - source={require('../assets/images/logo.png')} - style={styles.logo} - /> - <LoginInput - type={data.username} - isUsername={true} - onChangeText={(user) => handleUsernameUpdate(user)} - onSubmitEditing={() => handleUsernameSubmit()} - isValid={data.isValidUser} - validationWarning={'Username must be at least 6 characters long.'} - /> - <LoginInput - type={data.password} - isPassword={true} - onChangeText={(user) => handlePasswordUpdate(user)} - onSubmitEditing={() => handleLogin()} - isValid={data.isValidPassword} - validationWarning={'Password must be at least 8 characters long.'} - input_ref={input_ref} - /> - <TouchableOpacity - accessibilityLabel="Forgot password button" - accessibilityHint="Select this if you forgot your tagg password" - style={styles.forgotPassword} - onPress={() => Alert.alert("tagg! You're it!")}> - <Text style={styles.forgotPasswordText}>Forgot password</Text> - </TouchableOpacity> - <TouchableOpacity - accessibilityLabel="Let's start button" - accessibilityHint="Select this after entering your tagg username and password" - style={styles.start} - onPress={() => handleLogin()}> - <Text style={styles.startText}>Let's Start!</Text> - </TouchableOpacity> - <Text - accessible={true} - accessibilityLabel="New to tagg?" - style={styles.newUser}> - New to tagg?{' '} - <Text - accessible={true} - accessibilityLabel="Get started" - accessibilityHint="Select this if you do not have a tagg account" - style={styles.getStarted} - onPress={() => handleRegistration()}> - Get started! - </Text> - </Text> - </LinearGradient> - </View> - </TouchableWithoutFeedback> - </> + <CenteredView> + <Image + source={require('../assets/images/logo.png')} + style={styles.logo} + /> + <TaggInput + accessibilityHint="Enter your tagg username here" + accessibilityLabel="Username text entry box" + placeholder="Username" + autoCompleteType="username" + textContentType="username" + returnKeyType="next" + autoCapitalize="none" + onChangeText={handleUsernameUpdate} + onSubmitEditing={handleUsernameSubmit} + blurOnSubmit={false} + valid={form.isValidUser} + invalidWarning="Username must be at least 6 characters and can only contain letters, numbers, periods, and underscores." + attemptedSubmit={form.attemptedSubmit} + width="100%" + /> + + <TaggInput + accessibilityHint="Enter your tagg password here" + accessibilityLabel="Password text entry box" + placeholder="Password" + autoCompleteType="password" + textContentType="password" + returnKeyType="go" + autoCapitalize="none" + secureTextEntry + onChangeText={handlePasswordUpdate} + onSubmitEditing={handleLogin} + valid={form.isValidPassword} + invalidWarning="Password must be at least 8 characters long." + attemptedSubmit={form.attemptedSubmit} + ref={inputRef} + /> + <ForgotPassword /> + <LoginButton /> + <RegistrationPrompt /> + </CenteredView> + </Background> ); }; const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: 'transparent', - }, - linearGradient: { - flex: 1, - alignItems: 'center', - }, logo: { - top: 165, width: 215, height: 149, + marginBottom: '10%', }, forgotPassword: { - top: 190, - left: -60, + marginTop: 10, + marginBottom: 15, }, forgotPasswordText: { - fontSize: 15, - color: '#FFFFFF', + fontSize: 14, + color: '#fff', textDecorationLine: 'underline', }, start: { - top: 195, width: 144, height: 36, justifyContent: 'center', alignItems: 'center', - backgroundColor: '#FFFFFF', - borderRadius: 20, - marginTop: 15, + backgroundColor: '#fff', + borderRadius: 18, + marginBottom: '10%', + }, + startDisabled: { + backgroundColor: '#ddd', }, startText: { - fontSize: 15, - color: '#78A0EF', + fontSize: 16, + color: '#78a0ef', fontWeight: 'bold', }, - getStarted: { - color: '#FFFFFF', - textDecorationLine: 'underline', + newUserContainer: { + flexDirection: 'row', + color: '#fff', }, newUser: { - top: 240, - color: '#F4DDFF', + fontSize: 14, + color: '#f4ddff', }, - invalidCredentials: { - top: 180, - color: '#F4DDFF', + getStarted: { + fontSize: 14, + color: '#fff', + textDecorationLine: 'underline', }, }); |
