Building a payments system with React and Stripe - LogRocket Blog

November 14, 2019 0 Comments

Building a payments system with React and Stripe - LogRocket Blog

 

 

Stripe is a suite of APIs that makes it easy to set up online payment processing, and today, we’ll be leveraging it to create a bare-bones payment system using React.

Whether you’re implementing a subscription-based service, an e-commerce store, or a crowdfunding solution, Stripe offers the flexibility to get it done. We’re going to build a small proof-of-concept payments system to enable one-time purchases on a website.

By the end of this tutorial, you should be able to set up a backend and frontend for processing online payments in your React app.

Requirements to follow along

This tutorial requires that you have the following:

  1. Node installed on your computer
  2. A Stripe developer account
  3. Basic knowledge of Express
  4. Basic knowledge of React Hooks

If you do not have Node installed, you can get the latest version from the official website. All the code written in this tutorial can be accessed here.

Stripe setup

If you do not have a Stripe developer account, you can get started for free by signing up for an account here. After signing up, complete the following steps to get set up:

  • Select Developer integrations on the How do you want to get started? modal
  • Select Accept payments only on the next modal
  • Check the One-time payments option on the next modal
  • Finally, check Build a custom payment flow on the last modal

You should now have a base account set up. You can update the name of the account by clicking the Add a name link at the top left of the page.

You’ll need to copy your Publishable and Secret keys from the dashboard and store them somewhere, because we’ll need them very soon.

Copy Your Test Publishable And Secret Keys
Copy your test Publishable and Secret keys.

Building the payment server

Before we go ahead with building the React app, we’ll need to set up a server to handle payment requests.

We’ll need to set up a RESTful endpoint on an Express server, which will act as a middleman between our React code and the Stripe backend. If you’ve never built an API before, don’t worry, it’ll be pretty basic as we’re not implementing a production-ready backend here.

Let’s get started.

  1. Create a new project folder and name it whatever you want (I’m going with react-stripe-payment)
  2. Open your terminal in the folder and run npm init -y
  3. Install the dependencies by running npm install express dotenv body-parser stripe
  4. Create a folder src under the root folder by running mkdir src

server.js

Let’s create the server to listen for payment requests. Create a new file called server.js under the src folder and paste the following in it:

const path = require('path')
const express = require('express')
const bodyParser = require('body-parser')
const postCharge = require('./stripe')
require('dotenv').config() const app = express()
const router = express.Router()
const port = process.env.PORT || 7000 router.post('/stripe/charge', postCharge)
router.all('*', (_, res) => res.json({ message: 'please make a POST request to /stripe/charge' })
)
app.use((_, res, next) => { res.header('Access-Control-Allow-Origin', '*') res.header( 'Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept' ) next()
})
app.use(bodyParser.json())
app.use('/api', router)
app.use(express.static(path.join(__dirname, '../build'))) app.get('*', (_, res) => { res.sendFile(path.resolve(__dirname, '../build/index.html'))
}) app.listen(port, () => console.log(`server running on port ${port}`))

Let’s break down this file section by section.

const path = require('path')
const express = require('express')
const bodyParser = require('body-parser')
const postCharge = require('./stripe')
require('dotenv').config()

Here, we’re importing the required packages. You’ll notice that they are all third-party imports except for postCharge, which is being imported from a file called stripe. We’ll create that file later.

dotenv allows us to read sensitive information from the Node process so we don’t have to hardcode secret values in our code.

const app = express()
const router = express.Router()
const port = process.env.PORT || 7000

We’re initializing a new Express instance into a variable called app. We then create a new Router instance and store it in a variable called router. This is what we’ll use to define the payment endpoint.

Finally, we initialize a new variable called port and assign it a value from the Node process (process.env.PORT), and if that is undefined, it is assigned 7000.

router.post('/stripe/charge', postCharge)
router.all('*', (_, res) => res.json({ message: 'please make a POST request to /stripe/charge' })
)
app.use((_, res, next) => { res.header('Access-Control-Allow-Origin', '*') res.header( 'Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept' ) next()
})
app.use(bodyParser.json())
app.use('/api', router)
app.use(express.static(path.join(__dirname, '../build')))

Remember the router we initialized earlier? On the first line, we set up an endpoint called /stripe/charge and assign postCharge to handle all POST requests to this route.

We then catch all other requests to the server and respond with a JSON object containing a message directing the user to the appropriate endpoint.

Next, we define a middleware on the app instance to enable CORS for all requests. On the next line, we attach another middleware that enables us to parse JSON objects from the request body.

