Builder Book logo

Book: Builder Book

  1. Introduction
  2. Project structure. ESLint. Next.js. HOC. Material-UI. Server-side rendering. Styles.
  3. Server. Database. Session. Header and MenuDrop components.
  4. Authentication HOC. Promise. Async/await. Static method for User model. Google OAuth.
  5. Testing with Jest. Transactional emails with AWS SES API. In-app notifications.
  6. Book and Chapter data models. Internal API infrastructure, API methods and Express routes. ReadChapter page.
  7. Set up Github API infrastructure. Sync content API infrastructure. Missing UI infrastructure for Admin user. Redirects for Admin and Customer users. Testing.
  8. Table of Contents. Highlight for section. Hide Header. Mobile browser.
  9. BuyButton component. Buy book API infrastructure. Setup at Stripe dashboard and environmental variables. isPurchased and ReadChapter page. Redirect. My books API and MyBooks page. Mailchimp API.
  10. Prepare project for deployment. Environmental variables, production/development. Logger. SEO, robots.txt, sitemap.xml. Google Analytics. Compression and security. Heroku. Testing deployed project. AWS Elastic Beanstalk.

Chapter 8: BuyButton component. Buy book API infrastructure. Setup at Stripe dashboard and environmental variables. isPurchased and ReadChapter page. Redirect. My books API and MyBooks page. Mailchimp API.

The section below is a preview of Builder Book. To read the full text, you will need to purchase the book.

We are currently refactoring and updating the code in Builder Book. To reduce confusion, we have temporarily disabled the buy button. You can follow our progress here: https://github.com/builderbook/builderbook/issues/359

You can still buy our second book, SaaS Boilerplate: https://builderbook.org/books/saas-boilerplate/introduction-project-structure


In Chapter 8, you'll start with the codebase in the 8-begin folder of our builderbook repo and end up with the codebase in the 8-end folder. We'll cover the following topics in this chapter:

  • BuyButton component

  • Buy book API infrastructure
    - fetchCheckoutSessionApiMethod API method
    - Express route /stripe/fetch-checkout-session
    - Stripe method createSession
    - Express route for request from Stripe server, retrieveSession

  • Setup at Stripe dashboard and environmental variables
    - Static method Book.buy
    - Purchase data model

  • isPurchased and ReadChapter page
    - isPurchased, preview and full content
    - Adding BuyButton to ReadChapter
    - Testing Buy book API

  • Redirect

  • My books API and MyBooks page
    - getMyBookListApiMethod API method
    - Express route /my-books
    - Static method Book.getPurchasedBooks

  • Mailchimp API
    - callAPI method
    - addToMailchimp method
    - Adding addToMailChimp to User.signInOrSignUp and Book.buy
    - Environmental variables for Mailchimp API
    - Testing Mailchimp API


In the previous chapter (Chapter 7), we made many UX improvements to our ReadChapter page. In this chapter, we will add all code related to selling a book. Here is a high-level overview of what we want to accomplish:

  • introduce a BuyButton component
  • make this BuyButton button functional: corresponding API method, Express route, and static methods for our Book data model, server-side Stripe API methods, and Purchase data model
  • add a BuyButton component to our ReadChapter page and test the checkout flow
  • show list of books that a user bought on the user's MyBooks page
  • add the email of a user who signed up in our web application to a mailing list MAILCHIMP_SIGNEDUP_LIST_ID on Mailchimp
  • add the email of a user who bought a book to a mailing list MAILCHIMP_PURCHASED_LIST_ID on Mailchimp

Like with any other new feature that needs to GET or POST data, the flow of data is usually as follows:

  • a user takes an action on some page
  • the page's method gets called, and that in turn calls an API method
  • the API method sends a request to a corresponding Express route on the server, which executes a handler function
  • typically the handler function of an Express route calls a static method for a data model, waits for data, and returns data to the browser (the client)

Using this blueprint above, we've already implemented data exchange between the browser and the server many times in this book. In Chapters 5 and 6, we added many pages and components, API methods, Express routes, and static methods related to our Admin user. In this section, we will write code related to our Customer user, who buys and reads a book.

Our first step, as per the blueprint, is to create a BuyButton component that we eventually import and add to our ReadChapter page. Inside this component, we will call the buyBook() API method.

link BuyButton component

In Chapter 2, we wrote our Header component as a stateless functional component (link) and MenuDrop component as a regular component (link).

The BuyButton component will use props (but not state). Thus, we should write it as a regular component. Check out components/MenuDrop.jsx to remember how we write regular components. To define our new BuyButton component with ES6 class:

class BuyButton extends React.Component { ... }

We export a single value with default export:

export default BuyButton;

Since the BuyButton component is for our Customer user only, create a book/8-begin/components/customer/BuyButton.jsx file inside the customer directory.

