paint-brush
Setting up Subscriptions And Recurring Payments Using Django and Stripeby@ordinarycoders
2,331 reads
2,331 reads

Setting up Subscriptions And Recurring Payments Using Django and Stripe

by Ordinary CodersJuly 4th, 2020
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

Setting up Subscriptions and Recurring Payments using Django and Stripe. Stripe's API allows programmers to easily create one-time payments for sites such as e-commerce stores. Subscription services are a common practice found across the web especially among companies who promote software-as-a-service (SAAS) as their delivery model. We'll create a new virtual environment called the saas-saas project and use Django to set up a monthly subscription. Create a Stripe account and create products in the Stripe dashboard.

Companies Mentioned

Mention Thumbnail
Mention Thumbnail
featured image - Setting up Subscriptions And  Recurring Payments Using Django and Stripe
Ordinary Coders HackerNoon profile picture

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 the settings.py in 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 [email protected] 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.

Previously published at https://www.ordinarycoders.com/blog/article/django-stripe-monthly-subscription