Simply copy and paste the following command line in your terminal to create your first Strapi project.
npx create-strapi-app
my-project
This tutorial is part of the « Cooking a Deliveroo clone with Next.js (React), GraphQL, Strapi and Stripe » tutorial series.
Table of contents
Note: the source code is available on GitHub
You must start being starving... I am sure you want to be able to order!
We need to store the orders in our database, so we are going to create a new Content Type in our API.
Same process as usual:
+ Create new collection type
.order
as a name.Add New Field
and create the followings fields:address
with type Text
.city
with type Text
.dishes
with type JSON
.amount
with type Number
(big integer).To create new orders from the client, we are going to hit the create
endpoint of the order
API. To allow access, navigate to the Roles & Permissions section (http://localhost:1337/admin/plugins/users-permissions), select the authenticated
role, tick the order/create
checkbox, and save.
In this section, you will need Stripe API keys. To get them, create a Stripe account and navigate to https://dashboard.stripe.com/account/apikeys.
If you have already used Stripe, you probably know the credit card information does not go through your backend server. Instead, the credit card information is sent to the Stripe API (ideally using their SDK). Then, your front end receives a token that can be used to charge credit cards. The id
must be sent to your backend which will create the Stripe charge.
Not passing the credit card information through your server relieves you the responsibility to meet complicated data handling compliance, and is just far easier than worrying about securely storing sensitive data.
Install the stripe
package inside the backend directory:
$ cd ..
$ cd ..
$ cd ..
$ cd backend
$ npm i stripe --save
In order to integrate the Stripe logic, we need to update the create
charge endpoint in our Strapi API. To do so, edit backend/api/order/controllers/order.js
and replace its content with:
Path: /backend/api/order/controllers/order.js
Make sure to insert your stripe secret key (sk_) at the top where it instructs.
"use strict";
/**
* Order.js controller
*
* @description: A set of functions called "actions" for managing `Order`.
*/
const stripe = require("stripe")("YOUR STRIPE SECRET KEY");
module.exports = {
/**
* Create a/an order record.
*
* @return {Object}
*/
create: async (ctx) => {
const { address, amount, dishes, token, city, state } = JSON.parse(
ctx.request.body
);
const stripeAmount = Math.floor(amount * 100);
// charge on stripe
const charge = await stripe.charges.create({
// Transform cents to dollars.
amount: stripeAmount,
currency: "usd",
description: `Order ${new Date()} by ${ctx.state.user._id}`,
source: token,
});
// Register the order in the database
const order = await strapi.services.order.create({
user: ctx.state.user.id,
charge_id: charge.id,
amount: stripeAmount,
address,
dishes,
city,
state,
});
return order;
},
};
Note: in a real-world example, the amount should be checked on the backend side and the list of dishes related to the command should be stored in a more specific Content Type called orderDetail
.
Do not forget to restart the Strapi server.
To interact with the Stripe API, we will use the react-stripe-js which will give us Elements components to style our credit card form and submit the information properly to Stripe.
Now let's install the stripe UI elements for the frontend:
$ cd ..
$ cd frontend
$ yarn add @stripe/react-stripe-js@1.1.2 @stripe/stripe-js@1.5.0
Now we are going to create the checkout form and card section component to capture the credit card info and pass it to Stripe using the react-stripe-elements package:
Create the checkout form files:
$ cd components
$ mkdir checkout
$ cd checkout
$ touch CheckoutForm.js
Path: /frontend/components/checkout/CheckoutForm.js
/* /components/Checkout/CheckoutForm.js */
import React, { useState, useContext } from "react";
import { FormGroup, Label, Input } from "reactstrap";
import fetch from "isomorphic-fetch";
import Cookies from "js-cookie";
import { CardElement, useStripe, useElements } from "@stripe/react-stripe-js";
import CardSection from "./CardSection";
import AppContext from "../../context/AppContext";
function CheckoutForm() {
const [data, setData] = useState({
address: "",
city: "",
state: "",
stripe_id: "",
});
const [error, setError] = useState("");
const stripe = useStripe();
const elements = useElements();
const appContext = useContext(AppContext);
function onChange(e) {
// set the key = to the name property equal to the value typed
const updateItem = (data[e.target.name] = e.target.value);
// update the state data object
setData({ ...data, updateItem });
}
async function submitOrder() {
// event.preventDefault();
// // Use elements.getElement to get a reference to the mounted Element.
const cardElement = elements.getElement(CardElement);
// // Pass the Element directly to other Stripe.js methods:
// // e.g. createToken - https://stripe.com/docs/js/tokens_sources/create_token?type=cardElement
// get token back from stripe to process credit card
const token = await stripe.createToken(cardElement);
const userToken = Cookies.get("token");
const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/orders`, {
method: "POST",
headers: userToken && { Authorization: `Bearer ${userToken}` },
body: JSON.stringify({
amount: Number(Math.round(appContext.cart.total + "e2") + "e-2"),
dishes: appContext.cart.items,
address: data.address,
city: data.city,
state: data.state,
token: token.token.id,
}),
});
if (!response.ok) {
setError(response.statusText);
}
// OTHER stripe methods you can use depending on app
// // or createPaymentMethod - https://stripe.com/docs/js/payment_intents/create_payment_method
// stripe.createPaymentMethod({
// type: "card",
// card: cardElement,
// });
// // or confirmCardPayment - https://stripe.com/docs/js/payment_intents/confirm_card_payment
// stripe.confirmCardPayment(paymentIntentClientSecret, {
// payment_method: {
// card: cardElement,
// },
// });
}
return (
<div className="paper">
<h5>Your information:</h5>
<hr />
<FormGroup style={{ display: "flex" }}>
<div style={{ flex: "0.90", marginRight: 10 }}>
<Label>Address</Label>
<Input name="address" onChange={onChange} />
</div>
</FormGroup>
<FormGroup style={{ display: "flex" }}>
<div style={{ flex: "0.65", marginRight: "6%" }}>
<Label>City</Label>
<Input name="city" onChange={onChange} />
</div>
<div style={{ flex: "0.25", marginRight: 0 }}>
<Label>State</Label>
<Input name="state" onChange={onChange} />
</div>
</FormGroup>
<CardSection data={data} stripeError={error} submitOrder={submitOrder} />
<style jsx global>
{`
.paper {
border: 1px solid lightgray;
box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.2),
0px 1px 1px 0px rgba(0, 0, 0, 0.14),
0px 2px 1px -1px rgba(0, 0, 0, 0.12);
height: 550px;
padding: 30px;
background: #fff;
border-radius: 6px;
margin-top: 90px;
}
.form-half {
flex: 0.5;
}
* {
box-sizing: border-box;
}
body,
html {
background-color: #f6f9fc;
font-size: 18px;
font-family: Helvetica Neue, Helvetica, Arial, sans-serif;
}
h1 {
color: #32325d;
font-weight: 400;
line-height: 50px;
font-size: 40px;
margin: 20px 0;
padding: 0;
}
.Checkout {
margin: 0 auto;
max-width: 800px;
box-sizing: border-box;
padding: 0 5px;
}
label {
color: #6b7c93;
font-weight: 300;
letter-spacing: 0.025em;
}
button {
white-space: nowrap;
border: 0;
outline: 0;
display: inline-block;
height: 40px;
line-height: 40px;
padding: 0 14px;
box-shadow: 0 4px 6px rgba(50, 50, 93, 0.11),
0 1px 3px rgba(0, 0, 0, 0.08);
color: #fff;
border-radius: 4px;
font-size: 15px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.025em;
background-color: #6772e5;
text-decoration: none;
-webkit-transition: all 150ms ease;
transition: all 150ms ease;
margin-top: 10px;
}
form {
margin-bottom: 40px;
padding-bottom: 40px;
border-bottom: 3px solid #e6ebf1;
}
button:hover {
color: #fff;
cursor: pointer;
background-color: #7795f8;
transform: translateY(-1px);
box-shadow: 0 7px 14px rgba(50, 50, 93, 0.1),
0 3px 6px rgba(0, 0, 0, 0.08);
}
input,
.StripeElement {
display: block;
background-color: #f8f9fa !important;
margin: 10px 0 20px 0;
max-width: 500px;
padding: 10px 14px;
font-size: 1em;
font-family: "Source Code Pro", monospace;
box-shadow: rgba(50, 50, 93, 0.14902) 0px 1px 3px,
rgba(0, 0, 0, 0.0196078) 0px 1px 0px;
border: 0;
outline: 0;
border-radius: 4px;
background: white;
}
input::placeholder {
color: #aab7c4;
}
input:focus,
.StripeElement--focus {
box-shadow: rgba(50, 50, 93, 0.109804) 0px 4px 6px,
rgba(0, 0, 0, 0.0784314) 0px 1px 3px;
-webkit-transition: all 150ms ease;
transition: all 150ms ease;
}
.StripeElement.IdealBankElement,
.StripeElement.PaymentRequestButton {
padding: 0;
}
`}
</style>
</div>
);
}
export default CheckoutForm;
Now create a CardSection.js
file to use the React Elements in, this will house the input boxes that will capture the CC information.
$ touch CardSection.js
Path: /frontend/components/checkout/CardSection.js
Let's create the checkout page to bring it all together.
Create a new page: pages/checkout.js/
,
$ cd ..
$ cd ..
$ cd pages
$ touch checkout.js
Path: /frontend/pages/checkout.js
/* pages/checkout.js */
import React, { useContext } from "react";
import { Row, Col } from "reactstrap";
import { loadStripe } from "@stripe/stripe-js";
import { Elements } from "@stripe/react-stripe-js";
import InjectedCheckoutForm from "../components/checkout/CheckoutForm";
import AppContext from "../context/AppContext";
import Cart from "../components/cart/";
function Checkout() {
// get app context
const appContext = useContext(AppContext);
// isAuthenticated is passed to the cart component to display order button
const { isAuthenticated } = appContext;
// load stripe to inject into elements components
const stripePromise = loadStripe("YOUR STRIPE PUBLIC (pk_) KEY");
return (
<Row>
<Col style={{ paddingRight: 0 }} sm={{ size: 3, order: 1, offset: 2 }}>
<h1 style={{ margin: 20 }}>Checkout</h1>
<Cart isAuthenticated={isAuthenticated} />
</Col>
<Col style={{ paddingLeft: 5 }} sm={{ size: 6, order: 2 }}>
<Elements stripe={stripePromise}>
<InjectedCheckoutForm />
</Elements>
</Col>
</Row>
);
// }
}
export default Checkout;
Now if you select a dish and click order you should see:
Now if you submit your order, you should see the order under the Strapi dashboard as follows:
Explanations 🕵️
Note: explanations of code samples only, do not change your code to match this as you should already have this code this is simply a snippet
The stripe library will be initialized by the loadStripe('your key')
function/argument.
This will allow the stripe library to load the elements components.
This is what is rendering the card, zip and CVV on a single line.
Stripe will automatically detect which components are generating the CC information and what information to send to receive the token.
This submitOrder function will first make the call to Stripe with the CC information and receive back the Token if the CC check passed. If the token is received we next call the Strapi SDK to create the order passing in the appropriate extra order information and token id.
This is what creates the order in Stripe and creates the DB entry in Strapi. If successful you should see your Stripe test balances increase by the amount of the test order.
async function submitOrder() {
// event.preventDefault();
// // Use elements.getElement to get a reference to the mounted Element.
const cardElement = elements.getElement(CardElement);
// // Pass the Element directly to other Stripe.js methods:
// // e.g. createToken - https://stripe.com/docs/js/tokens_sources/create_token?type=cardElement
// get token back from stripe to process credit card
const token = await stripe.createToken(cardElement);
const userToken = Cookies.get("token");
const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/orders`, {
method: "POST",
headers: userToken && { Authorization: `Bearer ${userToken}` },
body: JSON.stringify({
amount: Number(Math.round(appContext.cart.total + "e2") + "e-2"),
dishes: appContext.cart.items,
address: data.address,
city: data.city,
state: data.state,
token: token.token.id,
}),
});
if (!response.ok) {
setError(response.statusText);
}
// OTHER stripe methods you can use depending on app
// // or createPaymentMethod - https://stripe.com/docs/js/payment_intents/create_payment_method
// stripe.createPaymentMethod({
// type: "card",
// card: cardElement,
// });
// // or confirmCardPayment - https://stripe.com/docs/js/payment_intents/confirm_card_payment
// stripe.confirmCardPayment(paymentIntentClientSecret, {
// payment_method: {
// card: cardElement,
// },
// });
}
You are now able to let users submit their order.
Bon appétit!
🚀 In the next (and last) section, you will learn how to deploy your Strapi app on Heroku and your frontend app on NOW: https://strapi.io/blog/nextjs-react-hooks-strapi-deploy.
Ryan is an active member of the Strapi community and he's been contributing at a very early stage by writing awesome tutorial series to help fellow Strapier grow and learn.