Add Stripe Payments to Your Django and React App

Turn your users into customers

Ewelina Bosko
Better Programming

--

Photo by rupixen.com on Unsplash

Introduction

Have you ever wanted to create an app in which the users would have to pay a few one-time fees and have a periodic subscription as well? On the one hand, you’d like to keep as much information as needed to have control over your customers and their payments. On the other hand, credit cards data are really sensitive, and storing them in your database would be very risky.

Fortunately, there’s a very interesting solution. Integrating your app with Stripe will save you time and provide a really clean and safe payment process. With its extensive API, you’ll be able to implement any type of money transaction you can imagine, and, moreover, all sensitive data will be secure in the Stripe cloud.

Our Goal

For the purpose of this article, we’ll create a simple app using the Django REST framework for our API back end and React for the front-end side.

Main flow of our app

  1. The user provides his email and credit/debit card data.
  2. We create a new Stripe customer based on the data the user provided.
  3. The user will be charged with a one-time fee for creating an account.
  4. The user will also be signed up for the monthly subscription to keep his account active.

Seems quite simple, doesn’t it? So let’s start!

Set Up the Django API

Assume we’ve already created a Django project called StripeApp with the Django REST framework library installed.

Let’s create a new app in our StripeAppproject root

python manage.py startapp payments

Our project structure should look like that (make sure you have the files with bolded names, and create missing ones, if needed):

- StripeApp/
- payments
- migrations/
- __init.py__
- admin.py
- apps.py
- models.py
- tests.py
- urls.py
- views.py
- stripeapp/
- __init.py__
- settings.py
- urls.py

Install the Stripe library

pip install --upgrade stripe

Sign up to the Stripe dashboard, and get your test keys

Create your Stripe account and get your test keys (publishable and secret) from the dashboard.

Stripe API Keys
Stripe dashboard: API keys

Copy your secret key to your Django project

# payments/views.pystripe.api_key = ‘sk_test_’ # your real key will be much longer

Make a test request to the Stripe API

Create a simple view making a Stripe payment to check if your key is valid.

Note: Right now, just copy the code and don’t try to understand what’s going on in this function. Everything will be explained a bit later.

# payments/views.py@api_view(['POST'])
def test_payment(request):
test_payment_intent = stripe.PaymentIntent.create(
amount=1000, currency='pln',
payment_method_types=['card'],
receipt_email='test@example.com')
return Response(status=status.HTTP_200_OK, data=test_payment_intent)

Connect the view with the URL in payments/urls.py.

# payments/urls.pyfrom django.conf.urls import url
from payments import views
urlpatterns = [
url(r'^test-payment/$', views.test_payment),
]

And define the payments URL prefix in stripeapp/urls.py.

# stripeapp/urls.pyurlpatterns = [
path('admin/', admin.site.urls),

# add this line
path('payments/', include('payments.urls'))
]

Send your new request (http://localhost:8000/payments/test-payment) using Postman. If you get a JSON object similar to the one below, it means you’ve just successfully sent your first request to Stripe.

{ 
"id": "pi_123", #you will have a unique id every time
"object": "payment_intent",
"amount": 1000,
"amount_capturable": 0,
"amount_received": 0,
"application": null,
"application_fee_amount": null,
...
}

Set Up the Front-End Project

Assume we’ve already created a React project called StripeAppFront.

Install React Stripe

npm install @stripe/react-stripe-js @stripe/stripe-js

Create the checkout form

Create a new React component that’ll display a form like this:

CheckoutForm

We want the user to provide his email and credit/debit card data. Although an email will be sent to our API, the card’s sensitive data will be processed by Stripe’s CardElement so we don’t store them anywhere insecurely. According to the Stripe documentation:

“Stripe Elements make collecting payment details more secure and help prevent malicious actors from stealing any sensitive information. We generate a secure iframe and isolate sensitive information from your site — eliminating entire classes of attacks — while still giving you full visual control.”

So let’s write some code:

In App.js, load you Stripe’s publishable bey and import Elements.

// App.js
import React from 'react';
import './App.css';
import {Elements} from '@stripe/react-stripe-js';
import {loadStripe} from "@stripe/stripe-js/pure";
import CheckoutForm from "./components/CheckoutForm";
const stripePromise = loadStripe('pk_test_');const App = () => (
<Elements stripe={stripePromise}>
<CheckoutForm />
</Elements>
);
export default App;

Create a new folder called components, and inside it, create the file CheckoutForm.js. It’ll be the most important component in the front-end project.

# components/CheckoutForm.jsimport {CardElement, useElements, useStripe} from "@stripe/react-stripe-js";
import React, {useState} from "react";
import ApiService from "../api";
const CheckoutForm = () => {
const [error, setError] = useState(null);
const [email, setEmail] = useState('');
const stripe = useStripe();
const elements = useElements();
// Handle real-time validation errors from the CardElement.
const handleChange = (event) => {
if (event.error) {
setError(event.error.message);
} else {
setError(null);
}
}
// Handle form submission.
const handleSubmit = async (event) => {
event.preventDefault();
};
return (
<form onSubmit={handleSubmit} className="stripe-form">
<div className="form-row">
<label htmlFor="email">Email Address</label>
<input className="form-input" id="email" name="name" type="email" placeholder="jenny.rosen@example.com" required
value={email} onChange={(event) => { setEmail(event.target.value)}} />
</div>
<div className="form-row">
<label for="card-element">Credit or debit card</label>
<CardElement id="card-element" onChange={handleChange}/>
<div className="card-errors" role="alert">{error}</div>
</div>
<button type="submit" className="submit-btn">
Submit Payment
</button>
</form>
);
};
export default CheckoutForm;

Create the payment method

Next we’ll use the Stripe API to create the PaymentMethod object and send its ID to our API. Once again, let’s take a quick look at the Stripe documentation and check out the definition of PaymentMethod:

PaymentMethod objects represent your customer’s payment instruments. They can be used with PaymentIntents to collect payments or saved to Customer objects to store instrument details for future payments.”

This means that PaymentMethod stores the user’s card data to use in payments transactions.

So let’s add a few lines to the handleSubmit method:

const handleSubmit = async (event) => {
event.preventDefault();
const card = elements.getElement(CardElement);

// add these lines
const {paymentMethod, error} = await stripe.createPaymentMethod({
type: 'card',
card: card
});

}

Note: You can check in the documentation to see the different types of PaymentMethod Stripe offers.

We also added the console.log operation to check how the PaymentMethod object really looks like. So open your page (http://localhost:3000) and pass the test data to the form.

Note: As a card number, you can use one of the testing cards provided by Stripe, (e.g., 4242 4242 4242 4242). The CVC and ZIP code can both be any number and the expiration date can be any future date.

Hit the Submit Payment button and take a look into the browser console. You can see an object containing quite a lot of data, but the most important thing is the card property (shown on the image below). We can see it isn’t storing the full card number, only the last four numbers. Now we’re sure nobody will be able to capture user’s card details.

PaymentMethod object
The PaymentMethod object

Send PaymentMethod.id to the Django API

Install the axios package to handle sending requests on the front-end side.

npm install axios --save

Then in the project root, create the file api.js, and create the ApiService class. Inside the newly created class, define the static method saveStripeInfo to send the POST request to our API (we’ll handle this request in a while):

// api.jsimport axios from "axios";
export const API_URL ='http://localhost:8000'
export const api = axios.create({
baseURL: API_URL,
headers: {
"Content-type": "application/json"
}
});
export default class ApiService{
static saveStripeInfo(data={}){
return api.post(`${API_URL}/payments/save-stripe-info/`, data)
}
}

Finally, call the method in the CheckoutForm component:

// CheckoutForm.jsconst handleSubmit = async (event) => {[...]
const {paymentMethod, error} = await stripe.createPaymentMethod({
type: 'card',
card: card
});
//add these lines ApiService.saveStripeInfo({
email, payment_method_id: paymentMethod.id})
.then(response => {
console.log(response.data);
}).catch(error => {
console.log(error)
})
};}

Implement the API Request Connecting With Stripe

Now it’s time to create an API view that’ll reach our main goals. In this view, we’re going to create a new Stripe Customer and charge his card with a one-time fee (PaymentIntent). Then, we’ll set up his monthly subscription (Subscription). Before we start coding, let’s take a look at the Stripe documentation and read about the mentioned objects.