The structure of the BuyButton component with all necessary imports:

import React from 'react';
import PropTypes from 'prop-types';
import NProgress from 'nprogress';
import Button from '@material-ui/core/Button';
import { loadStripe } from '@stripe/stripe-js';

import { fetchCheckoutSessionApiMethod } from '../../lib/api/customer';

import notify from '../../lib/notifier';

const styleBuyButton = {
    margin: '10px 20px 0px 0px',
    font: '14px Roboto',
};

// define stripePromise

const dev = process.env.NODE_ENV !== 'production';
const port = process.env.PORT || 8000;
const ROOT_URL = `http://localhost:${port}`;

class BuyButton extends React.Component {
    // define componentDidMount

    // define onLoginClicked

    // define handleCheckoutClick

    render() {
        // define variables using props

        if (!book) {
            return null;
        }

        if (!user) {
            return (
                // Material-UI's Button component that triggers onLoginClicked
            );
        }

        return (
            // Material-UI's Button component that triggers handleCheckoutClick
        );
    }
}

// define BuyButton.propTypes and BuyButton.defaultProps

export default BuyButton;

It is up to you how you want to organize the component's methods - we placed BuyButton.render at the very end, but you may find an alternative structure to be more productive for you. For example, in our second book (SaaS Boilerplate Book), we placed constructor, getInitialProps, and React's lifecycle methods above render and the rest of the methods under render.

We discuss all parts of the BuyButton component in detail below.

  • define stripePromise. The @stripe/stripe-js package prescribes creating a Stripe instance like this:

https://github.com/stripe/stripe-js#loadstripe

Code from the above link:

import {loadStripe} from '@stripe/stripe-js';

const stripe = await loadStripe('pk_test_TYooMQauvdEDq54NiTphI7jx');

We will use it like this:

const dev = process.env.NODE_ENV && process.env.NODE_ENV !== 'production';

const stripePromise = loadStripe(
    dev ? Stripe_Test_PublishableKey : Stripe_Live_PublishableKey,
);

Later, inside the page's method handleCheckoutClick, we will define our stripe instance as await stripePromise.

We will define and use these three props later in this section.

  • define componentDidMount. The BuyButton component, by our design, will receive three values as props from the ReadChapter page:
    <BuyButton user={user} book={book} redirectToCheckout={redirectToCheckout} />

The default value for this.props.redirectToCheckout is false. Later when we add our BuyButton component to the ReadChapter page, we will discuss how we calculate the value for the redirectToCheckout prop. Here we simply want to check the value of this.props.redirectToCheckout. If this value is true, we want to run the handleCheckoutClick method. The purpose of redirectToCheckout, as you will learn later in this section, is to redirect a logged-in user to the Checkout page hosted by Stripe from our application's ReadChapter page without an additional click on Buy book button:

componentDidMount() {
    if (this.props.redirectToCheckout) {
        this.handleCheckoutClick();
    }
}
  • define onLoginClicked. If a user (and poential buyer) is logged out, then it makes no sense to redirect this user to the Checkout page. As we will discuss in the next bullet point, we need userId to create a unique Session object for every payment intent event. If a user is logged out, we want to redirect the user to the Google OAuth page instead of the Checkout page. How do we achieve this? We use the window.location.href web API that works in all major browsers:

https://developer.mozilla.org/en-US/docs/Web/API/Location/href

From the above link - Setting the value of href navigates to the provided URL. So we can navigate a user to our first Express route for Google OAuth API that will result in redirecting the user to our Google OAuth page:

window.location = `${ROOT_URL}/auth/google?redirectUrl=${redirectUrl}`;

Inside onLoginClicked, we check if user is truthy (exists and not undefined). If not truthy, we navigate the user to Google OAuth's Express route:

onLoginClicked = () => {
    const { user } = this.props;

    if (!user) {
        const redirectUrl = `${window.location.pathname}?buy=1`;
        window.location = `${ROOT_URL}/auth/google?redirectUrl=${redirectUrl}`;
    }
};

You may have noticed that the value for redirectUrl contains a buy query. This query gets evaluated by the ReadChapter page component. If the value is truthy, then the value for redirectToCheckout is true. This means that after a successful login event, a user will be redirected to the Checkout page without any additional action because of:

componentDidMount() {
    if (this.props.redirectToCheckout) {
        this.handleCheckoutClick();
    }
}
  • define handleCheckoutClick. As you can see from our blueprint structure, we need to define handleCheckoutClick that runs after a logged-in user clicks on the Buy book button.

