Builder Book

  1. Introduction
  2. App structure. 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. Debugging with Winston. Transactional emails. In-app notifications.
  6. Book and Chapter models. Internal API. Render chapter.
  7. Github integration. Admin dashboard. Testing Admin UX and Github integration.
  8. Table of Contents. Highlight for section. Hide Header. Mobile browser.
  9. BuyButton component. Buy book logic. ReadChapter page. Checkout flow. MyBooks page. Mailchimp API. Deploy app.

Chapter 8: BuyButton component. Buy book logic. ReadChapter page. Checkout flow. MyBooks page. Mailchimp API. Deploy app.

We keep the book up-to-date with the latest frameworks and packages.


In Chapter 8, you'll start with the codebase in the 8-start 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 logic
    - API method buyBook()
    - Express route '/buy-book'
    - Static method Book.buy()
    - stripeCharge() function
    - Purchase model

  • ReadChapter page
    - Excerpt and full content
    - BuyButton
    - Testing

  • Checkout flow

  • MyBooks page
    - API method getMyBookList()
    - Express route '/my-books'
    - Static method Book.getPurchasedBooks()

  • Mailchimp API

  • Deploy app
    - NODE_ENV and ROOT_URL
    - Security
    - SEO
    - Now


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
  • define all necessary code to make the buy button functional: API method, Express route, and static methods for our Book model, Stripe API, and Purchase model
  • add a BuyButton component to our ReadChapter page and test the checkout flow
  • show books that a user bought on the MyBooks page (user dashboard)
  • add the email of a user who bought a book to a mailing list on Mailchimp

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

  • there is a page or component that calls an API method
  • the API method sends a request to a particular route, which executes a matching Express route
  • the function inside the matching Express route calls a static method for a model, waits for data, and returns data to the client

By using this blueprint above, we've already implemented data exchange between client and server many times in this book. In Chapters 5 and 6, we added many pages/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 have both props and state. Thus, we should write it as a regular component. Check out components/MenuDrop.js 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 components/customer/BuyButton.js file.

The carcass of the BuyButton component with all necessary imports is:
components/customer/BuyButton.js :

import React from 'react';
import PropTypes from 'prop-types';
import StripeCheckout from 'react-stripe-checkout';
import NProgress from 'nprogress';

import Button from '@material-ui/core/Button';

import { buyBook } from '../../lib/api/customer';
import notify from '../../lib/notifier';

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

class BuyButton extends React.Component {
    // 1. propTypes and defaultProps

    // 2. constructor (set initial state)

    // 3. onToken function

    // 4. onLoginClicked function

    render() {
        // 5. define variables with props and state

        if (!book) {
            return null;
        }

        if (!user) {
            return (
                // 6. Regular button with onClick={this.onLoginClicked} event handler
            );
        }

        return (
            // 7. StripeCheckout button with token and stripeKey parameters
        );
    }
}

export default BuyButton;

We discuss all code snippets in detail below.

1) When we wrote our Header component as a stateless function (components/Header.js), we used Header.propTypes to specify types of props. But when we define a regular component with ES6 class, we specify types of props with static propTypes. For our BuyButton component, we should use the latter method, since we defined this component with ES6 class class BuyButton extends React.Component. You get:

static defaultProps = {
    book: null,
    user: null,
    showModal: false,
};

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

2) Similar to how there are two ways to specify types of props, there are two popular ways to set initial state. When you don't use a prop to set an initial state object, you can simply write the following without using constructor:

state = {
    open: false,
    anchorEl: undefined,
};

Open components/MenuDrop.js to see how this was done for our MenuDrop component.

However, in the case of our BuyButton component, we want to use a showModal prop to set the initial state. Thus, we should use constuctor:

constructor(props) {
    super(props);

    this.state = {
        showModal: !!props.showModal,
    };
}

