Building a Finance Management App with React Native - Part 4

Revising the Sign-Up Process

Today, I finalize the sign-up process. I'll be improving the user experience in these ways:

  1. Separate the onboarding screens and the signup screens. The user will be able to view three onboarding screens and undergo a step-by-step registration process. I also added a skip button on the onboarding screens whereby the user can click on it and skip to the step-by-step registration process.

  2. Get rid of the progress dots and replace them with "Step 1/3" statements at the top-right corner of the screen.

    Github Link: https://github.com/faithgaiciumia/FinMan-App

    Final Output: https://www.youtube.com/shorts/3pvqFaGcvko

New Onboarding Screens

I simply included text in the three sliding screens. Each screen describes a feature of the app. On the final screen, there is a get started button which takes the user to the signup page using react native navigation: - the same code is used for the skip button which is on every onboarding screen.

<TouchableOpacity style={styles.button} onPress {()=>navigation.replace("signup") }>
          <LongButton text="Get Started" />
</TouchableOpacity>

The Signup Page

The signup page displays the different screens and their inputs conditionally. I used three boolean variables to conditionally render the screens. The boolean variables include stepOne, stepTwo, stepThree. Initially, only stepOne is true while the rest are false. Each screen is only displayed if its respective step variable is true. If the validation check for a screen is correct, the current step is set to false and the next step is set to true. Additionally, I declare and initialize all the input variables, their set functions, the error messages and their set functions on the signup page. These variables are then passed on to the respective screens.

Signup.js

import { Text, View, StyleSheet, Dimensions } from "react-native";
import SignUpOne from "./SignUpOne";
import SignUpTwo from "./SignUpTwo";
import SignUpThree from "./SignUpThree";
import React from "react";

export default function SignUp({ navigation }) {
  //initiliaze formdata variables
  const [email, setEmail] = React.useState("");
  const [fullName, setFullName] = React.useState("");
  const [password, setPassword] = React.useState("");
  const [confirmPass, setConfirmPass] = React.useState("");
  //initialise error messages
  const [emailErr, setEmailErr] = React.useState(null);
  const [fullNameErr, setFullNameErr] = React.useState(null);
  const [passwordErr, setPasswordErr] = React.useState(null);
  const [confirmPassErr, setConfirmPassErr] = React.useState(null);
  //initialise conditional rendering variables
  const [stepOne, setStepOne] = React.useState(true);
  const [stepTwo, setStepTwo] = React.useState(false);
  const [stepThree, setStepThree] = React.useState(false);
  const [step, setStep] = React.useState(1);

  return (
    <View style={styles.container}>
      {stepOne && (
        <SignUpOne
          fullName={fullName}
          setFullName={setFullName}
          fullNameErr={fullNameErr}
          setFullNameErr={setFullNameErr}
          setStepOne={setStepOne}
          setStepTwo={setStepTwo}
          step={step}
          setStep={setStep}
        />
      )}
      {stepTwo && (
        <SignUpTwo
          email={email}
          setEmail={setEmail}
          emailErr={emailErr}
          setEmailErr={setEmailErr}
          setStepTwo={setStepTwo}
          setStepThree={setStepThree}
          step={step}
          setStep={setStep}
        />
      )}
      {stepThree && (
        <SignUpThree
          password={password}
          setPassword={setPassword}
          confirmPass={confirmPass}
          setConfirmPass={setConfirmPass}
          confirmPassErr={confirmPassErr}
          setConfirmPassErr={setConfirmPassErr}
          passwordErr={passwordErr}
          setPasswordErr={setPasswordErr}
          setStepThree={setStepThree}
          step={step}
          setStep={setStep}
          navigation={navigation}

        />
      )}
    </View>
  );
}
const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: "flex-start",
    alignItems: "flex-start",
    alignContent: "center",
    backgroundColor: "#fff",
  },
  onbImg: {
    width: Dimensions.get("screen").width,
    height: 300,
  },
  heading: {
    marginTop: 0,
    fontFamily: "PoppinsBold",
    fontSize: 24,
    width: "30%",
    marginHorizontal: 20,
  },
  text: {
    margin: 20,
    fontFamily: "PoppinsMedium",
    fontSize: 12,
  },
  errorTxt: {
    marginHorizontal: 20,
    fontFamily: "PoppinsMedium",
    fontSize: 12,
    color: "red",
  },
  button: {
    marginHorizontal: 20,
  },
  row: {
    flexDirection: "row",
    justifyContent: "space-between",
    width: Dimensions.get("screen").width - 40,
    marginHorizontal: 20,
    marginTop: 40,
  },
  textInput: {
    width: Dimensions.get("screen").width - 40,
    marginHorizontal: 20,
    marginTop: 5,
  },
  btnTxt: {
    color: "#12B886",
    fontFamily: "PoppinsMedium",
  },
});

SignupOne.js

import { TextInput } from "@react-native-material/core";
import {
  Dimensions,
  Image,
  KeyboardAvoidingView,
  StyleSheet,
  Text,
  TouchableOpacity,
  TouchableWithoutFeedback,
  View,
  Keyboard,
} from "react-native";
import LongButton from "../../components/LongButton";

