DEV Community

Cover image for How to Add Customer Profile to Your Next.js Ecommerce Store
Shahed Nasser for Medusa

Posted on • Edited on • Originally published at medusajs.com

How to Add Customer Profile to Your Next.js Ecommerce Store

Medusa is an open source headless commerce platform that aims to provide developers with a great developer experience. Although it provides Next.js and Gatsby storefronts to use, you can also use any storefront of your choice.

To make your developer experience even easier, Medusa provides a client NPM package that you can use with JavaScript and Typescript frameworks.

In this tutorial, you’ll learn how to install and use the Medusa JS Client in your storefront to implement a customer sign-up and profile flow.

You can find the code for this tutorial in this GitHub repository.

Prerequisites

This tutorial assumes you already have a Medusa server installed. If not, please follow along the quickstart guide.

Furthermore, this tutorial uses the Next.js starter to implement the customer profile. However, the tutorial will focus on how to use the Medusa JS Client in particular. So, you can still follow along if you are using a different framework for your storefront.

Install the Medusa JS Client

In your storefront project’s directory, install the Medusa JS client if you don’t have it installed already:

npm install @medusajs/medusa-js
Enter fullscreen mode Exit fullscreen mode

If you’re using our Next.js or Gatsby starters, then you don’t need to install it.

Initialize Client

In the Next.js starter, the client is initialized in utils/client.js and you can just import it into your components. However, if you’re using your custom storefront here’s how you can initialize the Medusa client:

const client = new Medusa({ baseUrl: BACKEND_URL });
Enter fullscreen mode Exit fullscreen mode

Where BACKEND_URL is the URL to your Medusa server.

You can then use the methods and resources in the client to send all types of requests to your Medusa server which you’ll see later in this tutorial.

Add Styles

This step is optional and can be skipped. To add some styling for the rest of the tutorial, you can create the file styles/customer.module.css with this content.

Add Customer to Context

In the Next.js starter, the StoreContext holds all variables and methods important to the store like the cart object.

In this section, you’ll add a variable and a method to the context: customer and setCustomer respectively.

In the defaultStoreContext variable in context/store-context.js add the following:

export const defaultStoreContext = {
    ...,
    customer: null,
    setCustomer: async () => {}
}
Enter fullscreen mode Exit fullscreen mode

Then, in the reducer function, add a case for the setCustomer action:

case "setCustomer":
      return {
        ...state,
        customer: action.payload
      }
Enter fullscreen mode Exit fullscreen mode

Inside the StoreProvider function, add inside the useEffect under the cart initialization the following to retrieve the customer if they are logged in and set it in the context:

//try to retrieve customer
client.auth.getSession()
  .then(({customer}) => {
    setCustomer(customer)
  })
  .catch((e) => setCustomer(null))
Enter fullscreen mode Exit fullscreen mode

Notice here that you’re using client.auth.getSession. client has been initialized earlier in the file, so if you’re implementing this in your own storefront you’d need to initialize it before this code snippet.

Then, you use auth.getSession which allows you to retrieve the current logged in customer, if there is any.

If the customer is logged in and is returned in the response, you set it in the context, otherwise you set the customer to null.

Then, add a new function inside StoreProvider to handle setting the customer:

const setCustomer = (customer) => {
    dispatch({type: 'setCustomer', payload: customer})
}
Enter fullscreen mode Exit fullscreen mode

This will just dispatch the action to the reducer which will set the customer object in the context.

Finally, add the setCustomer function to the value prop of StoreContext.Provider returned in StoreProvider:

return (
    <StoreContext.Provider
      value={{
        ...,
        setCustomer
      }}
    >
      {children}
    </StoreContext.Provider>
  )
Enter fullscreen mode Exit fullscreen mode

Create Sign Up and Sign In Pages

In this section, you’ll create the sign up and sign in pages to allow the customer to either create an account or log in with an existing one.

Sign Up Page

Create the file pages/sign-up.js with the following content:

