Skip to content

Commit 71044de

Browse files
committed
Further improvements to our third-party payment processor flow.
* Brings back successUrl and the backend route * If the payment type was Paypal (or another pop-up window based type) then redirect them to a window.close template. * If the payment was not a pop-up window based type simply redirect them. * Added and enhanced plenty of structural window timeouts -.-.
1 parent ecdbdfc commit 71044de

File tree

3 files changed

+91
-22
lines changed

3 files changed

+91
-22
lines changed

assets/app/vue/views/SubscribeView/components/CheckoutStep.vue

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,24 @@ const initCurrencyFormatter = (code: string) => {
1616
});
1717
};
1818
19+
const DEFAULT_PAYMENT_TYPE = 'card';
20+
const ARE_WE_DONE_YET_TIMER_MS = 2500;
21+
const SHORT_WAIT_MS = 2500;
22+
const EXCEPTIONS_UNTIL_ERROR_SHOWN = 3;
23+
24+
// We don't need these to be reactive
1925
let currencyFormatter = initCurrencyFormatter('USD');
2026
let paddle = null;
2127
let doneCheckerHandler = null;
2228
let exceptionCounter = 0;
23-
29+
let transactionId = null;
30+
let paymentType = DEFAULT_PAYMENT_TYPE;
2431
2532
const planName = ref();
2633
const planSystemError = ref(false);
2734
const paymentComplete = ref(false);
2835
const paddleLoading = ref(true);
2936
const paddleUnknownError = ref(false);
30-
const ARE_WE_DONE_YET_TIMER_MS = 2500;
31-
const EXCEPTIONS_UNTIL_ERROR_SHOWN = 3;
3237
3338
// Placeholder information for the skeletons
3439
const order_summary = ref({
@@ -60,6 +65,8 @@ onUnmounted(() => {
6065
*
6166
* Poll the is-done api route every 2.5 seconds, and if we're doneish (paid or complete)
6267
* then reload the window to let the payment verification screen do their magic.
68+
*
69+
* Triggered by PaddleJS's checkout events. (Anyone with a transaction ID!)
6370
*/
6471
const areWeDoneHere = async () => {
6572
try {
@@ -76,7 +83,16 @@ const areWeDoneHere = async () => {
7683
7784
// For some reason PaddleJS has paid's constant as undefined. Hmmm...
7885
if (['completed', 'paid'].indexOf(status) > -1) {
79-
window.location.reload();
86+
// Remove the Paddle checkout form, and after a short wait reload the page.
87+
paymentComplete.value = true;
88+
}
89+
90+
if (status === 'completed') {
91+
// We re-use this handler since it's not currently used, and it's hooked up to unMount.
92+
doneCheckerHandler = window.setTimeout(() => {
93+
window.location.reload();
94+
}, SHORT_WAIT_MS);
95+
return;
8096
}
8197
8298
// Lastly clear up the exception counter, if we've reached here there's no exceptions happening and no errors need to be shown.
@@ -132,21 +148,34 @@ const onPaddleEvent = async (evt: PaddleEventData) => {
132148
// Just update the cart, every checkout.* event has all the information on it.
133149
if (evt.name.indexOf('checkout.') === 0) {
134150
const data = evt.data;
151+
const backendNeedsUpdate = transactionId !== (data?.transaction_id ?? null) || paymentType !== data.payment.method_details.type;
152+
153+
// Transaction ID should only update if we're not falsey.
154+
if (data?.transaction_id) {
155+
transactionId = data?.transaction_id ?? null;
156+
}
135157
136-
// Set the transaction id
137-
if (!doneCheckerHandler && data?.transaction_id) {
158+
// Payment type is only reliably updated on payment selected.
159+
if (evt.name == 'checkout.payment.selected') {
160+
paymentType = data.payment.method_details.type;
161+
}
162+
163+
if (backendNeedsUpdate) {
138164
await fetch('/api/v1/subscription/paddle/tx/set/', {
139165
mode: 'same-origin',
140166
credentials: 'include',
141167
method: 'PUT',
142168
body: JSON.stringify({
143-
txid: data?.transaction_id,
169+
payment_type: paymentType,
170+
txid: transactionId,
144171
}),
145172
headers: {
146173
'X-CSRFToken': csrfToken,
147174
},
148175
});
176+
}
149177
178+
if (!doneCheckerHandler && transactionId) {
150179
// Setup our done checker
151180
doneCheckerHandler = window.setTimeout(() => areWeDoneHere(), ARE_WE_DONE_YET_TIMER_MS);
152181
}
@@ -208,8 +237,9 @@ const setupPaddle = async () => {
208237
eventCallback: onPaddleEvent,
209238
checkout: {
210239
settings: {
240+
successUrl: `${window.location.origin}/subscription/paddle/complete/`,
211241
displayMode: 'inline',
212-
frameTarget: 'checkout-container',
242+
frameTarget: 'paddle-checkout',
213243
frameInitialHeight: 992,
214244
frameStyle: 'width: 100%; background-color: transparent; border: none;',
215245
variant: 'one-page',
@@ -241,10 +271,10 @@ export default {
241271
<h2>{{ t('views.subscribe.title') }}</h2>
242272
<notice-bar v-if="paddleUnknownError" :type="NoticeBarTypes.Critical">{{
243273
t('views.subscribe.paddleUnknownError')
244-
}}</notice-bar>
274+
}}</notice-bar>
245275
<notice-bar v-if="planSystemError" :type="NoticeBarTypes.Critical">{{
246276
t('views.subscribe.planSystemError')
247-
}}</notice-bar>
277+
}}</notice-bar>
248278
<div class="container">
249279
<card-container class="summary-card">
250280
<ul class="summary">
@@ -318,6 +348,7 @@ export default {
318348
<!-- Paddle's checkout will just disappear into the void once it starts redirecting us to successUrl.
319349
It looks ugly, so show a small message in its place. -->
320350
<p v-if="paymentComplete">{{ t('views.subscribe.paymentComplete') }}</p>
351+
<div v-else class="paddle-checkout"></div>
321352
</card-container>
322353
</div>
323354
</div>
@@ -350,6 +381,7 @@ export default {
350381
351382
.checkout-container {
352383
width: 100%;
384+
min-height: 1090px;
353385
}
354386
355387
.summary-card {

src/thunderbird_accounts/subscription/views.py

Lines changed: 47 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1+
from django.template.response import TemplateResponse
2+
from paddle_billing.Notifications.Entities.Shared.PaymentMethodType import PaymentMethodType
13
import datetime
24
import json
35
import logging
46

57
import sentry_sdk
68
from django.conf import settings
79
from django.contrib.auth.decorators import login_required
8-
from django.http import HttpResponse, JsonResponse
10+
from django.http import HttpResponse, JsonResponse, HttpRequest, HttpResponseRedirect
911
from django.utils.translation import gettext_lazy as _
1012
from django.views.decorators.http import require_http_methods
1113
from django.core.signing import Signer
@@ -25,6 +27,7 @@
2527

2628
# We only need this here right now
2729
SESSION_PADDLE_TRANSACTION_ID = 'paddle_txid'
30+
SESSION_PADDLE_PAYMENT_TYPE = str(PaymentMethodType.Card)
2831

2932

3033
def prefilter_paddle_webhook(event_type: str, event_data: dict) -> bool:
@@ -37,6 +40,48 @@ def prefilter_paddle_webhook(event_type: str, event_data: dict) -> bool:
3740
return True
3841

3942

43+
@login_required
44+
@inject_paddle
45+
def subscription_complete(request: HttpRequest, paddle: Client):
46+
"""User is redirected by Paddle via the successUrl. """
47+
user = request.user
48+
transaction_id = request.session.pop(SESSION_PADDLE_TRANSACTION_ID)
49+
payment_type = request.session.pop(SESSION_PADDLE_PAYMENT_TYPE)
50+
51+
if not transaction_id or not payment_type:
52+
pass
53+
54+
transaction = paddle.transactions.get(transaction_id=transaction_id)
55+
status = transaction.status.value
56+
57+
if transaction and status in [Transaction.StatusValues.COMPLETED.value, Transaction.StatusValues.PAID.value]:
58+
user.is_awaiting_payment_verification = True
59+
user.save()
60+
61+
if settings.IS_DEV:
62+
tasks.dev_only_paddle_fake_webhook.delay(transaction_id=transaction_id, user_uuid=user.uuid.hex)
63+
64+
"""
65+
Paddle folks mentioned these are the types that open pop-up windows:
66+
"""
67+
if payment_type in [
68+
PaymentMethodType.Paypal.value,
69+
# We don't use these, but for completeness' sake.
70+
PaymentMethodType.Alipay.value,
71+
PaymentMethodType.Bancontact.value,
72+
PaymentMethodType.Ideal.value,
73+
PaymentMethodType.KoreaLocal.value,
74+
]:
75+
# Tell their window to close
76+
return TemplateResponse(
77+
request,
78+
'close_window.html',
79+
status=200,
80+
)
81+
82+
return HttpResponseRedirect('/subscribe')
83+
84+
4085
@login_required
4186
@require_http_methods(['POST'])
4287
def get_paddle_information(request: Request):
@@ -66,6 +111,7 @@ def set_paddle_transaction_id(request: Request):
66111
It kinda sucks, but it works for now."""
67112
data = json.loads(request.body.decode())
68113
request.session[SESSION_PADDLE_TRANSACTION_ID] = data.get('txid')
114+
request.session[SESSION_PADDLE_PAYMENT_TYPE] = data.get('payment_type')
69115
return JsonResponse({'success': True})
70116

71117

@@ -94,17 +140,6 @@ def is_paddle_transaction_done(request: Request, paddle: Client):
94140
if transaction:
95141
status = transaction.status
96142

97-
# Once the transaction is doneish, set the user to awaiting verification and if on dev mode deploy the fake webhook
98-
if transaction and status in [Transaction.StatusValues.COMPLETED.value, Transaction.StatusValues.PAID.value]:
99-
user.is_awaiting_payment_verification = True
100-
user.save()
101-
102-
# Clean up transaction id if it's still in session
103-
request.session.pop(SESSION_PADDLE_TRANSACTION_ID, default=None)
104-
105-
if settings.IS_DEV:
106-
tasks.dev_only_paddle_fake_webhook.delay(transaction_id=transaction_id, user_uuid=user.uuid.hex)
107-
108143
return JsonResponse({'status': status})
109144

110145

src/thunderbird_accounts/urls.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@
4949
path('email-aliases/remove', mail_views.remove_email_alias, name='remove_email_alias'),
5050
# Authentication
5151
path('users/sign-up/', auth_views.sign_up, name='sign_up'),
52+
# Subscription
53+
path('subscription/paddle/complete/', subscription_views.subscription_complete, name='paddle_completed'),
5254
# CalDAV auto-setup for Appointment
5355
path('appointment/caldav/setup/', mail_views.appointment_caldav_setup, name='appointment_caldav_setup'),
5456
# API

0 commit comments

Comments
 (0)