Customer objects allow you to perform recurring charges, and to track multiple charges, that are associated with the same customer.”

“A PaymentIntent guides you through the process of collecting a payment from your customer. We recommend that you create exactly one PaymentIntent for each order or customer session in your system.”

Subscriptions allow you to charge a customer on a recurring basis.”

OK, now we know much more, don’t we? So let’s code it.

Create your customer

In payments/views.py, create the new method save_stripe_info. We’ll pass email and payment_method_id to Stripe so the user will be linked with the provided card data.

def save_stripe_info(request):
data = request.data
email = data['email']
payment_method_id = data['payment_method_id']

# creating customer
customer = stripe.Customer.create(
email=email, payment_method=payment_method_id)

return Response(status=status.HTTP_200_OK,
data={
'message': 'Success',
'data': {'customer_id': customer.id}
)

And add it to payments/urls.py:

urlpatterns = [
url(r'^test-payment/$', views.test_payment),
url(r'^save-stripe-info/$', views.save_stripe_info),

Now you can refresh our webpage and test it. Open you browser console, fill the form with test@gmail.com, use any test card number, and submit it.

In the console, you should see the Success status message along with the newly created customer ID.

Let’s check something else. Log into your Stripe dashboard, and open the Customers tab. Here, you can see a new customer with the email we provided in the form. If you click on it and open the customer details, you’ll see the same ID that was printed in the console.

Customers list in the Stripe dashboard
Stripe dashboard: Customer details

If you scroll down the customer-details page, you’ll find out that the payment-methods section contains the card data provided in the form.

Stripe dashboard: Client payment methods

This means we’ve just created our first Stripe customer.

Check if the customer already exists

OK, let’s slightly recap our app flow. The user is passing his email and credit card data, and based on that, we’re creating a new Stripe customer. In a moment, we’ll charge him, but what if the same person fills out the form for the second time? We definitely don’t want to have duplicated accounts in Stripe, so we need to validate if the provided email has already been used.

The Stripe API shares a method list that lists our customers. With an email parameter, we can fetch a filtered list and check if the email has been already linked to another customer. Let’s check it out:

# payments/views.py@api_view(['POST'])
def save_stripe_info(request):
data = request.data
email = data['email']
payment_method_id = data['payment_method_id']
extra_msg = '' # add new variable to response message
# checking if customer with provided email already exists
customer_data = stripe.Customer.list(email=email).data

# if the array is empty it means the email has not been used yet
if len(customer_data) == 0:
# creating customer
customer = stripe.Customer.create(
email=email, payment_method=payment_method_id)
else:
customer = customer_data[0]
extra_msg = "Customer already existed."
return Response(status=status.HTTP_200_OK,
data={'message': 'Success', 'data': {
'customer_id': customer.id, 'extra_msg': extra_msg}
})

We’ve just added fetching the list of users with their provided emails and checking if the returned array is empty or not. In the first case, it means no user was found, and we can create a new one. In the second case, we’re taking the first element of the array and returning it as our existing customer.

Note: Of course, the array returned from stripe.Customer.list may have more than one element, but for purpose of this article, we assume customer emails must be unique.

Let’s quickly check the current code. Refresh the browser, and send the same email again. You should see in the console the response contains the extra_msg key, and the customer_id is the same as the previous one. The Stripe dashboard also hasn’t changed and there’s still only one customer.

Let’s run one more test and send data with the email address test2@gmail.com.

What happened this time?

The console showed us a different customer_id and an empty extra_msg. Now in the Stripe dashboard, we can see a new customer with the test2@gmail.com email address.

Stripe dashboard: Customers ;ist

Our validation is working!

Note: Normally you should somehow handle email validation and show users some message or do anything else to keep the flow of your app. But in our case, we won’t focus on it, and all the operations will be just applied to existing or newly created customers.

Create PaymentIntent — charge the customer with a one-time fee

Let’s achieve our third goal and charge the customer with a one-time fee. As mentioned before, we’ll use the PaymentIntentobject for that.

# payments/views.py@api_view(['POST'])
def save_stripe_info(request):
[...]
else:
customer = customer_data[0]
extra_msg = "Customer already existed."
# add these lines
stripe.PaymentIntent.create(
customer=customer,
payment_method=payment_method_id,
currency='pln',
# you can provide any currency you want
amount=999)
# it equals 9.99 PLN

We passed to the stripe.PaymentIntent.create method our customerobject, the payment_method_id, the currency, and the amount of the fee. Let’s send the form again and check what will happen in the Stripe dashboard. When you open the Payments tab, you should see something like this:

Stripe dashboard: Payments

It looks like the payment has been created but not completed. What happened? Let’s open the payment details and scroll down to see the events and logs.

Stripe dashboard: Payment logs

It seems like the payment requires confirmation. Let’s get back to the documentation and read once again about creating PaymentIntents:

“After the PaymentIntent is created, attach a payment method and confirm to continue the payment. (…) When confirm=true is used during creation, it is equivalent to creating and confirming the PaymentIntent in the same call (...) This parameter defaults to false.”

It means that we have two possibilities: (1) Call the method stripe.PaymentIntent.confirm to confirm the payment or (2) set the parameter confirm=True in the stripe.PaymentIntent.create method.
Let’s choose the second option and modify our code a little bit:

# payments/views.pystripe.PaymentIntent.create(
customer=customer,
payment_method=payment_method_id,
currency='pln', # you can provide any currency you want
amount=1500, # I modified amount to distinguish payments
confirm=True)

Send the form once again, and refresh the Stripe dashboard. You should see a new payment with the Succeeded status:

And what about the logs?

Stripe Dashboard — Payment logs

Everything worked fine. The payment has been created and confirmed in one go. We’ve just accomplished out third goal.

Create Subscription

Finally, we can set up our user’s subscription. According to the Stripe documentation, Subscription needs to be linked with a Price object, and the Price is usually assigned to some Product.

To explain it better, let’s imagine different types of cinema tickets: regular, half-price, and free ones. In this case our Product would be the Ticket object with three different Prices. Just take a look at the the image below:

In our project, we’ll just create a Product called Monthly Subscription for our app with a Price 50.00 PLN paid monthly.

Stripe shares API requests to create all of the mentioned objects in the code. But there’s also another option — much faster and easier: using the dashboard. So let’s open the Products tab and click the Add Product button.

Stripe dashboard: Products

Then fill in the Name and (optionally) Description fields:

Stripe dashboard: New product form

And create a Price with a recurring (monthly) billing period:

Stripe dashboard: New pricing form

Great! Now you should see the new Product in your dashboard. Copy its Price’s ID (starting with price_ ), and edit our save_stripe_info.

# payments/views.py@api_view(['POST'])
def save_stripe_info(request):
[...]
stripe.Subscription.create(
customer=customer,
items=[
{
'price': 'price_' #here paste your price id
}
]
)

Everything seems fine. It should be working — shouldn’t it? Let’s check.

Oh no! Have you just gotten an error similar to the one below?

stripe.error.InvalidRequestError: Request req_123: 
This customer has no attached payment source or default payment method.

What does it mean? Let’s get back to the documentation, where you’ll find very important information:

Customer.invoice_settings.default_payment_method (optional)

“ID of a payment method that’s attached to the customer, to be used as the customer’s default payment method for subscriptions and invoices.”

This means when you want to charge your customer with recurring subscriptions, you have to assign default_payment_method first.

OK, let’s do it! Modify the stripe.Customer.create method:

customer = stripe.Customer.create(
email=email,
payment_method=payment_method_id,
invoice_settings={
'default_payment_method': payment_method_id
}

)

And try again with a new email:

You should see the Success message in your console, and when you refresh the Stripe dashboard, there should be a new payment:

Stripe dashboard: Payments

And when you click on it, you’ll even see the next payment date:

Stripe dashboard: Subscriptions

Great! You’ve just finished the app. Congratulations!

You can find the full code here: https://github.com/ewelina29/StripeApp/tree/master

Conclusion

I hope you’ll befriend Stripe, as it’s a very powerful library that can handle any payment nightmares you’ll encounter in your developer career.

I also recommend you read carefully through the Stripe documentation because there’s a lot of useful information that’ll help you avoid bugs and make your life easier.

--

--

Passionate about programming and writing :). I work as a software developer, mostly in Django and ReactJS. You can reach me here: https://pogromcykodu.pl