// NPM module imports
import React, { Component } from "react";
import PropTypes from "prop-types";
import { connect } from "react-redux";
// Component imports
import LargeTextInput from "../../FormInputs/TextInputs/LargeTextInput/LargeTextInput";
import RectangularButton from "../../Buttons/RectangularButton/RectangularButton";
import TextButton from "../../Buttons/TextButton/TextButton";
// File imports
import firebase from "../../../vendors/Firebase/firebase";
import { authError } from "../../../vendors/Firebase/logs";
import * as authStages from "../../../pages/Auth/authStages";
import * as authFlows from "../../../pages/Auth/authFlows";
import * as alertBarTypes from "../../AlertBar/alertBarTypes";
import * as textInputTypes from "../../FormInputs/TextInputs/textInputTypes";
import { startAuthFlow, changeAuthFlow } from "../../../store/actions/index";
// Style imports
import authCardStyles from "./AuthCard.module.scss";
import authStyles from "../AuthCard.module.scss";
/**
* Renders the AuthCard component.
* Handles both sign in and sign up authentication (username and password).
* @extends {Component}
* @component
* @category AuthCards
* @author Dan Levy <danlevy124@gmail.com>
*/
class AuthCard extends Component {
/**
* AuthCard component state
* @property {object} formData - Form input values
* @property {string} formData.email - The email input value
* @property {string} formData.password - The password input value
*/
state = {
formData: {
email: "",
password: "",
},
};
/**
* Indicates if the component is mounted.
* Used for asynchronous tasks.
* @see https://reactjs.org/blog/2015/12/16/ismounted-antipattern.html
*/
_isMounted = false;
/**
* Sets _isMounted to true
* Starts the auth flow
*/
componentDidMount() {
this._isMounted = true;
this.props.startAuthFlow(
this.props.authStage === authStages.SIGN_IN
? authFlows.SIGN_IN
: authFlows.SIGN_UP
);
}
/**
* Checks if a user is authenticated.
* Changes the auth flow if needed.
*/
componentDidUpdate(prevProps) {
this.changeAuthFlowIfNeeded(prevProps);
this.checkIfUserIsAuthenticated();
}
/**
* Sets _isMounted to false
*/
componentWillUnmount() {
this._isMounted = false;
}
/**
* Changes the auth flow if needed
* @function
* @param {object} prevProps - The previous props object
*/
changeAuthFlowIfNeeded = (prevProps) => {
if (
(prevProps.authStage === authStages.SIGN_IN &&
this.props.authStage === authStages.SIGN_UP) ||
(prevProps.authStage === authStages.SIGN_UP &&
this.props.authStage === authStages.SIGN_IN)
) {
this.props.changeAuthFlow(
this.props.authStage === authStages.SIGN_IN
? authFlows.SIGN_IN
: authFlows.SIGN_UP
);
}
};
/**
* Checks if a user is authenticated
* If a user is authenticated, the proper action is taken.
* @function
*/
checkIfUserIsAuthenticated = () => {
if (this.props.isAuthenticated) {
// A user is authenticated
if (this.props.authStage === authStages.SIGN_IN) {
// Sign in is complete, so this component is done being used
this.props.setLoading(false);
this.props.done(authStages.SIGN_IN);
} else {
// Sign up is complete, so send an email verification
this.sendEmailVerification();
}
}
};
/**
* Sends an email verification to the current user
* @function
*/
sendEmailVerification = () => {
firebase
.auth()
.currentUser.sendEmailVerification()
.then(() => {
// Sign up stage is done
this.props.setLoading(false);
this.props.done(authStages.SIGN_UP);
})
.catch((error) => {
authError(
error.code,
error.message,
"[AuthCard/sendEmailVerification]"
);
this.props.setLoading(false);
this.props.showAlert(
alertBarTypes.ERROR,
"Authentication Error",
error.message
);
});
};
/**
* Updates state with new text input value
* @function
* @param event - The event that called this function
*/
textInputValueChangedHandler = (event) => {
const inputName = event.target.name;
const text = event.target.value;
// Sets state with new input value
this.setState((prevState) => {
const updatedFormData = { ...prevState.formData };
updatedFormData[inputName] = text;
return {
formData: updatedFormData,
};
});
};
/**
* Submits the authentication form (sign in or sign up)
* @function
* @param event - The event that called this function
*/
submitHandler = (event) => {
// Prevents a page reload
event.preventDefault();
// Gets email and password values
const email = this.state.formData.email;
const password = this.state.formData.password;
if (this.isEmailValid() && this.isPasswordValid()) {
if (this.props.authStage === authStages.SIGN_IN) {
// Sign in
this.signInWithEmailPassword();
} else {
// Sign up
this.signUpWithEmailPassword(email, password);
}
}
};
/**
* Checks if the user's email is valid
* @function
* @returns {boolean} Indicates if the email is valid
*/
isEmailValid = () => {
const trimmedEmail = this.removeWhitespace(this.state.formData.email);
if (this.state.formData.email !== trimmedEmail) {
// Shows an alert and returns false (email is not valid)
this.props.showAlert(
alertBarTypes.ERROR,
"Email Error",
"Please remove whitespace from your email (e.g. spaces, tabs, etc.)"
);
return false;
}
// Email has no whitespace (valid)
return true;
};
/**
* Checks if the user's password is valid
* @function
* @returns {boolean} Indicates if the password is valid
*/
isPasswordValid = () => {
const trimmedPassword = this.removeWhitespace(
this.state.formData.password
);
if (this.state.formData.password !== trimmedPassword) {
// Shows an alert and returns false (password is not valid)
this.props.showAlert(
alertBarTypes.ERROR,
"Password Error",
"Please remove whitespace from your password (e.g. spaces, tabs, etc.)"
);
return false;
}
// Regex states that a password must have
// 8 characters with at least one of each of the following
// 1 uppercase letter
// 1 lowercase letter
// 1 number (0-9)
// 1 special character (!, @, #, $, %, ^, &, *, or -)
if (
!trimmedPassword.match(
/^(?=.*\d)(?=.*[!@#$%^&*-])(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z]).{8,}$/
)
) {
this.props.showAlert(
alertBarTypes.ERROR,
"Password Error",
`Invalid password. Your password must be at least 8 characters long and contain at least one of each:
uppercase letter [A-Z], lowercase letter [a-z], number [1-9], and special character [!, @, #, $, %, ^, &, *, or -]`
);
return false;
}
// Email has no whitespace and passes security requirements (valid)
return true;
};
/**
* Removes whitespace from a string
* @function
* @param {string} str - String to transform
* @returns {string} - A string without whitespace
*/
removeWhitespace = (str) => str.replace(/\s+/g, "");
/**
* Signs the user in with an email and password
* @function
*/
signInWithEmailPassword = () => {
this.props.setLoading(true);
// If the sign in succeeds, a Firebase observer will create a local copy of the user and alert Redux
// The Redux state property "isAuthenticated" will cause this component to update
firebase
.auth()
.signInWithEmailAndPassword(
this.state.formData.email,
this.state.formData.password
)
.catch((error) => {
authError(
error.code,
error.message,
"[AuthCard/signInWithEmailPassword]"
);
this.props.setLoading(false);
this.props.showAlert(
alertBarTypes.ERROR,
"Authentication Error",
error.message
);
});
};
/**
* Signs the user up with an email and password
* @function
*/
signUpWithEmailPassword = () => {
this.props.setLoading(true);
// If the sign up succeeds, a Firebase observer will create a local copy of the user and alert Redux
// The Redux state property "isAuthenticated" will cause this component to update
firebase
.auth()
.createUserWithEmailAndPassword(
this.state.formData.email,
this.state.formData.password
)
.catch((error) => {
authError(
error.code,
error.message,
"[AuthCard/signUpWithEmailPassword]"
);
this.props.setLoading(false);
this.props.showAlert(
alertBarTypes.ERROR,
"Sign Up Error",
error.message
);
});
};
/**
* Gets the component's heading
* @function
* @returns {string} A heading
*/
getHeading = () => {
return this.props.authStage === authStages.SIGN_IN
? "Sign In"
: "Sign Up";
};
/**
* Gets the form submission button's title
* @function
* @returns {string} A title
*/
getSubmitButtonTitle = () => {
return this.props.authStage === authStages.SIGN_IN
? "Sign In"
: "Sign Up";
};
/**
* Gets the change auth button's title
* @function
* @returns {string} A title
*/
getChangeAuthButtonTitle = () => {
return this.props.authStage === authStages.SIGN_IN
? "Don't have an account?"
: "Already have an account?";
};
/**
* Renders the AuthCard component
*/
render() {
return (
<div className={authStyles.authCard}>
{/* Heading */}
<h3 className={authStyles.authCardHeading}>
{this.getHeading()}
</h3>
{/* Auth form */}
<form
className={authStyles.authCardForm}
onSubmit={this.submitHandler}
>
{/* Email input */}
<div className={authCardStyles.authCardTextInput}>
<LargeTextInput
inputType={textInputTypes.EMAIL}
inputName="email"
labelText="Email"
value={this.state.formData.email}
isRequired={true}
onChange={this.textInputValueChangedHandler}
/>
</div>
{/* Password input */}
<div className={authCardStyles.authCardTextInput}>
<LargeTextInput
inputType={textInputTypes.PASSWORD}
inputName="password"
labelText="Password"
value={this.state.formData.password}
isRequired={true}
onChange={this.textInputValueChangedHandler}
/>
</div>
{/* Submit button */}
<div className={authStyles.authCardSubmitButtonContainer}>
<RectangularButton
type="submit"
value="submit"
text={this.getSubmitButtonTitle()}
backgroundColor="green"
/>
</div>
</form>
{/* A button for switching between auth flows (sign in and sign up) */}
<div className={authCardStyles.authCardChangeAuth}>
<TextButton
type="button"
value="change-auth"
text={this.getChangeAuthButtonTitle()}
textColor="blue"
center="false"
onClick={this.props.switchAuthFlow}
/>
</div>
</div>
);
}
}
// Prop types for the AuthCard component
AuthCard.propTypes = {
/**
* Indicates if a user is authenticated
*/
isAuthenticated: PropTypes.bool.isRequired,
/**
* The current auth stage.
* See [stages]{@link module:authStages}.
*/
authStage: PropTypes.oneOf([authStages.SIGN_IN, authStages.SIGN_UP])
.isRequired,
/**
* Tells Redux to show/hide the loading HUD (true for show and false for hide (i.e. remove))
*/
setLoading: PropTypes.func.isRequired,
/**
* Tells Redux to show an alert
*/
showAlert: PropTypes.func.isRequired,
/**
* Tells Redux to start the auth flow
*/
startAuthFlow: PropTypes.func.isRequired,
/**
* Tells Redux to change the auth flow
*/
changeAuthFlow: PropTypes.func.isRequired,
/**
* Tells Redux that this component is no longer needed (i.e. done)
*/
done: PropTypes.func.isRequired,
};
/**
* Gets the current state from Redux and passes parts of it to the AuthCard component as props.
* This function is used only by the react-redux connect function.
* @memberof AuthCard
* @param {object} state - The Redux state
* @returns {object} Redux state properties used in the AuthCard component
*/
const mapStateToProps = (state) => {
return {
isAuthenticated: state.auth.isAuthenticated,
};
};
/**
* Passes certain Redux actions to the AuthCard component as props.
* This function is used only by the react-redux connect function.
* @memberof AuthCard
* @param {function} dispatch - The react-redux dispatch function
* @returns {object} Redux actions used in the AuthCard component
*/
const mapDispatchToProps = (dispatch) => {
return {
startAuthFlow: (flow) => dispatch(startAuthFlow(flow)),
changeAuthFlow: (flow) => dispatch(changeAuthFlow(flow)),
};
};
export default connect(mapStateToProps, mapDispatchToProps)(AuthCard);
Source