Inside handleCheckoutClick, as per the Stripe docs, we call the API method fetchCheckoutSessionApiMethod to send a request to the server and create a Session object on our server. If successful, fetchCheckoutSessionApiMethod returns a Session object's id from the server to the browser. We then call stripePromise.redirectToCheckout({ sessionId }) that will either show an error or redirect a potential buyer to the Checkout page that is hosted by Stripe. The Session object will have data that is unique to the transaction, for example, userId (added on the server), bookId, and redirectUrl (sent from the browser) among other parameters:

handleCheckoutClick = async () => {
    NProgress.start();

    try {
        const { book } = this.props;
        const { sessionId } = await fetchCheckoutSessionApiMethod({
            bookId: book._id,
            redirectUrl: document.location.pathname,
        });

        // When the customer clicks on the button, redirect them to Checkout page hosted by Stripe.
        const stripe = await stripePromise;
        const { error } = await stripe.redirectToCheckout({ sessionId });

        if (error) {
            notify(error);
        }
    } catch (err) {
        notify(err);
    } finally {
        NProgress.done();
    }
};

Calling stripe.redirectToCheckout redirects a user from our ReadChapter page to Stripe's Checkout page. We are yet to define the fetchCheckoutSessionApiMethod API method and corresponding Express route /stripe/fetch-checkout-session.

  • define variables using props. We already accessed and used the value of the redirectToCheckout prop. The BuyButton component receives values for all three props from the ReadChapter page. Here we access the values of book and user props to make all necessary checks - to redirect a user to the Checkout page, both book and user must be truthy:

    const { book, user } = this.props;
  • Material-UI's Button component that triggers onLoginClicked. This part is straightforward to implement, since you already built multiple buttons when working on Github API and Admin-related UI. Here we use Material-UI's component Button as we did before. By specifying this.onLoginClicked value for the onClick prop, we make sure onLoginClicked executes when a user clicks on the button.

    <div>
      <Button
          variant="contained"
          color="primary"
          style={styleBuyButton}
          onClick={this.onLoginClicked}
      >
          {`Buy book for $${book.price}`}
      </Button>
      <p style={{ verticalAlign: 'middle', fontSize: '15px' }}>{book.textNearButton}</p>
      <hr />
    </div>
  • Material-UI's Button component that triggers handleCheckoutClick. This button is practically the same as the previous button, except for the value of the onClick prop. Here we want the handleCheckoutClick method to execute instead of the onLoginClicked method when a user clicks on the button.

    <div>
      <Button
          variant="contained"
          color="primary"
          style={styleBuyButton}
          onClick={this.handleCheckoutClick}
      >
          {`Buy book for $${book.price}`}
      </Button>
      <p style={{ verticalAlign: 'middle', fontSize: '15px' }}>{book.textNearButton}</p>
      <hr />
    </div>
  • define BuyButton.propTypes and BuyButton.defaultProps. This part is easy, since we already know that the BuyButton component receives three values for three props from the ReadChapter page. These three props are book, user, and redirectToCheckout:

    BuyButton.propTypes = {
      book: PropTypes.shape({
          _id: PropTypes.string.isRequired,
          name: PropTypes.string.isRequired,
          slug: PropTypes.string.isRequired,
          price: PropTypes.number.isRequired,
          textNearButton: PropTypes.string,
      }),
      user: PropTypes.shape({
          _id: PropTypes.string.isRequired,
          email: PropTypes.string.isRequired,
      }),
      redirectToCheckout: PropTypes.bool,
    };
    BuyButton.defaultProps = {
      book: null,
      user: null,
      redirectToCheckout: false,
    };

Stripe requires us to have a publishable key available on the browser. To do so, we need to define the global variable StripePublishableKey and make this variable available to our components/customer/BuyButton.jsx file. The problem is that env variables inside .env are only available on server.

There are multiple ways to make environmental variables universally available. Universally, in this context, means on both client (browser) and server.

If you pass environmental variables as part of a script command, like this:

StripePublishableKey=pk_test_12345 yarn dev

We simply prepended StripePublishableKey=pk_test_12345 to our yarn dev command. After you run the above command, you are able to access the StripePublishableKey environmental variable in your application as process.env.StripePublishableKey. However, process.env.StripePublishableKey is only accessible on the server, it's not universally accessible.

In Next.js, we can make the StripePublishableKey environmental variable available on the client (in addition to the server) by adding it to next.config.js at the root of our project. Create a new file, next.config.js, inside the book/8-begin/* folder with the following content:


You've reached the end of the Chapter 8 preview. To continue reading, you will need to purchase the book.

We are currently refactoring and updating the code in Builder Book. To reduce confusion, we have temporarily disabled the buy button. You can follow our progress here: https://github.com/builderbook/builderbook/issues/359

You can still buy our second book, SaaS Boilerplate: https://builderbook.org/books/saas-boilerplate/introduction-project-structure

format_list_bulleted
help_outline
lens