diff --git a/doc/development/api/payment.rst b/doc/development/api/payment.rst index a09844213d..052dce71df 100644 --- a/doc/development/api/payment.rst +++ b/doc/development/api/payment.rst @@ -70,6 +70,8 @@ The provider class .. autoattribute:: settings_form_fields + .. autoattribute:: walletqueries + .. automethod:: settings_form_clean .. automethod:: settings_content_render diff --git a/src/pretix/base/middleware.py b/src/pretix/base/middleware.py index 61af59c221..74721fb589 100644 --- a/src/pretix/base/middleware.py +++ b/src/pretix/base/middleware.py @@ -249,7 +249,7 @@ class SecurityMiddleware(MiddlewareMixin): h = { 'default-src': ["{static}"], - 'script-src': ['{static}', 'https://checkout.stripe.com', 'https://js.stripe.com'], + 'script-src': ['{static}', 'https://checkout.stripe.com', 'https://js.stripe.com', 'https://pay.google.com'], 'object-src': ["'none'"], 'frame-src': ['{static}', 'https://checkout.stripe.com', 'https://js.stripe.com'], 'style-src': ["{static}", "{media}"], diff --git a/src/pretix/base/payment.py b/src/pretix/base/payment.py index a79af66c8e..de0dc8eaba 100644 --- a/src/pretix/base/payment.py +++ b/src/pretix/base/payment.py @@ -78,6 +78,16 @@ from pretix.presale.views.cart import cart_session, get_or_create_cart_id logger = logging.getLogger(__name__) +class WalletQueries: + APPLEPAY = 'applepay' + GOOGLEPAY = 'googlepay' + + WALLETS = ( + (APPLEPAY, pgettext_lazy('payment', 'Apple Pay')), + (GOOGLEPAY, pgettext_lazy('payment', 'Google Pay')), + ) + + class PaymentProviderForm(Form): def clean(self): cleaned_data = super().clean() @@ -436,6 +446,19 @@ class BasePaymentProvider: d['_restrict_to_sales_channels']._as_type = list return d + @property + def walletqueries(self): + """ + .. warning:: This property is considered **experimental**. It might change or get removed at any time without + prior notice. + + A list of wallet payment methods that should be dynamically joined to the public name of the payment method, + if they are available to the user. + The detection is made on a best effort basis with no guarantees of correctness and actual availability. + Wallets that pretix can check for are exposed through ``pretix.base.payment.WalletQueries``. + """ + return [] + def settings_form_clean(self, cleaned_data): """ Overriding this method allows you to inject custom validation into the settings form. diff --git a/src/pretix/plugins/stripe/payment.py b/src/pretix/plugins/stripe/payment.py index fbd2094eca..c648d64e53 100644 --- a/src/pretix/plugins/stripe/payment.py +++ b/src/pretix/plugins/stripe/payment.py @@ -59,7 +59,9 @@ from pretix import __version__ from pretix.base.decimal import round_decimal from pretix.base.forms import SecretKeySettingsField from pretix.base.models import Event, OrderPayment, OrderRefund, Quota -from pretix.base.payment import BasePaymentProvider, PaymentException +from pretix.base.payment import ( + BasePaymentProvider, PaymentException, WalletQueries, +) from pretix.base.plugins import get_all_plugins from pretix.base.services.mail import SendMailException from pretix.base.settings import SettingsSandbox @@ -219,6 +221,20 @@ class StripeSettingsHolder(BasePaymentProvider): ] extra_fields = [ + ('walletdetection', + forms.BooleanField( + label=mark_safe( + _('Check for Apple Pay/Google Pay') + + ' ' + + '{}'.format(_('experimental')) + ), + help_text=_("pretix will attempt to check if the customer's webbrowser supports wallet-based payment " + "methods like Apple Pay or Google Pay and display them prominently with the credit card" + "payment method. This detection does not take into consideration if Google Pay/Apple Pay " + "has been disabled in the Stripe Dashboard."), + initial=True, + required=False, + )), ('postfix', forms.CharField( label=_('Statement descriptor postfix'), @@ -747,6 +763,15 @@ class StripeCC(StripeMethod): public_name = _('Credit card') method = 'cc' + @property + def walletqueries(self): + # ToDo: Check against Stripe API, if ApplePay and GooglePay are even activated/available + # This is probably only really feasable once the Payment Methods Configuration API is out of beta + # https://stripe.com/docs/connect/payment-method-configurations + if self.settings.get("walletdetection", True, as_type=bool): + return [WalletQueries.APPLEPAY, WalletQueries.GOOGLEPAY] + return [] + def payment_form_render(self, request, total) -> str: account = get_stripe_account_key(self) if not RegisteredApplePayDomain.objects.filter(account=account, domain=request.host).exists(): diff --git a/src/pretix/presale/templates/pretixpresale/event/checkout_payment.html b/src/pretix/presale/templates/pretixpresale/event/checkout_payment.html index ca8ba52221..f0f05523f1 100644 --- a/src/pretix/presale/templates/pretixpresale/event/checkout_payment.html +++ b/src/pretix/presale/templates/pretixpresale/event/checkout_payment.html @@ -3,6 +3,10 @@ {% load money %} {% load bootstrap3 %} {% load rich_text %} +{% block custom_header %} + {{ block.super }} + {% include "pretixpresale/event/fragment_walletdetection_head.html" %} +{% endblock %} {% block inner %} {% if current_payments %}

{% trans "You already selected the following payment methods:" %}

@@ -71,7 +75,8 @@ {% if selected == p.provider.identifier %}checked="checked"{% endif %} id="input_payment_{{ p.provider.identifier }}" aria-describedby="payment_{{ p.provider.identifier }}" - data-toggle="radiocollapse" data-target="#payment_{{ p.provider.identifier }}"/> + data-toggle="radiocollapse" data-target="#payment_{{ p.provider.identifier }}" + data-wallets="{{ p.provider.walletqueries|join:"|" }}" />

diff --git a/src/pretix/presale/templates/pretixpresale/event/fragment_walletdetection_head.html b/src/pretix/presale/templates/pretixpresale/event/fragment_walletdetection_head.html new file mode 100644 index 0000000000..cecf2fd5ce --- /dev/null +++ b/src/pretix/presale/templates/pretixpresale/event/fragment_walletdetection_head.html @@ -0,0 +1,6 @@ +{% load static %} +{% load compress %} + +{% compress js %} + +{% endcompress %} \ No newline at end of file diff --git a/src/pretix/presale/templates/pretixpresale/event/order_pay_change.html b/src/pretix/presale/templates/pretixpresale/event/order_pay_change.html index ea680f3f14..1655631f87 100644 --- a/src/pretix/presale/templates/pretixpresale/event/order_pay_change.html +++ b/src/pretix/presale/templates/pretixpresale/event/order_pay_change.html @@ -3,6 +3,10 @@ {% load eventurl %} {% load money %} {% block title %}{% trans "Change payment method" %}{% endblock %} +{% block custom_header %} + {{ block.super }} + {% include "pretixpresale/event/fragment_walletdetection_head.html" %} +{% endblock %} {% block content %}

{% blocktrans trimmed with code=order.code %} @@ -29,7 +33,8 @@ + data-toggle="radiocollapse" data-target="#payment_{{ p.provider.identifier }}" + data-wallets="{{ p.provider.walletqueries|join:"|" }}"/> {{ p.provider.public_name }}

diff --git a/src/pretix/static/pretixpresale/js/walletdetection.js b/src/pretix/static/pretixpresale/js/walletdetection.js new file mode 100644 index 0000000000..fb64e90c4a --- /dev/null +++ b/src/pretix/static/pretixpresale/js/walletdetection.js @@ -0,0 +1,71 @@ +'use strict'; + +var walletdetection = { + applepay: async function () { + // This is a weak check for Apple Pay - in order to do a proper check, we would need to also call + // canMakePaymentsWithActiveCard(merchantIdentifier) + + return !!(window.ApplePaySession && window.ApplePaySession.canMakePayments()); + }, + googlepay: async function () { + // Checking for Google Pay is a little bit more involved, since it requires including the Google Pay JS SDK, and + // providing a lot of information. + // So for the time being, we only check if Google Pay is available in TEST-mode, which should hopefully give us a + // good enough idea if Google Pay could be present on this device; even though there are still a lot of other + // factors that could inhibit Google Pay from actually being offered to the customer. + + return $.ajax({ + url: 'https://pay.google.com/gp/p/js/pay.js', + dataType: 'script', + }).then(function() { + const paymentsClient = new google.payments.api.PaymentsClient({environment: 'TEST'}); + return paymentsClient.isReadyToPay({ + apiVersion: 2, + apiVersionMinor: 0, + allowedPaymentMethods: [{ + type: 'CARD', + parameters: { + allowedAuthMethods: ["PAN_ONLY", "CRYPTOGRAM_3DS"], + allowedCardNetworks: ["AMEX", "DISCOVER", "INTERAC", "JCB", "MASTERCARD", "VISA"] + } + }], + }) + }).then(function (response) { + return !!response.result; + }); + }, + name_map: { + applepay: gettext('Apple Pay'), + googlepay: gettext('Google Pay'), + } +} + +$(function () { + const wallets = $('[data-wallets]') + .map(function(index, pm) { + return pm.getAttribute("data-wallets").split("|"); + }) + .get() + .flat() + .filter(function(item, pos, self) { + // filter out empty or duplicate values + return item && self.indexOf(item) == pos; + }); + + wallets.forEach(function(wallet) { + const labels = $('[data-wallets*='+wallet+'] + label strong, [data-wallets*='+wallet+'] + strong') + .append(' ') + walletdetection[wallet]() + .then(function(result) { + const spans = labels.find(".wallet-loading:nth-of-type(1)"); + if (result) { + spans.removeClass('wallet-loading').hide().text(', ' + walletdetection.name_map[wallet]).fadeIn(300); + } else { + spans.remove(); + } + }) + .catch(function(result) { + labels.find(".wallet-loading:nth-of-type(1)").remove(); + }) + }); +}); diff --git a/src/pretix/static/pretixpresale/scss/_checkout.scss b/src/pretix/static/pretixpresale/scss/_checkout.scss index 61b5aa59d5..09bd7d2ef7 100644 --- a/src/pretix/static/pretixpresale/scss/_checkout.scss +++ b/src/pretix/static/pretixpresale/scss/_checkout.scss @@ -179,3 +179,7 @@ flex: 1; } } + +.wallet-loading + .wallet-loading { + display: none; +}