PayPal Payments Tutorial with Django and React
In this tutorial we're going to build a basic page with React to accept payments using PayPal.
Introduction
Services like Gumroad are useful for selling access to digital products like Airtable databases, Notion templates and other private links. In this tutorial you will learn how to sell digital products using PayPal payments.
We will use PayPal Checkout to handle the payments and use Django to provide access to the private content.
If you'd prefer to watch the video instead of read:
Prerequisites
This tutorial is better for learners who are already comfortable working with Django and React. If you are new to Django consider watching Getting Started with Django first. To continue with the tutorial make sure:
- You have a good understanding of the basics of Django
- You have a good understanding of the basics of React
- You are familiar with TypeScript. However, it's not required
Setting up your PayPal business account
Go to the PayPal website and create an account. When creating your account make sure you select the type of account as a "business account" in order to have all of the necessary tools visible in your account. If you already have an existing PayPal account that is a personal account you can always upgrade it to a business account in your profile settings.
PayPal developer documentation
Once you have an account head to the PayPal developer website. There are two important links in the navigation bar; Docs
and Tools
. Under Docs
you will find all of the documentation and tutorials to start accepting payments. Under Tools
you will find more information about PayPal's sandbox and API tools.
Hovering over Docs
, click on the Overview
. Now, in the sidebar, under Developer Resources
click on Get Started
. You should now be here. This page is explains how to obtain API credentials. We will not be working with the API as much as this documentation shows. However, follow the first four steps shown. Those steps are as follows:
- Log in to the Developer Dashboard with your PayPal account.
- Under the DASHBOARD menu, select My Apps & Credentials.
- Make sure you're on the Sandbox tab to get the API credentials you'll use while you're developing code. After you test and before you go live, switch to the Live tab to get live credentials.
- Under the App Name column, select Default Application, which PayPal creates with a new Developer Dashboard account. Select Create App if you don't see the default app.
Under your Default Application you will see your credentials. Take note of your Client ID and Secret. You will need both of these later.
PayPal Sandbox Account
When developing and testing you can use fake accounts to login to PayPal and pay for things. In the **DASHBOARD **menu select Accounts. You will see two accounts. One business type account and one personal type account. You can use the business account to receive funds and the personal account as the account that pays. You can login to both of these accounts. Click on the (...) button to View/Edit the account. There you will find the password for each sandbox account.
Django Project Setup
We are going to use the Cookiecutter Django package to bootstrap our Django project. This is a great tool for generating production-ready Django projects while not having to deal with as much boilerplate code. If you're not familiar with it you can read more on Dockerizing Django using Cookiecutter.
Install Cookiecutter
To use Cookiecutter Django you'll need to have Cookiecutter installed. Cookiecutter is a Python package that is recommended to be globally installed so that you can use it at any point in your terminal.
Install Cookiecutter:
pip install cookiecutter
Create a Virtual Environment
The type of virtual environment is up to you. I prefer virtualenv.
virtualenv venv
source venv/bin/activate
Create the Django Project
Inside your project folder run this command to use the Cookiecutter Django template:
cookiecutter gh:pydanny/cookiecutter-django
This command will output a few questions you need to answer about the project you want to build. Once the project is generated you can run the project locally or using Docker - whichever is your preference.
I will be using Docker to run the project. If you haven't used Docker before, consider learning the basics along with Cookiecutter.
Build the images:
docker-compose -f local.yml build
And then run the containers with Docker Compose:
docker-compose -f local.yml up
Once the containers are up you should see the Django project load on your localhost.
The version of Django used in this tutorial is 3.1.13 which is version specified in the Cookiecutter Django.
React Project Setup
We will use CRA (Create React App) to create the React project. Read the getting started guide to learn more about Create React App.
Integrating Django and React
One of the trickiest parts of working with Django and React is deciding how to architect the project structure. There are a few ways to integrate Django and React. In this tutorial we will setup the project folder as a monorepo (single repository contain both projects).
Create React App
We will use the npx
command to create the React project. This is installed when installing npm
. We will also pass in the typescript
flag to bootstrap the project configured with TypeScript.
Where the frontend folder is placed is up to you but for consistency we are going to run this command inside the django_react_paypal
folder in the Django project so that the React project folder is placed alongside the Django templates
folder.
npx create-react-app frontend --template typescript
The version of React used in this tutorial is 17.0.2
Dockerizing React
Since the Django project is Dockerized it makes sense to Dockerize the React project as well. We already have a local.yml
file for Docker Compose that contains all of the services of the project so we will add a React service to this file. But first we will create a Dockerfile for the React Image.
Inside frontend
create a folder compose/local
and inside that create a Dockerfile with the following contents:
FROM node:14-alpine AS development
ENV NODE_ENV development
WORKDIR /app
# Cache and Install dependencies - very important step
COPY package.json .
COPY package-lock.json .
RUN npm install
COPY . .
EXPOSE 3000
CMD [ "npm", "run", "start" ]
Take note of the COPY
commands. These are very important because they cache the dependencies of the project.
Now inside the local.yml
file add the react service:
services:
...
react:
build:
context: ./django_react_paypal/frontend
dockerfile: ./compose/local/Dockerfile
target: development
image: django_react_paypal_local_react
container_name: django_react_paypal_react
volumes:
- ./django_react_paypal/frontend/src:/app/src
ports:
- 3000:3000
The service points to the frontend
folder as the root of the code and also points to the corresponding Dockerfile inside the frontend
folder to build the React image.
After updating the local.yml
file you will need to rebuild the Docker images with docker-compose -f local.yml build
to implement the new changes.
Now after running docker-compose -f local.yml up
you should have four services running; Django, Postgres, Docs and React.
Selling Once-Off Digital Products
With all of the project configuration done we can now start with the PayPal payments. Before writing any code it's important to understand how the payments will give users access to the products they purchase.
The image above shows the process for how a user gains access to the digital product. The steps are as follows:
- The user visits the page and starts the payment process by clicking on the PayPal button
- After a successful payment the user is redirected to a success page
- PayPal sends a webhook event to the Django backend confirming a new order
- Django sends the user an email with the URL of the product
PayPal Checkout
Login to your PayPal business dashboard. In the navigation bar, under "Pay and Get Paid", click on PayPal Checkout. You can use PayPal Checkout for simple fixed price payments or for a shopping cart experience. Clicking on the first button to "Start Setup" will bring you to a page to configure the PayPal Checkout details. If you were using standard HTML pages you could fill in these details and copy the code from this PayPal Checkout to your page and you'd be finished. Of course for React it will be different but it's good to understand how the underlying code will look.
PayPal React Package
We are going to use the official PayPal React package to setup PayPal in the React project.
In the frontend
project install this package with:
npm install @paypal/react-paypal-js
Now inside App.tsx
replace everything with the following:
import { PayPalScriptProvider, PayPalButtons } from "@paypal/react-paypal-js";
export default function App() {
return (
<PayPalScriptProvider options={{ "client-id": "test" }}>
<PayPalButtons style={{ layout: "horizontal" }} />
</PayPalScriptProvider>
);
}
The PayPalScriptProvider
requires our PayPal client ID. We will add this as an environment variable.
Replace test
with process.env.REACT_APP_PAYPAL_CLIENT_ID
. This will load the variable REACT_APP_PAYPAL_CLIENT_ID
from environment variables. React requires that all environment variable names start with REACT_APP
.
Now create a file .env.local
in the root of the frontend
folder and inside it put the following:
REACT_APP_PAYPAL_CLIENT_ID=your-paypal-client-id
Make sure to replace it with your own client ID.
Lastly we need to tell TypeScript that REACT_APP_PAYPAL_CLIENT_ID
is a valid environment variable. Inside react-app-env.d.ts
add the following:
declare namespace NodeJS {
export interface ProcessEnv {
REACT_APP_PAYPAL_CLIENT_ID: string;
}
}
This declares the environment variables for the project which includes the name and type of each variable.
If you are using Docker remember to rebuild the image before running the containers. You should now see a PayPal button when the frontend opens in the browser.
Payments Component
Since we're selling access to a digital product lets make the page look like we're actually selling something. We'll create a React component to contain all the logic for selling the once-off payment product.
Inside the src
folder create a components
folder and create a file Payment.tsx
inside of it. Add the following to it:
import { PayPalButtons } from "@paypal/react-paypal-js";
export function Payment() {
return (
<div className="card">
<img src="https://images.unsplash.com/photo-1594498257673-9f36b767286c?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1350&q=80" alt="Airtable product" style={{ width: '100%' }} />
<div className="card-details">
<h1>Airtable Product</h1>
<p className="price">$10.00</p>
<p>Some information about the product</p>
<PayPalButtons style={{ layout: "horizontal" }} />
</div>
</div>
)
}
Replace the contents of index.css
with the following:
body {
background-color: #fdf1ec;
}
h1, img {
margin: 0;
padding: 0;
}
.card {
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2);
max-width: 300px;
margin: auto;
text-align: center;
font-family: arial;
}
.card h1, p {
color: #474747;
}
.card-details {
background-color: #ffffff;
padding-bottom: 10px;
padding-left: 5px;
padding-right: 5px;
}
.price {
color: grey;
font-size: 22px;
}
Delete App.css
and logo.svg
as we no longer need them.
Now inside App.tsx
use the new component:
import { PayPalScriptProvider } from "@paypal/react-paypal-js";
import { Payment } from './components/Payment'
export default function App() {
return (
<PayPalScriptProvider options={{ "client-id": process.env.REACT_APP_PAYPAL_CLIENT_ID }}>
<Payment />
</PayPalScriptProvider>
);
}
You should now see the following:
PayPalButtons Props
The PayPalButtons
component takes a lot of props. You can find the props inside node_modules/@paypal/paypal-js/types/components/buttons.d.ts
or by using CMD + click
on the component to take you directly to its props.
The props are as follows:
export interface PayPalButtonsComponentOptions {
/**
* Called on button click. Often used for [Braintree vault integrations](https://developers.braintreepayments.com/guides/paypal/vault/javascript/v3).
*/
createBillingAgreement?: () => Promise<string>;
/**
* Called on button click to set up a one-time payment. [createOrder docs](https://developer.paypal.com/docs/business/javascript-sdk/javascript-sdk-reference/#createorder).
*/
createOrder?: (
data: UnknownObject,
actions: CreateOrderActions
) => Promise<string>;
/**
* Called on button click to set up a recurring payment. [createSubscription docs](https://developer.paypal.com/docs/business/javascript-sdk/javascript-sdk-reference/#createsubscription).
*/
createSubscription?: (
data: UnknownObject,
actions: CreateSubscriptionActions
) => Promise<string>;
/**
* Used for defining a standalone button.
* Learn more about [configuring the funding source for standalone buttons](https://developer.paypal.com/docs/business/checkout/configure-payments/standalone-buttons/#4-funding-sources).
*/
fundingSource?: string;
/**
* Called when finalizing the transaction. Often used to inform the buyer that the transaction is complete. [onApprove docs](https://developer.paypal.com/docs/business/javascript-sdk/javascript-sdk-reference/#onapprove).
*/
onApprove?: (
data: OnApproveData,
actions: OnApproveActions
) => Promise<void>;
/**
* Called when the buyer cancels the transaction.
* Often used to show the buyer a [cancellation page](https://developer.paypal.com/docs/business/checkout/add-capabilities/buyer-experience/#3-show-cancellation-page).
*/
onCancel?: (data: UnknownObject, actions: OnCancelledActions) => void;
/**
* Called when the button is clicked. Often used for [validation](https://developer.paypal.com/docs/checkout/integration-features/validation/).
*/
onClick?: (
data: UnknownObject,
actions: OnClickActions
) => Promise<void> | void;
/**
* Catch all for errors preventing buyer checkout.
* Often used to show the buyer an [error page](https://developer.paypal.com/docs/checkout/integration-features/handle-errors/).
*/
onError?: (err: UnknownObject) => void;
/**
* Called when the buttons are initialized. The component is initialized after the iframe has successfully loaded.
*/
onInit?: (data: UnknownObject, actions: OnInitActions) => void;
/**
* Called when the buyer changes their shipping address on PayPal.
*/
onShippingChange?: () => void;
/**
* [Styling options](https://developer.paypal.com/docs/business/checkout/reference/style-guide/#customize-the-payment-buttons) for customizing the button appearance.
*/
style?: {
color?: "gold" | "blue" | "silver" | "white" | "black";
height?: number;
label?:
| "paypal"
| "checkout"
| "buynow"
| "pay"
| "installment"
| "subscribe"
| "donate";
layout?: "vertical" | "horizontal";
shape?: "rect" | "pill";
tagline?: boolean;
};
}
You can go through each of the links to understand more about what each function does and when it is called during the payment process. For one-time payments we are interested in the createOrder
function.
You can also take a look at all of their Storybook examples to get an idea for how to use the component.
In Payment.tsx
update the PaymentButtons
component to look like this:
<PayPalButtons
style={{ layout: "horizontal" }}
createOrder={(data, actions) => {
return actions.order.create({
purchase_units: [
{
amount: {
value: "10.00",
},
},
],
});
}}
/>
This will use the createOrder
function to pass in an amount of $10.00 as the one-time payment amount.
Try to Pay
Try clicking on the PayPal button. It should start the payment process by opening a modal to login to PayPal. Remember to login using your personal sandbox account. Once logged in you will choose how to pay the $10.00. After payment the modal will close.
Cancelled Payments
We are going to use the React Hot Toast package to display success and error messages based on the status of the payment. We'll do this by using some of the methods on the PayPalButtons
component like onError
, onApprove
and onCancel
.
Install the package with:
npm i react-hot-toast
Inside App.tsx
add the Toaster
component:
import { PayPalScriptProvider } from "@paypal/react-paypal-js";
import { Toaster } from "react-hot-toast";
import { Payment } from './components/Payment'
export default function App() {
return (
<PayPalScriptProvider options={{ "client-id": process.env.REACT_APP_PAYPAL_CLIENT_ID }}>
<Toaster position="top-center" />
<Payment />
</PayPalScriptProvider>
);
}
Now inside Payment.tsx
we will add the onCancel
function to the PayPalButtons
component:
onCancel={() => toast(
"You cancelled the payment. Try again by clicking the PayPal button",
{
duration: 6000
}
)}
Add the toast
import at the top:
import toast from "react-hot-toast";
To test the notification click the PayPal button and then cancel the payment. You should see the notification at the top of the page.
Payment Errors
For the onError
function we will add a callback function:
onError={(err) => {
toast.error("There was an error processing your payment. If this error please contact support.", { duration: 6000 });
}}
Errors occurring at this point are unexpected and usually just result in a generic error message or error page.
Successful Payments
After a payment it's good to show some sort of success message to the user. This could be a notification popup on the page or a redirect to a /success
URL.
We will make use of the onApprove
callback which is normally used to show a message to the buyer to indicate the payment was successful.
Add the onApprove callback function:
onApprove={(data, actions) => {
return actions.order.capture().then(function (details) {
toast.success('Payment completed. Thank you, ' + details.payer.name.given_name)
});
}}
Go through the payment process again and you should see the success message.
Webhooks
Now that we are accepting payments we need to give access to the content that the user purchased. This is where webhooks come in. A webhook is an event sent from PayPal to the Django server. It is the most reliable way to know that a payment was successful.
We are going to write a view that will handle the webhook event and send an email to the customer that purchased the content.
Tunnelling localhost to https
PayPal will only accept an HTTPS URL as the endpoint to send events to. A service like ngrok provides secure URLs to your localhost. Create an account and follow the steps to setup ngrok.
Once downloaded you can use ngrok with the Django server by running this command inside a terminal:
./ngrok http 8000
Make sure the ngrok executable is available in your folder
In the terminal ngrok will output the HTTPS URL for your session. You will need to add the URL to your ALLOWED_HOSTS
in settings/local.py
. For example if your ngrok URL is https://12345.ngrok.io
then add 12345.ngrok.io
to the ALLOWED_HOSTS
.
Configuring Webhooks in PayPal
In the **DASHBOARD **menu select My Apps and Credentials. Select your Default Application. At the bottom of the page under Sandbox Webhooks you can add a new webhook and select all of the events to be sent. For the Webhook URL use your ngrok endpoint with a unique path at the end.
For example, if your ngrok endpoint is https://12345.ngrok.io
the webhook URL could be something like https://12345.ngrok.io/webhooks/paypal/
. We will then create a Django view to handle requests on /webhooks/paypal/
.
Each created webhook will show its **Webhook ID **so take note of it because Django will need it.
At the bottom of settings/base.py
add your PayPal credentials:
PAYPAL_CLIENT_ID = env("PAYPAL_CLIENT_ID")
PAYPAL_CLIENT_SECRET = env("PAYPAL_CLIENT_SECRET")
PAYPAL_WEBHOOK_ID = env("PAYPAL_WEBHOOK_ID")
Remember to add these credentials to your environment variables as well.
Django Payments App
Start by creating an app to contain all of the payments logic. If you're using Docker you will need to run the following command:
docker-compose -f local.yml run --rm django python manage.py startapp payments
Move the payments
folder inside django_react_paypal
so it is alongside the other apps. Inside settings/base.py
add the app to the LOCAL_APPS
:
LOCAL_APPS = [
"django_react_paypal.users.apps.UsersConfig",
"django_react_paypal.payment.apps.PaymentConfig",
]
Now change the PaymentsConfig
class in payments/apps.py
to look like this:
from django.apps import AppConfig
class PaymentsConfig(AppConfig):
name = "django_react_paypal.payments"
We are changing the name of the app to be consistent with the other apps
Webhook View
The paypalrestsdk package can be used to interact with PayPal. Although the package has been deprecated you can continue to use it. We are going to use this package to help verify webhooks come from PayPal.
Add the package to requirements/base.txt
:
paypalrestsdk==1.13.1 # https://github.com/paypal/PayPal-Python-SDK
In payments/views.py
add the following:
import json
from django.conf import settings
from django.http import HttpResponse, HttpResponseBadRequest
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt
from django.views.generic import View
from paypalrestsdk import notifications
@method_decorator(csrf_exempt, name="dispatch")
class ProcessWebhookView(View):
def post(self, request):
if "HTTP_PAYPAL_TRANSMISSION_ID" not in request.META:
return HttpResponseBadRequest()
auth_algo = request.META['HTTP_PAYPAL_AUTH_ALGO']
cert_url = request.META['HTTP_PAYPAL_CERT_URL']
transmission_id = request.META['HTTP_PAYPAL_TRANSMISSION_ID']
transmission_sig = request.META['HTTP_PAYPAL_TRANSMISSION_SIG']
transmission_time = request.META['HTTP_PAYPAL_TRANSMISSION_TIME']
webhook_id = settings.PAYPAL_WEBHOOK_ID
event_body = request.body.decode(request.encoding or "utf-8")
valid = notifications.WebhookEvent.verify(
transmission_id=transmission_id,
timestamp=transmission_time,
webhook_id=webhook_id,
event_body=event_body,
cert_url=cert_url,
actual_sig=transmission_sig,
auth_algo=auth_algo,
)
if not valid:
return HttpResponseBadRequest()
webhook_event = json.loads(event_body)
event_type = webhook_event["event_type"]
print(event_type)
return HttpResponse()
This view starts by checking for the HTTP_PAYPAL_TRANSMISSION_ID
value in the request.META
. This is a header in the request and is always included when coming from PayPal.
We then use the header values, our PAYPAL_WEBHOOK_ID
and the decoded request body as arguments to the WebhookEvent.verify
function. This calls the PayPal API to verify that the webhook came from PayPal. If the webhook is valid we then load the request body into a json
object and print the type of event.
In config/urls.py
add the view to the path you set for the webhook URL:
# ...
from django_paypal_react.payments.views import ProcessWebhookView
urlpatterns = [
# ...
path('webhooks/paypal/', ProcessWebhookView.as_view())
]
Complete a payment on the React frontend and you should see CHECKOUT.ORDER.APPROVED
printed in your terminal. This is the type of event sent by PayPal when a Checkout is completed.
You can also explore the webhook event by printing the entire event and looking at all of the values included:
from pprint import pprint
pprint(webhook_event)
pprint prints objects with prettified format - it's very helpful for large data structures like webhook events In the event you can access the email address of the customer with:
webhook_event["resource"]["payer"]["email_address"]
Now we can send an email to the customer to provide them with access to the content. Add the following code to the ProcessWebhookView
CHECKOUT_ORDER_APPROVED = "CHECKOUT.ORDER.APPROVED"
if event_type == CHECKOUT_ORDER_APPROVED:
customer_email = webhook_event["resource"]["payer"]["email_address"]
product_link = "https://justdjango.com/pricing"
send_mail(
subject="Your access",
message=f"Thank you for purchasing my product. Here is the link: {product_link}",
from_email="your@email.com",
recipient_list=[customer_email]
)
Try test another payment and should see the email being printed in the bash.
Dealing with Multiple Products
If you are only selling one product then it is easy to know which product the user purchased. Once you start to sell more than one product, the Django backend will need to identify the product that was purchased when receiving a webhook event. For subscription payments this is much easier but for once-off payments we will need to provide some extra information to the PayPalButtons
component.
Change the purchase units
to look like this:
purchase_units: [
{
amount: {
value: "10.00"
},
custom_id: "e-book-1234" // the name or slug of the thing you're selling
},
],
The custom_id
property will be sent in the webhook. We can access it like this:
webhook_event["resource"]["purchase_units"][0]["custom_id"] # 'e-book-1234'
Conclusion
PayPal is one of the simplest ways to handle payments. You only have to write a little bit of code, but with that code you can easily sell your own digital products like you would on a platform like Gumroad.
You can find the code to this project on GitHub
Sponsor
This post is sponsored by JustDjango Learn, a set of Django courses that teach you the skills needed to become a professional Django developer.