├── .gitignore ├── LICENSE ├── README.md ├── djstripetut ├── __init__.py ├── asgi.py ├── settings.py ├── urls.py └── wsgi.py ├── manage.py ├── products ├── __init__.py ├── admin.py ├── apps.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_auto_20210210_1400.py │ └── __init__.py ├── models.py ├── tests.py └── views.py ├── requirements.txt └── templates ├── cancel.html ├── landing.html └── success.html /.gitignore: -------------------------------------------------------------------------------- 1 | ### Python template 2 | # Byte-compiled / optimized / DLL files 3 | __pycache__/ 4 | *.py[cod] 5 | *$py.class 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | staticfiles/ 55 | 56 | # Sphinx documentation 57 | docs/_build/ 58 | 59 | # PyBuilder 60 | target/ 61 | 62 | # pyenv 63 | .python-version 64 | 65 | 66 | 67 | # Environments 68 | .venv 69 | venv/ 70 | ENV/ 71 | 72 | # Rope project settings 73 | .ropeproject 74 | 75 | # mkdocs documentation 76 | /site 77 | 78 | # mypy 79 | .mypy_cache/ 80 | 81 | 82 | ### Node template 83 | # Logs 84 | logs 85 | *.log 86 | npm-debug.log* 87 | yarn-debug.log* 88 | yarn-error.log* 89 | 90 | # Runtime data 91 | pids 92 | *.pid 93 | *.seed 94 | *.pid.lock 95 | 96 | # Directory for instrumented libs generated by jscoverage/JSCover 97 | lib-cov 98 | 99 | # Coverage directory used by tools like istanbul 100 | coverage 101 | 102 | # nyc test coverage 103 | .nyc_output 104 | 105 | # Bower dependency directory (https://bower.io/) 106 | bower_components 107 | 108 | # node-waf configuration 109 | .lock-wscript 110 | 111 | # Compiled binary addons (http://nodejs.org/api/addons.html) 112 | build/Release 113 | 114 | # Dependency directories 115 | node_modules/ 116 | jspm_packages/ 117 | 118 | # Typescript v1 declaration files 119 | typings/ 120 | 121 | # Optional npm cache directory 122 | .npm 123 | 124 | # Optional eslint cache 125 | .eslintcache 126 | 127 | # Optional REPL history 128 | .node_repl_history 129 | 130 | # Output of 'npm pack' 131 | *.tgz 132 | 133 | # Yarn Integrity file 134 | .yarn-integrity 135 | 136 | 137 | ### Linux template 138 | *~ 139 | 140 | # temporary files which can be created if a process still has a handle open of a deleted file 141 | .fuse_hidden* 142 | 143 | # KDE directory preferences 144 | .directory 145 | 146 | # Linux trash folder which might appear on any partition or disk 147 | .Trash-* 148 | 149 | # .nfs files are created when an open file is removed but is still being accessed 150 | .nfs* 151 | 152 | 153 | ### VisualStudioCode template 154 | .vscode/* 155 | !.vscode/settings.json 156 | !.vscode/tasks.json 157 | !.vscode/launch.json 158 | !.vscode/extensions.json 159 | 160 | 161 | 162 | 163 | 164 | ### Windows template 165 | # Windows thumbnail cache files 166 | Thumbs.db 167 | ehthumbs.db 168 | ehthumbs_vista.db 169 | 170 | # Dump file 171 | *.stackdump 172 | 173 | # Folder config file 174 | Desktop.ini 175 | 176 | # Recycle Bin used on file shares 177 | $RECYCLE.BIN/ 178 | 179 | # Windows Installer files 180 | *.cab 181 | *.msi 182 | *.msm 183 | *.msp 184 | 185 | # Windows shortcuts 186 | *.lnk 187 | 188 | 189 | ### macOS template 190 | # General 191 | *.DS_Store 192 | .AppleDouble 193 | .LSOverride 194 | 195 | # Icon must end with two \r 196 | Icon 197 | 198 | # Thumbnails 199 | ._* 200 | 201 | # Files that might appear in the root of a volume 202 | .DocumentRevisions-V100 203 | .fseventsd 204 | .Spotlight-V100 205 | .TemporaryItems 206 | .Trashes 207 | .VolumeIcon.icns 208 | .com.apple.timemachine.donotpresent 209 | 210 | # Directories potentially created on remote AFP share 211 | .AppleDB 212 | .AppleDesktop 213 | Network Trash Folder 214 | Temporary Items 215 | .apdisk 216 | 217 | 218 | ### SublimeText template 219 | # Cache files for Sublime Text 220 | *.tmlanguage.cache 221 | *.tmPreferences.cache 222 | *.stTheme.cache 223 | 224 | # Workspace files are user-specific 225 | *.sublime-workspace 226 | 227 | # Project files should be checked into the repository, unless a significant 228 | # proportion of contributors will probably not be using Sublime Text 229 | # *.sublime-project 230 | 231 | # SFTP configuration file 232 | sftp-config.json 233 | 234 | # Package control specific files 235 | Package Control.last-run 236 | Package Control.ca-list 237 | Package Control.ca-bundle 238 | Package Control.system-ca-bundle 239 | Package Control.cache/ 240 | Package Control.ca-certs/ 241 | Package Control.merged-ca-bundle 242 | Package Control.user-ca-bundle 243 | oscrypto-ca-bundle.crt 244 | bh_unicode_properties.cache 245 | 246 | # Sublime-github package stores a github token in this file 247 | # https://packagecontrol.io/packages/sublime-github 248 | GitHub.sublime-settings 249 | 250 | 251 | ### Vim template 252 | # Swap 253 | [._]*.s[a-v][a-z] 254 | [._]*.sw[a-p] 255 | [._]s[a-v][a-z] 256 | [._]sw[a-p] 257 | 258 | # Session 259 | Session.vim 260 | 261 | # Temporary 262 | .netrwhist 263 | 264 | # Auto-generated tag files 265 | tags 266 | 267 | ### Project template 268 | 269 | django_tutorial/media/ 270 | 271 | .pytest_cache/ 272 | 273 | .env 274 | 275 | ngrok -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | Copyright (c) 2021, JustDjango 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |

