If you are familiar with Stripe, you know how big of a player they are in the online payment processing field. Their API not only allows programmers to easily create one-time payments for sites such as e-commerce stores, but also provides quick integrations for monthly subscriptions and routing payouts. If new to Django and Stripe, check out our recent article on integrating one time payments. Otherwise, let's get into setting up monthly payments with Django and Stripe.
Why a Monthly Subscription?
Monthly subscriptions are a common practice found across the web especially among companies who promote software-as-a-service(SAAS) as their delivery model. For example, SAAS companies such as Hubspot (marketing), Dropbox (data storage), and Mailchimp (email marketing) all offer tiered pricing options to their prospective customers. Many consider this model to be favorable given that revenue is easier to predict once basic metrics are calculated (customer acquisition cost, lifetime value, churn rate). Predictable revenue of course creates stability and produces more accurate financial forecasts.
Mailchimp pricing page
Django Setup
Start by setting up a virtual environment and creating a basic Django project. We'll create a new virtual environment called saas
. Note: on Mac, use python3
instead of py
for all commands
C:\Users\Owner\Desktop\code>py -m venv saas
Next change directory into the virtual environment, install Django, and setup your project and app.
C:\Users\Owner\Desktop\code>cd saas
C:\Users\Owner\Desktop\code\saas>Scripts\activate
(saas) C:\Users\Owner\Desktop\code\saas>pip install Django
(saas) C:\Users\Owner\Desktop\code\saas>django-admin startproject mysite
(saas) C:\Users\Owner\Desktop\code\saas\mysite>py manage.py startapp main
Add main app to settings.py in mysite.
#mysite/settings.py
INSTALLED_APPS = [
'main.apps.MainConfig', #add this
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
]
Create urls.py in the main folder and include in mysite > urls.py.
#mysite > urls.py
from django.contrib import admin
from django.urls import path, include #add include
urlpatterns = [
path('', include('main.urls')), #add path
path('admin/', admin.site.urls),
]
Stripe Integration
Start by installing the official library for connecting to Stripe's API.
pip install --upgrade stripe
Next create a Stripe account and create products in their dashboard. While this can be performed using Stripe CLI's we'll use the dashboard since we are already on the page from creating an account. Make sure you are in test mode and can only view test data. By default, you should be in test mode after creating an account. Click products in the left-hand side navigation menu. Create two new products:
Name the product Premium Plan
Add description "paid plan for advanced features"
Use standard pricing
Enter $15.00 and ensure "recurring" is selected
Keep billing period as "monthly"
Name the product Enterprise Plan
Add description "enterprise plan for advanced features"
Use standard pricing
Enter $30.00 and ensure "recurring" is selected
Keep billing period as "monthly"
Now let's sync the product in the Stripe dashboard. While we could create a model and store the relevant product information, such as as product id, as model fields, an easier solution is to simply install the dj-stripe package and use the sync command. We also add need to add our API keys in settings.py. Note your live keys should always be secured and never listed in the settings. For more information on securing environment variables, check out Python securing.
pip install dj-stripe
#mysite/settings.py
STRIPE_TEST_PUBLIC_KEY ='pk_test_E52Nh0gTCRpJ7h4JhuEX7BIO006LVew6GG'
STRIPE_TEST_SECRET_KEY = 'sk_test_L87kx7GQNbz9tajOluDts7da00mSbze3dW'
STRIPE_LIVE_MODE = False # Change to True in production
DJSTRIPE_WEBHOOK_SECRET = "whsec_xxx"
Finally migrate the database. Note: if database is sqlite, migrating make take longer than usual.
py manage.py migrate
Add products to database automatically with the following command:
py manage.py djstripe_sync_plans_from_stripe
View the admin page to see the changes we just made. First create an admin user. Include an email such as test@example.com since we'll use this as the Stripe customer name then visit http://127.0.0.1:8000/admin/. You should see a variety of new models including our newly created products as well as plans for the products.
py manage.py createsuperuser
Checkout Page
Now that we have our data synced, let's create a checkout page where users can select their plan and checkout. We'll start with the view and checkout template. Note: we have imported the Bootstrap CDN in home.html:
#views.py
from django.shortcuts import render, redirect
import stripe
import json
from django.http import JsonResponse
from djstripe.models import Product
from django.contrib.auth.decorators import login_required
# Create your views here.
def homepage(request):
return render(request, "home.html")
@login_required
def checkout(request):
products = Product.objects.all()
return render(request,"checkout.html",{"products": products})
#checkout.html
{% extends "home.html" %}
{% block content %}
<script src="https://js.stripe.com/v3/"></script>
<br><br>
<div class="container ">
<div class="row ">
{% for p in products %}
<div class="col-6">
<div class="card mx-5 shadow" style="border-radius: 10px; border:none; ">
<div class="card-body">
<h5 class="card-title font-weight-bold">{{p.name}}</h5>
<p class="card-text text-muted"><svg class="bi bi-check" width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M10.97 4.97a.75.75 0 0 1 1.071 1.05l-3.992 4.99a.75.75 0 0 1-1.08.02L4.324 8.384a.75.75 0 1 1 1.06-1.06l2.094 2.093 3.473-4.425a.236.236 0 0 1 .02-.022z"/>
</svg>{{p.description}}</p>
{% for plan in p.plan_set.all %}
<h5 >{{ plan.human_readable_price }}</h5>
<div class="text-right">
<input type="checkbox" name="{{p.name}}" value="{{p.id}}" onclick="planSelect('{{p.name}}' ,'{{plan.human_readable_price}}', '{{plan.id}}')">
{% endfor %}
</div>
</div>
</div>
</div>
{% endfor %}
</div>
<br><br><hr><br><br>
<div>
<div class="row">
<div class="col-12">
<div class="card mx-5 shadow rounded" style="border-radius:50px;border:none">
<div class="card-body">
<h5 class="card-title font-weight-bold">Checkout</h5>
<p class="text-muted ">Enter card details. Your subscription will start immediately</p>
<div class="row">
<div class="col-6 text-muted">
<p>Plan:</p>
<p>Total:</p>
</div>
<div class="col-6 text-right">
<p id="plan"></p>
<p id="price"></p>
<p hidden id="priceId"></p>
</div>
</div>
<br>
<form id="subscription-form" >
<div id="card-element" class="MyCardElement">
<!-- Elements will create input elements here -->
</div>
<!-- We'll put the error messages in this element -->
<div id="card-errors" role="alert"></div>
<button id="submit" type="submit">
<div class="spinner-border spinner-border-sm text-light hidden" id="spinner" role="status">
<span class="sr-only">Loading...</span>
</div>
<span id="button-text">Subscribe</span>
</button>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
Now that we have a template for selecting the subscription plan. Let's add some styling and setup the credit card form by using Stripe Elements, a pre-built set of UI components.
<style>
body {
.StripeElement {
box-sizing: border-box;
height: 40px;
padding: 10px 12px;
border: 1px solid transparent;
border-radius: 4px;
background-color: white;
box-shadow: 0 1px 3px 0 #e6ebf1;
-webkit-transition: box-shadow 150ms ease;
transition: box-shadow 150ms ease;
}
.StripeElement--focus {
box-shadow: 0 1px 3px 0 #cfd7df;
}
.StripeElement--invalid {
border-color: #fa755a;
}
.StripeElement--webkit-autofill {
background-color: #fefde5 !important;
}
.hidden {
display: none;
}
#submit:hover {
filter: contrast(120%);
}
#submit {
font-feature-settings: "pnum";
--body-color: #f7fafc;
--button-color: #556cd6;
--accent-color: #556cd6;
--gray-border: #e3e8ee;
--link-color: #fff;
--font-color: #697386;
--body-font-family: -apple-system,BlinkMacSystemFont,sans-serif;
--radius: 4px;
--form-width: 400px;
-webkit-box-direction: normal;
word-wrap: break-word;
box-sizing: border-box;
font: inherit;
overflow: visible;
-webkit-appearance: button;
-webkit-font-smoothing: antialiased;
margin: 0;
font-family: inherit;
-webkit-tap-highlight-color: transparent;
font-size: 16px;
padding: 0 12px;
line-height: 32px;
outline: none;
text-decoration: none;
text-transform: none;
margin-right: 8px;
height: 36px;
border-radius: var(--radius);
color: #fff;
border: 0;
margin-top: 16px;
font-weight: 600;
cursor: pointer;
transition: all .2s ease;
display: block;
box-shadow: 0 4px 5.5px 0 rgba(0,0,0,.07);
width: 100%;
background: var(--button-color);
}
</style>
document.getElementById("submit").disabled = true;
stripeElements();
function stripeElements() {
stripe = Stripe('pk_test_E52Nh0gTCRpJ7h4JhuEX7BIO006LVew6GG');
if (document.getElementById('card-element')) {
let elements = stripe.elements();
// Card Element styles
let style = {
base: {
color: "#32325d",
fontFamily: '"Helvetica Neue", Helvetica, sans-serif',
fontSmoothing: "antialiased",
fontSize: "16px",
"::placeholder": {
color: "#aab7c4"
}
},
invalid: {
color: "#fa755a",
iconColor: "#fa755a"
}
};
card = elements.create('card', { style: style });
card.mount('#card-element');
card.on('focus', function () {
let el = document.getElementById('card-errors');
el.classList.add('focused');
});
card.on('blur', function () {
let el = document.getElementById('card-errors');
el.classList.remove('focused');
});
card.on('change', function (event) {
displayError(event);
});
}
//we'll add payment form handling here
}
function displayError(event) {
let displayError = document.getElementById('card-errors');
if (event.error) {
displayError.textContent = event.error.message;
} else {
displayError.textContent = '';
}
}
This should allow the credit card form to render. First, we disabled the subscribe button and then called stripeElements() to create and then mount the credit card form. Next, we add a small script to update the template with the selected subscription plan and to re-enable the subscribe button if a plan is selected.
function planSelect(name, price, priceId) {
var inputs = document.getElementsByTagName('input');
for(var i = 0; i<inputs.length; i++){
inputs[i].checked = false;
if(inputs[i].name== name){
inputs[i].checked = true;
}
}
var n = document.getElementById('plan');
var p = document.getElementById('price');
var pid = document.getElementById('priceId');
n.innerHTML = name;
p.innerHTML = price;
pid.innerHTML = priceId;
document.getElementById("submit").disabled = false;
}
Now we need to handle the payment form submission. We'll add the first code block to the end of the stripeElements() function. First grab the form by its ID and add an event listener. After preventing the default form submission, we change the loading state of the form in order to prevent any issues associated with double clicking the subscribe button. Finally, with the inputted card information, we create a payment method and submit this data to our server along with the price id of the selected subscription. As demonstrated, we use Django's CSRF token to authorize the submission to our server. Note: Stripe's documentation includes some additional validation that you should check out if interested. Also, unlike the documentation, we create a customer and subscription in one request to the server instead of sending separate requests.
//we'll add payment form handling here
let paymentForm = document.getElementById('subscription-form');
if (paymentForm) {
paymentForm.addEventListener('submit', function (evt) {
evt.preventDefault();
changeLoadingState(true);
// create new payment method & create subscription
createPaymentMethod({ card });
});
}
}
function createPaymentMethod({ card }) {
// Set up payment method for recurring usage
let billingName = '{{user.username}}';
stripe
.createPaymentMethod({
type: 'card',
card: card,
billing_details: {
name: billingName,
},
})
.then((result) => {
if (result.error) {
displayError(result);
} else {
const paymentParams = {
price_id: document.getElementById("priceId").innerHTML,
payment_method: result.paymentMethod.id,
};
fetch("/create-sub", {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken':'{{ csrf_token }}',
},
credentials: 'same-origin',
body: JSON.stringify(paymentParams),
}).then((response) => {
return response.json();
}).then((result) => {
if (result.error) {
// The card had an error when trying to attach it to a customer
throw result;
}
return result;
}).then((result) => {
if (result && result.status === 'active') {
window.location.href = '/complete';
};
}).catch(function (error) {
displayError(result.error.message);
});
}
});
}
var changeLoadingState = function(isLoading) {
if (isLoading) {
document.getElementById("submit").disabled = true;
document.querySelector("#spinner").classList.remove("hidden");
document.querySelector("#button-text").classList.add("hidden");
} else {
document.getElementById("submit").disabled = false;
document.querySelector("#spinner").classList.add("hidden");
document.querySelector("#button-text").classList.remove("hidden");
}
};
Add url paths for creating a subscription/customer and when payment is complete.
#main/urls.py
from django.urls import path
from . import views
app_name = "main"
urlpatterns = [
path("", views.homepage, name="homepage"),
path("checkout", views.checkout, name="checkout"),
path("logout", views.logout_request, name= "logout_request"),
path("login", views.login_request, name= "logout_request"),
path("register", views.register, name="register"),
path("create-sub", views.create_sub, name="create sub"), #add
path("complete", views.complete, name="complete"), #add
]
Next, create a view to handle creating a Stripe customer and subscription. The customer and subscription should also be stored in our local database. Luckily, we can easily syn our data using dj-stripe.
#main/views.py
...
import stripe
import json
from django.http import JsonResponse
from djstripe.models import Product
from django.contrib.auth.decorators import login_required
import djstripe
from django.http import HttpResponse
...
@login_required
def create_sub(request):
if request.method == 'POST':
# Reads application/json and returns a response
data = json.loads(request.body)
payment_method = data['payment_method']
stripe.api_key = djstripe.settings.STRIPE_SECRET_KEY
payment_method_obj = stripe.PaymentMethod.retrieve(payment_method)
djstripe.models.PaymentMethod.sync_from_stripe_data(payment_method_obj)
try:
# This creates a new Customer and attaches the PaymentMethod in one API call.
customer = stripe.Customer.create(
payment_method=payment_method,
email=request.user.email,
invoice_settings={
'default_payment_method': payment_method
}
)
djstripe_customer = djstripe.models.Customer.sync_from_stripe_data(customer)
request.user.customer = djstripe_customer
# At this point, associate the ID of the Customer object with your
# own internal representation of a customer, if you have one.
# print(customer)
# Subscribe the user to the subscription created
subscription = stripe.Subscription.create(
customer=customer.id,
items=[
{
"price": data["price_id"],
},
],
expand=["latest_invoice.payment_intent"]
)
djstripe_subscription = djstripe.models.Subscription.sync_from_stripe_data(subscription)
request.user.subscription = djstripe_subscription
request.user.save()
return JsonResponse(subscription)
except Exception as e:
return JsonResponse({'error': (e.args[0])}, status =403)
else:
return HTTPresponse('requet method not allowed')
Also add a view function for the payment complete page:
def complete(request):
return render(request, "complete.html")
Go through and test the integration. As suggested by Stripe, use "4242 4242 4242 4242" as the test credit card and view the results upon submitting your payment. Click on "Subscriptions" under "Customers" in your Stripe dashboard to view the newly created subscription and customer. You should see something similar as below:
Conclusion
Thanks for reading about using Django and Stripe to create monthly subscriptions. Hopefully this provides a foundation for creating your own SAAS project. We plan on writing additional pieces on Django and Stripe to cover topics such as routing payments with Stripe Connect, the underlying payment technology behind Lyft, Postmates, Kickstarter, and more. Also if you have any suggestions on other Django integrations that you'd like to read about, leave a comment below.
Originally published at https://www.ordinarycoders.com.
Top comments (0)