diff --git a/doc/user/events/widget.rst b/doc/user/events/widget.rst
index 6f497b1c8..30d7e6132 100644
--- a/doc/user/events/widget.rst
+++ b/doc/user/events/widget.rst
@@ -101,4 +101,43 @@ voucher's settings.
+pretix Button
+-------------
+
+Instead of a product list, you can also display just a single button. When pressed, the button will add a number of
+products associated with the button to the cart and will immediately proceed to checkout if the operation succeeded.
+You can try out this behavior here:
+
+.. raw:: html
+
+ Buy ticket!
+
+
+
+You can embed the pretix Button just like the pretix Widget. Just like above, first embed the CSS and JavaScript
+resources. Then, instead of the ``pretix-widget`` tag, use the ``pretix-button`` tag::
+
+
+ Buy ticket!
+
+
+As you can see, the ``pretix-button`` element takes an additional ``items`` attribute that specifies the items that
+should be added to the cart. The syntax of this attribute is ``item_ITEMID=1,item_ITEMID=2,variation_ITEMID_VARID=4``
+where ``ITEMID`` are the internal IDs of items to be added and ``VARID`` are the internal IDs of variations of those
+items, if the items have variations.
+
+Just as the widget, the button supports the optional attributes ``voucher`` and ``skip-ssl-check``.
+
+You can style the button using the ``pretix-button`` CSS class.
+
+.. versionchanged:: 1.13
+
+ The pretix Button has been added in version 1.13.
+
.. _Let's Encrypt: https://letsencrypt.org/
diff --git a/src/pretix/presale/views/widget.py b/src/pretix/presale/views/widget.py
index ea0e5b469..cd8e0076a 100644
--- a/src/pretix/presale/views/widget.py
+++ b/src/pretix/presale/views/widget.py
@@ -101,7 +101,7 @@ def generate_widget_js(lang):
@condition(etag_func=widget_js_etag)
-@cache_page(60)
+@cache_page(1 if settings.DEBUG else 60)
def widget_js(request, lang, **kwargs):
if lang not in [lc for lc, ll in settings.LANGUAGES]:
raise Http404()
diff --git a/src/pretix/static/pretixpresale/js/widget/widget.js b/src/pretix/static/pretixpresale/js/widget/widget.js
index d5dc9a6b4..39a93ca8f 100644
--- a/src/pretix/static/pretixpresale/js/widget/widget.js
+++ b/src/pretix/static/pretixpresale/js/widget/widget.js
@@ -373,12 +373,168 @@ Vue.component('category', {
category: Object
}
});
+
+var shared_methods = {
+ buy: function (event) {
+ if (this.$root.useIframe) {
+ event.preventDefault();
+ } else {
+ return;
+ }
+ var url = this.$root.formTarget + "&locale=" + lang + "&ajax=1";
+ this.$root.frame_loading = true;
+ this.async_task_interval = 100;
+ api._postFormJSON(url, this.$refs.form, this.buy_callback, this.buy_error_callback);
+ },
+ buy_error_callback: function (xhr, data) {
+ this.$root.error_message = strings['cart_error'];
+ this.$root.frame_loading = false;
+ },
+ buy_check_error_callback: function (xhr, data) {
+ if (xhr.status == 200 || (xhr.status >= 400 && xhr.status < 500)) {
+ this.$root.error_message = strings['cart_error'];
+ this.$root.frame_loading = false;
+ } else {
+ this.async_task_timeout = window.setTimeout(this.buy_check, 1000);
+ }
+ },
+ buy_callback: function (data) {
+ if (data.redirect) {
+ var iframe = this.$refs['frame-container'].children[0];
+ this.$root.cart_id = data.cart_id;
+ setCookie(this.$root.cookieName, data.cart_id, 30);
+ if (data.redirect.substr(0, 1) === '/') {
+ data.redirect = this.$root.event_url.replace(/^([^\/]+:\/\/[^\/]+)\/.*$/, "$1") + data.redirect;
+ }
+ var url = data.redirect + '?iframe=1&locale=' + lang + '&take_cart_id=' + this.$root.cart_id;
+ if (data.success === false) {
+ url = url.replace(/checkout\/start/g, "");
+ this.$root.error_message = data.message;
+ if (data.has_cart) {
+ this.$root.error_url_after = url;
+ }
+ this.$root.frame_loading = false;
+ } else {
+ iframe.src = url;
+ }
+ } else {
+ this.async_task_id = data.async_id;
+ if (data.check_url) {
+ this.async_task_check_url = this.$root.event_url.replace(/^([^\/]+:\/\/[^\/]+)\/.*$/, "$1") + data.check_url;
+ }
+ this.async_task_timeout = window.setTimeout(this.buy_check, this.async_task_interval);
+ this.async_task_interval = 250;
+ }
+ },
+ buy_check: function () {
+ api._getJSON(this.async_task_check_url, this.buy_callback, this.buy_check_error_callback);
+ },
+ errorContinue: function () {
+ var iframe = this.$refs['frame-container'].children[0];
+ iframe.src = this.$root.error_url_after;
+ this.$root.frame_loading = true;
+ this.$root.error_message = null;
+ this.$root.error_url_after = null;
+ },
+ errorClose: function () {
+ this.$root.error_message = null;
+ this.$root.error_url_after = null;
+ },
+ redeem: function () {
+ if (this.$root.useIframe) {
+ event.preventDefault();
+ } else {
+ return;
+ }
+ var redirect_url = this.$root.voucherFormTarget + '&voucher=' + this.voucher + '&subevent=' + this.$root.subevent;
+ var iframe = this.$refs['frame-container'].children[0];
+ this.$root.frame_loading = true;
+ iframe.src = redirect_url;
+ },
+ resume: function () {
+ var redirect_url = this.$root.event_url + 'w/' + widget_id + '/checkout/start?iframe=1&locale=' + lang + '&take_cart_id=' + this.$root.cart_id;
+ if (this.$root.useIframe) {
+ var iframe = this.$refs['frame-container'].children[0];
+ this.$root.frame_loading = true;
+ iframe.src = redirect_url;
+ } else {
+ window.open(redirect_url);
+ }
+ },
+ close: function () {
+ this.$root.frame_shown = false;
+ },
+ iframeLoaded: function () {
+ if (this.$root.frame_loading) {
+ this.$root.frame_loading = false;
+ this.$root.frame_shown = true;
+ }
+ }
+};
+
+var shared_widget_data = function () {
+ return {
+ async_task_id: null,
+ async_task_check_url: null,
+ async_task_timeout: null,
+ async_task_interval: 100,
+ voucher: null,
+ }
+};
+
+var shared_widget_computed = {
+ frameClasses: function () {
+ return {
+ 'pretix-widget-frame-holder': true,
+ 'pretix-widget-frame-shown': this.$root.frame_shown || this.$root.frame_loading,
+ };
+ },
+ alertClasses: function () {
+ return {
+ 'pretix-widget-alert-holder': true,
+ 'pretix-widget-alert-shown': this.$root.error_message,
+ };
+ },
+};
+
+var shared_loading_fragment = (
+ '