Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 38 additions & 22 deletions assets/js/cardpayments.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"use strict";

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

class OneTimePayment {

Expand Down Expand Up @@ -135,32 +136,47 @@ class OneTimePayment {
class RecurringPayment {

/**
* Creates a new recurring payment object.
* @param {number} amount integer $$$
* @param {string} currency EUR or USD
* @param {string} languageCode The IETF language tag of the locale to display Stripe placeholders and error strings in
* Initializes the recurring payment helper and stores a reference to the status object
* @param {Object} status
* @param {string} status.captcha The captcha (if captcha validation finished) or null
* @param {string} status.errorMessage An error message or null
* @param {boolean} status.inProgress Whether an async payment task is currently running
*/
constructor(status) {
this._status = status;
}

/**
* Creates a Stripe Checkout Session and redirects to it
* @param {number} amount How many units of the given currency to pay per month
* @param {string} currency Which currency to pay in (EUR or USD)
* @param {string} languageCode The IETF language tag for Stripe Checkout UI locale
*/
checkout(amount, currency, languageCode) {
let plan = STRIPE_PLANS[currency];
this._status.inProgress = true;
this._status.errorMessage = '';

const successUrl = window.location.href.split('#')[0] + 'thanks';
const cancelUrl = window.location.href;
Comment on lines +159 to +160
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

URL construction may produce incorrect path without trailing slash.

If window.location.href is /donate (no trailing slash), the successUrl becomes /donatethanks instead of /donate/thanks. Consider normalizing the base URL:

🔎 Proposed fix
-    const successUrl = window.location.href.split('#')[0] + 'thanks';
+    const baseUrl = window.location.href.split('#')[0];
+    const successUrl = baseUrl.endsWith('/') ? baseUrl + 'thanks' : baseUrl + '/thanks';
🤖 Prompt for AI Agents
assets/js/cardpayments.js around lines 159-160: the current successUrl
concatenation can merge path segments when the current URL lacks a trailing
slash (e.g., "/donate" -> "/donatethanks"); strip any fragment from
window.location.href, normalize the resulting base so it ends with a single
trailing slash (add one if missing, avoid duplicating), then append "thanks" to
produce "/donate/thanks"; keep cancelUrl as the full current href.


$.ajax({
url: 'https://js.stripe.com/v3/',
cache: true,
dataType: 'script'
url: STRIPE_SUBSCRIPTION_CHECKOUT_URL,
type: 'POST',
data: {
amount: parseInt(amount),
currency: currency,
successUrl: successUrl,
cancelUrl: cancelUrl,
locale: languageCode,
captcha: this._status.captcha
}
}).then(response => {
return window.Stripe(STRIPE_PK);
}).then(stripe => {
stripe.redirectToCheckout({
items: [
{plan: plan, quantity: parseInt(amount)}
],
successUrl: window.location.href.split('#')[0] + 'thanks',
cancelUrl: window.location.href,
locale: languageCode
}).then(result => {
if (result.error) {
console.log(result.error.message);
}
});
// Redirect to Stripe Checkout
window.location.href = response.url;
}).catch(error => {
console.error('Failed to create checkout session:', error);
this._status.errorMessage = error.responseJSON?.message || 'Failed to create checkout session';
this._status.inProgress = false;
});
}

Expand Down
1 change: 0 additions & 1 deletion assets/js/const.template.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,3 @@ const PADDLE_DISCOUNT_ID = '{{ .Site.Params.paddleDiscountId }}';
const PADDLE_DISCOUNT_CODE = '{{ .Site.Params.paddleDiscountCode }}';
const LEGACY_STORE_URL = '{{ .Site.Params.legacyStoreUrl }}';
const STRIPE_PK = '{{ .Site.Params.stripePk }}';
const STRIPE_PLANS = {{ .Site.Params.stripePlans | jsonify }};
3 changes: 0 additions & 3 deletions config/development/params.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,3 @@ paddleDiscountCode: WINTER2025

# STRIPE
stripePk: pk_test_51RCM24IBZmkR4F9UiLBiSmsAnJvWqmHcDLxXR8ABKK1MNsZk3zCk2VJW7ZfaBlD81zpQxCX243sS3LEp9dABwiG800kJnGykDF
stripePlans:
EUR: plan_GgVY2JfD49bc02
USD: plan_GgVZwj545E0uH3
3 changes: 0 additions & 3 deletions config/production/params.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,3 @@ paddleDiscountCode: WINTER2025

# STRIPE
stripePk: pk_live_eSasX216vGvC26GdbVwA011V
stripePlans:
EUR: plan_GgW4ovr7c6upzx
USD: plan_GejOEdJtfL3kdH
3 changes: 0 additions & 3 deletions config/staging/params.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,3 @@ paddleDiscountCode: WINTER2025

# STRIPE
stripePk: pk_test_51RCM24IBZmkR4F9UiLBiSmsAnJvWqmHcDLxXR8ABKK1MNsZk3zCk2VJW7ZfaBlD81zpQxCX243sS3LEp9dABwiG800kJnGykDF
stripePlans:
EUR: plan_GgVY2JfD49bc02
USD: plan_GgVZwj545E0uH3
2 changes: 1 addition & 1 deletion layouts/partials/captcha.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,5 @@
verifying: '{{ i18n "altcha_verifying" }}',
waitAlert: '{{ i18n "altcha_waitAlert" }}'
})"
x-ref="captcha"
x-ref="{{ with .ref }}{{ . }}{{ else }}captcha{{ end }}"
></altcha-widget>
23 changes: 14 additions & 9 deletions layouts/partials/donate-creditcard.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<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)">
<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)">
<div x-show="!oneTimePaymentStatus.success">
<div class="flex flex-wrap md:flex-nowrap">
<div class="w-full mb-4 md:w-1/2 md:pr-3">
Expand Down Expand Up @@ -30,7 +30,7 @@
</div>
</div>

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

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

<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">
<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">
<i :class="{'fa-credit-card': !oneTimePaymentStatus.inProgress, 'fa-spinner fa-spin': oneTimePaymentStatus.inProgress}" class="fa-solid" aria-hidden="true"></i>
{{ i18n "donate_creditcard_once_paynow" }}
</button>

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

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

<div x-show="frequency === 'recurring'" class="text-center">
<form x-show="frequency === 'recurring'" @submit.prevent="recurringPayment.checkout(amount, currency, '{{ .Site.Language.Lang }}'); $refs.recurringCaptcha.reset()" class="text-center">
<p class="font-p mb-4">{{ i18n "donate_creditcard_recurring_instruction" | safeHTML }}</p>
<p class="font-p mb-4">{{ partial "checkbox.html" (dict "context" . "alpineVariable" "acceptTerms" "label" (i18n "accept_privacy" | safeHTML)) }}</p>
<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">
<i class="fa-solid fa-external-link" aria-hidden="true"></i> {{ i18n "donate_creditcard_recurring_calltoaction" }}
<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'">
<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" }}
</button>
</div>

{{ $recurringChallengeUrl := printf "%s/donations/stripe/subscriptions/challenge" .Site.Params.apiBaseUrl }}
{{ partial "captcha.html" (dict "challengeUrl" $recurringChallengeUrl "captchaPayload" "recurringPaymentStatus.captcha" "captchaState" "recurringCaptchaState" "ref" "recurringCaptcha") }}

<p class="text-sm text-red-600 mt-2" x-text="recurringPaymentStatus.errorMessage"></p>
</form>
</div>

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