mirror of https://gitlab.com/tildes/tildes.git
Browse Source
Re-add donations via Stripe
Re-add donations via Stripe
Multiple people have been asking me how they can make a one-time donation without going through GitHub or Patreon, so I'll re-add this for now but will need to keep an eye out for fraud and potentially disable it again soon.merge-requests/137/head
Deimos
1 year ago
10 changed files with 253 additions and 0 deletions
-
4tildes/production.ini.example
-
1tildes/requirements.in
-
10tildes/static/js/behaviors/stripe-checkout.js
-
28tildes/static/js/behaviors/stripe-donate-form.js
-
4tildes/tildes/lib/ratelimit.py
-
4tildes/tildes/routes.py
-
54tildes/tildes/templates/donate_stripe.jinja2
-
18tildes/tildes/templates/donate_stripe_redirect.jinja2
-
15tildes/tildes/templates/donate_success.jinja2
-
115tildes/tildes/views/donate.py
@ -0,0 +1,10 @@ |
|||||
|
// Copyright (c) 2019 Tildes contributors <code@tildes.net>
|
||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
|
||||
|
$.onmount("[data-js-stripe-checkout]", function() { |
||||
|
/* eslint-disable-next-line no-undef */ |
||||
|
var stripe = Stripe($(this).attr("data-js-stripe-checkout")); |
||||
|
stripe.redirectToCheckout({ |
||||
|
sessionId: $(this).attr("data-js-stripe-checkout-session") |
||||
|
}); |
||||
|
}); |
@ -0,0 +1,28 @@ |
|||||
|
// Copyright (c) 2019 Tildes contributors <code@tildes.net>
|
||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
|
||||
|
$.onmount("[data-js-stripe-donate-form]", function() { |
||||
|
$(this).on("submit", function(event) { |
||||
|
var $amountInput = $(this).find("#amount"); |
||||
|
var amount = $amountInput.val(); |
||||
|
|
||||
|
var $errorDiv = $(this).find(".text-status-message"); |
||||
|
|
||||
|
// remove dollar sign and/or comma, then parse into float
|
||||
|
amount = amount.replace(/[$,]/g, ""); |
||||
|
amount = parseFloat(amount); |
||||
|
|
||||
|
if (isNaN(amount)) { |
||||
|
$errorDiv.text("Please enter a valid dollar amount."); |
||||
|
event.preventDefault(); |
||||
|
return; |
||||
|
} else if (amount < 1.0) { |
||||
|
$errorDiv.text("Donation amount must be at least $1."); |
||||
|
event.preventDefault(); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// set the value in case any of the replacements happened
|
||||
|
$amountInput.val(amount); |
||||
|
}); |
||||
|
}); |
@ -0,0 +1,54 @@ |
|||||
|
{# Copyright (c) 2019 Tildes contributors <code@tildes.net> #} |
||||
|
{# SPDX-License-Identifier: AGPL-3.0-or-later #} |
||||
|
|
||||
|
{% extends 'base_no_sidebar.jinja2' %} |
||||
|
|
||||
|
{% block title %}Donate to Tildes{% endblock %} |
||||
|
|
||||
|
{% block main_heading %}Credit card donation (via Stripe){% endblock %} |
||||
|
|
||||
|
{% block content %} |
||||
|
<p>After submitting this form, you will be redirected to the Stripe site to enter your payment info.</p> |
||||
|
|
||||
|
<p>Note that the donation is in Canadian Dollars (CAD) by default, but you can switch to USD if you prefer. At the moment, $10 CAD is about $7.50 USD.</p> |
||||
|
|
||||
|
<div class="divider"></div> |
||||
|
|
||||
|
<form class="form-narrow" method="post" data-js-stripe-donate-form> |
||||
|
<input type="hidden" name="csrf_token" value="{{ get_csrf_token() }}"> |
||||
|
|
||||
|
<div class="form-group"> |
||||
|
<label class="form-label">Donation type</label> |
||||
|
<label class="form-radio"> |
||||
|
<input type="radio" name="interval" value="onetime" checked> |
||||
|
<i class="form-icon"></i> One time |
||||
|
</label> |
||||
|
<label class="form-radio"> |
||||
|
<input type="radio" name="interval" value="month"> |
||||
|
<i class="form-icon"></i> Monthly |
||||
|
</label> |
||||
|
<label class="form-radio"> |
||||
|
<input type="radio" name="interval" value="year"> |
||||
|
<i class="form-icon"></i> Yearly |
||||
|
</label> |
||||
|
</div> |
||||
|
|
||||
|
<div class="form-group"> |
||||
|
<label class="form-label" for="amount">Donation amount (must be at least $1)</label> |
||||
|
|
||||
|
<div class="input-group"> |
||||
|
<span class="input-group-addon">$</span> |
||||
|
<input class="form-input" id="amount" name="amount" type="text" data-js-auto-focus> |
||||
|
<select class="form-select" id="currency" name="currency"> |
||||
|
<option value="CAD" selected>CAD</option> |
||||
|
<option value="USD">USD</option> |
||||
|
</select> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="form-buttons"> |
||||
|
<button class="btn btn-primary" type="submit">Donate</button> |
||||
|
<div class="text-status-message text-error"></div> |
||||
|
</div> |
||||
|
</form> |
||||
|
{% endblock %} |
@ -0,0 +1,18 @@ |
|||||
|
{# Copyright (c) 2018 Tildes contributors <code@tildes.net> #} |
||||
|
{# SPDX-License-Identifier: AGPL-3.0-or-later #} |
||||
|
|
||||
|
{% extends 'base_no_sidebar.jinja2' %} |
||||
|
|
||||
|
{% block title %}Stripe donation{% endblock %} |
||||
|
|
||||
|
{% block content %} |
||||
|
<script src="https://js.stripe.com/v3/"></script> |
||||
|
|
||||
|
<p>Redirecting to Stripe...</p> |
||||
|
|
||||
|
{# This div will cause the page to redirect to the Stripe Checkout page #} |
||||
|
<div |
||||
|
data-js-stripe-checkout="{{ publishable_key }}" |
||||
|
data-js-stripe-checkout-session="{{ session_id }}" |
||||
|
></div> |
||||
|
{% endblock %} |
@ -0,0 +1,15 @@ |
|||||
|
{# Copyright (c) 2019 Tildes contributors <code@tildes.net> #} |
||||
|
{# SPDX-License-Identifier: AGPL-3.0-or-later #} |
||||
|
|
||||
|
{% extends 'base_no_sidebar.jinja2' %} |
||||
|
|
||||
|
{% block title %}Thanks for donating!{% endblock %} |
||||
|
|
||||
|
{% block content %} |
||||
|
<div class="empty"> |
||||
|
<h2 class="empty-title">Thanks for donating to Tildes!</h2> |
||||
|
<p class="empty-subtitle">You should receive an email receipt. If you have any questions, please feel free to contact <a href="mailto:donate@tildes.net">donate@tildes.net</a></p> |
||||
|
|
||||
|
<div class="empty-action"><a href="/" class="btn btn-primary">Back to the home page</a></div> |
||||
|
</div> |
||||
|
{% endblock %} |
@ -0,0 +1,115 @@ |
|||||
|
# Copyright (c) 2018 Tildes contributors <code@tildes.net> |
||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later |
||||
|
|
||||
|
"""The view for donating via Stripe.""" |
||||
|
|
||||
|
import stripe |
||||
|
from marshmallow.fields import Float, String |
||||
|
from marshmallow.validate import OneOf, Range |
||||
|
from pyramid.httpexceptions import HTTPInternalServerError |
||||
|
from pyramid.request import Request |
||||
|
from pyramid.security import NO_PERMISSION_REQUIRED |
||||
|
from pyramid.view import view_config |
||||
|
|
||||
|
from tildes.metrics import incr_counter |
||||
|
from tildes.views.decorators import rate_limit_view, use_kwargs |
||||
|
|
||||
|
|
||||
|
@view_config( |
||||
|
route_name="donate_stripe", |
||||
|
request_method="GET", |
||||
|
renderer="donate_stripe.jinja2", |
||||
|
permission=NO_PERMISSION_REQUIRED, |
||||
|
) |
||||
|
def get_donate_stripe(request: Request) -> dict: |
||||
|
"""Display the form for donating with Stripe.""" |
||||
|
# pylint: disable=unused-argument |
||||
|
return {} |
||||
|
|
||||
|
|
||||
|
@view_config( |
||||
|
route_name="donate_stripe", |
||||
|
request_method="POST", |
||||
|
renderer="donate_stripe_redirect.jinja2", |
||||
|
permission=NO_PERMISSION_REQUIRED, |
||||
|
) |
||||
|
@use_kwargs( |
||||
|
{ |
||||
|
"amount": Float(required=True, validate=Range(min=1.0)), |
||||
|
"currency": String(required=True, validate=OneOf(("CAD", "USD"))), |
||||
|
"interval": String(required=True, validate=OneOf(("onetime", "month", "year"))), |
||||
|
}, |
||||
|
location="form", |
||||
|
) |
||||
|
@rate_limit_view("global_donate_stripe") |
||||
|
@rate_limit_view("donate_stripe") |
||||
|
def post_donate_stripe( |
||||
|
request: Request, amount: int, currency: str, interval: str |
||||
|
) -> dict: |
||||
|
"""Process a Stripe donation.""" |
||||
|
try: |
||||
|
stripe.api_key = request.registry.settings["api_keys.stripe.secret"] |
||||
|
publishable_key = request.registry.settings["api_keys.stripe.publishable"] |
||||
|
product_id = request.registry.settings["stripe.recurring_donation_product_id"] |
||||
|
except KeyError as exc: |
||||
|
raise HTTPInternalServerError from exc |
||||
|
|
||||
|
incr_counter("donation_initiations", type="stripe") |
||||
|
|
||||
|
if interval == "onetime": |
||||
|
session = stripe.checkout.Session.create( |
||||
|
payment_method_types=["card"], |
||||
|
line_items=[ |
||||
|
{ |
||||
|
"name": "One-time donation - tildes.net", |
||||
|
"amount": int(amount * 100), |
||||
|
"currency": currency, |
||||
|
"quantity": 1, |
||||
|
} |
||||
|
], |
||||
|
submit_type="donate", |
||||
|
success_url="https://tildes.net/donate_success", |
||||
|
cancel_url="https://docs.tildes.net/donate", |
||||
|
) |
||||
|
else: |
||||
|
product = stripe.Product.retrieve(product_id) |
||||
|
existing_plans = stripe.Plan.list(product=product, active=True, limit=100) |
||||
|
|
||||
|
# look through existing plans to see if there's already a matching one, or |
||||
|
# create a new plan if not |
||||
|
for existing_plan in existing_plans: |
||||
|
if ( |
||||
|
existing_plan.amount == int(amount * 100) |
||||
|
and existing_plan.currency == currency.lower() |
||||
|
and existing_plan.interval == interval |
||||
|
): |
||||
|
plan = existing_plan |
||||
|
break |
||||
|
else: |
||||
|
plan = stripe.Plan.create( |
||||
|
amount=int(amount * 100), |
||||
|
currency=currency, |
||||
|
interval=interval, |
||||
|
product=product, |
||||
|
) |
||||
|
|
||||
|
session = stripe.checkout.Session.create( |
||||
|
payment_method_types=["card"], |
||||
|
subscription_data={"items": [{"plan": plan.id}]}, |
||||
|
success_url="https://tildes.net/donate_success", |
||||
|
cancel_url="https://docs.tildes.net/donate", |
||||
|
) |
||||
|
|
||||
|
return {"publishable_key": publishable_key, "session_id": session.id} |
||||
|
|
||||
|
|
||||
|
@view_config(route_name="donate_success", renderer="donate_success.jinja2") |
||||
|
def get_donate_success(request: Request) -> dict: |
||||
|
"""Display a message after a successful donation.""" |
||||
|
# pylint: disable=unused-argument |
||||
|
|
||||
|
# incrementing this metric on page-load and hard-coding Stripe isn't ideal, but it |
||||
|
# should do the job for now |
||||
|
incr_counter("donations", type="stripe") |
||||
|
|
||||
|
return {} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue