├── public
├── favicon.ico
├── manifest.json
└── index.html
├── src
├── scss
│ ├── _App.scss
│ ├── _Page.scss
│ ├── index.scss
│ ├── _Button.scss
│ ├── _Login.scss
│ ├── _Loader.scss
│ └── Spinner.scss
├── store
│ ├── actions
│ │ ├── apiStatus.js
│ │ ├── actionTypes.js
│ │ └── auth.js
│ └── reducers
│ │ ├── index.js
│ │ ├── apiStatus.js
│ │ └── auth.js
├── components
│ ├── Loader.js
│ ├── Main.js
│ ├── App.js
│ ├── hoc
│ │ └── requireAuth.js
│ ├── Bunny.js
│ ├── Spinner.js
│ ├── Home.js
│ └── Login.js
├── services
│ └── firebase.js
├── index.js
├── utils
│ ├── validateLoginForm.js
│ └── useForm.js
└── css
│ └── index.css
├── .gitignore
├── package.json
├── LICENSE
└── README.md
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/clairechabas/firebase-auth-react-redux/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/src/scss/_App.scss:
--------------------------------------------------------------------------------
1 | .App {
2 | display: flex;
3 | justify-content: center;
4 | align-items: center;
5 | height: 100vh;
6 | }
7 | a {
8 | text-decoration: none;
9 | }
10 |
--------------------------------------------------------------------------------
/src/store/actions/apiStatus.js:
--------------------------------------------------------------------------------
1 | import { BEGIN_API_CALL, API_CALL_ERROR } from "./actionTypes";
2 |
3 | export function beginApiCall() {
4 | return { type: BEGIN_API_CALL };
5 | }
6 |
7 | export function apiCallError() {
8 | return { type: API_CALL_ERROR };
9 | }
10 |
--------------------------------------------------------------------------------
/src/scss/_Page.scss:
--------------------------------------------------------------------------------
1 | .page {
2 | display: flex;
3 | flex-direction: column;
4 | justify-content: center;
5 | align-items: center;
6 | h1 {
7 | margin-bottom: 0;
8 | }
9 | .emoji {
10 | font-size: 4em;
11 | }
12 | button,
13 | a {
14 | margin-top: 3em;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/components/Loader.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const Loader = () => {
4 | return (
5 |
9 | );
10 | };
11 |
12 | export default Loader;
13 |
--------------------------------------------------------------------------------
/src/scss/index.scss:
--------------------------------------------------------------------------------
1 | html,
2 | body,
3 | input,
4 | button {
5 | font-family: "Capriola", sans-serif;
6 | }
7 |
8 | // VARIABLES
9 | $primary-color: #29b1cc;
10 |
11 | // PARTIALS
12 | @import "App";
13 | @import "Button";
14 | @import "Login";
15 | @import "Page";
16 | @import "Loader";
17 | @import "Spinner";
18 |
--------------------------------------------------------------------------------
/src/store/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from "redux";
2 | import { firebaseReducer } from "react-redux-firebase";
3 | import authReducer from "./auth";
4 | import apiStatusReducer from "./apiStatus";
5 |
6 | export default combineReducers({
7 | firebaseReducer,
8 | authReducer,
9 | apiStatusReducer
10 | });
11 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": ".",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/src/components/Main.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { connect } from "react-redux";
3 | import Home from "./Home";
4 | import Login from "./Login";
5 | import Loader from "./Loader";
6 |
7 | const Main = ({ auth }) => {
8 | return (
9 |
10 | {!auth.isLoaded ? : !auth.isEmpty ? : }
11 |
12 | );
13 | };
14 |
15 | function mapStateToProps(state) {
16 | return {
17 | auth: state.firebaseReducer.auth
18 | };
19 | }
20 |
21 | export default connect(mapStateToProps)(Main);
22 |
--------------------------------------------------------------------------------
/src/components/App.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Switch, Route } from "react-router-dom";
3 |
4 | import Main from "./Main";
5 | import Login from "./Login";
6 | import Bunny from "./Bunny";
7 |
8 | const App = () => {
9 | return (
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | );
18 | };
19 |
20 | export default App;
21 |
--------------------------------------------------------------------------------
/src/components/hoc/requireAuth.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import { connect } from "react-redux";
3 |
4 | export default ChildComponent => {
5 | const ComposedComponent = props => {
6 | useEffect(() => {
7 | if (props.auth.isLoaded && props.auth.isEmpty) props.history.push("/");
8 | }, [props.auth, props.history]);
9 |
10 | return ;
11 | };
12 |
13 | function mapStateToProps(state) {
14 | return {
15 | auth: state.firebaseReducer.auth
16 | };
17 | }
18 |
19 | return connect(mapStateToProps)(ComposedComponent);
20 | };
21 |
--------------------------------------------------------------------------------
/src/components/Bunny.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Link } from "react-router-dom";
3 | import requireAuth from "./hoc/requireAuth";
4 |
5 | const Bunny = () => {
6 | return (
7 |
8 |
9 |
10 | 🐰
11 |
12 |
13 |
14 | You are on the Bunny component. When in bunnies country, do what bunnies
15 | do.
16 |
17 |
18 | Jump back home
19 |
20 |
21 | );
22 | };
23 |
24 | export default requireAuth(Bunny);
25 |
--------------------------------------------------------------------------------
/src/store/actions/actionTypes.js:
--------------------------------------------------------------------------------
1 | // API CALLS
2 | export const BEGIN_API_CALL = "BEGIN_API_CALL";
3 | export const API_CALL_ERROR = "API_CALL_ERROR";
4 |
5 | // SIGN UP
6 | export const SIGNUP_SUCCESS = "SIGNUP_SUCCESS";
7 | export const SIGNUP_ERROR = "SIGNUP_ERROR";
8 |
9 | // SIGN IN
10 | export const SIGNIN_SUCCESS = "SIGNIN_SUCCESS";
11 | export const SIGNIN_ERROR = "SIGNIN_ERROR";
12 | export const EMAIL_NOT_VERIFIED = "EMAIL_NOT_VERIFIED";
13 |
14 | // SIGN OUT
15 | export const SIGNOUT_SUCCESS = "SIGNOUT_SUCCESS";
16 | export const SIGNOUT_ERROR = "SIGNOUT_ERROR";
17 |
18 | // RESET PASSWORD
19 | export const RESET_SUCCESS = "RESET_SUCCESS";
20 | export const RESET_ERROR = "RESET_ERROR";
21 |
--------------------------------------------------------------------------------
/src/store/reducers/apiStatus.js:
--------------------------------------------------------------------------------
1 | import { BEGIN_API_CALL, API_CALL_ERROR } from "../actions/actionTypes";
2 |
3 | const INITIAL_STATE = {
4 | apiCallsInProgress: 0
5 | };
6 |
7 | function actionTypeEndsInSuccess(type) {
8 | return type.substring(type.length - 8) === "_SUCCESS";
9 | }
10 |
11 | export default function apiCallStatusReducer(state = INITIAL_STATE, action) {
12 | if (action.type === BEGIN_API_CALL) {
13 | return { ...state, apiCallsInProgress: 1 };
14 | } else if (action.type === API_CALL_ERROR) {
15 | return { ...state, apiCallsInProgress: 0 };
16 | } else if (actionTypeEndsInSuccess(action.type)) {
17 | return { ...state, apiCallsInProgress: 0 };
18 | } else {
19 | return state;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/components/Spinner.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const Spinner = () => {
4 | return (
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | );
20 | };
21 |
22 | export default Spinner;
23 |
--------------------------------------------------------------------------------
/src/services/firebase.js:
--------------------------------------------------------------------------------
1 | // Firebase App (the core Firebase SDK) is always required and
2 | // must be listed before other Firebase SDKs
3 | import * as firebase from "firebase/app";
4 |
5 | // Add the Firebase services that you want to use
6 | // We only want to use Firebase Auth here
7 | import "firebase/auth";
8 |
9 | // Your app's Firebase configuration
10 | var firebaseConfig = {
11 | apiKey: "[YOUR_API_KEY]",
12 | authDomain: "[YOUR_FIREBASE_AUTH_DOMAIN]",
13 | databaseURL: "[YOUR_FIREBASE_DATABASE_URL]",
14 | projectId: "[YOUR_FIREBASE_PROJECT_ID]",
15 | storageBucket: "",
16 | messagingSenderId: "[YOUR_FIREBASE_MESSAGING_SENDER_ID]",
17 | appId: "[YOUR_FIREBASE_APP_ID]"
18 | };
19 |
20 | // Initialize Firebase
21 | firebase.initializeApp(firebaseConfig);
22 |
23 | // Finally, export it to use it throughout your app
24 | export default firebase;
25 |
--------------------------------------------------------------------------------
/src/scss/_Button.scss:
--------------------------------------------------------------------------------
1 | %btn {
2 | padding: 1em 3em;
3 | border-radius: 20px;
4 | font-weight: bold;
5 | border: none;
6 | outline: none;
7 | border: 2px solid $primary-color;
8 | &:hover {
9 | cursor: pointer;
10 | }
11 | }
12 | %btn-on {
13 | background-color: $primary-color;
14 | color: #fff;
15 | }
16 | %btn-off {
17 | background-color: #fff;
18 | color: $primary-color;
19 | }
20 | .btn-login {
21 | @extend %btn;
22 | @extend %btn-on;
23 | &:hover {
24 | box-shadow: 0 10px 20px rgba($primary-color, 0.3);
25 | }
26 | }
27 | .btn-switch {
28 | @extend %btn;
29 | @extend %btn-off;
30 | &:hover {
31 | @extend %btn-on;
32 | }
33 | }
34 | .btn-link {
35 | color: $primary-color;
36 | border: none;
37 | border-bottom: 1px solid $primary-color;
38 | padding: 0;
39 | margin-left: 2em;
40 | &:hover {
41 | border-bottom: none;
42 | cursor: pointer;
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/store/reducers/auth.js:
--------------------------------------------------------------------------------
1 | import {
2 | SIGNUP_SUCCESS,
3 | SIGNUP_ERROR,
4 | SIGNIN_SUCCESS,
5 | SIGNIN_ERROR,
6 | EMAIL_NOT_VERIFIED,
7 | SIGNOUT_SUCCESS,
8 | SIGNOUT_ERROR,
9 | RESET_SUCCESS,
10 | RESET_ERROR
11 | } from "../actions/actionTypes";
12 |
13 | const INITIAL_STATE = {
14 | authMsg: ""
15 | };
16 |
17 | export default function(state = INITIAL_STATE, action) {
18 | if (action.type === SIGNIN_SUCCESS || action.type === SIGNOUT_SUCCESS) {
19 | return { ...state, authMsg: "" };
20 | } else if (
21 | action.type === SIGNUP_SUCCESS ||
22 | action.type === SIGNUP_ERROR ||
23 | action.type === SIGNIN_ERROR ||
24 | action.type === EMAIL_NOT_VERIFIED ||
25 | action.type === SIGNOUT_ERROR ||
26 | action.type === RESET_SUCCESS ||
27 | action.type === RESET_ERROR
28 | ) {
29 | return { ...state, authMsg: action.payload };
30 | } else {
31 | return state;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import { BrowserRouter as Router } from "react-router-dom";
4 | import "./css/index.css";
5 | import App from "./components/App";
6 |
7 | // SETTING UP REDUX STORE
8 | import { Provider } from "react-redux";
9 | import { createStore, applyMiddleware, compose } from "redux";
10 | import reduxThunk from "redux-thunk";
11 | import reducers from "./store/reducers";
12 |
13 | // ENHANCING STORE WITH FIREBASE
14 | import { reactReduxFirebase } from "react-redux-firebase";
15 | import firebase from "./services/firebase";
16 | const createStoreWithFirebase = compose(reactReduxFirebase(firebase))(
17 | createStore
18 | );
19 | const store = createStoreWithFirebase(
20 | reducers,
21 | {},
22 | applyMiddleware(reduxThunk)
23 | );
24 |
25 | ReactDOM.render(
26 |
27 |
28 |
29 |
30 | ,
31 | document.getElementById("root")
32 | );
33 |
--------------------------------------------------------------------------------
/src/scss/_Login.scss:
--------------------------------------------------------------------------------
1 | .login {
2 | width: 35vw;
3 | h1 {
4 | text-align: center;
5 | margin-bottom: 1em;
6 | }
7 | h2 {
8 | text-align: center;
9 | color: $primary-color;
10 | margin-bottom: 2em;
11 | }
12 | .auth-message {
13 | text-align: center;
14 | color: orange;
15 | }
16 | .input-group {
17 | display: flex;
18 | flex-direction: column;
19 | margin-bottom: 2em;
20 | label {
21 | margin-bottom: 0.8em;
22 | }
23 | input {
24 | outline: none;
25 | border: 1px solid #dfe2e6;
26 | color: #6b6c6f;
27 | border-radius: 20px;
28 | padding: 1.2em 1.5em;
29 | &.input-error {
30 | border: 1px solid red;
31 | }
32 | }
33 | small {
34 | font-size: 0.6em;
35 | margin: 0.6em 0 0 1.7em;
36 | color: red;
37 | }
38 | }
39 | .login-footer {
40 | display: flex;
41 | flex-direction: column;
42 | align-items: center;
43 | margin-top: 1em;
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "firebase-auth-react-redux",
3 | "version": "1.0.0",
4 | "private": true,
5 | "dependencies": {
6 | "firebase": "^6.3.3",
7 | "react": "^16.8.6",
8 | "react-dom": "^16.8.6",
9 | "react-redux": "^7.1.0",
10 | "react-redux-firebase": "^2.3.0",
11 | "react-router-dom": "^5.0.1",
12 | "react-scripts": "3.0.1",
13 | "redux": "^4.0.4",
14 | "redux-thunk": "^2.3.0",
15 | "typescript": "^3.5.3"
16 | },
17 | "scripts": {
18 | "start": "react-scripts start",
19 | "build": "react-scripts build",
20 | "test": "react-scripts test",
21 | "eject": "react-scripts eject"
22 | },
23 | "eslintConfig": {
24 | "extends": "react-app"
25 | },
26 | "browserslist": {
27 | "production": [
28 | ">0.2%",
29 | "not dead",
30 | "not op_mini all"
31 | ],
32 | "development": [
33 | "last 1 chrome version",
34 | "last 1 firefox version",
35 | "last 1 safari version"
36 | ]
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/utils/validateLoginForm.js:
--------------------------------------------------------------------------------
1 | export default function validate(credentials, isReset) {
2 | let errors = {};
3 |
4 | // Checking if email is not empty
5 | if (!credentials.email) {
6 | errors.emailIsEmpty = "You need to enter your e-mail address";
7 | }
8 | // Checking if email format is valid
9 | if (credentials.email && !/\S+@\S+\.\S+/.test(credentials.email)) {
10 | errors.emailFormatInvalid = "Your e-mail format doesn't seem right";
11 | }
12 |
13 | // Don't check password if user is resetting password
14 | if (!isReset) {
15 | // Checking if password is not empty
16 | if (!credentials.password) {
17 | errors.passIsEmpty = "You need a password";
18 | }
19 | // Checking if password is strong enough
20 | let strengthCheck = /^(?=.*[A-Z])(?=.*[!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~])(?=.*[0-9])(?=.*[a-z]).{8,250}$/;
21 | if (credentials.password && !credentials.password.match(strengthCheck))
22 | errors.passIsStrong = "You need a stronger password";
23 | }
24 |
25 | return errors;
26 | }
27 |
--------------------------------------------------------------------------------
/src/components/Home.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { compose } from "redux";
3 | import { connect } from "react-redux";
4 | import { signout } from "../store/actions/auth";
5 | import requireAuth from "./hoc/requireAuth";
6 |
7 | const Main = ({ signout }) => {
8 | return (
9 |
10 |
11 |
12 | 🏡
13 |
14 |
15 |
Welcome on Home
16 |
You have successfully signed in, congrats!
17 |
20 |
21 | );
22 | };
23 |
24 | function mapStateToProps(state) {
25 | return {
26 | auth: state.firebaseReducer.auth
27 | };
28 | }
29 |
30 | function mapDispatchToProps(dispatch) {
31 | return {
32 | signout: () => dispatch(signout())
33 | };
34 | }
35 |
36 | export default compose(
37 | connect(
38 | mapStateToProps,
39 | mapDispatchToProps
40 | ),
41 | requireAuth
42 | )(Main);
43 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Claire Chabas
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/src/scss/_Loader.scss:
--------------------------------------------------------------------------------
1 | $width: 15px;
2 | $height: 15px;
3 | $bounce_height: 30px;
4 |
5 | @keyframes bounce {
6 | 0% {
7 | top: $bounce_height;
8 | height: 5px;
9 | border-radius: 60px 60px 20px 20px;
10 | transform: scaleX(2);
11 | }
12 | 35% {
13 | height: $height;
14 | border-radius: 50%;
15 | transform: scaleX(1);
16 | }
17 | 100% {
18 | top: 0;
19 | }
20 | }
21 |
22 | .loader {
23 | position: absolute;
24 | top: 50%;
25 | left: 50%;
26 | transform: translate(-50%, -50%);
27 | .loader-bounceball {
28 | position: relative;
29 | display: inline-block;
30 | height: 37px;
31 | width: $width;
32 | &:before {
33 | position: absolute;
34 | top: 0;
35 | content: "";
36 | display: block;
37 | width: $width;
38 | height: $height;
39 | border-radius: 50%;
40 | background-color: $primary-color;
41 | transform-origin: 50%;
42 | animation: bounce 500ms alternate infinite ease;
43 | }
44 | }
45 | .loader-text {
46 | color: $primary-color;
47 | display: inline-block;
48 | margin-left: 1em;
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/utils/useForm.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 |
3 | const useForm = (loginFunction, validate, isReset) => {
4 | const [errors, setErrors] = useState({});
5 | const [credentials, setCredentials] = useState({ email: "", password: "" });
6 | const [isSubmitting, setIsSubmitting] = useState(false);
7 |
8 | useEffect(() => {
9 | // Checking if any error occurred during validation
10 | // and if a form is still submitting
11 | if (Object.keys(errors).length === 0 && isSubmitting) {
12 | // If no errors and form still haven't been submitted
13 | // We sign up or in the user according to the login function passed
14 | loginFunction();
15 |
16 | // Cleaning inputs after signup/in
17 | setCredentials({ email: "", password: "" });
18 |
19 | // Now that the form has been submitted we set isSubmitting back to false
20 | // This prevents the form being submitted again and again each time useEffect is activated
21 | setIsSubmitting(false);
22 | }
23 | }, [errors, isReset, isSubmitting, loginFunction]);
24 |
25 | const handleSubmit = e => {
26 | if (e) e.preventDefault();
27 | setIsSubmitting(true);
28 |
29 | // Checking credentials for validation
30 | // And passing validate output (which return eventual errors) into setErrors
31 | setErrors(validate(credentials, isReset));
32 | };
33 |
34 | const handleChange = e => {
35 | e.persist();
36 | setCredentials(credentials => ({
37 | ...credentials,
38 | [e.target.name]: e.target.value
39 | }));
40 | };
41 |
42 | return [credentials, handleChange, handleSubmit, errors];
43 | };
44 |
45 | export default useForm;
46 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
22 |
26 | Auth with Firebase for React + Redux Apps : From 0 to 1
27 |
28 |
29 |
30 |
31 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | # Auth with Firebase for React + Redux Apps : From 0 to 1
6 |
7 | ### Full Step-By-Step Guide: [Read the Medium post](https://medium.com/@clairechabas/https-medium-com-clairechabas-auth-with-firebase-for-react-redux-apps-from-0-to-1-104e7343521b)
8 |
9 | 
10 |
11 | This project is a boilerplate for initialize an authentication system using Firebase Auth on a React application using Redux.
12 |
13 | I found developing an authentication system from scratch could take time and efforts, as well as raising security concerns. Firebase makes it really easy and blazzing fast to set up all the auth functionalities an application needs. I thought sharing the full step-by-step guide to doing so could be helpful. So the purpose of this boilerplate and of [the post that goes with it](https://medium.com/@clairechabas/https-medium-com-clairechabas-auth-with-firebase-for-react-redux-apps-from-0-to-1-104e7343521b) are to get people up and running with a React application with a fully functional authentication system using Firebase Auth and Redux.
14 |
15 | ### Features included
16 |
17 | - Sign up (with email and password)
18 | - Sign in
19 | - Sign out
20 | - Password reset
21 | - E-mail verification after sign up
22 | - Redirecting after sign in
23 | - Persisting auth status
24 | - Restricting access according to auth status
25 | - Loader
26 |
27 | ### Project setup
28 |
29 | 1. Create a [Firebase account](https://firebase.google.com/).
30 | 2. Create a new project to get your firebaseConfig object.
31 | 3. Activate Firebase Authentication from your Firebase console.
32 | 4. Get the code from this repo, cd in the project folder and install all dependencies :
33 |
34 | ```
35 | npm install
36 | ```
37 |
38 | 5. In the services/firebase.js file, insert your own firebaseConfig object :
39 |
40 | ```
41 | var firebaseConfig = {
42 | apiKey: "[YOUR_API_KEY]",
43 | authDomain: "[YOUR_FIREBASE_AUTH_DOMAIN]",
44 | databaseURL: "[YOUR_FIREBASE_DATABASE_URL]",
45 | projectId: "[YOUR_FIREBASE_PROJECT_ID]",
46 | storageBucket: "",
47 | messagingSenderId: "[YOUR_FIREBASE_MESSAGING_SENDER_ID]",
48 | appId: "[YOUR_FIREBASE_APP_ID]"
49 | };
50 | ```
51 |
52 | 6. Run the app in the development mode:
53 |
54 | ```
55 | npm start
56 | ```
57 |
58 | ### Other available scripts
59 |
60 | Compiles and minifies for production to the `build` folder.
61 |
62 | ```
63 | npm run build
64 | ```
65 |
66 | Run your tests
67 |
68 | ```
69 | npm test
70 | ```
71 |
72 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app), therefore using the configurations and build tool implemented in Create React App.
73 | You can `eject` to set your own configuration choices and build tool. This command will remove the single build dependency from your project.
74 |
75 | ```
76 | npm run eject
77 | ```
78 |
79 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!**
80 |
--------------------------------------------------------------------------------
/src/scss/Spinner.scss:
--------------------------------------------------------------------------------
1 | .sk-circle {
2 | margin: 0 auto;
3 | width: 1.5em;
4 | height: 1.5em;
5 | position: relative;
6 | }
7 | .sk-circle .sk-child {
8 | width: 100%;
9 | height: 100%;
10 | position: absolute;
11 | left: 0;
12 | top: 0;
13 | }
14 | .sk-circle .sk-child:before {
15 | content: "";
16 | display: block;
17 | margin: 0 auto;
18 | width: 15%;
19 | height: 15%;
20 | background-color: #fff;
21 | border-radius: 100%;
22 | -webkit-animation: sk-circleBounceDelay 1.2s infinite ease-in-out both;
23 | animation: sk-circleBounceDelay 1.2s infinite ease-in-out both;
24 | }
25 | .sk-circle .sk-circle2 {
26 | -webkit-transform: rotate(30deg);
27 | -ms-transform: rotate(30deg);
28 | transform: rotate(30deg);
29 | }
30 | .sk-circle .sk-circle3 {
31 | -webkit-transform: rotate(60deg);
32 | -ms-transform: rotate(60deg);
33 | transform: rotate(60deg);
34 | }
35 | .sk-circle .sk-circle4 {
36 | -webkit-transform: rotate(90deg);
37 | -ms-transform: rotate(90deg);
38 | transform: rotate(90deg);
39 | }
40 | .sk-circle .sk-circle5 {
41 | -webkit-transform: rotate(120deg);
42 | -ms-transform: rotate(120deg);
43 | transform: rotate(120deg);
44 | }
45 | .sk-circle .sk-circle6 {
46 | -webkit-transform: rotate(150deg);
47 | -ms-transform: rotate(150deg);
48 | transform: rotate(150deg);
49 | }
50 | .sk-circle .sk-circle7 {
51 | -webkit-transform: rotate(180deg);
52 | -ms-transform: rotate(180deg);
53 | transform: rotate(180deg);
54 | }
55 | .sk-circle .sk-circle8 {
56 | -webkit-transform: rotate(210deg);
57 | -ms-transform: rotate(210deg);
58 | transform: rotate(210deg);
59 | }
60 | .sk-circle .sk-circle9 {
61 | -webkit-transform: rotate(240deg);
62 | -ms-transform: rotate(240deg);
63 | transform: rotate(240deg);
64 | }
65 | .sk-circle .sk-circle10 {
66 | -webkit-transform: rotate(270deg);
67 | -ms-transform: rotate(270deg);
68 | transform: rotate(270deg);
69 | }
70 | .sk-circle .sk-circle11 {
71 | -webkit-transform: rotate(300deg);
72 | -ms-transform: rotate(300deg);
73 | transform: rotate(300deg);
74 | }
75 | .sk-circle .sk-circle12 {
76 | -webkit-transform: rotate(330deg);
77 | -ms-transform: rotate(330deg);
78 | transform: rotate(330deg);
79 | }
80 | .sk-circle .sk-circle2:before {
81 | -webkit-animation-delay: -1.1s;
82 | animation-delay: -1.1s;
83 | }
84 | .sk-circle .sk-circle3:before {
85 | -webkit-animation-delay: -1s;
86 | animation-delay: -1s;
87 | }
88 | .sk-circle .sk-circle4:before {
89 | -webkit-animation-delay: -0.9s;
90 | animation-delay: -0.9s;
91 | }
92 | .sk-circle .sk-circle5:before {
93 | -webkit-animation-delay: -0.8s;
94 | animation-delay: -0.8s;
95 | }
96 | .sk-circle .sk-circle6:before {
97 | -webkit-animation-delay: -0.7s;
98 | animation-delay: -0.7s;
99 | }
100 | .sk-circle .sk-circle7:before {
101 | -webkit-animation-delay: -0.6s;
102 | animation-delay: -0.6s;
103 | }
104 | .sk-circle .sk-circle8:before {
105 | -webkit-animation-delay: -0.5s;
106 | animation-delay: -0.5s;
107 | }
108 | .sk-circle .sk-circle9:before {
109 | -webkit-animation-delay: -0.4s;
110 | animation-delay: -0.4s;
111 | }
112 | .sk-circle .sk-circle10:before {
113 | -webkit-animation-delay: -0.3s;
114 | animation-delay: -0.3s;
115 | }
116 | .sk-circle .sk-circle11:before {
117 | -webkit-animation-delay: -0.2s;
118 | animation-delay: -0.2s;
119 | }
120 | .sk-circle .sk-circle12:before {
121 | -webkit-animation-delay: -0.1s;
122 | animation-delay: -0.1s;
123 | }
124 |
125 | @-webkit-keyframes sk-circleBounceDelay {
126 | 0%,
127 | 80%,
128 | 100% {
129 | -webkit-transform: scale(0);
130 | transform: scale(0);
131 | }
132 | 40% {
133 | -webkit-transform: scale(1);
134 | transform: scale(1);
135 | }
136 | }
137 |
138 | @keyframes sk-circleBounceDelay {
139 | 0%,
140 | 80%,
141 | 100% {
142 | -webkit-transform: scale(0);
143 | transform: scale(0);
144 | }
145 | 40% {
146 | -webkit-transform: scale(1);
147 | transform: scale(1);
148 | }
149 | }
150 |
--------------------------------------------------------------------------------
/src/store/actions/auth.js:
--------------------------------------------------------------------------------
1 | import {
2 | SIGNUP_SUCCESS,
3 | SIGNUP_ERROR,
4 | SIGNIN_SUCCESS,
5 | SIGNIN_ERROR,
6 | EMAIL_NOT_VERIFIED,
7 | SIGNOUT_SUCCESS,
8 | SIGNOUT_ERROR,
9 | RESET_SUCCESS,
10 | RESET_ERROR
11 | } from "./actionTypes";
12 | import { beginApiCall, apiCallError } from "./apiStatus";
13 | import firebase from "../../services/firebase";
14 |
15 | // Signing up with Firebase
16 | export const signup = (email, password) => async dispatch => {
17 | try {
18 | dispatch(beginApiCall());
19 | firebase
20 | .auth()
21 | .createUserWithEmailAndPassword(email, password)
22 | .then(dataBeforeEmail => {
23 | firebase.auth().onAuthStateChanged(function(user) {
24 | user.sendEmailVerification();
25 | });
26 | })
27 | .then(dataAfterEmail => {
28 | firebase.auth().onAuthStateChanged(function(user) {
29 | if (user) {
30 | // Sign up successful
31 | dispatch({
32 | type: SIGNUP_SUCCESS,
33 | payload:
34 | "Your account was successfully created! Now you need to verify your e-mail address, please go check your inbox."
35 | });
36 | } else {
37 | // Signup failed
38 | dispatch({
39 | type: SIGNUP_ERROR,
40 | payload:
41 | "Something went wrong, we couldn't create your account. Please try again."
42 | });
43 | }
44 | });
45 | })
46 | .catch(() => {
47 | dispatch(apiCallError());
48 | dispatch({
49 | type: SIGNUP_ERROR,
50 | payload:
51 | "Something went wrong, we couldn't create your account. Please try again."
52 | });
53 | });
54 | } catch (err) {
55 | dispatch(apiCallError());
56 | dispatch({
57 | type: SIGNUP_ERROR,
58 | payload:
59 | "Something went wrong, we couldn't create your account. Please try again."
60 | });
61 | }
62 | };
63 |
64 | // Signing in with Firebase
65 | export const signin = (email, password, callback) => async dispatch => {
66 | try {
67 | dispatch(beginApiCall());
68 | firebase
69 | .auth()
70 | .signInWithEmailAndPassword(email, password)
71 | .then(data => {
72 | if (data.user.emailVerified) {
73 | console.log("IF", data.user.emailVerified);
74 | dispatch({ type: SIGNIN_SUCCESS });
75 | callback();
76 | } else {
77 | console.log("ELSE", data.user.emailVerified);
78 | dispatch({
79 | type: EMAIL_NOT_VERIFIED,
80 | payload: "You haven't verified your e-mail address."
81 | });
82 | }
83 | })
84 | .catch(() => {
85 | dispatch(apiCallError());
86 | dispatch({
87 | type: SIGNIN_ERROR,
88 | payload: "Invalid login credentials"
89 | });
90 | });
91 | } catch (err) {
92 | dispatch(apiCallError());
93 | dispatch({ type: SIGNIN_ERROR, payload: "Invalid login credentials" });
94 | }
95 | };
96 |
97 | // Signing out with Firebase
98 | export const signout = () => async dispatch => {
99 | try {
100 | dispatch(beginApiCall());
101 | firebase
102 | .auth()
103 | .signOut()
104 | .then(() => {
105 | dispatch({ type: SIGNOUT_SUCCESS });
106 | })
107 | .catch(() => {
108 | dispatch(apiCallError());
109 | dispatch({
110 | type: SIGNOUT_ERROR,
111 | payload: "Error, we were not able to log you out. Please try again."
112 | });
113 | });
114 | } catch (err) {
115 | dispatch(apiCallError());
116 | dispatch({
117 | type: SIGNOUT_ERROR,
118 | payload: "Error, we were not able to log you out. Please try again."
119 | });
120 | }
121 | };
122 |
123 | // Reset password with Firebase
124 | export const resetPassword = email => async dispatch => {
125 | try {
126 | dispatch(beginApiCall());
127 | firebase
128 | .auth()
129 | .sendPasswordResetEmail(email)
130 | .then(() =>
131 | dispatch({
132 | type: RESET_SUCCESS,
133 | payload:
134 | "Check your inbox. We've sent you a secured reset link by e-mail."
135 | })
136 | )
137 | .catch(() => {
138 | dispatch(apiCallError());
139 | dispatch({
140 | type: RESET_ERROR,
141 | payload:
142 | "Oops, something went wrong we couldn't send you the e-mail. Try again and if the error persists, contact admin."
143 | });
144 | });
145 | } catch (err) {
146 | dispatch(apiCallError());
147 | dispatch({ type: RESET_ERROR, payload: err });
148 | }
149 | };
150 |
--------------------------------------------------------------------------------
/src/components/Login.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { connect } from "react-redux";
3 | import { signup, signin, resetPassword } from "../store/actions/auth";
4 | import useForm from "../utils/useForm";
5 | import validate from "../utils/validateLoginForm";
6 | import Spinner from "./Spinner";
7 |
8 | const Login = ({
9 | signup,
10 | signin,
11 | resetPassword,
12 | authMsg,
13 | history,
14 | loading
15 | }) => {
16 | const [newUser, setNewUser] = useState(false);
17 | const [reset, SetReset] = useState(false);
18 | const [credentials, handleChange, handleSubmit, errors] = useForm(
19 | login,
20 | validate,
21 | reset
22 | );
23 |
24 | function login() {
25 | if (newUser) {
26 | // signup
27 | signup(credentials.email, credentials.password);
28 | } else {
29 | if (reset) {
30 | // reset password
31 | resetPassword(credentials.email);
32 | } else {
33 | // signin
34 | signin(credentials.email, credentials.password, () =>
35 | history.push("/")
36 | );
37 | }
38 | }
39 | }
40 |
41 | return (
42 |
43 |
Hi there!
44 |
45 | {reset ? "Reset password" : newUser ? "Create an account" : "Sign in"}
46 |
47 | {authMsg &&
{authMsg}
}
48 |
115 |
129 |
130 | );
131 | };
132 |
133 | function mapStateToProps(state) {
134 | return {
135 | authMsg: state.authReducer.authMsg,
136 | loading: state.apiStatusReducer.apiCallsInProgress > 0
137 | };
138 | }
139 |
140 | function mapDispatchToProps(dispatch) {
141 | return {
142 | signup: (email, password) => dispatch(signup(email, password)),
143 | signin: (email, password, callback) =>
144 | dispatch(signin(email, password, callback)),
145 | resetPassword: email => dispatch(resetPassword(email))
146 | };
147 | }
148 |
149 | export default connect(
150 | mapStateToProps,
151 | mapDispatchToProps
152 | )(Login);
153 |
--------------------------------------------------------------------------------
/src/css/index.css:
--------------------------------------------------------------------------------
1 | html,
2 | body,
3 | input,
4 | button {
5 | font-family: "Capriola", sans-serif; }
6 |
7 | .App {
8 | display: flex;
9 | justify-content: center;
10 | align-items: center;
11 | height: 100vh; }
12 |
13 | a {
14 | text-decoration: none; }
15 |
16 | .btn-login, .btn-switch {
17 | padding: 1em 3em;
18 | border-radius: 20px;
19 | font-weight: bold;
20 | border: none;
21 | outline: none;
22 | border: 2px solid #29b1cc; }
23 | .btn-login:hover, .btn-switch:hover {
24 | cursor: pointer; }
25 |
26 | .btn-login, .btn-switch:hover {
27 | background-color: #29b1cc;
28 | color: #fff; }
29 |
30 | .btn-switch {
31 | background-color: #fff;
32 | color: #29b1cc; }
33 |
34 | .btn-login:hover {
35 | box-shadow: 0 10px 20px rgba(41, 177, 204, 0.3); }
36 |
37 | .btn-link {
38 | color: #29b1cc;
39 | border: none;
40 | border-bottom: 1px solid #29b1cc;
41 | padding: 0;
42 | margin-left: 2em; }
43 | .btn-link:hover {
44 | border-bottom: none;
45 | cursor: pointer; }
46 |
47 | .login {
48 | width: 35vw; }
49 | .login h1 {
50 | text-align: center;
51 | margin-bottom: 1em; }
52 | .login h2 {
53 | text-align: center;
54 | color: #29b1cc;
55 | margin-bottom: 2em; }
56 | .login .auth-message {
57 | text-align: center;
58 | color: orange; }
59 | .login .input-group {
60 | display: flex;
61 | flex-direction: column;
62 | margin-bottom: 2em; }
63 | .login .input-group label {
64 | margin-bottom: 0.8em; }
65 | .login .input-group input {
66 | outline: none;
67 | border: 1px solid #dfe2e6;
68 | color: #6b6c6f;
69 | border-radius: 20px;
70 | padding: 1.2em 1.5em; }
71 | .login .input-group input.input-error {
72 | border: 1px solid red; }
73 | .login .input-group small {
74 | font-size: 0.6em;
75 | margin: 0.6em 0 0 1.7em;
76 | color: red; }
77 | .login .login-footer {
78 | display: flex;
79 | flex-direction: column;
80 | align-items: center;
81 | margin-top: 1em; }
82 |
83 | .page {
84 | display: flex;
85 | flex-direction: column;
86 | justify-content: center;
87 | align-items: center; }
88 | .page h1 {
89 | margin-bottom: 0; }
90 | .page .emoji {
91 | font-size: 4em; }
92 | .page button,
93 | .page a {
94 | margin-top: 3em; }
95 |
96 | @keyframes bounce {
97 | 0% {
98 | top: 30px;
99 | height: 5px;
100 | border-radius: 60px 60px 20px 20px;
101 | transform: scaleX(2); }
102 | 35% {
103 | height: 15px;
104 | border-radius: 50%;
105 | transform: scaleX(1); }
106 | 100% {
107 | top: 0; } }
108 | .loader {
109 | position: absolute;
110 | top: 50%;
111 | left: 50%;
112 | transform: translate(-50%, -50%); }
113 | .loader .loader-bounceball {
114 | position: relative;
115 | display: inline-block;
116 | height: 37px;
117 | width: 15px; }
118 | .loader .loader-bounceball:before {
119 | position: absolute;
120 | top: 0;
121 | content: "";
122 | display: block;
123 | width: 15px;
124 | height: 15px;
125 | border-radius: 50%;
126 | background-color: #29b1cc;
127 | transform-origin: 50%;
128 | animation: bounce 500ms alternate infinite ease; }
129 | .loader .loader-text {
130 | color: #29b1cc;
131 | display: inline-block;
132 | margin-left: 1em; }
133 |
134 | .sk-circle {
135 | margin: 0 auto;
136 | width: 1.5em;
137 | height: 1.5em;
138 | position: relative; }
139 |
140 | .sk-circle .sk-child {
141 | width: 100%;
142 | height: 100%;
143 | position: absolute;
144 | left: 0;
145 | top: 0; }
146 |
147 | .sk-circle .sk-child:before {
148 | content: "";
149 | display: block;
150 | margin: 0 auto;
151 | width: 15%;
152 | height: 15%;
153 | background-color: #fff;
154 | border-radius: 100%;
155 | -webkit-animation: sk-circleBounceDelay 1.2s infinite ease-in-out both;
156 | animation: sk-circleBounceDelay 1.2s infinite ease-in-out both; }
157 |
158 | .sk-circle .sk-circle2 {
159 | -webkit-transform: rotate(30deg);
160 | -ms-transform: rotate(30deg);
161 | transform: rotate(30deg); }
162 |
163 | .sk-circle .sk-circle3 {
164 | -webkit-transform: rotate(60deg);
165 | -ms-transform: rotate(60deg);
166 | transform: rotate(60deg); }
167 |
168 | .sk-circle .sk-circle4 {
169 | -webkit-transform: rotate(90deg);
170 | -ms-transform: rotate(90deg);
171 | transform: rotate(90deg); }
172 |
173 | .sk-circle .sk-circle5 {
174 | -webkit-transform: rotate(120deg);
175 | -ms-transform: rotate(120deg);
176 | transform: rotate(120deg); }
177 |
178 | .sk-circle .sk-circle6 {
179 | -webkit-transform: rotate(150deg);
180 | -ms-transform: rotate(150deg);
181 | transform: rotate(150deg); }
182 |
183 | .sk-circle .sk-circle7 {
184 | -webkit-transform: rotate(180deg);
185 | -ms-transform: rotate(180deg);
186 | transform: rotate(180deg); }
187 |
188 | .sk-circle .sk-circle8 {
189 | -webkit-transform: rotate(210deg);
190 | -ms-transform: rotate(210deg);
191 | transform: rotate(210deg); }
192 |
193 | .sk-circle .sk-circle9 {
194 | -webkit-transform: rotate(240deg);
195 | -ms-transform: rotate(240deg);
196 | transform: rotate(240deg); }
197 |
198 | .sk-circle .sk-circle10 {
199 | -webkit-transform: rotate(270deg);
200 | -ms-transform: rotate(270deg);
201 | transform: rotate(270deg); }
202 |
203 | .sk-circle .sk-circle11 {
204 | -webkit-transform: rotate(300deg);
205 | -ms-transform: rotate(300deg);
206 | transform: rotate(300deg); }
207 |
208 | .sk-circle .sk-circle12 {
209 | -webkit-transform: rotate(330deg);
210 | -ms-transform: rotate(330deg);
211 | transform: rotate(330deg); }
212 |
213 | .sk-circle .sk-circle2:before {
214 | -webkit-animation-delay: -1.1s;
215 | animation-delay: -1.1s; }
216 |
217 | .sk-circle .sk-circle3:before {
218 | -webkit-animation-delay: -1s;
219 | animation-delay: -1s; }
220 |
221 | .sk-circle .sk-circle4:before {
222 | -webkit-animation-delay: -0.9s;
223 | animation-delay: -0.9s; }
224 |
225 | .sk-circle .sk-circle5:before {
226 | -webkit-animation-delay: -0.8s;
227 | animation-delay: -0.8s; }
228 |
229 | .sk-circle .sk-circle6:before {
230 | -webkit-animation-delay: -0.7s;
231 | animation-delay: -0.7s; }
232 |
233 | .sk-circle .sk-circle7:before {
234 | -webkit-animation-delay: -0.6s;
235 | animation-delay: -0.6s; }
236 |
237 | .sk-circle .sk-circle8:before {
238 | -webkit-animation-delay: -0.5s;
239 | animation-delay: -0.5s; }
240 |
241 | .sk-circle .sk-circle9:before {
242 | -webkit-animation-delay: -0.4s;
243 | animation-delay: -0.4s; }
244 |
245 | .sk-circle .sk-circle10:before {
246 | -webkit-animation-delay: -0.3s;
247 | animation-delay: -0.3s; }
248 |
249 | .sk-circle .sk-circle11:before {
250 | -webkit-animation-delay: -0.2s;
251 | animation-delay: -0.2s; }
252 |
253 | .sk-circle .sk-circle12:before {
254 | -webkit-animation-delay: -0.1s;
255 | animation-delay: -0.1s; }
256 |
257 | @-webkit-keyframes sk-circleBounceDelay {
258 | 0%,
259 | 80%,
260 | 100% {
261 | -webkit-transform: scale(0);
262 | transform: scale(0); }
263 | 40% {
264 | -webkit-transform: scale(1);
265 | transform: scale(1); } }
266 | @keyframes sk-circleBounceDelay {
267 | 0%,
268 | 80%,
269 | 100% {
270 | -webkit-transform: scale(0);
271 | transform: scale(0); }
272 | 40% {
273 | -webkit-transform: scale(1);
274 | transform: scale(1); } }
275 |
--------------------------------------------------------------------------------