diff options
Diffstat (limited to 'src/screens/onboarding/RegistrationTwo.tsx')
-rw-r--r-- | src/screens/onboarding/RegistrationTwo.tsx | 375 |
1 files changed, 375 insertions, 0 deletions
diff --git a/src/screens/onboarding/RegistrationTwo.tsx b/src/screens/onboarding/RegistrationTwo.tsx new file mode 100644 index 00000000..09e217f6 --- /dev/null +++ b/src/screens/onboarding/RegistrationTwo.tsx @@ -0,0 +1,375 @@ +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, + ActivityIndicator, +} from 'react-native'; +import {usePromiseTracker, trackPromise} from 'react-promise-tracker'; + +import {RootStackParamList} from '../../routes'; +import { + ArrowButton, + RegistrationWizard, + TaggInput, + TermsConditions, + Background, +} from '../../components'; +import { + passwordRegex, + usernameRegex, + REGISTER_ENDPOINT, + SEND_OTP_ENDPOINT, +} from '../../constants'; + +type RegistrationScreenTwoRouteProp = RouteProp< + RootStackParamList, + 'RegistrationTwo' +>; +type RegistrationScreenTwoNavigationProp = StackNavigationProp< + RootStackParamList, + 'RegistrationTwo' +>; +interface RegistrationTwoProps { + route: RegistrationScreenTwoRouteProp; + navigation: RegistrationScreenTwoNavigationProp; +} +/** + * Registration screen 2 for email, username, password, and terms and conditions + * @param navigation react-navigation navigation object + */ +const RegistrationTwo: React.FC<RegistrationTwoProps> = ({ + route, + navigation, +}) => { + // refs for changing focus + 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 '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({ + email: '', + username: '', + password: '', + confirm: '', + isValidEmail: false, + isValidUsername: false, + isValidPassword: false, + passwordsMatch: false, + tcAccepted: false, + attemptedSubmit: false, + }); + + /* + * 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.isValidUsername && form.isValidPassword && form.passwordsMatch) { + if (form.tcAccepted) { + let registerResponse = await fetch(REGISTER_ENDPOINT, { + method: 'POST', + body: JSON.stringify({ + first_name: fname, + last_name: lname, + email: email, + username: form.username, + password: form.password, + }), + }); + let statusCode = registerResponse.status; + let data = await registerResponse.json(); + if (statusCode === 201) { + let sendOtpResponse = await trackPromise( + fetch(SEND_OTP_ENDPOINT, { + method: 'POST', + body: JSON.stringify({ + username: form.username, + email: email, + }), + }), + ); + let otpStatusCode = sendOtpResponse.status; + if (otpStatusCode === 200) { + const userId: string = data.UserID; + navigation.navigate('Verification', { + username: form.username, + email: email, + userId: userId, + }); + } + } 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( + 'Registration failed ๐', + 'Please double-check your network connection and retry.', + ); + return { + name: 'Registration error', + description: error, + }; + } + }; + + const Footer = () => ( + <View style={styles.footer}> + <ArrowButton + direction="backward" + onPress={() => navigation.navigate('RegistrationOne')} + /> + <TouchableOpacity onPress={handleRegister}> + <ArrowButton + direction="forward" + disabled={ + !( + form.isValidUsername && + form.isValidPassword && + form.passwordsMatch && + form.tcAccepted + ) + } + onPress={handleRegister} + /> + </TouchableOpacity> + </View> + ); + + /** + * An activity indicator to indicate that the app is working during the send_otp request. + */ + const LoadingIndicator = () => { + const {promiseInProgress} = usePromiseTracker(); + return promiseInProgress ? ( + <ActivityIndicator style={styles.load} size="large" color="#fff" /> + ) : ( + <></> + ); + }; + + 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}> + <View> + <Text style={styles.formHeader}>SIGN UP</Text> + </View> + <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ย at least 6 characters and contain only alphanumerics.' + } + 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 at least 8 characters & contain at least one of a-z, A-Z, 0-9, 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} + /> + <LoadingIndicator /> + <TermsConditions + style={styles.tc} + accepted={form.tcAccepted} + onChange={handleTcUpdate} + /> + </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%', + }, + tc: { + marginVertical: '5%', + top: '8%', + }, + load: { + top: '5%', + }, + footer: { + width: '100%', + flexDirection: 'row', + justifyContent: 'space-around', + ...Platform.select({ + ios: { + bottom: '20%', + }, + android: { + bottom: '10%', + }, + }), + }, +}); + +export default RegistrationTwo; |