From 5dee1e585353b6d7407f521dfa9186dbf10e8226 Mon Sep 17 00:00:00 2001 From: meganhong <34787696+meganhong@users.noreply.github.com> Date: Mon, 13 Jul 2020 15:08:06 -0700 Subject: TMA123: Add Profile Pictures UI (#17) * rebasing * rebasing * remove debug code * fixed margins and added navigation from login * moved plist file into gitignore * moved index.ts to onboarding directory * install react native image crop picker * added permissions into Info.plist * rebasing * minor changes for Justins PR * change debug code back Co-authored-by: meganhong --- src/screens/onboarding/Camera.tsx | 28 ++ src/screens/onboarding/Login.tsx | 317 +++++++++++++++++++++++ src/screens/onboarding/Profile.tsx | 121 +++++++++ src/screens/onboarding/Registration.tsx | 441 ++++++++++++++++++++++++++++++++ src/screens/onboarding/Verification.tsx | 143 +++++++++++ src/screens/onboarding/index.ts | 5 + 6 files changed, 1055 insertions(+) create mode 100644 src/screens/onboarding/Camera.tsx create mode 100644 src/screens/onboarding/Login.tsx create mode 100644 src/screens/onboarding/Profile.tsx create mode 100644 src/screens/onboarding/Registration.tsx create mode 100644 src/screens/onboarding/Verification.tsx create mode 100644 src/screens/onboarding/index.ts (limited to 'src/screens/onboarding') diff --git a/src/screens/onboarding/Camera.tsx b/src/screens/onboarding/Camera.tsx new file mode 100644 index 00000000..23776e2d --- /dev/null +++ b/src/screens/onboarding/Camera.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import {RouteProp} from '@react-navigation/native'; +import {StackNavigationProp} from '@react-navigation/stack'; +import {RootStackParamList} from '../../routes'; +import {Background, CenteredView} from '../../components'; +import {Text} from 'react-native-animatable'; + +type CameraScreenRouteProp = RouteProp; +type CameraScreenNavigationProp = StackNavigationProp< + RootStackParamList, + 'Camera' +>; +interface CameraProps { + route: CameraScreenRouteProp; + navigation: CameraScreenNavigationProp; +} + +const Camera: React.FC = ({}) => { + return ( + + + Camera! + + + ); +}; + +export default Camera; diff --git a/src/screens/onboarding/Login.tsx b/src/screens/onboarding/Login.tsx new file mode 100644 index 00000000..c06f6f27 --- /dev/null +++ b/src/screens/onboarding/Login.tsx @@ -0,0 +1,317 @@ +import React, {useRef} from 'react'; +import {RouteProp} from '@react-navigation/native'; +import {StackNavigationProp} from '@react-navigation/stack'; +import { + View, + Text, + Alert, + StatusBar, + Image, + TouchableOpacity, + StyleSheet, + KeyboardAvoidingView, + Platform, +} from 'react-native'; + +import {RootStackParamList} from '../../routes'; +import {Background, TaggInput, SubmitButton} from '../../components'; +import {usernameRegex, LOGIN_ENDPOINT} from '../../constants'; + +type VerificationScreenRouteProp = RouteProp; +type VerificationScreenNavigationProp = StackNavigationProp< + RootStackParamList, + 'Login' +>; +interface LoginProps { + route: VerificationScreenRouteProp; + navigation: VerificationScreenNavigationProp; +} +/** + * Login screen. + * @param navigation react-navigation navigation object. + */ +const Login: React.FC = ({navigation}: LoginProps) => { + // ref for focusing on input fields + const inputRef = useRef(); + // login form state + const [form, setForm] = React.useState({ + username: '', + password: '', + isValidUser: false, + isValidPassword: false, + attemptedSubmit: false, + }); + + /** + * 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.length >= 6; + let validChars: boolean = usernameRegex.test(val); + + if (validLength && validChars) { + setForm({ + ...form, + username: val, + isValidUser: true, + }); + } else { + setForm({ + ...form, + username: val, + isValidUser: false, + }); + } + }; + + /** + * 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) { + setForm({ + ...form, + password: val, + isValidPassword: true, + }); + } else { + setForm({ + ...form, + password: val, + isValidPassword: false, + }); + } + }; + + /* + * 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 (form.isValidUser && form.isValidPassword) { + let response = await fetch(LOGIN_ENDPOINT, { + method: 'POST', + body: JSON.stringify({ + username: form.username, + password: form.password, + }), + }); + + let statusCode = response.status; + if (statusCode === 200) { + Alert.alert('Successfully logged in! πŸ₯³', `Welcome ${form.username}`); + } else if (statusCode === 401) { + Alert.alert( + 'Login failed πŸ˜”', + 'Try re-entering your login information.', + ); + } else { + Alert.alert( + 'Something went wrong! 😭', + "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( + 'Looks like our servers are down. πŸ˜“', + "Try again in a couple minutes. We're sorry for the inconvenience.", + ); + return { + name: 'Login error', + description: error, + }; + } + }; + + /* + * 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 = () => ( + Alert.alert("tagg! You're it!")}> + Forgot password + + ); + + /** + * Login screen login button. + */ + const LoginButton = () => ( + + ); + + /** + * Login screen registration prompt. + */ + const RegistrationPrompt = () => ( + + + New to tagg?{' '} + + + + Get started! + + + + ); + + return ( + + + + + + + + + + + + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + }, + keyboardAvoidingView: { + alignItems: 'center', + }, + logo: { + width: 215, + height: 149, + marginBottom: '10%', + }, + forgotPassword: { + marginTop: 10, + marginBottom: 15, + }, + forgotPasswordText: { + fontSize: 14, + color: '#fff', + textDecorationLine: 'underline', + }, + start: { + width: 144, + height: 36, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: '#fff', + borderRadius: 18, + marginBottom: '15%', + }, + startDisabled: { + backgroundColor: '#ddd', + }, + startText: { + fontSize: 16, + color: '#78a0ef', + fontWeight: 'bold', + }, + newUserContainer: { + flexDirection: 'row', + color: '#fff', + }, + newUser: { + fontSize: 14, + color: '#f4ddff', + }, + getStarted: { + fontSize: 14, + color: '#fff', + textDecorationLine: 'underline', + }, + button: { + marginVertical: '10%' + } +}); + +export default Login; diff --git a/src/screens/onboarding/Profile.tsx b/src/screens/onboarding/Profile.tsx new file mode 100644 index 00000000..5553fe7f --- /dev/null +++ b/src/screens/onboarding/Profile.tsx @@ -0,0 +1,121 @@ +import React from 'react'; +import {RouteProp} from '@react-navigation/native'; +import {StackNavigationProp} from '@react-navigation/stack'; +import {View, Text, StatusBar, StyleSheet} from 'react-native'; +import {RootStackParamList} from '../../routes'; +import {Background, CenteredView} from '../../components'; + +type ProfileScreenRouteProp = RouteProp; +type ProfileScreenNavigationProp = StackNavigationProp< + RootStackParamList, + 'Profile' +>; +interface ProfileProps { + route: ProfileScreenRouteProp; + navigation: ProfileScreenNavigationProp; +} +/** + * Create Profile screen for onboarding. + * @param navigation react-navigation navigation object. + */ + +const Profile: React.FC = ({navigation}) => { + /** + * Profile screen "Add Large Profile Pic Here" button + */ + const LargeProfilePic = () => ( + + + + ADD LARGE PROFILE PIC HERE + + + + ); + + /** + * Profile screen "Add Smaller Profile Pic Here" button + */ + const SmallProfilePic = () => ( + + + + ADD SMALLER PIC + + + + ); + + /* + * Handles tap on add profile picture buttons by navigating to camera access + */ + const goToCamera = () => { + navigation.navigate('Camera'); + }; + + return ( + + + + + + + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + flexDirection: 'row', + justifyContent: 'center', + alignItems: 'center' + }, + largeButton: { + padding: 5, + height: 180, + width: 180, + borderRadius: 400, + backgroundColor: '#fff', + justifyContent: 'center', + marginTop: '100%', + marginRight: '12%', + zIndex: 2, + }, + addLargeProfilePicText: { + fontWeight: 'bold', + padding: 22, + fontSize: 12, + color: '#863FF9', + textAlign: 'center', + }, + smallButton: { + position: 'relative', + padding: 5, + height: 110, + width: 110, + borderRadius: 400, + backgroundColor: '#E1F0FF', + justifyContent: 'center', + zIndex: 1, + marginTop: '128%', + marginLeft: '30%' + }, + addSmallProfilePicText: { + fontWeight: 'bold', + padding: 10, + fontSize: 12, + color: '#806DF4', + textAlign: 'center', + }, +}); + +export default Profile; diff --git a/src/screens/onboarding/Registration.tsx b/src/screens/onboarding/Registration.tsx new file mode 100644 index 00000000..29a2b3f3 --- /dev/null +++ b/src/screens/onboarding/Registration.tsx @@ -0,0 +1,441 @@ +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, + KeyboardAvoidingView, +} from 'react-native'; + +import {RootStackParamList} from '../../routes'; +import { + ArrowButton, + RegistrationWizard, + TaggInput, + TermsConditions, + Background, +} 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 = ({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); + let passwordsMatch: boolean = form.password === form.confirm; + setForm({ + ...form, + password, + isValidPassword, + passwordsMatch, + }); + }; + + /* + * 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 registered!πŸ₯³", + `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 Footer = () => ( + + navigation.navigate('Login')} + /> + + + + + ); + + return ( + + + + + + Sign up. + + handleFocusChange('lname')} + blurOnSubmit={false} + valid={form.isValidFname} + invalidWarning="First name cannot be empty." + attemptedSubmit={form.attemptedSubmit} + width={280} + /> + handleFocusChange('email')} + blurOnSubmit={false} + ref={lnameRef} + valid={form.isValidLname} + invalidWarning="Last name cannot be empty." + attemptedSubmit={form.attemptedSubmit} + width={280} + /> + handleFocusChange('username')} + blurOnSubmit={false} + ref={emailRef} + valid={form.isValidEmail} + invalidWarning={'Please enter a valid email address.'} + attemptedSubmit={form.attemptedSubmit} + width={280} + /> + handleFocusChange('password')} + blurOnSubmit={false} + ref={usernameRef} + valid={form.isValidUsername} + invalidWarning={ + 'Username must beΒ at least 6 characters and contain only alphanumerics.' + } + attemptedSubmit={form.attemptedSubmit} + width={280} + /> + handleFocusChange('confirm')} + blurOnSubmit={false} + secureTextEntry + ref={passwordRef} + valid={form.isValidPassword} + invalidWarning={ + 'Password must be at least 8 characters & contain at least one of a-z, A-Z, 0-9, and a special character.' + } + attemptedSubmit={form.attemptedSubmit} + width={280} + /> + + + +