mirror of
https://github.com/pretix/pretix.git
synced 2026-05-04 15:04:03 +00:00
Stripe: ApplePay/Payment Request Button (#988)
As discussed, this is a WIP for integrating Stripe's Payment Request Buttons (with also includes the ApplePay-Button on iOS-devices). Todos: - [x] Payment Request Button is still displayed, even when a card has already been tokenized (when going back in the order-flow) - [x] The domains used need to be verified using the Stripe API to enable ApplePay: https://stripe.com/docs/stripe-js/elements/payment-request-button#verifying-your-domain-with-apple-pay - [x] Migration: Get the account-country for existing Stripe Connect users - [x] Migration: Verify the domains using the above mentioned API for existing users - [x] Converting the chargeable amount is not right for non-decimal currencies like JPY Other considerations: - On iOS-devices using Safari (probably also on MacBooks, etc. - not tested), the [regular payment request button](https://user-images.githubusercontent.com/157270/38515749-f53f8392-3be9-11e8-8917-61ef78dd354a.png) is automatically replaced with a [buy with Apple Pay button](https://docs-assets.developer.apple.com/published/094d0eb90e/988c36a8-a43c-4ff9-85ef-beda16c4b7c9.png). - On all other platforms, the generic payment request button is displayed. Even if the device supports a specific payment provider like Google Pay, Microsoft Wallet, Samsung Pay, etc., the generic button will first offer the cards saved within the webbrowser in addition to the other payment methods. Only upon selecting the specific payment provider like GPay, the corresponding payment flow is started. - Right now, the rendering of the payment button is completely in the hands of Stripe. Once pretix takes on the task of doing this, we should try to detect if the browser supports well known payment methods like GPay in addition to the browser-saved cards. If that's the case, we should add the corresponding marks onto the "Pay Now"-Button (like [this](https://developers.google.com/pay/api/images/brand-guidelines/google-pay-mark.png), [this](https://assets.pcmag.com/media/images/490984-samsung-pay.png?width=1600&height=900), or [this](https://www.firstffcu.com/images/MS-Wallet_stacked_rgb_grey.png)), so the customer can identify the purpose of the button easier. - [x] Also, all of this is still based against the pretix 1.x codebase ;-)
This commit is contained in:
committed by
Raphael Michel
parent
673a4e6805
commit
a4ced609cd
@@ -17,7 +17,7 @@ class StripeApp(AppConfig):
|
||||
"via Stripe")
|
||||
|
||||
def ready(self):
|
||||
from . import signals # NOQA
|
||||
from . import signals, tasks # NOQA
|
||||
|
||||
@cached_property
|
||||
def compatibility_errors(self):
|
||||
|
||||
0
src/pretix/plugins/stripe/management/__init__.py
Normal file
0
src/pretix/plugins/stripe/management/__init__.py
Normal file
@@ -0,0 +1,29 @@
|
||||
import stripe
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from pretix.base.models import Event
|
||||
from pretix.base.settings import GlobalSettingsObject
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Detect country for Stripe Connect accounts connected with pretix 2.0 (required for payment request buttons)"
|
||||
|
||||
def handle(self, *args, **options):
|
||||
cache = {}
|
||||
gs = GlobalSettingsObject()
|
||||
api_key = gs.settings.payment_stripe_connect_secret_key or gs.settings.payment_stripe_connect_test_secret_key
|
||||
if not api_key:
|
||||
self.stderr.write(self.style.ERROR("Stripe Connect is not set up!"))
|
||||
return
|
||||
|
||||
for e in Event.objects.filter(plugins__icontains="pretix.plugins.stripe"):
|
||||
uid = e.settings.payment_stripe_connect_user_id
|
||||
if uid and not e.settings.payment_stripe_merchant_country:
|
||||
if uid in cache:
|
||||
e.settings.payment_stripe_merchant_country = cache[uid]
|
||||
else:
|
||||
account = stripe.Account.retrieve(
|
||||
uid,
|
||||
api_key=api_key
|
||||
)
|
||||
e.settings.payment_stripe_merchant_country = cache[uid] = account.get('country')
|
||||
@@ -0,0 +1,21 @@
|
||||
# Generated by Django 2.1 on 2018-08-12 14:51
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('stripe', '0002_referencedstripeobject_payment'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='RegisteredApplePayDomain',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('domain', models.CharField(max_length=190)),
|
||||
('account', models.CharField(max_length=190)),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -5,3 +5,8 @@ class ReferencedStripeObject(models.Model):
|
||||
reference = models.CharField(max_length=190, db_index=True, unique=True)
|
||||
order = models.ForeignKey('pretixbase.Order', on_delete=models.CASCADE)
|
||||
payment = models.ForeignKey('pretixbase.OrderPayment', null=True, blank=True, on_delete=models.CASCADE)
|
||||
|
||||
|
||||
class RegisteredApplePayDomain(models.Model):
|
||||
domain = models.CharField(max_length=190)
|
||||
account = models.CharField(max_length=190)
|
||||
|
||||
@@ -15,6 +15,7 @@ from django.urls import reverse
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.http import urlquote
|
||||
from django.utils.translation import pgettext, ugettext, ugettext_lazy as _
|
||||
from django_countries import countries
|
||||
|
||||
from pretix import __version__
|
||||
from pretix.base.decimal import round_decimal
|
||||
@@ -25,7 +26,12 @@ from pretix.base.settings import SettingsSandbox
|
||||
from pretix.helpers.urls import build_absolute_uri as build_global_uri
|
||||
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||
from pretix.plugins.stripe.forms import StripeKeyValidator
|
||||
from pretix.plugins.stripe.models import ReferencedStripeObject
|
||||
from pretix.plugins.stripe.models import (
|
||||
ReferencedStripeObject, RegisteredApplePayDomain,
|
||||
)
|
||||
from pretix.plugins.stripe.tasks import (
|
||||
get_stripe_account_key, stripe_verify_domain,
|
||||
)
|
||||
|
||||
logger = logging.getLogger('pretix.plugins.stripe')
|
||||
|
||||
@@ -109,6 +115,9 @@ class StripeSettingsHolder(BasePaymentProvider):
|
||||
else:
|
||||
return {}
|
||||
else:
|
||||
allcountries = list(countries)
|
||||
allcountries.insert(0, ('', _('Select country')))
|
||||
|
||||
fields = [
|
||||
('publishable_key',
|
||||
forms.CharField(
|
||||
@@ -128,6 +137,13 @@ class StripeSettingsHolder(BasePaymentProvider):
|
||||
StripeKeyValidator(['sk_', 'rk_']),
|
||||
),
|
||||
)),
|
||||
('merchant_country',
|
||||
forms.ChoiceField(
|
||||
choices=allcountries,
|
||||
label=_('Merchant country'),
|
||||
help_text=_('The country in which your Stripe-account is registred in. Usually, this is your '
|
||||
'country of residence.'),
|
||||
)),
|
||||
]
|
||||
d = OrderedDict(
|
||||
fields + [
|
||||
@@ -235,9 +251,12 @@ class StripeMethod(BasePaymentProvider):
|
||||
places = settings.CURRENCY_PLACES.get(self.event.currency, 2)
|
||||
return round_decimal(float(cents) / (10 ** places), self.event.currency)
|
||||
|
||||
def _get_amount(self, payment):
|
||||
def _decimal_to_int(self, amount):
|
||||
places = settings.CURRENCY_PLACES.get(self.event.currency, 2)
|
||||
return int(payment.amount * 10 ** places)
|
||||
return int(amount * 10 ** places)
|
||||
|
||||
def _get_amount(self, payment):
|
||||
return self._decimal_to_int(payment.amount)
|
||||
|
||||
@property
|
||||
def api_kwargs(self):
|
||||
@@ -510,7 +529,11 @@ class StripeCC(StripeMethod):
|
||||
public_name = _('Credit card')
|
||||
method = 'cc'
|
||||
|
||||
def payment_form_render(self, request) -> str:
|
||||
def payment_form_render(self, request, total) -> str:
|
||||
account = get_stripe_account_key(self)
|
||||
if not RegisteredApplePayDomain.objects.filter(account=account, domain=request.host).exists():
|
||||
stripe_verify_domain.apply_async(args=(self.event.pk, request.host))
|
||||
|
||||
ui = self.settings.get('ui', default='pretix')
|
||||
if ui == 'checkout':
|
||||
template = get_template('pretixplugins/stripe/checkout_payment_form_stripe_checkout.html')
|
||||
@@ -519,6 +542,7 @@ class StripeCC(StripeMethod):
|
||||
ctx = {
|
||||
'request': request,
|
||||
'event': self.event,
|
||||
'total': self._decimal_to_int(total),
|
||||
'settings': self.settings,
|
||||
}
|
||||
return template.render(ctx)
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
.sep {
|
||||
}
|
||||
|
||||
.sepText {
|
||||
width: 75px;
|
||||
background: #FFFFFF;
|
||||
margin: -15px 0 0 -38px;
|
||||
padding: 5px 0;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.hr {
|
||||
width:2px;
|
||||
height:100px;
|
||||
background-color: #DDDDDD;
|
||||
position:inherit;
|
||||
top:0px;
|
||||
left:50%;
|
||||
z-index:10;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 999px) {
|
||||
.hr {
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
left: 0px;
|
||||
margin: 15px 0 15px 0;
|
||||
}
|
||||
.sepText {
|
||||
left: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 999px) {
|
||||
.row.equal {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
|
||||
.vcenter {
|
||||
margin: auto;
|
||||
}
|
||||
@@ -5,6 +5,8 @@ var pretixstripe = {
|
||||
stripe: null,
|
||||
elements: null,
|
||||
card: null,
|
||||
paymentRequest: null,
|
||||
paymentRequestButton: null,
|
||||
|
||||
'cc_request': function () {
|
||||
waitingDialog.show(gettext("Contacting Stripe …"));
|
||||
@@ -37,8 +39,47 @@ var pretixstripe = {
|
||||
url: 'https://js.stripe.com/v3/',
|
||||
dataType: 'script',
|
||||
success: function () {
|
||||
pretixstripe.stripe = Stripe($.trim($("#stripe_pubkey").html()));
|
||||
if ($.trim($("#stripe_connectedAccountId").html())) {
|
||||
pretixstripe.stripe = Stripe($.trim($("#stripe_pubkey").html()), {
|
||||
stripeAccount: $.trim($("#stripe_connectedAccountId").html())
|
||||
});
|
||||
} else {
|
||||
pretixstripe.stripe = Stripe($.trim($("#stripe_pubkey").html()));
|
||||
}
|
||||
pretixstripe.elements = pretixstripe.stripe.elements();
|
||||
if ($.trim($("#stripe_merchantcountry").html()) !== "") {
|
||||
try {
|
||||
pretixstripe.paymentRequest = pretixstripe.stripe.paymentRequest({
|
||||
country: $("#stripe_merchantcountry").html(),
|
||||
currency: $("#stripe_currency").val().toLowerCase(),
|
||||
total: {
|
||||
label: gettext('Total'),
|
||||
amount: parseInt($("#stripe_total").val())
|
||||
},
|
||||
displayItems: [],
|
||||
requestPayerName: false,
|
||||
requestPayerEmail: false,
|
||||
requestPayerPhone: false,
|
||||
requestShipping: false,
|
||||
});
|
||||
|
||||
pretixstripe.paymentRequest.on('token', function (ev) {
|
||||
ev.complete('success');
|
||||
|
||||
var $form = $("#stripe_token").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);
|
||||
// and submit
|
||||
$form.get(0).submit();
|
||||
});
|
||||
} catch {
|
||||
pretixstripe.paymentRequest = null;
|
||||
}
|
||||
} else {
|
||||
pretixstripe.paymentRequest = null;
|
||||
}
|
||||
if ($("#stripe-card").length) {
|
||||
pretixstripe.card = pretixstripe.elements.create('card', {
|
||||
'style': {
|
||||
@@ -63,6 +104,27 @@ var pretixstripe = {
|
||||
});
|
||||
pretixstripe.card.mount("#stripe-card");
|
||||
}
|
||||
if ($("#stripe-payment-request-button").length && pretixstripe.paymentRequest != null) {
|
||||
pretixstripe.paymentRequestButton = pretixstripe.elements.create('paymentRequestButton', {
|
||||
paymentRequest: pretixstripe.paymentRequest,
|
||||
});
|
||||
|
||||
pretixstripe.paymentRequest.canMakePayment().then(function(result) {
|
||||
if (result) {
|
||||
pretixstripe.paymentRequestButton.mount('#stripe-payment-request-button');
|
||||
$('#stripe-payment-request-button').parent().hide();
|
||||
$('#stripe-payment-request-button').parent().next("div").hide();
|
||||
$('#stripe-payment-request-button').parent().removeClass("hidden");
|
||||
$('#stripe-payment-request-button').parent().next("div").removeClass("hidden");
|
||||
$('#stripe-payment-request-button').parent().show(500);
|
||||
$('#stripe-payment-request-button').parent().next("div").show(500);
|
||||
} else {
|
||||
$('#stripe-payment-request-button').hide();
|
||||
$('#stripe-card').parent().removeClass("col-md-5").addClass("col-md-12");
|
||||
document.getElementById('stripe-payment-request-button').style.display = 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
$('.stripe-container').closest("form").find(".btn-primary").prop("disabled", false);
|
||||
}
|
||||
}
|
||||
@@ -144,7 +206,7 @@ $(function () {
|
||||
pretixstripe.show_checkout();
|
||||
} else {
|
||||
$("#stripe-current-card").slideUp();
|
||||
$("#stripe-card").slideDown();
|
||||
$("#stripe-elements").slideDown();
|
||||
}
|
||||
e.preventDefault();
|
||||
return false;
|
||||
@@ -152,7 +214,7 @@ $(function () {
|
||||
);
|
||||
|
||||
if ($("#stripe-current-card").length) {
|
||||
$("#stripe-card").hide();
|
||||
$("#stripe-elements").hide();
|
||||
}
|
||||
|
||||
$('.stripe-container').closest("form").submit(
|
||||
|
||||
51
src/pretix/plugins/stripe/tasks.py
Normal file
51
src/pretix/plugins/stripe/tasks.py
Normal file
@@ -0,0 +1,51 @@
|
||||
import logging
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
import stripe
|
||||
from django.conf import settings
|
||||
|
||||
from pretix.base.models import Event
|
||||
from pretix.celery_app import app
|
||||
from pretix.multidomain.urlreverse import get_domain
|
||||
from pretix.plugins.stripe.models import RegisteredApplePayDomain
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_domain_for_event(event):
|
||||
domain = get_domain(event.organizer)
|
||||
if not domain:
|
||||
siteurlsplit = urlsplit(settings.SITE_URL)
|
||||
return siteurlsplit.hostname
|
||||
return domain
|
||||
|
||||
|
||||
def get_stripe_account_key(prov):
|
||||
if prov.settings.connect_user_id:
|
||||
return prov.settings.connect_user_id
|
||||
else:
|
||||
return prov.settings.publishable_key
|
||||
|
||||
|
||||
@app.task(max_retries=5, default_retry_delay=1)
|
||||
def stripe_verify_domain(event_id, domain):
|
||||
from pretix.plugins.stripe.payment import StripeCC
|
||||
event = Event.objects.get(pk=event_id)
|
||||
prov = StripeCC(event)
|
||||
account = get_stripe_account_key(prov)
|
||||
|
||||
if RegisteredApplePayDomain.objects.filter(account=account, domain=domain).exists():
|
||||
return
|
||||
|
||||
try:
|
||||
stripe.ApplePayDomain.create(
|
||||
domain_name=domain,
|
||||
**prov.api_kwargs
|
||||
)
|
||||
except stripe.error.StripeError:
|
||||
logger.exception('Could not verify domain with Stripe')
|
||||
else:
|
||||
RegisteredApplePayDomain.objects.create(
|
||||
domain=domain,
|
||||
account=account
|
||||
)
|
||||
File diff suppressed because one or more lines are too long
@@ -30,20 +30,39 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div id="stripe-card" class="form-control">
|
||||
<span class="fa fa-spinner fa-spin"></span>
|
||||
<!-- a Stripe Element will be inserted here. -->
|
||||
<div class="row equal" id="stripe-elements">
|
||||
<div class="col-md-5 vcenter hidden">
|
||||
<div id="stripe-payment-request-button">
|
||||
<span class="fa fa-spinner fa-spin"></span>
|
||||
<!-- A Stripe Element will be inserted here. -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-1 hidden">
|
||||
<div class="hr">
|
||||
<div class="sep">
|
||||
<div class="sepText">{% trans "OR" %}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-5 vcenter">
|
||||
<div id="stripe-card" class="form-control">
|
||||
<span class="fa fa-spinner fa-spin"></span>
|
||||
<!-- a Stripe Element will be inserted here. -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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 %}
|
||||
<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_card_last4" value="{{ request.session.payment_stripe_last4 }}"
|
||||
id="stripe_card_last4"/>
|
||||
<input type="hidden" name="stripe_card_brand" value="{{ request.session.payment_stripe_brand }}"
|
||||
id="stripe_card_brand"/>
|
||||
<input type="hidden" id="stripe_currency" value="{{ event.currency }}"/>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -5,9 +5,15 @@
|
||||
{% compress js %}
|
||||
<script type="text/javascript" src="{% static "pretixplugins/stripe/pretix-stripe.js" %}"></script>
|
||||
{% endcompress %}
|
||||
{% compress css %}
|
||||
<link type="text/css" rel="stylesheet" href="{% static "pretixplugins/stripe/pretix-stripe.css" %}">
|
||||
{% endcompress %}
|
||||
{% if settings.endpoint == "test" and settings.publishable_test_key %}
|
||||
<script type="text/plain" id="stripe_pubkey">{{ settings.publishable_test_key }}</script>
|
||||
{% else %}
|
||||
<script type="text/plain" id="stripe_pubkey">{{ settings.publishable_key }}</script>
|
||||
{% endif %}
|
||||
|
||||
{% if settings.connect_user_id %}
|
||||
<script type="text/plain" id="stripe_connectedAccountId">{{ settings.connect_user_id }}</script>
|
||||
{% endif %}
|
||||
<script type="text/plain" id="stripe_merchantcountry">{{ settings.merchant_country|default:"" }}</script>
|
||||
|
||||
@@ -3,7 +3,8 @@ from django.conf.urls import include, url
|
||||
from pretix.multidomain import event_url
|
||||
|
||||
from .views import (
|
||||
ReturnView, oauth_disconnect, oauth_return, redirect_view, webhook,
|
||||
ReturnView, applepay_association, oauth_disconnect, oauth_return,
|
||||
redirect_view, webhook,
|
||||
)
|
||||
|
||||
event_patterns = [
|
||||
@@ -19,4 +20,5 @@ urlpatterns = [
|
||||
oauth_disconnect, name='oauth.disconnect'),
|
||||
url(r'^_stripe/webhook/$', webhook, name='webhook'),
|
||||
url(r'^_stripe/oauth_return/$', oauth_return, name='oauth.return'),
|
||||
url(r'^.well-known/apple-developer-merchantid-domain-association$', applepay_association, name='applepay.association'),
|
||||
]
|
||||
|
||||
@@ -26,6 +26,9 @@ 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.tasks import (
|
||||
get_domain_for_event, stripe_verify_domain,
|
||||
)
|
||||
|
||||
logger = logging.getLogger('pretix.plugins.stripe')
|
||||
|
||||
@@ -111,6 +114,7 @@ def oauth_return(request, *args, **kwargs):
|
||||
# event.settings.payment_stripe_connect_access_token = data['access_token'] we don't need it, right?
|
||||
event.settings.payment_stripe_connect_refresh_token = data['refresh_token']
|
||||
event.settings.payment_stripe_connect_user_id = data['stripe_user_id']
|
||||
event.settings.payment_stripe_merchant_country = account.get('country')
|
||||
if account.get('business_name') or account.get('display_name') or account.get('email'):
|
||||
event.settings.payment_stripe_connect_user_name = (
|
||||
account.get('business_name') or account.get('display_name') or account.get('email')
|
||||
@@ -125,6 +129,8 @@ def oauth_return(request, *args, **kwargs):
|
||||
event.settings.payment_stripe__enabled = True
|
||||
del request.session['payment_stripe_oauth_enable']
|
||||
|
||||
stripe_verify_domain.apply_async(args=(event.pk, get_domain_for_event(event)))
|
||||
|
||||
return redirect(reverse('control:event.settings.payment.provider', kwargs={
|
||||
'organizer': event.organizer.slug,
|
||||
'event': event.slug,
|
||||
@@ -340,6 +346,13 @@ def oauth_disconnect(request, **kwargs):
|
||||
}))
|
||||
|
||||
|
||||
@xframe_options_exempt
|
||||
def applepay_association(request, *args, **kwargs):
|
||||
r = render(request, 'pretixplugins/stripe/apple-developer-merchantid-domain-association')
|
||||
r._csp_ignore = True
|
||||
return r
|
||||
|
||||
|
||||
class StripeOrderView:
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user