Later in this chapter, we plan to pass a showModal prop from the ReadChapter page to our BuyButton component. showModal: !!props.showModal ensures that when showModal is true in the ReadChapter page, then showModal is true in the BuyButton component as well.

As we discussed in Chapter 5, calling super(props) makes props available inside constructor.

3) As discussed earlier in this book (Chapter 1, SSR) - for SSR, we call an API method inside getInitialProps(); for CSR, we call an API method inside componentDidMount() or inside an event-handling function.

In the BuyButton component, we want to call a buyBook() API method after our user takes an action (clicks the buy button). So we defintely don't want to do SSR and execute this buyBook() API method on the server. To execute buyBook() on the client, we would normally place buyBook() inside componentDidMount(). However, if placed inside componentDidMount(), this API method will be called right after the component mounts on the client. This is not what we want - we want to call our API method on a click event. Thus, we place buyBook() inside an onToken function that gets executed after a user clicks the buy button.

We will point onToken to an async anonymous function and use the try/catch construct along with async/await (as we did many times before in this book). If you want to refresh your memory, check out Chapter 3. Let's write an async anonymous function that calls the buyBook() API method and uses Nprogress and notify() to communicate success to our user. You should get:

onToken = async (token) => {
    NProgress.start();
    const { book } = this.props;
    this.setState({ showModal: false });

    try {
        await buyBook({ stripeToken: token, id: book._id });
        notify('Success!');
        NProgress.done();
    } catch (err) {
        NProgress.done();
        notify(err);
    }
};

When a user clicks the BuyButton component, our code calls the onToken function.

We pass book prop from the ReadChapter page to the BuyButton component. (We'll discuss this more in the next section when we add BuyButton to ReadChapter.) Thus, constant book is defined as this.props.book. Using ES6 object destructuring:

const { book } = this.props;

After a user clicks the buy button, we want to close the modal (Stripe modal with a form for card details):

this.setState({ showModal: false })

Then the code calls and waits for successful execution of our API method buyBook() that takes two arguments:

await buyBook({ stripeToken: token, id: book._id });

At this point, the rest of the code should be self-explanatory.

4) Why do we need an onLoginClicked function? If a user is logged in, then we simply call the onToken function and buyBook() API method. However, if a user is not logged in, we want to redirect the user to the /auth/google route (Google OAuth).

Similar to the book prop, we pass the user prop from our ReadChapter page to the BuyButton component:

const { user } = this.props

We check if a user object exists, and it if does not exist or is empty, we redirect to Google OAuth:

if (!user) {
    window.location.href = '/auth/google';
}

Put these two snippets together and you define onLoginClicked:

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

    if (!user) {
        window.location.href = '/auth/google';
    }
};

5) Define book and user constants (block-scoped variables) as this.props.book and this.props.user, respectively. Also define the showModal constant as this.state.showModal. Using ES6 object destructuring:

const { book, user } = this.props;
const { showModal } = this.state;

6) If a user is not logged in, we simply show a button from Material-UI. This button has an onClick event handler that points to {this.onLoginClicked}:

<div>
    <Button
        variant="raised"
        style={styleBuyButton}
        color="primary"
        onClick={this.onLoginClicked}
    >
    Buy for ${book.price}
    </Button>
</div>

7) If a user is logged in, we again show a button, but now we wrap it with <StripeCheckout>...</StripeCheckout> from the react-stripe-checkout package.

Take a look at this example of usage. The StripeCheckout component requires two props: stripeKey and token. Other props are optional, and you are familiar with all of them except desktopShowModal. This prop controls whether the Stripe modal is open or closed. Read more about desktopShowModal.

Wrap the Material-UI <Button> with <StripeCheckout>:

<StripeCheckout
    stripeKey={StripePublishableKey}
    token={this.onToken}
    name={book.name}
    amount={book.price * 100}
    email={user.email}
    desktopShowModal={showModal || null}
>
    <Button variant="raised" style={styleBuyButton} color="primary">
        Buy for ${book.price}
    </Button>