export default function SignUpOne({
  fullName,
  setFullName,
  fullNameErr,
  setFullNameErr,
  setStepOne,
  setStepTwo,
  step,
  setStep
}) {
  const validateData = () => {    
    //check if it is empty
    if (fullName === "") {
      setFullNameErr("Name is required");
      return;
    }
    //check length of name - should be at least 3 characters
    if (fullName.length < 3) {
      setFullNameErr(
        "Name is too short. - It should be at least 3 characters."
      );
      return;
    }
    //move to the next step.
    setStepOne(false);
    setStep(2);
    setStepTwo(true);
  };
  return (
    <KeyboardAvoidingView style={styles.container} behavior="padding">
      <TouchableWithoutFeedback onPress={Keyboard.dismiss}>
        <View>          
          <Text style={styles.text}>Step {step}/3</Text>
          <Text style={[styles.heading]}>Tell us your name</Text>
          <TextInput
            label="Name"
            variant="outlined"
            style={styles.textInput}
            value={fullName}
            onChangeText={(fullName) => setFullName(fullName)}
          />
          {/* conditionally render the error message */}
          {fullNameErr && <Text style={styles.errorTxt}>{fullNameErr}</Text>}
          <View style={styles.row}>
            <TouchableOpacity style={styles.button} onPress={validateData}>
              <LongButton text="Save" />
            </TouchableOpacity>
          </View>
        </View>
      </TouchableWithoutFeedback>
    </KeyboardAvoidingView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: "flex-start",
    alignItems: "flex-start",
    alignContent: "center",
    backgroundColor: "#fff",
    marginTop:50
  },
  onbImg: {
    width: Dimensions.get("screen").width,
    height: 300,
  },
  heading: {
    marginTop: 0,
    fontFamily: "PoppinsBold",
    fontSize: 24,
    width: "30%",
    marginHorizontal: 20,
  },
  text: {
    margin: 10,
    fontFamily: "PoppinsMedium",
    fontSize: 12,
    textAlign:"right"
  },
  errorTxt: {
    marginHorizontal: 20,
    fontFamily: "PoppinsMedium",
    fontSize: 12,
    color: "red",
  },
  button: {
    margin: 20,
  },
  row: {
    flexDirection: "row",
    justifyContent: "space-between",
    width: Dimensions.get("screen").width - 40,
    marginHorizontal: 20,
    marginTop: 0,
  },
  textInput: {
    width: Dimensions.get("screen").width - 40,
    marginHorizontal: 20,
  },
  btnTxt: {
    color: "#12B886",
    fontFamily: "PoppinsMedium",
  },
});

SignupTwo.js

import { TextInput } from "@react-native-material/core";
import {
  Dimensions,
  Image,
  KeyboardAvoidingView,
  StyleSheet,
  Text,
  TouchableOpacity,
  TouchableWithoutFeedback,
  View,
  Keyboard,
} from "react-native";
import LongButton from "../../components/LongButton";

export default function SignUpTwo({
  email,
  setEmail,
  emailErr,
  setEmailErr,
  setStepTwo,
  setStepThree,
  step,
  setStep
}) {
  const validateData = () => {
    let validEmail = /\S+@\S+\.\S+/.test(email);
    //check if it is empty
    if (email === "") {
      setemailErr("Email is required");
      return;
    }
    //check if it is a valid email
    if (!validEmail) {
      setEmailErr("The email is invalid");
      return;
    }
    //move to the next step
    setStepTwo(false);
    setStep(3);
    setStepThree(true);
  };
  return (
    <KeyboardAvoidingView style={styles.container} behavior="padding">
      <TouchableWithoutFeedback onPress={Keyboard.dismiss}>
        <View>
          <Text style={styles.text}>Step {step}/3</Text>
          <Text style={[styles.heading]}>Tell us your email</Text>
          <TextInput
            label="Email"
            variant="outlined"
            style={styles.textInput}
            value={email}
            onChangeText={(email) => setEmail(email)}
          />
          {/* conditionally render the error message */}
          {emailErr && <Text style={styles.errorTxt}>{emailErr}</Text>}
          <View style={styles.row}>
            <TouchableOpacity style={styles.button} onPress={validateData}>
              <LongButton text="Save" />
            </TouchableOpacity>
          </View>
        </View>
      </TouchableWithoutFeedback>
    </KeyboardAvoidingView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: "flex-start",
    alignItems: "flex-start",
    alignContent: "center",
    backgroundColor: "#fff",
    marginTop: 50,
  },
  onbImg: {
    width: Dimensions.get("screen").width,
    height: 300,
  },
  heading: {
    marginTop: 0,
    fontFamily: "PoppinsBold",
    fontSize: 24,
    width: "30%",
    marginHorizontal: 20,
  },
  text: {
    margin: 20,
    fontFamily: "PoppinsMedium",
    fontSize: 12,
    textAlign: "right",
  },
  errorTxt: {
    marginHorizontal: 20,
    fontFamily: "PoppinsMedium",
    fontSize: 12,
    color: "red",
  },
  button: {
    margin: 20,
  },
  row: {
    flexDirection: "row",
    justifyContent: "space-between",
    width: Dimensions.get("screen").width - 40,
    marginHorizontal: 20,
    marginTop: 0,
  },
  textInput: {
    width: Dimensions.get("screen").width - 40,
    marginHorizontal: 20,
  },
  btnTxt: {
    color: "#12B886",
    fontFamily: "PoppinsMedium",
  },
});

