Fix #1749 -- Stripe: Rewrite for Payment Methods and Payment Intents (#2494)

Co-authored-by: Raphael Michel <michel@rami.io>
Co-authored-by: Raphael Michel <mail@raphaelmichel.de>
This commit is contained in:
Martin Gross
2023-07-21 13:19:24 +02:00
committed by GitHub
parent 19e1d132c2
commit b134f29cf6
12 changed files with 585 additions and 137 deletions

View File

@@ -0,0 +1,22 @@
# Generated by Django 3.2.2 on 2021-12-08 14:07
from django.core.cache import cache
from django.db import migrations
def cleanup(app, schema_editor):
EventSettingsStore = app.get_model('pretixbase', 'Event_SettingsStore')
for setting in EventSettingsStore.objects.filter(key='payment_stripe_method_cc'):
setting.key = 'payment_stripe_method_card'
cache.delete('hierarkey_{}_{}'.format('event', setting.object_id))
setting.save()
class Migration(migrations.Migration):
dependencies = [
('stripe', '0003_registeredapplepaydomain'),
]
operations = [
migrations.RunPython(cleanup, migrations.RunPython.noop)
]

View File

@@ -58,7 +58,10 @@ from django_countries import countries
from pretix import __version__
from pretix.base.decimal import round_decimal
from pretix.base.forms import SecretKeySettingsField
from pretix.base.models import Event, OrderPayment, OrderRefund, Quota
from pretix.base.forms.questions import guess_country
from pretix.base.models import (
Event, InvoiceAddress, Order, OrderPayment, OrderRefund, Quota,
)
from pretix.base.payment import (
BasePaymentProvider, PaymentException, WalletQueries,
)
@@ -66,6 +69,8 @@ from pretix.base.plugins import get_all_plugins
from pretix.base.services.mail import SendMailException
from pretix.base.settings import SettingsSandbox
from pretix.helpers import OF_SELF
from pretix.helpers.countries import CachedCountries
from pretix.helpers.http import get_client_ip
from pretix.helpers.urls import build_absolute_uri as build_global_uri
from pretix.multidomain.urlreverse import build_absolute_uri, eventreverse
from pretix.plugins.stripe.forms import StripeKeyValidator
@@ -75,9 +80,73 @@ from pretix.plugins.stripe.models import (
from pretix.plugins.stripe.tasks import (
get_stripe_account_key, stripe_verify_domain,
)
from pretix.presale.views.cart import cart_session
logger = logging.getLogger('pretix.plugins.stripe')
# State of the payment methods
#
# Source: https://stripe.com/docs/payments/payment-methods/overview
# Last Update: 2023-04-24
#
# Cards
# - Credit and Debit Cards: ✓
# - Apple, Google Pay: ✓
#
# Bank debits
# - ACH Debit: ✗
# - Canadian PADs: ✗
# - BACS Direct Debit: ✗
# - SEPA Direct Debit: ✓
# - BECS Direct Debit: ✗
#
# Bank redirects
# - Bancontact: ✓
# - BLIK: ✗
# - EPS: ✓
# - giropay: ✓
# - iDEAL: ✓
# - P24: ✓
# - Sofort: ✓
# - FPX: ✗
# - PayNow: ✗
# - UPI: ✗
# - Netbanking: ✗
#
# Bank transfers
# - ACH Bank Transfer: ✗
# - SEPA Bank Transfer: ✗
# - UK Bank Transfer: ✗
# - Multibanco: ✗
# - Furikomi (Japan): ✗
# - Mexico Bank Transfer: ✗
#
# Buy now, pay later
# - Affirm: ✗
# - Afterpay/Clearpay: ✗
# - Klarna: ✗
#
# Real-time payments
# - PayNow: ✗
# - PromptPay: ✗
# - Pix: ✗
#
# Vouchers
# - Konbini: ✗
# - OXXO: ✗
# - Boleto: ✗
#
# Wallets
# - Apple Pay: ✓ (Cards)
# - Google Pay: ✓ (Cards)
# - Secure Remote Commerce: ✗
# - Link: ✓ (PaymentRequestButton)
# - Cash App Pay: ✗
# - MobilePay: ✗
# - Alipay: ✓
# - WeChat Pay: ✓
# - GrabPay: ✓
class StripeSettingsHolder(BasePaymentProvider):
identifier = 'stripe_settings'
@@ -250,7 +319,7 @@ class StripeSettingsHolder(BasePaymentProvider):
d = OrderedDict(
fields + [
('method_cc',
('method_card',
forms.BooleanField(
label=_('Credit card payments'),
required=False,
@@ -283,6 +352,32 @@ class StripeSettingsHolder(BasePaymentProvider):
help_text=_('Needs to be enabled in your Stripe account first.'),
required=False,
)),
('method_sepa_debit',
forms.BooleanField(
label=_('SEPA Direct Debit'),
disabled=self.event.currency != 'EUR',
help_text=(
_('Needs to be enabled in your Stripe account first.') +
'<div class="alert alert-warning">%s</div>' % _(
'SEPA Direct Debit payments via Stripe are <strong>not</strong> processed '
'instantly but might take up to <strong>14 days</strong> to be confirmed in some cases. '
'Please only activate this payment method if your payment term allows for this lag.'
)),
required=False,
)),
('sepa_creditor_name',
forms.CharField(
label=_('SEPA Creditor Mandate Name'),
disabled=self.event.currency != 'EUR',
help_text=_('Please provide your SEPA Creditor Mandate Name, that will be displayed to the user.'),
required=False,
widget=forms.TextInput(
attrs={
'data-display-dependency': '#id_payment_stripe_method_sepa_debit',
'data-required-if': '#id_payment_stripe_method_sepa_debit'
}
),
)),
('method_sofort',
forms.BooleanField(
label=_('SOFORT'),
@@ -757,44 +852,17 @@ class StripeMethod(BasePaymentProvider):
le.save(update_fields=['data', 'shredded'])
class StripeCC(StripeMethod):
identifier = 'stripe'
verbose_name = _('Credit card via Stripe')
public_name = _('Credit card')
method = 'cc'
@property
def walletqueries(self):
# ToDo: Check against Stripe API, if ApplePay and GooglePay are even activated/available
# This is probably only really feasable once the Payment Methods Configuration API is out of beta
# https://stripe.com/docs/connect/payment-method-configurations
if self.settings.get("walletdetection", True, as_type=bool):
return [WalletQueries.APPLEPAY, WalletQueries.GOOGLEPAY]
return []
def payment_form_render(self, request, total) -> str:
account = get_stripe_account_key(self)
if not RegisteredApplePayDomain.objects.filter(account=account, domain=request.host).exists():
stripe_verify_domain.apply_async(args=(self.event.pk, request.host))
template = get_template('pretixplugins/stripe/checkout_payment_form_cc.html')
ctx = {
'request': request,
'event': self.event,
'total': self._decimal_to_int(total),
'settings': self.settings,
'is_moto': self.is_moto(request)
}
return template.render(ctx)
class StripePaymentIntentMethod(StripeMethod):
identifier = ''
method = ''
def payment_is_valid_session(self, request):
return request.session.get('payment_stripe_payment_method_id', '') != ''
return request.session.get('payment_stripe_{}_payment_method_id'.format(self.method), '') != ''
def checkout_prepare(self, request, cart):
payment_method_id = request.POST.get('stripe_payment_method_id', '')
request.session['payment_stripe_payment_method_id'] = payment_method_id
request.session['payment_stripe_brand'] = request.POST.get('stripe_card_brand', '')
request.session['payment_stripe_last4'] = request.POST.get('stripe_card_last4', '')
payment_method_id = request.POST.get('stripe_{}_payment_method_id'.format(self.method), '')
request.session['payment_stripe_{}_payment_method_id'.format(self.method)] = payment_method_id
if payment_method_id == '':
messages.warning(request, _('You may need to enable JavaScript for Stripe payments.'))
return False
@@ -804,21 +872,13 @@ class StripeCC(StripeMethod):
try:
return self._handle_payment_intent(request, payment)
finally:
del request.session['payment_stripe_payment_method_id']
del request.session['payment_stripe_{}_payment_method_id'.format(self.method)]
def is_moto(self, request, payment=None) -> bool:
# We don't have a payment yet when checking if we should display the MOTO-flag
# However, before we execute the payment, we absolutely have to check if the request-SalesChannel as well as the
# order are tagged as a reseller-transaction. Else, a user with a valid reseller-session might be able to place
# a MOTO transaction trough the WebShop.
return False
moto = self.settings.get('reseller_moto', False, as_type=bool) and \
request.sales_channel.identifier == 'resellers'
if payment:
return moto and payment.order.sales_channel == 'resellers'
return moto
def _payment_intent_kwargs(self, request, payment):
return {}
def _handle_payment_intent(self, request, payment, intent=None):
self._init_api()
@@ -828,6 +888,7 @@ class StripeCC(StripeMethod):
params = {}
params.update(self._connect_kwargs(payment))
params.update(self.api_kwargs)
params.update(self._payment_intent_kwargs(request, payment))
if self.is_moto(request, payment):
params.update({
@@ -841,7 +902,8 @@ class StripeCC(StripeMethod):
intent = stripe.PaymentIntent.create(
amount=self._get_amount(payment),
currency=self.event.currency.lower(),
payment_method=request.session['payment_stripe_payment_method_id'],
payment_method=request.session['payment_stripe_{}_payment_method_id'.format(self.method)],
payment_method_types=[self.method],
confirmation_method='manual',
confirm=True,
description='{event}-{code}'.format(
@@ -855,7 +917,7 @@ class StripeCC(StripeMethod):
'code': payment.order.code
},
# TODO: Is this sufficient?
idempotency_key=str(self.event.id) + payment.order.code + request.session['payment_stripe_payment_method_id'],
idempotency_key=str(self.event.id) + payment.order.code + request.session['payment_stripe_{}_payment_method_id'.format(self.method)],
return_url=build_absolute_uri(self.event, 'plugins:stripe:sca.return', kwargs={
'order': payment.order.code,
'payment': payment.pk,
@@ -1002,6 +1064,78 @@ class StripeCC(StripeMethod):
raise PaymentException(_('We had trouble communicating with Stripe. Please try again and get in touch '
'with us if this problem persists.'))
class StripeCC(StripePaymentIntentMethod):
identifier = 'stripe'
verbose_name = _('Credit card via Stripe')
public_name = _('Credit card')
method = 'card'
@property
def walletqueries(self):
# ToDo: Check against Stripe API, if ApplePay and GooglePay are even activated/available
# This is probably only really feasable once the Payment Methods Configuration API is out of beta
# https://stripe.com/docs/connect/payment-method-configurations
if self.settings.get("walletdetection", True, as_type=bool):
return [WalletQueries.APPLEPAY, WalletQueries.GOOGLEPAY]
return []
def payment_form_render(self, request, total, order=None) -> str:
account = get_stripe_account_key(self)
if not RegisteredApplePayDomain.objects.filter(account=account, domain=request.host).exists():
stripe_verify_domain.apply_async(args=(self.event.pk, request.host))
template = get_template('pretixplugins/stripe/checkout_payment_form_card.html')
ctx = {
'request': request,
'event': self.event,
'total': self._decimal_to_int(total),
'settings': self.settings,
'is_moto': self.is_moto(request)
}
return template.render(ctx)
def _migrate_session(self, request):
# todo: remove after pretix 2023.8 was released
keymap = {
'payment_stripe_payment_method_id': 'payment_stripe_card_payment_method_id',
'payment_stripe_brand': 'payment_stripe_card_brand',
'payment_stripe_last4': 'payment_stripe_card_last4',
}
for old, new in keymap.items():
if old in request.session:
request.session[new] = request.session[old]
del request.session[old]
def checkout_prepare(self, request, cart):
self._migrate_session(request)
request.session['payment_stripe_card_brand'] = request.POST.get('stripe_card_brand', '')
request.session['payment_stripe_card_last4'] = request.POST.get('stripe_card_last4', '')
return super().checkout_prepare(request, cart)
def payment_is_valid_session(self, request):
self._migrate_session(request)
return super().payment_is_valid_session(request)
def _handle_payment_intent(self, request, payment, intent=None):
self._migrate_session(request)
return super()._handle_payment_intent(request, payment, intent)
def is_moto(self, request, payment=None) -> bool:
# We don't have a payment yet when checking if we should display the MOTO-flag
# However, before we execute the payment, we absolutely have to check if the request-SalesChannel as well as the
# order are tagged as a reseller-transaction. Else, a user with a valid reseller-session might be able to place
# a MOTO transaction trough the WebShop.
moto = self.settings.get('reseller_moto', False, as_type=bool) and \
request.sales_channel.identifier == 'resellers'
if payment:
return moto and payment.order.sales_channel == 'resellers'
return moto
def payment_presale_render(self, payment: OrderPayment) -> str:
pi = payment.info_data or {}
try:
@@ -1018,6 +1152,131 @@ class StripeCC(StripeMethod):
f'{_("expires {month}/{year}").format(month=card.get("exp_month"), year=card.get("exp_year"))}'
class StripeSEPADirectDebit(StripePaymentIntentMethod):
identifier = 'stripe_sepa_debit'
verbose_name = _('SEPA Debit via Stripe')
public_name = _('SEPA Debit')
method = 'sepa_debit'
ia = InvoiceAddress()
def payment_form_render(self, request: HttpRequest, total: Decimal, order: Order=None) -> str:
def get_invoice_address():
if order and getattr(order, 'invoice_address', None):
request._checkout_flow_invoice_address = order.invoice_address
if not hasattr(request, '_checkout_flow_invoice_address'):
cs = cart_session(request)
iapk = cs.get('invoice_address')
if not iapk:
request._checkout_flow_invoice_address = InvoiceAddress()
else:
try:
request._checkout_flow_invoice_address = InvoiceAddress.objects.get(pk=iapk, order__isnull=True)
except InvoiceAddress.DoesNotExist:
request._checkout_flow_invoice_address = InvoiceAddress()
return request._checkout_flow_invoice_address
cs = cart_session(request)
self.ia = get_invoice_address()
template = get_template('pretixplugins/stripe/checkout_payment_form_sepadirectdebit.html')
ctx = {
'request': request,
'event': self.event,
'settings': self.settings,
'form': self.payment_form(request),
'email': order.email if order else cs.get('email', '')
}
return template.render(ctx)
@property
def payment_form_fields(self):
return OrderedDict(
[
('accountname',
forms.CharField(
label=_('Account Holder Name'),
initial=self.ia.name,
)),
('line1',
forms.CharField(
label=_('Account Holder Street'),
required=False,
widget=forms.TextInput(
attrs={
'data-display-dependency': '#stripe_sepa_debit_country',
'data-required-if': '#stripe_sepa_debit_country'
}
),
initial=self.ia.street,
)),
('postal_code',
forms.CharField(
label=_('Account Holder Postal Code'),
required=False,
widget=forms.TextInput(
attrs={
'data-display-dependency': '#stripe_sepa_debit_country',
'data-required-if': '#stripe_sepa_debit_country'
}
),
initial=self.ia.zipcode,
)),
('city',
forms.CharField(
label=_('Account Holder City'),
required=False,
widget=forms.TextInput(
attrs={
'data-display-dependency': '#stripe_sepa_debit_country',
'data-required-if': '#stripe_sepa_debit_country'
}
),
initial=self.ia.city,
)),
('country',
forms.ChoiceField(
label=_('Account Holder Country'),
required=False,
choices=CachedCountries(),
widget=forms.Select(
attrs={
'data-display-dependency': '#stripe_sepa_debit_country',
'data-required-if': '#stripe_sepa_debit_country'
}
),
initial=self.ia.country or guess_country(self.event),
)),
])
def _payment_intent_kwargs(self, request, payment):
return {
'mandate_data': {
'customer_acceptance': {
'type': 'online',
'online': {
'ip_address': get_client_ip(request),
'user_agent': request.META['HTTP_USER_AGENT'],
}
},
}
}
def checkout_prepare(self, request, cart):
request.session['payment_stripe_sepa_debit_last4'] = request.POST.get('stripe_sepa_debit_last4', '')
request.session['payment_stripe_sepa_debit_bank'] = request.POST.get('stripe_sepa_debit_bank', '')
return super().checkout_prepare(request, cart)
def execute_payment(self, request: HttpRequest, payment: OrderPayment):
try:
super().execute_payment(request, payment)
finally:
fields = ['accountname', 'line1', 'postal_code', 'city', 'country']
for field in fields:
if 'payment_stripe_sepa_debit_{}'.format(field) in request.session:
del request.session['payment_stripe_sepa_debit_{}'.format(field)]
class StripeGiropay(StripeMethod):
identifier = 'stripe_giropay'
verbose_name = _('giropay via Stripe')

View File

@@ -46,13 +46,14 @@ from pretix.presale.signals import html_head, process_response
def register_payment_provider(sender, **kwargs):
from .payment import (
StripeAlipay, StripeBancontact, StripeCC, StripeEPS, StripeGiropay,
StripeIdeal, StripeMultibanco, StripePrzelewy24, StripeSettingsHolder,
StripeSofort, StripeWeChatPay,
StripeIdeal, StripeMultibanco, StripePrzelewy24, StripeSEPADirectDebit,
StripeSettingsHolder, StripeSofort, StripeWeChatPay,
)
return [
StripeSettingsHolder, StripeCC, StripeGiropay, StripeIdeal, StripeAlipay, StripeBancontact,
StripeSofort, StripeEPS, StripeMultibanco, StripePrzelewy24, StripeWeChatPay
StripeSofort, StripeEPS, StripeMultibanco, StripePrzelewy24, StripeWeChatPay,
StripeSEPADirectDebit,
]
@@ -110,7 +111,7 @@ def pretixcontrol_logentry_display(sender, logentry, **kwargs):
return _('Stripe reported an event: {}').format(text)
settings_hierarkey.add_default('payment_stripe_method_cc', True, bool)
settings_hierarkey.add_default('payment_stripe_method_card', True, bool)
settings_hierarkey.add_default('payment_stripe_reseller_moto', False, bool)

View File

@@ -39,39 +39,39 @@
.sepText {
left: 50%;
}
#stripe-elements > div.hidden {
#stripe-card-elements > div.hidden {
height: 0;
padding-top: 0;
padding-bottom: 0;
overflow: hidden;
display: block !important;
}
#stripe-elements .stripe-or {
#stripe-card-elements .stripe-or {
height: 16px;
}
#stripe-elements .stripe-payment-request-button {
#stripe-card-elements .stripe-payment-request-button {
height: 40px;
}
#stripe-elements > div {
#stripe-card-elements > div {
transition: height 0.3s ease-out, padding-top 0.3s ease-out, padding-bottom 0.3s ease-out;
}
}
@media only screen and (min-width: 999px) {
#stripe-elements {
#stripe-card-elements {
display: flex;
flex-wrap: wrap;
}
.stripe-card-holder {
flex-grow: 1;
}
#stripe-elements > div.hidden {
#stripe-card-elements > div.hidden {
width: 0;
padding: 0;
overflow: hidden;
display: block !important;
}
#stripe-elements > div {
#stripe-card-elements > div {
transition: width 0.3s ease-out, padding-left 0.3s ease-out, padding-right 0.3s ease-out;
}
}

View File

@@ -5,26 +5,31 @@ var pretixstripe = {
stripe: null,
elements: null,
card: null,
sepa: null,
paymentRequest: null,
paymentRequestButton: null,
'cc_request': function () {
'pm_request': function (method, element, kwargs = {}) {
waitingDialog.show(gettext("Contacting Stripe …"));
$(".stripe-errors").hide();
// ToDo: 'card' --> proper type of payment method
pretixstripe.stripe.createPaymentMethod('card', pretixstripe.card).then(function (result) {
pretixstripe.stripe.createPaymentMethod(method, element, kwargs).then(function (result) {
waitingDialog.hide();
if (result.error) {
$(".stripe-errors").stop().hide().removeClass("sr-only");
$(".stripe-errors").html("<div class='alert alert-danger'>" + result.error.message + "</div>");
$(".stripe-errors").slideDown();
} else {
var $form = $("#stripe_payment_method_id").closest("form");
var $form = $("#stripe_" + method + "_payment_method_id").closest("form");
// Insert the token into the form so it gets submitted to the server
$("#stripe_payment_method_id").val(result.paymentMethod.id);
$("#stripe_card_brand").val(result.paymentMethod.card.brand);
$("#stripe_card_last4").val(result.paymentMethod.card.last4);
$("#stripe_" + method + "_payment_method_id").val(result.paymentMethod.id);
if (method === 'card') {
$("#stripe_card_brand").val(result.paymentMethod.card.brand);
$("#stripe_card_last4").val(result.paymentMethod.card.last4);
}
if (method === 'sepa_debit') {
$("#stripe_sepa_debit_last4").val(result.paymentMethod.sepa_debit.last4);
}
// and submit
$form.get(0).submit();
}
@@ -36,10 +41,10 @@ var pretixstripe = {
});
},
'load': function () {
if (pretixstripe.stripe !== null) {
return;
}
$('.stripe-container').closest("form").find(".checkout-button-row .btn-primary").prop("disabled", true);
if (pretixstripe.stripe !== null) {
return;
}
$('.stripe-container').closest("form").find(".checkout-button-row .btn-primary").prop("disabled", true);
$.ajax(
{
url: 'https://js.stripe.com/v3/',
@@ -60,10 +65,10 @@ var pretixstripe = {
try {
pretixstripe.paymentRequest = pretixstripe.stripe.paymentRequest({
country: $("#stripe_merchantcountry").html(),
currency: $("#stripe_currency").val().toLowerCase(),
currency: $("#stripe_card_currency").val().toLowerCase(),
total: {
label: gettext('Total'),
amount: parseInt($("#stripe_total").val())
amount: parseInt($("#stripe_card_total").val())
},
displayItems: [],
requestPayerName: false,
@@ -75,9 +80,9 @@ var pretixstripe = {
pretixstripe.paymentRequest.on('paymentmethod', function (ev) {
ev.complete('success');
var $form = $("#stripe_payment_method_id").closest("form");
var $form = $("#stripe_card_payment_method_id").closest("form");
// Insert the token into the form so it gets submitted to the server
$("#stripe_payment_method_id").val(ev.paymentMethod.id);
$("#stripe_card_payment_method_id").val(ev.paymentMethod.id);
$("#stripe_card_brand").val(ev.paymentMethod.card.brand);
$("#stripe_card_last4").val(ev.paymentMethod.card.last4);
// and submit
@@ -112,25 +117,67 @@ var pretixstripe = {
}
});
pretixstripe.card.mount("#stripe-card");
pretixstripe.card.on('ready', function () {
$('.stripe-container').closest("form").find(".checkout-button-row .btn-primary").prop("disabled", false);
});
}
if ($("#stripe-sepa").length) {
pretixstripe.sepa = pretixstripe.elements.create('iban', {
'style': {
'base': {
'fontFamily': '"Open Sans","OpenSans","Helvetica Neue",Helvetica,Arial,sans-serif',
'fontSize': '14px',
'color': '#555555',
'lineHeight': '1.42857',
'border': '1px solid #ccc',
'::placeholder': {
color: 'rgba(0,0,0,0.4)',
},
},
'invalid': {
'color': 'red',
},
},
supportedCountries: ['SEPA'],
classes: {
focus: 'is-focused',
invalid: 'has-error',
}
});
pretixstripe.sepa.on('change', function (event) {
// List of IBAN-countries, that require the country as well as line1-property according to
// https://stripe.com/docs/payments/sepa-debit/accept-a-payment?platform=web&ui=element#web-submit-payment
if (['AD', 'PF', 'TF', 'GI', 'GB', 'GG', 'VA', 'IM', 'JE', 'MC', 'NC', 'BL', 'PM', 'SM', 'CH', 'WF'].indexOf(event.country) > 0) {
$("#stripe_sepa_debit_country").prop('checked', true);
$("#stripe_sepa_debit_country").change();
} else {
$("#stripe_sepa_debit_country").prop('checked', false);
$("#stripe_sepa_debit_country").change();
}
if (event.bankName) {
$("#stripe_sepa_debit_bank").val(event.bankName);
}
});
pretixstripe.sepa.mount("#stripe-sepa");
pretixstripe.sepa.on('ready', function () {
$('.stripe-container').closest("form").find(".checkout-button-row .btn-primary").prop("disabled", false);
});
}
pretixstripe.card.on('ready', function () {
$('.stripe-container').closest("form").find(".checkout-button-row .btn-primary").prop("disabled", false);
});
if ($("#stripe-payment-request-button").length && pretixstripe.paymentRequest != null) {
pretixstripe.paymentRequestButton = pretixstripe.elements.create('paymentRequestButton', {
paymentRequest: pretixstripe.paymentRequest,
});
pretixstripe.paymentRequestButton = pretixstripe.elements.create('paymentRequestButton', {
paymentRequest: pretixstripe.paymentRequest,
});
pretixstripe.paymentRequest.canMakePayment().then(function(result) {
if (result) {
pretixstripe.paymentRequestButton.mount('#stripe-payment-request-button');
$('#stripe-elements .stripe-or').removeClass("hidden");
$('#stripe-payment-request-button').parent().removeClass("hidden");
} else {
$('#stripe-payment-request-button').hide();
document.getElementById('stripe-payment-request-button').style.display = 'none';
}
});
pretixstripe.paymentRequest.canMakePayment().then(function (result) {
if (result) {
pretixstripe.paymentRequestButton.mount('#stripe-payment-request-button');
$('#stripe-card-elements .stripe-or').removeClass("hidden");
$('#stripe-payment-request-button').parent().removeClass("hidden");
} else {
$('#stripe-payment-request-button').hide();
document.getElementById('stripe-payment-request-button').style.display = 'none';
}
});
}
}
}
@@ -183,7 +230,7 @@ $(function () {
pretixstripe.handleCardAction(payment_intent_client_secret);
}
$(window).on("message onmessage", function(e) {
$(window).on("message onmessage", function (e) {
if (typeof e.originalEvent.data === "string" && e.originalEvent.data.startsWith('3DS-authentication-complete.')) {
waitingDialog.show(gettext("Confirming your payment …"));
$('#scacontainer').hide();
@@ -200,11 +247,11 @@ $(function () {
if (!$(".stripe-container").length)
return;
if ($("input[name=payment][value=stripe]").is(':checked') || $(".payment-redo-form").length) {
pretixstripe.load();
if ($("input[name=payment][value=stripe]").is(':checked') || $("input[name=payment][value=stripe_sepa_debit]").is(':checked') || $(".payment-redo-form").length) {
pretixstripe.load();
} else {
$("input[name=payment]").change(function () {
if ($(this).val() === 'stripe') {
if (['stripe', 'stripe_sepa_debit'].indexOf($(this).val()) > -1) {
pretixstripe.load();
}
})
@@ -212,9 +259,9 @@ $(function () {
$("#stripe_other_card").click(
function (e) {
$("#stripe_payment_method_id").val("");
$("#stripe_card_payment_method_id").val("");
$("#stripe-current-card").slideUp();
$("#stripe-elements").slideDown();
$("#stripe-card-elements").slideDown();
e.preventDefault();
return false;
@@ -222,7 +269,26 @@ $(function () {
);
if ($("#stripe-current-card").length) {
$("#stripe-elements").hide();
$("#stripe-card-elements").hide();
}
$("#stripe_other_account").click(
function (e) {
$("#stripe_sepa_debit_payment_method_id").val("");
$("#stripe-current-account").slideUp();
// We're using a css-selector here instead of the id-selector,
// as we're hiding Stripe Elements *and* Django form fields
$('.stripe-sepa_debit-form').slideDown();
e.preventDefault();
return false;
}
);
if ($("#stripe-current-account").length) {
// We're using a css-selector here instead of the id-selector,
// as we're hiding Stripe Elements *and* Django form fields
$('.stripe-sepa_debit-form').hide();
}
$('.stripe-container').closest("form").submit(
@@ -231,8 +297,24 @@ $(function () {
return null;
}
if (($("input[name=payment][value=stripe]").prop('checked') || $("input[name=payment][type=radio]").length === 0)
&& $("#stripe_payment_method_id").val() == "") {
pretixstripe.cc_request();
&& $("#stripe_card_payment_method_id").val() == "") {
pretixstripe.pm_request('card', pretixstripe.card);
return false;
}
if (($("input[name=payment][value=stripe_sepa_debit]").prop('checked')) && $("#stripe_sepa_debit_payment_method_id").val() == "") {
pretixstripe.pm_request('sepa_debit', pretixstripe.sepa, {
billing_details: {
name: $("#id_payment_stripe_sepa_debit-accountname").val(),
email: $("#stripe_sepa_debit_email").val(),
address: {
line1: $("#id_payment_stripe_sepa_debit-line1").val(),
postal_code: $("#id_payment_stripe_sepa_debit-postal_code").val(),
city: $("#id_payment_stripe_sepa_debit-city").val(),
country: $("#id_payment_stripe_sepa_debit-country").val(),
}
}
});
return false;
}
}

View File

@@ -1,14 +1,24 @@
{% load i18n %}
{% if provider.method == "cc" %}
{% if provider.method == "card" %}
<p>{% blocktrans trimmed %}
The total amount will be withdrawn from your credit card.
{% endblocktrans %}</p>
<dl class="dl-horizontal">
<dt>{% trans "Card type" %}</dt>
<dd>{{ request.session.payment_stripe_brand }}</dd>
<dd>{{ request.session.payment_stripe_card_brand }}</dd>
<dt>{% trans "Card number" %}</dt>
<dd>**** **** **** {{ request.session.payment_stripe_last4 }}</dd>
<dd>**** **** **** {{ request.session.payment_stripe_card_last4 }}</dd>
</dl>
{% elif provider.method == "sepa_debit" %}
<p>{% blocktrans trimmed %}
The total amount will be withdrawn from your bank account.
{% endblocktrans %}</p>
<dl class="dl-horizontal">
<dt>{% trans "Banking Institution" %}</dt>
<dd>{{ request.session.payment_stripe_sepa_debit_bank }}</dd>
<dt>{% trans "Account number" %}</dt>
<dd>**** **** **** {{ request.session.payment_stripe_sepa_debit_last4 }}</dd>
</dl>
{% else %}
<p>{% blocktrans trimmed %}

View File

@@ -3,7 +3,7 @@
<div class="form-horizontal stripe-container">
{% if is_moto %}
<h1>
<span class="label label-info pull-right flip" data-toggle="tooltip" title="{% trans "This transaction will be marked as Mail Order/Telephone Order, exempting it from Strong Customer Authentication (SCA) whenever possible" %}">MOTO</span>
<span class="label label-info pull-right flip" data-toggle="tooltip_html" title="{% trans "This transaction will be marked as Mail Order/Telephone Order, exempting it from Strong Customer Authentication (SCA) whenever possible" %}">MOTO</span>
</h1>
<div class="clearfix"></div>
{% endif %}
@@ -17,18 +17,18 @@
</div>
</noscript>
{% if request.session.payment_stripe_payment_method_id %}
{% if request.session.payment_stripe_card_payment_method_id %}
<div id="stripe-current-card">
<p>{% blocktrans trimmed %}
You already entered a card number that we will use to charge the payment amount.
{% endblocktrans %}</p>
<dl class="dl-horizontal">
<dt>{% trans "Card type" %}</dt>
<dd id="stripe_card_brand_display">{{ request.session.payment_stripe_brand }}</dd>
<dd id="stripe_card_brand_display">{{ request.session.payment_stripe_card_brand }}</dd>
<dt>{% trans "Card number" %}</dt>
<dd>
**** **** ****
<span id="stripe_card_last4_display">{{ request.session.payment_stripe_last4 }}</span>
<span id="stripe_card_last4_display">{{ request.session.payment_stripe_card_last4 }}</span>
<button class="btn btn-xs btn-default" id="stripe_other_card" type="button">
{% trans "Use a different card" %}
</button>
@@ -37,7 +37,7 @@
</div>
{% endif %}
<div class="row equal" id="stripe-elements">
<div class="row equal" id="stripe-card-elements">
<div class="col-md-5 vcenter stripe-card-holder">
<div id="stripe-card" class="form-control">
<span class="fa fa-spinner fa-spin"></span>
@@ -64,12 +64,10 @@
Your payment will be processed by Stripe, Inc. Your credit card data will be transmitted directly to
Stripe and never touches our servers.
{% endblocktrans %}
<input type="hidden" name="stripe_total" value="{{ total }}" id="stripe_total"/>
<input type="hidden" name="stripe_payment_method_id" value="{{ request.session.payment_stripe_payment_method_id }}" id="stripe_payment_method_id"/>
<input type="hidden" name="stripe_card_last4" value="{{ request.session.payment_stripe_last4 }}"
id="stripe_card_last4"/>
<input type="hidden" name="stripe_card_brand" value="{{ request.session.payment_stripe_brand }}"
id="stripe_card_brand"/>
<input type="hidden" id="stripe_currency" value="{{ event.currency }}"/>
<input type="hidden" name="stripe_card_total" value="{{ total }}" id="stripe_card_total"/>
<input type="hidden" name="stripe_card_payment_method_id" value="{{ request.session.payment_stripe_card_payment_method_id }}" id="stripe_card_payment_method_id"/>
<input type="hidden" name="stripe_card_last4" value="{{ request.session.payment_stripe_card_last4 }}" id="stripe_card_last4"/>
<input type="hidden" name="stripe_card_brand" value="{{ request.session.payment_stripe_card_brand }}" id="stripe_card_brand"/>
<input type="hidden" id="stripe_card_currency" value="{{ event.currency }}"/>
</p>
</div>

View File

@@ -0,0 +1,67 @@
{% load i18n %}
{% load bootstrap3 %}
<div class="form-horizontal stripe-container">
<div class="stripe-errors sr-only">
</div>
<noscript>
<div class="alert alert-warning">
{% trans "For a SEPA Debit payment, please turn on JavaScript." %}
</div>
</noscript>
{% if request.session.payment_stripe_sepa_debit_payment_method_id %}
<div id="stripe-current-account">
<p>{% blocktrans trimmed %}
You already entered a bank account that we will use to charge the payment amount.
{% endblocktrans %}</p>
<dl class="dl-horizontal">
<dt>{% trans "Banking Institution" %}</dt>
<dd id="stripe_sepa_debit_bank_display">{{ request.session.payment_stripe_sepa_debit_bank }}</dd>
<dt>{% trans "Account number" %}</dt>
<dd>
**** **** ****
<span id="stripe_sepa_debit_last4_display">{{ request.session.payment_stripe_sepa_debit_last4 }}</span>
<button class="btn btn-xs btn-default" id="stripe_other_account" type="button">
{% trans "Use a different account" %}
</button>
</dd>
</dl>
</div>
{% endif %}
<div class="stripe-sepa_debit-form">
<div class="form-group">
<label class="col-md-3 control-label">IBAN</label>
<div class="col-md-9">
<div class="row equal" id="stripe-sepa_debit-elements">
<div class="col-md-12 vcenter stripe-sepa">
<div id="stripe-sepa" class="form-control">
<span class="fa fa-spinner fa-spin"></span>
<!-- a Stripe Element will be inserted here. -->
</div>
</div>
</div>
</div>
</div>
{% bootstrap_form form layout='horizontal' %}
</div>
<p class="help-block">
{% blocktrans trimmed with sepa_creditor_name=settings.sepa_creditor_name %}
By providing your payment information and confirming this payment, you authorise (A)
{{ sepa_creditor_name }} and Stripe, our payment service provider and/or PPRO, its local service provider,
to send instructions to your bank to debit your account and (B) your bank to debit your account in
accordance with those instructions. As part of your rights, you are entitled to a refund from your bank
under the terms and conditions of your agreement with your bank. A refund must be claimed within 8 weeks
starting from the date on which your account was debited. Your rights are explained in a statement that you
can obtain from your bank. You agree to receive notifications for future debits up to 2 days before they
occur.
{% endblocktrans %}
<input type="hidden" name="stripe_sepa_debit_payment_method_id" value="{{ request.session.payment_stripe_sepa_debit_payment_method_id }}" id="stripe_sepa_debit_payment_method_id"/>
<input type="checkbox" name="stripe_sepa_debit_country" value="{{ request.session.payment_stripe_sepa_debit_country }}" id="stripe_sepa_debit_country" class="hidden"/>
<input type="hidden" name="stripe_sepa_debit_last4" value="{{ request.session.payment_stripe_sepa_debit_last4 }}" id="stripe_sepa_debit_last4"/>
<input type="hidden" name="stripe_sepa_debit_bank" value="{{ request.session.payment_stripe_sepa_debit_bank }}" id="stripe_sepa_debit_bank"/>
<input type="hidden" name="stripe_sepa_debit_email" value="{{ email }}" id="stripe_sepa_debit_email"/>
</p>
</div>

View File

@@ -17,6 +17,12 @@
<dd>{{ payment_info.source.owner.name }}</dd>
{% endif %}
{% endif %}
{% if payment_info.source.type == "sepa_debit" %}
<dt>{% trans "Bank" %}</dt>
<dd>{{ payment_info.source.sepadirectdebit.bank_name }}</dd>
<dt>{% trans "Payer name" %}</dt>
<dd>{{ payment_info.source.owner.verified_name|default:payment_info.source.owner.name }}</dd>
{% endif %}
{% if payment_info.source.type == "giropay" %}
<dt>{% trans "Bank" %}</dt>
<dd>{{ payment_info.source.giropay.bank_name }} ({{ payment_info.source.giropay.bic }})</dd>

View File

@@ -259,6 +259,7 @@ SOURCE_TYPES = {
'sofort': 'stripe_sofort',
'three_d_secure': 'stripe',
'card': 'stripe',
'sepa_debit': 'stripe_sepa_debit',
'giropay': 'stripe_giropay',
'ideal': 'stripe_ideal',
'alipay': 'stripe_alipay',