import * as Yup from 'yup';

import { useContext, useEffect, useRef } from 'react';

import StoreContext from '../context/store-context';
import { createClient } from "../utils/client"
import styles from "../styles/customer.module.css";
import { useFormik } from "formik";
import { useRouter } from 'next/router';

export default function SignUp() {
  const router = useRouter();
  const { setCustomer, customer } = useContext(StoreContext)
  useEffect(() => {
    if (customer) {
      router.push("/")
    }
  }, [customer, router])
  const buttonRef = useRef();
  const { handleSubmit, handleBlur, handleChange, values, touched, errors } = useFormik({
    initialValues: {
      email: '',
      first_name: '',
      last_name: '',
      password: ''
    },
    validationSchema: Yup.object().shape({
      email: Yup.string().email().required(),
      first_name: Yup.string().required(),
      last_name: Yup.string().required(),
      password: Yup.string().required()
    }),
    onSubmit: function (values) {
      if (buttonRef.current) {
        buttonRef.current.disabled = true;
      }

      const client = createClient()

      client.customers.create({
        email: values.email,
        first_name: values.first_name,
        last_name: values.last_name,
        password: values.password
      }).then(({ customer }) => {
        setCustomer(customer);
      })
    }
  })
  return (
    <div className={styles.container}>
      <main className={styles.main}>
        <form onSubmit={handleSubmit} className={styles.signForm}>
          <h1>Sign Up</h1>
          <div className={styles.inputContainer}>
            <label htmlFor="email">Email</label>
            <input type="email" name="email" id="email" className={styles.input} 
              onChange={handleChange} onBlur={handleBlur} value={values.email} />
            {errors.email && touched.email && <span className={styles.error}>{errors.email}</span>}
          </div>
          <div className={styles.inputContainer}>
            <label htmlFor="first_name">First Name</label>
            <input type="text" name="first_name" id="first_name" className={styles.input} 
              onChange={handleChange} onBlur={handleBlur} value={values.first_name} />
            {errors.first_name && touched.first_name && <span className={styles.error}>{errors.first_name}</span>}
          </div>
          <div className={styles.inputContainer}>
            <label htmlFor="last_name">Last Name</label>
            <input type="text" name="last_name" id="last_name" className={styles.input} 
              onChange={handleChange} onBlur={handleBlur} value={values.last_name} />
            {errors.last_name && touched.last_name && <span className={styles.error}>{errors.last_name}</span>}
          </div>
          <div className={styles.inputContainer}>
            <label htmlFor="password">Password</label>
            <input type="password" name="password" id="password" className={styles.input} 
              onChange={handleChange} onBlur={handleBlur} value={values.password} />
            {errors.password && touched.password && <span className={styles.error}>{errors.password}</span>}
          </div>
          <div className={styles.inputContainer}>
            <button type="submit" className={styles.btn} ref={buttonRef}>Sign Up</button>
          </div>
        </form>
      </main>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

In this page, you use Formik and Yup to create and validate the form. This form has 4 fields: email, first name, last name, and password. These fields are required to sign up a customer in Medusa.

The important bit here is the part that uses the Medusa client in the onSubmit function passed to useFormik:

const client = createClient()

client.customers.create({
  email: values.email,
  first_name: values.first_name,
  last_name: values.last_name,
  password: values.password
}).then(({ customer }) => {
  setCustomer(customer);
})
Enter fullscreen mode Exit fullscreen mode

You first initialize the Medusa client. You use the utility function in utils/client.js to do that, but if you don’t have that in your storefront you can replace it with the initialization mentioned earlier in the tutorial.

Then, you use client.customers.create which will send a request to the create customer endpoint. This endpoint requires the email, first_name, last_name, and password fields.

If the sign up is successful, the customer object will be returned and a session token will be saved in the cookies to keep the user logged in. You use the customer object to set the customer in the context.

Notice also that at the beginning of the component you check if the customer is already logged in and redirect them to the home page in that case:

useEffect(() => {
  if (customer) {
    router.push("/")
  }
}, [customer, router])
Enter fullscreen mode Exit fullscreen mode

Sign In Page

Next, create the file pages/sign-in.js with the following content:

import * as Yup from 'yup';

import { useContext, useEffect, useRef } from 'react';

import StoreContext from '../context/store-context';
import { createClient } from "../utils/client"
import styles from "../styles/customer.module.css";
import { useFormik } from "formik";
import { useRouter } from 'next/router';

export default function SignIn() {
  const router = useRouter();
  const { setCustomer, customer } = useContext(StoreContext)
  useEffect(() => {
    if (customer) {
      router.push("/")
    }
  }, [customer, router])
  const buttonRef = useRef();
  const { handleSubmit, handleBlur, handleChange, values, touched, errors } = useFormik({
    initialValues: {
      email: '',
      password: ''
    },
    validationSchema: Yup.object().shape({
      email: Yup.string().email().required(),
      password: Yup.string().required()
    }),
    onSubmit: function (values) {
      if (buttonRef.current) {
        buttonRef.current.disabled = true;
      }

      const client = createClient()

      client.auth.authenticate({
        email: values.email,
        password: values.password
      }).then(({ customer }) => {
        setCustomer(customer);
      })
    }
  })
  return (
    <div className={styles.container}>
      <main className={styles.main}>
        <form onSubmit={handleSubmit} className={styles.signForm}>
          <h1>Sign In</h1>
          <div className={styles.inputContainer}>
            <label htmlFor="email">Email</label>
            <input type="email" name="email" id="email" className={styles.input} 
              onChange={handleChange} onBlur={handleBlur} value={values.email} />
            {errors.email && touched.email && <span className={styles.error}>{errors.email}</span>}
          </div>
          <div className={styles.inputContainer}>
            <label htmlFor="password">Password</label>
            <input type="password" name="password" id="password" className={styles.input} 
              onChange={handleChange} onBlur={handleBlur} value={values.password} />
            {errors.password && touched.password && <span className={styles.error}>{errors.password}</span>}
          </div>
          <div className={styles.inputContainer}>
            <button type="submit" className={styles.btn} ref={buttonRef}>Sign In</button>
          </div>
        </form>
      </main>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

This page also makes use of Formik and Yup to create and validate the form. This form only needs an email and a password.

The important bit is in the onSubmit function passed to useFormik:

const client = createClient()

client.auth.authenticate({
  email: values.email,
  password: values.password
}).then(({ customer }) => {
  setCustomer(customer);
})
Enter fullscreen mode Exit fullscreen mode

Just like before, you start by initializing the Medusa client. Then, you authenticate the customer using client.auth.authenticate which calls the Authenticate Customer endpoint. This endpoint requires 2 parameters: email and password.

If successful, a customer object is returned which you use to set the customer in the store context using setCustomer. A cookie will also be set to maintain the login session for the customer.

Add Links to Navigation Bar

Finally, in components/layout/nav-bar.jsx , initialize the customer from the StoreContext:

const { cart, customer } = useContext(StoreContext)
Enter fullscreen mode Exit fullscreen mode

Then, in the returned JSX add the following to add a link to the new pages:

{!customer && <Link href="/sign-up">Sign Up</Link>}
{!customer && <Link href="/sign-in">Sign In</Link>}
{customer && <Link href="/customer">Profile</Link>}
Enter fullscreen mode Exit fullscreen mode

Notice that you also add a link to the customer profile, which you’ll implement in the next section.

Test it Out

Make sure that the Medusa server is running first. Then, run the server for your storefront:

npm run dev
Enter fullscreen mode Exit fullscreen mode

If you open localhost:8000 now, you’ll see that there are 2 new links in the navigation bar for sign up and sign in.

Navigation Links

You can try clicking on Sign Up and registering as a new user.

Sign Up Page

Or click Sign In and log in as an existing user.

Sign In Page

Once you’re logged in, you should be redirected back to the home page and you should see a Profile link in the navigation bar.

Add Customer Profile

The customer profile will have 3 pages: Edit customer info, view orders, and view addresses.

Create Profile Layout

You’ll start by creating a layout that all profile pages will use.

Create components/layout/profile.jsx with the following content:

import { useContext, useEffect } from 'react';

import Link from 'next/link';
import StoreContext from '../../context/store-context';
import styles from "../../styles/customer.module.css";
import { useRouter } from 'next/router';

export default function Profile ({children, activeLink}) {
  const router = useRouter()
  const { customer } = useContext(StoreContext)
  useEffect(() => {
    if (!customer) {
      router.push('/')
    }
  }, [customer, router])

  return (
    <div className={styles.container}>
      <main className={styles.main}>
        <div className={styles.profile}>
          <div className={styles.profileSidebar}>
            <Link href="/customer">
              <a className={activeLink === 'customer' ? styles.active : ''}>Edit Profile</a>
            </Link>
            <Link href="/customer/orders">
              <a className={activeLink === 'orders' ? styles.active : ''}>Orders</a>
            </Link>
            <Link href="/customer/addresses">
              <a className={activeLink === 'addresses' ? styles.active : ''}>Addresses</a>
            </Link>
          </div>
          <div className={styles.profileContent}>
            {children}
          </div>
        </div>
      </main>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

This first checks the customer and context to see if the user is logged in. Then, it displays a sidebar with 3 links, and the main content displays the children.

Create Edit Profile Page

The edit profile page will be the main customer profile page. Create the file pages/customer/index.js with the following content:

import * as Yup from 'yup';

import { useContext, useRef } from 'react';

import Profile from '../../components/layout/profile';
import StoreContext from "../../context/store-context";
import { createClient } from "../../utils/client"
import styles from "../../styles/customer.module.css";
import { useFormik } from 'formik';

export default function CustomerIndex() {
  const { customer, setCustomer } = useContext(StoreContext)
  const buttonRef = useRef()
  const { handleSubmit, handleChange, handleBlur, values, errors, touched } = useFormik({
    initialValues: {
      email: customer?.email,
      first_name: customer?.first_name,
      last_name: customer?.last_name,
      password: ''
    },
    validationSchema: Yup.object().shape({
      email: Yup.string().email().required(),
      first_name: Yup.string().required(),
      last_name: Yup.string().required(),
      password: Yup.string()
    }),

    onSubmit: (values) => {
      buttonRef.current.disabled = true;

      const client = createClient()

      if (!values.password) {
        delete values['password']
      }

      client.customers.update(values)
        .then(({ customer }) => {
          setCustomer(customer)
          alert("Account updated successfully")
          buttonRef.current.disabled = false;
        })
    }
  })

  return (
    <Profile activeLink='customer'>
      <form onSubmit={handleSubmit}>
        <h1>Edit Profile</h1>
        <div className={styles.inputContainer}>
          <label htmlFor="email">Email</label>
          <input type="email" name="email" id="email" className={styles.input} 
            onChange={handleChange} onBlur={handleBlur} value={values.email} />
          {errors.email && touched.email && <span className={styles.error}>{errors.email}</span>}
        </div>
        <div className={styles.inputContainer}>
          <label htmlFor="first_name">First Name</label>
          <input type="text" name="first_name" id="first_name" className={styles.input} 
            onChange={handleChange} onBlur={handleBlur} value={values.first_name} />
          {errors.first_name && touched.first_name && <span className={styles.error}>{errors.first_name}</span>}
        </div>
        <div className={styles.inputContainer}>
          <label htmlFor="last_name">Last Name</label>
          <input type="text" name="last_name" id="last_name" className={styles.input} 
            onChange={handleChange} onBlur={handleBlur} value={values.last_name} />
          {errors.last_name && touched.last_name && <span className={styles.error}>{errors.last_name}</span>}
        </div>
        <div className={styles.inputContainer}>
          <label htmlFor="password">Password</label>
          <input type="password" name="password" id="password" className={styles.input} 
            onChange={handleChange} onBlur={handleBlur} value={values.password} />
          {errors.password && touched.password && <span className={styles.error}>{errors.password}</span>}
        </div>
        <div className={styles.inputContainer}>
          <button type="submit" className={styles.btn} ref={buttonRef}>Save</button>
        </div>
      </form>
    </Profile>
  )
}
Enter fullscreen mode Exit fullscreen mode

This is very similar to the sign-up form. You use Formik and Yup to create and validate the form. The form has 4 inputs: email, first_name, last_name, and password which is optional.

The important bit here is the part in the onSubmit function passed to useFormik:

const client = createClient()

if (!values.password) {
  delete values['password']
}

client.customers.update(values)
  .then(({ customer }) => {
    setCustomer(customer)
    alert("Account updated successfully")
    buttonRef.current.disabled = false;
  })
Enter fullscreen mode Exit fullscreen mode

Just like you’ve done before, you start by initializing the Medusa client. Then, if the password is not set, you remove it from the list of values since you’ll be passing it to the server as-is. You should only pass the password if the customer wants to change it.

To update the customer info, you can use client.customers.update which sends a request to the Update Customer endpoint. This endpoint accepts a few optional parameters including email, last_name, first_name, and password.

If the customer info is updated successfully, a customer object is returned which you use to set the updated customer in the context. You also show an alert to the customer that their account is now updated.

If you click on the Profile link in the navigation bar now, you’ll see the profile page with the sidebar and the edit profile page as the first page.

Profile Page

Try updating any of the information and click Save. You should then see an alert to let you know that it’s been updated successfully.

Update Alert

Orders Page

The next page you’ll create is an orders page which will display the customer’s orders.

Create a new file pages/customer/orders.js with the following content:

import { useEffect, useState } from 'react';

import Link from 'next/link';
import Profile from '../../components/layout/profile';
import { createClient } from "../../utils/client"
import { formatMoneyAmount } from '../../utils/prices';
import styles from "../../styles/customer.module.css";
import { useRouter } from 'next/router';

export default function Orders () {
  const [orders, setOrders] = useState([])
  const [pages, setPages] = useState(0)
  const router = useRouter()
  const p = router.query.p ? parseInt(router.query.p - 1) : 0

  useEffect(() => {
    const client = createClient()

    client.customers.listOrders({
      limit: 20,
      offset: 20 * p
    }).then((result) => {
      setOrders(result.orders)
      setPages(Math.ceil(result.count / result.limit))
    })
  }, [p])

  return (
    <Profile activeLink='orders'>
      <h1>Orders</h1>
      <table className={styles.table}>
        <thead>
          <tr>
            <th>ID</th>
            <th>Total</th>
            <th>Status</th>
          </tr>
        </thead>
        <tbody>
          {orders.map((order) => (
            <tr key={order.id}>
              <td>{order.id}</td>
              <td>{formatMoneyAmount({
                currencyCode: order.currency_code,
                amount: order.total
              }, 2)}</td>
              <td>{order.status}</td>
            </tr>
          ))}
        </tbody>
      </table>
      <div className={styles.pagination}>
        {pages > 0 && p > 1 && (
          <Link href={`/customer/orders?p=${p - 1}`}>Previous</Link>
        )}
        {pages > 1 && p > 0 && p < pages && <span> - </span>}
        {pages > 1 && (p + 1) < pages && (
          <Link href={`/customer/orders?p=${p + 1}`}>Next</Link>
        )}
      </div>
    </Profile>
  )
}
Enter fullscreen mode Exit fullscreen mode

You first create an orders state variable which starts out as empty. You also have a pages state variable to keep track of the number of pages available. Then, in useEffect, you retrieve the orders using the Medusa client:

useEffect(() => {
  const client = createClient()

  client.customers.listOrders({
    limit: 20,
    offset: 20 * p
  }).then((result) => {
    setOrders(result.orders)
    setPages(Math.ceil(result.count / result.limit))
  })
}, [p])
Enter fullscreen mode Exit fullscreen mode

After initializing the client, you retrieve the orders of the customer using client.customers.listOrders. This method sends a request to the Retrieve Customer Orders endpoint. This endpoint accepts 4 fields: limit, offset, fields, and expand. Here, you just use limit and offset.

limit indicates how many orders should be retrieved per page, and offset indicates how many orders to skip from the beginning to get the orders of the current page.

This request returns the list of orders as well as additional fields important for pagination including count which indicates the total count of the orders and limit which is the current limit set.

You set the orders state variable to the orders returned from the method and you set the number of pages based on the count and limit fields.

Finally, you display the orders in a table showing the ID, total, and status. You also show pagination links for “Previous” and “Next” if applicable for the page. This is done for the simplicity of the tutorial.

If you open the orders page now, you’ll see the list of orders for your customer if they have any.

Orders List

Addresses Page

The last page you’ll create in the profile is the Addresses page which will allow the customer to see their shipping addresses.

Create the file pages/customer/addresses.js with the following content:

import Profile from "../../components/layout/profile"
import StoreContext from "../../context/store-context"
import styles from "../../styles/customer.module.css"
import { useContext } from 'react'

export default function Addresses() {
  const { customer } = useContext(StoreContext)

  return (
    <Profile activeLink='addresses'>
      <h1>Addresses</h1>
      {customer && customer.shipping_addresses.length === 0 && <p>You do not have any addresses</p>}
      {customer && customer.shipping_addresses.map((address) => (
        <div key={address.id} className={styles.address}>
          <span><b>First Name:</b> {address.first_name}</span>
          <span><b>Last Name:</b> {address.last_name}</span>
          <span><b>Company:</b> {address.company}</span>
          <span><b>Address Line 1:</b> {address.address_1}</span>
          <span><b>Address Line 2:</b> {address.address_2}</span>
          <span><b>City:</b> {address.city}</span>
          <span><b>Country:</b> {address.country}</span>
        </div>
      ))}
    </Profile>
  )
}
Enter fullscreen mode Exit fullscreen mode

You use the shipping_addresses field in the customer object stored in the context and display the addresses one after the other. You can also access the billing address if the customer has any using customer.billing_address.

If you go to the Addresses page now, you’ll see the customer’s shipping addresses listed.

Addresses Page

What’s Next

Using the Medusa JS Client, you can easily interact with the APIs to create an even better customer experience in your storefront.

This tutorial did not cover all the aspects of a profile for simplicity. Here’s what else you can implement:

  1. View Single Orders: You can use the orders object you already retrieved on the orders page to show the information of a single order or you can use the Retrieve Order endpoint which is accessible in the client under client.orders.retrieve.
  2. Address Management: You can use the Update Shipping Address endpoint accessible in the client under client.customers.addresses.updateAddress; you can use the Add Shipping Address endpoint which is accessible in the client under client.customers.addresses.addAddress; and you can use the Delete Shipping Address endpoint which is accessible in the client under client.customers.addresses.deleteAddress.
  3. Saved Payment Methods: You can retrieve the saved payment methods if you use a payment provider that supports saving payment methods like Stripe using the Retrieve Saved Payment Methods endpoint accessible in the client under client.customers.paymentMethods.list.
  4. Reset Password: You can add reset password functionality using the Create Reset Password Token endpoint accessible in the client under client.customers.generatePasswordToken, then you can reset the password using the Reset Customer Password endpoint accessible in the client under client.customers.resetPassword.

You can check out the API documentation for a full list of endpoints to see what more functionalities you can add to your storefront.

Should you have any issues or questions related to Medusa, then feel free to reach out to the Medusa team via Discord.

Top comments (0)