aboutsummaryrefslogtreecommitdiff
path: root/src/screens/onboarding
diff options
context:
space:
mode:
authormeganhong <34787696+meganhong@users.noreply.github.com>2020-07-13 15:08:06 -0700
committerGitHub <noreply@github.com>2020-07-13 18:08:06 -0400
commit5dee1e585353b6d7407f521dfa9186dbf10e8226 (patch)
tree752f2053ec50f817a44c90501f3594427d50af5a /src/screens/onboarding
parent95e160e64dc6a5763fdbdc7d7e5b814302446ba9 (diff)
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 <meganhong31@g.ucla.edu>
Diffstat (limited to 'src/screens/onboarding')
-rw-r--r--src/screens/onboarding/Camera.tsx28
-rw-r--r--src/screens/onboarding/Login.tsx317
-rw-r--r--src/screens/onboarding/Profile.tsx121
-rw-r--r--src/screens/onboarding/Registration.tsx441
-rw-r--r--src/screens/onboarding/Verification.tsx143
-rw-r--r--src/screens/onboarding/index.ts5
6 files changed, 1055 insertions, 0 deletions
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<RootStackParamList, 'Camera'>;
+type CameraScreenNavigationProp = StackNavigationProp<
+ RootStackParamList,
+ 'Camera'
+>;
+interface CameraProps {
+ route: CameraScreenRouteProp;
+ navigation: CameraScreenNavigationProp;
+}
+
+const Camera: React.FC<CameraProps> = ({}) => {
+ return (
+ <Background>
+ <CenteredView>
+ <Text>Camera!</Text>
+ </CenteredView>
+ </Background>
+ );
+};
+
+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<RootStackParamList, 'Login'>;
+type VerificationScreenNavigationProp = StackNavigationProp<
+ RootStackParamList,
+ 'Login'
+>;
+interface LoginProps {
+ route: VerificationScreenRouteProp;
+ navigation: VerificationScreenNavigationProp;
+}
+/**
+ * Login screen.
+ * @param navigation react-navigation navigation object.
+ */
+const Login: React.FC<LoginProps> = ({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 = () => (
+ <TouchableOpacity
+ accessibilityLabel="Forgot password button"
+ accessibilityHint="Select this if you forgot your tagg password"
+ style={styles.forgotPassword}
+ onPress={() => Alert.alert("tagg! You're it!")}>
+ <Text style={styles.forgotPasswordText}>Forgot password</Text>
+ </TouchableOpacity>
+ );
+
+ /**
+ * Login screen login button.
+ */
+ const LoginButton = () => (
+ <SubmitButton
+ text="Let's Start!"
+ color="#fff"
+ style={styles.button}
+ accessibilityLabel="Let's Start!"
+ accessibilityHint="Select this after entering your tagg username and password"
+ onPress={handleLogin}
+ />
+ );
+
+ /**
+ * Login screen registration prompt.
+ */
+ const RegistrationPrompt = () => (
+ <View style={styles.newUserContainer}>
+ <Text
+ accessible={true}
+ accessibilityLabel="New to tagg?"
+ style={styles.newUser}>
+ New to tagg?{' '}
+ </Text>
+ <TouchableOpacity
+ accessibilityLabel="Get started."
+ accessibilityHint="Select this if you do not have a tagg account">
+ <Text
+ accessible={true}
+ accessibilityLabel="Get started"
+ style={styles.getStarted}
+ onPress={goToRegistration}>
+ Get started!
+ </Text>
+ </TouchableOpacity>
+ </View>
+ );
+
+ return (
+ <Background centered style={styles.container}>
+ <StatusBar barStyle="light-content" />
+ <KeyboardAvoidingView
+ behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
+ style={styles.keyboardAvoidingView}>
+ <Image
+ source={require('../../assets/images/logo.png')}
+ style={styles.logo}
+ />
+ <TaggInput
+ accessibilityHint="Enter your tagg username here"
+ accessibilityLabel="Username text entry box"
+ placeholder="Username"
+ autoCompleteType="username"
+ textContentType="username"
+ returnKeyType="next"
+ autoCapitalize="none"
+ onChangeText={handleUsernameUpdate}
+ onSubmitEditing={handleUsernameSubmit}
+ blurOnSubmit={false}
+ valid={form.isValidUser}
+ invalidWarning="Username must be at least 6 characters and can only contain letters, numbers, periods, and underscores."
+ attemptedSubmit={form.attemptedSubmit}
+ />
+
+ <TaggInput
+ accessibilityHint="Enter your tagg password here"
+ accessibilityLabel="Password text entry box"
+ placeholder="Password"
+ autoCompleteType="password"
+ textContentType="password"
+ returnKeyType="go"
+ autoCapitalize="none"
+ secureTextEntry
+ onChangeText={handlePasswordUpdate}
+ onSubmitEditing={handleLogin}
+ valid={form.isValidPassword}
+ invalidWarning="Password must be at least 8 characters long."
+ attemptedSubmit={form.attemptedSubmit}
+ ref={inputRef}
+ />
+ <ForgotPassword />
+ <LoginButton />
+ </KeyboardAvoidingView>
+ <RegistrationPrompt />
+ </Background>
+ );
+};
+
+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<RootStackParamList, 'Profile'>;
+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<ProfileProps> = ({navigation}) => {
+ /**
+ * Profile screen "Add Large Profile Pic Here" button
+ */
+ const LargeProfilePic = () => (
+ <View style={styles.container}>
+ <View style={styles.largeButton}>
+ <Text
+ accessible={true}
+ accessibilityLabel="ADD LARGE PROFILE PIC HERE"
+ style={styles.addLargeProfilePicText}
+ onPress={goToCamera}>
+ ADD LARGE PROFILE PIC HERE
+ </Text>
+ </View>
+ </View>
+ );
+
+ /**
+ * Profile screen "Add Smaller Profile Pic Here" button
+ */
+ const SmallProfilePic = () => (
+ <View style={styles.container}>
+ <View style={styles.smallButton}>
+ <Text
+ accessible={true}
+ accessibilityLabel="ADD SMALLER PIC"
+ style={styles.addSmallProfilePicText}
+ onPress={goToCamera}>
+ ADD SMALLER PIC
+ </Text>
+ </View>
+ </View>
+ );
+
+ /*
+ * Handles tap on add profile picture buttons by navigating to camera access
+ */
+ const goToCamera = () => {
+ navigation.navigate('Camera');
+ };
+
+ return (
+ <Background>
+ <StatusBar barStyle="light-content" />
+ <CenteredView>
+ <LargeProfilePic />
+ <SmallProfilePic />
+ </CenteredView>
+ </Background>
+ );
+};
+
+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<RegistrationProps> = ({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 = () => (
+ <View style={styles.footer}>
+ <ArrowButton
+ direction="backward"
+ onPress={() => navigation.navigate('Login')}
+ />
+ <TouchableOpacity onPress={handleRegister}>
+ <ArrowButton
+ direction="forward"
+ disabled={
+ !(
+ form.isValidFname &&
+ form.isValidLname &&
+ form.isValidEmail &&
+ form.isValidUsername &&
+ form.isValidPassword &&
+ form.passwordsMatch &&
+ form.tcAccepted
+ )
+ }
+ onPress={handleRegister}
+ />
+ </TouchableOpacity>
+ </View>
+ );
+
+ return (
+ <Background style={styles.container}>
+ <StatusBar barStyle="light-content" />
+ <KeyboardAvoidingView
+ behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
+ style={styles.container}>
+ <RegistrationWizard style={styles.wizard} step="one" />
+ <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="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"
+ 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}
+ />
+ <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: {
+ bottom: '10%',
+ },
+ android: {
+ bottom: '5%',
+ },
+ }),
+ },
+ formHeader: {
+ color: '#fff',
+ fontSize: 30,
+ fontWeight: '600',
+ marginBottom: '5%',
+ },
+ tc: {
+ marginTop: '5%',
+ },
+ footer: {
+ width: '100%',
+ flexDirection: 'row',
+ justifyContent: 'space-around',
+ ...Platform.select({
+ ios: {
+ bottom: '20%',
+ },
+ android: {
+ bottom: '10%',
+ },
+ }),
+ },
+});
+
+export default Registration;
diff --git a/src/screens/onboarding/Verification.tsx b/src/screens/onboarding/Verification.tsx
new file mode 100644
index 00000000..1dd4cf9e
--- /dev/null
+++ b/src/screens/onboarding/Verification.tsx
@@ -0,0 +1,143 @@
+import React from 'react';
+
+import {RootStackParamList} from '../../routes';
+import {RouteProp} from '@react-navigation/native';
+import {StackNavigationProp} from '@react-navigation/stack';
+import {
+ Background,
+ CenteredView,
+ RegistrationWizard,
+ SubmitButton,
+} from '../../components';
+import {Text} from 'react-native-animatable';
+import {
+ CodeField,
+ Cursor,
+ useBlurOnFulfill,
+ useClearByFocusCell,
+} from 'react-native-confirmation-code-field';
+import {StyleSheet, View, TouchableOpacity, KeyboardAvoidingView} from 'react-native';
+type LoginScreenRouteProp = RouteProp<RootStackParamList, 'Login'>;
+type LoginScreenNavigationProp = StackNavigationProp<
+ RootStackParamList,
+ 'Login'
+>;
+interface VerificationProps {
+ route: VerificationScreenRouteProp;
+ navigation: VerificationScreenNavigationProp;
+}
+
+const Verification: React.FC<VerificationProps> = ({}) => {
+ const [value, setValue] = React.useState('');
+ const ref = useBlurOnFulfill({value, cellCount: 6});
+ const [valueProps, getCellOnLayoutHandler] = useClearByFocusCell({
+ value,
+ setValue,
+ });
+
+ return (
+ <Background centered style={styles.container}>
+ <RegistrationWizard style={styles.wizard} step="one" />
+ <KeyboardAvoidingView behavior='padding' style={styles.form}>
+ <Text style={styles.formHeader}>Enter 6 digit code</Text>
+ <Text style={styles.description}>
+ We sent a 6 digit verification code to the email you provided.
+ </Text>
+ <CodeField
+ ref={ref}
+ {...valueProps}
+ value={value}
+ onChangeText={setValue}
+ cellCount={6}
+ rootStyle={styles.codeFieldRoot}
+ keyboardType="number-pad"
+ textContentType="oneTimeCode"
+ renderCell={({index, symbol, isFocused}) => (
+ <View
+ onLayout={getCellOnLayoutHandler(index)}
+ key={index}
+ style={[styles.cellRoot, isFocused && styles.focusCell]}>
+ <Text style={styles.cellText}>
+ {symbol || (isFocused ? <Cursor /> : null)}
+ </Text>
+ </View>
+ )}
+ />
+ <SubmitButton
+ text="Verify"
+ color="#fff"
+ style={styles.button}
+ accessibilityLabel="Verify"
+ accessibilityHint="Select this after entering your email verification code"
+ />
+ <TouchableOpacity>
+ <Text style={styles.resend}>Resend Code</Text>
+ </TouchableOpacity>
+ </KeyboardAvoidingView>
+ </Background>
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ wizard: {
+ marginTop: '3.5%',
+ flex: 1,
+ justifyContent: 'center'
+ },
+ form: {
+ alignItems: 'center',
+ justifyContent: 'flex-start',
+ flex: 3,
+ },
+ formHeader: {
+ color: '#fff',
+ fontSize: 20,
+ fontWeight: 'bold',
+ alignSelf: 'flex-start',
+ marginBottom: '6%',
+ marginHorizontal: '10%',
+ },
+ description: {
+ color: '#fff',
+ fontWeight: '600',
+ fontSize: 17,
+ marginHorizontal: '10%',
+ },
+ resend: {
+ textDecorationLine: 'underline',
+ color: '#fff',
+ fontSize: 15,
+ fontWeight: '600',
+ },
+ codeFieldRoot: {
+ width: 280,
+ marginHorizontal: 'auto',
+ marginVertical: '15%',
+ },
+ cellRoot: {
+ width: 40,
+ height: 60,
+ justifyContent: 'center',
+ alignItems: 'center',
+ borderBottomColor: '#fff',
+ borderBottomWidth: 1,
+ },
+ cellText: {
+ color: '#fff',
+ fontSize: 48,
+ textAlign: 'center',
+ },
+ focusCell: {
+ borderBottomColor: '#78a0ef',
+ borderBottomWidth: 2,
+ },
+ button: {
+ marginVertical: '5%'
+ }
+});
+export default Verification;
diff --git a/src/screens/onboarding/index.ts b/src/screens/onboarding/index.ts
new file mode 100644
index 00000000..a45daa5d
--- /dev/null
+++ b/src/screens/onboarding/index.ts
@@ -0,0 +1,5 @@
+export {default as Login} from './Login';
+export {default as Registration} from './Registration';
+export {default as Verification} from './Verification';
+export {default as Camera} from './Camera';
+export {default as Profile} from './Profile';