├── djangostripe
├── __init__.py
├── urls.py
├── asgi.py
├── wsgi.py
└── settings.py
├── subscriptions
├── __init__.py
├── migrations
│ ├── __init__.py
│ └── 0001_initial.py
├── tests.py
├── apps.py
├── admin.py
├── urls.py
├── models.py
└── views.py
├── .gitignore
├── requirements.txt
├── manage.py
├── static
└── main.js
├── templates
├── cancel.html
├── success.html
└── home.html
├── README.md
└── LICENSE
/djangostripe/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/subscriptions/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/subscriptions/migrations/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | env
2 | venv/
3 | .env
4 | .idea/
5 | __pycache__
6 | *.sqlite3
--------------------------------------------------------------------------------
/subscriptions/tests.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase
2 |
3 | # Create your tests here.
4 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | Django==4.0.6
2 | django-allauth==0.51.0
3 | requests==2.28.1
4 | stripe==3.5.0
5 |
--------------------------------------------------------------------------------
/subscriptions/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class SubscriptionsConfig(AppConfig):
5 | name = 'subscriptions'
6 |
--------------------------------------------------------------------------------
/subscriptions/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 | from subscriptions.models import StripeCustomer
3 |
4 |
5 | admin.site.register(StripeCustomer)
6 |
--------------------------------------------------------------------------------
/djangostripe/urls.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 | from django.urls import path, include
3 |
4 | urlpatterns = [
5 | path('admin/', admin.site.urls),
6 | path('', include('subscriptions.urls')),
7 | path('accounts/', include('allauth.urls')),
8 | ]
9 |
--------------------------------------------------------------------------------
/subscriptions/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 | from . import views
3 |
4 | urlpatterns = [
5 | path('', views.home, name='subscriptions-home'),
6 | path('config/', views.stripe_config),
7 | path('create-checkout-session/', views.create_checkout_session),
8 | path('success/', views.success),
9 | path('cancel/', views.cancel),
10 | path('webhook/', views.stripe_webhook),
11 | ]
12 |
--------------------------------------------------------------------------------
/subscriptions/models.py:
--------------------------------------------------------------------------------
1 | from django.contrib.auth.models import User
2 | from django.db import models
3 |
4 |
5 | class StripeCustomer(models.Model):
6 | user = models.OneToOneField(to=User, on_delete=models.CASCADE)
7 | stripeCustomerId = models.CharField(max_length=255)
8 | stripeSubscriptionId = models.CharField(max_length=255)
9 |
10 | def __str__(self):
11 | return self.user.username
12 |
--------------------------------------------------------------------------------
/djangostripe/asgi.py:
--------------------------------------------------------------------------------
1 | """
2 | ASGI config for djangostripe project.
3 |
4 | It exposes the ASGI callable as a module-level variable named ``application``.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/3.1/howto/deployment/asgi/
8 | """
9 |
10 | import os
11 |
12 | from django.core.asgi import get_asgi_application
13 |
14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'djangostripe.settings')
15 |
16 | application = get_asgi_application()
17 |
--------------------------------------------------------------------------------
/djangostripe/wsgi.py:
--------------------------------------------------------------------------------
1 | """
2 | WSGI config for djangostripe project.
3 |
4 | It exposes the WSGI callable as a module-level variable named ``application``.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/3.1/howto/deployment/wsgi/
8 | """
9 |
10 | import os
11 |
12 | from django.core.wsgi import get_wsgi_application
13 |
14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'djangostripe.settings')
15 |
16 | application = get_wsgi_application()
17 |
--------------------------------------------------------------------------------
/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | """Django's command-line utility for administrative tasks."""
3 | import os
4 | import sys
5 |
6 |
7 | def main():
8 | """Run administrative tasks."""
9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'djangostripe.settings')
10 | try:
11 | from django.core.management import execute_from_command_line
12 | except ImportError as exc:
13 | raise ImportError(
14 | "Couldn't import Django. Are you sure it's installed and "
15 | "available on your PYTHONPATH environment variable? Did you "
16 | "forget to activate a virtual environment?"
17 | ) from exc
18 | execute_from_command_line(sys.argv)
19 |
20 |
21 | if __name__ == '__main__':
22 | main()
23 |
--------------------------------------------------------------------------------
/static/main.js:
--------------------------------------------------------------------------------
1 | console.log("Sanity check!");
2 |
3 | // Get Stripe publishable key
4 | fetch("/config/")
5 | .then((result) => { return result.json(); })
6 | .then((data) => {
7 | // Initialize Stripe.js
8 | const stripe = Stripe(data.publicKey);
9 |
10 | // Event handler
11 | let submitBtn = document.querySelector("#submitBtn");
12 | if (submitBtn !== null) {
13 | submitBtn.addEventListener("click", () => {
14 | // Get Checkout Session ID
15 | fetch("/create-checkout-session/")
16 | .then((result) => { return result.json(); })
17 | .then((data) => {
18 | console.log(data);
19 | // Redirect to Stripe Checkout
20 | return stripe.redirectToCheckout({sessionId: data.sessionId})
21 | })
22 | .then((res) => {
23 | console.log(res);
24 | });
25 | });
26 | }
27 | });
28 |
--------------------------------------------------------------------------------
/subscriptions/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.1 on 2020-10-01 14:53
2 |
3 | from django.conf import settings
4 | from django.db import migrations, models
5 | import django.db.models.deletion
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | initial = True
11 |
12 | dependencies = [
13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL),
14 | ]
15 |
16 | operations = [
17 | migrations.CreateModel(
18 | name='StripeCustomer',
19 | fields=[
20 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
21 | ('stripeCustomerId', models.CharField(max_length=255)),
22 | ('stripeSubscriptionId', models.CharField(max_length=255)),
23 | ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
24 | ],
25 | ),
26 | ]
27 |
--------------------------------------------------------------------------------
/templates/cancel.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Django + Stripe Subscriptions
7 |
8 |
9 |
10 |
11 |
12 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/templates/success.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Django + Stripe Subscriptions
7 |
8 |
9 |
10 |
11 |
12 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Setting up Stripe subscriptions with Django
2 |
3 | ## Want to learn how to build this?
4 |
5 | Check out the [post](https://testdriven.io/blog/django-stripe-subscriptions/).
6 |
7 | ## Want to use this project?
8 |
9 | 1. Fork/Clone
10 |
11 | 1. Create and activate a virtual environment:
12 |
13 | ```sh
14 | $ python3 -m venv venv && source venv/bin/activate
15 | ```
16 |
17 | 1. Install the requirements:
18 |
19 | ```sh
20 | (venv)$ pip install -r requirements.txt
21 | ```
22 |
23 | 1. Apply the migrations:
24 |
25 | ```sh
26 | (venv)$ python manage.py migrate
27 | ```
28 |
29 | 1. Add your Stripe test secret key, test publishable key, endpoint secret and price API ID to the *settings.py* file:
30 |
31 | ```python
32 | STRIPE_PUBLISHABLE_KEY = ''
33 | STRIPE_SECRET_KEY = ''
34 | STRIPE_PRICE_ID = ''
35 | STRIPE_ENDPOINT_SECRET = ''
36 | ```
37 |
38 | 1. Run the server:
39 |
40 | ```sh
41 | (venv)$ python manage.py runserver
42 | ```
43 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Nik Tomazic
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/templates/home.html:
--------------------------------------------------------------------------------
1 | {% load static %}
2 |
3 |
4 |
5 |
6 |
7 |
8 | Django + Stripe Subscriptions
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | {% if subscription.status == "active" %}
18 |
Your subscription:
19 |
20 |
21 |
{{ product.name }}
22 |
23 | {{ product.description }}
24 |
25 |
26 |
27 | {% else %}
28 |
Subscribe
29 | {% endif %}
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/subscriptions/views.py:
--------------------------------------------------------------------------------
1 | import stripe
2 | from django.conf import settings
3 | from django.contrib.auth.decorators import login_required
4 | from django.contrib.auth.models import User # new
5 | from django.http.response import JsonResponse, HttpResponse # updated
6 | from django.shortcuts import render
7 | from django.views.decorators.csrf import csrf_exempt
8 |
9 | from subscriptions.models import StripeCustomer # new
10 |
11 |
12 | @login_required
13 | def home(request):
14 | try:
15 | # Retrieve the subscription & product
16 | stripe_customer = StripeCustomer.objects.get(user=request.user)
17 | stripe.api_key = settings.STRIPE_SECRET_KEY
18 | subscription = stripe.Subscription.retrieve(stripe_customer.stripeSubscriptionId)
19 | product = stripe.Product.retrieve(subscription.plan.product)
20 |
21 | # Feel free to fetch any additional data from 'subscription' or 'product'
22 | # https://stripe.com/docs/api/subscriptions/object
23 | # https://stripe.com/docs/api/products/object
24 |
25 | return render(request, 'home.html', {
26 | 'subscription': subscription,
27 | 'product': product,
28 | })
29 |
30 | except StripeCustomer.DoesNotExist:
31 | return render(request, 'home.html')
32 |
33 |
34 | @csrf_exempt
35 | def stripe_config(request):
36 | if request.method == 'GET':
37 | stripe_config = {'publicKey': settings.STRIPE_PUBLISHABLE_KEY}
38 | return JsonResponse(stripe_config, safe=False)
39 |
40 |
41 | @csrf_exempt
42 | def create_checkout_session(request):
43 | if request.method == 'GET':
44 | domain_url = 'http://localhost:8000/'
45 | stripe.api_key = settings.STRIPE_SECRET_KEY
46 | try:
47 | checkout_session = stripe.checkout.Session.create(
48 | client_reference_id=request.user.id if request.user.is_authenticated else None,
49 | success_url=domain_url + 'success?session_id={CHECKOUT_SESSION_ID}',
50 | cancel_url=domain_url + 'cancel/',
51 | payment_method_types=['card'],
52 | mode='subscription',
53 | line_items=[
54 | {
55 | 'price': settings.STRIPE_PRICE_ID,
56 | 'quantity': 1,
57 | }
58 | ]
59 | )
60 | return JsonResponse({'sessionId': checkout_session['id']})
61 | except Exception as e:
62 | return JsonResponse({'error': str(e)})
63 |
64 |
65 | @login_required
66 | def success(request):
67 | return render(request, 'success.html')
68 |
69 |
70 | @login_required
71 | def cancel(request):
72 | return render(request, 'cancel.html')
73 |
74 |
75 | @csrf_exempt
76 | def stripe_webhook(request):
77 | stripe.api_key = settings.STRIPE_SECRET_KEY
78 | endpoint_secret = settings.STRIPE_ENDPOINT_SECRET
79 | payload = request.body
80 | sig_header = request.META['HTTP_STRIPE_SIGNATURE']
81 | event = None
82 |
83 | try:
84 | event = stripe.Webhook.construct_event(
85 | payload, sig_header, endpoint_secret
86 | )
87 | except ValueError as e:
88 | # Invalid payload
89 | return HttpResponse(status=400)
90 | except stripe.error.SignatureVerificationError as e:
91 | # Invalid signature
92 | return HttpResponse(status=400)
93 |
94 | # Handle the checkout.session.completed event
95 | if event['type'] == 'checkout.session.completed':
96 | session = event['data']['object']
97 |
98 | # Fetch all the required data from session
99 | client_reference_id = session.get('client_reference_id')
100 | stripe_customer_id = session.get('customer')
101 | stripe_subscription_id = session.get('subscription')
102 |
103 | # Get the user and create a new StripeCustomer
104 | user = User.objects.get(id=client_reference_id)
105 | StripeCustomer.objects.create(
106 | user=user,
107 | stripeCustomerId=stripe_customer_id,
108 | stripeSubscriptionId=stripe_subscription_id,
109 | )
110 | print(user.username + ' just subscribed.')
111 |
112 | return HttpResponse(status=200)
113 |
--------------------------------------------------------------------------------
/djangostripe/settings.py:
--------------------------------------------------------------------------------
1 | """
2 | Django settings for djangostripe project.
3 |
4 | Generated by 'django-admin startproject' using Django 3.1.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/3.1/topics/settings/
8 |
9 | For the full list of settings and their values, see
10 | https://docs.djangoproject.com/en/3.1/ref/settings/
11 | """
12 |
13 | from pathlib import Path
14 |
15 | # Build paths inside the project like this: BASE_DIR / 'subdir'.
16 | BASE_DIR = Path(__file__).resolve(strict=True).parent.parent
17 |
18 |
19 | # Quick-start development settings - unsuitable for production
20 | # See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/
21 |
22 | # SECURITY WARNING: keep the secret key used in production secret!
23 | SECRET_KEY = '&zh09&src7nab9=*&@a5yj8(-aqy35a_g_jkx(%(a*hj306q!x'
24 |
25 | # SECURITY WARNING: don't run with debug turned on in production!
26 | DEBUG = True
27 |
28 | ALLOWED_HOSTS = []
29 |
30 |
31 | # Application definition
32 |
33 | INSTALLED_APPS = [
34 | 'django.contrib.admin',
35 | 'django.contrib.auth',
36 | 'django.contrib.contenttypes',
37 | 'django.contrib.sessions',
38 | 'django.contrib.messages',
39 | 'django.contrib.staticfiles',
40 | 'django.contrib.sites',
41 | 'allauth',
42 | 'allauth.account',
43 | 'allauth.socialaccount',
44 | 'subscriptions.apps.SubscriptionsConfig',
45 | ]
46 |
47 | MIDDLEWARE = [
48 | 'django.middleware.security.SecurityMiddleware',
49 | 'django.contrib.sessions.middleware.SessionMiddleware',
50 | 'django.middleware.common.CommonMiddleware',
51 | 'django.middleware.csrf.CsrfViewMiddleware',
52 | 'django.contrib.auth.middleware.AuthenticationMiddleware',
53 | 'django.contrib.messages.middleware.MessageMiddleware',
54 | 'django.middleware.clickjacking.XFrameOptionsMiddleware',
55 | ]
56 |
57 | ROOT_URLCONF = 'djangostripe.urls'
58 |
59 | TEMPLATES = [
60 | {
61 | 'BACKEND': 'django.template.backends.django.DjangoTemplates',
62 | 'DIRS': ['templates'],
63 | 'APP_DIRS': True,
64 | 'OPTIONS': {
65 | 'context_processors': [
66 | 'django.template.context_processors.debug',
67 | 'django.template.context_processors.request',
68 | 'django.contrib.auth.context_processors.auth',
69 | 'django.contrib.messages.context_processors.messages',
70 | ],
71 | },
72 | },
73 | ]
74 |
75 | WSGI_APPLICATION = 'djangostripe.wsgi.application'
76 |
77 |
78 | # Database
79 | # https://docs.djangoproject.com/en/3.1/ref/settings/#databases
80 |
81 | DATABASES = {
82 | 'default': {
83 | 'ENGINE': 'django.db.backends.sqlite3',
84 | 'NAME': BASE_DIR / 'db.sqlite3',
85 | }
86 | }
87 |
88 |
89 | # Password validation
90 | # https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators
91 |
92 | AUTH_PASSWORD_VALIDATORS = [
93 | {
94 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
95 | },
96 | {
97 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
98 | },
99 | {
100 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
101 | },
102 | {
103 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
104 | },
105 | ]
106 |
107 |
108 | # Internationalization
109 | # https://docs.djangoproject.com/en/3.1/topics/i18n/
110 |
111 | LANGUAGE_CODE = 'en-us'
112 |
113 | TIME_ZONE = 'UTC'
114 |
115 | USE_I18N = True
116 |
117 | USE_L10N = True
118 |
119 | USE_TZ = True
120 |
121 |
122 | # Static files (CSS, JavaScript, Images)
123 | # https://docs.djangoproject.com/en/3.1/howto/static-files/
124 |
125 | STATIC_URL = '/static/'
126 | STATICFILES_DIRS = [Path(BASE_DIR).joinpath('static')]
127 |
128 | STRIPE_PUBLISHABLE_KEY = ''
129 | STRIPE_SECRET_KEY = ''
130 | STRIPE_PRICE_ID = ''
131 | STRIPE_ENDPOINT_SECRET = ''
132 |
133 | AUTHENTICATION_BACKENDS = [
134 | # Needed to login by username in Django admin, regardless of `allauth`
135 | 'django.contrib.auth.backends.ModelBackend',
136 |
137 | # `allauth` specific authentication methods, such as login by e-mail
138 | 'allauth.account.auth_backends.AuthenticationBackend',
139 | ]
140 |
141 | # We have to set this variable, because we enabled 'django.contrib.sites'
142 | SITE_ID = 1
143 |
144 | # User will be redirected to this page after logging in
145 | LOGIN_REDIRECT_URL = '/'
146 |
147 | # If you don't have an email server running yet add this line to avoid any possible errors.
148 | EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
149 |
--------------------------------------------------------------------------------