DEV Community

Cover image for 🍝 Cooking a Deliveroo clone with Next.js (React), GraphQL, Strapi and Stripe - πŸ’΅ Order and Checkout (part 6/7)
Ryan
Ryan

Posted on β€’ Edited on

1

🍝 Cooking a Deliveroo clone with Next.js (React), GraphQL, Strapi and Stripe - πŸ’΅ Order and Checkout (part 6/7)

Strapi Next.js tutorial

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: https://github.com/strapi/strapi-examples/tree/master/nextjs-react-strapi-deliveroo-clone-tutorial*.

πŸ’΅ Order and Checkout

You must start being starving... I am sure you want to be able to order!

Order

Define Content Type

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:

  • Navigate to the Content Type Builder (http://localhost:1337/admin/plugins/content-type-builder).
  • Click on Add Content Type.
  • Set order as name.
  • Click on Add New Field and create the followings fields:
    • address with type String.
    • city with type String.
    • dishes with type JSON.
    • amount with type Integer (decimal).
  • Click on Save.

Order Content Type Builder

Allow access

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.

Stripe setup

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.

Add logic

If you 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 frontend 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.

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

"use strict";
/**
* Order.js controller
*
* @description: A set of functions called "actions" for managing `Order`.
*/
const stripe = require("stripe")("sk_test_INSERT YOUR API KEY");
module.exports = {
/**
* Retrieve order records.
*
* @return {Object|Array}
*/
find: async ctx => {
if (ctx.query._q) {
return strapi.services.order.search(ctx.query);
} else {
return strapi.services.order.fetchAll(ctx.query);
}
},
/**
* Retrieve a order record.
*
* @return {Object}
*/
findOne: async ctx => {
if (!ctx.params._id.match(/^[0-9a-fA-F]{24}$/)) {
return ctx.notFound();
}
return strapi.services.order.fetch(ctx.params);
},
/**
* Count order records.
*
* @return {Number}
*/
count: async ctx => {
return strapi.services.order.count(ctx.query);
},
/**
* Create a/an order record.
*
* @return {Object}
*/
create: async ctx => {
const { address, amount, dishes, token, city, state } = ctx.request.body;
const charge = await stripe.charges.create({
// Transform cents to dollars.
amount: amount * 100,
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.add({
user: ctx.state.user._id,
address,
amount,
dishes,
city,
state
});
return order;
},
/**
* Update a/an order record.
*
* @return {Object}
*/
update: async (ctx, next) => {
return strapi.services.order.edit(ctx.params, ctx.request.body);
},
/**
* Destroy a/an order record.
*
* @return {Object}
*/
destroy: async (ctx, next) => {
return strapi.services.order.remove(ctx.params);
}
};
view raw order.js hosted with ❀ by GitHub

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.

Install the stripe package inside the backend directory:

cd ..
cd ..
cd ..
cd backend 
npm i stripe --save
Enter fullscreen mode Exit fullscreen mode

Do not forget to restart the Strapi server.

Note: if an error occurs, run npm i strapi-hook-mongoose.

To interact with the Stripe API, we will use the react-stripe-elements which will give us a StripeProvider and Elements components to style our credit card form and submit the information properly to Stripe.

Checkout page

Create a new page: pages/checkout.js/,

cd ..
cd frontend
yarn add react-stripe-elements
cd pages
touch checkout.js
Enter fullscreen mode Exit fullscreen mode

Path: /frontend/pages/checkout.js

/* pages/checkout.js */
import React, { Component } from "react";
import defaultPage from "../hocs/defaultPage";
import Cart from "../components/Cart/Cart";
import InjectedCheckoutForm from "../components/Checkout/CheckoutForm";
import { Row, Col } from "reactstrap";
import { StripeProvider, Elements } from "react-stripe-elements";
import { withContext } from "../components/Context/AppProvider";
import { compose } from "recompose";
import Router from "next/router";
class Checkout extends Component {
constructor(props) {
super(props);
this.state = {
items: {},
stripe: null
};
}
componentDidMount() {
// Create Stripe instance in componentDidMount
// (componentDidMount only fires in browser/DOM environment)
const { context } = this.props;
const { isAuthenticated } = this.props;
if (context.items.length === 0 || !isAuthenticated) {
Router.push("/");
}
this.setState({
stripe: window.Stripe("pk_test_YOUR_STRIPE_PK_KEY")
});
}
render() {
const { isAuthenticated } = this.props;
const { context } = this.props;
if (context.items.length === 0) {
return <h1>Loading</h1>;
} else {
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 }}>
<StripeProvider stripe={this.state.stripe}>
<Elements>
<InjectedCheckoutForm context={this.props.context} />
</Elements>
</StripeProvider>
</Col>
</Row>
);
}
}
}
export default compose(
defaultPage,
withContext
)(Checkout);
view raw checkout.js hosted with ❀ by GitHub

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 ..
cd components
mkdir Checkout
cd Checkout
touch CheckoutForm.js
Enter fullscreen mode Exit fullscreen mode

Path: /frontend/components/Checkout/CheckoutForm.js

