In this lesson we’re going to add payment integration using Stripe.

Stripe has a product called Connect that allows to create a marketplace where people get money directly from customers.

That looks something worth exploring, but not in our context. In our app we’ll do like Airbnb does: we collect the payment ourselves, and we’ll distribute the earning in the backend, once every month or so.

This is not something we’ll implement in the course.

What we’ll do is, we’ll collect the payment for the booking. That’s it.

We do this using Stripe Checkout.

Sign up to Stripe if you don’t have an account yet.

We have 2 different types of Checkout.

One is the client-only integration, the other is the client & server integration.

From the Stripe docs:

With the client-only integration, you define your products directly in the Stripe Dashboard and reference them by ID on the client side. This approach makes it possible to integrate Checkout into your website without needing any server-side code. It is best suited for simple integrations that don’t need dynamic pricing.

It’s clear that this is not enough. We must define each product separately, as a “booking”, because our prices vary depending on the house, and on the duration of the stay.

Also client-only integration does not support placing a hold on a card before charging it. This could be interesting: you don’t charge a card immediately, but just after the person stayed at the house. Or checks in.

But in our case, to simplify the workflow, we’ll bill directly at the booking time.

So, the work on the following workflow:

  • we create a checkout session server-side, when the user clicks “Reserve now”, and we provide a success URL that will be where people are sent, on our site, after the payment was successful
  • we store the booking, set as paid=false
  • we redirect to checkout on Stripe’s webpage
  • as soon as the payment is done, Stripe redirects to the success URL, where we’ll later set up the lists of booked places of the user, along with the dates
  • meanwhile, Stripe will send us a confirmation via Webhook to set the booking as paid

As soon as a person clicks “Reserve now”, we’ll add the reservation into the database, along with the Stripe session id.

This clears the possibility that a person books meanwhile another person books, so the dates are immediately marked as unavailable on the calendar.

We’ll store it with a new paid field set to false.

As soon as Stripe calls our Webhook to inform of the payment, we’ll set the reservation to paid = true.

Let’s do it!

I start by adding this new paid field to the Booking model, and a new sessionId string, too:

Booking.init({
  //...
  paid: { type: Sequelize.DataTypes.BOOLEAN, defaultValue: false, allowNull: false },
  sessionId: { type: Sequelize.DataTypes.STRING }
}, {
  //...
})

Full code:

models/booking.js

const Sequelize = require('sequelize')
const sequelize = require('../database.js')

class Booking extends Sequelize.Model {}

Booking.init(
  {
    id: {
      type: Sequelize.DataTypes.INTEGER,
      autoIncrement: true,
      primaryKey: true
    },
    houseId: { type: Sequelize.DataTypes.INTEGER, allowNull: false },
    userId: { type: Sequelize.DataTypes.INTEGER, allowNull: false },
    startDate: { type: Sequelize.DataTypes.DATEONLY, allowNull: false },
    endDate: { type: Sequelize.DataTypes.DATEONLY, allowNull: false },
    paid: { type: Sequelize.DataTypes.BOOLEAN, defaultValue: false, allowNull: false },
    sessionId: { type: Sequelize.DataTypes.STRING }
  },
  {
    sequelize,
    modelName: 'booking',
    timestamps: true
  }
)

module.exports = Booking

Next we install the stripe npm package for server-side usage:

npm install stripe

and we need to add the Stripe frontend code:

<script src="https://js.stripe.com/v3/"></script>

How? In the components/Layout.js file, which is what every page component includes, we’re going to add this to the top:

import Head from 'next/head'

and then, when we return the JSX:

return (
  <div>
    <Head>
      <script src='https://js.stripe.com/v3/'></script>
    </Head>

This will put this script tag in the page <head> tag.

Now I’m going to modify the process we use to reserve the house, a little bit.

Before actually POSTing to /api/houses/reserve, I’m going to POST to a new endpoint we’ll create that listens on /api/stripe/session and wait for the end result.

In that endpoint we’ll set up the payment, with the amount and details, and Stripe will give us a sessionId for the payment, which we’ll use in the frontend.

Before going on, we must go on the Stripe dashboard and gather the API secret key and the public key. The first must never be exposed to the frontend, while the second will be used in code that can be seen by users (hence the name public).

In my case they look like sk_SOMETHING and pk_SOMETHING.

We add them to a file in the root folder of the project, outside of src. The folder that hosts the package.json file, among others.

In there, create a file called .env and write there the keys, like this:

STRIPE_SECRET_KEY=sk_SOMETHING
STRIPE_PUBLIC_KEY=pk_SOMETHING

The syntax is a bit strange, because it’s not JavaScript. It’s a shell script.

Now we install this special package:

npm install dotenv

which allows us to gather those variables in the Node.js code like this:

import dotenv from 'dotenv'

dotenv.config()

console.log(process.env.STRIPE_SECRET_KEY) //prints the secret key
console.log(process.env.STRIPE_PUBLIC_KEY) //prints the public key

It’s very handy to keep all keys OUTSIDE of the code. You can for example publish your project on GitHub (like I do with the example) and use .gitignore to ignore the content of the .env file, so it’s never made public.

Let’s do it. Open server.js, and at the top, add:

import dotenv from 'dotenv'
dotenv.config()

Now let’s define the /api/stripe/session endpoint:

server.post('/api/stripe/session', async (req, res) => {
  const amount = req.body.amount

  const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY)
  const session = await stripe.checkout.sessions.create({
    payment_method_types: ['card'],
    line_items: [
      {
        name: 'Booking house on Airbnb clone',
        amount: amount * 100,
        currency: 'usd',
        quantity: 1
      }
    ],
    success_url: process.env.BASE_URL + '/bookings',
    cancel_url: process.env.BASE_URL + '/bookings'
  })

  res.writeHead(200, {
    'Content-Type': 'application/json'
  })
  res.end(
    JSON.stringify({
      status: 'success',
      sessionId: session.id,
      stripePublicKey: process.env.STRIPE_PUBLIC_KEY
    })
  )
})

