diff --git a/apps/passport-client/public/images/zuzalu/zuconnect-landscape.webp b/apps/passport-client/public/images/zuzalu/zuconnect-landscape.webp new file mode 100644 index 0000000000..ae5cbe0e50 Binary files /dev/null and b/apps/passport-client/public/images/zuzalu/zuconnect-landscape.webp differ diff --git a/apps/passport-client/public/images/zuzalu/zuzalu-landscape.webp b/apps/passport-client/public/images/zuzalu/zuzalu-landscape.webp new file mode 100644 index 0000000000..cfb938bd26 Binary files /dev/null and b/apps/passport-client/public/images/zuzalu/zuzalu-landscape.webp differ diff --git a/apps/passport-server/src/services/generic-issuance/pipelines/LemonadePipeline.ts b/apps/passport-server/src/services/generic-issuance/pipelines/LemonadePipeline.ts index 3d9452192e..13b54e2385 100644 --- a/apps/passport-server/src/services/generic-issuance/pipelines/LemonadePipeline.ts +++ b/apps/passport-server/src/services/generic-issuance/pipelines/LemonadePipeline.ts @@ -13,6 +13,7 @@ import { BadgeConfig, CONTACT_EVENT_NAME, GenericIssuanceSendPipelineEmailResponseValue, + ImageOptions, LemonadePipelineDefinition, LemonadePipelineEventConfig, LemonadePipelineTicketTypeConfig, @@ -586,6 +587,40 @@ export class LemonadePipeline implements BasePipeline { }); } + private imageOptionsToImageUrl( + imageOptions: ImageOptions | undefined, + isCheckedIn: boolean + ): string | undefined { + if (!imageOptions) return undefined; + if (imageOptions.requireCheckedIn && !isCheckedIn) return undefined; + return imageOptions.imageUrl; + } + + private atomToQrCodeOverrideImageUrl( + ticketAtom: LemonadeAtom + ): string | undefined { + const event = this.lemonadeAtomToEvent(ticketAtom); + return event.imageOptions?.qrCodeOverrideImageUrl; + } + + private atomToImageUrl(ticketAtom: LemonadeAtom): string | undefined { + const event = this.lemonadeAtomToEvent(ticketAtom); + return this.imageOptionsToImageUrl( + event.imageOptions, + ticketAtom.checkinDate !== undefined + ); + } + + private atomToEventLocation(atom: LemonadeAtom): string | undefined { + const event = this.lemonadeAtomToEvent(atom); + return event.imageOptions?.eventLocation; + } + + private atomToEventStartDate(atom: LemonadeAtom): string | undefined { + const event = this.lemonadeAtomToEvent(atom); + return event.imageOptions?.eventStartDate; + } + private async manualTicketToTicketData( client: PoolClient, manualTicket: ManualTicket, @@ -608,6 +643,10 @@ export class LemonadePipeline implements BasePipeline { attendeeName: manualTicket.attendeeName, attendeeSemaphoreId: sempahoreId, isConsumed: checkIn ? true : false, + imageUrl: this.imageOptionsToImageUrl(event.imageOptions, !!checkIn), + qrCodeOverrideImageUrl: event.imageOptions?.qrCodeOverrideImageUrl, + eventStartDate: event.imageOptions?.eventStartDate, + eventLocation: event.imageOptions?.eventLocation, isRevoked: false, timestampSigned: Date.now(), timestampConsumed: checkIn ? checkIn.timestamp.getTime() : 0, @@ -783,7 +822,6 @@ export class LemonadePipeline implements BasePipeline { ): Promise { return traced(LOG_NAME, "getTicketsForEmail", async () => { // Load atom-backed tickets - const relevantTickets = await this.db.loadByEmail(this.id, email); // Load check-in data @@ -832,7 +870,6 @@ export class LemonadePipeline implements BasePipeline { ): Promise { return traced(LOG_NAME, "issueLemonadeTicketPCDs", async (span) => { tracePipeline(this.definition); - if (!req.pcd) { throw new Error("missing credential pcd"); } @@ -1160,6 +1197,10 @@ export class LemonadePipeline implements BasePipeline { timestampConsumed: atom.checkinDate instanceof Date ? atom.checkinDate.getTime() : 0, timestampSigned: Date.now(), + imageUrl: this.atomToImageUrl(atom), + qrCodeOverrideImageUrl: this.atomToQrCodeOverrideImageUrl(atom), + eventStartDate: this.atomToEventStartDate(atom), + eventLocation: this.atomToEventLocation(atom), attendeeSemaphoreId: semaphoreId, isConsumed: atom.checkinDate instanceof Date, isRevoked: false, // Not clear what concept this maps to in Lemonade diff --git a/apps/passport-server/src/services/generic-issuance/pipelines/PretixPipeline.ts b/apps/passport-server/src/services/generic-issuance/pipelines/PretixPipeline.ts index e69b2ffb65..f8ac743bff 100644 --- a/apps/passport-server/src/services/generic-issuance/pipelines/PretixPipeline.ts +++ b/apps/passport-server/src/services/generic-issuance/pipelines/PretixPipeline.ts @@ -1299,7 +1299,7 @@ export class PretixPipeline implements BasePipeline { ticketId: atom.id, eventId: atom.eventId, productId: atom.productId, - timestampConsumed: atom.timestampConsumed?.getTime() ?? 0, + timestampConsumed: atom?.timestampConsumed?.getTime() ?? 0, timestampSigned: Date.now(), owner: semaphoreV4Id, imageUrl: this.atomToImageUrl(atom), diff --git a/apps/passport-server/src/services/issuanceService.ts b/apps/passport-server/src/services/issuanceService.ts index 7e6d7fbf80..a84eeb2f3f 100644 --- a/apps/passport-server/src/services/issuanceService.ts +++ b/apps/passport-server/src/services/issuanceService.ts @@ -684,8 +684,19 @@ export class IssuanceService { isConsumed: false, isRevoked: false, ticketCategory: TicketCategory.Zuzalu, - imageUrl: urljoin(imageServerUrl, "images/zuzalu", "zuzalu.png"), - imageAltText: "Zuzalu logo" + imageUrl: urljoin( + imageServerUrl, + "images/zuzalu", + "zuzalu-landscape.webp" + ), + qrCodeOverrideImageUrl: urljoin( + imageServerUrl, + "images/zuzalu", + "zuzalu.png" + ), + imageAltText: "Zuzalu logo", + eventLocation: "Lustica Bay, Montenegro", + eventStartDate: "2023-03-11T00:00:00.000Z" }) ); } @@ -759,11 +770,18 @@ export class IssuanceService { isRevoked: false, ticketCategory: TicketCategory.ZuConnect, imageUrl: urljoin( + imageServerUrl, + "images/zuzalu", + "zuconnect-landscape.webp" + ), + qrCodeOverrideImageUrl: urljoin( imageServerUrl, "images/zuzalu", "zuconnect.png" ), - imageAltText: "ZuConnect" + imageAltText: "ZuConnect", + eventLocation: "Istanbul, Turkey", + eventStartDate: "2023-10-29T00:00:00.000Z" }) ); } diff --git a/packages/lib/passport-interface/src/genericIssuanceTypes.ts b/packages/lib/passport-interface/src/genericIssuanceTypes.ts index b4fef2cb9f..f4247408e6 100644 --- a/packages/lib/passport-interface/src/genericIssuanceTypes.ts +++ b/packages/lib/passport-interface/src/genericIssuanceTypes.ts @@ -182,6 +182,14 @@ const SemaphoreGroupListSchema = z { message: "Semaphore group names must be unique" } ); +const ImageOptionsSchema = z.object({ + imageUrl: z.string(), + requireCheckedIn: z.boolean(), + qrCodeOverrideImageUrl: z.string().optional(), + eventStartDate: z.string().optional(), + eventLocation: z.string().optional() +}); + /** * Generic Issuance-specific ticket type configuration - roughly corresponds to a * 'Product' in Pretix-land. @@ -206,7 +214,11 @@ const LemonadePipelineEventConfigSchema = z.object({ /** * Roughly translates to Products in {@link EdDSATicketPCD}. */ - ticketTypes: z.array(LemonadePipelineTicketTypeConfigSchema) + ticketTypes: z.array(LemonadePipelineTicketTypeConfigSchema), + /** + * Options to configure displaying an image instead of the QR code + */ + imageOptions: ImageOptionsSchema.optional() }); /** @@ -275,14 +287,6 @@ const FeedIssuanceOptionsSchema = z.object({ export type FeedIssuanceOptions = z.infer; -const ImageOptionsSchema = z.object({ - imageUrl: z.string(), - requireCheckedIn: z.boolean(), - qrCodeOverrideImageUrl: z.string().optional(), - eventStartDate: z.string().optional(), - eventLocation: z.string().optional() -}); - export type ImageOptions = z.infer; const LemonadePipelineOptionsSchema = BasePipelineOptionsSchema.extend({ diff --git a/packages/ui/eddsa-ticket-pcd-ui/src/CardBody.tsx b/packages/ui/eddsa-ticket-pcd-ui/src/CardBody.tsx index 5ef831fcd0..3a2fd3c3ee 100644 --- a/packages/ui/eddsa-ticket-pcd-ui/src/CardBody.tsx +++ b/packages/ui/eddsa-ticket-pcd-ui/src/CardBody.tsx @@ -49,10 +49,9 @@ function EdDSATicketPCDCardBody({ // If ticket has an `eventStartDate` render the `qrCodeOverrideImageUrl`, if it exists // Else, render the `imageUrl`, if it existss - const imageToRender = - ticketData?.eventStartDate && idBasedVerifyURL !== undefined - ? ticketData.qrCodeOverrideImageUrl - : ticketData?.imageUrl; + const imageToRender = ticketData?.eventStartDate + ? ticketData.qrCodeOverrideImageUrl + : ticketData?.imageUrl; return (