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.
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 ourReadChapter
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 theReadChapter
page. These three props arebook
,user
, andredirectToCheckout
: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 theReadChapter
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 uniqueSession
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 thewindow.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 theBuy 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. TheBuyButton
component receives values for all three props from theReadChapter
page. Here we access the values ofbook
anduser
props to make all necessary checks - to redirect a user to the Checkout page, bothbook
anduser
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 specifyingthis.onLoginClicked
value for theonClick
prop, we make sureonLoginClicked
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 thehandleCheckoutClick
method to execute instead of theonLoginClicked
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.