🌟 Help others discover us, star our GitHub repo! 🌟

Mobile Menu

Build an E-commerce Website with Webiny Serverless Headless CMS, Next.js, and Stripe

Albiona HotiTwitter
November 16, 2020

In this tutorial, we will create a simple e-commerce website, where you can buy Swag from the best Open Source projects such as Webiny, Next.js, React, etc.

Before we continue, let's go through what you'll learn about building this website.

You will learn how to create the back-end using Webiny Headless CMS and set up two content models, the products, and the categories. Afterward, we'll fetch the data from the Headless CMS to the Next.js project using Apollo GraphQL. Last but not least, we'll integrate Stripe to implement a checkout experience.

While working on this starter, I shared my progress on Twitter, and Josh from Stripe reached out for being available for any feedback regarding improvements, developer experience, and documentation. Thank you, Josh!

Without further ado let's get started.

First, let's take a look at the diagram of what we'll build ⬇️

E commerce starter

1. the E-Commerce-Starter

To get started, we'll clone the e-commerce-starter git repository.

The starter will have a ready-made Next.js application, and the Ant Design UI Library. We already have some components and functionalities ready, such as the Header, Search, Product List, the Product card, and the Cart component. Check out the image below.

E commerce starter

Clone the e-commerce-starter project by running the following commands to have the project set up and running.

git clone https://github.com/webiny/e-commerce-starter cd e-commerce-starter yarn install yarn run dev // head over to localhost:3000
Note

For the e-commerce starter project we use static data, throughout the tutorial we will update the project so the data is pulled from the Headless CMS via the GraphQL API.

Now that we have the starter project, we can focus on creating the back-end using Webiny Headless CMS.

2. Webiny Headless CMS

Prerequisites

  • A Webiny Headless CMS Project

    First of all, make sure you have a working Webiny project set up.

  • Content Delivery API URL

    The Headless CMS app exposes data via the Content Delivery API, which is a simple GraphQL API that dynamically updates its schema on content model changes. Once you have deployed your API stack (using the yarn webiny deploy api --env=local command), you should be able to find the Content Delivery API URL in the API Information menu item, in your admin app.

    Admin App

  • Content Delivery API Access Token

    In order to access the data via the Content Delivery API, we'll need a valid Access Token.

    1. Follow the link here to create an access token for a specific environment.
Warning

In the root of the e-commerce-starter app that you've set up, you will find the .env file, that's the place where you need to save the Content Delivery API, and the Content Delivery API Access Token | Make sure to set up your .env file before proceeding to the next steps.

Now that we have all of the prerequisites out of the way, it's time to create the content models for the e-commerce website.

We are going to create two content models, the products and category. First, you have to run your Headless CMS backend project, then open the admin menu and head over to the Content Models as shown in the image below.

Content models admin menu

Click on the Models menu item, and from there you'll create the content models by clicking on the plus button on the right corner on the bottom.

The images below will guide you through the fields we'll use to create our content models.

The Category content model.

Category content model Comment: Provide a screenshot of the menu of content models.

The Category content model will contain the fields such as:

  • Title - Text(entry title)

The Products content model.

Products content model

The Products content model, will contain the fields as shown in the image:

  • Title - Text
  • Image - Files
  • Price - Number
  • Description - Long text
  • Permalink - Text, and
  • Category - Reference(multiple values).

When adding a Reference field, you can toggle the Use as a list of references switch in field settings, to do a multi-select. In this case, a product can have multiple categories.

Products Ref Field

Once the content models are created, add a few record entries to each πŸŽ‰

Content models admin menu

3. Next.js + Apollo GraphQL to Fetch Data From the Backend

Now, finally, we're going to start fetching the actual content from the Content Delivery API.

Start off by installing a few NPM packages in the e-commerce-starter project:

  • @apollo/client - This single package contains virtually everything we need to set up Apollo Client. It includes the in-memory cache, local state management, error handling, and a React-based view layer.

  • graphql - This package provides logic for parsing GraphQL queries.

yarn i @apollo/client graphql --save

Now that you installed the necessary packages, let's check the e-commerce-starter website structure in the image below:

E commerce starter structure

  • /components folder contains the react antd starter components, as seen in the image above.

  • /context/context.js contains the React context that holds the state of the Cart, Favorite products, Shopping Cart, and Total price of the cart.

  • /pages/api/payment_intents.js file provides a server side function for the Stripe integration.

  • /pages/_app.js component file is provided by Next.js, and serves as a wrapper of every single Next.js page of your frontend. The pages/_app.js file allows us to wrap the Apollo Provider around the function component so that we can have it available in every single page.

    Now, you may ask What is Apollo Provider⁉️ Good point ‼️ We'll get back to this later when we set up the ApolloClient.


We've covered the packages used and the folder structure of our e-commerce-starter app, let's jump on the code.

Open the pages/_app.js file and replace the existing code with the following snippet:

import '../assets/antd-custom.less'; // Layout Component import LayoutComponent from '../components/Layout'; // React Context import { CartProvider } from '../context/Context'; // Apollo import { ApolloProvider } from '@apollo/client'; import { useApollo } from '../lib/apolloClient'; export default function App({ Component, pageProps }) { const apolloClient = useApollo(pageProps.initialApolloState); // useApollo hook will initialize our apollo client return ( <ApolloProvider client={apolloClient}> {/** * * ApolloProvider will privde the apolloClient * to every single page of our application. * */} <CartProvider> <LayoutComponent> <Component {...pageProps} /> </LayoutComponent> </CartProvider> </ApolloProvider> ); }

As you can see, the pages/_app.js has different external components that act as wrappers to our Next.js app.

  • LayoutComponent - Holds the base layout for our app.
  • CartProvider - Is the React context for state management, in this case for the products cart data.
  • ApolloProvider - We'll explain the ApolloProvider below.

Let's go into the actual GraphQL client file.

Go ahead in the root of the project, and create the lib/apolloClient.js file, and paste the following snippet:

import { useMemo } from 'react' import { ApolloClient, HttpLink, InMemoryCache } from '@apollo/client' let apolloClient function createApolloClient() { return new ApolloClient({ ssrMode: typeof window === 'undefined', link: new HttpLink({ uri: process.env.CONTENT_DELIVERY_API_URL, // The CONTENT_DELIVERY_API_URL is comming from the `.env` file that you created on step 2 of the Prerequisites - Content Delivery API URL // Server URL (must be absolute) // credentials: 'same-origin', // Additional fetch() options like `credentials` or `headers` headers: { authorization: process.env.CONTENT_DELIVERY_API_ACCESS_TOKEN, // The CONTENT_DELIVERY_API_ACCESS_TOKEN is comming from the `.env` file that you created on step 3 of the Prerequisites - Content Delivery API Access Token }, }), cache: new InMemoryCache(), }) } export function initializeApollo(initialState = null) { const _apolloClient = apolloClient ?? createApolloClient() // If your page has Next.js data fetching methods that use Apollo Client, the initial state // gets hydrated here if (initialState) { // Get existing cache, loaded during client side data fetching const existingCache = _apolloClient.extract() // Restore the cache using the data passed from getStaticProps/getServerSideProps // combined with the existing cached data _apolloClient.cache.restore({ ...existingCache, ...initialState }) } // For SSG and SSR always create a new Apollo Client if (typeof window === 'undefined') return _apolloClient // Create the Apollo Client once in the client if (!apolloClient) apolloClient = _apolloClient return _apolloClient } export function useApollo(initialState) { const store = useMemo(() => initializeApollo(initialState), [initialState]) return store }

The important bit of the above snippet is the new instance of ApolloClient which has some options such as the link β†’ this is the part where we tell the Apollo how to go and fetch the data, and we do that by calling a new instance of HttpLink, and pass the options to this, which is the URI β†’ That means: Where on the internet does your GraphQL URL exist? πŸš€ This is where the Webiny's Content Delivery API URL comes into play.

Now, finally, we are going to start fetching the actual content from our Headless CMS back-end project. πŸš€πŸš€

With this set-up, now you have the apolloClient that is passed to the ApolloProvider in the pages/_app.js file, for the ApolloProvider wraps every single page within our application, you can actually go into a page, and import a hook called useQuery and the gql. With these two, you can perform queries against our GraphQL API. Let's do that!

Navigate to the components/ProductList.js file, and replace the code with the below snippet:

// Apollo import { useQuery, gql } from '@apollo/client'; // Ant design import { Row, Col } from 'antd'; // Components import ProductCard from './Product'; export const QUERY = gql` query listProducts { listProducts { data { id title description price image category { title } } } listCategories { data { title } } } `; export default function ProductList(props) { const { loading, error, data } = useQuery(QUERY); // if products are returned from the GraphQL query, run the filter query // and set equal to variable searchQuery if (loading) return <h1>Fetching</h1>; if (error) return 'Error loading products: ' + error.message; let ProductData = data.listProducts.data; // Remove the static data that came up with the e-commerce-starter code // let ProductData = [ // ... // ]; if (ProductData && ProductData.length) { const searchQuery = ProductData.filter((query) => query.title.toLowerCase().includes(props.search), ); if (searchQuery.length != 0) { return ( <Row> {searchQuery.map((res) => ( <Col xs={24} md={12} lg={8} key={('card: ', res.title)} // flex={3} > <ProductCard {...res} /> </Col> ))} </Row> ); } else { return <h1>No Products Found</h1>; } } return <h5>Visit your Webiny Headless CMS to add your Products</h5>; }

We've defined the QUERY, where we fetch the list of products, and it's fields. Besides the products, we also fetch the categories' content model.

Info

For now, we are still using static data in the components/Categories.js component, to use your actual data, you have to fetch the categories data in the components/Categories.js file. Furthermore, you can freely open a PR to place this feature on the e-commerce-starter project. Make it your first, or second PR @ Webiny. πŸ‘©πŸ»β€πŸš€ Head over to our project repository to open the PR πŸš€

Now, you will use the QUERY with the useQuery hook, which will return the data, the error, and a boolean loading, for whether it's still loading the data.

When you get the data from the Headless CMS, in the starter project we implemented a simple search functionality for the products. The search input value is received through the props of the components/ProductList component.

Before jumping on the localhost:3000/ to see the actual data, you need to add one more detail!

Next.js provides different methods for fetching data, one of them is getStaticProps - this method allows you to update existing pages by re-rendering them in the background as traffic comes in.

Navigate to the pages/index.js file, and replace its content with the following snippet:

import { Col, Row } from 'antd'; import React, { useState } from 'react'; // Ant design import { Input } from 'antd'; // Products import ProductList from '../components/ProductList'; import { QUERY } from '../components/ProductList'; import { Typography } from 'antd'; // Apollo import { initializeApollo } from '../lib/apolloClient'; const { Title } = Typography; const { Search } = Input; export default function Home() { const [query, updateQuery] = useState(''); return ( <> <Row gutter={[16, 24]}> <Col xs={{ span: 24, offset: 4 }} lg={{ span: 24, offset: 2 }} span={24} > <Title level={2} style={{ color: '#fa5723' }}> {' '} E-commerce website build with Webiny Headless CMS, Next.js, and Stripe </Title> </Col> </Row> <Row> <Col xs={{ span: 12, offset: 6 }} lg={{ span: 24, offset: 8 }} span={24} > <Title level={4} type="success"> {' '} Buy Swag from the best Open Source Projects </Title> </Col> </Row> <Row> <Col xs={{ span: 12, offset: 6 }} lg={{ span: 24, offset: 7 }} span={24} > <Search placeholder="Search for products" onSearch={(value) => console.log(value)} style={{ maxWidth: 500, }} onChange={(e) => updateQuery(e.target.value.toLocaleLowerCase()) } value={query} /> </Col> </Row> <Row> <Col span={24}> <ProductList search={query} /> </Col> </Row> </> ); } export async function getStaticProps() { const apolloClient = initializeApollo(); await apolloClient.query({ query: QUERY, }); return { props: { initialApolloState: apolloClient.cache.extract(), }, }; }

The getStaticProps function gets called at build time on server-side. It won't be called on client-side, so you can even do direct database queries.

Inside this function, you are querying the GraphQL API, and cashing the data in the background. Now, you can freely update your content on the Webiny Headless CMS πŸŽ‰

Fetching products

As you can see, I already created some products on my Headless CMS back-end project!

Now that we have the data, let's go and check the components/ProductCard component that came with the e-commerce-starter pack πŸš€ Navigate to the components/Product.js file, there you'll find the product cart functionalities such as addToCart, removeFromCart, and addToFavorites. One functionality is missing though, and that is the goToProduct that should open a modal, and show the product details⁉️

You can freely open a PR for the goToProduct function and make it your first PR @ Webiny. πŸ‘©πŸ»β€πŸš€ Head over to our Community repository to open the PR πŸš€

4. Next.js + Stripe to Create the Payment Intents

In this section, you will use the Stripe Payment intents API with Next.js to integrate Stripe for our shopping cart, and follow best practices as set out by Stripe and the industry.

Prerequisites

Before you continue, let's get ready for the section by following the two prerequisites below.

  1. Create a Stripe account
  2. You need the Publishable and Secret Key from the dashboard.stripe.com

We already have pre-build components such as the components/CheckoutForm.js, and the components/Layout.js component in the e-commerce-starter app, but these two are not complete! We'll work on them in this section.

The components/CheckoutForm.js component has the billingDetails and an empty spot for the CardElement in the <CardElementContainer></CardElementContainer> where you are going to add it, and it has a submit button and a spot to display some errors. Let's take a look at the existing components/CheckoutForm.js file below:

// Ant design import { Button, Form } from 'antd'; // React context import { CartContext, TotalContext } from '../context/Context'; import { Col, Row } from 'antd'; import React, { useContext, useState } from 'react'; // Components import { BillingDetailsFields } from './BillingDetailsField'; // styled components import styled from 'styled-components'; const iframeStyles = { base: { color: '#ff748c', fontSize: '16px', iconColor: '#ff748c', '::placeholder': { color: '#87bbfd', }, border: '1px solid gray', }, invalid: { iconColor: '#ff748c', color: '#ff748c', }, complete: { iconColor: '#ff748c', }, }; const cardElementOpts = { iconStyle: 'solid', style: iframeStyles, hidePostalCode: true, }; const CardElementContainer = styled.div` height: 40px; display: flex; align-items: center; & .StripeElement { width: 100%; padding: 15px; } `; const CheckoutForm = () => { const [form] = Form.useForm(); const [cart, setCart] = useContext(CartContext); const [totalPrice, settotalPrice] = useContext(TotalContext); const [isProcessing, setProcessingTo] = useState(false); const [checkoutError, setCheckoutError] = useState(); const handleCardDetailsChange = (ev) => { ev.error ? setCheckoutError(ev.error.message) : setCheckoutError(); }; const handleSubmit = async (e) => { // e.preventDefault(); const billingDetails = { name: e.name.value, email: e.email.value, address: { city: e.city.value, state: e.state.value, postal_code: e.zip.value, }, }; }; return ( <> <Row> <Col xs={{ span: 10, offset: 4 }} lg={{ span: 10, offset: 6 }} span={24} > <Form form={form} name="checkout" onFinish={handleSubmit} scrollToFirstError > <BillingDetailsFields /> <CardElementContainer></CardElementContainer>{' '} {checkoutError && ( <span style={{ color: 'red' }}> {checkoutError} </span> )} <br /> <Form.Item> <Button type="primary" htmlType="submit" disabled={isProcessing} > {isProcessing ? 'Processing...' : `Pay ${totalPrice}`} </Button> </Form.Item> </Form> </Col> </Row> </> ); }; export default CheckoutForm;

For the components/Layout.js component, you can think of it as a wrapper component around all of our pages on the site. In here you'll load the Stripe library later when we'll start coding.

Have a look at the existing components/Layout.js component below:

import { Col, Row } from 'antd'; import React, { useState } from 'react'; // Header import HeaderComponent from './Header'; // Ant Design import { Layout } from 'antd'; import { Typography } from 'antd'; const { Title } = Typography; const { Content, Footer } = Layout; function LayoutComponent(props) { const title = 'Webiny'; return ( <div> <HeaderComponent title={title} /> <Content className="site-layout"> <Layout className="site-layout-background" style={{ padding: '24px 0', margin: 100, display: 'flex', minHeight: '400px', background: '#fff', }} > <Row gutter={[16, 24]}> <Col xs={{ span: 24, offset: 4 }} lg={{ span: 24, offset: 2 }} span={24} > <Title level={2} style={{ color: '#fa5723' }}> {' '} E-commerce website build with Webiny Headless CMS, Next.js, and Stripe </Title> </Col> </Row> <Content style={{ padding: '24px', }} > {props.children} </Content> </Layout> <Footer style={{ textAlign: 'center', background: '#fff' }}> <a href="http://webiny.com/serverless-app/headless-cms?utm_source=Webiny-blog&utm_medium=webiny-website&utm_campaign=webiny-blog-e-commerce-oct-12&utm_content=webiny-blog-e-commerce-nextjs&utm_term=W00176" target="_blank" rel="noopener noreferrer" > Webiny Serverless Headless CMS @ 2020 </a> </Footer> </Content> </div> ); } export default LayoutComponent;

One last check regarding the Stripe integration before you can actually start coding, you need to save the two Stripe keys in a sticky desktop note or anywhere it's easier for you to grab the data later:

PUBLISHABLE_KEY = pk_test_1234; SECRET_KEY = sk_test_1234;

The publishable and the secret key can be found in the Stripe dashboard, make sure to sign up for a Stripe account if you didn't already, and navigate to the dashboard.stripe.com - make sure that you are Viewing test data, if not, toggle it on. Click on Developers and on the API keys as shown in the image below.

On the right, you will find the data for your publishable and secret keys.

Tip: Don't reveal your keys, if you do so by accident, go on the right and click on Roll key...

Stripe Dashboard

Now, run the e-commerce-starter project by running the yarn run dev, and the project will be served in the localhost:3000

When clicking on the cart, and proceed to do the payment, this is the view you'll get in /checkout page. πŸš€

Checkout page

Let's go and add the Pay functionality!


Install the below packages in the e-commerce-starter:

yarn install stripe @stripe/stripe-js @stripe/react-stripe-js axios --save

Now, head back to the code editor and open the component/Layout.js file and add these changes:

import { loadStripe } from "@stripe/stripe-js"; import { Elements } from "@stripe/react-stripe-js"; const stripePromise = loadStripe("YOUR_STRIPE_PUBLISHABLE_KEY");
Note

You have to replace the YOUR_STRIPE_PUBLISHABLE_KEY with your publishable Stripe's key before proceeding.

Now, we have a Stripe promise which resolves to Stripe's JavaScript file, we have to find a way to inject that Stripe's JavaScript object into the rest of our Next.js components.

To do that, we will use the Elements provider from react-stripe-js library, and wrap the {props.children} in the Elements provider at line 48 in the components/Layout.js component:

<Elements stripe={stripePromise}>{props.children}</Elements>

Next, you'll need the Card input to provide the credit cart for the checkout, you'll use the Stripe's CardElement component.

:::into Info The Card Element lets you collect card information all within one Element. It includes a dynamically-updating card brand icon as well as inputs for number, expiry, CVC, and postal code. - Resource :::

Navigate to the components/CheckoutForm.js file and import the CardElement like so:

import { CardElement } from "@stripe/react-stripe-js";

Now, add the CardElement to the CardElementContainer, on line 88

<CardElementContainer> <CardElement /> </CardElementContainer>

You should see now a Card input in the /checkout page, check out the image below.

Card input

Let's go and change the styles of the Card input by adding the options prop to the CardElement and passing there the cardElementOpts variable that was pre-build with the e-commerce-starter, check out the below snippet.

<CardElementContainer> <CardElement options={cardElementOpts} onChange={handleCardDetailsChange} /> </CardElementContainer>

Card input designed

Whoa! πŸš€ Now that we have the Card input set-up, one thing is missing and that is the payment, we are not able to accept payments yet.

Note

Head over to components/CheckoutForm.js to continue adding the payment functionality.

The steps we will do are:

  1. Create a payment intent on the server

    1. One request to the Stripe node library, to get the client_secret of that payment intent
  2. Create a payment method

    1. To create a payment method, we will need a reference of Stripe's object which has the function to create the payment method
    2. When we create the payment method we will need a reference to the CardElement that we defined earlier.
  3. Confirm the card payment

    1. We will combine the payment method id, and use the client_secret that we'll get from the first step.

1. Create a payment intent on the server

Now, we will use axios to make a post request to the server-side of the Next.js app from the components/CheckoutForm.js file.

First, import axios library to the components/CheckoutForm.js component as below.

import axios from "axios";

Inside the handleSumit function at 61 line, paste the below code.

try { const { data: clientSecret } = await axios.post("/api/payment_intents", { amount: totalPrice * 100, }); console.log("clientSecret:", clientSecret); } catch (err) { setCheckoutError(err.message); }

The totalPrice we are getting is from the total context, that holds the total price of all the products that are added on the Cart. Whenever the price changes, it will end up into the total context and triple down into our component components/CheckoutForm.js

Now, let's check what is happening on the server side, navigate to pages/api/payment_intents.js. Check out the code snipped below:

import Stripe from 'stripe'; const stripe = new Stripe( 'YOUR_STRIPE_SECRET_KEY', ); export default async (req, res) => { if (req.method === 'POST') { try { const { amount } = req.body; const paymentIntent = await stripe.paymentIntents.create({ amount, currency: 'usd', }); res.status(200).send(paymentIntent.client_secret); } catch (err) { res.status(500).json({ statusCode: 500, message: err.message }); } } else { res.setHeader('Allow', 'POST'); res.status(405).end('Method Not Allowed'); } };
Note:

Provide your Stripe secret key on the 4th line on pages/api/payment_intents.js

As you can see, for the /payment_intents endpoint, we have a simple handler. We extract the amount from the request body, we make a request to stripe.paymentIntents.create sending the amount and the currency, and we return the payment intents client secret.

Now, go back to Chrome and see what happens when we have the credit card number in the input πŸš€

Note:

Stripe provides test credit cards: for a default U.S. card 4242 4242 4242 4242

In the console tab of developer tools, if there are no errors, you'll see the clientSecret logged.

We will be using the clientSecret to confirm the card payment after we create the payment method!

2. Create a payment method

Now, use the Stripe's createPaymentMethod, stripe.createPaymentMethod in the components/CheckoutForm.js file, provide the card type that you'll get from the CardElement of the react-stripe-js library, and the billing_details from our form, follow the snippets below.

First, import the following from the Stripe's react-stripe-js library:

import { CardElement, useStripe, useElements } from "@stripe/react-stripe-js";

Second, inside the CheckoutForm component, on line 53 add the reference to the elements object using the useElements() hook and the reference to the Stripe object, using useStripe() hook.

const stripe = useStripe(); const elements = useElements();

Third, paste the paymentMethodReq method by placing it under the clientSecret request in the try and catch statement.

try { const { data: clientSecret } = await axios.post("/api/payment_intents", { amount: totalPrice * 100, }); console.log("clientSecret:", clientSecret); // create the payment method: const paymentMethodReq = await stripe.createPaymentMethod({ type: "card", card: cardElement, billing_details: billingDetails, }); console.log("paymentMethodReq: ", paymentMethodReq); } catch (err) { setCheckoutError(err.message); }

Now let's head over to chrome's developer tools console. πŸ™‚

Payment method Request

This is what we'll get from the paymentMethodReq, it resolved to a paymentMethod object which has a paymentMethod id which we'll going to use to confirm the card payment πŸŽ‰

3. Confirm the card payment

Now, we'll confirm the card payment by using the confirmCardPayment Stripe's method in which we will provide the paymentMethod.id we got earlier. Add the following snippet on components/CheckoutForm.js file, just after the paymentMethodReq:

const confirmCardPayment = await stripe.confirmCardPayment(clientSecret, { payment_method: paymentMethodReq.paymentMethod.id, }); console.log("confirmCardPayment: ", confirmCardPayment);

Now, go ahead at localhost:3000 and add some products to the cart, and hit pay, check on the console log to find the below result:

Confirm Payment Method

We got the paymentIntent id, and the status which is succeeded.

Check out the below snippet, to get the full code for the components/CheckoutForm.js file:

import React, { useState, useContext } from 'react'; // React context import { CartContext, TotalContext } from '../context/Context'; import { Col, Row } from 'antd'; // Components import { BillingDetailsFields } from '../components/BillingDetailsField'; // styled components import styled from 'styled-components'; import axios from 'axios'; // Ant design import { Form, Button, Typography } from 'antd'; const { Title } = Typography; // Stripe import { CardElement, useStripe, useElements } from '@stripe/react-stripe-js'; const iframeStyles = { base: { color: '#ff748c', fontSize: '16px', iconColor: '#ff748c', '::placeholder': { color: '#87bbfd', }, border: '1px solid gray', }, invalid: { iconColor: '#ff748c', color: '#ff748c', }, complete: { iconColor: '#ff748c', }, }; const cardElementOpts = { iconStyle: 'solid', style: iframeStyles, hidePostalCode: true, }; const CardElementContainer = styled.div` height: 40px; display: flex; align-items: center; & .StripeElement { width: 100%; padding: 15px; } `; const CheckoutForm = () => { const [form] = Form.useForm(); const [cart, setCart] = useContext(CartContext); const [isProcessing, setProcessingTo] = useState(false); const stripe = useStripe(); const elements = useElements(); console.log('stripe'); const [totalPrice, settotalPrice] = useContext(TotalContext); const [checkoutError, setCheckoutError] = useState(); const [checkoutSuccess, setCheckoutSuccess] = useState(); const handleCardDetailsChange = (ev) => { ev.error ? setCheckoutError(ev.error.message) : setCheckoutError(); }; const handleSubmit = async (e) => { // e.preventDefault(); const billingDetails = { name: e.name, email: e.email, address: { city: e.city, state: e.state, postal_code: e.zip, }, }; setProcessingTo(true); const cardElement = elements.getElement('card'); try { const { data: clientSecret } = await axios.post( '/api/payment_intents', { amount: totalPrice * 100, }, ); const paymentMethodReq = await stripe.createPaymentMethod({ type: 'card', card: cardElement, billing_details: billingDetails, }); if (paymentMethodReq.error) { setCheckoutError(paymentMethodReq.error.message); setProcessingTo(false); return; } const { error, paymentIntent: { status }, } = await stripe.confirmCardPayment(clientSecret, { payment_method: paymentMethodReq.paymentMethod.id, }); if (error) { setCheckoutError(error.message); return; } if (status === 'succeeded') { setCheckoutSuccess(true); setCart([]); settotalPrice(0); } } catch (err) { setCheckoutError(err.message); } }; if (checkoutSuccess) return <Title level={4}>Payment was successfull!</Title>; return ( <> <Row> <Col xs={{ span: 10, offset: 4 }} lg={{ span: 10, offset: 6 }} span={24} > <Form form={form} name="checkout" onFinish={handleSubmit} scrollToFirstError > <BillingDetailsFields /> <CardElementContainer> <CardElement options={cardElementOpts} onChange={handleCardDetailsChange} /> </CardElementContainer>{' '} {checkoutError && ( <span style={{ color: 'red' }}>{checkoutError}</span> )} <br /> <Form.Item> <Button type="primary" htmlType="submit" disabled={isProcessing || !stripe} > {isProcessing ? 'Processing...' : `Pay $${totalPrice}`} </Button> </Form.Item> </Form> </Col> </Row> </> ); }; export default CheckoutForm;

So far, we successfully created a functional payment integration with Stripe. πŸŽ‰ We started by adding the card input with Stripe javascript libraries, and continued with the set-up of payment intent, the payment method, and lastly, confirm the card payment.

Summary

We've created a simple e-commerce:

  • With Webiny Headless CMS for the back-end project and we created the content models for the e-commerce
  • Fetched the data from the Headless CMS to the Next.js project using Apollo GraphQL
  • Integrated Stripe Payment Intents to implement the shopping cart

YAY

You did it!!! πŸš€ Now you can continue extending the functionalities of the e-commerce project and explore the possible solutions with Webiny Headless CMS!

If you like the post please share it on Twitter . Webiny has a very welcoming Community! If you have any questions, please join us on slack .

You can also follow us on Twitter @WebinyCMS.


Thanks for reading! My name is Albiona and I work as a developer relations engineer at Webiny. I enjoy learning new tech and building communities around them = ) If you have any questions, comments or just wanna say hi, feel free to reach out to me via Twitter .

About Webiny

Webiny is an open source serverless CMS that offers you all the enterprise-grade functionalities, while keeping your data within the security perimeter of your own infrastructure.

Learn More

Newsletter

Want to get more great articles like this one in your inbox. We only send one newsletter a week, don't spam, nor share your data with 3rd parties.

Webiny Inc Β© 2024
Email
  • We send one newsletter a week.
  • Contains only Webiny relevant content.
  • Your email is not shared with any 3rd parties.
Webiny Inc Β© 2024
By using this website you agree to our privacy policy
Webiny Chat

Find us on Slack

Webiny Community Slack