diff --git a/src/pretix/plugins/stripe/payment.py b/src/pretix/plugins/stripe/payment.py
index b19d5445bd..f9fcdb0cb1 100644
--- a/src/pretix/plugins/stripe/payment.py
+++ b/src/pretix/plugins/stripe/payment.py
@@ -151,15 +151,6 @@ class StripeSettingsHolder(BasePaymentProvider):
]
d = OrderedDict(
fields + [
- ('ui',
- forms.ChoiceField(
- label=_('User interface'),
- choices=(
- ('pretix', _('Simple (pretix design)')),
- ('checkout', _('Stripe Checkout')),
- ),
- help_text=_('Only relevant for credit card payments.')
- )),
('method_cc',
forms.BooleanField(
label=_('Credit card payments'),
@@ -297,7 +288,7 @@ class StripeMethod(BasePaymentProvider):
return kwargs
def _init_api(self):
- stripe.api_version = '2018-02-28'
+ stripe.api_version = '2019-05-16'
stripe.set_app_info("pretix", version=__version__, url="https://pretix.eu")
def checkout_confirm_render(self, request) -> str:
@@ -423,6 +414,7 @@ class StripeMethod(BasePaymentProvider):
'order': payment.order,
'payment': payment,
'payment_info': payment_info,
+ 'payment_hash': hashlib.sha1(payment.order.secret.lower().encode()).hexdigest()
}
return template.render(ctx)
@@ -583,11 +575,7 @@ class StripeCC(StripeMethod):
if not RegisteredApplePayDomain.objects.filter(account=account, domain=request.host).exists():
stripe_verify_domain.apply_async(args=(self.event.pk, request.host))
- ui = self.settings.get('ui', default='pretix')
- if ui == 'checkout':
- template = get_template('pretixplugins/stripe/checkout_payment_form_stripe_checkout.html')
- else:
- template = get_template('pretixplugins/stripe/checkout_payment_form.html')
+ template = get_template('pretixplugins/stripe/checkout_payment_form.html')
ctx = {
'request': request,
'event': self.event,
@@ -597,93 +585,232 @@ class StripeCC(StripeMethod):
return template.render(ctx)
def payment_is_valid_session(self, request):
- return request.session.get('payment_stripe_token', '') != ''
+ return request.session.get('payment_stripe_payment_method_id', '') != ''
def checkout_prepare(self, request, cart):
- token = request.POST.get('stripe_token', '')
- request.session['payment_stripe_token'] = token
+ 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', '')
- if token == '':
- messages.error(request, _('You may need to enable JavaScript for Stripe payments.'))
+ if payment_method_id == '':
+ messages.warning(request, _('You may need to enable JavaScript for Stripe payments.'))
return False
return True
- def _use_3ds(self, card):
- if self.settings.cc_3ds_mode == 'recommended':
- return card.three_d_secure in ('required', 'recommended')
- elif self.settings.cc_3ds_mode == 'optional':
- return card.three_d_secure in ('required', 'recommended', 'optional')
- else:
- return card.three_d_secure == 'required'
-
def execute_payment(self, request: HttpRequest, payment: OrderPayment):
+ try:
+ return self._handle_payment_intent(request, payment)
+ finally:
+ del request.session['payment_stripe_payment_method_id']
+
+ def _handle_payment_intent(self, request, payment, intent=None):
self._init_api()
- if request.session['payment_stripe_token'].startswith('src_'):
- try:
- src = stripe.Source.retrieve(request.session['payment_stripe_token'], **self.api_kwargs)
- if src.type == 'card' and src.card and self._use_3ds(src.card):
- request.session['payment_stripe_order_secret'] = payment.order.secret
- source = stripe.Source.create(
- type='three_d_secure',
- amount=self._get_amount(payment),
- currency=self.event.currency.lower(),
- three_d_secure={
- 'card': src.id
- },
- statement_descriptor=ugettext('{event}-{code}').format(
- event=self.event.slug.upper(),
- code=payment.order.code
- )[:22],
- metadata={
- 'order': str(payment.order.id),
- 'event': self.event.id,
- 'code': payment.order.code
- },
- redirect={
- 'return_url': build_absolute_uri(self.event, 'plugins:stripe:return', kwargs={
- 'order': payment.order.code,
- 'payment': payment.pk,
- 'hash': hashlib.sha1(payment.order.secret.lower().encode()).hexdigest(),
- })
- },
- **self.api_kwargs
- )
- ReferencedStripeObject.objects.get_or_create(
- reference=source.id,
- defaults={'order': payment.order, 'payment': payment}
- )
- if source.status == "pending":
- payment.info = str(source)
- payment.state = OrderPayment.PAYMENT_STATE_PENDING
- payment.save()
- return self.redirect(request, source.redirect.url)
- except stripe.error.StripeError as e:
- if e.json_body:
- err = e.json_body['error']
- logger.exception('Stripe error: %s' % str(err))
+ try:
+ if 'payment_stripe_payment_method_id' in request.session:
+ intent = stripe.PaymentIntent.create(
+ amount=self._get_amount(payment),
+ currency=self.event.currency.lower(),
+ payment_method=request.session['payment_stripe_payment_method_id'],
+ confirmation_method='manual',
+ confirm=True,
+ description='{event}-{code}'.format(
+ event=self.event.slug.upper(),
+ code=payment.order.code
+ ),
+ statement_descriptor=ugettext('{event}-{code}').format(
+ event=self.event.slug.upper(),
+ code=payment.order.code
+ )[:22],
+ metadata={
+ 'order': str(payment.order.id),
+ 'event': self.event.id,
+ 'code': payment.order.code
+ },
+ # TODO: Is this sufficient?
+ idempotency_key=str(self.event.id) + payment.order.code + request.session['payment_stripe_payment_method_id'],
+ return_url=build_absolute_uri(self.event, 'plugins:stripe:sca.return', kwargs={
+ 'order': payment.order.code,
+ 'payment': payment.pk,
+ 'hash': hashlib.sha1(payment.order.secret.lower().encode()).hexdigest(),
+ }),
+ **self.api_kwargs
+ )
+ else:
+ payment_info = json.loads(payment.info)
+
+ if 'id' in payment_info:
+ if not intent:
+ intent = stripe.PaymentIntent.retrieve(
+ payment_info['id'],
+ **self.api_kwargs
+ )
else:
- err = {'message': str(e)}
- logger.exception('Stripe error: %s' % str(e))
- payment.info_data = {
- 'error': True,
- 'message': err['message'],
- }
+ return
+
+ except stripe.error.CardError as e:
+ if e.json_body:
+ err = e.json_body['error']
+ logger.exception('Stripe error: %s' % str(err))
+ else:
+ err = {'message': str(e)}
+ logger.exception('Stripe error: %s' % str(e))
+ logger.info('Stripe card error: %s' % str(err))
+ payment.info_data = {
+ 'error': True,
+ 'message': err['message'],
+ }
+ payment.state = OrderPayment.PAYMENT_STATE_FAILED
+ payment.save()
+ payment.order.log_action('pretix.event.order.payment.failed', {
+ 'local_id': payment.local_id,
+ 'provider': payment.provider,
+ 'message': err['message']
+ })
+ raise PaymentException(_('Stripe reported an error with your card: %s') % err['message'])
+
+ except stripe.error.StripeError as e:
+ if e.json_body:
+ err = e.json_body['error']
+ logger.exception('Stripe error: %s' % str(err))
+ else:
+ err = {'message': str(e)}
+ logger.exception('Stripe error: %s' % str(e))
+ payment.info_data = {
+ 'error': True,
+ 'message': err['message'],
+ }
+ payment.state = OrderPayment.PAYMENT_STATE_FAILED
+ payment.save()
+ payment.order.log_action('pretix.event.order.payment.failed', {
+ 'local_id': payment.local_id,
+ 'provider': payment.provider,
+ 'message': err['message']
+ })
+ raise PaymentException(_('We had trouble communicating with Stripe. Please try again and get in touch '
+ 'with us if this problem persists.'))
+ else:
+ ReferencedStripeObject.objects.get_or_create(
+ reference=intent.id,
+ defaults={'order': payment.order, 'payment': payment}
+ )
+ if intent.status == 'requires_action':
+ payment.info = str(intent)
+ payment.state = OrderPayment.PAYMENT_STATE_CREATED
+ payment.save()
+ return build_absolute_uri(self.event, 'plugins:stripe:sca', kwargs={
+ 'order': payment.order.code,
+ 'payment': payment.pk,
+ 'hash': hashlib.sha1(payment.order.secret.lower().encode()).hexdigest(),
+ })
+
+ if intent.status == 'requires_confirmation':
+ payment.info = str(intent)
+ payment.state = OrderPayment.PAYMENT_STATE_CREATED
+ payment.save()
+ self._confirm_payment_intent(request, payment)
+
+ elif intent.status == 'succeeded' and intent.charges.data[-1].paid:
+ try:
+ payment.info = str(intent)
+ payment.confirm()
+ except Quota.QuotaExceededException as e:
+ raise PaymentException(str(e))
+
+ except SendMailException:
+ raise PaymentException(_('There was an error sending the confirmation mail.'))
+ elif intent.status == 'pending':
+ if request:
+ messages.warning(request, _('Your payment is pending completion. We will inform you as soon as the '
+ 'payment completed.'))
+ payment.info = str(intent)
+ payment.state = OrderPayment.PAYMENT_STATE_PENDING
+ payment.save()
+ return
+ elif intent.status == 'requires_payment_method':
+ if request:
+ messages.warning(request, _('Your payment failed. Please try again.'))
+ payment.info = str(intent)
+ payment.state = OrderPayment.PAYMENT_STATE_FAILED
+ payment.save()
+ return
+ else:
+ logger.info('Charge failed: %s' % str(intent))
+ payment.info = str(intent)
payment.state = OrderPayment.PAYMENT_STATE_FAILED
payment.save()
payment.order.log_action('pretix.event.order.payment.failed', {
'local_id': payment.local_id,
'provider': payment.provider,
- 'message': err['message']
+ 'info': str(intent)
})
- raise PaymentException(_('We had trouble communicating with Stripe. Please try again and get in touch '
- 'with us if this problem persists.'))
+ raise PaymentException(_('Stripe reported an error: %s') % intent.last_payment_error.message)
+
+ def _confirm_payment_intent(self, request, payment):
+ self._init_api()
try:
- self._charge_source(request, request.session['payment_stripe_token'], payment)
- finally:
- del request.session['payment_stripe_token']
+ payment_info = json.loads(payment.info)
+
+ intent = stripe.PaymentIntent.confirm(
+ payment_info['id'],
+ return_url=build_absolute_uri(self.event, 'plugins:stripe:sca.return', kwargs={
+ 'order': payment.order.code,
+ 'payment': payment.pk,
+ 'hash': hashlib.sha1(payment.order.secret.lower().encode()).hexdigest(),
+ }),
+ **self.api_kwargs
+ )
+
+ payment.info = str(intent)
+ payment.save()
+
+ self._handle_payment_intent(request, payment)
+ except stripe.error.CardError as e:
+ if e.json_body:
+ err = e.json_body['error']
+ logger.exception('Stripe error: %s' % str(err))
+ else:
+ err = {'message': str(e)}
+ logger.exception('Stripe error: %s' % str(e))
+ logger.info('Stripe card error: %s' % str(err))
+ payment.info_data = {
+ 'error': True,
+ 'message': err['message'],
+ }
+ payment.state = OrderPayment.PAYMENT_STATE_FAILED
+ payment.save()
+ payment.order.log_action('pretix.event.order.payment.failed', {
+ 'local_id': payment.local_id,
+ 'provider': payment.provider,
+ 'message': err['message']
+ })
+ raise PaymentException(_('Stripe reported an error with your card: %s') % err['message'])
+ except stripe.error.InvalidRequestError as e:
+ if e.json_body:
+ err = e.json_body['error']
+ logger.exception('Stripe error: %s' % str(err))
+ else:
+ err = {'message': str(e)}
+ logger.exception('Stripe error: %s' % str(e))
+ payment.info_data = {
+ 'error': True,
+ 'message': err['message'],
+ }
+ payment.state = OrderPayment.PAYMENT_STATE_FAILED
+ payment.save()
+ payment.order.log_action('pretix.event.order.payment.failed', {
+ 'local_id': payment.local_id,
+ 'provider': payment.provider,
+ 'message': err['message']
+ })
+ raise PaymentException(_('We had trouble communicating with Stripe. Please try again and get in touch '
+ 'with us if this problem persists.'))
+
+ def payment_pending_render(self, request, payment) -> str:
+ self._handle_payment_intent(request, payment)
+
+ return super().payment_pending_render(request, payment)
class StripeGiropay(StripeMethod):
diff --git a/src/pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe-checkout.js b/src/pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe-checkout.js
deleted file mode 100644
index 9797d5ffa0..0000000000
--- a/src/pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe-checkout.js
+++ /dev/null
@@ -1,89 +0,0 @@
-/*global $, stripe_pubkey, stripe_loadingmessage, gettext */
-'use strict';
-
-var Stripe = null;
-var pretixstripe = {
- 'load': function () {
- $.ajax(
- {
- url: 'https://checkout.stripe.com/checkout.js',
- dataType: 'script',
- success: function () {
- pretixstripe.handler = StripeCheckout.configure({
- key: $.trim($("#stripe_pubkey").html()),
- locale: 'auto',
- token: function(token) {
- var $form = $("#stripe-checkout").parents("form");
- $("#stripe_token").val(token.id);
- $("#stripe_card_brand").val(token.card.brand);
- $("#stripe_card_last4").val(token.card.last4);
- $("#stripe_card_brand_display").text(token.card.brand);
- $("#stripe_card_last4_display").text(token.card.last4);
- $($form.get(0)).submit();
- },
- shippingAddress: false,
- allowRememberMe: false,
- billingAddress: false
- });
- }
- }
- );
- },
-
- start: function () {
- var amount = Math.round(
- parseFloat(
- $("#stripe-checkout").parents("[data-total]").attr("data-total").replace(",", ".")
- ) * 100
- );
- pretixstripe.handler.open({
- name: $("#organizer_name").val(),
- description: $("#event_name").val(),
- currency: $("#stripe_currency").val(),
- email: $("#stripe_email").val(),
- amount: amount
- });
- },
-
- handler: null
-};
-$(function () {
- if (!$("#stripe-checkout").length) { // Not on the checkout page
- return;
- }
-
- if ($("input[name=payment][value=stripe]").is(':checked') || $(".payment-redo-form").length) {
- pretixstripe.load();
- } else {
- $("input[name=payment]").change(function() {
- if ($(this).val() == 'stripe') {
- pretixstripe.load();
- }
- })
- }
-
- $(".checkout-button-row .btn-primary").click(
- function (e) {
- if (($("input[name=payment][value=stripe]").prop('checked') || $("input[type=checkbox][name=radio]").length === 0)
- && $("#stripe_token").val() == "") {
- pretixstripe.start();
- e.preventDefault();
- return false;
- }
- }
- );
-
- $("#stripe_other_card").click(
- function (e) {
- pretixstripe.start();
- e.preventDefault();
- return false;
- }
- );
-
- $(window).on('popstate', function () {
- if (pretixstripe.handler) {
- pretixstripe.handler.close();
- }
- });
-});
diff --git a/src/pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.css b/src/pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.css
index edae2e3818..c9e8a9faad 100644
--- a/src/pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.css
+++ b/src/pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.css
@@ -24,6 +24,11 @@
margin: 15px 0;
}
+.embed-responsive-sca {
+ padding-bottom: 75%;
+ min-height: 600px;
+}
+
@media only screen and (max-width: 999px) {
.hr {
width: 100%;
diff --git a/src/pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js b/src/pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js
index 7648831444..22a40c2e31 100644
--- a/src/pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js
+++ b/src/pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js
@@ -12,18 +12,19 @@ var pretixstripe = {
waitingDialog.show(gettext("Contacting Stripe …"));
$(".stripe-errors").hide();
- pretixstripe.stripe.createSource(pretixstripe.card).then(function (result) {
+ // ToDo: 'card' --> proper type of payment method
+ pretixstripe.stripe.createPaymentMethod('card', pretixstripe.card).then(function (result) {
waitingDialog.hide();
if (result.error) {
$(".stripe-errors").stop().hide().removeClass("sr-only");
$(".stripe-errors").html("
" + result.error.message + "
");
$(".stripe-errors").slideDown();
} else {
- var $form = $("#stripe_token").closest("form");
+ var $form = $("#stripe_payment_method_id").closest("form");
// Insert the token into the form so it gets submitted to the server
- $("#stripe_token").val(result.source.id);
- $("#stripe_card_brand").val(result.source.card.brand);
- $("#stripe_card_last4").val(result.source.card.last4);
+ $("#stripe_payment_method_id").val(result.paymentMethod.id);
+ $("#stripe_card_brand").val(result.paymentMethod.card.brand);
+ $("#stripe_card_last4").val(result.paymentMethod.card.last4);
// and submit
$form.get(0).submit();
}
@@ -63,14 +64,14 @@ var pretixstripe = {
requestShipping: false,
});
- pretixstripe.paymentRequest.on('token', function (ev) {
+ pretixstripe.paymentRequest.on('paymentmethod', function (ev) {
ev.complete('success');
- var $form = $("#stripe_token").closest("form");
+ var $form = $("#stripe_payment_method_id").closest("form");
// Insert the token into the form so it gets submitted to the server
- $("#stripe_token").val(ev.token.id);
- $("#stripe_card_brand").val(ev.token.card.brand);
- $("#stripe_card_last4").val(ev.token.card.last4);
+ $("#stripe_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
$form.get(0).submit();
});
@@ -125,84 +126,83 @@ var pretixstripe = {
}
);
},
- 'load_checkout': function () {
- if (pretixstripe.checkout_handler !== null) {
- return;
- }
- $('.stripe-container').closest("form").find(".checkout-button-row .btn-primary").prop("disabled", true);
- $.ajax(
- {
- url: 'https://checkout.stripe.com/checkout.js',
- dataType: 'script',
- success: function () {
- pretixstripe.checkout_handler = StripeCheckout.configure({
- key: $.trim($("#stripe_pubkey").html()),
- locale: 'auto',
- token: function (token) {
- var $form = $("#stripe-checkout").parents("form");
- $("#stripe_token").val(token.id);
- $("#stripe_card_brand").val(token.card.brand);
- $("#stripe_card_last4").val(token.card.last4);
- $("#stripe_card_brand_display").text(token.card.brand);
- $("#stripe_card_last4_display").text(token.card.last4);
- $($form.get(0)).submit();
- },
- shippingAddress: false,
- allowRememberMe: false,
- billingAddress: false
+ 'handleCardAction': function (payment_intent_client_secret) {
+ $.ajax({
+ url: 'https://js.stripe.com/v3/',
+ dataType: 'script',
+ success: function () {
+ if ($.trim($("#stripe_connectedAccountId").html())) {
+ pretixstripe.stripe = Stripe($.trim($("#stripe_pubkey").html()), {
+ stripeAccount: $.trim($("#stripe_connectedAccountId").html())
});
- $('.stripe-container').closest("form").find(".checkout-button-row .btn-primary").prop("disabled", false);
+ } else {
+ pretixstripe.stripe = Stripe($.trim($("#stripe_pubkey").html()));
}
+ pretixstripe.stripe.handleCardAction(
+ payment_intent_client_secret
+ ).then(function (result) {
+ waitingDialog.show(gettext("Confirming your payment …"));
+ location.reload();
+ });
}
- );
- },
- 'show_checkout': function () {
- var amount = Math.round(
- parseFloat(
- $("#stripe-checkout").parents("[data-total]").attr("data-total").replace(",", ".")
- ) * 100
- );
- pretixstripe.checkout_handler.open({
- name: $("#organizer_name").val(),
- description: $("#event_name").val(),
- currency: $("#stripe_currency").val(),
- email: $("#stripe_email").val(),
- amount: amount
});
},
- 'checkout_handler': null
+ 'handleCardActioniFrame': function (payment_intent_next_action_redirect_url) {
+ waitingDialog.show(gettext("Contacting your bank …"));
+ let iframe = document.createElement('iframe');
+ iframe.src = payment_intent_next_action_redirect_url;
+ iframe.className = 'embed-responsive-item';
+ $('#scacontainer').append(iframe);
+ $('#scacontainer iframe').load(function () {
+ waitingDialog.hide();
+ });
+ }
};
$(function () {
- if (!$(".stripe-container, #stripe-checkout").length) // Not on the checkout page
+ if ($("#stripe_payment_intent_SCA_status").length) {
+ window.parent.postMessage('3DS-authentication-complete.' + $.trim($("#order_status").html()), '*');
+ return;
+ } else if ($("#stripe_payment_intent_next_action_redirect_url").length) {
+ let payment_intent_next_action_redirect_url = $.trim($("#stripe_payment_intent_next_action_redirect_url").html());
+ pretixstripe.handleCardActioniFrame(payment_intent_next_action_redirect_url);
+ } else if ($("#stripe_payment_intent_client_secret").length) {
+ let payment_intent_client_secret = $.trim($("#stripe_payment_intent_client_secret").html());
+ pretixstripe.handleCardAction(payment_intent_client_secret);
+ }
+
+ $(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();
+ $('#continuebutton').removeClass('hidden');
+
+ if (e.originalEvent.data.split('.')[1] == 'p') {
+ window.location.href = $('#continuebutton').attr('href') + '?paid=yes';
+ } else {
+ window.location.href = $('#continuebutton').attr('href');
+ }
+ }
+ });
+
+ if (!$(".stripe-container").length)
return;
if ($("input[name=payment][value=stripe]").is(':checked') || $(".payment-redo-form").length) {
- if ($("#stripe-checkout").length) {
- pretixstripe.load_checkout();
- } else {
pretixstripe.load();
- }
} else {
$("input[name=payment]").change(function () {
if ($(this).val() === 'stripe') {
- if ($("#stripe-checkout").length) {
- pretixstripe.load_checkout();
- } else {
- pretixstripe.load();
- }
+ pretixstripe.load();
}
})
}
$("#stripe_other_card").click(
function (e) {
- $("#stripe_token").val("");
- if ($("#stripe-checkout").length) {
- pretixstripe.show_checkout();
- } else {
- $("#stripe-current-card").slideUp();
- $("#stripe-elements").slideDown();
- }
+ $("#stripe_payment_method_id").val("");
+ $("#stripe-current-card").slideUp();
+ $("#stripe-elements").slideDown();
+
e.preventDefault();
return false;
}
@@ -218,19 +218,10 @@ $(function () {
return null;
}
if (($("input[name=payment][value=stripe]").prop('checked') || $("input[name=payment][type=radio]").length === 0)
- && $("#stripe_token").val() == "") {
- if ($("#stripe-checkout").length) {
- pretixstripe.show_checkout();
- } else {
- pretixstripe.cc_request();
- }
+ && $("#stripe_payment_method_id").val() == "") {
+ pretixstripe.cc_request();
return false;
}
}
);
- $(window).on('popstate', function () {
- if (pretixstripe.checkout_handler) {
- pretixstripe.checkout_handler.close();
- }
- });
-});
+});
\ No newline at end of file
diff --git a/src/pretix/plugins/stripe/tasks.py b/src/pretix/plugins/stripe/tasks.py
index afd6c6bbc3..e703ff6b05 100644
--- a/src/pretix/plugins/stripe/tasks.py
+++ b/src/pretix/plugins/stripe/tasks.py
@@ -4,7 +4,6 @@ from urllib.parse import urlsplit
import stripe
from django.conf import settings
-from pretix.base.models import Event
from pretix.base.services.tasks import EventTask
from pretix.celery_app import app
from pretix.multidomain.urlreverse import get_domain
@@ -29,9 +28,8 @@ def get_stripe_account_key(prov):
@app.task(base=EventTask, max_retries=5, default_retry_delay=1)
-def stripe_verify_domain(event_id, domain):
+def stripe_verify_domain(event, domain):
from pretix.plugins.stripe.payment import StripeCC
- event = Event.objects.get(pk=event_id)
prov = StripeCC(event)
account = get_stripe_account_key(prov)
diff --git a/src/pretix/plugins/stripe/templates/pretixplugins/stripe/checkout_payment_form.html b/src/pretix/plugins/stripe/templates/pretixplugins/stripe/checkout_payment_form.html
index 22afbcb654..3c6ee426a5 100644
--- a/src/pretix/plugins/stripe/templates/pretixplugins/stripe/checkout_payment_form.html
+++ b/src/pretix/plugins/stripe/templates/pretixplugins/stripe/checkout_payment_form.html
@@ -10,7 +10,7 @@
- {% if request.session.payment_stripe_token %}
+ {% if request.session.payment_stripe_payment_method_id %}
{% blocktrans trimmed %}
You already entered a card number that we will use to charge the payment amount.
@@ -58,7 +58,7 @@
Stripe and never touches our servers.
{% endblocktrans %}
-
+
-
-
- {% trans "For a credit card payment, please turn on JavaScript." %}
-
-
- {% if request.session.payment_stripe_token %}
-
{% blocktrans trimmed %}
- You already entered a card number that we will use to charge the payment amount.
- {% endblocktrans %}
-
- {% trans "Card type" %}
- {{ request.session.payment_stripe_brand }}
- {% trans "Card number" %}
-
- **** **** **** {{ request.session.payment_stripe_last4 }}
-
- {% trans "Use a different card" %}
-
-
-
-
-
- {% else %}
-
- {% blocktrans trimmed %}
- Please continue below to start the credit card payment.
- {% endblocktrans %}
-
- {% endif %}
-
- {% blocktrans trimmed %}
- Your payment will be processed by Stripe, Inc. Your credit card data will be transmitted directly to
- Stripe and never touches our servers.
- {% endblocktrans %}
-
-
-
-
-
-
-
-
- {% if order %}
-
- {% else %}
-
- {% endif %}
-
-
diff --git a/src/pretix/plugins/stripe/templates/pretixplugins/stripe/pending.html b/src/pretix/plugins/stripe/templates/pretixplugins/stripe/pending.html
index ab9da078f5..ad4fa21af8 100644
--- a/src/pretix/plugins/stripe/templates/pretixplugins/stripe/pending.html
+++ b/src/pretix/plugins/stripe/templates/pretixplugins/stripe/pending.html
@@ -1,10 +1,23 @@
{% load i18n %}
-
+{% load eventurl %}
{% if payment.state == "pending" %}
{% blocktrans trimmed %}
We're waiting for an answer from the payment provider regarding your payment. Please contact us if this
takes more than a few days.
{% endblocktrans %}
+{% elif payment.state == "created" and payment_info.status == "requires_action" %}
+ {% blocktrans trimmed %}
+ You need to confirm your payment. Please click the link below to do so or start a new payment.
+ {% endblocktrans %}
+
+
+
+
{% else %}
{% blocktrans trimmed %}
The payment transaction could not be completed for the following reason:
@@ -16,4 +29,4 @@
{% trans "Unknown reason" %}
{% endif %}
-{% endif %}
+{% endif %}
\ No newline at end of file
diff --git a/src/pretix/plugins/stripe/templates/pretixplugins/stripe/sca.html b/src/pretix/plugins/stripe/templates/pretixplugins/stripe/sca.html
new file mode 100644
index 0000000000..02a51ae13c
--- /dev/null
+++ b/src/pretix/plugins/stripe/templates/pretixplugins/stripe/sca.html
@@ -0,0 +1,41 @@
+{% extends "pretixpresale/event/base.html" %}
+{% load i18n %}
+{% load eventurl %}
+{% load static %}
+{% block title %}{% trans "Pay order" %}{% endblock %}
+{% block custom_header %}
+ {{ block.super }}
+ {% include "pretixplugins/stripe/presale_head.html" with settings=stripe_settings %}
+
+
+{% endblock %}
+{% block content %}
+
+
+
+ {% blocktrans trimmed with code=order.code %}
+ Confirm payment: {{ code }}
+ {% endblocktrans %}
+
+
+
+
+
+
+
+{% endblock %}
diff --git a/src/pretix/plugins/stripe/templates/pretixplugins/stripe/sca_return.html b/src/pretix/plugins/stripe/templates/pretixplugins/stripe/sca_return.html
new file mode 100644
index 0000000000..3579b6df89
--- /dev/null
+++ b/src/pretix/plugins/stripe/templates/pretixplugins/stripe/sca_return.html
@@ -0,0 +1,20 @@
+{% extends "pretixpresale/base.html" %}
+{% load i18n %}
+{% load static %}
+{% load thumb %}
+{% load eventurl %}
+{% block title %}{% trans "Pay order" %}{% endblock %}
+{% block custom_header %}
+ {{ block.super }}
+ {% include "pretixplugins/stripe/presale_head.html" with settings=stripe_settings %}
+
+
+{% endblock %}
+{% block page %}
+
+
+
+
+ {% trans "Confirming your payment…" %}
+
+{% endblock %}
diff --git a/src/pretix/plugins/stripe/urls.py b/src/pretix/plugins/stripe/urls.py
index f277fd282f..bad2fd9ffb 100644
--- a/src/pretix/plugins/stripe/urls.py
+++ b/src/pretix/plugins/stripe/urls.py
@@ -3,8 +3,8 @@ from django.conf.urls import include, url
from pretix.multidomain import event_url
from .views import (
- ReturnView, applepay_association, oauth_disconnect, oauth_return,
- redirect_view, webhook,
+ ReturnView, ScaReturnView, ScaView, applepay_association, oauth_disconnect,
+ oauth_return, redirect_view, webhook,
)
event_patterns = [
@@ -12,6 +12,8 @@ event_patterns = [
event_url(r'^webhook/$', webhook, name='webhook', require_live=False),
url(r'^redirect/$', redirect_view, name='redirect'),
url(r'^return/(?P[^/]+)/(?P[^/]+)/(?P[0-9]+)/$', ReturnView.as_view(), name='return'),
+ url(r'^sca/(?P[^/]+)/(?P[^/]+)/(?P[0-9]+)/$', ScaView.as_view(), name='sca'),
+ url(r'^sca/(?P[^/]+)/(?P[^/]+)/(?P[0-9]+)/return/$', ScaReturnView.as_view(), name='sca.return'),
])),
]
diff --git a/src/pretix/plugins/stripe/views.py b/src/pretix/plugins/stripe/views.py
index 4b3cc0fc2f..249f577eca 100644
--- a/src/pretix/plugins/stripe/views.py
+++ b/src/pretix/plugins/stripe/views.py
@@ -26,7 +26,7 @@ from pretix.base.settings import GlobalSettingsObject
from pretix.control.permissions import event_permission_required
from pretix.multidomain.urlreverse import eventreverse
from pretix.plugins.stripe.models import ReferencedStripeObject
-from pretix.plugins.stripe.payment import StripeCC
+from pretix.plugins.stripe.payment import StripeCC, StripeSettingsHolder
from pretix.plugins.stripe.tasks import (
get_domain_for_event, stripe_verify_domain,
)
@@ -160,6 +160,9 @@ def webhook(request, *args, **kwargs):
elif event_json['data']['object']['object'] == "source":
func = source_webhook
objid = event_json['data']['object']['id']
+ elif event_json['data']['object']['object'] == "payment_intent":
+ func = paymentintent_webhook
+ objid = event_json['data']['object']['id']
else:
return HttpResponse("Not interested in this data type", status=200)
@@ -187,6 +190,7 @@ SOURCE_TYPES = {
def charge_webhook(event, event_json, charge_id, rso):
prov = StripeCC(event)
prov._init_api()
+
try:
charge = stripe.Charge.retrieve(charge_id, expand=['dispute'], **prov.api_kwargs)
except stripe.error.StripeError:
@@ -359,6 +363,25 @@ def source_webhook(event, event_json, source_id, rso):
return HttpResponse(status=200)
+def paymentintent_webhook(event, event_json, paymentintent_id, rso):
+ prov = StripeCC(event)
+ prov._init_api()
+
+ try:
+ paymentintent = stripe.PaymentIntent.retrieve(paymentintent_id, **prov.api_kwargs)
+ except stripe.error.StripeError:
+ logger.exception('Stripe error on webhook. Event data: %s' % str(event_json))
+ return HttpResponse('Charge not found', status=500)
+
+ for charge in paymentintent.charges.data:
+ ReferencedStripeObject.objects.get_or_create(
+ reference=charge.id,
+ defaults={'order': rso.payment.order, 'payment': rso.payment}
+ )
+
+ return HttpResponse(status=200)
+
+
@event_permission_required('can_change_event_settings')
@require_POST
def oauth_disconnect(request, **kwargs):
@@ -409,18 +432,36 @@ class StripeOrderView:
def pprov(self):
return self.request.event.get_payment_providers()[self.payment.provider]
+ def _redirect_to_order(self):
+ if self.request.session.get('payment_stripe_order_secret') != self.order.secret:
+ messages.error(self.request, _('Sorry, there was an error in the payment process. Please check the link '
+ 'in your emails to continue.'))
+ return redirect(eventreverse(self.request.event, 'presale:event.index'))
+
+ return redirect(eventreverse(self.request.event, 'presale:event.order', kwargs={
+ 'order': self.order.code,
+ 'secret': self.order.secret
+ }) + ('?paid=yes' if self.order.status == Order.STATUS_PAID else ''))
+
@method_decorator(xframe_options_exempt, 'dispatch')
class ReturnView(StripeOrderView, View):
def get(self, request, *args, **kwargs):
prov = self.pprov
prov._init_api()
- src = stripe.Source.retrieve(request.GET.get('source'), **prov.api_kwargs)
- if src.client_secret != request.GET.get('client_secret'):
+ try:
+ src = stripe.Source.retrieve(request.GET.get('source'), **prov.api_kwargs)
+ except stripe.error.InvalidRequestError:
+ logger.exception('Could not retrieve source')
messages.error(self.request, _('Sorry, there was an error in the payment process. Please check the link '
'in your emails to continue.'))
return redirect(eventreverse(self.request.event, 'presale:event.index'))
+ if src.client_secret != request.GET.get('client_secret'):
+ messages.error(self.request, _('Sorry, there was an error in the payment process.'
+ 'in your emails to continue.'))
+ return redirect(eventreverse(self.request.event, 'presale:event.index'))
+
with transaction.atomic():
self.order.refresh_from_db()
self.payment.refresh_from_db()
@@ -460,13 +501,67 @@ class ReturnView(StripeOrderView, View):
'get in touch with us if this problem persists.'))
return self._redirect_to_order()
- def _redirect_to_order(self):
- if self.request.session.get('payment_stripe_order_secret') != self.order.secret:
- messages.error(self.request, _('Sorry, there was an error in the payment process. Please check the link '
- 'in your emails to continue.'))
- return redirect(eventreverse(self.request.event, 'presale:event.index'))
- return redirect(eventreverse(self.request.event, 'presale:event.order', kwargs={
- 'order': self.order.code,
- 'secret': self.order.secret
- }) + ('?paid=yes' if self.order.status == Order.STATUS_PAID else ''))
+@method_decorator(xframe_options_exempt, 'dispatch')
+class ScaView(StripeOrderView, View):
+
+ def get(self, request, *args, **kwargs):
+ prov = self.pprov
+ prov._init_api()
+
+ if self.payment.state in (OrderPayment.PAYMENT_STATE_CONFIRMED,
+ OrderPayment.PAYMENT_STATE_CANCELED,
+ OrderPayment.PAYMENT_STATE_FAILED):
+ return self._redirect_to_order()
+
+ payment_info = json.loads(self.payment.info)
+
+ if 'id' in payment_info:
+ try:
+ intent = stripe.PaymentIntent.retrieve(
+ payment_info['id'],
+ **prov.api_kwargs
+ )
+ except stripe.error.InvalidRequestError:
+ logger.exception('Could not retrieve payment intent')
+ messages.error(self.request, _('Sorry, there was an error in the payment process.'))
+ return self._redirect_to_order()
+ else:
+ messages.error(self.request, _('Sorry, there was an error in the payment process.'))
+ return self._redirect_to_order()
+
+ if intent.status == 'requires_action' and intent.next_action.type in ['use_stripe_sdk', 'redirect_to_url']:
+ ctx = {
+ 'order': self.order,
+ 'stripe_settings': StripeSettingsHolder(self.order.event).settings,
+ }
+ if intent.next_action.type == 'use_stripe_sdk':
+ ctx['payment_intent_client_secret'] = intent.client_secret
+ elif intent.next_action.type == 'redirect_to_url':
+ ctx['payment_intent_next_action_redirect_url'] = intent.next_action.redirect_to_url['url']
+
+ r = render(request, 'pretixplugins/stripe/sca.html', ctx)
+ r._csp_ignore = True
+ return r
+ else:
+ try:
+ prov._handle_payment_intent(request, self.payment, intent)
+ except PaymentException as e:
+ messages.error(request, str(e))
+
+ return self._redirect_to_order()
+
+
+@method_decorator(xframe_options_exempt, 'dispatch')
+class ScaReturnView(StripeOrderView, View):
+ def get(self, request, *args, **kwargs):
+ prov = self.pprov
+
+ try:
+ prov._handle_payment_intent(request, self.payment)
+ except PaymentException as e:
+ messages.error(request, str(e))
+
+ self.order.refresh_from_db()
+
+ return render(request, 'pretixplugins/stripe/sca_return.html', {'order': self.order})
diff --git a/src/pretix/static/pretixpresale/scss/main.scss b/src/pretix/static/pretixpresale/scss/main.scss
index 265a71ba80..b0f6edd43a 100644
--- a/src/pretix/static/pretixpresale/scss/main.scss
+++ b/src/pretix/static/pretixpresale/scss/main.scss
@@ -88,6 +88,12 @@ body.loading .container {
filter: blur(2px);
}
+.big-rotating-icon {
+ -webkit-animation: fa-spin 8s infinite linear;
+ animation: fa-spin 8s infinite linear;
+ font-size: 120px;
+ color: $brand-primary;
+}
#loadingmodal, #ajaxerr {
position: fixed;
top: 0;
@@ -121,12 +127,6 @@ body.loading .container {
float: left;
width: 150px;
text-align: center;
- .big-rotating-icon {
- -webkit-animation: fa-spin 8s infinite linear;
- animation: fa-spin 8s infinite linear;
- font-size: 120px;
- color: $brand-primary;
- }
}
.modal-card-content {
margin-left: 160px;
diff --git a/src/requirements/production.txt b/src/requirements/production.txt
index 590411a7d6..7342eecd72 100644
--- a/src/requirements/production.txt
+++ b/src/requirements/production.txt
@@ -42,7 +42,7 @@ oauthlib==2.1.*
django-jsonfallback>=2.1.2
psycopg2-binary
# Stripe
-stripe==2.0.*
+stripe==2.29.*
# PayPal
paypalrestsdk==1.13.*
pycparser==2.13 # https://github.com/eliben/pycparser/issues/147
diff --git a/src/setup.py b/src/setup.py
index fc44434950..95604e1f92 100644
--- a/src/setup.py
+++ b/src/setup.py
@@ -125,7 +125,7 @@ setup(
'pycparser==2.13',
'django-redis==4.10.*',
'redis==3.2.*',
- 'stripe==2.0.*',
+ 'stripe==2.29.*',
'chardet<3.1.0,>=3.0.2',
'mt-940==3.2',
'django-i18nfield>=1.4.0',
diff --git a/src/tests/plugins/stripe/test_checkout.py b/src/tests/plugins/stripe/test_checkout.py
index 11f58ae1ad..5e0e89e97b 100644
--- a/src/tests/plugins/stripe/test_checkout.py
+++ b/src/tests/plugins/stripe/test_checkout.py
@@ -10,15 +10,26 @@ from pretix.testutils.sessions import add_cart_session, get_cart_session_key
class MockedCharge():
- def __init__(self):
- self.status = ''
- self.paid = False
- self.id = 'ch_123345345'
+ status = ''
+ paid = False
+ id = 'ch_123345345'
def refresh(self):
pass
+class Object():
+ pass
+
+
+class MockedPaymentintent():
+ status = ''
+ id = 'pi_1EUon12Tb35ankTnZyvC3SdE'
+ charges = Object()
+ charges.data = [MockedCharge()]
+ last_payment_error = None
+
+
@pytest.fixture
def env(client):
orga = Organizer.objects.create(name='CCC', slug='ccc')
@@ -41,16 +52,17 @@ def env(client):
@pytest.mark.django_db
def test_payment(env, monkeypatch):
- def charge_create(**kwargs):
+ def paymentintent_create(**kwargs):
assert kwargs['amount'] == 1337
assert kwargs['currency'] == 'eur'
- assert kwargs['source'] == 'tok_189fTT2eZvKYlo2CvJKzEzeu'
- c = MockedCharge()
+ assert kwargs['payment_method'] == 'pm_189fTT2eZvKYlo2CvJKzEzeu'
+ c = MockedPaymentintent()
c.status = 'succeeded'
- c.paid = True
- charge_create.called = True
+ c.charges.data[0].paid = True
+ setattr(paymentintent_create, 'called', True)
return c
- monkeypatch.setattr("stripe.Charge.create", charge_create)
+
+ monkeypatch.setattr("stripe.PaymentIntent.create", paymentintent_create)
client, ticket = env
session_key = get_cart_session_key(client, ticket.event)
@@ -62,17 +74,16 @@ def test_payment(env, monkeypatch):
client.post('/%s/%s/checkout/questions/' % (ticket.event.organizer.slug, ticket.event.slug), {
'email': 'admin@localhost'
}, follow=True)
- charge_create.called = False
+ paymentintent_create.called = False
response = client.post('/%s/%s/checkout/payment/' % (ticket.event.organizer.slug, ticket.event.slug), {
'payment': 'stripe',
- 'stripe_token': 'tok_189fTT2eZvKYlo2CvJKzEzeu',
+ 'payment_method': 'pm_189fTT2eZvKYlo2CvJKzEzeu',
'stripe_card_brand': 'visa',
'stripe_card_last4': '1234'
}, follow=True)
- assert not charge_create.called
+ assert not paymentintent_create.called
assert response.status_code == 200
assert 'alert-danger' not in response.rendered_content
response = client.post('/%s/%s/checkout/confirm/' % (ticket.event.organizer.slug, ticket.event.slug), {
}, follow=True)
- assert charge_create.called
assert response.status_code == 200
diff --git a/src/tests/plugins/stripe/test_provider.py b/src/tests/plugins/stripe/test_provider.py
index e5102cc116..6af5bc1858 100644
--- a/src/tests/plugins/stripe/test_provider.py
+++ b/src/tests/plugins/stripe/test_provider.py
@@ -46,39 +46,51 @@ class MockedRefunds():
class MockedCharge():
- def __init__(self):
- self.status = ''
- self.paid = False
- self.id = 'ch_123345345'
- self.refunds = MockedRefunds()
+ status = ''
+ paid = False
+ id = 'ch_123345345'
+ refunds = MockedRefunds()
def refresh(self):
pass
+class Object():
+ pass
+
+
+class MockedPaymentintent():
+ status = ''
+ id = 'pi_1EUon12Tb35ankTnZyvC3SdE'
+ charges = Object()
+ charges.data = [MockedCharge()]
+ last_payment_error = None
+
+
@pytest.mark.django_db
def test_perform_success(env, factory, monkeypatch):
event, order = env
- def charge_create(**kwargs):
+ def paymentintent_create(**kwargs):
assert kwargs['amount'] == 1337
assert kwargs['currency'] == 'eur'
- assert kwargs['source'] == 'tok_189fTT2eZvKYlo2CvJKzEzeu'
- c = MockedCharge()
+ assert kwargs['payment_method'] == 'pm_189fTT2eZvKYlo2CvJKzEzeu'
+ c = MockedPaymentintent()
c.status = 'succeeded'
- c.paid = True
+ c.charges.data[0].paid = True
return c
- monkeypatch.setattr("stripe.Charge.create", charge_create)
+ monkeypatch.setattr("stripe.PaymentIntent.create", paymentintent_create)
+
prov = StripeCC(event)
req = factory.post('/', {
- 'stripe_token': 'tok_189fTT2eZvKYlo2CvJKzEzeu',
+ 'stripe_payment_method_id': 'pm_189fTT2eZvKYlo2CvJKzEzeu',
'stripe_last4': '4242',
'stripe_brand': 'Visa'
})
req.session = {}
prov.checkout_prepare(req, {})
- assert 'payment_stripe_token' in req.session
+ assert 'payment_stripe_payment_method_id' in req.session
payment = order.payments.create(
provider='stripe_cc', amount=order.total
)
@@ -93,25 +105,25 @@ def test_perform_success_zero_decimal_currency(env, factory, monkeypatch):
event.currency = 'JPY'
event.save()
- def charge_create(**kwargs):
+ def paymentintent_create(**kwargs):
assert kwargs['amount'] == 13
assert kwargs['currency'] == 'jpy'
- assert kwargs['source'] == 'tok_189fTT2eZvKYlo2CvJKzEzeu'
- c = MockedCharge()
+ assert kwargs['payment_method'] == 'pm_189fTT2eZvKYlo2CvJKzEzeu'
+ c = MockedPaymentintent()
c.status = 'succeeded'
- c.paid = True
+ c.charges.data[0].paid = True
return c
- monkeypatch.setattr("stripe.Charge.create", charge_create)
+ monkeypatch.setattr("stripe.PaymentIntent.create", paymentintent_create)
prov = StripeCC(event)
req = factory.post('/', {
- 'stripe_token': 'tok_189fTT2eZvKYlo2CvJKzEzeu',
+ 'stripe_payment_method_id': 'pm_189fTT2eZvKYlo2CvJKzEzeu',
'stripe_last4': '4242',
'stripe_brand': 'Visa'
})
req.session = {}
prov.checkout_prepare(req, {})
- assert 'payment_stripe_token' in req.session
+ assert 'payment_stripe_payment_method_id' in req.session
payment = order.payments.create(
provider='stripe_cc', amount=order.total
)
@@ -136,7 +148,7 @@ def test_perform_card_error(env, factory, monkeypatch):
})
req.session = {}
prov.checkout_prepare(req, {})
- assert 'payment_stripe_token' in req.session
+ assert 'payment_stripe_payment_method_id' in req.session
with pytest.raises(PaymentException):
payment = order.payments.create(
provider='stripe_cc', amount=order.total
@@ -162,7 +174,7 @@ def test_perform_stripe_error(env, factory, monkeypatch):
})
req.session = {}
prov.checkout_prepare(req, {})
- assert 'payment_stripe_token' in req.session
+ assert 'payment_stripe_payment_method_id' in req.session
with pytest.raises(PaymentException):
payment = order.payments.create(
provider='stripe_cc', amount=order.total
@@ -192,7 +204,7 @@ def test_perform_failed(env, factory, monkeypatch):
})
req.session = {}
prov.checkout_prepare(req, {})
- assert 'payment_stripe_token' in req.session
+ assert 'payment_stripe_payment_method_id' in req.session
with pytest.raises(PaymentException):
payment = order.payments.create(
provider='stripe_cc', amount=order.total