diff options
Diffstat (limited to 'src/screens/onboarding/BasicInfoOnboarding.tsx')
-rw-r--r-- | src/screens/onboarding/BasicInfoOnboarding.tsx | 588 |
1 files changed, 588 insertions, 0 deletions
diff --git a/src/screens/onboarding/BasicInfoOnboarding.tsx b/src/screens/onboarding/BasicInfoOnboarding.tsx new file mode 100644 index 00000000..3fa33f63 --- /dev/null +++ b/src/screens/onboarding/BasicInfoOnboarding.tsx @@ -0,0 +1,588 @@ +import AsyncStorage from '@react-native-community/async-storage'; +import {useNavigation} from '@react-navigation/core'; +import {RouteProp} from '@react-navigation/native'; +import {StackNavigationProp} from '@react-navigation/stack'; +import React, {useEffect, useState} from 'react'; +import { + Alert, + KeyboardAvoidingView, + Platform, + StatusBar, + StyleSheet, + Text, + TouchableOpacity, +} from 'react-native'; +import {normalize} from 'react-native-elements'; +import Animated, {Easing, useValue} from 'react-native-reanimated'; +import { + ArrowButton, + Background, + LoadingIndicator, + TaggInput, + TaggSquareButton, + TermsConditions, +} from '../../components'; +import { + emailRegex, + nameRegex, + passwordRegex, + phoneRegex, + usernameRegex, +} from '../../constants'; +import { + ERROR_NEXT_PAGE, + ERROR_PHONE_IN_USE, + ERROR_REGISTRATION, + ERROR_SOMETHING_WENT_WRONG_REFRESH, + ERROR_TWILIO_SERVER_ERROR, + ERROR_T_AND_C_NOT_ACCEPTED, +} from '../../constants/strings'; +import {OnboardingStackParams} from '../../routes'; +import {sendOtpStatusCode, sendRegister} from '../../services'; +import {BackgroundGradientType} from '../../types'; +import {HeaderHeight, SCREEN_HEIGHT, SCREEN_WIDTH} from '../../utils'; + +type BasicInfoOnboardingRouteProp = RouteProp< + OnboardingStackParams, + 'BasicInfoOnboarding' +>; +type BasicInfoOnboardingNavigationProp = StackNavigationProp< + OnboardingStackParams, + 'BasicInfoOnboarding' +>; +interface BasicInfoOnboardingProps { + route: BasicInfoOnboardingRouteProp; + navigation: BasicInfoOnboardingNavigationProp; +} + +const BasicInfoOnboarding: React.FC<BasicInfoOnboardingProps> = ({route}) => { + const {isPhoneVerified} = route.params; + const navigation = useNavigation(); + const [attemptedSubmit, setAttemptedSubmit] = useState(false); + const [valid, setValid] = useState(false); + const [currentStep, setCurrentStep] = useState(0); + const [tcAccepted, setTCAccepted] = useState(false); + const [passVisibility, setPassVisibility] = useState(false); + const [autoCapitalize, setAutoCap] = useState< + 'none' | 'sentences' | 'words' | 'characters' | undefined + >('none'); + const [fadeValue, setFadeValue] = useState<Animated.Value<number>>( + new Animated.Value(0), + ); + const fadeButtonValue = useValue<number>(0); + const [form, setForm] = useState({ + fname: '', + lname: '', + username: '', + phone: '', + email: '', + password: '', + }); + + const fadeFormIn = () => { + setFadeValue(new Animated.Value(0)); + }; + + const fadeButtonTo = (target: number) => { + Animated.timing(fadeButtonValue, { + toValue: target, + duration: 100, + easing: Easing.linear, + }).start(); + }; + + useEffect(() => { + const fade = async () => { + Animated.timing(fadeValue, { + toValue: 1, + duration: 1000, + easing: Easing.linear, + }).start(); + }; + fade(); + }, [fadeValue]); + + useEffect(() => { + if (valid) { + fadeButtonTo(1); + } else { + fadeButtonTo(0); + } + }, [valid]); + + const goToPhoneVerification = async () => { + if (!attemptedSubmit) { + setAttemptedSubmit(true); + } + try { + if (valid) { + const {phone} = form; + const code = await sendOtpStatusCode(phone); + if (code) { + switch (code) { + case 200: + const {fname, lname} = form; + navigation.navigate('PhoneVerification', { + firstName: fname, + lastName: lname, + phone, + }); + break; + case 409: + Alert.alert(ERROR_PHONE_IN_USE); + break; + default: + Alert.alert(ERROR_TWILIO_SERVER_ERROR); + } + } else { + setAttemptedSubmit(false); + setTimeout(() => { + setAttemptedSubmit(true); + }); + } + } + } catch (error) { + Alert.alert(ERROR_NEXT_PAGE); + return { + name: 'Navigation error', + description: error, + }; + } + }; + // 0 = first name, 1 = last name, 2 = username, 3 = phone # + const handleNameUpdate = (name: string, nameType: number) => { + name = name.trim(); + let isValidName: boolean = nameRegex.test(name); + switch (nameType) { + case 0: + setForm({ + ...form, + fname: name, + }); + setAutoCap('words'); + setValid(isValidName); + break; + case 1: + setForm({ + ...form, + lname: name, + }); + setAutoCap('words'); + setValid(isValidName); + break; + case 2: + setForm({ + ...form, + username: name, + }); + setValid(usernameRegex.test(name)); + setAutoCap('none'); + break; + } + }; + const handlePhoneUpdate = (phone: string) => { + phone = phone.trim(); + setForm({ + ...form, + phone, + }); + setAutoCap('none'); + setValid(phoneRegex.test(phone)); + }; + const handleEmailUpdate = (email: string) => { + email = email.trim(); + setForm({ + ...form, + email, + }); + setAutoCap('none'); + setValid(emailRegex.test(email)); + }; + const handlePasswordUpdate = (password: string) => { + setForm({ + ...form, + password, + }); + setAutoCap('none'); + setValid(passwordRegex.test(password)); + }; + const formSteps: { + placeholder: string; + onChangeText: (text: string) => void; + }[] = [ + { + placeholder: 'First Name', + onChangeText: (text) => handleNameUpdate(text, 0), + }, + { + placeholder: 'Last Name', + onChangeText: (text) => handleNameUpdate(text, 1), + }, + { + placeholder: 'Phone', + onChangeText: handlePhoneUpdate, + }, + { + placeholder: 'School Email', + onChangeText: handleEmailUpdate, + }, + { + placeholder: 'Username', + onChangeText: (text) => handleNameUpdate(text, 2), + }, + { + placeholder: 'Password', + onChangeText: handlePasswordUpdate, + }, + ]; + const resetForm = (formStep: String) => { + setValid(false); + switch (formStep) { + case 'First Name': + setForm({ + ...form, + fname: '', + }); + break; + case 'Last Name': + setForm({ + ...form, + lname: '', + }); + break; + case 'Email': + setForm({ + ...form, + email: '', + }); + break; + case 'Password': + setForm({ + ...form, + password: '', + }); + break; + case 'School Email': + setForm({ + ...form, + email: '', + }); + break; + case 'Username': + setForm({ + ...form, + username: '', + }); + break; + } + }; + const step = formSteps[currentStep]; + const advance = () => { + setAttemptedSubmit(true); + if (valid) { + setCurrentStep(currentStep + 1); + setAttemptedSubmit(false); + setValid(false); + fadeFormIn(); + } + }; + const advanceRegistration = async () => { + setAttemptedSubmit(true); + if (!valid) { + return; + } + if (!tcAccepted) { + Alert.alert('Terms and conditions', ERROR_T_AND_C_NOT_ACCEPTED); + return; + } + const {fname, lname, phone, email, username, password} = form; + const response = await sendRegister( + fname, + lname, + phone, + email, + username, + password, + ); + if (response) { + const data = await response.json(); + const {token, UserID} = data; + switch (response.status) { + case 201: + await AsyncStorage.setItem('token', token); + navigation.navigate('ProfileInfoOnboarding', { + userId: UserID, + username: username, + }); + break; + case 400: + Alert.alert(ERROR_REGISTRATION(data.toLowerCase())); + break; + default: + Alert.alert(ERROR_SOMETHING_WENT_WRONG_REFRESH); + break; + } + } else { + Alert.alert(ERROR_SOMETHING_WENT_WRONG_REFRESH); + } + }; + + useEffect(() => { + if (isPhoneVerified) { + advance(); + } + }, [isPhoneVerified]); + + return ( + <Background + style={styles.container} + gradientType={BackgroundGradientType.Light}> + <StatusBar barStyle="light-content" /> + <KeyboardAvoidingView + behavior={Platform.OS === 'ios' ? 'padding' : 'height'} + keyboardVerticalOffset={-(SCREEN_HEIGHT * 0.2)} + style={styles.container}> + {currentStep !== 0 && currentStep !== 3 && ( + <ArrowButton + style={styles.backArrow} + direction="backward" + onboarding={true} + onPress={() => { + // if I go back do I want to reset the previous form? + setCurrentStep(currentStep - 1); + resetForm(step.placeholder); + setAttemptedSubmit(false); + setFadeValue(new Animated.Value(0)); + }} + /> + )} + {step.placeholder === 'Phone' && !isPhoneVerified ? ( + <> + <Animated.Text style={[styles.formHeader, {opacity: fadeValue}]}> + PHONE NUMBER + </Animated.Text> + <Animated.View style={[styles.formContainer, {opacity: fadeValue}]}> + <TaggInput + style={ + attemptedSubmit && !valid + ? [styles.input, styles.invalidColor] + : styles.input + } + maxLength={10} // currently only support US phone numbers + accessibilityHint="Enter your phone number." + accessibilityLabel="Phone number input field." + placeholder="Phone Number" + autoCompleteType="tel" + selectionColor="white" + textContentType="telephoneNumber" + externalStyles={{ + warning: styles.passWarning, + }} + keyboardType="number-pad" + onChangeText={handlePhoneUpdate} + autoFocus={true} + blurOnSubmit={false} + valid={valid} + invalidWarning={'Please enter a valid 10 digit number.'} + attemptedSubmit={attemptedSubmit} + onSubmitEditing={goToPhoneVerification} + /> + <Animated.View style={{opacity: fadeButtonValue}}> + <TaggSquareButton + onPress={goToPhoneVerification} + title={'Verify'} + buttonStyle={'normal'} + buttonColor={'white'} + labelColor={'blue'} + /> + </Animated.View> + </Animated.View> + </> + ) : ( + <> + {step.placeholder !== 'Password' ? ( + <> + <Text style={styles.formHeader}>SIGN UP</Text> + <Animated.View + style={[styles.formContainer, {opacity: fadeValue}]}> + <TaggInput + key={step.placeholder} + style={ + attemptedSubmit && !valid + ? [styles.input, styles.invalidColor] + : styles.input + } + accessibilityHint={`Enter your ${step.placeholder.toLowerCase()}`} + accessibilityLabel={`${step.placeholder} input field.`} + placeholder={step.placeholder} + autoCompleteType="name" + autoCapitalize={autoCapitalize} + textContentType="name" + returnKeyType="done" + selectionColor="white" + onChangeText={step.onChangeText} + onSubmitEditing={advance} + autoFocus={true} + blurOnSubmit={false} + externalStyles={{ + warning: styles.passWarning, + }} + valid={valid} + invalidWarning={`Please enter a valid ${step.placeholder.toLowerCase()}`} + attemptedSubmit={attemptedSubmit} + /> + <Animated.View style={{opacity: fadeButtonValue}}> + <TaggSquareButton + onPress={advance} + title={'Next'} + buttonStyle={'normal'} + buttonColor={'white'} + labelColor={'blue'} + /> + </Animated.View> + </Animated.View> + </> + ) : ( + <Animated.View + style={[styles.formContainer, {opacity: fadeValue}]}> + <TaggInput + accessibilityHint="Enter a password." + accessibilityLabel="Password input field." + placeholder="Password" + autoCompleteType="password" + textContentType="oneTimeCode" + returnKeyType="done" + selectionColor="white" + onChangeText={handlePasswordUpdate} + onSubmitEditing={advanceRegistration} + blurOnSubmit={false} + autoFocus={true} + secureTextEntry={!passVisibility} + valid={valid} + externalStyles={{ + warning: styles.passWarning, + }} + invalidWarning={ + 'Password must be at least 8 characters & contain at least one of a-z, A-Z, 0-9, and a special character.' + } + attemptedSubmit={attemptedSubmit} + style={ + attemptedSubmit && !valid + ? [styles.input, styles.invalidColor] + : styles.input + } + /> + <TouchableOpacity + accessibilityLabel="Show password button" + accessibilityHint="Select this if you want to display your tagg password" + style={styles.showPassContainer} + onPress={() => setPassVisibility(!passVisibility)}> + <Text style={styles.showPass}>Show Password</Text> + </TouchableOpacity> + <LoadingIndicator /> + <TermsConditions + style={styles.tc} + accepted={tcAccepted} + onChange={setTCAccepted} + /> + <Animated.View style={{opacity: fadeButtonValue}}> + <TaggSquareButton + onPress={advanceRegistration} + title={'Next'} + buttonStyle={'normal'} + buttonColor={'white'} + labelColor={'blue'} + /> + </Animated.View> + </Animated.View> + )} + </> + )} + </KeyboardAvoidingView> + </Background> + ); +}; + +const styles = StyleSheet.create({ + container: { + height: SCREEN_HEIGHT, + width: SCREEN_WIDTH, + alignItems: 'center', + justifyContent: 'center', + }, + formContainer: { + marginTop: '20%', + height: SCREEN_HEIGHT * 0.2, + width: '80%', + justifyContent: 'space-between', + alignItems: 'center', + }, + arrow: { + color: '#6EE7E7', + }, + showPassContainer: { + marginVertical: '1%', + borderBottomWidth: 1, + paddingBottom: '1%', + alignSelf: 'flex-start', + borderBottomColor: 'white', + marginBottom: '8%', + }, + showPass: { + color: 'white', + }, + passWarning: { + fontSize: 14, + marginTop: 5, + color: '#FF8989', + maxWidth: 350, + alignSelf: 'flex-start', + }, + input: { + minWidth: '100%', + height: 40, + fontSize: 16, + fontWeight: '600', + color: '#fff', + borderBottomWidth: 1, + borderBottomColor: '#fff', + }, + invalidColor: { + color: '#FF8989', + }, + errorInput: { + minWidth: '60%', + height: 40, + fontSize: 16, + fontWeight: '600', + color: '#FF8989', + borderBottomWidth: 1, + borderBottomColor: '#fff', + }, + button: { + alignItems: 'center', + width: 40, + aspectRatio: 10, + }, + formHeader: { + color: '#fff', + fontSize: 30, + fontWeight: '600', + position: 'absolute', + top: SCREEN_HEIGHT * 0.15, + }, + load: { + top: '5%', + }, + tc: { + marginVertical: '5%', + }, + backArrow: { + width: normalize(29), + height: normalize(25), + position: 'absolute', + top: HeaderHeight * 1.5, + left: 20, + }, +}); +export default BasicInfoOnboarding; |