Skip to content

Commit 851abd3

Browse files
committed
Merge branch 'main' into prod-cluster
2 parents 098af7d + d706c7c commit 851abd3

File tree

8 files changed

+190
-41
lines changed

8 files changed

+190
-41
lines changed

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

Lines changed: 38 additions & 9 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,9 +83,15 @@ 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+
// We re-use this handler since it's not currently used, and it's hooked up to unMount.
89+
doneCheckerHandler = window.setTimeout(() => {
90+
window.location.reload();
91+
}, SHORT_WAIT_MS);
92+
return;
8093
}
81-
94+
8295
// Lastly clear up the exception counter, if we've reached here there's no exceptions happening and no errors need to be shown.
8396
exceptionCounter = 0;
8497
} catch (e) {
@@ -132,21 +145,34 @@ const onPaddleEvent = async (evt: PaddleEventData) => {
132145
// Just update the cart, every checkout.* event has all the information on it.
133146
if (evt.name.indexOf('checkout.') === 0) {
134147
const data = evt.data;
148+
const backendNeedsUpdate = transactionId !== (data?.transaction_id ?? null) || paymentType !== data.payment.method_details.type;
149+
150+
// Transaction ID should only update if we're not falsey.
151+
if (data?.transaction_id) {
152+
transactionId = data?.transaction_id ?? null;
153+
}
135154
136-
// Set the transaction id
137-
if (!doneCheckerHandler && data?.transaction_id) {
155+
// Payment type is only reliably updated on payment selected.
156+
if (evt.name == 'checkout.payment.selected') {
157+
paymentType = data.payment.method_details.type;
158+
}
159+
160+
if (backendNeedsUpdate) {
138161
await fetch('/api/v1/subscription/paddle/tx/set/', {
139162
mode: 'same-origin',
140163
credentials: 'include',
141164
method: 'PUT',
142165
body: JSON.stringify({
143-
txid: data?.transaction_id,
166+
payment_type: paymentType,
167+
txid: transactionId,
144168
}),
145169
headers: {
146170
'X-CSRFToken': csrfToken,
147171
},
148172
});
173+
}
149174
175+
if (!doneCheckerHandler && transactionId) {
150176
// Setup our done checker
151177
doneCheckerHandler = window.setTimeout(() => areWeDoneHere(), ARE_WE_DONE_YET_TIMER_MS);
152178
}
@@ -208,8 +234,9 @@ const setupPaddle = async () => {
208234
eventCallback: onPaddleEvent,
209235
checkout: {
210236
settings: {
237+
successUrl: `${window.location.origin}/subscription/paddle/complete/`,
211238
displayMode: 'inline',
212-
frameTarget: 'checkout-container',
239+
frameTarget: 'paddle-checkout',
213240
frameInitialHeight: 992,
214241
frameStyle: 'width: 100%; background-color: transparent; border: none;',
215242
variant: 'one-page',
@@ -318,6 +345,7 @@ export default {
318345
<!-- Paddle's checkout will just disappear into the void once it starts redirecting us to successUrl.
319346
It looks ugly, so show a small message in its place. -->
320347
<p v-if="paymentComplete">{{ t('views.subscribe.paymentComplete') }}</p>
348+
<div v-else class="paddle-checkout"></div>
321349
</card-container>
322350
</div>
323351
</div>
@@ -350,6 +378,7 @@ export default {
350378
351379
.checkout-container {
352380
width: 100%;
381+
min-height: 1090px;
353382
}
354383
355384
.summary-card {

pulumi/Pulumi.stage.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,4 +71,4 @@ config:
7171
accounts:keycloak-admin-client-secret:
7272
secure: AAABACh3RnRJFRb37U658qPm+wN8GUDqeZ/Ck90/qJnGWmULNhVtn5u/+BD8NtVVvaqiMhRmwzja7wLeYypciQ==
7373
accounts:appointment-caldav-secret:
74-
secure: AAABAForTqyhyLA5jEP8mv6uX1s2Kp9CFim/R+NCEAvC8ZgmBhqxsmcL8zWeTBsOZlfLGlkVNBFVo7/teEknbw==
74+
secure: AAABAPXX4cm62Agkc0pob1MUB1c7qYxWgJeAFm1051m+Pe3uhZh9KA5qG9ZuTVCbj0i38CCkrefveMpxja23CQ==

pulumi/config.stage.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,7 @@ resources:
300300
- stalwart-api-auth-key
301301
- keycloak-admin-client-id
302302
- keycloak-admin-client-secret
303+
- appointment-caldav-secret
303304

304305
tb:ec2:SshableInstance: {}
305306
# Fill out this template to build an SSH bastion

src/thunderbird_accounts/mail/utils.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@ def decode_app_password(secret):
4848

4949

5050
def create_stalwart_account(user, app_password: Optional[str] = None) -> bool:
51-
# Run this immediately for now, in the future we'll ship these to celery
5251
if user.account_set.count() > 0 and user.account_set.first().stalwart_id:
5352
return False
5453

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<html lang="en">
2+
<head></head>
3+
<body>
4+
<p>You may close this window.</p>
5+
<noscript>...you'll need to close the window manually because you do not have javascript enabled. Sorry!</noscript>
6+
<script>window.close();</script>
7+
</body>
8+
</html>

src/thunderbird_accounts/subscription/tests/test_views.py

Lines changed: 66 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -121,17 +121,48 @@ def test_transaction_found_but_not_doneish(self):
121121

122122
instance.notifications.list.assert_not_called()
123123

124-
def _test_doneish_by_status(self, tx_status):
124+
125+
class PaddleTransactionCompleteCase(TestCase):
126+
def setUp(self):
127+
self.client = RequestClient()
128+
self.user = User.objects.create(
129+
username=f'test@{settings.PRIMARY_EMAIL_DOMAIN}', oidc_id='1234', is_awaiting_payment_verification=False
130+
)
131+
oidc_force_login(self.client, self.user)
132+
self.url = reverse('paddle_completed')
133+
self.txid = 'abc123'
134+
135+
def set_paddle_transaction_session_data(self, payment_type):
136+
"""This actually tests set_paddle_transaction_id too!"""
137+
txid_response = self.client.put(
138+
reverse('paddle_txid'), data=json.dumps({'txid': self.txid, 'payment_type': payment_type})
139+
)
140+
self.assertEqual(txid_response.status_code, 200)
141+
data = txid_response.json()
142+
self.assertTrue(data.get('success'))
143+
144+
def _test_doneish_by_status(self, tx_status, is_popup_payment_provider=False):
125145
"""Helper function so we can reduce some code without introducing artifacts between test runs."""
126-
# Make sure we have a txid in session
127-
self.set_paddle_transaction_id()
146+
# Make sure we have a txid and payment type in session
147+
payment_type = 'card' if not is_popup_payment_provider else 'paypal'
148+
ok_status_code = 200 if is_popup_payment_provider else 302
149+
ok_payment_verification = tx_status in [
150+
Transaction.StatusValues.PAID.value,
151+
Transaction.StatusValues.COMPLETED.value,
152+
]
153+
154+
self.set_paddle_transaction_session_data(payment_type)
128155

129-
transaction = Transaction.objects.create(paddle_id=self.txid, status=tx_status)
156+
transaction = Transaction.objects.create(paddle_id=self.txid, status=tx_status.value)
130157
self.assertIsNotNone(transaction)
131158

132159
with patch('thunderbird_accounts.subscription.tasks.dev_only_paddle_fake_webhook', MagicMock()) as task_mock:
133160
with patch('thunderbird_accounts.subscription.decorators.Client', MagicMock()) as paddle_client_mock:
134161
instance = paddle_client_mock()
162+
status_mock = MagicMock()
163+
164+
status_mock.status = tx_status
165+
instance.transactions.get.return_value = status_mock
135166

136167
self.assertFalse(self.user.is_awaiting_payment_verification)
137168

@@ -140,26 +171,31 @@ def _test_doneish_by_status(self, tx_status):
140171
follow=False,
141172
)
142173
self.assertTrue(response)
143-
self.assertEqual(response.status_code, 200)
144-
145-
data = response.json()
146-
self.assertTrue(data)
147-
self.assertEqual(data.get('status'), tx_status)
174+
self.assertEqual(response.status_code, ok_status_code)
148175

149176
self.user.refresh_from_db()
150-
self.assertTrue(self.user.is_awaiting_payment_verification)
177+
self.assertEqual(self.user.is_awaiting_payment_verification, ok_payment_verification)
151178

179+
instance.transactions.get.assert_called()
152180
instance.notifications.list.assert_not_called()
153181
task_mock.assert_not_called()
154182

183+
@override_settings(IS_DEV=False)
184+
def test_transaction_found_and_is_not_doneish_by_being_ready(self):
185+
"""We found the transaction but it's doneish (status=READY). This shouldn't do anything!
186+
187+
We should also ensure IS_DEV=False does not call Paddle or the fake webhook task."""
188+
189+
self._test_doneish_by_status(Transaction.StatusValues.READY)
190+
155191
@override_settings(IS_DEV=False)
156192
def test_transaction_found_and_is_doneish_by_being_paid(self):
157193
"""We found the transaction but it's doneish (status=PAID). This should trigger payment verification
158194
and remove txid from session.
159195
160196
We should also ensure IS_DEV=False does not call Paddle or the fake webhook task."""
161197

162-
self._test_doneish_by_status(Transaction.StatusValues.PAID.value)
198+
self._test_doneish_by_status(Transaction.StatusValues.PAID)
163199

164200
@override_settings(IS_DEV=False)
165201
def test_transaction_found_and_is_doneish_by_being_completed(self):
@@ -168,4 +204,22 @@ def test_transaction_found_and_is_doneish_by_being_completed(self):
168204
169205
We should also ensure IS_DEV=False does not call Paddle or the fake webhook task."""
170206

171-
self._test_doneish_by_status(Transaction.StatusValues.COMPLETED.value)
207+
self._test_doneish_by_status(Transaction.StatusValues.COMPLETED)
208+
209+
@override_settings(IS_DEV=False)
210+
def test_transaction_found_and_is_doneish_by_being_paid_popup_popup_edition(self):
211+
"""We found the transaction but it's doneish (status=PAID). This should trigger payment verification
212+
and remove txid from session.
213+
214+
We should also ensure IS_DEV=False does not call Paddle or the fake webhook task."""
215+
216+
self._test_doneish_by_status(Transaction.StatusValues.PAID, True)
217+
218+
@override_settings(IS_DEV=False)
219+
def test_transaction_found_and_is_doneish_by_being_completed_popup_edition(self):
220+
"""We found the transaction but it's doneish (status=COMPLETED). This should trigger payment verification
221+
and remove txid from session.
222+
223+
We should also ensure IS_DEV=False does not call Paddle or the fake webhook task."""
224+
225+
self._test_doneish_by_status(Transaction.StatusValues.COMPLETED, True)

0 commit comments

Comments
 (0)