Designing a React E-Commerce App for Digital Products—Part 2 (Handling Authentication and Authorization)

Jollen Moyani - Dec 28 '23 - - Dev Community

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!

Using SupaBase as the database

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.

Supabase API settings reference

Supabase API settings reference

Let’s use its JavaScript SDK to use the database directly from the React code.

npm install @supabase/supabase-js
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

Creating the useAuth() hook for authentication

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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.

Creating the signup and login pages

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;
Enter fullscreen mode Exit fullscreen mode

Signup page

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.

Signup page in React e-commerce app

Signup page in React e-commerce app

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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.

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.

Login page in React e-commerce app

Login page in React e-commerce app

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;
Enter fullscreen mode Exit fullscreen mode

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.

Dashboard page in React e-commerce app

Dashboard page in React e-commerce app

Now, the user can sign up and log into our e-commerce app!

Conclusion

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!

Related blogs

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .