diff options
Diffstat (limited to 'src/screens')
| -rw-r--r-- | src/screens/Login.tsx | 325 | ||||
| -rw-r--r-- | src/screens/Registration.tsx | 448 | ||||
| -rw-r--r-- | src/screens/Verification.tsx | 28 | ||||
| -rw-r--r-- | src/screens/index.ts | 1 |
4 files changed, 650 insertions, 152 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', }, }); diff --git a/src/screens/Registration.tsx b/src/screens/Registration.tsx index 57b0eb18..52508a76 100644 --- a/src/screens/Registration.tsx +++ b/src/screens/Registration.tsx @@ -1,24 +1,448 @@ -import React from 'react'; -import {View, Text, StyleSheet} from 'react-native'; +import React, {useState, useRef} from 'react'; +import {RouteProp} from '@react-navigation/native'; +import {StackNavigationProp} from '@react-navigation/stack'; +import { + View, + Text, + StyleSheet, + StatusBar, + Alert, + Platform, + TouchableOpacity, +} from 'react-native'; -interface RegistrationProps {} +import {RootStackParamList} from '../routes'; +import { + ArrowButton, + RegistrationWizard, + TaggInput, + TermsConditions, + Background, + CenteredView, +} from '../components'; +import { + emailRegex, + passwordRegex, + usernameRegex, + REGISTER_ENDPOINT, +} from '../constants'; + +type RegistrationScreenRouteProp = RouteProp< + RootStackParamList, + 'Registration' +>; +type RegistrationScreenNavigationProp = StackNavigationProp< + RootStackParamList, + 'Registration' +>; +interface RegistrationProps { + route: RegistrationScreenRouteProp; + navigation: RegistrationScreenNavigationProp; +} +/** + * Registration screen. + * @param navigation react-navigation navigation object + */ +const Registration: React.FC<RegistrationProps> = ({navigation}) => { + // refs for changing focus + const lnameRef = useRef(); + const emailRef = useRef(); + const usernameRef = useRef(); + const passwordRef = useRef(); + const confirmRef = useRef(); + /** + * Handles focus change to the next input field. + * @param field key for field to move focus onto + */ + const handleFocusChange = (field: string): void => { + switch (field) { + case 'lname': + const lnameField: any = lnameRef.current; + lnameField.focus(); + break; + case 'email': + const emailField: any = emailRef.current; + emailField.focus(); + break; + case 'username': + const usernameField: any = usernameRef.current; + usernameField.focus(); + break; + case 'password': + const passwordField: any = passwordRef.current; + passwordField.focus(); + break; + case 'confirm': + const confirmField: any = confirmRef.current; + confirmField.focus(); + break; + default: + return; + } + }; + + // registration form state + const [form, setForm] = useState({ + fname: '', + lname: '', + email: '', + username: '', + password: '', + confirm: '', + isValidFname: false, + isValidLname: false, + isValidEmail: false, + isValidUsername: false, + isValidPassword: false, + passwordsMatch: false, + tcAccepted: false, + attemptedSubmit: false, + }); + + /* + * Handles changes to the first name field value and verifies the input by updating state and running a validation function. + */ + const handleFnameUpdate = (fname: string) => { + let isValidFname: boolean = fname.length > 0; + setForm({ + ...form, + fname, + isValidFname, + }); + }; + /* + * Handles changes to the last name field value and verifies the input by updating state and running a validation function. + */ + const handleLnameUpdate = (lname: string) => { + let isValidLname: boolean = lname.length > 0; + setForm({ + ...form, + lname, + isValidLname, + }); + }; + /* + * Handles changes to the email field value and verifies the input by updating state and running a validation function. + */ + const handleEmailUpdate = (email: string) => { + let isValidEmail: boolean = emailRegex.test(email); + setForm({ + ...form, + email, + isValidEmail, + }); + }; + + /* + * Handles changes to the username field value and verifies the input by updating state and running a validation function. + */ + const handleUsernameUpdate = (username: string) => { + let isValidUsername: boolean = usernameRegex.test(username); + setForm({ + ...form, + username, + isValidUsername, + }); + }; + /* + * Handles changes to the password field value and verifies the input by updating state and running a validation function. + */ + const handlePasswordUpdate = (password: string) => { + let isValidPassword: boolean = passwordRegex.test(password); + setForm({ + ...form, + password, + isValidPassword, + }); + }; + + /* + * Handles changes to the confirm password field value and verifies the input by updating state and running a validation function. + */ + const handleConfirmUpdate = (confirm: string) => { + let passwordsMatch: boolean = form.password === confirm; + setForm({ + ...form, + confirm, + passwordsMatch, + }); + }; + + /** + * Handles changes to the terms and conditions accepted boolean. + * @param tcAccepted the boolean to set the terms and conditions value to + */ + const handleTcUpdate = (tcAccepted: boolean) => { + setForm({ + ...form, + tcAccepted, + }); + }; + + /** + * Handles a click on the "next" arrow button by sending an API request to the backend and displaying the appropriate response. + */ + const handleRegister = async () => { + if (!form.attemptedSubmit) { + setForm({ + ...form, + attemptedSubmit: true, + }); + } + try { + if ( + form.isValidFname && + form.isValidLname && + form.isValidUsername && + form.isValidPassword && + form.passwordsMatch + ) { + if (form.tcAccepted) { + let response = await fetch(REGISTER_ENDPOINT, { + method: 'POST', + body: JSON.stringify({ + first_name: form.fname, + last_name: form.lname, + email: form.email, + username: form.username, + password: form.password, + }), + }); + let statusCode = response.status; + let data = await response.json(); + if (statusCode === 201) { + navigation.navigate('Verification'); + Alert.alert( + "You've successfully registrated!🥳", + `Welcome, ${form.username}`, + ); + } else if (statusCode === 409) { + Alert.alert('Registration failed 😔', `${data}`); + } else { + Alert.alert( + 'Something went wrong! ðŸ˜', + "Would you believe me if I told you that I don't know what happened?", + ); + } + } else { + Alert.alert( + 'Terms and conditions', + 'You must first agree to the terms and conditions.', + ); + } + } else { + setForm({...form, attemptedSubmit: false}); + setTimeout(() => setForm({...form, attemptedSubmit: true})); + } + } catch (error) { + Alert.alert( + 'Looks like our servers are down. 😓', + "Try again in a couple minutes. We're sorry for the inconvenience.", + ); + return { + name: 'Registration error', + description: error, + }; + } + }; -const Registration: React.FC<RegistrationProps> = ({}) => { return ( - <View style={styles.view}> - <Text style={styles.text}>Registration sequence begins here!</Text> - </View> + <Background style={styles.container}> + <StatusBar barStyle="light-content" /> + <CenteredView> + <RegistrationWizard style={styles.wizard} step="one" /> + <View style={styles.form}> + <Text style={styles.formHeader}>SIGN UP</Text> + <TaggInput + accessibilityHint="Enter your first name." + accessibilityLabel="First name input field." + placeholder="First Name" + autoCompleteType="name" + textContentType="name" + returnKeyType="next" + onChangeText={handleFnameUpdate} + onSubmitEditing={() => handleFocusChange('lname')} + blurOnSubmit={false} + valid={form.isValidFname} + invalidWarning="First name cannot be empty." + attemptedSubmit={form.attemptedSubmit} + width={280} + /> + <TaggInput + accessibilityHint="Enter your last name." + accessibilityLabel="Last name input field." + placeholder="Last Name" + autoCompleteType="name" + textContentType="name" + returnKeyType="next" + onChangeText={handleLnameUpdate} + onSubmitEditing={() => handleFocusChange('email')} + blurOnSubmit={false} + ref={lnameRef} + valid={form.isValidLname} + invalidWarning="Last name cannot be empty." + attemptedSubmit={form.attemptedSubmit} + width={280} + /> + <TaggInput + accessibilityHint="Enter your email." + accessibilityLabel="Email input field." + placeholder="Email" + autoCompleteType="email" + textContentType="emailAddress" + autoCapitalize="none" + returnKeyType="next" + keyboardType="email-address" + onChangeText={handleEmailUpdate} + onSubmitEditing={() => handleFocusChange('username')} + blurOnSubmit={false} + ref={emailRef} + valid={form.isValidEmail} + invalidWarning={'Please enter a valid email address.'} + attemptedSubmit={form.attemptedSubmit} + width={280} + /> + <TaggInput + accessibilityHint="Enter a username." + accessibilityLabel="Username input field." + placeholder="Username" + autoCompleteType="username" + textContentType="username" + autoCapitalize="none" + returnKeyType="next" + onChangeText={handleUsernameUpdate} + onSubmitEditing={() => handleFocusChange('password')} + blurOnSubmit={false} + ref={usernameRef} + valid={form.isValidUsername} + invalidWarning={ + 'Username must be 6 characters long and contain only alphanumeric characters.' + } + attemptedSubmit={form.attemptedSubmit} + width={280} + /> + <TaggInput + accessibilityHint="Enter a password." + accessibilityLabel="Password input field." + placeholder="Password" + autoCompleteType="password" + textContentType="newPassword" + returnKeyType="next" + onChangeText={handlePasswordUpdate} + onSubmitEditing={() => handleFocusChange('confirm')} + blurOnSubmit={false} + secureTextEntry + ref={passwordRef} + valid={form.isValidPassword} + invalidWarning={ + 'Password must be 8 characters long & contain at least one lowercase, one uppercase, a number, and a special character.' + } + attemptedSubmit={form.attemptedSubmit} + width={280} + /> + <TaggInput + accessibilityHint={'Re-enter your password.'} + accessibilityLabel={'Password confirmation input field.'} + placeholder={'Confirm Password'} + autoCompleteType="password" + textContentType="password" + returnKeyType={form.tcAccepted ? 'go' : 'default'} + onChangeText={handleConfirmUpdate} + onSubmitEditing={handleRegister} + secureTextEntry + ref={confirmRef} + valid={form.passwordsMatch} + invalidWarning={'Passwords must match.'} + attemptedSubmit={form.attemptedSubmit} + width={280} + /> + <TermsConditions + style={styles.tc} + accepted={form.tcAccepted} + onChange={handleTcUpdate} + /> + </View> + <View style={styles.footer}> + <ArrowButton + direction="backward" + onPress={() => navigation.navigate('Login')} + /> + <TouchableOpacity onPress={handleRegister}> + <ArrowButton + direction="forward" + disabled={ + !( + form.isValidFname && + form.isValidLname && + form.isValidEmail && + form.isValidUsername && + form.isValidPassword && + form.passwordsMatch && + form.tcAccepted + ) + } + onPress={handleRegister} + /> + </TouchableOpacity> + </View> + </CenteredView> + </Background> ); }; const styles = StyleSheet.create({ - view: { + container: { flex: 1, - alignSelf: 'center', - justifyContent: 'center', }, - text: { - fontSize: 18, + wizard: { + ...Platform.select({ + ios: { + marginBottom: '18%', + }, + android: { + marginTop: '20%', + marginBottom: '10%', + }, + }), + }, + form: { + alignItems: 'center', + }, + formHeader: { + color: '#fff', + fontSize: 30, + fontWeight: '600', + ...Platform.select({ + ios: { + marginBottom: '6%', + }, + android: { + marginBottom: '2%', + }, + }), + }, + tc: { + ...Platform.select({ + ios: { + marginTop: '5%', + marginBottom: '20%', + }, + android: { + marginTop: '7%', + marginBottom: '12%', + }, + }), + }, + footer: { + width: '100%', + flexDirection: 'row', + justifyContent: 'space-around', + ...Platform.select({ + android: { + marginBottom: '22%', + }, + }), }, }); + export default Registration; diff --git a/src/screens/Verification.tsx b/src/screens/Verification.tsx new file mode 100644 index 00000000..92032594 --- /dev/null +++ b/src/screens/Verification.tsx @@ -0,0 +1,28 @@ +import React from 'react'; + +import {RootStackParamList} from '../routes'; +import {RouteProp} from '@react-navigation/native'; +import {StackNavigationProp} from '@react-navigation/stack'; +import {Background, CenteredView} from '../components'; +import {Text} from 'react-native-animatable'; +type LoginScreenRouteProp = RouteProp<RootStackParamList, 'Login'>; +type LoginScreenNavigationProp = StackNavigationProp< + RootStackParamList, + 'Login' +>; +interface VerificationProps { + route: LoginScreenRouteProp; + navigation: LoginScreenNavigationProp; +} + +const Verification: React.FC<VerificationProps> = ({}) => { + return ( + <Background> + <CenteredView> + <Text>Verification!</Text> + </CenteredView> + </Background> + ); +}; + +export default Verification; diff --git a/src/screens/index.ts b/src/screens/index.ts index 60b26b4c..8c8e7b26 100644 --- a/src/screens/index.ts +++ b/src/screens/index.ts @@ -1,2 +1,3 @@ export {default as Login} from './Login'; export {default as Registration} from './Registration'; +export {default as Verification} from './Verification'; |
