Builder Book logo

Book: Builder Book

  1. Introduction
  2. Set up Node.js project. VS code editor and lint. Set up Next.js project. Material-UI integration. Server-side rendering. Custom styles.
  3. HTTP. Express server. Next-Express server, nodemon. Index.getInitialProps. User data model and mongoose. MongoDB database and dotenv. Testing server-database connection. Retrieving document. Session and cookie. MenuWithAvatar and Header components.
  4. Authentication HOC. getInitialProps method. Login page and NProgress. Asynchronous execution. Promise.then. async/await. Google Oauth API infrastructure. setupGoogle, verify, passport, strategy. Express routes /auth/google, /oauth2callback, /logout. generateSlug. this. Set up at Google Cloud Platform.
  5. Testing method with Jest. Transactional email API with AWS SES service. Set up AWS SES service, security credentials. sendEmail method. Export and import syntax for server code. EmailTemplate data model. Update User.signInOrSignUp. Informational success/error messages. Notifier component. notify method.
  6. Book data model. Chapter data model. MongoDB index. API infrastructure and user roles. Read chapter API.
  7. Set up Github API infrastructure. Sync content API infrastructure. Missing UI infrastructure for Admin user. Two improvements. Testing.
  8. Table of Contents. Sections. Sidebar. Toggle TOC. Highlight for section. Active 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. Compression and security. Deploy project. 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.

We keep our book up to date with recent libraries and packages. Latest update Nov 2023.


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


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.

BuyButton component link

In Chapter 2, we wrote our Header component as a stateless functional component and MenuWithAvatar 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/MenuWithAvatar.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 '@mui/material/Button';
import { loadStripe } from '@stripe/stripe-js';

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

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

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

// define stripePromise
// define propTypes

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
        );
    }
}

BuyButton.propTypes = propTypes;
BuyButton.defaultProps = 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 ? NEXT_PUBLIC_STRIPE_TEST_PUBLISHABLEKEY : NEXT_PUBLIC_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 propTypes. 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:

    const 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,
    };
    const defaultProps = {
      book: null,
      user: null,
      redirectToCheckout: false,
    };
  • 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.


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

We keep our book up to date with recent libraries and packages. Latest update Nov 2023.