forked from CGM_Public/pretix_original
Stripe SCA (#1275)
* Stripe SCA - Upgrade to latest Stripe API - Deprecate Stripe Checkout for CC - Migrate CC payments to Payment Intents * Move SCA to its own view * Handle CardErrors for PaymentIntents * Abilty to handle charge webhooks with PaymentIntents * Better handling of Stripe References * Fix Stripe Tests * Move SCA page into orderlayout; perform iFrame SCA * Handle disputes and pi-webhooks better, fill more into ReferencedStripeObject * Optionally pass prefetched PaymentIntent to handle-func * Fix style * Send message to window.parent not window.top (widget compatibility) * More accurate loading message * Show a cog on sca_return.html. On a good internet connection, you barely see it, but on a bad one… * Robust error handling * If it's a method and used like a method, let's actually call it like a method! * Remove logging statement * Fix JavaScript interference with other frame events * Use 4:3 aspect ratio, but at least 600px * Adjust to django_scopes
This commit is contained in:
committed by
Raphael Michel
parent
b727207e79
commit
446cf68377
@@ -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):
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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%;
|
||||
|
||||
@@ -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("<div class='alert alert-danger'>" + result.error.message + "</div>");
|
||||
$(".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();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
</div>
|
||||
</noscript>
|
||||
|
||||
{% if request.session.payment_stripe_token %}
|
||||
{% if request.session.payment_stripe_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.
|
||||
@@ -58,7 +58,7 @@
|
||||
Stripe and never touches our servers.
|
||||
{% endblocktrans %}
|
||||
<input type="hidden" name="stripe_total" value="{{ total }}" id="stripe_total"/>
|
||||
<input type="hidden" name="stripe_token" value="{{ request.session.payment_stripe_token }}" id="stripe_token"/>
|
||||
<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 }}"
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
{% load i18n %}
|
||||
|
||||
<div class="form-horizontal stripe-container" id="stripe-checkout">
|
||||
<noscript>
|
||||
<div class="alert alert-warning">
|
||||
{% trans "For a credit card payment, please turn on JavaScript." %}
|
||||
</div>
|
||||
</noscript>
|
||||
{% if request.session.payment_stripe_token %}
|
||||
<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>
|
||||
<dt>{% trans "Card number" %}</dt>
|
||||
<dd>
|
||||
**** **** **** <span id="stripe_card_last4_display">{{ request.session.payment_stripe_last4 }}</span>
|
||||
<button class="btn btn-xs btn-default" id="stripe_other_card" type="button">
|
||||
{% trans "Use a different card" %}
|
||||
</button>
|
||||
</dd>
|
||||
</dl>
|
||||
<p>
|
||||
</p>
|
||||
{% else %}
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
Please continue below to start the credit card payment.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
{% endif %}
|
||||
<p class="help-block">
|
||||
{% 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 %}
|
||||
</p>
|
||||
<input type="hidden" name="stripe_token" value="{{ request.session.payment_stripe_token }}" id="stripe_token"/>
|
||||
<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="organizer_name" value="{{ event.organizer.name }}"/>
|
||||
<input type="hidden" id="event_name" value="{{ event.name }}"/>
|
||||
<input type="hidden" id="stripe_currency" value="{{ event.currency }}"/>
|
||||
<input type="hidden" id="event_name" value="{{ event.name }}"/>
|
||||
{% if order %}
|
||||
<input type="hidden" id="stripe_email" value="{{ order.email }}"/>
|
||||
{% else %}
|
||||
<input type="hidden" id="stripe_email" value="{{ request.session.email }}"/>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,23 @@
|
||||
{% load i18n %}
|
||||
|
||||
{% load eventurl %}
|
||||
{% if payment.state == "pending" %}
|
||||
<p>{% 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 %}</p>
|
||||
{% elif payment.state == "created" and payment_info.status == "requires_action" %}
|
||||
<p>{% blocktrans trimmed %}
|
||||
You need to confirm your payment. Please click the link below to do so or start a new payment.
|
||||
{% endblocktrans %}
|
||||
<div class="text-right">
|
||||
<a href="{% eventurl event "plugins:stripe:sca" order=order.code payment=payment.pk hash=payment_hash %}"
|
||||
class="btn btn-primary">
|
||||
{% trans "Confirm payment" %}
|
||||
</a>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</p>
|
||||
|
||||
{% else %}
|
||||
<p>{% blocktrans trimmed %}
|
||||
The payment transaction could not be completed for the following reason:
|
||||
@@ -16,4 +29,4 @@
|
||||
{% trans "Unknown reason" %}
|
||||
{% endif %}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
@@ -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 %}
|
||||
<script type="text/plain" id="stripe_payment_intent_client_secret">{{ payment_intent_client_secret }}</script>
|
||||
<script type="text/plain" id="stripe_payment_intent_next_action_redirect_url">{{ payment_intent_next_action_redirect_url }}</script>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div class="panel panel-primary">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
{% blocktrans trimmed with code=order.code %}
|
||||
Confirm payment: {{ code }}
|
||||
{% endblocktrans %}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="panel-body embed-responsive embed-responsive-sca" id="scacontainer">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="row checkout-button-row">
|
||||
<div class="col-md-4">
|
||||
<a class="btn btn-block btn-default btn-lg"
|
||||
href="{% eventurl request.event "presale:event.order" secret=order.secret order=order.code %}">
|
||||
{% trans "Cancel" %}
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-4 col-md-offset-4">
|
||||
<a class="btn btn-block btn-primary btn-lg hidden"
|
||||
href="{% eventurl request.event "presale:event.order" secret=order.secret order=order.code %}"
|
||||
id="continuebutton">
|
||||
{% trans "Continue" %}
|
||||
</a>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -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 %}
|
||||
<script type="text/plain" id="stripe_payment_intent_SCA_status">3DS-authentication-complete</script>
|
||||
<script type="text/plain" id="order_status">{{ order.status }}</script>
|
||||
{% endblock %}
|
||||
{% block page %}
|
||||
<div class="text-center">
|
||||
<i class="fa fa-cog big-rotating-icon"></i>
|
||||
</div>
|
||||
<h2 class="text-center">
|
||||
{% trans "Confirming your payment…" %}
|
||||
</h2>
|
||||
{% endblock %}
|
||||
@@ -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<order>[^/]+)/(?P<hash>[^/]+)/(?P<payment>[0-9]+)/$', ReturnView.as_view(), name='return'),
|
||||
url(r'^sca/(?P<order>[^/]+)/(?P<hash>[^/]+)/(?P<payment>[0-9]+)/$', ScaView.as_view(), name='sca'),
|
||||
url(r'^sca/(?P<order>[^/]+)/(?P<hash>[^/]+)/(?P<payment>[0-9]+)/return/$', ScaReturnView.as_view(), name='sca.return'),
|
||||
])),
|
||||
]
|
||||
|
||||
|
||||
@@ -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})
|
||||
|
||||
Reference in New Issue
Block a user