Skip to content

Commit 6d98b10

Browse files
authored
Post checkout (#22)
* Add post checkout API * Add template to easily include and setup PaddleJS * Add template to register post checkout API call * Add docs for Paddle checkout pages * Use update_or_create for all subscription signals * Add redirect url to post checkout * Make Checkout fields not required As Checkout is currently intended to be a Transient model the only thing which should stop someone creating an instance quickly is the ID not being unique or valid
1 parent 1dbf345 commit 6d98b10

File tree

12 files changed

+611
-15
lines changed

12 files changed

+611
-15
lines changed

README.rst

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,24 @@ Add your paddle keys and set the operating mode:
6969
# can be found at https://vendors.paddle.com/public-key
7070
DJPADDLE_PUBLIC_KEY = '<your-public-key>'
7171
72+
djpaddle includes ``vendor_id`` template context processor which adds your vendor ID as ``DJPADDLE_VENDOR_ID`` to each template context
73+
74+
.. code-block:: python
75+
76+
TEMPLATES = [
77+
{
78+
...
79+
'OPTIONS': {
80+
...
81+
'context_processors': [
82+
...
83+
'djpaddle.context_processors.vendor_id',
84+
...
85+
]
86+
}
87+
}
88+
89+
7290
Run the commands::
7391
7492
python manage.py migrate
@@ -77,6 +95,67 @@ Run the commands::
7795
python manage.py djpaddle_sync_plans_from_paddle
7896
7997
98+
Paddle Checkout
99+
---------------
100+
101+
Next to setup a `PaddleJS checkout page <https://developer.paddle.com/guides/how-tos/checkout/paddle-checkout>`_
102+
103+
First load in PaddleJS and initialise it by including the dj-paddle PaddleJS template in your own template to load PaddleJS::
104+
105+
{% include "djpaddle_paddlejs.html" %}
106+
107+
108+
Next add a Paddle product or subscription plan into the page context. Below is an example of how to do this using a class based view where ``plan_id`` is passed through as a value from the URL
109+
110+
.. code-block:: python
111+
112+
from django.conf import settings
113+
from django.views.generic import TemplateView
114+
115+
from djpaddle.models import Plan
116+
117+
118+
class Checkout(TemplateView):
119+
template_name = 'checkout.html'
120+
121+
def get_context_data(self, **kwargs):
122+
context = super().get_context_data(**kwargs)
123+
124+
context['paddle_plan'] = Plan.objects.get(pk=kwargs['plan_id'])
125+
# If you have not added 'djpaddle.context_processors.vendor_id' as a template context processors
126+
context['DJPADDLE_VENDOR_ID'] = settings.DJPADDLE_VENDOR_ID
127+
128+
return context
129+
130+
131+
Finally put a ``Buy Now!`` button for the plan subscription you added to the context ::
132+
133+
<a href="#!" class="paddle_button" data-product="{{ paddle_plan.id }}">Buy Now!</a>
134+
135+
136+
You can pass data to Paddle JS by add data attributes to the button. For example to set the users email you can use the ``data-email`` attribute ::
137+
138+
<a href="#!" class="paddle_button" data-product="{{ paddle_plan.id }}" data-email="{{ user.email }}" >Buy Now!</a>
139+
140+
141+
A full list of parameters can be found on the `PaddleJS parameters page <https://developer.paddle.com/webhook-reference/intro>`_
142+
143+
Subscription model
144+
------------------
145+
146+
You can override the model that subscriptions are attached to using the ``DJPADDLE_SUBSCRIBER_MODEL`` setting. This setting must use the string model reference in the style 'app_label.ModelName'.
147+
148+
The model chosen must have an ``email`` field.
149+
150+
.. code-block:: python
151+
152+
# Defaults to AUTH_USER_MODEL
153+
DJPADDLE_SUBSCRIBER_MODEL = 'myapp.MyModel'
154+
155+
**Warning**: To use this setting you must have already created and ran the initial migration for the app/model before adding ``djpadding`` to ``INSTALLED_APPS``.
156+
157+
158+
80159
Reporting Security Issues
81160
-------------------------
82161

djpaddle/admin.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
from . import models
44

5+
admin.site.register(models.Checkout)
6+
57

68
class PriceInline(admin.TabularInline):
79
model = models.Price

djpaddle/migrations/0002_checkout.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Generated by Django 3.1a1 on 2020-05-22 23:52
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('djpaddle', '0001_initial'),
10+
]
11+
12+
operations = [
13+
migrations.CreateModel(
14+
name='Checkout',
15+
fields=[
16+
('id', models.CharField(max_length=40, primary_key=True, serialize=False)),
17+
('completed', models.NullBooleanField()),
18+
('passthrough', models.TextField(blank=True, null=True)),
19+
('email', models.EmailField(blank=True, max_length=254, null=True)),
20+
('created_at', models.DateTimeField(blank=True, null=True)),
21+
],
22+
),
23+
]

djpaddle/models.py

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -187,12 +187,7 @@ def _sanitize_webhook_payload(cls, payload):
187187
return data
188188

189189
@classmethod
190-
def from_subscription_created(cls, payload):
191-
data = cls._sanitize_webhook_payload(payload)
192-
return cls.objects.get_or_create(**data)
193-
194-
@classmethod
195-
def update_by_payload(cls, payload):
190+
def create_or_update_by_payload(cls, payload):
196191
data = cls._sanitize_webhook_payload(payload)
197192
pk = data.pop("id")
198193
return cls.objects.update_or_create(pk=pk, defaults=data)
@@ -201,19 +196,32 @@ def __str__(self):
201196
return "Subscription <{}:{}>".format(str(self.subscriber), str(self.id))
202197

203198

199+
class Checkout(models.Model):
200+
"""
201+
Used to store checkout info from PaddleJS. Transient model which acts as
202+
a backup in case the webhook is not recieved straight away
203+
"""
204+
205+
id = models.CharField(max_length=40, primary_key=True)
206+
completed = models.NullBooleanField()
207+
passthrough = models.TextField(null=True, blank=True)
208+
email = models.EmailField(null=True, blank=True)
209+
created_at = models.DateTimeField(null=True, blank=True)
210+
211+
204212
@receiver(signals.subscription_created)
205213
def on_subscription_created(sender, payload, *args, **kwargs):
206-
Subscription.from_subscription_created(payload)
214+
Subscription.create_or_update_by_payload(payload)
207215

208216

209217
@receiver(signals.subscription_updated)
210218
def on_subscription_updated(sender, payload, *args, **kwargs):
211-
Subscription.update_by_payload(payload)
219+
Subscription.create_or_update_by_payload(payload)
212220

213221

214222
@receiver(signals.subscription_cancelled)
215223
def on_subscription_cancelled(sender, payload, *args, **kwargs):
216-
Subscription.update_by_payload(payload)
224+
Subscription.create_or_update_by_payload(payload)
217225

218226

219227
if settings.DJPADDLE_LINK_STALE_SUBSCRIPTIONS:
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<script src="https://cdn.paddle.com/paddle/paddle.js"></script>
2+
<script type="text/javascript">
3+
Paddle.Setup({
4+
vendor: {{ DJPADDLE_VENDOR_ID }},
5+
{% if debug %}
6+
debug: true,
7+
{% endif %}
8+
});
9+
</script>
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<div id="csrf">{% csrf_token %}</div>
2+
<script type="text/javascript">
3+
4+
function checkoutComplete(data) {
5+
var csrftoken = document.getElementById("csrf").getElementsByTagName("input")[0].value;
6+
7+
var email = "";
8+
if (data.hasOwnProperty("user") && data.user !== null){
9+
email = data.user.email;
10+
}
11+
var postData = {
12+
id: data.checkout.id,
13+
completed: data.checkout.completed,
14+
passthrough: data.checkout.passthrough,
15+
email: email,
16+
created_at: data.checkout.created_at,
17+
redirect_url: data.checkout.redirect_url,
18+
};
19+
var encodedString = buildFormData(postData);
20+
21+
var xhr = new XMLHttpRequest();
22+
xhr.open("POST", "{% url 'djpaddle:post_checkout_api' %}?next={{ djpaddle_checkout_success_redirect }}");
23+
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
24+
xhr.setRequestHeader("X-CSRFToken", csrftoken);
25+
xhr.onreadystatechange = function(){
26+
if (xhr.readyState === XMLHttpRequest.DONE) {
27+
if (xhr.status === 200) {
28+
var jsonResponse = JSON.parse(xhr.responseText);
29+
if (jsonResponse.hasOwnProperty("redirect_url")) {
30+
window.location.href = jsonResponse["redirect_url"];
31+
}
32+
}
33+
}
34+
}
35+
xhr.send(encodedString);
36+
}
37+
38+
function buildFormData(data) {
39+
var encodedString = "";
40+
for (var prop in data) {
41+
if (data.hasOwnProperty(prop)) {
42+
if (encodedString.length > 0) {
43+
encodedString += "&";
44+
}
45+
encodedString += encodeURI(prop + "=" + data[prop]);
46+
}
47+
}
48+
return encodedString;
49+
}
50+
51+
function addCheckoutCompleteToPaddleButtons() {
52+
var paddleButtons = document.getElementsByClassName("paddle_button");
53+
for (var k in paddleButtons) {
54+
if (paddleButtons.hasOwnProperty(k)) {
55+
paddleButtons[k].setAttribute("data-success-callback", "checkoutComplete");
56+
}
57+
}
58+
}
59+
60+
document.addEventListener("DOMContentLoaded", function(event) {
61+
addCheckoutCompleteToPaddleButtons();
62+
});
63+
</script>

djpaddle/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@
66

77
urlpatterns = [
88
path("webhook/", views.paddle_webhook_view, name="webhook"),
9+
path("post-checkout/", views.post_checkout_api_view, name="post_checkout_api"),
910
]

djpaddle/views.py

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
1-
from django.http import HttpResponse, HttpResponseBadRequest
1+
from distutils.util import strtobool
2+
3+
from django.core.exceptions import ValidationError
4+
from django.http import HttpResponse, HttpResponseBadRequest, JsonResponse
25
from django.utils.decorators import method_decorator
36
from django.views.decorators.csrf import csrf_exempt
47
from django.views.generic import View
8+
from django.views.generic.edit import BaseCreateView
59

610
from . import signals
11+
from .models import Checkout
712
from .utils import is_valid_webhook
813

914

@@ -45,16 +50,48 @@ def post(self, request, *args, **kwargs):
4550
if not is_valid_webhook(payload):
4651
return HttpResponseBadRequest("webhook validation failed")
4752

48-
alert_name = payload.get("alert_name", None)
49-
if alert_name is None:
53+
alert_name = payload.get("alert_name")
54+
if not alert_name:
5055
return HttpResponseBadRequest("'alert_name' missing")
5156

5257
if alert_name in self.SUPPORTED_WEBHOOKS:
53-
signal = getattr(signals, alert_name, None)
54-
if signal is not None: # pragma: no cover
58+
signal = getattr(signals, alert_name)
59+
if signal: # pragma: no cover
5560
signal.send(sender=self.__class__, payload=payload)
5661

5762
return HttpResponse()
5863

5964

65+
class PaddlePostCheckoutApiView(BaseCreateView):
66+
http_method_names = ["post"]
67+
68+
def post(self, request, *args, **kwargs):
69+
data = request.POST.dict()
70+
redirect_url = data.pop("redirect_url") if "redirect_url" in data else ""
71+
pk = data.pop("id")
72+
if not pk:
73+
return HttpResponseBadRequest('Missing "id"')
74+
try:
75+
data["completed"] = bool(strtobool(data["completed"]))
76+
except KeyError:
77+
return HttpResponseBadRequest('Missing "completed"')
78+
79+
try:
80+
Checkout.objects.update_or_create(pk=pk, defaults=data)
81+
except ValidationError as e:
82+
return HttpResponseBadRequest(e)
83+
84+
next_url = request.GET.get("next")
85+
if next_url:
86+
next_url = "{0}?checkout={1}".format(next_url, pk)
87+
return JsonResponse({"redirect_url": next_url}, status=200)
88+
89+
if redirect_url:
90+
redirect_url = "{0}?checkout={1}".format(redirect_url, pk)
91+
return JsonResponse({"redirect_url": redirect_url}, status=200)
92+
93+
return JsonResponse({}, status=204)
94+
95+
6096
paddle_webhook_view = PaddleWebhookView.as_view()
97+
post_checkout_api_view = PaddlePostCheckoutApiView.as_view()

0 commit comments

Comments
 (0)