Then we tell our app instance to use the router instance to handle all requests to the /api endpoint. Finally, we tell Express to serve up the /build folder. This folder will hold the transpiled code for the app’s frontend.

app.get('*', (_, res) => { res.sendFile(path.resolve(__dirname, '../build/index.html'))
}) app.listen(port, () => console.log(`server running on port ${port}`))

Here, we’re telling the app instance to handle all GET requests by serving the index.html file located in the /build folder. This is how we’ll serve the frontend in production.

Finally, we spin up the server on the port we defined earlier and log a message to the console on a successful startup.

stripe.js

We’ll then create the postCharge handler we required in server.js above. Under the src folder, create a new file, stripe.js, and paste the following in it:

const stripe = require('stripe')(<your_secret_key>) async function postCharge(req, res) { try { const { amount, source, receipt_email } = req.body const charge = await stripe.charges.create({ amount, currency: 'usd', source, receipt_email }) if (!charge) throw new Error('charge unsuccessful') res.status(200).json({ message: 'charge posted successfully', charge }) } catch (error) { res.status(500).json({ message: error.message }) }
} module.exports = postCharge

Let’s break it down.

const stripe = require('stripe')(<your_secret_key>)

Here, we initialize a new Stripe instance by requiring the stripe package and calling it with the secret key we copied earlier as a string. We save this instance in a variable called stripe.

async function postCharge(req, res) { try { const { amount, source, receipt_email } = req.body const charge = await stripe.charges.create({ amount, currency: 'usd', source, receipt_email })

We then create a new function called postCharge. This function is a request handler, so we have to take in two parameters: req and res.

We then open a try catch block inside this function. We destructure all the variables we’re expecting to be sent along with the request from the request object; in this case, those variables are amount, source, and receipt_email.

We then create a new variable called charge. This variable holds the result of an asynchronous call to the Stripe API to create a new charge (stripe.charges.create).

if (!charge) throw new Error('charge unsuccessful')

If the result of the Stripe call is a falsy value — undefined, in this case — it means our payment request failed, and so we throw a new error with the message “charge unsuccessful.”

res.status(200).json({ message: 'charge posted successfully', charge
})

Otherwise, we respond to the request with a 200 status code and a JSON object containing a message and the charge object.

} catch (error) { res.status(500).json({ message: error.message }) }
} module.exports = postCharge

In the catch block, we intercept all other errors and send them to the client with a 500 status code and a message containing the error message.

At the end of the file, we export the postCharge function using module.exports.

That is all there is to the payment server. Of course, this isn’t production-ready and should not be used in a real application processing real payments, but it is enough for our current use case. Let’s move on to the frontend.

Building the frontend

Since we’re done building the payments server, it’s time to flesh out the frontend. It’s not going to be anything fancy since I’m trying to keep this tutorial bite-sized. Here are the different components of the app:

  • A router component
  • A products list component
  • A checkout form component

Let’s get started.

  1. Run the following command to install the required packages:
    npm install axios babel-polyfill history parcel parcel-bundler react react-dom react-router-dom react-stripe-elements
  2. In the project root, run the following command:
    mkdir public && touch public/index.html

This will create a folder called public and create an index.html file in this new folder. Open the index.html file and paste the following:

<!DOCTYPE html>
<html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="description" content="React + Stripe" /> <title>React and Stripe Payment</title> </head> <body> <noscript>You need to enable JavaScript to run this app.</noscript> <div id="root"></div> <script src="https://js.stripe.com/v3/"></script> <script src="../src/index.js"></script> </body>
</html>

If you’re already familiar with React, this should be nothing new; this is simply the entry point of our app. Also notice that we import the Stripe SDK in the first <script> tag — the Stripe SDK import must come before our own code.

Inside the src folder, run the following command:

touch src/index.js && touch src/products.js

Open index.js and paste the following:

import React from 'react'
import ReactDOM from 'react-dom'
import App from './components/App'
import 'babel-polyfill' const rootNode = document.querySelector('#root')
ReactDOM.render(<App />, rootNode)

Now we need to get the list of products from somewhere. Usually, this would be from a database or some API, but for this simple use case, we can just hardcode two or three products in a JavaScript file. This is why we need products.js. Open it and paste the following:

