Skip to content

Commit a5c12e3

Browse files
committed
Add server-side Stripe checkout for recurring donations
1 parent 3581854 commit a5c12e3

File tree

7 files changed

+53
-42
lines changed

7 files changed

+53
-42
lines changed

assets/js/cardpayments.js

Lines changed: 38 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"use strict";
22

33
const STRIPE_PREPARE_PAYMENT_URL = API_BASE_URL + '/donations/stripe/payments/prepare';
4+
const STRIPE_SUBSCRIPTION_CHECKOUT_URL = API_BASE_URL + '/donations/stripe/subscriptions/checkout';
45

56
class OneTimePayment {
67

@@ -135,32 +136,47 @@ class OneTimePayment {
135136
class RecurringPayment {
136137

137138
/**
138-
* Creates a new recurring payment object.
139-
* @param {number} amount integer $$$
140-
* @param {string} currency EUR or USD
141-
* @param {string} languageCode The IETF language tag of the locale to display Stripe placeholders and error strings in
139+
* Initializes the recurring payment helper and stores a reference to the status object
140+
* @param {Object} status
141+
* @param {string} status.captcha The captcha (if captcha validation finished) or null
142+
* @param {string} status.errorMessage An error message or null
143+
* @param {boolean} status.inProgress Whether an async payment task is currently running
144+
*/
145+
constructor(status) {
146+
this._status = status;
147+
}
148+
149+
/**
150+
* Creates a Stripe Checkout Session and redirects to it
151+
* @param {number} amount How many units of the given currency to pay per month
152+
* @param {string} currency Which currency to pay in (EUR or USD)
153+
* @param {string} languageCode The IETF language tag for Stripe Checkout UI locale
142154
*/
143155
checkout(amount, currency, languageCode) {
144-
let plan = STRIPE_PLANS[currency];
156+
this._status.inProgress = true;
157+
this._status.errorMessage = '';
158+
159+
const successUrl = window.location.href.split('#')[0] + 'thanks';
160+
const cancelUrl = window.location.href;
161+
145162
$.ajax({
146-
url: 'https://js.stripe.com/v3/',
147-
cache: true,
148-
dataType: 'script'
163+
url: STRIPE_SUBSCRIPTION_CHECKOUT_URL,
164+
type: 'POST',
165+
data: {
166+
amount: parseInt(amount),
167+
currency: currency,
168+
successUrl: successUrl,
169+
cancelUrl: cancelUrl,
170+
locale: languageCode,
171+
captcha: this._status.captcha
172+
}
149173
}).then(response => {
150-
return window.Stripe(STRIPE_PK);
151-
}).then(stripe => {
152-
stripe.redirectToCheckout({
153-
items: [
154-
{plan: plan, quantity: parseInt(amount)}
155-
],
156-
successUrl: window.location.href.split('#')[0] + 'thanks',
157-
cancelUrl: window.location.href,
158-
locale: languageCode
159-
}).then(result => {
160-
if (result.error) {
161-
console.log(result.error.message);
162-
}
163-
});
174+
// Redirect to Stripe Checkout
175+
window.location.href = response.url;
176+
}).catch(error => {
177+
console.error('Failed to create checkout session:', error);
178+
this._status.errorMessage = error.responseJSON?.message || 'Failed to create checkout session';
179+
this._status.inProgress = false;
164180
});
165181
}
166182

assets/js/const.template.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,3 @@ const PADDLE_DISCOUNT_ID = '{{ .Site.Params.paddleDiscountId }}';
1313
const PADDLE_DISCOUNT_CODE = '{{ .Site.Params.paddleDiscountCode }}';
1414
const LEGACY_STORE_URL = '{{ .Site.Params.legacyStoreUrl }}';
1515
const STRIPE_PK = '{{ .Site.Params.stripePk }}';
16-
const STRIPE_PLANS = {{ .Site.Params.stripePlans | jsonify }};

config/development/params.yaml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,3 @@ paddleDiscountCode: WINTER2025
2222

2323
# STRIPE
2424
stripePk: pk_test_51RCM24IBZmkR4F9UiLBiSmsAnJvWqmHcDLxXR8ABKK1MNsZk3zCk2VJW7ZfaBlD81zpQxCX243sS3LEp9dABwiG800kJnGykDF
25-
stripePlans:
26-
EUR: plan_GgVY2JfD49bc02
27-
USD: plan_GgVZwj545E0uH3

config/production/params.yaml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,3 @@ paddleDiscountCode: WINTER2025
2222

2323
# STRIPE
2424
stripePk: pk_live_eSasX216vGvC26GdbVwA011V
25-
stripePlans:
26-
EUR: plan_GgW4ovr7c6upzx
27-
USD: plan_GejOEdJtfL3kdH

config/staging/params.yaml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,3 @@ paddleDiscountCode: WINTER2025
2222

2323
# STRIPE
2424
stripePk: pk_test_51RCM24IBZmkR4F9UiLBiSmsAnJvWqmHcDLxXR8ABKK1MNsZk3zCk2VJW7ZfaBlD81zpQxCX243sS3LEp9dABwiG800kJnGykDF
25-
stripePlans:
26-
EUR: plan_GgVY2JfD49bc02
27-
USD: plan_GgVZwj545E0uH3

layouts/partials/captcha.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,5 @@
1212
verifying: '{{ i18n "altcha_verifying" }}',
1313
waitAlert: '{{ i18n "altcha_waitAlert" }}'
1414
})"
15-
x-ref="captcha"
15+
x-ref="{{ with .ref }}{{ . }}{{ else }}captcha{{ end }}"
1616
></altcha-widget>

layouts/partials/donate-creditcard.html

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<div x-data="{amount: 30, currency: 'EUR', frequency: 'once', oneTimePayment: null, oneTimePaymentStatus: {validCardNum: false, captcha: null, errorMessage: '', inProgress: false, success: false}, recurringPayment: new RecurringPayment(), acceptTerms: false, captchaState: null}" x-init="oneTimePayment = new OneTimePayment(oneTimePaymentStatus)">
1+
<div x-data="{amount: 30, currency: 'EUR', frequency: 'once', oneTimePayment: null, oneTimePaymentStatus: {validCardNum: false, captcha: null, errorMessage: '', inProgress: false, success: false}, recurringPayment: null, recurringPaymentStatus: {captcha: null, errorMessage: '', inProgress: false}, acceptTerms: false, oneTimeCaptchaState: null, recurringCaptchaState: null}" x-init="oneTimePayment = new OneTimePayment(oneTimePaymentStatus); recurringPayment = new RecurringPayment(recurringPaymentStatus)">
22
<div x-show="!oneTimePaymentStatus.success">
33
<div class="flex flex-wrap md:flex-nowrap">
44
<div class="w-full mb-4 md:w-1/2 md:pr-3">
@@ -30,7 +30,7 @@
3030
</div>
3131
</div>
3232

33-
<form x-show="frequency === 'once'" @submit.prevent="oneTimePayment.charge(amount, currency); $refs.captcha.reset()">
33+
<form x-show="frequency === 'once'" @submit.prevent="oneTimePayment.charge(amount, currency); $refs.oneTimeCaptcha.reset()">
3434
<div class="mb-4">
3535
<label class="label-uppercase mb-2">{{ i18n "donate_creditcard_number" }}</label>
3636
<div> <!-- wrapper needed for stripe text field -->
@@ -43,25 +43,30 @@
4343

4444
<p class="font-p mb-4">{{ partial "checkbox.html" (dict "context" . "alpineVariable" "acceptTerms" "label" (i18n "accept_privacy" | safeHTML)) }}</p>
4545

46-
<button :disabled="oneTimePaymentStatus.inProgress || !oneTimePaymentStatus.validCardNum || !acceptTerms || captchaState == 'verifying'" type="submit" class="btn btn-primary w-full md:w-64" data-umami-event="donate-creditcard-onetime-checkout">
46+
<button :disabled="oneTimePaymentStatus.inProgress || !oneTimePaymentStatus.validCardNum || !acceptTerms || oneTimeCaptchaState == 'verifying'" type="submit" class="btn btn-primary w-full md:w-64" data-umami-event="donate-creditcard-onetime-checkout">
4747
<i :class="{'fa-credit-card': !oneTimePaymentStatus.inProgress, 'fa-spinner fa-spin': oneTimePaymentStatus.inProgress}" class="fa-solid" aria-hidden="true"></i>
4848
{{ i18n "donate_creditcard_once_paynow" }}
4949
</button>
5050

51-
{{ $challengeUrl := printf "%s/donations/stripe/payments/challenge" .Site.Params.apiBaseUrl }}
52-
{{ partial "captcha.html" (dict "challengeUrl" $challengeUrl "captchaPayload" "oneTimePaymentStatus.captcha" "captchaState" "captchaState") }}
51+
{{ $oneTimeChallengeUrl := printf "%s/donations/stripe/payments/challenge" .Site.Params.apiBaseUrl }}
52+
{{ partial "captcha.html" (dict "challengeUrl" $oneTimeChallengeUrl "captchaPayload" "oneTimePaymentStatus.captcha" "captchaState" "oneTimeCaptchaState" "ref" "oneTimeCaptcha") }}
5353

5454
<p class="text-sm text-red-600 mt-2" x-text="oneTimePaymentStatus.errorMessage"></p>
5555
</div>
5656
</form>
5757

58-
<div x-show="frequency === 'recurring'" class="text-center">
58+
<form x-show="frequency === 'recurring'" @submit.prevent="recurringPayment.checkout(amount, currency, '{{ .Site.Language.Lang }}'); $refs.recurringCaptcha.reset()" class="text-center">
5959
<p class="font-p mb-4">{{ i18n "donate_creditcard_recurring_instruction" | safeHTML }}</p>
6060
<p class="font-p mb-4">{{ partial "checkbox.html" (dict "context" . "alpineVariable" "acceptTerms" "label" (i18n "accept_privacy" | safeHTML)) }}</p>
61-
<button type="button" class="btn btn-primary w-full md:w-64" data-umami-event="donate-creditcard-recurring-checkout" @click="recurringPayment.checkout(amount, currency, '{{ .Site.Language.Lang }}')" :disabled="!acceptTerms">
62-
<i class="fa-solid fa-external-link" aria-hidden="true"></i> {{ i18n "donate_creditcard_recurring_calltoaction" }}
61+
<button type="submit" class="btn btn-primary w-full md:w-64" data-umami-event="donate-creditcard-recurring-checkout" :disabled="!acceptTerms || recurringPaymentStatus.inProgress || recurringCaptchaState == 'verifying'">
62+
<i :class="{'fa-external-link': !recurringPaymentStatus.inProgress, 'fa-spinner fa-spin': recurringPaymentStatus.inProgress}" class="fa-solid" aria-hidden="true"></i> {{ i18n "donate_creditcard_recurring_calltoaction" }}
6363
</button>
64-
</div>
64+
65+
{{ $recurringChallengeUrl := printf "%s/donations/stripe/subscriptions/challenge" .Site.Params.apiBaseUrl }}
66+
{{ partial "captcha.html" (dict "challengeUrl" $recurringChallengeUrl "captchaPayload" "recurringPaymentStatus.captcha" "captchaState" "recurringCaptchaState" "ref" "recurringCaptcha") }}
67+
68+
<p class="text-sm text-red-600 mt-2" x-text="recurringPaymentStatus.errorMessage"></p>
69+
</form>
6570
</div>
6671

6772
<div x-show="oneTimePaymentStatus.success" x-cloak>

0 commit comments

Comments
 (0)