Merge branch 'master' into questions-vue3

# Conflicts:
#	.github/workflows/tests.yml
#	.gitignore
#	Dockerfile
#	package-lock.json
#	package.json
#	src/pretix/base/management/commands/runserver.py
#	src/pretix/base/middleware.py
#	src/pretix/base/templatetags/vite.py
#	src/pretix/control/forms/global_settings.py
#	src/pretix/control/templates/pretixcontrol/checkin/list_edit.html
#	src/pretix/control/views/item.py
#	src/pretix/presale/views/widget.py
#	src/pretix/settings.py
#	src/pretix/static/pretixcontrol/js/ui/checkinrules/App.vue
#	src/pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts
#	src/pretix/static/pretixcontrol/js/ui/checkinrules/django-interop.ts
#	src/pretix/static/pretixcontrol/js/ui/checkinrules/lookup-select2.vue
#	src/pretix/static/pretixcontrol/js/ui/checkinrules/timefield.vue
#	src/pretix/static/pretixcontrol/js/ui/checkinrules/viz-node.vue
#	src/pretix/static/pretixpresale/widget/index.html
#	src/pretix/static/pretixpresale/widget/src/api.ts
#	src/pretix/static/pretixpresale/widget/src/button.ts
#	src/pretix/static/pretixpresale/widget/src/components/PriceBox.vue
#	src/pretix/static/pretixpresale/widget/src/main.ts
#	src/pretix/static/pretixpresale/widget/src/sharedStore.ts
#	src/pretix/static/pretixpresale/widget/src/utils.ts
#	src/pretix/static/pretixpresale/widget/src/widget.ts
#	src/tests/e2e/conftest.py
#	vite.config.ts
This commit is contained in:
Mira Weller
2026-06-17 12:24:49 +02:00
344 changed files with 157817 additions and 151255 deletions
-1
View File
@@ -1,5 +1,4 @@
'use strict';
{
const globals = this;
+14 -3
View File
@@ -53,7 +53,17 @@ function async_task_on_success(data) {
// hide waitingDialog when using browser's history back
waitingDialog.hide();
});
location.href = data.redirect;
if (async_task_is_download && window.self !== window.top) {
// if in an iframe, force to download an async_task_is_download
// e.g. pretix-reseller embeds order-page in iframe, which would cause ticket-PDFs to be displayed inline
var a = document.createElement("a");
a.href = data.redirect;
a.download = "";
a.target = "_blank";
a.click();
} else {
location.href = data.redirect;
}
}
$(this).trigger('pretix:async-task-success', data);
}
@@ -164,15 +174,16 @@ function async_task_error(jqXHR, textStatus, errorThrown) {
var respdom = $(jqXHR.responseText);
var c = respdom.filter('.container');
if (respdom.filter('form') && (respdom.filter('.has-error') || respdom.filter('.alert-danger'))) {
// This is a failed form validation, let's just use it
if (respdom.filter('#page-wrapper') && $('#page-wrapper').length) {
// This is a failed form validation, let's just use it
async_task_replace_page("#page-wrapper", respdom.find("#page-wrapper").html());
} else {
async_task_replace_page("body", jqXHR.responseText.substring(
jqXHR.responseText.indexOf("<body"),
jqXHR.responseText.indexOf("</body")
));
document.dispatchEvent(new Event("pretix:async-task-error"))
}
} else if (c.length > 0) {
+11 -5
View File
@@ -1,5 +1,11 @@
document.getElementById('goback').onclick =
function() {window.history.back()};
document.getElementById('reload').onclick =
function() {window.location.reload(true)};
['DOMContentLoaded', 'pretix:async-task-error'].forEach(function (ev) {
document.addEventListener(ev, function () {
document.querySelectorAll('#goback, #reload').forEach(function (element) {
const regularLoad = ev === 'DOMContentLoaded' && element.id === 'goback';
element.addEventListener('click', regularLoad
? () => window.history.back()
: () => window.location.reload()
);
});
});
});
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { computed } from 'vue'
import { rules as rawRules, items, allProducts, limitProducts } from './django-interop'
import { rules as rawRules, allItems, activeItems, allProducts, limitProducts } from './django-interop'
import { convertToDNF } from './jsonlogic-boolalg'
import RulesEditor from './checkin-rules-editor.vue'
@@ -53,7 +53,7 @@ const missingItems = computed(() => {
}
let missing = []
for (const item of items.value) {
for (const item of activeItems.value) {
if (productsSeen[item.id]) continue
if (!allProducts.value && !limitProducts.value.includes(item.id)) continue
if (item.variations.length > 0) {
@@ -75,15 +75,19 @@ const missingItems = computed(() => {
li.active(role="presentation")
a(href="#rules-edit", role="tab", data-toggle="tab")
span.fa.fa-edit
// space between icon and string
|
| {{ gettext("Edit") }}
li(role="presentation")
a(href="#rules-viz", role="tab", data-toggle="tab")
span.fa.fa-eye
// space between icon and string
|
| {{ gettext("Visualize") }}
//- Tab panes
.tab-content
#rules-edit.tab-pane.active(v-if="items", role="tabpanel")
#rules-edit.tab-pane.active(v-if="allItems", role="tabpanel")
RulesEditor
#rules-viz.tab-pane(role="tabpanel")
RulesVisualization
@@ -191,3 +191,8 @@ export const DATETIME_OPTIONS = {
close: 'fa fa-remove'
}
}
export const TIME_OPTIONS = {
...DATETIME_OPTIONS,
format: document.body.dataset.timeformat,
}
@@ -17,7 +17,7 @@ export const rules = ref<any>({})
// grab rules from hidden input
const rulesInput = document.querySelector<HTMLInputElement>('#id_rules')
if (rulesInput?.value) {
rules.value = JSON.parse(rulesInput.value)
rules.value = JSON.parse(rulesInput.value) ?? {}
}
// sync back to hidden input
@@ -26,11 +26,13 @@ watch(rules, (newVal) => {
rulesInput.value = JSON.stringify(newVal)
}, { deep: true })
export const items = ref<any[]>([])
export const activeItems = ref<any[]>([])
export const allItems = ref<any[]>([])
const itemsEl = document.querySelector('#items')
if (itemsEl?.textContent) {
items.value = JSON.parse(itemsEl.textContent || '[]')
allItems.value = JSON.parse(itemsEl.textContent || '[]')
activeItems.value = allItems.value.filter(item => item.active)
function checkForInvalidIds (validProducts: Record<string, string>, validVariations: Record<string, string>, rule: any) {
if (rule['and']) {
@@ -57,12 +59,12 @@ if (itemsEl?.textContent) {
}
checkForInvalidIds(
Object.fromEntries(items.value.map(p => [p.id, p.name])),
Object.fromEntries(items.value.flatMap(p => p.variations?.map(v => [v.id, p.name + ' ' + v.name]) ?? [])),
Object.fromEntries(allItems.value.map(p => [p.id, p.name])),
Object.fromEntries(allItems.value.flatMap(p => p.variations?.map(v => [v.id, p.name + ' ' + v.name]) ?? [])),
rules.value
)
}
export const productSelectURL = ref(document.querySelector('#product-select2')?.textContent)
export const variationSelectURL = ref(document.querySelector('#variations-select2')?.textContent)
export const gateSelectURL = ref(document.querySelector('#gate-select2')?.textContent)
export const gateSelectURL = ref(document.querySelector('#gates-select2')?.textContent)
@@ -100,14 +100,19 @@ watch(() => props.value, (newval, oldval) => {
}
})
let rawSelectEl: HTMLSelectElement | null = null
onMounted(() => {
rawSelectEl = select.value
build()
})
onUnmounted(() => {
$(select.value)
if (!rawSelectEl) return
$(rawSelectEl)
.off()
.select2('destroy')
rawSelectEl = null
})
</script>
<template lang="pug">
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { ref, watch, onMounted, onUnmounted } from 'vue'
import { DATETIME_OPTIONS } from './constants'
import { TIME_OPTIONS } from './constants'
const props = defineProps<{
required?: boolean
@@ -20,7 +20,7 @@ watch(() => props.value, (val) => {
onMounted(() => {
$(input.value)
.datetimepicker({
...DATETIME_OPTIONS,
...TIME_OPTIONS,
showClear: props.required,
})
.trigger('change')
@@ -180,7 +180,7 @@ g
br
span(v-if="varresult !== null") {{ varresult }}
strong
| {{ op.label }} {{ rightoperand }}
| {{ op?.label}} {{ rightoperand }}
span(v-else-if="vardata && vardata.type === 'int_by_datetime'")
span.fa.fa-sign-in(v-if="variable.startsWith('entries_')")
| {{ vardata.label }}
@@ -193,21 +193,21 @@ g
br
span(v-if="varresult !== null") {{ varresult }}
strong
| {{ op.label }} {{ rightoperand }}
| {{ op?.label }} {{ rightoperand }}
span(v-else-if="vardata && variable === 'now'")
span.fa.fa-clock-o
| {{ vardata.label }}
br
span(v-if="varresult !== null") {{ varresult }}
strong
| {{ op.label }}
| {{ op?.label }}
br
span(v-if="rightoperand.buildTime[0] === 'custom'")
| {{ df(rightoperand.buildTime[1]) }}
span(v-else-if="rightoperand.buildTime[0] === 'customtime'")
| {{ tf(rightoperand.buildTime[1]) }}
span(v-if="rightoperand?.buildTime[0] === 'custom'")
| {{ df(rightoperand?.buildTime[1]) }}
span(v-else-if="rightoperand?.buildTime[0] === 'customtime'")
| {{ tf(rightoperand?.buildTime[1]) }}
span(v-else)
| {{ TEXTS[rightoperand.buildTime[0]] }}
| {{ TEXTS[rightoperand?.buildTime[0]] }}
span(v-if="operands[2]")
span(v-if="operator === 'isBefore'") +
span(v-else) -
@@ -220,14 +220,14 @@ g
span(v-if="varresult !== null") ({{ varresult }})
br
strong
| {{ rightoperand.objectList.map((o: any) => o.lookup[2]).join(", ") }}
| {{ rightoperand?.objectList.map((o: any) => o.lookup[2]).join(", ") }}
span(v-else-if="vardata && vardata.type === 'enum_entry_status'")
span.fa.fa-check-circle-o
| {{ vardata.label }}
span(v-if="varresult !== null") ({{ varresult }})
br
strong
| {{ op.label }} {{ rightoperand }}
| {{ op?.label }} {{ rightoperand }}
g(v-if="result === false", :transform="`translate(${x + boxWidth - 15}, ${y - 10})`")
ellipse(fill="#fff", cx="14.685823", cy="14.318233", rx="12.140151", ry="11.55523")
@@ -71,6 +71,7 @@ $(document).ajaxError(function (event, jqXHR, settings, thrownError) {
});
var form_handlers = function (el) {
el.trigger("rescan.areYouSure");
el.find("[data-formset]").formset(
{
animateForms: true,
@@ -638,11 +639,13 @@ var form_handlers = function (el) {
).append(" ").append($("<div>").text(res.organizer).html())
);
}
$ret.append(
$("<span>").addClass("event-daterange").append(
$("<span>").addClass("fa fa-calendar fa-fw")
).append(" ").append(res.date_range)
);
if (res.date_range) {
$ret.append(
$("<span>").addClass("event-daterange").append(
$("<span>").addClass("fa fa-calendar fa-fw")
).append(" ").append(res.date_range)
);
}
return $ret;
},
}).on("select2:select", function () {
@@ -864,6 +864,9 @@ tbody th {
.checkin-sim-result-status-incomplete {
background: $brand-primary;
}
.checkin-sim-result-status-exchange {
background: $brand-primary;
}
.checkin-sim-result-status-error {
background: $brand-danger;
}
@@ -110,8 +110,17 @@ var setCookie = function (cname, cvalue, exdays) {
var d = new Date();
d.setTime(d.getTime() + (exdays * 24 * 60 * 60 * 1000));
var expires = "expires=" + d.toUTCString();
document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/";
if (!cvalue) {
var expires = "expires=Thu, 01 Jan 1970 00:00:00 GMT";
cvalue = "";
}
var same_site = "";
if (site_is_secure()) {
same_site = ";SameSite=None;Secure"
}
document.cookie = cname + "=" + cvalue + ";" + expires + same_site + ";path=/";
};
var getCookie = function (name) {
var value = "; " + document.cookie;
var parts = value.split("; " + name + "=");
@@ -726,17 +735,16 @@ var shared_methods = {
buy_callback: function (data) {
if (data.redirect) {
if (data.cart_id) {
this.$root.cart_id = data.cart_id;
setCookie(this.$root.cookieName, data.cart_id, 30);
this.$root.set_cart_id(data.cart_id);
}
if (data.redirect.substr(0, 1) === '/') {
data.redirect = this.$root.target_url.replace(/^([^\/]+:\/\/[^\/]+)\/.*$/, "$1") + data.redirect;
}
var url = data.redirect;
if (url.indexOf('?')) {
url = url + '&iframe=1&locale=' + lang + '&take_cart_id=' + this.$root.cart_id;
url = url + '&iframe=1&locale=' + lang + '&take_cart_id=' + encodeURIComponent(this.$root.get_cart_id());
} else {
url = url + '?iframe=1&locale=' + lang + '&take_cart_id=' + this.$root.cart_id;
url = url + '?iframe=1&locale=' + lang + '&take_cart_id=' + encodeURIComponent(this.$root.get_cart_id());
}
url += this.$root.consent_parameter;
if (this.$root.additionalURLParams) {
@@ -779,15 +787,24 @@ var shared_methods = {
}
},
resume: function () {
if (!this.$root.get_cart_id() && this.$root.keep_cart) {
// create an empty cart whose id we can persist
this.$root.create_cart(this.resume)
return;
}
var redirect_url;
redirect_url = this.$root.target_url + 'w/' + widget_id + '/';
if (this.$root.subevent && !this.$root.cart_id) {
if (this.$root.subevent && this.$root.is_button && this.$root.items.length === 0) {
// button with subevent but no items
redirect_url += this.$root.subevent + '/';
}
redirect_url += '?iframe=1&locale=' + lang;
if (this.$root.cart_id) {
redirect_url += '&take_cart_id=' + this.$root.cart_id;
if (this.$root.get_cart_id()) {
redirect_url += '&take_cart_id=' + encodeURIComponent(this.$root.get_cart_id());
if (this.$root.keep_cart) {
// make sure the cart-id is used, even if the cart is currently empty
redirect_url += '&ajax=1'
}
}
if (this.$root.widget_data) {
redirect_url += '&widget_data=' + encodeURIComponent(this.$root.widget_data_json);
@@ -1864,12 +1881,11 @@ var shared_root_methods = {
if (this.$root.variation_filter) {
url += '&variations=' + encodeURIComponent(this.$root.variation_filter);
}
var cart_id = getCookie(this.cookieName);
if (this.$root.voucher_code) {
url += '&voucher=' + encodeURIComponent(this.$root.voucher_code);
}
if (cart_id) {
url += "&cart_id=" + encodeURIComponent(cart_id);
if (this.$root.get_cart_id()) {
url += "&cart_id=" + encodeURIComponent(this.$root.get_cart_id());
}
if (this.$root.date !== null) {
url += "&date=" + this.$root.date.substr(0, 7);
@@ -1939,7 +1955,6 @@ var shared_root_methods = {
root.display_add_to_cart = data.display_add_to_cart;
root.waiting_list_enabled = data.waiting_list_enabled;
root.show_variations_expanded = data.show_variations_expanded || !!root.variation_filter;
root.cart_id = cart_id;
root.cart_exists = data.cart_exists;
root.vouchers_exist = data.vouchers_exist;
root.has_seating_plan = data.has_seating_plan;
@@ -2004,8 +2019,8 @@ var shared_root_methods = {
if (this.$root.voucher_code) {
redirect_url += '&voucher=' + encodeURIComponent(this.$root.voucher_code);
}
if (this.$root.cart_id) {
redirect_url += '&take_cart_id=' + this.$root.cart_id;
if (this.$root.get_cart_id()) {
redirect_url += '&take_cart_id=' + encodeURIComponent(this.$root.get_cart_id());
}
if (this.$root.widget_data) {
redirect_url += '&widget_data=' + encodeURIComponent(this.$root.widget_data_json);
@@ -2027,7 +2042,40 @@ var shared_root_methods = {
this.$root.subevent = event.subevent;
this.$root.loading++;
this.$root.reload();
}
},
create_cart: function(callback) {
var url = this.$root.target_url + 'w/' + widget_id + '/cart/create?ajax=1';
this.$root.overlay.frame_loading = true;
api._getJSON(url, (data) => {
this.$root.set_cart_id(data.cart_id);
this.$root.overlay.frame_loading = false;
callback()
}, (xhr, data) => {
if (xhr.status === 429 && typeof xhr.responseURL !== "undefined") {
this.$root.overlay.error_message = strings['cart_error_429'];
this.$root.overlay.frame_loading = false;
this.$root.overlay.error_url_after = this.$root.newTabTarget;
this.$root.overlay.error_url_after_new_tab = true;
} else {
this.$root.overlay.error_message = strings['cart_error'];
this.$root.overlay.frame_loading = false;
}
})
},
get_cart_id: function() {
if (!this.$root.keep_cart) {
return null
}
if (this.$root.cart_id) {
return this.$root.cart_id
}
return getCookie(this.$root.cookieName);
},
set_cart_id: function(newValue) {
this.$root.cart_id = newValue
setCookie(this.$root.cookieName, newValue, 30);
},
};
var shared_root_computed = {
@@ -2049,9 +2097,8 @@ var shared_root_computed = {
},
voucherFormTarget: function () {
var form_target = this.target_url + 'w/' + widget_id + '/redeem?iframe=1&locale=' + lang;
var cookie = getCookie(this.cookieName);
if (cookie) {
form_target += "&take_cart_id=" + cookie;
if (this.get_cart_id()) {
form_target += "&take_cart_id=" + encodeURIComponent(this.get_cart_id());
}
if (this.subevent) {
form_target += "&subevent=" + this.subevent;
@@ -2091,9 +2138,8 @@ var shared_root_computed = {
checkout_url += '?' + this.$root.additionalURLParams;
}
var form_target = this.target_url + 'w/' + widget_id + '/cart/add?iframe=1&next=' + encodeURIComponent(checkout_url);
var cookie = getCookie(this.cookieName);
if (cookie) {
form_target += "&take_cart_id=" + cookie;
if (this.get_cart_id()) {
form_target += "&take_cart_id=" + encodeURIComponent(this.get_cart_id());
}
form_target += this.$root.consent_parameter
return form_target
@@ -2103,7 +2149,14 @@ var shared_root_computed = {
if (this.subevent) {
target = this.target_url + this.subevent + '/';
}
return target;
var parameters = this.$root.consent_parameter
if (this.$root.additionalURLParams) {
parameters += `&${this.$root.additionalURLParams}`
}
if (parameters) {
target += '?' + parameters.replace(/^&/, '')
}
return target
},
useIframe: function () {
if (window.crossOriginIsolated === true) {
@@ -2329,6 +2382,8 @@ var create_widget = function (element, html_id=null) {
has_seating_plan: false,
has_seating_plan_waitinglist: false,
meta_filter_fields: [],
keep_cart: true,
cart_id: null
}
},
created: function () {
@@ -2366,6 +2421,7 @@ var create_button = function (element, html_id=null) {
var raw_items = element.attributes.items ? element.attributes.items.value : "";
var skip_ssl = element.attributes["skip-ssl-check"] ? true : false;
var disable_iframe = element.attributes["disable-iframe"] ? true : false;
var keep_cart = element.attributes["keep-cart"] ? true : false;
var button_text = element.innerHTML;
var widget_data = JSON.parse(JSON.stringify(window.PretixWidget.widget_data));
for (var i = 0; i < element.attributes.length; i++) {
@@ -2417,7 +2473,9 @@ var create_button = function (element, html_id=null) {
widget_data: widget_data,
widget_id: 'pretix-widget-' + widget_id,
html_id: html_id,
button_text: button_text
button_text: button_text,
keep_cart: keep_cart || items.length > 0,
cart_id: null
}
},
created: function () {
@@ -2426,7 +2484,7 @@ var create_button = function (element, html_id=null) {
observer.observe(this.$el, observerOptions);
},
computed: shared_root_computed,
methods: shared_root_methods
methods: shared_root_methods,
});
create_overlay(app);
return app;
@@ -2492,13 +2550,15 @@ window.PretixWidget.open = function (target_url, voucher, subevent, items, widge
frame_dismissed: false,
widget_data: all_widget_data,
widget_id: 'pretix-widget-' + widget_id,
button_text: ""
button_text: "",
keep_cart: true,
cart_id: null
}
},
created: function () {
},
computed: shared_root_computed,
methods: shared_root_methods
methods: shared_root_methods,
});
create_overlay(app);
app.$nextTick(function () {
@@ -201,13 +201,16 @@ footer nav li:not(:first-child):before {
width: 1.5em;
text-align: center;
}
footer nav .btn,
footer nav .btn-link {
display: inline;
padding: 0;
margin: 0;
font-size: 11px;
vertical-align: baseline;
}
footer nav .btn-link {
padding: 0;
}
.js-only {
display: none;
@@ -966,6 +966,7 @@ $table-bg-accent: rgba(128, 128, 128, 0.05);
width: 80vw;
max-width: 1080px;
height: 80vh;
max-height: 100dvh;
}
.pretix-widget-frame-inner iframe {
width: 100% !important;
@@ -4,12 +4,44 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Pretix Widget</title>
<link rel="stylesheet" type="text/css" href="http://localhost:8000/testorg/testevent/widget/v2.css" crossorigin>
<link id="widget-css" rel="stylesheet" type="text/css" crossorigin>
</head>
<body>
<pretix-widget event="http://localhost:8000/testorg/testevent/"></pretix-widget>
<!-- <script type="text/javascript" src="http://localhost:8000/widget/v2.en.js" async crossorigin></script> -->
<div id="widget-container"></div>
<script>
{
const params = new URLSearchParams(window.location.search)
const knownParams = new Set(['type', 'host', 'org', 'event', 'mode', 'lang', 'button-text'])
const type = params.get('type') || 'widget'
const host = params.get('host') || 'http://localhost:8000'
const org = params.get('org') || 'testorg'
const event = params.get('event') || 'testevent'
const mode = params.get('mode') || 'dev'
const lang = params.get('lang') || 'de'
<script type="module" src="/src/main.ts"></script>
const baseUrl = `${host}/${org}/${event}`
document.getElementById('widget-css').href = `${baseUrl}/widget/v2.css`
const el = document.createElement(type === 'button' ? 'pretix-button' : 'pretix-widget')
el.setAttribute('event', `${baseUrl}/`)
if (type === 'button') {
el.textContent = params.get('button-text') || 'Buy tickets!'
}
for (const [key, value] of params) {
if (knownParams.has(key)) continue
el.setAttribute(key, value)
}
document.getElementById('widget-container').appendChild(el)
const script = document.createElement('script')
if (mode === 'prod') {
Object.assign(script, { type: 'text/javascript', src: `${host}/widget/v2.${lang}.js`, async: true, crossOrigin: 'anonymous' })
} else {
Object.assign(script, { type: 'module', src: '/src/main.ts' })
}
document.body.appendChild(script)
}
</script>
</body>
</html>
@@ -83,3 +83,11 @@ export async function checkAsyncTask (url: string) {
}
return await response.json() as CartResponse
}
export async function createCart (url: string) {
const response = await fetch(url)
if (!response.ok) {
throw new ApiError(response.status, response.url)
}
return await response.json() as CartResponse
}
@@ -39,7 +39,8 @@ export function createButtonInstance (element: Element, htmlId?: string): App {
htmlId: htmlId || element.id || makeid(16),
isButton: true,
buttonItems,
buttonText: element.innerHTML
buttonText: element.innerHTML,
keepCart: 'keep-cart' in element.attributes || buttonItems.length > 0,
})
const observer = new MutationObserver((mutationList) => {
@@ -54,13 +55,6 @@ export function createButtonInstance (element: Element, htmlId?: string): App {
}
})
// TODO I don't think we need this anymore in vue3
// if (element.tagName !== 'pretix-button') {
// element.innerHTML = '<pretix-button>' + element.innerHTML + '</pretix-button>'
// // Vue does not replace the container, so watch container as well
// observer.observe(element, observerOptions)
// }
const app = createApp(ButtonComponent)
app.provide(StoreKey, store)
app.config.errorHandler = (error, _vm, info) => {
@@ -93,11 +93,11 @@ const showTaxline = computed(() => props.price.rate !== '0.00' && props.price.gr
span(v-if="!freePrice && !originalPrice", v-html="priceline")
span(v-if="!freePrice && originalPrice")
del.pretix-widget-pricebox-original-price(:aria-label="originalPriceAriaLabel", v-html="originalLine")
|
|!{' '}
ins.pretix-widget-pricebox-new-price(:aria-label="newPriceAriaLabel", v-html="priceline")
div(v-if="freePrice")
span.pretix-widget-pricebox-currency(:id="priceBoxId") {{ store.currency }}
|
|!{' '}
input.pretix-widget-pricebox-price-input(
type="number",
placeholder="0",
@@ -48,10 +48,6 @@ window.PretixWidget = {
}
async function buildWidgets () {
// TODO what does this do?
document.createElement('pretix-widget')
document.createElement('pretix-button')
await docReady()
const widgetElements = document.querySelectorAll('pretix-widget, div.pretix-widget-compat')
for (const [i, el] of Array.from(widgetElements).entries()) {
@@ -96,6 +92,7 @@ function openWidget (
isButton: true,
buttonItems: items ?? [],
buttonText: '',
keepCart: true
})
const app = createApp(ButtonComponent)
@@ -1,9 +1,9 @@
import { nextTick, type InjectionKey } from 'vue'
import { createStore } from '~/lib/store'
import { fetchProductList, submitCart, checkAsyncTask, ApiError } from '~/api'
import { fetchProductList, submitCart, checkAsyncTask, ApiError, createCart } from '~/api'
import type { CartResponse } from '~/api'
import { STRINGS } from '~/i18n'
import { setCookie, getCookie, makeid } from '~/utils'
import { setCookie, getCookie, makeid, siteIsSecure } from '~/utils'
import type { Category, DayEntry, EventEntry, LightboxState, MetaFilterField, WidgetData } from '~/types'
export const globalWidgetId = makeid(16)
@@ -28,6 +28,7 @@ export function createWidgetStore (config: {
variations?: string | null
widgetData: WidgetData
htmlId: string
keepCart: boolean
// Button-specific
buttonItems?: { item: string; count: string }[]
buttonText?: string
@@ -54,6 +55,7 @@ export function createWidgetStore (config: {
widgetData: config.widgetData,
widgetId: `pretix-widget-${globalWidgetId}`,
htmlId: config.htmlId,
keepCart: config.keepCart,
// View state
view: null as 'event' | 'events' | 'weeks' | 'days' | null,
@@ -74,7 +76,7 @@ export function createWidgetStore (config: {
displayAddToCart: false,
waitingListEnabled: false,
showVariationsExpanded: !!config.variations,
cartId: null as string | null,
_cartId: null as string | null,
cartExists: false,
vouchersExist: false,
hasSeatingPlan: false,
@@ -123,13 +125,18 @@ export function createWidgetStore (config: {
getters: {
useIframe (): boolean {
if ((window as any).crossOriginIsolated === true) return false
return !this.disableIframe && (this.skipSsl || /https.*/.test(document.location.protocol))
return !this.disableIframe && (this.skipSsl || siteIsSecure())
},
cookieName (): string {
return `pretix_widget_${this.targetUrl.replace(/[^a-zA-Z0-9]+/g, '_')}`
},
cartIdFromCookie (): string | null {
return getCookie(this.cookieName) ?? null
cartId (): string | null {
if (this._cartId) {
return this._cartId
}
if (this.keepCart) {
return getCookie(this.cookieName) ?? null
}
},
widgetDataJson (): string {
const cloned = { ...this.widgetData }
@@ -155,7 +162,15 @@ export function createWidgetStore (config: {
return params.toString()
},
newTabTarget (): string {
return this.subevent ? `${this.targetUrl}${this.subevent}/` : this.targetUrl
let url = this.subevent ? `${this.targetUrl}${this.subevent}/` : this.targetUrl
let parameters = this.consentParameter
if (this.additionalURLParams) {
parameters += `&${this.additionalURLParams}`
}
if (parameters) {
url += '?' + parameters.replace(/^&/, '')
}
return url
},
formTarget (): string {
const isFirefox = navigator.userAgent.toLowerCase().includes('firefox')
@@ -187,12 +202,12 @@ export function createWidgetStore (config: {
}
let formTarget = `${this.targetUrl}w/${globalWidgetId}/cart/add?iframe=1&next=${encodeURIComponent(checkoutUrl)}`
if (this.cartIdFromCookie) {
formTarget += `&take_cart_id=${this.cartIdFromCookie}`
if (this.cartId) {
formTarget += `&take_cart_id=${this.cartId}`
}
formTarget += this.consentParameter
return formTarget
},
}
},
actions: {
triggerLoadCallback () {
@@ -219,8 +234,7 @@ export function createWidgetStore (config: {
if (this.variationFilter) url += `&variations=${encodeURIComponent(this.variationFilter)}`
if (this.voucherCode) url += `&voucher=${encodeURIComponent(this.voucherCode)}`
const cartIdCookie = this.cartIdFromCookie
if (cartIdCookie) url += `&cart_id=${encodeURIComponent(cartIdCookie)}`
if (this.cartId) url += `&cart_id=${encodeURIComponent(this.cartId)}`
if (this.date !== null) {
url += `&date=${this.date.substring(0, 7)}`
} else if (this.week !== null) {
@@ -291,7 +305,6 @@ export function createWidgetStore (config: {
this.displayAddToCart = data.display_add_to_cart ?? false
this.waitingListEnabled = data.waiting_list_enabled ?? false
this.showVariationsExpanded = data.show_variations_expanded || !!this.variationFilter
this.cartId = cartIdCookie
this.cartExists = data.cart_exists ?? false
this.vouchersExist = data.vouchers_exist ?? false
this.hasSeatingPlan = data.has_seating_plan ?? false
@@ -335,12 +348,13 @@ export function createWidgetStore (config: {
this.loading--
this.triggerLoadCallback()
}
throw e
}
},
getVoucherFormTarget (): string {
let formTarget = `${this.targetUrl}w/${globalWidgetId}/redeem?iframe=1&locale=${LANG}`
if (this.cartIdFromCookie) {
formTarget += `&take_cart_id=${this.cartIdFromCookie}`
if (this.cartId) {
formTarget += `&take_cart_id=${this.cartId}`
}
if (this.subevent) {
formTarget += `&subevent=${this.subevent}`
@@ -357,8 +371,7 @@ export function createWidgetStore (config: {
handleCartResponse (data: CartResponse) {
if (data.redirect) {
if (data.cart_id) {
this.cartId = data.cart_id
setCookie(this.cookieName, data.cart_id, 30)
this.setCartId(data.cart_id)
}
let url = data.redirect
@@ -436,6 +449,7 @@ export function createWidgetStore (config: {
this.overlay.frameLoading = false
this.overlay.errorUrlAfter = this.newTabTarget
this.overlay.errorUrlAfterNewTab = true
return
} else if (e.status === 405) {
// Likely a redirect!
this.targetUrl = e.responseUrl.substring(0, e.responseUrl.indexOf('/cart/add') - 18)
@@ -448,6 +462,27 @@ export function createWidgetStore (config: {
this.overlay.frameLoading = false
}
},
async createCart () {
const url = `${this.targetUrl}w/${globalWidgetId}/cart/create?ajax=1`
try {
this.overlay.frameLoading = true
const data = await createCart(url)
this.setCartId(data.cart_id)
return true
} catch (e) {
if (e instanceof ApiError && e.status === 429) {
this.overlay.errorMessage = STRINGS.cart_error_429
this.overlay.frameLoading = false
this.overlay.errorUrlAfter = this.newTabTarget
this.overlay.errorUrlAfterNewTab = true
} else if (e instanceof ApiError && (e.status === 200 || (e.status >= 400 && e.status < 500))) {
this.overlay.errorMessage = STRINGS.cart_error
this.overlay.frameLoading = false
}
return false
}
},
redeem (voucherCode: string, event?: Event) {
if (!this.useIframe) return
if (event) event.preventDefault()
@@ -462,15 +497,24 @@ export function createWidgetStore (config: {
window.open(redirectUrl)
}
},
resume () {
async resume () {
if (!this.cartId && this.keepCart) {
// create an empty cart whose id we can persist
if (!await this.createCart()) return
}
let redirectUrl = `${this.targetUrl}w/${globalWidgetId}/`
if (this.subevent && this.isButton && this.items.length === 0) {
// button with subevent but no items
redirectUrl += `${this.subevent}/`
}
if (this.subevent && !this.cartId) {
// button with subevent but no items
redirectUrl += `${this.subevent}/`
}
redirectUrl += `?iframe=1&locale=${LANG}`
if (this.cartId) {
redirectUrl += `&take_cart_id=${this.cartId}`
// ajax to make sure the cart-id is used, even if the cart is currently empty
redirectUrl += `&take_cart_id=${this.cartId}&ajax=1`
}
if (this.widgetData) {
redirectUrl += `&widget_data=${encodeURIComponent(this.widgetDataJson)}`
@@ -523,6 +567,10 @@ export function createWidgetStore (config: {
} else {
window.open(redirectUrl)
}
},
setCartId (cartId: string) {
this._cartId = cartId
setCookie(this.cookieName, cartId, 30)
}
}
})
@@ -7,7 +7,7 @@ export function setCookie (cname: string, cvalue: string, exdays: number): void
const d = new Date()
d.setTime(d.getTime() + exdays * 24 * 60 * 60 * 1000)
const expires = `expires=${d.toUTCString()}`
document.cookie = `${cname}=${cvalue};${expires};path=/`
document.cookie = `${cname}=${cvalue};${expires};${siteIsSecure() ? 'SameSite=None;Secure;' : ''}path=/`
}
export function getCookie (name: string): string | null {
@@ -38,6 +38,7 @@ export function createWidgetInstance (element: Element, htmlId?: string): App {
variations: element.attributes.variations?.value || null,
widgetData,
htmlId: htmlId || element.id || makeid(16),
keepCart: true
})
const observer = new MutationObserver((mutationList) => {
@@ -50,13 +51,6 @@ export function createWidgetInstance (element: Element, htmlId?: string): App {
}
})
// TODO I don't think we need this anymore in vue3
// if (element.tagName !== 'pretix-widget') {
// element.innerHTML = '<pretix-widget></pretix-widget>'
// // we need to watch the container as well as the replaced root-node (see mounted())
// observer.observe(element, observerOptions)
// }
const app = createApp(WidgetComponent)
app.provide(StoreKey, store)
app.config.errorHandler = (error, _vm, info) => {