export const products = [ { name: 'Rubber Duck', desc: `Rubber ducks can lay as many eggs as the best chicken layers, and they are fun to watch with their antics in your backyard, your barnyard, or your pond.`, price: 9.99, img: 'https://encrypted-tbn0.gstatic.com/images?q=tbn%3AANd9GcSqkN8wkHiAuT2FQ14AsJFgihZDzKmS6OHQ6eMiC63rW8CRDcbK', id: 100 }, { name: 'Chilli Sauce', desc: `This Chilli Sauce goes well with some nice roast rubber duck. Flavored with the best spices and the hottest chillis, you can rest assured of a tasty Sunday rubber roast.`, price: 12.99, img: 'https://encrypted-tbn0.gstatic.com/images?q=tbn%3AANd9GcRTREm1dEzdI__xc6O8eAz5-4s88SP-Gg9dWYMkBKltGMi84RW5', id: 101 }
]

This is an array of products that are available for purchase. You can add as many as you like and then move on to creating the components.

Run the following command from the project root: mkdir src/components. This will create a new folder called components inside the src folder to hold our React components. Let’s go ahead and create the first component.

App.jsx

This is the root component and will be responsible for routing to the various pages we have in our app. Create a new file called App.jsx inside the components folder and paste in the following:

import React, { useState } from 'react'
import { Router, Route, Switch } from 'react-router-dom'
import { createBrowserHistory } from 'history'
import Products from './Products'
import Checkout from './Checkout'
import { products } from '../products' const history = createBrowserHistory() const App = () => { const [selectedProduct, setSelectedProduct] = useState(null) return ( <Router history={history}> <Switch> <Route exact path="/" render={() => ( <Products products={products} selectProduct={setSelectedProduct} history={history} /> )} /> <Route path="/checkout" render={() => ( <Checkout selectedProduct={selectedProduct} history={history} /> )} /> </Switch> </Router> )
} export default App

Let’s break it down.

import React, { useState } from 'react'
import { Router, Route, Switch } from 'react-router-dom'
import { createBrowserHistory } from 'history'
import Products from './Products'
import Checkout from './Checkout'
import { products } from '../products' const history = createBrowserHistory()

This first part is just a bunch of dependency imports. The first three imports are required for any single-page React application. The next two imports are custom components that we’ll write later on. The last import is the hardcoded products we created earlier. We’ll pass it down as a prop to the Products component.

Finally, we create a new history instance from the history package and save it in a variable aptly named history.

const App = () => { const [selectedProduct, setSelectedProduct] = useState(null) return ( <Router history={history}> <Switch> <Route exact path="/" render={() => ( <Products products={products} selectProduct={setSelectedProduct} history={history} /> )} /> <Route path="/checkout" render={() => ( <Checkout selectedProduct={selectedProduct} history={history} /> )} /> </Switch> </Router> )
} export default App

We then create a new functional component called App. App has a state variable called selectedProduct, which holds the product currently selected to be purchased.

We return a Router instance that defines all the routes and their respective components.

In the first route, /, we render the Products component and pass in three props: the list of hardcoded products, a function to set a product in the App state, and the history object to enable us to navigate to new pages without breaking the browser history.

In the second route, /checkout, we render the Checkout component and pass in a couple props: the currently selected product and the history object.

At the end of the file, we export the App component as the default export.

Products.jsx

This component is responsible for rendering the list of products to the DOM, and it’s fairly simple. Create a new file called Products.jsx in the components folder and paste in the following:

import React from 'react'
import './Products.scss' const Products = ({ products, selectProduct, history }) => { const handlePurchase = prod => () => { selectProduct(prod) history.push('/checkout') } return products.map(prod => ( <div className="product" key={prod.id}> <section> <h2>{prod.name}</h2> <p>{prod.desc}</p> <h3>{'$' + prod.price}</h3> <button type="button" onClick={handlePurchase(prod)}> PURCHASE </button> </section> <img src={prod.img} alt={prod.name} /> </div> ))
} export default Products

Note: You can get the Products.scss contents from here.

Let’s break it down.

const Products = ({ products, selectProduct, history }) => { const handlePurchase = prod => () => { selectProduct(prod) history.push('/checkout') }

We start off defining a functional component that takes in three props:

  1. products
  2. selectProduct
  3. history

products is the array of products we hardcoded earlier. We’ll be mapping over this array later on to render the individual products to the DOM.

selectProduct is a function that takes in a single product object. It updates the App component’s state to hold this product so that the Checkout component can access it through its props.

history is the history object that will allow us to navigate to other routes safely.

Then we define the handlePurchase function, which will be called when a user wants to purchase a certain product. It takes in a single parameter, prod, and calls selectProduct with this parameter. After calling selectProduct, it then navigates to the /checkout route by calling history.push.

return products.map(prod => ( <div className="product" key={prod.id}> <section> <h2>{prod.name}</h2> <p>{prod.desc}</p> <h3>{'$' + prod.price}</h3> <button type="button" onClick={handlePurchase(prod)}> PURCHASE </button> </section> <img src={prod.img} alt={prod.name} /> </div> ))
} export default Products

