diff --git a/src/pretix/base/middleware.py b/src/pretix/base/middleware.py index 61af59c22..74721fb58 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 a79af66c8..e18d7b764 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,16 @@ class BasePaymentProvider: d['_restrict_to_sales_channels']._as_type = list return d + @property + def walletqueries(self): + """ + 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 fbd2094ec..a507840e1 100644 --- a/src/pretix/plugins/stripe/payment.py +++ b/src/pretix/plugins/stripe/payment.py @@ -59,7 +59,7 @@ 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 @@ -747,6 +747,11 @@ 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 + return [WalletQueries.APPLEPAY, WalletQueries.GOOGLEPAY] + 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 ca8ba5222..f0f05523f 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 000000000..cecf2fd5c --- /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 ea680f3f1..1655631f8 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 000000000..ddedefd73 --- /dev/null +++ b/src/pretix/static/pretixpresale/js/walletdetection.js @@ -0,0 +1,120 @@ +'use strict'; + +var walletdetection = { + applepay: 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: 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. + + const baseRequest = { + apiVersion: 2, + apiVersionMinor: 0 + }; + const tokenizationSpecification = { + type: 'PAYMENT_GATEWAY', + parameters: { + 'gateway': 'example', + 'gatewayMerchantId': 'exampleGatewayMerchantId' + } + }; + const allowedCardNetworks = ["AMEX", "DISCOVER", "INTERAC", "JCB", "MASTERCARD", "VISA"]; + const allowedCardAuthMethods = ["PAN_ONLY", "CRYPTOGRAM_3DS"]; + const baseCardPaymentMethod = { + type: 'CARD', + parameters: { + allowedAuthMethods: allowedCardAuthMethods, + allowedCardNetworks: allowedCardNetworks + } + }; + const cardPaymentMethod = Object.assign( + {tokenizationSpecification: tokenizationSpecification}, + baseCardPaymentMethod + ); + + return $.ajax({ + url: 'https://pay.google.com/gp/p/js/pay.js', + dataType: 'script', + success: function () { + const paymentsClient = new google.payments.api.PaymentsClient({environment: 'TEST'}); + const isReadyToPayRequest = Object.assign({}, baseRequest); + isReadyToPayRequest.allowedPaymentMethods = [baseCardPaymentMethod]; + + paymentsClient.isReadyToPay(isReadyToPayRequest) + .then(function (response) { + if (response.result) { + return true; + } + }) + .catch(function (err) { + return false; + }); + }, + }); + }, + name_map: { + applepay: gettext('Apple Pay'), + googlepay: gettext('Google Pay'), + } +} + +$(function () { + let requestedWallets = Array(); + let paymentMethods = $('[data-wallets][data-wallets!=""]'); + + paymentMethods.each(function () { + let $s = $(this); + requestedWallets = requestedWallets.concat($s.data("wallets").split("|")); + }) + + // Filtering out any doubles + requestedWallets = new Set(requestedWallets); + + // Perform the actual check *only* on the requested wallets, if they are supported by the browser + let availableWallets = Array(); + requestedWallets.forEach(function (it) { + if (walletdetection[it]()) { + availableWallets.push(it); + } + }); + + paymentMethods.each(function () { + let $s = $(this); + let wallets = Array(); + + // Run the translation on the available wallet strings before pushing them out. + $s.data("wallets").split("|").forEach(function (it) { + if (availableWallets.includes(it)) { + wallets.push(gettext(walletdetection.name_map[it])); + } + }) + + // In case there is no wallets available, we do not want to flicker the screen + if (wallets.length === 0) { + return; + } + + // The first selector is used on the regular payment-step of the checkout flow + // The second selector is used for the payment method change view. + // In the long run, the layout of both pages should be adjusted to be one. + let textselector = $s.next('label').find('strong'); + let textselector2 = $s.next("strong"); + textselector.fadeOut(300, function () { + wallets.unshift(textselector.text()); + textselector.text(wallets.join(", ")); + textselector.fadeIn(300); + }); + textselector2.fadeOut(300, function () { + wallets.unshift(textselector2.text()); + textselector2.text(wallets.join(", ")); + textselector2.fadeIn(300); + }); + }); +}); \ No newline at end of file