Stripe: Add Support for Affirm Pay Later (#3737)

Co-authored-by: Raphael Michel <michel@rami.io>
This commit is contained in:
Martin Gross
2023-11-23 13:02:29 +01:00
committed by GitHub
parent 1dea908152
commit 7648be7937
7 changed files with 144 additions and 21 deletions

View File

@@ -123,7 +123,7 @@ logger = logging.getLogger('pretix.plugins.stripe')
# - Mexico Bank Transfer: ✗ # - Mexico Bank Transfer: ✗
# #
# Buy now, pay later # Buy now, pay later
# - Affirm: # - Affirm:
# - Afterpay/Clearpay: ✗ # - Afterpay/Clearpay: ✗
# - Klarna: ✗ # - Klarna: ✗
# #
@@ -431,6 +431,16 @@ class StripeSettingsHolder(BasePaymentProvider):
'before work properly.'), 'before work properly.'),
required=False, required=False,
)), )),
('method_affirm',
forms.BooleanField(
label=_('Affirm'),
disabled=self.event.currency not in ['USD', 'CAD'],
help_text=' '.join([
str(_('Needs to be enabled in your Stripe account first.')),
str(_('Only available for payments between $50 and $30,000.'))
]),
required=False,
)),
] + extra_fields + list(super().settings_form_fields.items()) + moto_settings ] + extra_fields + list(super().settings_form_fields.items()) + moto_settings
) )
if not self.settings.connect_client_id or self.settings.secret_key: if not self.settings.connect_client_id or self.settings.secret_key:
@@ -866,6 +876,7 @@ class StripeMethod(BasePaymentProvider):
class StripePaymentIntentMethod(StripeMethod): class StripePaymentIntentMethod(StripeMethod):
identifier = '' identifier = ''
method = '' method = ''
redirect_action_handling = 'iframe' # or redirect
def payment_is_valid_session(self, request): def payment_is_valid_session(self, request):
return request.session.get('payment_stripe_{}_payment_method_id'.format(self.method), '') != '' return request.session.get('payment_stripe_{}_payment_method_id'.format(self.method), '') != ''
@@ -896,6 +907,9 @@ class StripePaymentIntentMethod(StripeMethod):
try: try:
if self.payment_is_valid_session(request): if self.payment_is_valid_session(request):
payment_method_id = request.session.get('payment_stripe_{}_payment_method_id'.format(self.method), None)
idempotency_key_seed = payment_method_id if payment_method_id is not None else payment.full_id
params = {} params = {}
params.update(self._connect_kwargs(payment)) params.update(self._connect_kwargs(payment))
params.update(self.api_kwargs) params.update(self.api_kwargs)
@@ -913,7 +927,7 @@ class StripePaymentIntentMethod(StripeMethod):
intent = stripe.PaymentIntent.create( intent = stripe.PaymentIntent.create(
amount=self._get_amount(payment), amount=self._get_amount(payment),
currency=self.event.currency.lower(), currency=self.event.currency.lower(),
payment_method=request.session['payment_stripe_{}_payment_method_id'.format(self.method)], payment_method=payment_method_id,
payment_method_types=[self.method], payment_method_types=[self.method],
confirmation_method='manual', confirmation_method='manual',
confirm=True, confirm=True,
@@ -928,7 +942,7 @@ class StripePaymentIntentMethod(StripeMethod):
'code': payment.order.code 'code': payment.order.code
}, },
# TODO: Is this sufficient? # TODO: Is this sufficient?
idempotency_key=str(self.event.id) + payment.order.code + request.session['payment_stripe_{}_payment_method_id'.format(self.method)], idempotency_key=str(self.event.id) + payment.order.code + idempotency_key_seed,
return_url=build_absolute_uri(self.event, 'plugins:stripe:sca.return', kwargs={ return_url=build_absolute_uri(self.event, 'plugins:stripe:sca.return', kwargs={
'order': payment.order.code, 'order': payment.order.code,
'payment': payment.pk, 'payment': payment.pk,
@@ -1288,6 +1302,49 @@ class StripeSEPADirectDebit(StripePaymentIntentMethod):
del request.session['payment_stripe_sepa_debit_{}'.format(field)] del request.session['payment_stripe_sepa_debit_{}'.format(field)]
class StripeAffirm(StripePaymentIntentMethod):
identifier = 'stripe_affirm'
verbose_name = _('Affirm via Stripe')
public_name = _('Affirm')
method = 'affirm'
redirect_action_handling = 'redirect'
def payment_is_valid_session(self, request):
# Affirm does not have a payment_method_id, so we set it manually to None during checkout.
# But we still need to check for its presence here.
if 'payment_stripe_{}_payment_method_id'.format(self.method) in request.session:
return True
return False
def checkout_prepare(self, request, cart):
# Affirm does not have a payment_method_id, so we set it manually to None during checkout, so that we can
# verify later on if we are in or outside the checkout process.
request.session['payment_stripe_{}_payment_method_id'.format(self.method)] = None
return True
def is_allowed(self, request: HttpRequest, total: Decimal=None) -> bool:
return Decimal(50.00) <= total <= Decimal(30000.00) and super().is_allowed(request, total)
def order_change_allowed(self, order: Order, request: HttpRequest=None) -> bool:
return Decimal(50.00) <= order.pending_sum <= Decimal(30000.00) and super().order_change_allowed(order, request)
def _payment_intent_kwargs(self, request, payment):
return {
'payment_method_data': {
'type': 'affirm',
}
}
def payment_form_render(self, request, total, order=None) -> str:
template = get_template('pretixplugins/stripe/checkout_payment_form_affirm.html')
ctx = {
'request': request,
'event': self.event,
'total': self._decimal_to_int(total),
}
return template.render(ctx)
class StripeGiropay(StripeMethod): class StripeGiropay(StripeMethod):
identifier = 'stripe_giropay' identifier = 'stripe_giropay'
verbose_name = _('giropay via Stripe') verbose_name = _('giropay via Stripe')

View File

@@ -45,15 +45,16 @@ from pretix.presale.signals import html_head, process_response
@receiver(register_payment_providers, dispatch_uid="payment_stripe") @receiver(register_payment_providers, dispatch_uid="payment_stripe")
def register_payment_provider(sender, **kwargs): def register_payment_provider(sender, **kwargs):
from .payment import ( from .payment import (
StripeAlipay, StripeBancontact, StripeCC, StripeEPS, StripeGiropay, StripeAffirm, StripeAlipay, StripeBancontact, StripeCC, StripeEPS,
StripeIdeal, StripeMultibanco, StripePrzelewy24, StripeSEPADirectDebit, StripeGiropay, StripeIdeal, StripeMultibanco, StripePrzelewy24,
StripeSettingsHolder, StripeSofort, StripeWeChatPay, StripeSEPADirectDebit, StripeSettingsHolder, StripeSofort,
StripeWeChatPay,
) )
return [ return [
StripeSettingsHolder, StripeCC, StripeGiropay, StripeIdeal, StripeAlipay, StripeBancontact, StripeSettingsHolder, StripeCC, StripeGiropay, StripeIdeal, StripeAlipay, StripeBancontact,
StripeSofort, StripeEPS, StripeMultibanco, StripePrzelewy24, StripeWeChatPay, StripeSofort, StripeEPS, StripeMultibanco, StripePrzelewy24, StripeWeChatPay,
StripeSEPADirectDebit, StripeSEPADirectDebit, StripeAffirm,
] ]

View File

@@ -6,6 +6,7 @@ var pretixstripe = {
elements: null, elements: null,
card: null, card: null,
sepa: null, sepa: null,
affirm: null,
paymentRequest: null, paymentRequest: null,
paymentRequestButton: null, paymentRequestButton: null,
@@ -163,6 +164,14 @@ var pretixstripe = {
$('.stripe-container').closest("form").find(".checkout-button-row .btn-primary").prop("disabled", false); $('.stripe-container').closest("form").find(".checkout-button-row .btn-primary").prop("disabled", false);
}); });
} }
if ($("#stripe-affirm").length) {
pretixstripe.affirm = pretixstripe.elements.create('affirmMessage', {
'amount': parseInt($("#stripe_affirm_total").val()),
'currency': $("#stripe_affirm_currency").val(),
});
pretixstripe.affirm.mount('#stripe-affirm');
}
if ($("#stripe-payment-request-button").length && pretixstripe.paymentRequest != null) { if ($("#stripe-payment-request-button").length && pretixstripe.paymentRequest != null) {
pretixstripe.paymentRequestButton = pretixstripe.elements.create('paymentRequestButton', { pretixstripe.paymentRequestButton = pretixstripe.elements.create('paymentRequestButton', {
paymentRequest: pretixstripe.paymentRequest, paymentRequest: pretixstripe.paymentRequest,
@@ -207,24 +216,44 @@ var pretixstripe = {
} }
}); });
}, },
'handleCardActioniFrame': function (payment_intent_next_action_redirect_url) { 'handlePaymentRedirectAction': function (payment_intent_next_action_redirect_url) {
waitingDialog.show(gettext("Contacting your bank …")); waitingDialog.show(gettext("Contacting your bank …"));
let iframe = document.createElement('iframe');
iframe.src = payment_intent_next_action_redirect_url; let payment_intent_redirect_action_handling = $.trim($("#stripe_payment_intent_redirect_action_handling").html());
iframe.className = 'embed-responsive-item'; if (payment_intent_redirect_action_handling === 'iframe') {
$('#scacontainer').append(iframe); let iframe = document.createElement('iframe');
$('#scacontainer iframe').on("load", function () { iframe.src = payment_intent_next_action_redirect_url;
waitingDialog.hide(); iframe.className = 'embed-responsive-item';
}); $('#scacontainer').append(iframe);
$('#scacontainer iframe').on("load", function () {
waitingDialog.hide();
});
} else if (payment_intent_redirect_action_handling === 'redirect') {
window.location.href = payment_intent_next_action_redirect_url;
}
} }
}; };
$(function () { $(function () {
if ($("#stripe_payment_intent_SCA_status").length) { if ($("#stripe_payment_intent_SCA_status").length) {
window.parent.postMessage('3DS-authentication-complete.' + $.trim($("#order_status").html()), '*'); let payment_intent_redirect_action_handling = $.trim($("#stripe_payment_intent_redirect_action_handling").html());
return; let order_status = $.trim($("#order_status").html());
let order_url = $.trim($("#order_url").html())
if (payment_intent_redirect_action_handling === 'iframe') {
window.parent.postMessage('3DS-authentication-complete.' + order_status, '*');
return;
} else if (payment_intent_redirect_action_handling === 'redirect') {
waitingDialog.show(gettext("Confirming your payment …"));
if (order_status === 'p') {
window.location.href = order_url + '?paid=yes';
} else {
window.location.href = order_url;
}
}
} else if ($("#stripe_payment_intent_next_action_redirect_url").length) { } 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()); let payment_intent_next_action_redirect_url = $.trim($("#stripe_payment_intent_next_action_redirect_url").html());
pretixstripe.handleCardActioniFrame(payment_intent_next_action_redirect_url); pretixstripe.handlePaymentRedirectAction(payment_intent_next_action_redirect_url);
} else if ($("#stripe_payment_intent_client_secret").length) { } else if ($("#stripe_payment_intent_client_secret").length) {
let payment_intent_client_secret = $.trim($("#stripe_payment_intent_client_secret").html()); let payment_intent_client_secret = $.trim($("#stripe_payment_intent_client_secret").html());
pretixstripe.handleCardAction(payment_intent_client_secret); pretixstripe.handleCardAction(payment_intent_client_secret);
@@ -247,11 +276,15 @@ $(function () {
if (!$(".stripe-container").length) if (!$(".stripe-container").length)
return; return;
if ($("input[name=payment][value=stripe]").is(':checked') || $("input[name=payment][value=stripe_sepa_debit]").is(':checked') || $(".payment-redo-form").length) { if (
$("input[name=payment][value=stripe]").is(':checked')
|| $("input[name=payment][value=stripe_sepa_debit]").is(':checked')
|| $("input[name=payment][value=stripe_affirm]").is(':checked')
|| $(".payment-redo-form").length) {
pretixstripe.load(); pretixstripe.load();
} else { } else {
$("input[name=payment]").change(function () { $("input[name=payment]").change(function () {
if (['stripe', 'stripe_sepa_debit'].indexOf($(this).val()) > -1) { if (['stripe', 'stripe_sepa_debit', 'stripe_affirm'].indexOf($(this).val()) > -1) {
pretixstripe.load(); pretixstripe.load();
} }
}) })

View File

@@ -0,0 +1,20 @@
{% load i18n %}
{% load bootstrap3 %}
<div class="form-horizontal stripe-container">
<div id="stripe-affirm">
<span class="fa fa-spinner fa-spin"></span>
<!-- a Stripe Element will be inserted here. -->
</div>
<p class="help-block">
{% blocktrans trimmed %}
After you submitted your order, we will redirect you to the payment service provider to complete your
payment.
You will then be redirected back here to get your tickets.
{% endblocktrans %}
</p>
<input type="hidden" name="stripe_affirm_total" value="{{ total }}" id="stripe_affirm_total"/>
<input type="hidden" id="stripe_affirm_currency" value="{{ event.currency }}"/>
</div>

View File

@@ -8,6 +8,7 @@
{% include "pretixplugins/stripe/presale_head.html" with settings=stripe_settings %} {% 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_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> <script type="text/plain" id="stripe_payment_intent_next_action_redirect_url">{{ payment_intent_next_action_redirect_url }}</script>
<script type="text/plain" id="stripe_payment_intent_redirect_action_handling">{{ payment_intent_redirect_action_handling }}</script>
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div class="panel panel-primary"> <div class="panel panel-primary">

View File

@@ -8,7 +8,9 @@
{{ block.super }} {{ block.super }}
{% include "pretixplugins/stripe/presale_head.html" with settings=stripe_settings %} {% 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="stripe_payment_intent_SCA_status">3DS-authentication-complete</script>
<script type="text/plain" id="stripe_payment_intent_redirect_action_handling">{{ payment_intent_redirect_action_handling }}</script>
<script type="text/plain" id="order_status">{{ order.status }}</script> <script type="text/plain" id="order_status">{{ order.status }}</script>
<script type="text/plain" id="order_url">{{ order_url }}</script>
{% endblock %} {% endblock %}
{% block page %} {% block page %}
<div class="text-center"> <div class="text-center">

View File

@@ -599,6 +599,7 @@ class ScaView(StripeOrderView, View):
ctx['payment_intent_client_secret'] = intent.client_secret ctx['payment_intent_client_secret'] = intent.client_secret
elif intent.next_action.type == 'redirect_to_url': elif intent.next_action.type == 'redirect_to_url':
ctx['payment_intent_next_action_redirect_url'] = intent.next_action.redirect_to_url['url'] ctx['payment_intent_next_action_redirect_url'] = intent.next_action.redirect_to_url['url']
ctx['payment_intent_redirect_action_handling'] = prov.redirect_action_handling
r = render(request, 'pretixplugins/stripe/sca.html', ctx) r = render(request, 'pretixplugins/stripe/sca.html', ctx)
r._csp_ignore = True r._csp_ignore = True
@@ -623,8 +624,16 @@ class ScaReturnView(StripeOrderView, View):
messages.error(request, str(e)) messages.error(request, str(e))
self.order.refresh_from_db() self.order.refresh_from_db()
ctx = {
'order': self.order,
'payment_intent_redirect_action_handling': prov.redirect_action_handling,
'order_url': eventreverse(self.request.event, 'presale:event.order', kwargs={
'order': self.order.code,
'secret': self.order.secret
}),
}
return render(request, 'pretixplugins/stripe/sca_return.html', {'order': self.order}) return render(request, 'pretixplugins/stripe/sca_return.html', ctx)
class OrganizerSettingsFormView(DecoupleMixin, OrganizerDetailViewMixin, AdministratorPermissionRequiredMixin, FormView): class OrganizerSettingsFormView(DecoupleMixin, OrganizerDetailViewMixin, AdministratorPermissionRequiredMixin, FormView):