mirror of
https://github.com/pretix/pretix.git
synced 2026-05-04 15:04:03 +00:00
Co-authored-by: Raphael Michel <michel@rami.io> Co-authored-by: Raphael Michel <mail@raphaelmichel.de>
This commit is contained in:
@@ -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)
|
||||
]
|
||||
@@ -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')
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user