It’s time to render the products to the DOM. We map over the products array and, for each product in the array, return a bunch of JSX. The JSX should be pretty straightforward and will result in the following image being painted in the screen:

Rubber Duck And Chilli Sauce Product Images

Checkout.jsx

Next, we want to create the checkout page where the user will be routed to when they click on the PURCHASE button on a product.

Create a Checkout.jsx file under the components folder and paste the following in it:

import React, { useEffect } from 'react'
import { StripeProvider, Elements } from 'react-stripe-elements'
import CheckoutForm from './CheckoutForm' const Checkout = ({ selectedProduct, history }) => { useEffect(() => { window.scrollTo(0, 0) }, []) return ( <StripeProvider apiKey="pk_test_UrBUzJWPNse3I03Bsaxh6WFX00r6rJ1YCq"> <Elements> <CheckoutForm selectedProduct={selectedProduct} history={history} /> </Elements> </StripeProvider> )
} export default Checkout

This is when we begin to bring Stripe into the mix. In the second line, we’re importing something called StripeProvider and another thing called Elements from the react-stripe-elements package we installed at the beginning of this section.

StripeProvider is required for our app to have access to the Stripe object, and any component that interacts with the Stripe object must be a child of StripeProvider.

Elements is a React component that wraps around the actual checkout form. It helps group the set of Stripe Elements (more on this in a bit) together and makes it easy to tokenize all the data from each Stripe Element.

The Checkout component itself is fairly simple. It takes in two props, selectedProduct and history, which it passes on to a CheckoutForm component we’ll create next.

There’s also a useEffect call that scrolls the document to the top when the page mounts for the first time. This is necessary because react-router-dom preserves the previous scroll state when you switch routes.

Also notice that we’re passing a prop, apiKey, to StripeProvider. This key is the publishable key you copied earlier when setting up Stripe. Note that this prop is required because it serves as a way to authenticate your application to the Stripe servers.

CheckoutForm.jsx

This is the last component we’ll be creating, and it’s also the most important. The CheckoutForm component will hold the inputs for getting the user’s card details as well as actually making a call to the backend to process the payment charge.

Create a new file called CheckoutForm.jsx inside the components directory. We’re going to go through the content of this file section by section.

import React, { useState } from 'react'
import { Link } from 'react-router-dom'
import { CardNumberElement, CardExpiryElement, CardCVCElement, injectStripe
} from 'react-stripe-elements'
import axios from 'axios'
import './CheckoutForm.scss'
...to be continued below...

First, we import the required packages we’ll be working with into the file. Notice the imports from the react-stripe-elements package. This is a good time to talk more about Stripe Elements.

Stripe Elements are a set of prebuilt UI elements that allow you to collect your user’s card information without managing such sensitive information yourself.

The react-stripe-elements package is a wrapper for Stripe Elements that exposes these elements as React components you can just plug into your app — no need to create them from scratch.

We are importing some of these components into this file along with a HOC component, injectStripe.

injectStripe basically takes the Stripe object initialized in the StripeProvider component and “injects” the object into any component wrapped with it. This is how we’ll get access to the Stripe Object.

We then import a package called axios. Axios is just a promise-based HTTP client for the browser that we’re going to use to communicate with our payments server.

You can get the contents of CheckoutForm.scss from here.