/* /components/Checkout/CheckoutForm.js */
import React from "react";
import CardSection from "./CardSection";
import { FormGroup, Label, Input, FormText, Row, Col } from "reactstrap";
import { injectStripe } from "react-stripe-elements";
import Strapi from "strapi-sdk-javascript/build/main";
import Router from "next/router";
const apiUrl = process.env.API_URL || "http://localhost:1337";
const strapi = new Strapi(apiUrl);
/* components/Checkout/CheckoutForm.js */
class CheckoutForm extends React.Component {
constructor(props) {
super(props);
this.state = {
data: {
address: "",
city: "",
state: "",
stripe_id: ""
},
error: ""
};
this.submitOrder = this.submitOrder.bind(this);
}
onChange(propertyName, e) {
const { data } = this.state;
data[propertyName] = e.target.value;
this.setState({ data });
}
submitOrder() {
const { context } = this.props;
const { data } = this.state;
console.log(context);
console.log(this.props.stripe.createToken())
this.props.stripe.createToken()
.then(res => {
strapi
.createEntry("orders", {
amount: context.total,
dishes: context.items,
address: data.address,
city: data.city,
state: data.state,
token: res.token.id
})
.then(Router.push("/"));
})
.catch(err => this.setState({ error: err}))
}
render() {
return (
<div className="paper">
<h5>Your information:</h5>
<hr />
<FormGroup style={{ display: "flex" }}>
<div style={{ flex: "0.90", marginRight: 10 }}>
<Label>Address</Label>
<Input onChange={this.onChange.bind(this, "address")} />
</div>
</FormGroup>
<FormGroup style={{ display: "flex" }}>
<div style={{ flex: "0.65", marginRight: "6%" }}>
<Label>City</Label>
<Input onChange={this.onChange.bind(this, "city")} />
</div>
<div style={{ flex: "0.25", marginRight: 0 }}>
<Label>State</Label>
<Input onChange={this.onChange.bind(this, "state")} />
</div>
</FormGroup>
<CardSection
context={this.props.context}
data={this.state.data}
submitOrder={this.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: #fafafa;
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;
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 injectStripe(CheckoutForm);
view raw checkoutform.js hosted with ❀ by GitHub

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
Enter fullscreen mode Exit fullscreen mode

Path: /frontend/components/Checkout/CardSection.js

/* components/Checkout/cardsection.js */
import React from "react";
import { CardElement, CardNumberElement } from "react-stripe-elements";
import { injectStripe } from "react-stripe-elements";
class CardSection extends React.Component {
render() {
return (
<div>
<div>
<label htmlFor="card-element">Credit or debit card</label>
<div>
<fieldset style={{ border: "none" }}>
<div className="form-row">
<div id="card-element" style={{ width: "100%" }}>
<CardElement
style={{ width: "100%", base: { fontSize: "18px" } }}
/>
</div>
<br />
<div className="order-button-wrapper">
<button onClick={this.props.submitOrder}>
Confirm order
</button>
</div>
{this.props.stripeError ? (
<div>{this.props.stripeError.toString()}</div>
) : null}
<div id="card-errors" role="alert" />
</div>
</fieldset>
</div>
</div>
<style jsx>
{`
.order-button-wrapper {
display: flex;
width: 100%;
align-items: flex-end;
justify-content: flex-end;
}
`}
</style>
</div>
);
}
}
export default injectStripe(CardSection);
view raw cardsection.js hosted with ❀ by GitHub

Now if you select a dish and click order you should see:

CheckoutPicture

Now if you submit your order, you should see the order under the Strapi dashboard as follows:

Strapi

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

For server side rendering with react-stripe elements, some modifications need to be made as Stripe will only be available on the client not the server.

To account for this, the stripe pk_key is set in the ComponentDidMount lifecycle hook that only fires on the browser:

class App extends React.Component {
constructor() {
super();
this.state = {stripe: null};
}
componentDidMount() {
// Create Stripe instance in componentDidMount
// (componentDidMount only fires in browser/DOM environment)
this.setState({stripe: window.Stripe('pk_test_12345')});
}
render() {
return (
<StripeProvider stripe={this.state.stripe}>
<Elements>
<InjectedCheckoutForm />
</Elements>
</StripeProvider>
);
}
}
view raw stripe-ssr.js hosted with ❀ by GitHub

Stripe will use a <StripeProvider> component that will take in your stripe pk_key as a prop. This allows the children of the component access to the stripe key.

To use the integrated stripe components we will wrap our CheckoutForm component in the <Elements> component.

The downstream import { injectStripe } from "react-stripe-elements" inside the CheckoutForm component is required for the Elements children components to pass the CC information to Stripe.

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 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.

submitOrder() {
const { context } = this.props;
const { data } = this.state;
console.log(context);
this.props.stripe.createToken().then(res => {
strapi
.createEntry("orders", {
amount: context.total,
dishes: context.items,
address: data.address,
city: data.city,
state: data.state,
stripe_id: data.stripe_id,
token: res.token.id
})
.then(Router.push("/"));
});
}
view raw gettoken.js hosted with ❀ by GitHub

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://dev.to/ryanrez/-cooking-a-deliveroo-clone-with-nextjs-react-graphql-strapi-and-stripe----bonus-deploy---part-77-1i8e

Postmark Image

Speedy emails, satisfied customers

Are delayed transactional emails costing you user satisfaction? Postmark delivers your emails almost instantly, keeping your customers happy and connected.

Sign up

Top comments (0)

πŸ‘‹ Kindness is contagious

Please leave a ❀️ or a friendly comment on this post if you found it helpful!

Okay