Browse Source

Add recurring Stripe donations

This uses Stripe's Subscriptions capability to set up recurring
donations. Requires setting up a Product for the recurring donation, and
defining its product ID in the INI file.
merge-requests/85/head
Deimos 5 years ago
parent
commit
6df3227d63
  1. 2
      tildes/production.ini.example
  2. 10
      tildes/scss/modules/_form.scss
  3. 16
      tildes/tildes/templates/donate_stripe.jinja2
  4. 35
      tildes/tildes/views/donate.py

2
tildes/production.ini.example

@ -23,6 +23,8 @@ redis.sessions.timeout = 3600
sqlalchemy.url = postgresql+psycopg2://tildes:@:6432/tildes sqlalchemy.url = postgresql+psycopg2://tildes:@:6432/tildes
stripe.recurring_donation_product_id = prod_ProductID
tildes.default_user_comment_label_weight = 1.0 tildes.default_user_comment_label_weight = 1.0
tildes.welcome_message_sender = Deimos tildes.welcome_message_sender = Deimos

10
tildes/scss/modules/_form.scss

@ -1,6 +1,12 @@
// Copyright (c) 2018 Tildes contributors <code@tildes.net> // Copyright (c) 2018 Tildes contributors <code@tildes.net>
// SPDX-License-Identifier: AGPL-3.0-or-later // SPDX-License-Identifier: AGPL-3.0-or-later
.form-group {
.form-radio {
margin-left: 1rem;
}
}
.form-narrow { .form-narrow {
max-width: 20rem; max-width: 20rem;
@ -97,6 +103,10 @@ textarea.form-input {
} }
} }
.form-radio {
display: block;
}
.form-search .form-input { .form-search .form-input {
margin-right: 0.4rem; margin-right: 0.4rem;
} }

16
tildes/tildes/templates/donate_stripe.jinja2

@ -17,6 +17,22 @@
<form class="form-narrow" method="post" data-js-stripe-donate-form> <form class="form-narrow" method="post" data-js-stripe-donate-form>
<input type="hidden" name="csrf_token" value="{{ get_csrf_token() }}"> <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"> <div class="form-group">
<label class="form-label" for="amount">Donation amount (must be at least $1)</label> <label class="form-label" for="amount">Donation amount (must be at least $1)</label>

35
tildes/tildes/views/donate.py

@ -37,18 +37,23 @@ def get_donate_stripe(request: Request) -> dict:
{ {
"amount": Float(required=True, validate=Range(min=1.0)), "amount": Float(required=True, validate=Range(min=1.0)),
"currency": String(required=True, validate=OneOf(("CAD", "USD"))), "currency": String(required=True, validate=OneOf(("CAD", "USD"))),
"interval": String(required=True, validate=OneOf(("onetime", "month", "year"))),
} }
) )
def post_donate_stripe(request: Request, amount: int, currency: str) -> dict:
def post_donate_stripe(
request: Request, amount: int, currency: str, interval: str
) -> dict:
"""Process a Stripe donation.""" """Process a Stripe donation."""
try: try:
stripe.api_key = request.registry.settings["api_keys.stripe.secret"] stripe.api_key = request.registry.settings["api_keys.stripe.secret"]
publishable_key = request.registry.settings["api_keys.stripe.publishable"] publishable_key = request.registry.settings["api_keys.stripe.publishable"]
product_id = request.registry.settings["stripe.recurring_donation_product_id"]
except KeyError: except KeyError:
raise HTTPInternalServerError raise HTTPInternalServerError
incr_counter("donation_initiations", type="stripe") incr_counter("donation_initiations", type="stripe")
if interval == "onetime":
session = stripe.checkout.Session.create( session = stripe.checkout.Session.create(
payment_method_types=["card"], payment_method_types=["card"],
line_items=[ line_items=[
@ -63,6 +68,34 @@ def post_donate_stripe(request: Request, amount: int, currency: str) -> dict:
success_url="https://tildes.net/donate_success", success_url="https://tildes.net/donate_success",
cancel_url="https://docs.tildes.net/donate", 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} return {"publishable_key": publishable_key, "session_id": session.id}

Loading…
Cancel
Save