diff --git a/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/preview/fieldgroup-preview.vue b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/preview/fieldgroup-preview.vue
new file mode 100644
index 0000000000..effcfb96c1
--- /dev/null
+++ b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/preview/fieldgroup-preview.vue
@@ -0,0 +1,36 @@
+
+
+
+ PlaceholderFieldgroupPreview(v-if="style_def.type == 'placeholder'" :config="config" :style_def="style_def")
+ //- // TODO: support predefined group
+ //- div(:style="{'flex-grow': config.relSize}")
+ //- div(style="background-color: gray; display: flex; flex-direction: row;")
+ //- div(v-for="entry in layout.entries.slice(0,style_def.max_entries)")
+ //- div(v-if="style_def.labels") {{ i18nstringLocalize(entry.label) }}
+ //- div(v-if="entry.type == 'custom'") {{ i18nstringLocalize(entry.content) }}
+ //- div(v-if="entry.type == 'placeholder'") {{ store.variables[style_def.content_type][entry.content] }}
+ //- pre
+ //- //- code {{ config }}
+ //- code {{ style_def }}
+ //- code {{ layout }}
+ //- div(style="position: absolute; background-color: red; top: 0.5cm; left: 0cm; height: 1cm; width: 2cm;") Logo
+ //- div(style="position: absolute; background-color: red; top: 0.5cm; left: 2.25cm; height: 1cm; width: 5.5cm;") Logo Text
+ //- div(style="position: absolute; background-color: red; top: 0.5cm; left: 8cm; height: 1cm; width: 2cm;") Header
+ //- div(style="position: absolute; background-color: red; top: 2.5cm; left: 0cm; height: 2cm; width: 10cm;") Primary
+ //- div(style="position: absolute; background-color: red; top: 4.75cm; left: 0cm; height: 1cm; width: 10cm;") Secondary
+ //- div(style="position: absolute; background-color: red; top: 7cm; left: 0cm; height: 1cm; width: 10cm;") Auxiliary
+ //- div(style="position: absolute; background-color: red; top: 10cm; left: 3cm; height: 4cm; width: 4cm;")
+
\ 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
new file mode 100644
index 0000000000..dfdf7258ea
--- /dev/null
+++ b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/preview/pass-preview.vue
@@ -0,0 +1,38 @@
+
+
+
+ div.pass-container
+ div.pass-content
+ RowPreview(v-for="row in layout" :config="row")
+
+
diff --git a/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/preview/placeholder-fieldgroup-preview.vue b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/preview/placeholder-fieldgroup-preview.vue
new file mode 100644
index 0000000000..ef020737f2
--- /dev/null
+++ b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/preview/placeholder-fieldgroup-preview.vue
@@ -0,0 +1,78 @@
+
+
+
+ // TODO: support predefined group
+ div.fieldgroup-container(:style="{'flex-grow': config.relSize, 'flex-direction': config.direction || 'row'}")
+ div.fieldgroup-item(v-for="{ entry, label, content } in store.currentLayoutFieldContent[config.fieldgroup]")
+ div.fieldgroup-item-qrcode(v-if="style_def.display == 'code'") {{ label }}
+
+ template(v-else)
+ div.fieldgroup-label.nowrap(v-if="style_def.display == 'with_label'") {{ label }}
+ div.nowrap.content(v-if="style_def.content_type == 'text'" :class="config.display") {{ content }}
+ img.fieldgroup-item-image(v-else-if="style_def.content_type == 'image' && content" :src="i18nstringLocalize(content)")
+
+
+
+
+
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
new file mode 100644
index 0000000000..ccb1d3a1f1
--- /dev/null
+++ b/src/pretix/plugins/wallet/static/pretixplugins/wallet/components/preview/row-preview.vue
@@ -0,0 +1,14 @@
+
+
+
+ div(v-if="'children' in config" style="width: 100%; display: flex; gap: 1em")
+ RowPreview(v-for="child in config.children" :config="child")
+ FieldgroupPreview(v-else :config="config")
+
\ No newline at end of file
diff --git a/src/pretix/plugins/wallet/static/pretixplugins/wallet/helpers.ts b/src/pretix/plugins/wallet/static/pretixplugins/wallet/helpers.ts
new file mode 100644
index 0000000000..dbd7f66691
--- /dev/null
+++ b/src/pretix/plugins/wallet/static/pretixplugins/wallet/helpers.ts
@@ -0,0 +1,32 @@
+export function i18nstringLocalize(s: I18nString): string {
+ if (typeof s === 'string') {
+ return s
+ }
+ if (s === null) {
+ return null
+ }
+
+ var locale = document.body.attributes['data-pretixlocale'].value
+ var short_locale = locale.split('-')[0]
+ if (locale in s)
+ return s[locale]
+
+ if (short_locale in s)
+ return s[short_locale]
+
+ for (const k of Object.keys(s)) {
+ if (k.split('-')[0] === short_locale && s[k]) {
+ return s[k]
+ }
+ }
+
+ if (s['en'])
+ return s['en']
+
+ for (const k of Object.keys(s)) {
+ if (s[k]) {
+ return s[k]
+ }
+ }
+}
+
diff --git a/src/pretix/plugins/wallet/styles/google.py b/src/pretix/plugins/wallet/styles/google.py
index 76d7e076f9..8d31226df5 100644
--- a/src/pretix/plugins/wallet/styles/google.py
+++ b/src/pretix/plugins/wallet/styles/google.py
@@ -1,10 +1,210 @@
-from .base import FieldGroupDisplay, PassStyle, PredefinedFieldGroup, TextFieldGroup, WalletPlatform
-from django.utils.translation import gettext_lazy as _
+from pretix.base.models import Event, OrderPosition
+
+from .base import FieldGroupDisplay, PassLayout, PassStyle, 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,
+)
+from pretix.base.settings import GlobalSettingsObject
+import uuid
+from pretix.multidomain.urlreverse import eventreverse_absolute
+from django.utils import translation
+
+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")
+
+def get_class_id(event: Event):
+ 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)
+
+
+def get_object_id(op: OrderPosition):
+ instance_uuid = _get_instance_uuid()
+ 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)
+
+def get_translated_dict(string, locales):
+ translated = {}
+
+ for locale in locales:
+ translation.activate(locale)
+ translated[locale] = gettext(string)
+ translation.deactivate()
+
+ return translated
+
+
+def get_translated_string(string, locale):
+ translation.activate(locale)
+ translated = gettext(string)
+ translation.deactivate()
+
+ 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)
+
+ # ticket_object = cls._get_object(op)
+
+ # if not ticket_object:
+ # return False
+
+ # generated_jwt = self._comms().sign_jwt(
+ # ButtonJWT(
+ # origins=[django_settings.SITE_URL],
+ # issuer=self._comms().client_email,
+ # event_ticket_objects=[ticket_object],
+ # skinny=True
+ # )
+ # )
+
+ # 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)
+ # from ..views import get_layout_variables
+
+ # order = op.order
+ # event = order.event
+ # filename = "{}-{}.pkpass".format(order.event.slug, order.code)
+
+ # ticket = str(op.item.name)
+ # if op.variation:
+ # ticket += " - " + str(op.variation)
+
+ # serialNumber = "%s-%s-%s-%d" % (
+ # order.event.organizer.slug,
+ # order.event.slug,
+ # order.code,
+ # op.pk,
+ # )
+
+ # context = {
+ # "placeholders": get_layout_variables(op.order.event),
+ # "evaluation_context": [op, order, order.event],
+ # "ca_certificate": order.event.settings.wallet_apple_ca_certificate.read(),
+ # "certificate": order.event.settings.wallet_apple_certificate.read(),
+ # "key": order.event.settings.wallet_apple_key.read(),
+ # "password": order.event.settings.wallet_apple_key_password,
+ # "description": _("Ticket for {event} ({product})").format( # TODO: i18n
+ # event=event.name, product=ticket
+ # ),
+ # "organizationName": event.organizer.name,
+ # "passTypeIdentifier": order.event.settings.wallet_apple_pass_type_id,
+ # "teamIdentifier": order.event.settings.wallet_apple_team_id,
+ # "serialNumber": serialNumber,
+ # "locales": event.settings.locales,
+ # }
+
+ # data = layout.generate(context)
+ # return filename, "application/vnd.apple.pkpass", data
+
+ @classmethod
+ def _generate_class(cls, event: Event):
+ class_name = get_class_id(event)
+
+ output_class = EventTicketClass(
+ event.organizer.name,
+ class_name,
+ MultipleDevicesAndHoldersAllowedStatus.multipleHolders, # TODO: Make configurable
+ event.name,
+ ReviewStatus.draft, # TODO: ReviewStatus.underReview,
+ event.settings.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)
+ )
+
+ # output_class.callback_url(eventreverse_absolute(event.organizer, 'plugins:pretix_googlepaypasses:webhook'))
+
+ # if (event.settings.get('ticketoutput_googlepaypasses_latitude')
+ # and event.settings.get('ticketoutput_googlepaypasses_longitude')):
+ # output_class.locations(
+ # event.settings.get('ticketoutput_googlepaypasses_latitude'),
+ # event.settings.get('ticketoutput_googlepaypasses_longitude')
+ # )
+ # elif event.geo_lat and event.geo_lon:
+ # output_class.locations(
+ # event.geo_lat,
+ # event.geo_lon
+ # )
+
+ output_class.country_code(event.settings.locale)
+
+ output_class.hide_barcode(False)
+
+ # if event.settings.get('ticketoutput_googlepaypasses_hero'):
+ # output_class.hero_image(
+ # urljoin(django_settings.SITE_URL, event.settings.get('ticketoutput_googlepaypasses_hero').url),
+ # str(event.name),
+ # event.name,
+ # )
+
+ # output_class.hex_background_color(event.settings.get('primary_color'))
+ # output_class.event_id('pretix-%s-%s-%s' % (gs.settings.get('update_check_id'), event.organizer.id, event.id))
+
+ # if event.settings.get('ticketoutput_googlepaypasses_logo'):
+ # output_class.logo(
+ # urljoin(django_settings.SITE_URL, event.settings.get('ticketoutput_googlepaypasses_logo').url),
+ # str(event.name),
+ # event.name,
+ # )
+
+ # if event.location:
+ # name = {}
+ # address = {}
+
+ # for key, value in event.location.data.items():
+ # lines = value.splitlines()
+ # name[key] = lines[0]
+ # # We must provide at least one address line each for the name and address - no way around it.
+ # if len(lines) > 1:
+ # address[key] = '\n'.join(value.splitlines()[1:])
+ # else:
+ # address[key] = lines[0]
+
+ # output_class.venue(name, address)
+
+ # if event.date_from and event.date_to and event.date_admission:
+ # output_class.date_time(
+ # DoorsOpen.doorsOpen,
+ # event.date_admission.isoformat(),
+ # event.date_from.isoformat(),
+ # event.date_to.isoformat(),
+ # )
+
+ # output_class.confirmation_code_label(ConfirmationCode.orderNumber)
+
+ # if event.seating_plan_id is not None:
+ # output_class.seat_label(Seat.seat)
+
+ # return self._comms().put_item(ClassType.eventTicketClass, class_name, output_class)
+ return output_class._eventTicketClass
+
class GoogleWalletStyle(PassStyle):
platform = GooglePlatform
diff --git a/src/pretix/plugins/wallet/ticketoutput.py b/src/pretix/plugins/wallet/ticketoutput.py
index 4bc3937b0b..5d413421d5 100644
--- a/src/pretix/plugins/wallet/ticketoutput.py
+++ b/src/pretix/plugins/wallet/ticketoutput.py
@@ -71,7 +71,9 @@ class WalletOutput(BaseTicketOutput):
wallet_layout = op.item.walletlayout
else:
wallet_layout = op.event.wallet_layouts.get(default=True)
- platform_layout = get_object_or_404(wallet_layout.platform_layouts, platform=self.platform.identifier)
+ platform_layout = get_object_or_404(
+ wallet_layout.platform_layouts, platform=self.platform.identifier
+ )
return self.platform.generate(platform_layout.pass_layout, op)
@@ -81,6 +83,48 @@ class GoogleWalletTicketOutput(WalletOutput):
download_button_text = "Add to Google Wallet"
platform = GooglePlatform
+ def get_global_settings(sender, **kwargs):
+ return OrderedDict(
+ [
+ (
+ "wallet_google_issuer_id",
+ forms.CharField(
+ label=_("Google Wallet Issuer/Merchant ID"),
+ help_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/"
+ ),
+ required=False,
+ ),
+ ),
+ (
+ "wallet_google_credentials",
+ forms.FileField(
+ label=_("Google Wallet Service Account Credentials"),
+ help_text=_(
+ "Please paste the contents of the JSON credentials file "
+ "of the service account you tied to your Google Pay API "
+ "for Passes Issuer ID"
+ ),
+ required=False,
+ # validators=[validate_json_credentials]
+ ),
+ ),
+ (
+ "wallet_google_instance_uuid",
+ forms.CharField(
+ label=_("Google Wallet Pass Instance UUID"),
+ help_text=_(
+ "Instance-specific part to be added to the wallet ids"
+ ),
+ required=False,
+ ),
+ ),
+
+ ]
+ )
+
class AppleWalletTicketOutput(WalletOutput):
identifier = "wallet_apple"
@@ -129,7 +173,7 @@ class AppleWalletTicketOutput(WalletOutput):
label=_("Apple Wallet Pass secret key"),
required=False,
validators=[validate_rsa_privkey],
- widget=ClearableBasenameFileInput
+ widget=ClearableBasenameFileInput,
),
),
(