├── 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 |
6 |
7 |
Loading...
8 |
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 | ![Firebase Auth for React + Redux Apps](https://res.cloudinary.com/clairec/image/upload/v1565618778/firebase_-auth-react-redux_nmxs6c.png) 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 |
49 | {/* Email */} 50 |
51 | 52 | 64 | {errors.emailIsEmpty && {errors.emailIsEmpty}} 65 | {errors.emailFormatInvalid && ( 66 | {errors.emailFormatInvalid} 67 | )} 68 |
69 | 70 | {/* PASSWORD */} 71 | {!reset && ( 72 |
73 | 74 | 85 | {errors.passIsStrong && {errors.passIsStrong}} 86 | {errors.passIsEmpty && {errors.passIsEmpty}} 87 |
88 | )} 89 | 90 | {/* BUTTONS */} 91 |
92 | 103 | {!newUser && !reset && ( 104 | 107 | )} 108 | {reset && ( 109 | 112 | )} 113 |
114 |
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 | --------------------------------------------------------------------------------