From 41cded095cd2e0367c9006b1da10b288e1d8a2ee Mon Sep 17 00:00:00 2001
From: Martin Gross
Date: Tue, 11 Jul 2023 11:51:43 +0200
Subject: [PATCH] PProv: Implement detection of wallets such as Google Pay and
Apple Pay (#3444)
Co-authored-by: Richard Schreiber
---
doc/development/api/payment.rst | 2 +
src/pretix/base/middleware.py | 2 +-
src/pretix/base/payment.py | 23 ++++++
src/pretix/plugins/stripe/payment.py | 27 ++++++-
.../pretixpresale/event/checkout_payment.html | 7 +-
.../event/fragment_walletdetection_head.html | 6 ++
.../pretixpresale/event/order_pay_change.html | 7 +-
.../pretixpresale/js/walletdetection.js | 71 +++++++++++++++++++
.../static/pretixpresale/scss/_checkout.scss | 4 ++
9 files changed, 145 insertions(+), 4 deletions(-)
create mode 100644 src/pretix/presale/templates/pretixpresale/event/fragment_walletdetection_head.html
create mode 100644 src/pretix/static/pretixpresale/js/walletdetection.js
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:" %}
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;
+}