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
2 years 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