DEV Community

Cover image for Build Marketplace App with Stripe Billing
Kartik Grewal for Canonic Inc.

Posted on • Updated on

Build Marketplace App with Stripe Billing

Introduction

As a result of being home bound amidst the pandemic, public interest in marketplace apps has skyrocketed as it became the primary place for people to buy products. The marketplace model explains why online marketplaces are so popular and beneficial. It can be built for any market and niche with no initial inventory required and can grow exponentially, with benefits to all - sellers, buyers, and of course the marketplace owners.

To sell products online without a lot of marketing effort is appealing to sellers. Marketplace app is much easier and faster than creating your own e-commerce website/app and then struggling to get traffic.

It also makes it convenient for customers to buy products because of the wide selection and prices available, as well as the ability to compare products.

And owning a marketplace provides you with many effective ways to generate revenue. Advertising, charging a percentage for each deal, and a lot more. So, let’s take a deep dive and know how to build one yourself!

Image

Let’s get started

  • Setting up the React

Let’s begin by creating a boilerplate using the creact-react-app

npx create-react-app marketplace
Enter fullscreen mode Exit fullscreen mode
  • Install Material UI

We will be using Material UI to style the frontend that way we don’t end up writing the CSS

cd marketplace
npm install @mui/material @emotion/react @emotion/styled
Enter fullscreen mode Exit fullscreen mode
  • Create a Product component

Create a directory inside src and name it components where we'll be place all the different components we are gonna build!

Create another directory inside of it and name it Product. Create a Product.js inside the Product directory. This component will display the list of products we have. At first, we will be looping through a static list of products

import React from "react";
import {
  Box,
  Card,
  CardActions,
  CardContent,
  Button,
  Typography,
  Rating,
} from "@mui/material";

const Products = [
  {
    title: "Oculus Quest All-in-one VR Gaming Headset",
    price: "11.96",
    rating: 5,
  },
  {
    title: "Nintendo Switch with Neon Blue and Neon Red Joy‑Con",
    price: "15.96",
    rating: 3,
  },
  {
    title: "Mass Effect",
    price: "23",
    rating: 5,
  },
  {
    title: "The LeanStartup2: How constant innovative creators",
    price: "9.96",
    rating: 2,
  },
  {
    title: "Dual Shock Controller",
    price: "19.96",
    rating: 5,
  },
];

function Product() {
  return (
    <Box
      sx={{
        display: "flex",
        flexDirection: "row",
        justifyContent: "space-between",
        alignItems: "start",
      }}
    >
      {Products.map((product) => (
        <Card
          sx={{
            maxHeight: 450,
            minWidth: 100,
            width: "25%",
            margin: "1.5rem",
            display: "flex",
            flexDirection: "column",
            justifyContent: "space-between",
            alignItems: "start",
          }}
        >
          <CardContent>
            <Typography gutterBottom variant="h5" component="div">
              {product.title}
            </Typography>
            <Typography gutterBottom variant="h5" component="div">
              ${product.price}
            </Typography>
            <Rating name="read-only" value={product.rating} readOnly />
          </CardContent>
          <CardActions>
            <Button variant="contained" size="small">
              Buy now
            </Button>
          </CardActions>
        </Card>
      ))}
    </Box>
  );
}

export default Product;
Enter fullscreen mode Exit fullscreen mode
  • Create a Header component.

Create a directory inside components and name it header, and create a file inside it and name it Header.js. This file will hold the header where we will display the app’s name and later on user sign in/sign out button.

import React from "react";

import Product from "./components/product/Product";
import Header from "./components/header/Header";

function App() {
  return (
    <>
      <Header />
      <Product />
    </>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

It should look something like this:

Image description

  • Time to get the API ready!

Getting your APIs for the Marketplace app comes at real ease. All you have to do is clone this production-ready project on Canonic, and you're done. It will provide you with the backend, APIs, and documentation you need for integration, without writing any code. Just make sure you add your own callback URI, client ID and secret for your 0auth provider

Image description

You will have to add CMS entries for the Products as cloning a project does not clone CMS entries.


Backend integration with GraphQL

Let's now integrate! Now that we have our APIs ready, let's move on by installing GraphQL packages.

  • Install GraphQL packages

To pull our data from the backend, we will need two packages - Apollo Client and GraphQL

npm i @apollo/client graphql
Enter fullscreen mode Exit fullscreen mode
  • Configure GraphQL to communicate with the backend

Configure the Apollo Client in the project directory, inside index.js configure your apollo client so it would communicate with the backend.

Note to replace the uri with the one you'll get from Canonic.

        import React from "react";
        import ReactDOM from "react-dom";
        import { ApolloClient, InMemoryCache, ApolloProvider } from "@apollo/client";

        import App from "./App";

        const client = new ApolloClient({
          uri: "https://marketplace-app.can.canonic.dev/graphql", //You can replace this with your URI
          cache: new InMemoryCache(),
        });

        ReactDOM.render(
          <React.StrictMode>
            <ApolloProvider client={client}>
              <App />
            </ApolloProvider>
          </React.StrictMode>,
          document.getElementById("root")
        );
Enter fullscreen mode Exit fullscreen mode
  • Query the data

For querying the data, we will create a directory inside src called gql and create a file inside it called query.js. In it will write all the data we need from the backend.

        import { gql } from "@apollo/client";

        export const LOGIN_WITH_GOOGLE = gql`
          query {
            getLoginUrlsForLogin {
              GOOGLE
            }
          }
        `;

        export const GET_PRODUCTS = gql`
          query {
            products {
              title
              _id
              price
              ratings
              priceApiId
              image {
                url
                name
              }
            }
          }
        `;
Enter fullscreen mode Exit fullscreen mode
  • Setting up login with Google 0auth

Let’s go back to the Header component, we will be importing the query we declared in previous step and holding it in data variable we get from useQuery hook by Apollo

        import { useQuery } from "@apollo/client";
        import { LOGIN_WITH_GOOGLE } from "../../gql/query";

            const { data, loading: loginDataLoading } = useQuery(LOGIN_WITH_GOOGLE)
Enter fullscreen mode Exit fullscreen mode

We will add the following JSX to display the login button. data?.getLoginUrlsForLogin?.GOOGLE In here, the GOOGLE object contains login and callback links to let user login themeselves in.

        <Box sx={{ flexGrow: 0 }}>
          <Tooltip title="Account">
             {loginDataLoading && <CircularProgress color="secondary" />}
                <a href={data?.getLoginUrlsForLogin?.GOOGLE}>
                    <Button variant="contained" startIcon={<GoogleIcon />}>
                       <span sx={{ textDecoration: "none" }}>Login</span>
                    </Button>
                </a>
        </Box>
Enter fullscreen mode Exit fullscreen mode

After adding these, the Header component should look something like this:

        import React from "react";
        import { useQuery } from "@apollo/client";
        import {
          AppBar,
          Box,
          Toolbar,
          Typography,
          Container,
          Button,
          Tooltip,
          CircularProgress,
        } from "@mui/material";
        import GoogleIcon from "@mui/icons-material/Google";

        import { LOGIN_WITH_GOOGLE } from "../../gql/query";

        function Header() {
          const { data, loading: loginDataLoading } = useQuery(LOGIN_WITH_GOOGLE); 
          return (
            <AppBar position="static">
              <Container maxWidth="xl">
                <Toolbar disableGutters sx={{ justifyContent: "space-between" }}>
                  <Typography
                    variant="h6"
                    noWrap
                    component="div"
                    sx={{ mr: 2, display: { xs: "none", md: "flex" } }}
                  >
                    Marketplace
                  </Typography>
                  <Box sx={{ flexGrow: 0 }}>
                    <Tooltip title="Account">
                      {loginDataLoading && <CircularProgress color="secondary" />}
                      <a href={data?.getLoginUrlsForLogin?.GOOGLE}>
                        <Button variant="contained" startIcon={<GoogleIcon />}>
                          <span sx={{ textDecoration: "none" }}>Login</span>
                        </Button>
                      </a>
                    </Tooltip>
                  </Box>
                </Toolbar>
              </Container>
            </AppBar>
          );
        }

        export default Header;
Enter fullscreen mode Exit fullscreen mode
  • Getting logged in user’s status.

We are not yet done with the Header component, now that we have added a feature of letting the user sign themselves in on the app, we need to retrieve and store their information, so we could identify whether they are logged in or not, we can even use this persists their session, we also get some useful information such as first name, last name, email ID and all that good stuff. Let’s set up for it, first we have to create a mutation. Create a file called mutation.js inside the gql directory

        import { gql } from "@apollo/client";

        export const LOGIN_WITH_GOOGLE_MUTATION = gql`
          mutation Login($code: String!, $service: String!) {
            loginForLogin(code: $code, service: $service) {
              token
              user {
                email
                firstName
                lastName
                avatar {
                  url
                }
              }
            }
          }
        `;
Enter fullscreen mode Exit fullscreen mode

Now that our mutation is ready, we can call that inside our Header component, let’s go back to it. The mutation would look like this:

        import { useMutation } from "@apollo/client";

        const [loginMutation, { data: mutationData }] = useMutation(LOGIN_WITH_GOOGLE_MUTATION);
        const urlCode = new URLSearchParams(window.location.search).get("code"); //We receive a code after a successful sign in, here pulling that code from the URL
            if (urlCode) {
              loginMutation({ variables: { code: urlCode, service: "GOOGLE" } });
                }
Enter fullscreen mode Exit fullscreen mode

We receive a code from our provider be it, Google, Facebook or GitHub after the user successfully sign ins. We can get that code using this call to windows object URLSearchParams(window.location.search).get("code") and once we store that code we could just pass it in as a variable to the mutation. In service, you should write which ever service you are using, here this app just uses Google, so we added Google statically.

We will pass in this mutation inside a useEffect hook with empty array as a dependency. We’ll also need a state to hold this data we get back from mutation, so it could be used elsewhere, and while we are at it, we can store the token we receive from the mutation inside local storage, so we can persist the user login session.

        const [accessToken, setAccessToken] = useState();
        const [isLoggedIn,setIsLoggedIn] = useState()

        useEffect(() => {
            const urlCode = new URLSearchParams(window.location.search).get("code");
            if (urlCode) {
              loginMutation({ variables: { code: urlCode, service: "GOOGLE" } }); 
          }, []);

        useEffect(() => {
            setAccessToken(mutationData?.loginForLogin?.token);
            setIsLoggedIn(mutationData?.loginForLogin?.user);
            if (accessToken) localStorage.setItem("_id", accessToken);
          }, [accessToken, mutationData, setIsLoggedIn]);

Enter fullscreen mode Exit fullscreen mode

But, we will need this isLoggedIn state elsewhere as well, so a better option is to move this to App.js since it is a parent to all the component, we’ll let Header component receive it as props. So the App.js will look like this:

        import React, { useState } from "react";

        import Header from "./components/header/Header";
        import Product from "./components/product/Product";

        function App() {
          const [isLoggedIn, setIsLoggedIn] = useState(); 

          return (
            <>
              {
                <div className="App">
                  <Header setIsLoggedIn={setIsLoggedIn} isLoggedIn={isLoggedIn} />
                  <Product />
                </div>
              }
            </>
          );
        }

        export default App;
Enter fullscreen mode Exit fullscreen mode

We will also add a way, so user can sign out. For that we will be using Material UI’s components, after adding all that UI enhancements, the Header component will look like this

        import React, { useEffect, useState, useCallback } from "react";
        import { useQuery, useMutation } from "@apollo/client";
        import {
          AppBar,
          Box,
          Toolbar,
          IconButton,
          Typography,
          Menu,
          Container,
          Avatar,
          Button,
          Tooltip,
          MenuItem,
          CircularProgress,
        } from "@mui/material";
        import GoogleIcon from "@mui/icons-material/Google";

        import { LOGIN_WITH_GOOGLE } from "../../gql/query";
        import { LOGIN_WITH_GOOGLE_MUTATION } from "../../gql/mutation";

        function Header({ setIsLoggedIn, isLoggedIn }) {
          const [accessToken, setAccessToken] = useState();
          const { data, loading: loginDataLoading } = useQuery(LOGIN_WITH_GOOGLE);
          const [loginMutation, { data: mutationData }] = useMutation(
            LOGIN_WITH_GOOGLE_MUTATION
          );
          useEffect(() => {
            const urlCode = new URLSearchParams(window.location.search).get("code"); 
            if (urlCode) {
              loginMutation({ variables: { code: urlCode, service: "GOOGLE" } }); 
            }
          }, []);
          useEffect(() => {
            setAccessToken(mutationData?.loginForLogin?.token);
            setIsLoggedIn(mutationData?.loginForLogin?.user);
            if (accessToken) localStorage.setItem("_id", accessToken); 
          }, [accessToken, mutationData, setIsLoggedIn]);

          const [anchorElNav, setAnchorElNav] = React.useState(null);
          const [anchorElUser, setAnchorElUser] = React.useState(null);

          const handleOpenUserMenu = useCallback((event) => {
            setAnchorElUser(event.currentTarget);
          });

          const handleCloseNavMenu = useCallback(() => {
            setAnchorElNav(null);
          });

          const handleCloseUserMenu = useCallback(() => {
            setAnchorElUser(null);
          });

          const onLogout = useCallback(() => {
            localStorage.removeItem("_id"); 
          });

          return (
            <AppBar position="static">
              <Container maxWidth="xl">
                <Toolbar disableGutters sx={{ justifyContent: "space-between" }}>
                  <Typography
                    variant="h6"
                    noWrap
                    component="div"
                    sx={{ mr: 2, display: { xs: "none", md: "flex" } }}
                  >
                    Marketplace
                  </Typography>

                  <Box sx={{ flexGrow: 0 }}>
                    <Tooltip title="Account">
                      {loginDataLoading ? (
                        <CircularProgress color="secondary" />
                      ) : !isLoggedIn && !localStorage.getItem("_id") ? ( 
                        <a href={data?.getLoginUrlsForLogin?.GOOGLE}>
                          <Button variant="contained" startIcon={<GoogleIcon />}>
                            <span sx={{ textDecoration: "none" }}>Login</span>
                          </Button>
                        </a>
                      ) : (
                        <IconButton onClick={handleOpenUserMenu} sx={{ p: 0 }}>
                          {isLoggedIn?.avatar?.url ? (
                            <Avatar alt="User" src={isLoggedIn.avatar.url} />
                          ) : (
                            <Avatar src="/broken-image.jpg" />
                          )}
                        </IconButton>
                      )}
                    </Tooltip>
                    <Menu
                      sx={{ mt: "45px" }}
                      id="menu-appbar"
                      anchorEl={anchorElUser}
                      anchorOrigin={{
                        vertical: "top",
                        horizontal: "right",
                      }}
                      keepMounted
                      transformOrigin={{
                        vertical: "top",
                        horizontal: "right",
                      }}
                      open={Boolean(anchorElUser)}
                      onClose={handleCloseUserMenu}
                    >
                      <MenuItem onClick={handleCloseNavMenu}>
                        <Typography textAlign="center">
                          <a
                            onClick={onLogout}
                            href={`https://www.google.com/accounts/Logout?continue=https://appengine.google.com/_ah/logout?continue=${window.origin}`}
                          >
                            Logout
                          </a>
                        </Typography>
                      </MenuItem>
                    </Menu>
                  </Box>
                </Toolbar>
              </Container>
            </AppBar>
          );
        }

        export default Header;
Enter fullscreen mode Exit fullscreen mode
  • Finish the Product component

We will now shift our focus to Product component. To maintain component simplicity so, one component doesn’t get too complicated, we will create a Home component that would act as a parent Product components. Let’s set that up by creating a directory inside component and calling it Home and creating a Home.js file inside it.

        import React, { useMemo } from "react";
        import { useQuery } from "@apollo/client";
        import { Box, CircularProgress } from "@mui/material";

        import Product from "../product/Product.js";
        import { GET_PRODUCTS } from "../../gql/query";

        function Home() {
          const { data, loading: productsLoading } = useQuery(GET_PRODUCTS); 
          const products = useMemo(() => data?.products || [], [data?.products]); 

          return (
            <Box
              sx={{
                display: "flex",
                flexDirection: "row",
                flexWrap: "wrap",
                gap: "4rem",
                marginTop: "4rem",
              }}
            >
              {productsLoading && (
                <CircularProgress sx={{ position: "absolute", left: "50%" }} />
              )}
              {products.map((item, i) => {
                return (
                  <Product
                    key={i}
                    id={item.id}
                    title={item.title}
                    image={item.image.url}
                    price={item.price}
                    rating={item.ratings}
                    price_api={item.priceApiId}
                  />
                );
              })}
            </Box>
          );
        }

        export default Home;
Enter fullscreen mode Exit fullscreen mode

Now that we are receiving the data dynamically, we can finally toss the static array of data from Product.js, let’s get to it.

        import React from "react";
        import {
          Card,
          CardContent,
          CardMedia,
          Typography,
          Rating,
        } from "@mui/material";

        function Product({ title, price, rating, image, price_api }) {
          return (
            <Card
              sx={{
                maxHeight: 450,
                minWidth: 100,
                width: "25%",
                margin: "1.5rem",
                display: "flex",
                flexDirection: "column",
                justifyContent: "space-between",
                alignItems: "start",
              }}
            >
              <CardMedia
                component="img"
                alt="title"
                height="auto"
                image={image}
                sx={{ objectFit: "contain", maxHeight: "200px" }}
              />
              <CardContent>
                <Typography gutterBottom variant="h5" component="div">
                  {title}
                </Typography>
                <Typography gutterBottom variant="h5" component="div">
                  ${price}
                </Typography>
                <Rating name="read-only" value={rating} readOnly />
              </CardContent>
            </Card>
          );
        }

        export default Product;
Enter fullscreen mode Exit fullscreen mode

We are almost the completion here, just need to Stripe’s client only checkout

For that, first you need to have an account on Stripe, after that you should have Stripe’s API key. You can get your keys here. Then we will install stripe

        npm install --save @stripe/react-stripe-js @stripe/stripe-js
Enter fullscreen mode Exit fullscreen mode

Now that all these is done, let’s hop back into Home component. We will create an asynchronous function let’s name it handle click since it will be handle click from a ‘Buy now’ button.

        import { loadStripe } from "@stripe/stripe-js";

        const stripePromise = loadStripe(process.env.REACT_APP_STRIPE_API);
        const handleClick = async () => {
              setLoading(true);
              const stripe = await stripePromise;
              const { error } = await stripe.redirectToCheckout({
                lineItems: [
                  {
                    price: price_api, 
                    quantity: 1,
                  },
                ],
                mode: "payment",
                cancelUrl: "https://canonic-marketplace.netlify.app/", 
                successUrl: "https://canonic-marketplace.netlify.app/", 
              if (error) {
                setLoading(false);
                console.log("The error ", error);
              }
          };
Enter fullscreen mode Exit fullscreen mode

Instead of process.env.REACT_APP_STRIPE_API you can add your own API key, you can replace cancelUrl and successUrl with your own URL as well. In here we are using price_api as a value to price key. In stripe every product has a unique product price ID I have stored all my product price ID on Canonic CMS, you thus using it as a value.

In your own project’s CMS you can add your own Product’s price ID in the price_api field.

Last bit of validation we can is to check if the user is signed in or not before proceeding, since we have isLoggedIn inside the App component, we can easily get it through pass it to Home components

        import React, { useState } from "react";

        import Header from "./components/header/Header";
        import Home from "./components/home/Home";

        function App() {
          const [isLoggedIn, setIsLoggedIn] = useState(); 

          return (
            <>
              {
                <div className="App">
                  <Header setIsLoggedIn={setIsLoggedIn} isLoggedIn={isLoggedIn} />
                  **<Home isLoggedIn={isLoggedIn} />**
                </div>
              }
            </>
          );
        }

        export default App;
Enter fullscreen mode Exit fullscreen mode

We will have to pass in handleClick function along with loading state, we are using loading state to disable to ‘Buy now’ button after user click on it, so there won’t be multiple call to Stripe.

        import React, { useState, useEffect, useMemo } from "react";
        import {  useQuery } from "@apollo/client";
        import { loadStripe } from "@stripe/stripe-js";
        import { Box, CircularProgress } from "@mui/material";

        import Product from "../product/Product.js";
        import { GET_PRODUCTS } from "../../gql/query";

        function Home({ **isLoggedIn** }) {
          const { data, loading: productsLoading } = useQuery(GET_PRODUCTS); 
          const products = useMemo(() => data?.products || [], [data?.products]);
          const stripePromise = loadStripe(process.env.REACT_APP_STRIPE_API); 
          const [loading, setLoading] = useState();

          const handleClick = async (price_api, title) => {

            if (isLoggedIn) {
              setLoading(true);
              const stripe = await stripePromise;
              const { error } = await stripe.redirectToCheckout({
                lineItems: [
                  {
                    price: price_api, 
                    quantity: 1,
                  },
                ],
                mode: "payment",
                cancelUrl: window.origin,
                successUrl: window.origin + `?session_id=${title}`,
              });
              if (error) {
                setLoading(false);
              }
            } else alert("Please log in to continue");
          };

          return (
            <Box
              sx={{
                display: "flex",
                flexDirection: "row",
                flexWrap: "wrap",
                gap: "4rem",
                marginTop: "4rem",
              }}
            >
              {productsLoading && (
                <CircularProgress sx={{ position: "absolute", left: "50%" }} />
              )}
              {products.map((item, i) => {
                return (
                  <Product
                    key={i}
                    id={item.id}
                    title={item.title}
                    image={item.image.url}
                    price={item.price}
                    rating={item.ratings}
                    price_api={item.priceApiId}
                    **handleClick={handleClick}
                    loading={loading}**
                  />
                );
              })}
            </Box>
          );
        }

        export default Home;
Enter fullscreen mode Exit fullscreen mode

Product component is now receiving props of handleClick and loading, we can finally create a Buy now button

        import React from "react";
        import {
          Card,
          CardActions,
          CardContent,
          CardMedia,
          Button,
          Typography,
          Rating,
        } from "@mui/material";

        function Product({
          title,
          price,
          rating,
          image,
          price_api,
          handleClick,
          loading,
        }) {
          return (
            <Card
              sx={{
                maxHeight: 450,
                minWidth: 100,
                width: "25%",
                margin: "1.5rem",
                display: "flex",
                flexDirection: "column",
                justifyContent: "space-between",
                alignItems: "start",
              }}
            >
              <CardMedia
                component="img"
                alt={title}
                height="auto"
                image={image}
                sx={{ objectFit: "contain", maxHeight: "200px" }}
              />
              <CardContent>
                <Typography gutterBottom variant="h5" component="div">
                  {title}
                </Typography>
                <Typography gutterBottom variant="h5" component="div">
                  ${price}
                </Typography>
                <Rating name="read-only" value={rating} readOnly />
              </CardContent>
              <CardActions>
                <Button
                  variant="contained"
                  size="small"
                  onClick={() => handleClick(price_api, title)} 
                  disabled={loading}
                >
                  Buy now
                </Button>
              </CardActions>
            </Card>
          );
        }

        export default Product;
Enter fullscreen mode Exit fullscreen mode

And, with this we can conclude our frontend.


Bonus! - Sending email and Slack notification

We can add webhooks to perform actions such as send a Slack notification whenever a product is purchased or send an email notification. Let’s see how that done.

  • Configuring the webhooks

Open your cloned project on Canonic, navigate to API and choose Notify table. Click on createNotify endpoint there you can see Message webhook of Slack, you can click on it and replace trigger URL and message body with your own trigger URL and message body. Chained to that you will find email webhook of Canonic, there as well, you can replace email subject and email body with your content.

Screenshot 2021-12-21 at 8.04.17 PM.png

  • Configuring frontend

In order to trigger these webhooks, we need to make a mutation. Let’s begin by declaring a mutation in our mutation.js file

            export const NOTIFY = gql`
              mutation Notify($title: String!) {
                createNotify(input: { title: $title }) {
                  title
                }
              }
            `;
Enter fullscreen mode Exit fullscreen mode

Final file would look like this:

            import { gql } from "@apollo/client";

            export const LOGIN_WITH_GOOGLE_MUTATION = gql`
              mutation Login($code: String!, $service: String!) {
                #This mutation is used to get logged in user's details
                loginForLogin(code: $code, service: $service) {
                  #We feed in code which we get after user successfully signs in, services are the 0auth services we are using such as Google,Github and Facebook.
                  token
                  user {
                    email
                    firstName
                    lastName
                    avatar {
                      url
                    }
                  }
                }
              }
            `;

            export const NOTIFY = gql`
              mutation Notify($title: String!) {
                createNotify(input: { title: $title }) {
                  title
                }
              }
            `;
Enter fullscreen mode Exit fullscreen mode

We have to trigger this mutation from Home component. Let’s go back to the Home component, there we will modify successUrl in handleClick function to include the title of the product in URL whenever a product is checked out successfully

            const handleClick = async (price_api, title) => {

                if (isLoggedIn) {
                  setLoading(true);
                  const stripe = await stripePromise;
                  const { error } = await stripe.redirectToCheckout({
                    lineItems: [
                      {
                      price: price_api, 
                        quantity: 1,
                      },
                    ],
                    mode: "payment",
                    cancelUrl: window.origin,
                    successUrl: window.origin + `?session_id=${title}`,
                  });
                  if (error) {
                    setLoading(false);
                  }
                } else alert("Please log in to continue");
              };
Enter fullscreen mode Exit fullscreen mode

And, add useEffect to with empty dependency array, to check if URL has the product’s title

            useEffect(() => {
                const hasSuccessUrl = new URLSearchParams(window.location.search).get(
                  "session_id"
                ); 
                if (hasSuccessUrl) {
                 //Do something
                }
              }, []);
Enter fullscreen mode Exit fullscreen mode

Now that everything is set up we can trigger the mutation

            import { useMutation } from "@apollo/client";
            import { NOTIFY } from "../../gql/mutation";

            const [notify] = useMutation(NOTIFY);

            useEffect(() => {
                const hasSuccessUrl = new URLSearchParams(window.location.search).get(
                  "session_id"
                ); 
                if (hasSuccessUrl) {
                  notify({ variables: { title: hasSuccessUrl } });
                }
              }, []);
Enter fullscreen mode Exit fullscreen mode

So the final file would look like this


import React, { useState, useEffect, useMemo } from "react";
import { useMutation, useQuery } from "@apollo/client";
import { NOTIFY } from "../../gql/mutation";
import { loadStripe } from "@stripe/stripe-js";
import { Box, CircularProgress } from "@mui/material";

import Product from "../product/Product.js";
import { GET_PRODUCTS } from "../../gql/query";

function Home({ isLoggedIn }) {
  const { data, loading: productsLoading } = 
  useQuery(GET_PRODUCTS); 
  const products = useMemo(() => data?.products || [], [data?.products]); 
  const stripePromise = loadStripe(process.env.REACT_APP_STRIPE_API); 
  const [loading, setLoading] = useState(); 
  const [notify] = useMutation(NOTIFY);

  const handleClick = async (price_api, title) => {
                if (isLoggedIn) {
                  setLoading(true);
                  const stripe = await stripePromise;
                  const { error } = await stripe.redirectToCheckout({
                    lineItems: [
                      {
                        price: price_api, 
                        quantity: 1,
                      },
                    ],
                    mode: "payment",
                    cancelUrl: window.origin,
                    successUrl: window.origin + `?session_id=${title}`, 
                  });

                  if (error) {
                    setLoading(false);
                  }
                } else alert("Please log in to continue");
              };

              useEffect(() => {
                const hasSuccessUrl = new URLSearchParams(window.location.search).get(
                  "session_id"
                ); 
                if (hasSuccessUrl) {
                  notify({ variables: { title: hasSuccessUrl } });
                }
              }, []);
              return (
                <Box
                  sx={{
                    display: "flex",
                    flexDirection: "row",
                    flexWrap: "wrap",
                    gap: "4rem",
                    marginTop: "4rem",
                  }}
                >
                  {productsLoading && (
                    <CircularProgress sx={{ position: "absolute", left: "50%" }} />
                  )}
                  {products.map((item, i) => {
                    return (
                      <Product
                        key={i}
                        id={item.id}
                        title={item.title}
                        image={item.image.url}
                        price={item.price}
                        rating={item.ratings}
                        price_api={item.priceApiId}
                        handleClick={handleClick}
                        loading={loading}
                      />
                    );
                  })}
                </Box>
              );
            }

  export default Home;

Enter fullscreen mode Exit fullscreen mode

Once you've reached here, you've successfully completed the Marketplace project.

You can clone this sample project here. We hope this guide helped you to create your own Marketplace app; if you do, then share it with us in our Discord community.If you are not already a member, join us, and let's build together.

Connect on Discord.If you'd like to see our other guides, they're all here.For any support requests, write to us at support@canonic.dev. Check out our website to know more about Canonic.

Top comments (0)