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 β¬οΈ
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.
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.
Content Delivery API Access Token
In order to access the data via the Content Delivery API, we'll need a valid
Access Token
.- 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.
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.
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.
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.
Once the content models are created, add a few record entries to each π
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:
/components
folder contains the reactantd
starter components, as seen in the image above./context/context.js
contains the React context that holds the state of theCart
,Favorite
products, Shopping Cart, andTotal
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. Thepages/_app.js
file allows us to wrap theApollo 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 theApolloProvider
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 π
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.
- Create a Stripe account
- You need the
Publishable
andSecret
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...
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. π
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.
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>
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:
Create a payment intent on the server
- One request to the Stripe node library, to get the
client_secret
of that payment intent
- One request to the Stripe node library, to get the
Create a payment method
- To create a payment method, we will need a reference of Stripe's object which has the function to create the
payment method
- When we create the
payment method
we will need a reference to theCardElement
that we defined earlier.
- To create a payment method, we will need a reference of Stripe's object which has the function to create the
Confirm the card payment
- We will combine the
payment method
id, and use theclient_secret
that we'll get from the first step.
- We will combine the
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. π
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:
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
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 .