3 | 4 | JustDjango 5 | 6 |

7 |

8 | The Definitive Django Learning Platform. 9 |

10 |

11 | 12 | # Django Stripe Tutorial 13 | 14 | This is a tutorial on how to integrate Stripe Payments with Django. 15 | 16 | You can find the written version here: 17 | https://justdjango.com/blog/django-stripe-payments-tutorial/ 18 | 19 | Or watch it on YouTube: 20 | https://www.youtube.com/watch?v=722A27IoQnk&ab_channel=JustDjango 21 | 22 | --- 23 | 24 |
25 | 26 | Other places you can find us:
27 | 28 | YouTube 29 | Twitter 30 | 31 |
32 | -------------------------------------------------------------------------------- /djstripetut/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdjango/django-stripe-tutorial/22d2b154955418c8cfc4340c128f53d0cebe6a3a/djstripetut/__init__.py -------------------------------------------------------------------------------- /djstripetut/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for djstripetut 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', 'djstripetut.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /djstripetut/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for djstripetut project. 3 | 4 | Generated by 'django-admin startproject' using Django 3.1.6. 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().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 = 'xy7vyv5c4tk+-d8i7-em61gm-tm0(=t0(0i%(lizeu2nhu_5ow' 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 | 'products' 41 | ] 42 | 43 | MIDDLEWARE = [ 44 | 'django.middleware.security.SecurityMiddleware', 45 | 'django.contrib.sessions.middleware.SessionMiddleware', 46 | 'django.middleware.common.CommonMiddleware', 47 | 'django.middleware.csrf.CsrfViewMiddleware', 48 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 49 | 'django.contrib.messages.middleware.MessageMiddleware', 50 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 51 | ] 52 | 53 | ROOT_URLCONF = 'djstripetut.urls' 54 | 55 | TEMPLATES = [ 56 | { 57 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 58 | 'DIRS': [BASE_DIR / 'templates'], 59 | 'APP_DIRS': True, 60 | 'OPTIONS': { 61 | 'context_processors': [ 62 | 'django.template.context_processors.debug', 63 | 'django.template.context_processors.request', 64 | 'django.contrib.auth.context_processors.auth', 65 | 'django.contrib.messages.context_processors.messages', 66 | ], 67 | }, 68 | }, 69 | ] 70 | 71 | WSGI_APPLICATION = 'djstripetut.wsgi.application' 72 | 73 | 74 | # Database 75 | # https://docs.djangoproject.com/en/3.1/ref/settings/#databases 76 | 77 | DATABASES = { 78 | 'default': { 79 | 'ENGINE': 'django.db.backends.sqlite3', 80 | 'NAME': BASE_DIR / 'db.sqlite3', 81 | } 82 | } 83 | 84 | 85 | # Password validation 86 | # https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators 87 | 88 | AUTH_PASSWORD_VALIDATORS = [ 89 | { 90 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 91 | }, 92 | { 93 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 94 | }, 95 | { 96 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 97 | }, 98 | { 99 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 100 | }, 101 | ] 102 | 103 | EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' 104 | 105 | 106 | # Internationalization 107 | # https://docs.djangoproject.com/en/3.1/topics/i18n/ 108 | 109 | LANGUAGE_CODE = 'en-us' 110 | 111 | TIME_ZONE = 'UTC' 112 | 113 | USE_I18N = True 114 | 115 | USE_L10N = True 116 | 117 | USE_TZ = True 118 | 119 | 120 | # Static files (CSS, JavaScript, Images) 121 | # https://docs.djangoproject.com/en/3.1/howto/static-files/ 122 | 123 | STATIC_URL = '/static/' 124 | 125 | STRIPE_PUBLIC_KEY = "" 126 | STRIPE_SECRET_KEY = "" 127 | STRIPE_WEBHOOK_SECRET = "" -------------------------------------------------------------------------------- /djstripetut/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import path 3 | from products.views import ( 4 | CreateCheckoutSessionView, 5 | ProductLandingPageView, 6 | SuccessView, 7 | CancelView, 8 | stripe_webhook, 9 | StripeIntentView 10 | ) 11 | 12 | urlpatterns = [ 13 | path('admin/', admin.site.urls), 14 | path('create-payment-intent//', StripeIntentView.as_view(), name='create-payment-intent'), 15 | path('webhooks/stripe/', stripe_webhook, name='stripe-webhook'), 16 | path('cancel/', CancelView.as_view(), name='cancel'), 17 | path('success/', SuccessView.as_view(), name='success'), 18 | path('', ProductLandingPageView.as_view(), name='landing-page'), 19 | path('create-checkout-session//', CreateCheckoutSessionView.as_view(), name='create-checkout-session') 20 | ] 21 | -------------------------------------------------------------------------------- /djstripetut/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for djstripetut 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', 'djstripetut.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', 'djstripetut.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 | -------------------------------------------------------------------------------- /products/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdjango/django-stripe-tutorial/22d2b154955418c8cfc4340c128f53d0cebe6a3a/products/__init__.py -------------------------------------------------------------------------------- /products/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import Product 3 | 4 | 5 | admin.site.register(Product) -------------------------------------------------------------------------------- /products/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ProductsConfig(AppConfig): 5 | name = 'products' 6 | -------------------------------------------------------------------------------- /products/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.6 on 2021-02-10 09:36 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Product', 16 | fields=[ 17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('name', models.CharField(max_length=100)), 19 | ('price', models.IntegerField(default=0)), 20 | ], 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /products/migrations/0002_auto_20210210_1400.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.6 on 2021-02-10 14:00 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('products', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='product', 15 | name='file', 16 | field=models.FileField(blank=True, null=True, upload_to='product_files/'), 17 | ), 18 | migrations.AddField( 19 | model_name='product', 20 | name='url', 21 | field=models.URLField(default='https://google.com'), 22 | preserve_default=False, 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /products/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justdjango/django-stripe-tutorial/22d2b154955418c8cfc4340c128f53d0cebe6a3a/products/migrations/__init__.py -------------------------------------------------------------------------------- /products/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class Product(models.Model): 5 | name = models.CharField(max_length=100) 6 | price = models.IntegerField(default=0) # cents 7 | file = models.FileField(upload_to="product_files/", blank=True, null=True) 8 | url = models.URLField() 9 | 10 | def __str__(self): 11 | return self.name 12 | 13 | def get_display_price(self): 14 | return "{0:.2f}".format(self.price / 100) -------------------------------------------------------------------------------- /products/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /products/views.py: -------------------------------------------------------------------------------- 1 | import json 2 | import stripe 3 | from django.core.mail import send_mail 4 | from django.conf import settings 5 | from django.views.generic import TemplateView 6 | from django.views.decorators.csrf import csrf_exempt 7 | from django.http import JsonResponse, HttpResponse 8 | from django.views import View 9 | from .models import Product 10 | 11 | 12 | stripe.api_key = settings.STRIPE_SECRET_KEY 13 | 14 | 15 | class SuccessView(TemplateView): 16 | template_name = "success.html" 17 | 18 | 19 | class CancelView(TemplateView): 20 | template_name = "cancel.html" 21 | 22 | 23 | class ProductLandingPageView(TemplateView): 24 | template_name = "landing.html" 25 | 26 | def get_context_data(self, **kwargs): 27 | product = Product.objects.get(name="Test Product") 28 | context = super(ProductLandingPageView, self).get_context_data(**kwargs) 29 | context.update({ 30 | "product": product, 31 | "STRIPE_PUBLIC_KEY": settings.STRIPE_PUBLIC_KEY 32 | }) 33 | return context 34 | 35 | 36 | class CreateCheckoutSessionView(View): 37 | def post(self, request, *args, **kwargs): 38 | product_id = self.kwargs["pk"] 39 | product = Product.objects.get(id=product_id) 40 | YOUR_DOMAIN = "http://127.0.0.1:8000" 41 | checkout_session = stripe.checkout.Session.create( 42 | payment_method_types=['card'], 43 | line_items=[ 44 | { 45 | 'price_data': { 46 | 'currency': 'usd', 47 | 'unit_amount': product.price, 48 | 'product_data': { 49 | 'name': product.name, 50 | # 'images': ['https://i.imgur.com/EHyR2nP.png'], 51 | }, 52 | }, 53 | 'quantity': 1, 54 | }, 55 | ], 56 | metadata={ 57 | "product_id": product.id 58 | }, 59 | mode='payment', 60 | success_url=YOUR_DOMAIN + '/success/', 61 | cancel_url=YOUR_DOMAIN + '/cancel/', 62 | ) 63 | return JsonResponse({ 64 | 'id': checkout_session.id 65 | }) 66 | 67 | 68 | @csrf_exempt 69 | def stripe_webhook(request): 70 | payload = request.body 71 | sig_header = request.META['HTTP_STRIPE_SIGNATURE'] 72 | event = None 73 | 74 | try: 75 | event = stripe.Webhook.construct_event( 76 | payload, sig_header, settings.STRIPE_WEBHOOK_SECRET 77 | ) 78 | except ValueError as e: 79 | # Invalid payload 80 | return HttpResponse(status=400) 81 | except stripe.error.SignatureVerificationError as e: 82 | # Invalid signature 83 | return HttpResponse(status=400) 84 | 85 | # Handle the checkout.session.completed event 86 | if event['type'] == 'checkout.session.completed': 87 | session = event['data']['object'] 88 | 89 | customer_email = session["customer_details"]["email"] 90 | product_id = session["metadata"]["product_id"] 91 | 92 | product = Product.objects.get(id=product_id) 93 | 94 | send_mail( 95 | subject="Here is your product", 96 | message=f"Thanks for your purchase. Here is the product you ordered. The URL is {product.url}", 97 | recipient_list=[customer_email], 98 | from_email="matt@test.com" 99 | ) 100 | 101 | # TODO - decide whether you want to send the file or the URL 102 | 103 | elif event["type"] == "payment_intent.succeeded": 104 | intent = event['data']['object'] 105 | 106 | stripe_customer_id = intent["customer"] 107 | stripe_customer = stripe.Customer.retrieve(stripe_customer_id) 108 | 109 | customer_email = stripe_customer['email'] 110 | product_id = intent["metadata"]["product_id"] 111 | 112 | product = Product.objects.get(id=product_id) 113 | 114 | send_mail( 115 | subject="Here is your product", 116 | message=f"Thanks for your purchase. Here is the product you ordered. The URL is {product.url}", 117 | recipient_list=[customer_email], 118 | from_email="matt@test.com" 119 | ) 120 | 121 | return HttpResponse(status=200) 122 | 123 | 124 | class StripeIntentView(View): 125 | def post(self, request, *args, **kwargs): 126 | try: 127 | req_json = json.loads(request.body) 128 | customer = stripe.Customer.create(email=req_json['email']) 129 | product_id = self.kwargs["pk"] 130 | product = Product.objects.get(id=product_id) 131 | intent = stripe.PaymentIntent.create( 132 | amount=product.price, 133 | currency='usd', 134 | customer=customer['id'], 135 | metadata={ 136 | "product_id": product.id 137 | } 138 | ) 139 | return JsonResponse({ 140 | 'clientSecret': intent['client_secret'] 141 | }) 142 | except Exception as e: 143 | return JsonResponse({ 'error': str(e) }) -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | asgiref==3.3.1 2 | certifi==2020.12.5 3 | chardet==4.0.0 4 | Django==3.1.6 5 | idna==2.10 6 | pytz==2021.1 7 | requests==2.25.1 8 | sqlparse==0.4.1 9 | stripe==2.55.2 10 | urllib3==1.26.3 11 | -------------------------------------------------------------------------------- /templates/cancel.html: -------------------------------------------------------------------------------- 1 | Cancelled! -------------------------------------------------------------------------------- /templates/landing.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Buy cool new product 5 | 6 | 7 | 8 | 168 | 169 | 170 |
171 |
172 | {% comment %} The cover of Stubborn Attachments {% endcomment %} 176 |
177 |

{{ product.name }}

178 |
${{ product.get_display_price }}
179 |
180 |
181 | 182 |
183 |

Checkout using custom payment flow

184 |
185 | 186 |
187 | 191 | 192 | 196 |
197 |
198 | {% csrf_token %} 199 | 200 | 339 | -------------------------------------------------------------------------------- /templates/success.html: -------------------------------------------------------------------------------- 1 | Success! --------------------------------------------------------------------------------