In part 1 of this series, we created an e-commerce web application in React to sell digital products. We created an architectural diagram to get an idea about the user navigation and wireframes and set up the boilerplate with the public and private routes.
In this part, we will handle the authentication and authorization of the web app. We’ll create the signup and login pages and integrate them with a database to persist the data.
Let’s get started!
In this demo, we’ll use SupaBase, which is an alternative to Firebase and provides an easy-to-use and integrated database (DB).
First, we sign into SupaBase, create a new project, and then get our project URL and API keys to create an instance of the DB.
Let’s use its JavaScript SDK to use the database directly from the React code.
npm install @supabase/supabase-js
To use the SupaBase, we have to create an instance of it so that we can invoke different methods of it. What I did was create a hook for the SupaBase to instantiate and then use it in all other hooks.
// hooks/supabase.js import { createClient } from "@supabase/supabase-js"; const useSupbase = () => { const supabase = createClient( "your-project-url", "your-anon-public-key" ); return supabase; }; export default useSupbase;
The first important hook we are going to create is an authentication hook called useAuth() that lets users sign up and log into the app. SupaBase already provides methods for seamless signup and login actions, so we are going to make use of them.
For signup, we’ll store the email address, password, first name, and last name details. You can store as many additional details as you want.
For the login, we’ll accept the email address and password and verify if the credential is valid or not.
Refer to the following code example.
// hooks/auth.js import useSupbase from "“./supbase"”; import useStore from "“../store/user"”; import { useState } from "“react"”; const useAuth = () => { const [signupData, setSignupData] = useState(null); const [loginData, setLoginData] = useState(null); const [error, setError] = useState(null); const supabase = useSupbase(); const setUserState = useStore((state) => state.setUserState); const signup = async (email, password, firstName, lastName) => { try { const { data, error } = await supabase.auth.signUp({ email: email, password: password, options: { data: { firstName, lastName, }, }, }); if (!error) { setSignupData(data); } else { setError(error); } } catch € { console.error("Error while creating an user please try agaiin!"); setError€(e); } }; const login = async (email, password) => { try { const { data, error } = await supabase.auth.signInWithPassword({ email, password, }); if (!error) { const { session, user } = data; setLoginData(data); setUserState({ isLoggedIn: true, email: user.email, firstName: user.user_metadata.firstName, lastName: user.user_metadata.lastName, accessToken: session.access_token, }); } else { setError(error); } } catch (error) { console.error("Invalid credenntials", error); setError(error); } }; return { login, loginData, signup, signupData, error }; }; export default useAuth;
The useAuth() hook provides the methods for signup and login and their respective data and errors, which helps us to abstract similar logic to a single stage.
Once the user has successfully logged in, we’ll store the user details like first name and last name in the Zustand along with the bearer access token. We’ll use them for further authorization to get the user-specific data from the database.
For this, the user state has to be updated in the Zustand to accommodate additional details.
import { create } from "zustand"; import { persist } from "zustand/middleware"; let store = (set) => ({ isLoggedIn: false, email: "", firstName: "", lastName: "", accessToken: "", setUserState: ({ isLoggedIn, email, firstName, lastName, accessToken }) => set(() => ({ isLoggedIn, email, firstName, lastName, accessToken })), }); //persist the state with key "randomKey" store = persist(store, { name: "user-store" }); //create the store let useStore = create(store); export default useStore;
On successful login, we’ll also change the state isLoggedIn to true. That will change the router to show on the dashboard page that the user is logged in.
By default, the user will be moved to the login page, and there will be a link at the bottom for the user to sign up if they haven’t already.
Let’s start by creating the signup page. Add it to the router and then create the page.
import { BrowserRouter, Routes, Route } from "react-router-dom"; import Login from "./pages/login"; import Signup from "./pages/signup"; import PrivateRoutes from "./routes/private"; import PublicRoutes from "./routes/public"; import NotFound from "./pages/404"; const App = () => { return ( <BrowserRouter> <Routes> <Route path="/" element={<PublicRoutes />}> <Route index element={<h1>Browse</h1>} /> <Route path="product-list" element={<h1>Product List</h1>}></Route> <Route path="login" element={<Login />}></Route> <Route path="signup" element={<Signup />}></Route> </Route> <Route path="/" element={<PrivateRoutes />}> <Route path="dashboard" element={<h1>Dashboard</h1>}></Route> <Route path="product-add" element={<h1>Product Add</h1>}></Route> <Route path="checkout" element={<h1>checkout</h1>}></Route> <Route path="thank-you" element={<h1>Thank You</h1>}></Route> </Route> <Route path="*" element={<NotFound />} /> </Routes> </BrowserRouter> ); }; export default App;
The signup page is part of the public routes, as users can access it without being logged in. To sign up, users must provide their first name, last name, email address, and password, and then re-enter the password to verify.
To create the layout, we’ll use the Syncfusion React inputs, buttons, and card controls.
import "@syncfusion/ej2-layouts/styles/material.css"; import "@syncfusion/ej2-react-inputs/styles/material.css"; import "@syncfusion/ej2-react-buttons/styles/material.css"; import "./login.css"; import { ButtonComponent } from "@syncfusion/ej2-react-buttons"; import { useState, useCallback, useEffect } from "react"; import { debounce } from "lodash"; import Toast from "../components/Toast"; import { Link, useNavigate } from "react-router-dom"; import useAuth from "../hooks/auth"; const Signup = () => { const navigate = useNavigate(); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [repassword, setRepassword] = useState(""); const [firstName, setFirstName] = useState(""); const [lastName, setLastName] = useState(""); const [invalidEmail, setInvalidEmail] = useState(false); const [passwordMatching, setPasswordMatching] = useState(true); const [toast, setToast] = useState({ show: false, message: "", type: "error", }); const { signup, signupData } = useAuth(); // helper function that checks email using regex const isValidEmail = (email) => { return /\S+@\S+\.\S+/.test(email); }; // function to validate email const validateEmail = (text) => { if (!!text && !isValidEmail(text)) { setInvalidEmail(true); } else { setInvalidEmail(false); } }; // debounce the function to initiate test // when user stops writing for certain time const debouncedValidateEmail = useCallback(debounce(validateEmail, 1000), []); const handleEmailChange = (e) => { setEmail(e.target.value); debouncedValidateEmail(e.target.value); }; // monitor change of re-entered password useEffect(() => { // if both the passwords mismatch, update the state to show error if (password && repassword && password !== repassword) { setPasswordMatching(false); } else { setPasswordMatching(true); } }, [repassword]); // on signup const handleSubmit = () => { // if the required fields are empty if (!firstName || !lastName || !email || !password) { // show toast message setToast({ message: "Required fields are missing", show: true, type: "error", }); } else { // initiate signup signup(email, password, firstName, lastName); } }; // monitor the change in data after signup useEffect(() => { // if user is successfully authenticated if (signupData?.user?.role === "authenticated") { // show toast message setToast({ message: "Successfully signed up", show: true, type: "success", }); // and redirect to the login page setTimeout(() => { navigate("/login"); }, 2000); } }, [signupData]); return ( <div className="e-card login-container"> <h1 className="text-center">Welcome to Geva Digital Shop</h1> <h2 className="text-center">Signup to sell any digital product</h2> <div className="field-area"> <label htmlFor="first-name">First Name *</label> <input className="e-input" type="text" placeholder="Your first name..." name="first-name" id="first-name" onChange={(e) => setFirstName(e.target.value)} value={firstName} required /> </div> <div className="field-area"> <label htmlFor="last-name">Last Name *</label> <input className="e-input" type="text" placeholder="Your last name..." name="last-name" id="last-name" onChange={(e) => setLastName(e.target.value)} value={lastName} required /> </div> <div className="field-area"> <label htmlFor="email">Email *</label> <input className="e-input" type="email" placeholder="Your email..." name="email" id="email" onChange={handleEmailChange} value={email} required /> {invalidEmail && ( <p className="error">Please enter a valid email address</p> )} </div> <div className="field-area"> <label htmlFor="password">Password *</label> <input className="e-input" type="password" placeholder="Your password..." name="password" id="password" onChange={(e) => setPassword(e.target.value)} value={password} required /> </div> <div className="field-area"> <label htmlFor="re-password">Re-enter Password *</label> <input className="e-input" type="password" placeholder="Re-enter your password..." name="re-password" id="re-password" onChange={(e) => setRepassword(e.target.value)} value={repassword} required disabled={!password} /> </div> {repassword && !passwordMatching && ( <span className="text-center" style={{ color: "red" }}> Entered passwords does not match </span> )} <div style={{ width: "120px", margin: "20px auto 0 auto", }} > <ButtonComponent cssClass="e-success e-block" type="submit" onClick={handleSubmit} style={{ fontSize: "1.2em" }} > Signup </ButtonComponent> </div> {toast.show && ( <Toast errorMessage={toast.message} type={toast.type} onClose={() => { setToast({ show: false, message: "", type: "error", }); }} /> )} <span className="text-center" style={{ marginTop: "1em" }}> Already have an account? <Link to="/login">login</Link> </span> </div> ); }; export default Signup;
Now, we’ve created the controlled components and maintained the state for each input.
On the email change, we’ll validate that the entered email address is in the correct format using Regex and debouncing the input using the lodash.debounce.
Also, on submission, we are validating whether all the inputs are filled or not. If not, we’ll show an error with the help of Synfusion’s React Toast component.
Import { ToastComponent } from “@syncfusion/ej2-react-notifications”; import “@syncfusion/ej2-base/styles/material.css”; import “@syncfusion/ej2-react-buttons/styles/material.css”; import “@syncfusion/ej2-react-popups/styles/material.css”; import “@syncfusion/ej2-react-notifications/styles/material.css”; const TOAST_TYPES = { warning: “e-toast-warning”, success: “e-toast-success”, error: “e-toast-danger”, info: “e-toast-infor”, }; const Toast = ({ errorMessage, type, onClose }) => { let toastInstance; let position = { X: “Center” }; function toastCreated() { toastInstance.show(); } function toastDestroyed€ { e.clickToClose = true; onClose && onClose(); } return ( <div> <ToastComponent ref={(toast) => (toastInstance = toast)} title={type.toUpperCase()} content={errorMessage} position={position} created={toastCreated.bind(this)} click={toastDestroyed.bind(this)} showCloseButton cssClass={TOAST_TYPES[type] || TOAST_TYPES["info"]} /> </div> ); }; export default Toast;
If every input is filled, we sign up the user using the signup method from the useAuth() hook and show a success toast. Then, we navigate the user to the login page.
To log in, we accept the email address and password and pass them to Supabase’s signinwithpassword method with our useAuth() hook.
Refer to the following image.
Refer to the following code example.
import "@syncfusion/ej2-layouts/styles/material.css"; import "@syncfusion/ej2-react-inputs/styles/material.css"; import "@syncfusion/ej2-react-buttons/styles/material.css"; import "./login.css"; import { ButtonComponent } from "@syncfusion/ej2-react-buttons"; import { useState, useCallback, useEffect } from "react"; import { debounce } from "lodash"; import Toast from "../components/Toast"; import { Link } from "react-router-dom"; import useAuth from "../hooks/auth"; const Login = () => { const { login, loginData } = useAuth(); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [invalidEmail, setInvalidEmail] = useState(false); const [showToast, setShowToast] = useState(false); // helper function that checks email using regex const isValidEmail = (email) => { return /\S+@\S+\.\S+/.test(email); }; // function to validate email const validateEmail = (text) => { if (!!text && !isValidEmail(text)) { setInvalidEmail(true); } else { setInvalidEmail(false); } }; // debounce the function to initiate test // when user stops writing for certain time const debouncedValidateEmail = useCallback(debounce(validateEmail, 1000), []); const handleEmailChange = (e) => { setEmail(e.target.value); debouncedValidateEmail(e.target.value); }; const handleSubmit = () => { if (!email || !password) { setShowToast(true); } else { login(email, password); } }; return ( <div className="e-card login-container"> <h1 className="text-center">Welcome to Geva Digital Shop</h1> <h2 className="text-center">Login to continue</h2> <div className="field-area"> <label htmlFor="email">Email *</label> <input className="e-input" type="email" placeholder="Your email..." name="email" id="email" onChange={handleEmailChange} value={email} required /> {invalidEmail && ( <p className="error">Please enter a valid email address</p> )} </div> <div className="field-area"> <label htmlFor="password">Password *</label> <input className="e-input" type="password" placeholder="Your password..." name="password" id="password" onChange={(e) => setPassword(e.target.value)} value={password} required /> </div> <div style={{ width: "120px", margin: "20px auto 0 auto", }} > <ButtonComponent cssClass="e-success e-block" type="submit" onClick={handleSubmit} style={{ fontSize: "1.2em" }} > Login </ButtonComponent> </div> {showToast && ( <Toast errorMessage={"Please enter valid credentials"} type={"error"} onClose={() => { setShowToast(false); }} /> )} <span className="text-center" style={{ marginTop: "1em" }}> Don't have an account? <Link to="/signup">signUp</Link> </span> </div> ); }; export default Login;
Here, too, we validate the email address and password. We show an appropriate error if the authentication fails using the Toast component.
If the user is successfully logged in, we’ll update the state in the Zustand in useAuth()’s login method. This will update the router and render the Dashboard page as the user is logged in.
Now, the user can sign up and log into our e-commerce app!
Thanks for reading! In this blog, we’ve seen how to design the Signup and Login pages for the React e-commerce app for digital products. We encourage you to put this knowledge into practice and share your experiences in the comments section below!
In the next part, we’ll design the home page with some dummy products to show and then the dashboard page, where the new products can be added.
Syncfusion’s Essential Studio® for React is a one-stop solution offering over 80 high-performance, lightweight, modular, and responsive UI components. It’s the perfect suite to build any web app.
For our existing Syncfusion users, the latest version of Essential Studio® is readily available for download on the License and Downloads page. If you’re new to Syncfusion, we invite you to explore our products’ features and capabilities with our 30-day free trial.
For questions, you can contact us through our support forum, support portal, or feedback portal. We are always here to assist you!