From edd5f29524b10ada357672aadf5e13de0cb22358 Mon Sep 17 00:00:00 2001 From: Kara Engelhardt Date: Tue, 21 Jul 2026 18:41:55 +0200 Subject: [PATCH] WIP --- .../pretixplugins/wallet/components/app.vue | 65 +++-- .../components/preview/fieldgroup-preview.vue | 7 +- .../components/preview/fixed-preview.vue | 58 ++++ .../components/preview/pass-preview.vue | 3 +- .../placeholder-fieldgroup-preview.vue | 5 +- .../preview/predefined-fieldgroup-preview.vue | 79 ++++++ .../wallet/components/preview/row-preview.vue | 16 +- src/pretix/plugins/wallet/styles/apple.py | 46 +++- src/pretix/plugins/wallet/styles/base.py | 2 +- src/pretix/plugins/wallet/styles/google.py | 258 ++++++++++++++---- src/pretix/plugins/wallet/ticketoutput.py | 15 +- 11 files changed, 431 insertions(+), 123 deletions(-) create mode 100644 src/pretix/plugins/wallet/static/pretixplugins/wallet/components/preview/fixed-preview.vue create mode 100644 src/pretix/plugins/wallet/static/pretixplugins/wallet/components/preview/predefined-fieldgroup-preview.vue diff --git a/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/app.vue b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/app.vue index 4111f7e500..fcd8c1788c 100644 --- a/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/app.vue +++ b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/app.vue @@ -10,49 +10,48 @@ const store = inject(StoreKey)!; const gettext = (window as any).gettext; function openForm(url: string, data: Record) { - let form = document.createElement("form"); - form.target = "_blank"; - form.method = "POST"; - form.action = url; - form.style.display = "none"; + let form = document.createElement("form"); + form.target = "_blank"; + form.method = "POST"; + form.action = url; + form.style.display = "none"; - for (var key in data) { - var input = document.createElement("input"); - input.type = "hidden"; - input.name = key; - input.value = data[key]; - form.appendChild(input); - } - document.body.appendChild(form); - form.submit(); - document.body.removeChild(form); + for (var key in data) { + var input = document.createElement("input"); + input.type = "hidden"; + input.name = key; + input.value = data[key]; + form.appendChild(input); + } + document.body.appendChild(form); + form.submit(); + document.body.removeChild(form); } function openPreview(e: SubmitEvent) { - e.preventDefault(); - openForm("../../preview/", { - csrfmiddlewaretoken: store.csrfToken, - platform: store.currentPlatform, - style: store.currentPlatformLayout.style, - layout: JSON.stringify(store.currentPlatformLayout.layout), - }); + e.preventDefault(); + openForm("../../preview/", { + csrfmiddlewaretoken: store.csrfToken, + platform: store.currentPlatform, + style: store.currentPlatformLayout.style, + layout: JSON.stringify(store.currentPlatformLayout.layout), + }); } - - const platformChoices = computed(() => { - return [ - [null, "Do not generate pass"], - ...Object.values(store.currentPlatformStyles).map((x) => [ - x.identifier, - x.name, - ]), - ]; + return [ + [null, "Do not generate pass"], + ...Object.values(store.currentPlatformStyles).map((x) => [ + x.identifier, + x.name, + ]), + ]; }); const preview_layout = computed(() => { - return JSON.parse(store.currentPlatformStyles[store.currentPlatformLayout.style].preview_layout) -}) + return store.currentPlatformStyles[store.currentPlatformLayout.style] + .preview_layout; +}); diff --git a/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/preview/fixed-preview.vue b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/preview/fixed-preview.vue new file mode 100644 index 0000000000..ae05605464 --- /dev/null +++ b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/preview/fixed-preview.vue @@ -0,0 +1,58 @@ + + + + + + \ No newline at end of file diff --git a/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/preview/pass-preview.vue b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/preview/pass-preview.vue index dfdf7258ea..f8c890805b 100644 --- a/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/preview/pass-preview.vue +++ b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/preview/pass-preview.vue @@ -11,8 +11,9 @@ const props = defineProps<{ layout: Array }>(); + + \ No newline at end of file diff --git a/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/preview/predefined-fieldgroup-preview.vue b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/preview/predefined-fieldgroup-preview.vue new file mode 100644 index 0000000000..2b80119ee4 --- /dev/null +++ b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/preview/predefined-fieldgroup-preview.vue @@ -0,0 +1,79 @@ + + + + + diff --git a/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/preview/row-preview.vue b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/preview/row-preview.vue index ccb1d3a1f1..db4efd7532 100644 --- a/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/preview/row-preview.vue +++ b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/preview/row-preview.vue @@ -1,5 +1,6 @@ \ No newline at end of file + div.preview-row(v-if="'children' in config" :style="{flexDirection: config.direction || 'row'}") + RowPreview(v-for="child of config.children" :config="child") + FieldgroupPreview(v-else-if="'fieldgroup' in config" :config="config") + FixedPreview(v-else-if="'value' in config" :config="config") + + + \ No newline at end of file diff --git a/src/pretix/plugins/wallet/styles/apple.py b/src/pretix/plugins/wallet/styles/apple.py index 3462443d80..d12c7f7a56 100644 --- a/src/pretix/plugins/wallet/styles/apple.py +++ b/src/pretix/plugins/wallet/styles/apple.py @@ -219,6 +219,7 @@ class AppleWalletStyle(PassStyle): pkpass.add_file("pass.json", json.dumps(pass_json)) return pkpass.finish() + class AppleWalletEventTicket(AppleWalletStyle): identifier = "event_1" name = _("Event Ticket Layout 1") @@ -283,7 +284,30 @@ class AppleWalletEventTicket(AppleWalletStyle): ), TextFieldGroup(identifier="back", name=_("Back")), ] - preview_layout = '[[{"children":[{"fieldgroup":"logo","relSize":1},{"fieldgroup":"logo_text","relSize":3,"display":["bold","large","centered"]},{"fieldgroup":"header","relSize":2,"display":["large", "tight"]}]},{"fieldgroup":"primary","display":"large"},{"fieldgroup":"secondary"},{"fieldgroup":"auxiliary"},{"fieldgroup":"code"}],[{"fieldgroup":"back","direction":"column"}]]' + preview_layout = [ + [ + { + "children": [ + {"fieldgroup": "logo", "relSize": 1}, + { + "fieldgroup": "logo_text", + "relSize": 3, + "display": ["bold", "large", "centered"], + }, + { + "fieldgroup": "header", + "relSize": 2, + "display": ["large", "tight"], + }, + ] + }, + {"fieldgroup": "primary", "display": "large"}, + {"fieldgroup": "secondary"}, + {"fieldgroup": "auxiliary"}, + {"fieldgroup": "code"}, + ], + [{"fieldgroup": "back", "direction": "column"}], + ] def convert_fields(self, strings, fields, prefix): converted = [] @@ -314,7 +338,9 @@ class AppleWalletEventTicket(AppleWalletStyle): strings, fields["auxiliary"], "auxiliary" ), "backFields": self.convert_fields(strings, fields["back"], "back"), - "headerFields": self.convert_fields(strings, fields["header"], "header"), + "headerFields": self.convert_fields( + strings, fields["header"], "header" + ), }, } if fields["logo_text"]: @@ -322,11 +348,13 @@ class AppleWalletEventTicket(AppleWalletStyle): strings, fields["logo_text"], "logo_text" )[0]["value"] - if fields['code']: - content["barcodes"] = [{ - "format": "PKBarcodeFormatQR", - "message": str(fields["code"][0]["value"]), - "messageEncoding": "utf-8", - "altText": str(fields["code"][0]["value"]) - }] + if fields["code"]: + content["barcodes"] = [ + { + "format": "PKBarcodeFormatQR", + "message": str(fields["code"][0]["value"]), + "messageEncoding": "utf-8", + "altText": str(fields["code"][0]["value"]), + } + ] return content diff --git a/src/pretix/plugins/wallet/styles/base.py b/src/pretix/plugins/wallet/styles/base.py index 30de3eb727..fac61f095f 100644 --- a/src/pretix/plugins/wallet/styles/base.py +++ b/src/pretix/plugins/wallet/styles/base.py @@ -232,7 +232,7 @@ class PassStyle: # order here limits in what order users can configure field "overspilling" (if too many fields are defined, where should the rest go) -> can only go down in the list # we evaluate the fields in this order, so they overspill in this order as well (fields from primary are appended to the overspilling field before fields from secondary are etc) fieldgroups: list[FieldGroup] - preview_layout: str | None + preview_layout: list | None def asdict(self): return { diff --git a/src/pretix/plugins/wallet/styles/google.py b/src/pretix/plugins/wallet/styles/google.py index 8d31226df5..c78477845f 100644 --- a/src/pretix/plugins/wallet/styles/google.py +++ b/src/pretix/plugins/wallet/styles/google.py @@ -1,40 +1,71 @@ from pretix.base.models import Event, OrderPosition -from .base import FieldGroupDisplay, PassLayout, PassStyle, PredefinedFieldGroup, TextFieldGroup, WalletPlatform +from .base import ( + FieldGroupDisplay, + ImageFieldGroup, + PassLayout, + PassStyle, + PlaceholderFieldEntry, + PredefinedFieldGroup, + TextFieldGroup, + WalletPlatform, +) from django.utils.translation import gettext_lazy as _, gettext import json from walletobjects import ButtonJWT, EventTicketClass, EventTicketObject from walletobjects.comms import Comms from walletobjects.constants import ( - Barcode, ClassType, ConfirmationCode, DoorsOpen, - MultipleDevicesAndHoldersAllowedStatus, ObjectState, ObjectType, - ReviewStatus, Seat, + Barcode, + ClassType, + ConfirmationCode, + DoorsOpen, + MultipleDevicesAndHoldersAllowedStatus, + ObjectState, + ObjectType, + ReviewStatus, + Seat, ) from pretix.base.settings import GlobalSettingsObject import uuid from pretix.multidomain.urlreverse import eventreverse_absolute from django.utils import translation +from django.conf import settings + def _get_instance_uuid(): gs = GlobalSettingsObject() if not gs.settings.wallet_google_instance_uuid: gs.settings.wallet_google_instance_uuid = str(uuid.uuid4()) - return gs.settings.get("wallet_google_instance_uuid") + + return gs.settings.wallet_google_instance_uuid + def get_class_id(event: Event): + # TODO: add layout id somewhere instance_uuid = _get_instance_uuid() - issuer_id = event.settings.get('wallet_google_issuer_id') - return "%s.pretix-%s-%s-%s" % (issuer_id, instance_uuid, event.organizer.slug, event.slug) + issuer_id = event.settings.get("wallet_google_issuer_id") + return "%s.pretix-%s-%s-%s" % ( + issuer_id, + instance_uuid, + event.organizer.slug, + event.slug, + ) def get_object_id(op: OrderPosition): instance_uuid = _get_instance_uuid() - issuer_id = op.order.event.settings.get('wallet_google_issuer_id') + issuer_id = op.order.event.settings.get("wallet_google_issuer_id") + + return "%s.pretix-%s-%s-%s-%s-%s" % ( + issuer_id, + instance_uuid, + op.order.event.organizer.slug, + op.order.event.slug, + op.order.code, + op.positionid, + ) - return "%s.pretix-%s-%s-%s-%s-%s" % (issuer_id, instance_uuid, - op.order.event.organizer.slug, op.order.event.slug, - op.order.code, op.positionid) def get_translated_dict(string, locales): translated = {} @@ -54,35 +85,69 @@ def get_translated_string(string, locale): return translated + class GooglePlatform(WalletPlatform): identifier = "google" name = _("Google") @classmethod def generate(cls, layout: PassLayout, op: OrderPosition): - class_object = cls._generate_class(op.order.event) + from ..views import get_layout_variables - # ticket_object = cls._get_object(op) + order = op.order + event = order.event - # if not ticket_object: - # return False + context = { + "placeholders": get_layout_variables(event), # TODO: move to higher class + "evaluation_context": [op, order, event], + "credentials": event.settings.get("wallet_google_credentials").read(), + "locale": event.settings.locale, + "locales": event.settings.locales, + "issuerName": event.organizer.name, + "eventName": event.name, + "classId": get_class_id(event), + "objectId": get_object_id(op), + "homepageUrl": eventreverse_absolute(event, "presale:event.index"), + # TODO: add webhook view & register in pass + "webhookUrl": "", # eventreverse_absolute(event.organizer,"plugins:wallet:google_webhook",)) + } - # generated_jwt = self._comms().sign_jwt( - # ButtonJWT( - # origins=[django_settings.SITE_URL], - # issuer=self._comms().client_email, - # event_ticket_objects=[ticket_object], - # skinny=True - # ) - # ) + data = layout.generate(context) + return "pass", "text/plain", data - # if generated_jwt: - # return 'googlepaypass', 'text/uri-list', 'https://pay.google.com/gp/v/save/%s' % generated_jwt - # else: - # return False - data = {"class": class_object} - return 'pass.json', 'application/json', json.dumps(data) +class GoogleWalletStyle(PassStyle): + platform = GooglePlatform + + @classmethod + def _comms(cls, event: Event): + return Comms() + + def _generate_class(self, layout, context, fields): + raise NotImplementedError() + + def _generate_object(self, layout, context, fields): + raise NotImplementedError() + + def generate(self, layout, context): + comms = Comms(context["credentials"]) + fields = self.get_pass_fields(layout, context) + + class_object = self._generate_class(layout, context, fields) + ticket_object = self._generate_object(layout, context, fields) + + generated_jwt = comms.sign_jwt( + ButtonJWT( + origins=[settings.SITE_URL], + issuer=comms.client_email, + event_ticket_classes=[class_object], + event_ticket_objects=[ticket_object], + skinny=False, + ) + ) + + return "https://pay.google.com/gp/v/save/%s" % generated_jwt + # from ..views import get_layout_variables # order = op.order @@ -120,27 +185,84 @@ class GooglePlatform(WalletPlatform): # data = layout.generate(context) # return filename, "application/vnd.apple.pkpass", data - @classmethod - def _generate_class(cls, event: Event): - class_name = get_class_id(event) +class GoogleWalletEventTicket(GoogleWalletStyle): + identifier = "event" + name = "Event Ticket" + fieldgroups = [ + ImageFieldGroup( + identifier="logo", + name=_("Logo"), + min_entries=0, + max_entries=1, + default_entries=[ + PlaceholderFieldEntry( + content="poweredby", + ) + ], + ), + PredefinedFieldGroup(identifier="seating", name=_("Seating")), + TextFieldGroup( + identifier="code", + name=_("QR-Code"), + max_entries=1, + display=FieldGroupDisplay.CODE, + default_entries=[ + PlaceholderFieldEntry( + content="secret", + ) + ], + ), + ] + preview_layout = [ + [ + { + "children": [ + {"fieldgroup": "logo", "relSize": 1}, + { + "value": "issuerName", + "relSize": 3, + "display": ["large", "centered"], + }, + ] + }, + { + "children": [ + {"value": "venueName", "display": "small"}, + {"value": "eventName", "display": "large"}, + ], + "direction": "column", + }, + { + "children": [ + {"value": "01/01/1970", "label": "Date"}, + {"value": "12:34", "label": "Time"}, + ] + }, + {"fieldgroup": "seating"}, + {"fieldgroup": "code"}, + ] + ] + + def _generate_class(self, layout: PassLayout, context, fields): output_class = EventTicketClass( - event.organizer.name, - class_name, + context["issuerName"], + context["classId"], MultipleDevicesAndHoldersAllowedStatus.multipleHolders, # TODO: Make configurable - event.name, - ReviewStatus.draft, # TODO: ReviewStatus.underReview, - event.settings.locale + context["eventName"], + ReviewStatus.underReview, + context["locale"], ) output_class.homepage_uri( - eventreverse_absolute(event, 'presale:event.index'), - get_translated_string('Website', event.settings.locale), - get_translated_dict('Website', event.settings.locales) + context["homepageUrl"], + get_translated_string("Website", context["locale"]), + get_translated_dict("Website", context["locales"]), ) + # TODO: add webhook view & register in pass + # output_class.callback_url(context['webhookUrl']) - # output_class.callback_url(eventreverse_absolute(event.organizer, 'plugins:pretix_googlepaypasses:webhook')) - + # TODO: move to pass settings or set defaults # if (event.settings.get('ticketoutput_googlepaypasses_latitude') # and event.settings.get('ticketoutput_googlepaypasses_longitude')): # output_class.locations( @@ -153,9 +275,7 @@ class GooglePlatform(WalletPlatform): # event.geo_lon # ) - output_class.country_code(event.settings.locale) - - output_class.hide_barcode(False) + # output_class.country_code(event.settings.locale) # if event.settings.get('ticketoutput_googlepaypasses_hero'): # output_class.hero_image( @@ -205,17 +325,43 @@ class GooglePlatform(WalletPlatform): # return self._comms().put_item(ClassType.eventTicketClass, class_name, output_class) return output_class._eventTicketClass + def _generate_object(self, layout: PassLayout, context, fields): + class_id = context["classId"] + object_id = context["objectId"] + output_object = EventTicketObject( + object_id, class_id, ObjectState.active, context["locale"] + ) -class GoogleWalletStyle(PassStyle): - platform = GooglePlatform + # output_object.barcode(Barcode.qrCode, op.secret, op.secret) + # output_object.reservation_info("%s-%s" % (op.order.event.slug, op.order.code)) + # output_object.ticket_holder_name(op.attendee_name or (op.addon_to.attendee_name if op.addon_to else '')) + # output_object.ticket_number(op.secret) + # output_object.ticket_type( + # get_translated_dict( + # str(op.item) + (" – " + str(op.variation.value) if op.variation else ""), + # op.order.event.settings.get('locales') + # ) + # ) -class GoogleWalletEventTicket(PassStyle): - identifier = "event" - name = "Event Ticket" - platform = GooglePlatform - fieldgroups = [ - PredefinedFieldGroup(identifier="seating", name=_("Seating")), - TextFieldGroup(identifier="qrcode", name=_("QR-Code"), display=FieldGroupDisplay.PLAIN), - ] - preview_layout = None \ No newline at end of file + # places = django_settings.CURRENCY_PLACES.get(op.order.event.currency, 2) + # output_object.face_value(int(op.price * 1000 ** places), op.order.event.currency) + + # if op.order.event.seating_plan_id is not None: + # if op.seat: + # output_object.seat( + # get_translated_dict( + # _(str(op.seat)), + # op.order.event.settings.get('locales') + # ) + # ) + # else: + # output_object.seat( + # get_translated_dict( + # _('General admission'), + # op.order.event.settings.get('locales') + # ) + # ) + + # return self._comms().put_item(ObjectType.eventTicketObject, object_name, output_object) + return output_object._eventTicketObject diff --git a/src/pretix/plugins/wallet/ticketoutput.py b/src/pretix/plugins/wallet/ticketoutput.py index 5d413421d5..d82ac22cf0 100644 --- a/src/pretix/plugins/wallet/ticketoutput.py +++ b/src/pretix/plugins/wallet/ticketoutput.py @@ -91,6 +91,7 @@ class GoogleWalletTicketOutput(WalletOutput): forms.CharField( label=_("Google Wallet Issuer/Merchant ID"), help_text=_( + # TODO: update text "After getting accepted by Google into the Google Pay API for Passes program, " "your Issuer ID can be found in the Merchant center at " "https://wallet.google.com/merchant/walletobjects/" @@ -108,6 +109,7 @@ class GoogleWalletTicketOutput(WalletOutput): "for Passes Issuer ID" ), required=False, + # TODO: add validator # validators=[validate_json_credentials] ), ), @@ -190,17 +192,4 @@ class AppleWalletTicketOutput(WalletOutput): ] ) - -# settings_hierarkey.add_default("wallet_apple_certificate_file", None, File) -# settings_hierarkey.add_default("wallet_apple_wwdr_certificate_file", None, File) -# settings_hierarkey.add_default("ticketoutput_wallet_apple_background", None, File) -# settings_hierarkey.add_default("ticketoutput_wallet_apple_background2x", None, File) -# settings_hierarkey.add_default("ticketoutput_wallet_apple_background3x", None, File) -# settings_hierarkey.add_default("ticketoutput_wallet_apple_icon", None, File) -# settings_hierarkey.add_default("ticketoutput_wallet_apple_icon2x", None, File) -# settings_hierarkey.add_default("ticketoutput_wallet_apple_icon3x", None, File) -# settings_hierarkey.add_default("ticketoutput_wallet_apple_logo", None, File) -# settings_hierarkey.add_default("ticketoutput_wallet_apple_logo2x", None, File) -# settings_hierarkey.add_default("ticketoutput_wallet_apple_logo3x", None, File) - OUTPUTS = [WalletSettingsHolder, GoogleWalletTicketOutput, AppleWalletTicketOutput]