Airbnb clone, adding Stripe for payments

Join the 2022 Full-Stack Web Dev Bootcamp!


This post is part of a new series where we build a clone of Airbnb with Next.js. See the first post here.

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

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 }
  },
  {
    //...
  }
)

Remember to call Booking.sync({ alter: true }) to sync the database table. You can add this line at the end of the model.js file.

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/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 (fill your actual keys!)

We add them to the .env.local in the project root folder:

STRIPE_SECRET_KEY=sk_SOMETHING
STRIPE_PUBLIC_KEY=pk_SOMETHING
BASE_URL=http://localhost:3000

Create a pages/api/stripe/session.js endpoint:

import dotenv from 'dotenv'
dotenv.config()

export default async (req, res) => {
  if (req.method !== 'POST') {
    res.status(405).end() //Method Not Allowed
    return
  }

  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.

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/reserve:

const sessionResponse = await axios.post('/api/stripe/session', {
  amount: 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 api/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/reserve', {
  houseId: house.id,
  startDate,
  endDate,
  sessionId
})

In the pages/api/reserve.js, in the reserve endpoint, we now need to gather this new sessionId field and then we pass it to Booking.create():

import { User, Booking } from '../../model.js'

export default async (req, res) => {
  //...

  User.findOne({ where: { session_token: user_session_token } }).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:

This will later list our bookings.

In the next lesson we’ll handle webhooks.

See the code on GitHub

Next part: Airbnb clone, handling Stripe webhooks

Want to become a better Web Developer? Join the 2022 Web Development Bootcamp!

⭐️⭐️⭐️ Join the 2022 Web Development Bootcamp ⭐️⭐️⭐️