Simply copy and paste the following command line in your terminal to create your first Strapi project.
npx create-strapi-app
my-project
Tutorial updated by Fredrick Emmanuel and Paul Bratslavsky
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 here
You must start starving... I am sure you want to be able to order!
You need to store the orders in the database, so a new Content Type will be created in Strapi. 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
.state
with type Text
.amount
with type Number
(Int).token
with type Text
.dishes
with type JSON
.user
relation with user.To create new orders from the client, you are going to hit the create
endpoint of the order
API. To allow access, navigate to the Roles 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 or log in to Stripe then 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 in the backend directory:
npm i stripe --save
To integrate the Stripe logic, you need to update the create
charge endpoint in our Strapi API.
You can learn more on how to customize Strapi controllers here
To do so, edit backend/src/api/order/controllers/order.js
and replace its content with the following code:
Make sure to insert your stripe secret key (sk_) at the top where it instructs.
Path: */backend/src/api/order/controllers/order.js*
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
"use strict";
const stripe = require("stripe")("sk_test_4eC39HqLyjWDarjtT1zdp7dc");
/**
* order controller
*/
const { createCoreController } = require("@strapi/strapi").factories;
module.exports = createCoreController("api::order.order", ({ strapi }) => ({
async create(ctx) {
const user = ctx.state.user;
if (!user) {
return ctx.unauthorized("You are not authorized!");
}
console.log(ctx.request.body.data);
console.log(ctx.state.user.id);
console.log("order controller");
const { address, amount, dishes, token, city, state } =
ctx.request.body.data;
try {
// Charge the customer
await stripe.charges.create({
amount: amount,
currency: "usd",
description: `Order ${new Date()} by ${ctx.state.user.id}`,
source: token,
});
// Create the order
const order = await strapi.service("api::order.order").create({
data: {
amount,
address,
dishes,
city,
state,
token,
user: ctx.state.user.id,
},
});
return order;
} catch (err) {
// return 500 error
console.log("err", err);
ctx.response.status = 500;
return {
error: { message: "There was a problem creating the charge" },
address,
amount,
dishes,
token,
city,
state,
};
}
},
}));
Do not forget to restart the Strapi server.
To interact with the Stripe API, the react-stripe-js library will be used, which will give Elements components to style the credit card form and submit the information properly to Stripe.
Now install the stripe UI elements for the frontend. Open the frontend directory in the terminal and run the following command:
npm install @stripe/react-stripe-js @stripe/stripe-js
Now let's create our checkout form, in the pages directory create a new file called checkout.js
and add the following code.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import { Elements } from "@stripe/react-stripe-js";
import { loadStripe } from "@stripe/stripe-js";
import { useInitialRender } from "@/utils/useInitialRender";
import CheckoutForm from "@/components/CheckoutForm";
import CheckoutCart from "@/components/CheckoutCart";
const stripePromise = loadStripe("pk_test_TYooMQauvdEDq54NiTphI7jx");
export default function Checkout() {
const initialRender = useInitialRender();
if (!initialRender) return null;
return (
<section className="container mx-auto py-24">
<div className="grid grid-cols-5 gap-4">
<div className="col-span-2">
<CheckoutCart />
</div>
<div className="col-span-3">
<Elements stripe={stripePromise}>
<CheckoutForm />
</Elements>
</div>
</div>
</section>
);
}
You will notice that we are using some components from Stripe, you can learn more here.
As well as useInitialRender, before creating this function, the reason we are using this, is to check if the initial client side code rendered, otherwise we would get hydration miss-match error
.
Let's create the following file useInitialRender.js
inside our utils
folder and paste the following code:
1
2
3
4
5
6
7
8
9
10
import { useState, useEffect } from "react";
export const useInitialRender = () => {
const [initialRenderComplete, setInitialRenderComplete] = useState(false);
useEffect(() => {
if (!initialRenderComplete) setInitialRenderComplete(true);
}, [initialRenderComplete]);
return initialRenderComplete;
};
Next, we are going to create the CheckoutForm
and CheckoutCart
components to capture the credit card info and pass it to Stripe using the react-stripe-elements package:
In the the components directory create a file named name CheckoutForm.jsx.
Once done, add the following code:
Path: frontend/components/CheckoutForm.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
import React, { useState } from "react";
import Cookie from "js-cookie";
import { client } from "@/pages/_app.js";
import { gql } from "@apollo/client";
import { CardElement, useStripe, useElements } from "@stripe/react-stripe-js";
import { useAppContext } from "@/context/AppContext";
import { useRouter } from "next/router";
import { useInitialRender } from "@/utils/useInitialRender";
const options = {
style: {
base: {
fontSize: "32px",
color: "#52a635",
"::placeholder": {
color: "#aab7c4",
},
},
invalid: {
color: "#9e2521",
},
},
};
const INITIAL_STATE = {
address: "",
city: "",
state: "",
error: null,
};
export default function CheckoutForm() {
const [data, setData] = useState(INITIAL_STATE);
const [loading, setLoading] = useState(false);
const { user, cart, resetCart, setShowCart } = useAppContext();
const initialRender = useInitialRender();
const stripe = useStripe();
const elements = useElements();
const router = useRouter();
if (!initialRender) return null;
function onChange(e) {
const updateItem = (data[e.target.name] = e.target.value);
setData({ ...data, updateItem });
}
async function submitOrder(e) {
e.preventDefault();
const cardElement = elements.getElement(CardElement);
const token = await stripe.createToken(cardElement);
if (data.address === "") {
setData({ ...data, error: { message: "Address is required" } });
return;
}
if (data.city === "") {
setData({ ...data, error: { message: "City is required" } });
return;
}
if (data.state === "") {
setData({ ...data, error: { message: "State is required" } });
return;
}
if (token.error) {
setData({ ...data, error: { message: token.error.message } });
return;
}
const jwt = Cookie.get("token");
try {
setLoading(true);
const { data: response } = await client.mutate({
mutation: gql`
mutation CreateOrder(
$amount: Int
$dishes: JSON
$address: String
$city: String
$state: String
$token: String
) {
createOrder(
data: {
amount: $amount
dishes: $dishes
address: $address
city: $city
state: $state
token: $token
}
) {
data {
id
attributes {
token
}
}
}
}
`,
variables: {
amount: cart.total,
dishes: cart.items,
address: data.address,
city: data.city,
state: data.state,
token: token.token.id,
},
context: {
headers: {
Authorization: `Bearer ${jwt}`,
},
},
});
if (response.createOrder.data) {
alert("Transaction Successful, continue your shopping");
setData(INITIAL_STATE);
resetCart();
setShowCart(true);
router.push("/");
}
} catch (error) {
setData({ ...data, error: { message: error.message } });
} finally {
setLoading(false);
}
}
return (
<form>
<div className="bg-white shadow-md rounded-lg p-8">
<h5 className="text-lg font-semibold">Your information:</h5>
<hr className="my-4" />
<div className="flex mb-6">
<div className="flex-1">
<label
className="block mb-2 test-gray-800 font-medium"
htmlFor="address"
>
Address
</label>
<input
id="address"
htmlFor="address"
className="appearance-none block w-full p-3 leading-5 text-gray-900 border border-gray-200 rounded-lg shadow-md placeholder-text-gray-400 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-opacity-50"
type="text"
name="address"
onChange={onChange}
placeholder="Enter your address"
/>
</div>
</div>
<div className="flex mb-6">
<div className="flex-1 mr-6">
<label
htmlFor="city"
className="block mb-2 test-gray-800 font-medium"
>
City
</label>
<input
type="text"
name="city"
id="city"
onChange={onChange}
className="appearance-none block w-full p-3 leading-5 text-gray-900 border border-gray-200 rounded-lg shadow-md placeholder-text-gray-400 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-opacity-50"
/>
</div>
<div className="w-1/4">
<label
htmlFor="state"
className="block mb-2 test-gray-800 font-medium"
>
State
</label>
<input
type="text"
name="state"
id="state"
onChange={onChange}
className="appearance-none block w-full p-3 leading-5 text-gray-900 border border-gray-200 rounded-lg shadow-md placeholder-text-gray-400 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-opacity-50"
/>
</div>
</div>
{cart.items.length > 0 ? (
<div className="p-6">
<div>Credit or debit card</div>
<div className="my-4">
<CardElement options={options} />
</div>
<button
className="inline-block w-full px-6 py-3 text-center font-bold text-white bg-green-500 hover:bg-green-600 transition duration-200 rounded-full"
onClick={(e) => (user ? submitOrder(e) : router.push("/login"))}
disabled={loading}
>
{loading ? "Submitting" : "Submit Order"}
</button>
</div>
) : (
<div className="text-center">
<h1 className="text-2xl font-semibold">Your cart is empty</h1>
<p className="text-gray-500">
Add some items to your cart to continue
</p>
</div>
)}
<div>
{data.error && (
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative">
<strong className="font-bold">Error!</strong>{" "}
<span className="block sm:inline">{data.error.message}</span>
</div>
)}
</div>
</div>
</form>
);
}
Here is what happens in our submitOrder function.
This is an asynchronous function named submitOrder that is triggered when clicking the order button and performs the following steps:
In summary, this code is a function for submitting an order that uses the Stripe API to create a payment token and GraphQL to create an order. It also handles errors and sets loading states accordingly.
Now let's create our CheckoutCart
component. Create a file inside our components folder called CheckoutCart.jsx
Path: frontend/components/CheckoutCart.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
import { useAppContext } from "@/context/AppContext";
import { centsToDollars } from "@/utils/centsToDollars";
function CartItem({ data }) {
const { addItem, removeItem } = useAppContext();
const { quantity, attributes } = data;
return (
<div className="p-6 flex flex-wrap justify-between border-b border-blueGray-800">
<div className="w-2/4">
<div className="flex flex-col h-full">
<h6 className="font-bold text-white mb-1">{attributes.name}</h6>
<span className="block pb-4 mb-auto font-medium text-gray-400">
{quantity} x ${centsToDollars(attributes.priceInCents)}
</span>
</div>
</div>
<div className="w-1/4">
<div className="flex flex-col items-end h-full">
<div className="flex justify-between">
<button
className="mr-2 inline-block mb-auto font-medium text-sm text-gray-400 hover:text-gray-200"
onClick={() => removeItem(data)}
>
Remove
</button>
<button
className="inline-block mb-auto font-medium text-sm text-gray-400 hover:text-gray-200"
onClick={() => addItem(data)}
>
Add
</button>
</div>
<span className="block mt-2 text-sm font-bold text-white">
${centsToDollars(attributes.priceInCents * quantity)}
</span>
</div>
</div>
</div>
);
}
export default function CheckoutCart() {
const { cart } = useAppContext();
const total = cart.total;
const displayTotal = Math.abs(total);
return (
<div className="rounded-2xl co bg-gray-800">
<div className="max-w-lg pt-6 pb-8 px-8 mx-auto bg-blueGray-900">
<div className="flex mb-10 items-center justify-between">
<h6 className="font-bold text-2xl text-white mb-0">Your Cart</h6>
</div>
<div>
{cart.items
? cart.items.map((item, index) => {
if (item.quantity > 0) {
return <CartItem key={index} data={item} />;
}
})
: null}
</div>
<div className="p-6">
<div className="flex mb-6 content-center justify-between">
<span className="font-bold text-white">Order total</span>
<span className="text-sm font-bold text-white">
${centsToDollars(displayTotal)}
</span>
</div>
</div>
</div>
</div>
);
}
Now let's test our order form out, for a test credit card number you can just use 4242 4242 4242 4242
, that should work for you.
Now if you select a dish and click order, you should see:
You are now able to let users submit their orders.
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.