diff options
author | meganhong <34787696+meganhong@users.noreply.github.com> | 2020-07-21 09:51:47 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-07-21 12:51:47 -0400 |
commit | f1300739189283929cb20a22e5281388d1bbeafc (patch) | |
tree | eaacd9999063c3ad45e3dc2e62f741e74aaf533e /src | |
parent | e87d4f2b10cff8cf5f31784cfddb22727a94f373 (diff) |
Tma119: Split Registration Screen (#23)
* split registration screen
* styling
* changed 4:2 to 3:3
* fade wizard with keyboard visibility
* added regex for first and last name
* accidentally saved videos in this folder
* shortened fade duration to 300ms
* add fade to Registration2
* rename RegistrationOne and RegistrationTwo
* moved keyboard logic into RegistrationWizard
* moved loading indicator out of the way
* moving loading to outside of keyboard avoiding view
* moved loading indicator up
Co-authored-by: Megan Hong <meganhong31@g.ucla.edu>
Diffstat (limited to 'src')
-rw-r--r-- | src/components/onboarding/RegistrationWizard.tsx | 59 | ||||
-rw-r--r-- | src/constants/regex.ts | 7 | ||||
-rw-r--r-- | src/routes/Routes.tsx | 17 | ||||
-rw-r--r-- | src/screens/onboarding/Login.tsx | 2 | ||||
-rw-r--r-- | src/screens/onboarding/RegistrationOne.tsx | 268 | ||||
-rw-r--r-- | src/screens/onboarding/RegistrationTwo.tsx (renamed from src/screens/onboarding/Registration.tsx) | 167 | ||||
-rw-r--r-- | src/screens/onboarding/Verification.tsx | 2 | ||||
-rw-r--r-- | src/screens/onboarding/index.ts | 3 |
8 files changed, 376 insertions, 149 deletions
diff --git a/src/components/onboarding/RegistrationWizard.tsx b/src/components/onboarding/RegistrationWizard.tsx index 1157455f..31c3bbdf 100644 --- a/src/components/onboarding/RegistrationWizard.tsx +++ b/src/components/onboarding/RegistrationWizard.tsx @@ -1,5 +1,6 @@ import React from 'react'; -import {View, StyleSheet, ViewProps} from 'react-native'; +import {View, StyleSheet, ViewProps, Keyboard} from 'react-native'; +import * as Animatable from 'react-native-animatable'; interface RegistrationWizardProps extends ViewProps { step: 'one' | 'two' | 'three' | 'four' | 'five'; @@ -8,17 +9,53 @@ interface RegistrationWizardProps extends ViewProps { const RegistrationWizard = (props: RegistrationWizardProps) => { const stepStyle = styles.step; const stepActiveStyle = [styles.step, styles.stepActive]; + + // detects visibility of keyboard to display or hide wizard + const [keyboardVisible, setKeyboardVisible] = React.useState(false); + + Keyboard.addListener('keyboardDidShow', () => { + setKeyboardVisible(true); + }); + Keyboard.addListener('keyboardDidHide', () => { + setKeyboardVisible(false); + }); + return ( <View {...props}> - <View style={styles.container}> - <View style={props.step === 'one' ? stepActiveStyle : stepStyle} /> - <View style={styles.progress} /> - <View style={props.step === 'two' ? stepActiveStyle : stepStyle} /> - <View style={styles.progress} /> - <View style={props.step === 'three' ? stepActiveStyle : stepStyle} /> - <View style={styles.progress} /> - <View style={props.step === 'four' ? stepActiveStyle : stepStyle} /> - </View> + {!keyboardVisible && ( + <Animatable.View animation="fadeIn" duration={300}> + <View style={styles.container}> + <View style={props.step === 'one' ? stepActiveStyle : stepStyle} /> + <View style={styles.progress} /> + <View style={props.step === 'two' ? stepActiveStyle : stepStyle} /> + <View style={styles.progress} /> + <View + style={props.step === 'three' ? stepActiveStyle : stepStyle} + /> + <View style={styles.progress} /> + <View style={props.step === 'four' ? stepActiveStyle : stepStyle} /> + <View style={styles.progress} /> + <View style={props.step === 'five' ? stepActiveStyle : stepStyle} /> + </View> + </Animatable.View> + )} + {keyboardVisible && ( + <Animatable.View animation="fadeOut" duration={300}> + <View style={styles.container}> + <View style={props.step === 'one' ? stepActiveStyle : stepStyle} /> + <View style={styles.progress} /> + <View style={props.step === 'two' ? stepActiveStyle : stepStyle} /> + <View style={styles.progress} /> + <View + style={props.step === 'three' ? stepActiveStyle : stepStyle} + /> + <View style={styles.progress} /> + <View style={props.step === 'four' ? stepActiveStyle : stepStyle} /> + <View style={styles.progress} /> + <View style={props.step === 'five' ? stepActiveStyle : stepStyle} /> + </View> + </Animatable.View> + )} </View> ); }; @@ -40,7 +77,7 @@ const styles = StyleSheet.create({ backgroundColor: '#e1f0ff', }, progress: { - width: '20%', + width: '16%', height: 2, backgroundColor: '#e1f0ff', }, diff --git a/src/constants/regex.ts b/src/constants/regex.ts index 350cb855..40c82691 100644 --- a/src/constants/regex.ts +++ b/src/constants/regex.ts @@ -19,3 +19,10 @@ export const passwordRegex: RegExp = /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[^a-zA * - match only alphanumerics, underscores, and periods */ export const usernameRegex: RegExp = /^[a-zA-Z0-9_.]{6,30}$/; + +/** + * The name regex has the following constraints + * - min. 2 chars, max. 20 chars ({2,20}) + * - match alphanumerics, apostrophes, commas, periods, dashes, and spaces + */ +export const nameRegex: RegExp = /^[A-Za-z'\-,. ]{2,20}$/; diff --git a/src/routes/Routes.tsx b/src/routes/Routes.tsx index 63486448..7379c2c2 100644 --- a/src/routes/Routes.tsx +++ b/src/routes/Routes.tsx @@ -3,14 +3,18 @@ import {createStackNavigator} from '@react-navigation/stack'; import { Login, - Registration, + RegistrationOne, + RegistrationTwo, Verification, Profile, } from '../screens/onboarding'; export type RootStackParamList = { Login: undefined; - Registration: undefined; + RegistrationOne: undefined; + RegistrationTwo: + | {firstName: string; lastName: string; email: string} + | undefined; Verification: {username: string; email: string; userId: string}; Profile: {username: string; userId: string}; }; @@ -28,8 +32,13 @@ const Routes: React.FC<RoutesProps> = ({}) => { options={{headerShown: false}} /> <RootStack.Screen - name="Registration" - component={Registration} + name="RegistrationOne" + component={RegistrationOne} + options={{headerShown: false}} + /> + <RootStack.Screen + name="RegistrationTwo" + component={RegistrationTwo} options={{headerShown: false}} /> <RootStack.Screen diff --git a/src/screens/onboarding/Login.tsx b/src/screens/onboarding/Login.tsx index 8aeb5e45..971595d3 100644 --- a/src/screens/onboarding/Login.tsx +++ b/src/screens/onboarding/Login.tsx @@ -150,7 +150,7 @@ const Login: React.FC<LoginProps> = ({navigation}: LoginProps) => { * Handles tap on "Get Started" text by resetting fields & navigating to the registration page. */ const goToRegistration = () => { - navigation.navigate('Registration'); + navigation.navigate('RegistrationOne'); setForm({...form, attemptedSubmit: false}); }; diff --git a/src/screens/onboarding/RegistrationOne.tsx b/src/screens/onboarding/RegistrationOne.tsx new file mode 100644 index 00000000..3b9ddb3e --- /dev/null +++ b/src/screens/onboarding/RegistrationOne.tsx @@ -0,0 +1,268 @@ +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, + Background, +} from '../../components'; +import {nameRegex, emailRegex} from '../../constants'; + +type RegistrationScreenOneRouteProp = RouteProp< + RootStackParamList, + 'RegistrationOne' +>; +type RegistrationScreenOneNavigationProp = StackNavigationProp< + RootStackParamList, + 'RegistrationOne' +>; +interface RegistrationOneProps { + route: RegistrationScreenOneRouteProp; + navigation: RegistrationScreenOneNavigationProp; +} +/** + * Registration screen 1 for First Name, Last Name, and email + * @param navigation react-navigation navigation object + */ +const RegistrationOne: React.FC<RegistrationOneProps> = ({navigation}) => { + // refs for changing focus + const lnameRef = useRef(); + const emailRef = 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; + default: + return; + } + }; + + // registration form state + const [form, setForm] = useState({ + fname: '', + lname: '', + email: '', + isValidFname: false, + isValidLname: false, + isValidEmail: 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 = nameRegex.test(fname); + 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 = nameRegex.test(lname); + 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 a click on the "next" arrow button by navigating to RegistrationTwo if First Name and Last Name are filled + */ + const goToRegisterTwo = async () => { + if (!form.attemptedSubmit) { + setForm({ + ...form, + attemptedSubmit: true, + }); + } + try { + if (form.isValidFname && form.isValidLname && form.isValidEmail) { + navigation.navigate('RegistrationTwo', { + firstName: form.fname, + lastName: form.lname, + email: form.email, + }); + } 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 = () => ( + <View style={styles.footer}> + <ArrowButton + direction="backward" + onPress={() => navigation.navigate('Login')} + /> + <TouchableOpacity onPress={goToRegisterTwo}> + <ArrowButton + direction="forward" + disabled={ + !(form.isValidFname && form.isValidLname && form.isValidEmail) + } + onPress={() => + navigation.navigate('RegistrationTwo', { + firstName: form.fname, + lastName: form.lname, + email: form.email, + }) + } + /> + </TouchableOpacity> + </View> + ); + + return ( + <Background style={styles.container}> + <StatusBar barStyle="light-content" /> + <RegistrationWizard style={styles.wizard} step="one" /> + <KeyboardAvoidingView + behavior={Platform.OS === 'ios' ? 'padding' : 'height'} + style={styles.container}> + <View> + <Text style={styles.formHeader}>SIGN UP</Text> + </View> + <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="Please enter a valid first name." + 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="Please enter a valid last name." + 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} + blurOnSubmit={false} + ref={emailRef} + valid={form.isValidEmail} + invalidWarning={'Please enter a valid email address.'} + attemptedSubmit={form.attemptedSubmit} + width={280} + onSubmitEditing={goToRegisterTwo} + /> + </KeyboardAvoidingView> + <Footer /> + </Background> + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + alignItems: 'center', + justifyContent: 'center', + }, + wizard: { + ...Platform.select({ + ios: { + top: 50, + }, + android: { + bottom: 40, + }, + }), + }, + formHeader: { + color: '#fff', + fontSize: 30, + fontWeight: '600', + marginBottom: '16%', + }, + footer: { + width: '100%', + flexDirection: 'row', + justifyContent: 'space-around', + ...Platform.select({ + ios: { + bottom: '20%', + }, + android: { + bottom: '10%', + }, + }), + }, +}); + +export default RegistrationOne; diff --git a/src/screens/onboarding/Registration.tsx b/src/screens/onboarding/RegistrationTwo.tsx index 6cad92a5..09e217f6 100644 --- a/src/screens/onboarding/Registration.tsx +++ b/src/screens/onboarding/RegistrationTwo.tsx @@ -23,50 +23,47 @@ import { Background, } from '../../components'; import { - emailRegex, passwordRegex, usernameRegex, REGISTER_ENDPOINT, SEND_OTP_ENDPOINT, } from '../../constants'; -type RegistrationScreenRouteProp = RouteProp< +type RegistrationScreenTwoRouteProp = RouteProp< RootStackParamList, - 'Registration' + 'RegistrationTwo' >; -type RegistrationScreenNavigationProp = StackNavigationProp< +type RegistrationScreenTwoNavigationProp = StackNavigationProp< RootStackParamList, - 'Registration' + 'RegistrationTwo' >; -interface RegistrationProps { - route: RegistrationScreenRouteProp; - navigation: RegistrationScreenNavigationProp; +interface RegistrationTwoProps { + route: RegistrationScreenTwoRouteProp; + navigation: RegistrationScreenTwoNavigationProp; } /** - * Registration screen. + * Registration screen 2 for email, username, password, and terms and conditions * @param navigation react-navigation navigation object */ -const Registration: React.FC<RegistrationProps> = ({navigation}) => { +const RegistrationTwo: React.FC<RegistrationTwoProps> = ({ + route, + navigation, +}) => { // refs for changing focus - const lnameRef = useRef(); - const emailRef = useRef(); const usernameRef = useRef(); const passwordRef = useRef(); const confirmRef = useRef(); + + const registrationName = route.params; + const fname: string = registrationName!.firstName; + const lname: string = registrationName!.lastName; + const email: string = registrationName!.email; /** * 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(); @@ -86,14 +83,10 @@ const Registration: React.FC<RegistrationProps> = ({navigation}) => { // registration form state const [form, setForm] = useState({ - fname: '', - lname: '', email: '', username: '', password: '', confirm: '', - isValidFname: false, - isValidLname: false, isValidEmail: false, isValidUsername: false, isValidPassword: false, @@ -103,40 +96,6 @@ const Registration: React.FC<RegistrationProps> = ({navigation}) => { }); /* - * 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) => { @@ -195,20 +154,14 @@ const Registration: React.FC<RegistrationProps> = ({navigation}) => { }); } try { - if ( - form.isValidFname && - form.isValidLname && - form.isValidUsername && - form.isValidPassword && - form.passwordsMatch - ) { + if (form.isValidUsername && form.isValidPassword && form.passwordsMatch) { if (form.tcAccepted) { let registerResponse = await fetch(REGISTER_ENDPOINT, { method: 'POST', body: JSON.stringify({ - first_name: form.fname, - last_name: form.lname, - email: form.email, + first_name: fname, + last_name: lname, + email: email, username: form.username, password: form.password, }), @@ -221,7 +174,7 @@ const Registration: React.FC<RegistrationProps> = ({navigation}) => { method: 'POST', body: JSON.stringify({ username: form.username, - email: form.email, + email: email, }), }), ); @@ -230,7 +183,7 @@ const Registration: React.FC<RegistrationProps> = ({navigation}) => { const userId: string = data.UserID; navigation.navigate('Verification', { username: form.username, - email: form.email, + email: email, userId: userId, }); } @@ -268,16 +221,13 @@ const Registration: React.FC<RegistrationProps> = ({navigation}) => { <View style={styles.footer}> <ArrowButton direction="backward" - onPress={() => navigation.navigate('Login')} + onPress={() => navigation.navigate('RegistrationOne')} /> <TouchableOpacity onPress={handleRegister}> <ArrowButton direction="forward" disabled={ !( - form.isValidFname && - form.isValidLname && - form.isValidEmail && form.isValidUsername && form.isValidPassword && form.passwordsMatch && @@ -296,7 +246,7 @@ const Registration: React.FC<RegistrationProps> = ({navigation}) => { const LoadingIndicator = () => { const {promiseInProgress} = usePromiseTracker(); return promiseInProgress ? ( - <ActivityIndicator size="large" color="#fff" /> + <ActivityIndicator style={styles.load} size="large" color="#fff" /> ) : ( <></> ); @@ -305,63 +255,14 @@ const Registration: React.FC<RegistrationProps> = ({navigation}) => { return ( <Background style={styles.container}> <StatusBar barStyle="light-content" /> + <RegistrationWizard style={styles.wizard} step="two" /> <KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : 'height'} style={styles.container}> - <RegistrationWizard style={styles.wizard} step="one" /> <View> - <Text style={styles.formHeader}>Sign up.</Text> + <Text style={styles.formHeader}>SIGN UP</Text> </View> <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" @@ -415,12 +316,12 @@ const Registration: React.FC<RegistrationProps> = ({navigation}) => { attemptedSubmit={form.attemptedSubmit} width={280} /> + <LoadingIndicator /> <TermsConditions style={styles.tc} accepted={form.tcAccepted} onChange={handleTcUpdate} /> - <LoadingIndicator /> </KeyboardAvoidingView> <Footer /> </Background> @@ -436,10 +337,10 @@ const styles = StyleSheet.create({ wizard: { ...Platform.select({ ios: { - bottom: '10%', + top: 50, }, android: { - bottom: '5%', + bottom: 40, }, }), }, @@ -447,10 +348,14 @@ const styles = StyleSheet.create({ color: '#fff', fontSize: 30, fontWeight: '600', - marginBottom: '5%', + marginBottom: '16%', }, tc: { marginVertical: '5%', + top: '8%', + }, + load: { + top: '5%', }, footer: { width: '100%', @@ -467,4 +372,4 @@ const styles = StyleSheet.create({ }, }); -export default Registration; +export default RegistrationTwo; diff --git a/src/screens/onboarding/Verification.tsx b/src/screens/onboarding/Verification.tsx index baa5c6b3..905de276 100644 --- a/src/screens/onboarding/Verification.tsx +++ b/src/screens/onboarding/Verification.tsx @@ -128,7 +128,7 @@ const Verification: React.FC<VerificationProps> = ({route, navigation}) => { return ( <Background centered style={styles.container}> - <RegistrationWizard style={styles.wizard} step="two" /> + <RegistrationWizard style={styles.wizard} step="three" /> <KeyboardAvoidingView behavior="padding" style={styles.form}> <Text style={styles.formHeader}>Enter 6 digit code</Text> <Text style={styles.description}> diff --git a/src/screens/onboarding/index.ts b/src/screens/onboarding/index.ts index 919ad7fd..e89e1d5f 100644 --- a/src/screens/onboarding/index.ts +++ b/src/screens/onboarding/index.ts @@ -1,4 +1,5 @@ export {default as Login} from './Login'; -export {default as Registration} from './Registration'; +export {default as RegistrationOne} from './RegistrationOne'; +export {default as RegistrationTwo} from './RegistrationTwo'; export {default as Verification} from './Verification'; export {default as Profile} from './Profile'; |