We get the amount value from the POST request body,and once we have that, we can require the stripe library and create a session. We pass an object that defines the payment, which includes the payment accepted, the item purchased and 2 lines that set the URLs of the pages to redirect to, after the purchase is done or cancelled.

Finally, we return the session id value, and also the process.env.STRIPE_PUBLIC_KEY, because the frontend can’t access it directly and we’ll need it later to invoke the Stripe checkout.

Now we can call this endpoint in pages/houses/[id].js before we call /api/houses/reserve:

const sessionResponse = await axios.post('/api/stripe/session', { amount: props.house.price * numberOfNightsBetweenDates })
if (sessionResponse.data.status === 'error') {
  alert(sessionResponse.data.message)
  return
}

const sessionId = sessionResponse.data.sessionId
const stripePublicKey = sessionResponse.data.stripePublicKey

Once this is done, we pass sessionId to the houses/reserve call, because we want to store it in the bookings table.

Why? Because when the Stripe payment confirmation webhook will be sent to us, that’s the way we can link the payment with the booking.

const reserveResponse = await axios.post(
  '/api/houses/reserve',
  {
    houseId: props.house.id,
    startDate,
    endDate,
    sessionId
  }
)

In the server.js, in the reserve endpoint, we now need to gather this new sessionId field from the body:

const sessionId = req.body.sessionId

and then we pass it inside Booking.create():

server.post('/api/houses/reserve', (req, res) => {
  const userEmail = req.session.passport.user

  User.findOne({ where: { email: userEmail } }).then(user => {
    Booking.create({
      houseId: req.body.houseId,
      userId: user.id,
      startDate: req.body.startDate,
      endDate: req.body.endDate,
      sessionId: req.body.sessionId
    }).then(() => {
      res.writeHead(200, {
        'Content-Type': 'application/json'
      })
      res.end(JSON.stringify({ status: 'success', message: 'ok' }))
    })
  })
})

Finally in pages/houses/[id].js, I can redirect the user to Stripe checkout, with this code:

pages/houses/[id].js

const stripe = Stripe(stripePublicKey)
const { error } = await stripe.redirectToCheckout({
  sessionId
})

This all happens transparently to the user. They are immediately sent to the Stripe checkout:

Stripe has this great testing tool that lets you add a credit card numbered 4242 4242 4242 4242, you add 4 or 2 to the expiration date and code, and it’s considered valid.

The success URL route

Remember? In the /api/stripe/session API endpoint, we set

success_url: process.env.BASE_URL + '/bookings',

This is a page on our site, where people will be redirected to when the Stripe payment is successful.

Let’s create this page. Create a pages/bookings.js file, and add this content to it:

import Layout from '../components/Layout'

const Bookings = () => {
  return <Layout content={<p>TODO</p>} />
}

export default Bookings

The app should respond to http://localhost:3000/bookings with:

The stripe webhook handler

Now we must implement the Stripe webhook handler. A webhook is an HTTP call in response to something, and in our case that’s sent to us when the payment is successful.

Using Webhooks locally

When the app will be “live” on a real server, you’d go to https://dashboard.stripe.com/account/checkout/settings and click “Configure webhooks” in the “Checkout client & server integration” section to configure the real webhook.

But since we are running the app locally, what can we do? Stripe does not let us use localhost as a valid domain, and for a good reason: they must be able to access your app, and if it’s running on localhost it’s just not possible - unless you set up an ip tunnel using ngrok for example.

Luckily for us, Stripe has this great tool called Stipe CLI that can provide a way to automatically get webhooks to our local server.