</StripeCheckout>

As you can see from above, Stripe requires us to pass a publishable key to our component on the client. To do so, we need to define the global variable StripePublishableKey and make this variable available to our components/customer/BuyButton.js 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 and server. Next.js provides at least two examples.

The first solution is to use the babel plugin babel-plugin-transform-define. With this plugin, we can create a file that contains universally available variables. This solution requires us to create a configuration file for Babel (.babelrc), so we can extend Babel functionality with the babel-plugin-transform-define plugin. As a team, we strive to keep as few configurations as possible. Otherwise, we may find ourselves in a so-called 'configuration hell'.

The second solution does not require configuring Babel. Instead, it requires us to pass environmental variables as part of a 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.

We can make the StripePublishableKey environmental variable available on the client by adding it to _document.js. When a user loads a page for the first time, our Next.js app renders _document.js on the server and sends it to the client together with any environmental variables.

Open your pages/_document.js file. Under the imports section, add the following code snippet:

const { StripePublishableKey } = process.env;
// console.log(process.env.StripePublishableKey);

const env = { StripePublishableKey };
// console.log(env); 

The first line simply defines our new variable StripePublishableKey, which points to process.env.StripePublishableKey. The fourth line, creates an env.StripePublishableKey parameter inside the env object. This parameter's value is StripePublishableKey (defined on the first line).

We used ES6 object destructuring for both of the above lines.

We also used object shorthand on the fourth line - we wrote const env = { StripePublishableKey } instead of const env = { StripePublishableKey: StripePublishableKey }.

Start your app with StripePublishableKey=pk_test_12345 yarn dev. Uncomment the console.log() statements and reload the page so your app executes _document.js. Your terminal will print:

pk_test_12345
{ StripePublishableKey: 'pk_test_12345' }

This terminal output means that we successfully accessed our StripePublishableKey value using process.env and successfully created an env object that has the env.StripePublishableKey name-value pair.

The next step is to add an env object to the MyDocument component. Find this location in page/_document.js:


<Main />
<NextScript />

Modify like this:


<Main />
{/* eslint-disable-next-line react/no-danger */}
<script dangerouslySetInnerHTML={{ __html: `ENV = ${htmlescape(env)}` }} />
<NextScript />

The first of these newly added lines simply disables the Eslint warning for dangerouslySetInnerHTML. The second line adds a script tag to the Document component.

Two explanatory notes:

  • You may have noticed two underscore symbols on each side of __ENV__. We've added underscores to avoid name collision and to indicate that tge variable is used on the browser. This is a practical convention, read more about it here.

  • The JavaScript object env can't be rendered as HTML since it's not a string. We need to stringify env. The proper way to stringify it is to use htmlescape() over JSON.stringify() since JSON is not exactly a subset of JavaScript. Some characters inside the string that are accepted by JSON are not accepted by JavaScript.

Go to your browser, open Dev Tools > Elements, and find our newly added script tag:
Builder Book

On the client (browser console), we have an __ENV__ object with the expected value for StripePublishableKey.

In Next.js, the initial load is server-side rendered and subsequent loads are client-side rendered. Thus we need to write conditional code to account for this conditional behavior.

Create a lib/env.js file that contains only a single line of code:

export default (typeof window !== 'undefined' ? window.__ENV__ : process.env);

If a window objects exists (typeof window !== 'undefined'), then this file exports window.__ENV__, which is {"StripePublishableKey":"pk_test_12345"}. That's because in our browser window, we have a script tag with content __ENV__ = {"StripePublishableKey":"pk_test_12345"}.

Else, when a window object does not exist (server environment), the file exports the server-side environment, which is process.env.

That's pretty much it! We've covered both cases for page rendering, server-side and client-side.

To continue reading, buy this book.

We keep the book up-to-date with the latest frameworks and packages.


format_list_bulleted
help_outline