DEV Community

Cover image for Using Django and Stripe for Monthly Subscriptions
Ordinary Coders
Ordinary Coders

Posted on • Originally published at ordinarycoders.com

Using Django and Stripe for Monthly Subscriptions

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

alt text

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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',
]
Enter fullscreen mode Exit fullscreen mode

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),
]
Enter fullscreen mode Exit fullscreen mode

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"

alt text

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"
Enter fullscreen mode Exit fullscreen mode

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

alt text

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})
Enter fullscreen mode Exit fullscreen mode
#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 %}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode
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 = '';
  }
}
Enter fullscreen mode Exit fullscreen mode

alt text

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;
    }
Enter fullscreen mode Exit fullscreen mode

alt text

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");
    }
};
Enter fullscreen mode Exit fullscreen mode

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

]
Enter fullscreen mode Exit fullscreen mode

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')
Enter fullscreen mode Exit fullscreen mode

Also add a view function for the payment complete page:

def complete(request):
    return render(request, "complete.html")
Enter fullscreen mode Exit fullscreen mode

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:

alt text

alt text

alt text

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)