SignupThree.js

  • I use secureTextEntry on the inputs to hide the passwords as the user types.

  • I have also included an eye icon button where the user can toggle the password's visibility.

  • If the passwords are valid, user is taken to the temporary home page. I use navigation.replace so that the user is restrained from going back to the signup pages.

import { IconButton, TextInput } from "@react-native-material/core";
import {
  Dimensions,
  Image,
  KeyboardAvoidingView,
  StyleSheet,
  Text,
  TouchableOpacity,
  TouchableWithoutFeedback,
  View,
  Keyboard,
} from "react-native";
import Icon from "@expo/vector-icons/MaterialCommunityIcons";
import LongButton from "../../components/LongButton";
import React from "react";

export default function SignUpThree({
  password,
  setPassword,
  confirmPass,
  setConfirmPass,
  confirmPassErr,
  setConfirmPassErr,
  passwordErr,
  setPasswordErr,
  setStepThree,
  step,
  setStep,
  navigation,
}) {
  //initialize the show password toggles
  const [hidePass, setHidePass] = React.useState(true);
  const [hideConfirmPass, setHideConfirmPass] = React.useState(true);
  const validateData = () => {
    let strongPassword =
      /(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[^A-Za-z0-9])(?=.{10,})/.test(
        password
      );
    //password is empty
    if (password === "") {
      setPasswordErr("Password is required.");
      return;
    }
    //check if passwords match
    if (password !== confirmPass) {
      setConfirmPassErr("Passwords do not match.");
      return;
    }
    //check if it is a strong password
    if (!strongPassword) {
      setPasswordErr(
        "Weak Password. Ensure it is more than 10 characters and has alphanumeric characters."
      );
      return;
    }
    //move to home page
    setStepThree(false);
    navigation.replace("Home");
  };
  return (
    <KeyboardAvoidingView style={styles.container} behavior="padding">
      <TouchableWithoutFeedback onPress={Keyboard.dismiss}>
        <View>
          <Text style={styles.text}>Step {step}/3</Text>
          <Text style={[styles.heading]}>Set a Password</Text>
          <TextInput
            label="Password"
            variant="outlined"
            style={styles.textInput}
            value={password}
            onChangeText={(password) => setPassword(password)}
            secureTextEntry={hidePass}
            trailing={(props) => (
              <IconButton
                icon={(props) => <Icon name="eye" {...props} />}
                {...props}
                onPress={() => setHidePass(!hidePass)}
              />
            )}
          />
          {/* conditionally render the error message */}
          {passwordErr && <Text style={styles.errorTxt}>{passwordErr}</Text>}
          <TextInput
            label="Confirm Password"
            variant="outlined"
            style={styles.textInput}
            value={confirmPass}
            onChangeText={(confirmPass) => setConfirmPass(confirmPass)}
            secureTextEntry={hideConfirmPass}
            trailing={(props) => (
              <IconButton
                icon={(props) => <Icon name="eye" {...props} />}
                {...props}
                onPress={() => setHideConfirmPass(!hideConfirmPass)}
              />
            )}
          />
          {/* conditionally render the error message */}
          {confirmPassErr && (
            <Text style={styles.errorTxt}>{confirmPassErr}</Text>
          )}

          <View style={styles.row}>
            <TouchableOpacity style={styles.button} onPress={validateData}>
              <LongButton text="Done" />
            </TouchableOpacity>
          </View>
        </View>
      </TouchableWithoutFeedback>
    </KeyboardAvoidingView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: "flex-start",
    alignItems: "flex-start",
    alignContent: "center",
    backgroundColor: "#fff",
    marginTop: 50,
  },
  onbImg: {
    width: Dimensions.get("screen").width,
    height: 300,
  },
  heading: {
    marginTop: 0,
    fontFamily: "PoppinsBold",
    fontSize: 24,
    width: "30%",
    marginHorizontal: 20,
  },
  text: {
    margin: 20,
    fontFamily: "PoppinsMedium",
    fontSize: 12,
    textAlign: "right",
  },
  errorTxt: {
    marginHorizontal: 20,
    fontFamily: "PoppinsMedium",
    fontSize: 12,
    color: "red",
  },
  button: {
    marginHorizontal: 20,
  },
  row: {
    flexDirection: "row",
    justifyContent: "space-between",
    width: Dimensions.get("screen").width - 40,
    marginHorizontal: 20,
    marginTop: 40,
  },
  textInput: {
    width: Dimensions.get("screen").width - 40,
    marginHorizontal: 20,
    marginTop: 5,
  },
  btnTxt: {
    color: "#12B886",
    fontFamily: "PoppinsMedium",
  },
});

Next up, I will design the home page and implement the design. Thank you for reading through.