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
BuyButtoncomponent - make this
BuyButtonbutton 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
BuyButtoncomponent to ourReadChapterpage and test the checkout flow - show list of books that a user bought on the user's
MyBookspage - add the email of a user who signed up in our web application to a mailing list
MAILCHIMP_SIGNEDUP_LIST_IDon Mailchimp - add the email of a user who bought a book to a mailing list
MAILCHIMP_PURCHASED_LIST_IDon 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-jspackage 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
BuyButtoncomponent receives three values for three props from theReadChapterpage. 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
BuyButtoncomponent, by our design, will receive three values as props from theReadChapterpage:<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
userIdto create a uniqueSessionobject 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.hrefweb 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
handleCheckoutClickthat runs after a logged-in user clicks on theBuy bookbutton.
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
redirectToCheckoutprop. TheBuyButtoncomponent receives values for all three props from theReadChapterpage. Here we access the values ofbookanduserprops to make all necessary checks - to redirect a user to the Checkout page, bothbookandusermust 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
Buttonas we did before. By specifyingthis.onLoginClickedvalue for theonClickprop, we make sureonLoginClickedexecutes 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
onClickprop. Here we want thehandleCheckoutClickmethod to execute instead of theonLoginClickedmethod 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.