See here how to install it. In my case, on macOS, it’s

brew install stripe/stripe-cli/stripe

Once installed I run

stripe login

and follow the instructions to log in.

Then run

stripe listen --forward-to localhost:3000/api/stripe/webhook

This command will return a webhook signing secret code, which you’ll need to put in the .env file:

STRIPE_WEBHOOK_SECRET=whsec_SOMETHING

The Webhook handler

Now let’s create the Webhook POST handler.

Open server.js and create a POST api/stripe/webhook endpoint.

We return a { received: true } JSON response to Stripe, to tell them “we got it!”:

server.post('/api/stripe/webhook', async (req, res) => {
  res.writeHead(200, {
    'Content-Type': 'application/json'
  })
  res.end(JSON.stringify({ received: true }))
})

We now use this code (which I found on the Stripe documentation) that we use to analyze the Webhook:

const sig = req.headers['stripe-signature']

let event

try {
  event = stripe.webhooks.constructEvent(req.rawBody, sig, endpointSecret)
} catch (err) {
  res.writeHead(400, {
    'Content-Type': 'application/json'
  })
  console.error(err.message)
  res.end(JSON.stringify({ status: 'success', message: `Webhook Error: ${err.message}` }))
  return
}

See that I use req.rawBody. This is not a property available by default to us, using Polka or Express. I use this to solve a problem. stripe.webhooks.constructEvent() wants the raw request body passed to it, to verify for security purposes.

Now, in src/server.js we pass the bodyParser.json() middleware that parses the request body as JSON, and from that point the raw body is not available any more.

So I changed the line bodyParser.json() in src/server.js to:

bodyParser.json({
  verify: (req, res, buf) => { //make rawBody available
    req.rawBody = buf
  }
})

so that we can access the raw body using req.rawBody in the POST endpoint.

This thing required quite a bit of research, it’s not a problem commonly found - and sometimes the little things can take a relatively long time to solve

Since we get lots of various Webhook notifications from Stripe we need to filter out the one we need, which is called checkout.session.completed.

When this happens we get the session id from the event and we use that to update the booking with the same session id assigned (remember? we added it into the table) and it sets the paid column to true:

if (event.type === 'checkout.session.completed') {
  const sessionId = event.data.object.id

  try {
    Booking.update(
      { paid: true },
      { where: { sessionId } }
    )
  } catch (err) {
    console.error(err)
  }
}

This is the complete code:

server.post('/api/stripe/webhook', async (req, res) => {
  const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY)
  const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET
  const sig = req.headers['stripe-signature']

  let event

  try {
    event = stripe.webhooks.constructEvent(req.rawBody, sig, endpointSecret)
  } catch (err) {
    res.writeHead(400, {
      'Content-Type': 'application/json'
    })
    console.error(err.message)
    res.end(
      JSON.stringify({
        status: 'success',
        message: `Webhook Error: ${err.message}`
      })
    )
    return
  }

  if (event.type === 'checkout.session.completed') {
    const sessionId = event.data.object.id

    try {
      Booking.update({ paid: true }, { where: { sessionId } })
    } catch (err) {
      console.error(err)
    }
  }

  res.writeHead(200, {
    'Content-Type': 'application/json'
  })
  res.end(JSON.stringify({ received: true }))
})

This is a central part of our application.

As soon as the user is back from the payment from Stripe, if all goes well the booking has already been marked as paid in our database.

Testing Webhooks

As with every entity that’s introduced in your application that you don’t have full control over, testing webhooks is complicated.

Luckily Stripe provides us the stripe CLI tool we already used to allow local webhooks to run.

If you open another window, you can invoke it again like this:

stripe trigger checkout.session.completed

This lets you test the Webhook code without having to follow the payments workflow manually over and over again.

You can now try the whole workflow, and ideally you should get a booking marked as paid in the database!

Clearing unpaid bookings

Now we have a little issue. Since we book dates before paying, some people might not pay (it will happen frequently!) and this leaves us with booked dates that are not really confirmed.

To solve this, we need to clear out bookings from time to time.

I made a POST endpoint to do this, in server.js:

server.js

server.post('/api/bookings/clean', (req, res) => {
  Booking.destroy({
    where: {
      paid: false
    }
  })

  res.writeHead(200, {
    'Content-Type': 'application/json'
  })

  res.end(
    JSON.stringify({
      status: 'success',
      message: 'ok'
    })
  )
})

It calls Booking.destroy to remove all unpaid bookings.

This needs some refinement, but it’s good for us now. I added an Insomnia POST request to help me clean the database:

Ideally you’d remove all bookings that have been created, say, more than 1 hour ago, and are still unpaid.

The code for this lesson is available at https://github.com/flaviocopes/airbnb-clone-react-nextjs/commit/da709b277654023520a244926344f2d5f4795727


Go to the next lesson