...continued...
const CheckoutForm = ({ selectedProduct, stripe, history }) => { if (selectedProduct === null) history.push('/') const [receiptUrl, setReceiptUrl] = useState('') const handleSubmit = async event => { event.preventDefault() const { token } = await stripe.createToken() const order = await axios.post('http://localhost:7000/api/stripe/charge', { amount: selectedProduct.price.toString().replace('.', ''), source: token.id, receipt_email: 'customer@example.com' }) setReceiptUrl(order.data.charge.receipt_url) }
...to be continued...

Next up is the actual CheckoutForm component itself. It takes in three props:

  1. selectedProduct
  2. stripe
  3. history

selectedProduct is the product the user clicked on to purchase. It’s coming from the root App component’s state and is being passed down as props.

stripe is the actual Stripe object that is being “injected” as a prop by the injectStripe HOC we imported. You already know what history does.

The first thing we do in the component is check whether selectedProduct actually exists. If it doesn’t, we route the user to the homepage. In a production-grade app, this would probably be handled by a route guard HOC.

We then define a new piece of state to hold the receipt URL for successful payments. It will initially be empty.

Next, we define a function called handleSubmit, which will be called when the checkout form is submitted (i.e., when the Pay button is clicked). Let’s go through this function.

Firstly, we prevent the default behavior of the form element so that the page doesn’t refresh.

Then we destructure a token value from the result of an async call to stripe.createToken. createToken tokenizes the card information from the form and sends it to the Stripe server. It then returns a token object, where you can get a token.id value as an alias for the actual card info. This ensures that you never actually send the user’s card details to your payment server.

Secondly, we make an HTTP POST request to localhost:7000/api/stripe/charge with a request body containing three things:

  1. amount
  2. source
  3. receipt_email

amount is the price of the item being purchased. We have to convert it to a string and remove all special characters like “.” and “,”. This means that a cost of $9.99 will get sent to the payment server as 999.

source is where the payment will be charged. In our case, it will be the ID of the token we just generated.

receipt_email is where the receipt of the payment will be sent. It is usually the customer’s email address, but in our case, we’re just hardcoding it because, again, we’re not implementing authentication.

After the request is done, we grab the URL of the receipt from the response object and set it to state. This is assuming that there are no errors, so in a production-grade app, you would usually implement error handling.

...continued...
if (receiptUrl) { return ( <div className="success"> <h2>Payment Successful!</h2> <a href={receiptUrl}>View Receipt</a> <Link to="/">Home</Link> </div> ) }
...to be continued...

Immediately after the handleSubmit function, we have an if check to see if there’s a receiptUrl in the state. If there is, we want to render a div containing a success message and a link to view the receipt as well as a link back to the homepage.

...continued... return ( <div className="checkout-form"> <p>Amount: ${selectedProduct.price}</p> <form onSubmit={handleSubmit}> <label> Card details <CardNumberElement /> </label> <label> Expiration date <CardExpiryElement /> </label> <label> CVC <CardCVCElement /> </label> <button type="submit" className="order-button"> Pay </button> </form> </div> )
} export default injectStripe(CheckoutForm)

Otherwise, we’re going to render the actual checkout form. We’re using the prebuilt Elements components instead of recreating them from scratch and having to manage sensitive information.

At the end of this file, we wrap the CheckoutForm component in the injectStripe HOC so that we have access to the Stripe object we use in the component.

Testing our app

Let’s go through what we’ve accomplished so far.

  1. We’ve created a payments server that communicates with Stripe
  2. We’ve created a homepage to list our products
  3. We’ve created a checkout page to capture the user’s payment details
  4. We’ve created a handleSubmit function to send a request to the server to process a payment charge

We just about have everything set up, so it’s time to actually run our app and see if we’re able to purchase a Rubber Duck. We have to add our scripts first, so open the package.json file and replace the “scripts” section with the following:

"scripts": { "build": "parcel build public/index.html --out-dir build --no-source-maps", "dev": "node src/server.js & parcel public/index.html", "start": "node src/server.js" },

Open your terminal and run npm run dev. This should start the payments server and expose the frontend on port 1234. Open your browser, navigate to http://localhost:1234, and follow the steps below:

  • Click on the PURCHASE button on any product
  • In the checkout page, fill in 4242 4242 4242 4242 for the Card details field
  • Fill in any expiration date and choose a random CVC value
  • Click on Pay

If everything goes well, you should see a Payment Successful message with links to view your receipt and go back to the homepage.

To confirm payment, log in to your Stripe dashboard, click on Payments, and you should see your payment there.

Stripe Payments Dashboard

Conclusions

This is a very simplified (and definitely not suitable for production) implementation of a payments system using Stripe. Let’s summarize the necessary components that are required for a real, production-ready implementation in case you’d like to try it out.

  1. A more robust payment server with proper authentication (JWT comes to mind) and validation
  2. A flow to capture and save customer details for easier billing in the future
  3. Utilize Stripe’s fraud detection service to decide which payments should be processed
  4. A much better UI and UX on the client side
  5. Robust error handling on the client side

While this tutorial should be enough to get you started with the basics, it’s not nearly enough to build a fully fledged payments solution, so please spend some time in the Stripe Docs, as they’re really well put together.

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.

.


Tag cloud