From a4ced609cd3697eda7e8278abec7ce20bfa0eb03 Mon Sep 17 00:00:00 2001 From: Martin Gross Date: Wed, 15 Aug 2018 09:22:31 +0200 Subject: [PATCH] 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 ;-) --- doc/development/api/payment_2.0.rst | 2 + src/pretix/base/payment.py | 2 +- src/pretix/plugins/stripe/__init__.py | 2 +- .../plugins/stripe/management/__init__.py | 0 .../stripe/management/commands/__init__.py | 0 .../commands/stripe_connect_fill_countries.py | 29 ++++++++ .../0003_registeredapplepaydomain.py | 21 ++++++ src/pretix/plugins/stripe/models.py | 5 ++ src/pretix/plugins/stripe/payment.py | 32 +++++++-- .../pretixplugins/stripe/pretix-stripe.css | 45 ++++++++++++ .../pretixplugins/stripe/pretix-stripe.js | 68 ++++++++++++++++++- src/pretix/plugins/stripe/tasks.py | 51 ++++++++++++++ ...le-developer-merchantid-domain-association | 1 + .../stripe/checkout_payment_form.html | 27 ++++++-- .../pretixplugins/stripe/presale_head.html | 8 ++- src/pretix/plugins/stripe/urls.py | 4 +- src/pretix/plugins/stripe/views.py | 13 ++++ src/pretix/presale/checkoutflow.py | 6 +- src/pretix/presale/views/order.py | 11 ++- 19 files changed, 309 insertions(+), 18 deletions(-) create mode 100644 src/pretix/plugins/stripe/management/__init__.py create mode 100644 src/pretix/plugins/stripe/management/commands/__init__.py create mode 100644 src/pretix/plugins/stripe/management/commands/stripe_connect_fill_countries.py create mode 100644 src/pretix/plugins/stripe/migrations/0003_registeredapplepaydomain.py create mode 100644 src/pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.css create mode 100644 src/pretix/plugins/stripe/tasks.py create mode 100644 src/pretix/plugins/stripe/templates/pretixplugins/stripe/apple-developer-merchantid-domain-association diff --git a/doc/development/api/payment_2.0.rst b/doc/development/api/payment_2.0.rst index 2acbfd78f0..ab5fcc8820 100644 --- a/doc/development/api/payment_2.0.rst +++ b/doc/development/api/payment_2.0.rst @@ -69,6 +69,8 @@ Payment processing ``BasePaymentProvider.payment_pending_render(request, payment)`` method that is passed an ``OrderPayment`` object instead of an ``Order``. +* The method ``BasePaymentProvider.payment_form_render`` now receives a new ``total`` parameter. + * The method ``BasePaymentProvider.payment_perform`` has been removed and replaced by a new method ``BasePaymentProvider.execute_payment(request, payment)`` that is passed an ``OrderPayment`` object instead of an ``Order``. diff --git a/src/pretix/base/payment.py b/src/pretix/base/payment.py index 1221699446..4c8024859e 100644 --- a/src/pretix/base/payment.py +++ b/src/pretix/base/payment.py @@ -373,7 +373,7 @@ class BasePaymentProvider: return timing and pricing - def payment_form_render(self, request: HttpRequest) -> str: + def payment_form_render(self, request: HttpRequest, total: Decimal) -> str: """ When the user selects this provider as their preferred payment method, they will be shown the HTML you return from this method. diff --git a/src/pretix/plugins/stripe/__init__.py b/src/pretix/plugins/stripe/__init__.py index 215ac28532..4022c3e566 100644 --- a/src/pretix/plugins/stripe/__init__.py +++ b/src/pretix/plugins/stripe/__init__.py @@ -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): diff --git a/src/pretix/plugins/stripe/management/__init__.py b/src/pretix/plugins/stripe/management/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/pretix/plugins/stripe/management/commands/__init__.py b/src/pretix/plugins/stripe/management/commands/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/pretix/plugins/stripe/management/commands/stripe_connect_fill_countries.py b/src/pretix/plugins/stripe/management/commands/stripe_connect_fill_countries.py new file mode 100644 index 0000000000..ed104a3721 --- /dev/null +++ b/src/pretix/plugins/stripe/management/commands/stripe_connect_fill_countries.py @@ -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') diff --git a/src/pretix/plugins/stripe/migrations/0003_registeredapplepaydomain.py b/src/pretix/plugins/stripe/migrations/0003_registeredapplepaydomain.py new file mode 100644 index 0000000000..c64fc0109b --- /dev/null +++ b/src/pretix/plugins/stripe/migrations/0003_registeredapplepaydomain.py @@ -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)), + ], + ), + ] diff --git a/src/pretix/plugins/stripe/models.py b/src/pretix/plugins/stripe/models.py index 9ebdc32d69..fea980b4f4 100644 --- a/src/pretix/plugins/stripe/models.py +++ b/src/pretix/plugins/stripe/models.py @@ -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) diff --git a/src/pretix/plugins/stripe/payment.py b/src/pretix/plugins/stripe/payment.py index 487766175d..4d4ac001f6 100644 --- a/src/pretix/plugins/stripe/payment.py +++ b/src/pretix/plugins/stripe/payment.py @@ -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) diff --git a/src/pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.css b/src/pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.css new file mode 100644 index 0000000000..fd6b629604 --- /dev/null +++ b/src/pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.css @@ -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; +} diff --git a/src/pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js b/src/pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js index cbd479ae5f..70dfb7d05d 100644 --- a/src/pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js +++ b/src/pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js @@ -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( diff --git a/src/pretix/plugins/stripe/tasks.py b/src/pretix/plugins/stripe/tasks.py new file mode 100644 index 0000000000..92894cca07 --- /dev/null +++ b/src/pretix/plugins/stripe/tasks.py @@ -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 + ) diff --git a/src/pretix/plugins/stripe/templates/pretixplugins/stripe/apple-developer-merchantid-domain-association b/src/pretix/plugins/stripe/templates/pretixplugins/stripe/apple-developer-merchantid-domain-association new file mode 100644 index 0000000000..41a1464744 --- /dev/null +++ b/src/pretix/plugins/stripe/templates/pretixplugins/stripe/apple-developer-merchantid-domain-association @@ -0,0 +1 @@ +7B227073704964223A2239373943394538343346343131343044463144313834343232393232313734313034353044314339464446394437384337313531303944334643463542433731222C2276657273696F6E223A312C22637265617465644F6E223A313437313435343137313137362C227369676E6174757265223A2233303830303630393261383634383836663730643031303730326130383033303830303230313031333130663330306430363039363038363438303136353033303430323031303530303330383030363039326138363438383666373064303130373031303030306130383033303832303365363330383230333862613030333032303130323032303836383630663639396439636361373066333030613036303832613836343863653364303430333032333037613331326533303263303630333535303430333063323534313730373036633635323034313730373036633639363336313734363936663665323034393665373436353637373236313734363936663665323034333431323032643230343733333331323633303234303630333535303430623063316434313730373036633635323034333635373237343639363636393633363137343639366636653230343137353734363836663732363937343739333131333330313130363033353530343061306330613431373037303663363532303439366536333265333130623330303930363033353530343036313330323535353333303165313730643331333633303336333033333331333833313336333433303561313730643332333133303336333033323331333833313336333433303561333036323331323833303236303630333535303430333063316636353633363332643733366437303264363237323666366236353732326437333639363736653566353534333334326435333431346534343432346635383331313433303132303630333535303430623063306236393466353332303533373937333734363536643733333131333330313130363033353530343061306330613431373037303663363532303439366536333265333130623330303930363033353530343036313330323535353333303539333031333036303732613836343863653364303230313036303832613836343863653364303330313037303334323030303438323330666461626333396366373565323032633530643939623435313265363337653261393031646436636233653062316364346235323637393866386366346562646538316132356138633231653463333364646365386532613936633266366166613139333033343563346538376134343236636539353162313239356133383230323131333038323032306433303435303630383262303630313035303530373031303130343339333033373330333530363038326230363031303530353037333030313836323936383734373437303361326632663666363337333730326536313730373036633635326536333666366432663666363337333730333033343264363137303730366336353631363936333631333333303332333031643036303335353164306530343136303431343032323433303062396165656564343633313937613461363561323939653432373138323163343533303063303630333535316431333031303166663034303233303030333031663036303335353164323330343138333031363830313432336632343963343466393365346566323765366334663632383663336661326262666432653462333038323031316430363033353531643230303438323031313433303832303131303330383230313063303630393261383634383836663736333634303530313330383166653330383163333036303832623036303130353035303730323032333038316236306338316233353236353663363936313665363336353230366636653230373436383639373332303633363537323734363936363639363336313734363532303632373932303631366537393230373036313732373437393230363137333733373536643635373332303631363336333635373037343631366536333635323036663636323037343638363532303734363836353665323036313730373036633639363336313632366336353230373337343631366536343631373236343230373436353732366437333230363136653634323036333666366536343639373436393666366537333230366636363230373537333635326332303633363537323734363936363639363336313734363532303730366636633639363337393230363136653634323036333635373237343639363636393633363137343639366636653230373037323631363337343639363336353230373337343631373436353664363536653734373332653330333630363038326230363031303530353037303230313136326136383734373437303361326632663737373737373265363137303730366336353265363336663664326636333635373237343639363636393633363137343635363137353734363836663732363937343739326633303334303630333535316431663034326433303262333032396130323761303235383632333638373437343730336132663266363337323663326536313730373036633635326536333666366432663631373037303663363536313639363336313333326536333732366333303065303630333535316430663031303166663034303430333032303738303330306630363039326138363438383666373633363430363164303430323035303033303061303630383261383634386365336430343033303230333439303033303436303232313030646131633633616538626535663634663865313165383635363933376239623639633437326265393365616333323333613136373933366534613864356538333032323130306264356166626638363966336330636132373462326664646534663731373135396362336264373139396232636130666634303964653635396138326232346433303832303265653330383230323735613030333032303130323032303834393664326662663361393864613937333030613036303832613836343863653364303430333032333036373331316233303139303630333535303430333063313234313730373036633635323035323666366637343230343334313230326432303437333333313236333032343036303335353034306230633164343137303730366336353230343336353732373436393636363936333631373436393666366532303431373537343638366637323639373437393331313333303131303630333535303430613063306134313730373036633635323034393665363332653331306233303039303630333535303430363133303235353533333031653137306433313334333033353330333633323333333433363333333035613137306433323339333033353330333633323333333433363333333035613330376133313265333032633036303335353034303330633235343137303730366336353230343137303730366336393633363137343639366636653230343936653734363536373732363137343639366636653230343334313230326432303437333333313236333032343036303335353034306230633164343137303730366336353230343336353732373436393636363936333631373436393666366532303431373537343638366637323639373437393331313333303131303630333535303430613063306134313730373036633635323034393665363332653331306233303039303630333535303430363133303235353533333035393330313330363037326138363438636533643032303130363038326138363438636533643033303130373033343230303034663031373131383431396437363438356435316135653235383130373736653838306132656664653762616534646530386466633462393365313333353664353636356233356165323264303937373630643232346537626261303866643736313763653838636237366262363637306265633865383239383466663534343561333831663733303831663433303436303630383262303630313035303530373031303130343361333033383330333630363038326230363031303530353037333030313836326136383734373437303361326632663666363337333730326536313730373036633635326536333666366432663666363337333730333033343264363137303730366336353732366636663734363336313637333333303164303630333535316430653034313630343134323366323439633434663933653465663237653663346636323836633366613262626664326534623330306630363033353531643133303130316666303430353330303330313031666633303166303630333535316432333034313833303136383031346262623064656131353833333838396161343861393964656265626465626166646163623234616233303337303630333535316431663034333033303265333032636130326161303238383632363638373437343730336132663266363337323663326536313730373036633635326536333666366432663631373037303663363537323666366637343633363136373333326536333732366333303065303630333535316430663031303166663034303430333032303130363330313030363061326138363438383666373633363430363032306530343032303530303330306130363038326138363438636533643034303330323033363730303330363430323330336163663732383335313136393962313836666233356333353663613632626666343137656464393066373534646132386562656631396338313565343262373839663839386637396235393966393864353431306438663964653963326665303233303332326464353434323162306133303537373663356466333338336239303637666431373763326332313664393634666336373236393832313236663534663837613764316239396362396230393839323136313036393930663039393231643030303033313832303136303330383230313563303230313031333038313836333037613331326533303263303630333535303430333063323534313730373036633635323034313730373036633639363336313734363936663665323034393665373436353637373236313734363936663665323034333431323032643230343733333331323633303234303630333535303430623063316434313730373036633635323034333635373237343639363636393633363137343639366636653230343137353734363836663732363937343739333131333330313130363033353530343061306330613431373037303663363532303439366536333265333130623330303930363033353530343036313330323535353330323038363836306636393964396363613730663330306430363039363038363438303136353033303430323031303530306130363933303138303630393261383634383836663730643031303930333331306230363039326138363438383666373064303130373031333031633036303932613836343838366637306430313039303533313066313730643331333633303338333133373331333733313336333133313561333032663036303932613836343838366637306430313039303433313232303432303733343832623432653665366332323264616536643963303961346336663332316534656136653666326661626631356430376562333338643264613435646233303061303630383261383634386365336430343033303230343438333034363032323130306564333264376438616131623536623036626164623162396639396264643063653662363931316530623032393232633934333362663564326130656135353830323231303066393433353637663030323361643061343561373236663238376636303062656334666566373335383832383935633733313531383337336163383934383137303030303030303030303030227D \ No newline at end of file diff --git a/src/pretix/plugins/stripe/templates/pretixplugins/stripe/checkout_payment_form.html b/src/pretix/plugins/stripe/templates/pretixplugins/stripe/checkout_payment_form.html index f196af3afe..768c1411b9 100644 --- a/src/pretix/plugins/stripe/templates/pretixplugins/stripe/checkout_payment_form.html +++ b/src/pretix/plugins/stripe/templates/pretixplugins/stripe/checkout_payment_form.html @@ -30,20 +30,39 @@ {% endif %} -
- - +
+ + +
+
+ + +
+
+

{% 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 %} + +

- diff --git a/src/pretix/plugins/stripe/templates/pretixplugins/stripe/presale_head.html b/src/pretix/plugins/stripe/templates/pretixplugins/stripe/presale_head.html index 07430fef00..0e67d2ceb9 100644 --- a/src/pretix/plugins/stripe/templates/pretixplugins/stripe/presale_head.html +++ b/src/pretix/plugins/stripe/templates/pretixplugins/stripe/presale_head.html @@ -5,9 +5,15 @@ {% compress js %} {% endcompress %} +{% compress css %} + +{% endcompress %} {% if settings.endpoint == "test" and settings.publishable_test_key %} {% else %} {% endif %} - +{% if settings.connect_user_id %} + +{% endif %} + diff --git a/src/pretix/plugins/stripe/urls.py b/src/pretix/plugins/stripe/urls.py index 4766e3bdac..2dfac9f0d2 100644 --- a/src/pretix/plugins/stripe/urls.py +++ b/src/pretix/plugins/stripe/urls.py @@ -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'), ] diff --git a/src/pretix/plugins/stripe/views.py b/src/pretix/plugins/stripe/views.py index 3f354ac1e5..e346ba67b5 100644 --- a/src/pretix/plugins/stripe/views.py +++ b/src/pretix/plugins/stripe/views.py @@ -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: diff --git a/src/pretix/presale/checkoutflow.py b/src/pretix/presale/checkoutflow.py index 2a78acf9e8..433ce3b159 100644 --- a/src/pretix/presale/checkoutflow.py +++ b/src/pretix/presale/checkoutflow.py @@ -440,11 +440,15 @@ class PaymentStep(QuestionsViewMixin, CartMixin, TemplateFlowStep): if not provider.is_enabled or not self._is_allowed(provider, self.request): continue fee = provider.calculate_fee(self._total_order_value) + try: + form = provider.payment_form_render(self.request, self._total_order_value + fee) + except TypeError: + form = provider.payment_form_render(self.request) providers.append({ 'provider': provider, 'fee': fee, 'total': self._total_order_value + fee, - 'form': provider.payment_form_render(self.request) + 'form': form }) return providers diff --git a/src/pretix/presale/views/order.py b/src/pretix/presale/views/order.py index dbfd8a26f2..347bee7c40 100644 --- a/src/pretix/presale/views/order.py +++ b/src/pretix/presale/views/order.py @@ -199,7 +199,10 @@ class OrderPaymentStart(EventViewMixin, OrderDetailMixin, TemplateView): @cached_property def form(self): - return self.payment.payment_provider.payment_form_render(self.request) + try: + return self.payment.payment_provider.payment_form_render(self.request, self.payment.amount) + except TypeError: + return self.payment.payment_provider.payment_form_render(self.request) @cached_property def payment(self): @@ -384,13 +387,17 @@ class OrderPayChangeMethod(EventViewMixin, OrderDetailMixin, TemplateView): continue current_fee = sum(f.value for f in self.open_fees) or Decimal('0.00') fee = provider.calculate_fee(pending_sum - current_fee) + try: + form = provider.payment_form_render(self.request, abs(pending_sum + fee - current_fee)) + except TypeError: + form = provider.payment_form_render(self.request) providers.append({ 'provider': provider, 'fee': fee, 'fee_diff': fee - current_fee, 'fee_diff_abs': abs(fee - current_fee), 'total': abs(pending_sum + fee - current_fee), - 'form': provider.payment_form_render(self.request) + 'form': form }) return providers