diff --git a/src/common/apple/apple-wallet.service.ts b/src/common/apple/apple-wallet.service.ts index c2a329e7..2849cf97 100644 --- a/src/common/apple/apple-wallet.service.ts +++ b/src/common/apple/apple-wallet.service.ts @@ -30,19 +30,28 @@ export class AppleWalletService { const signerKeyPassphrase = this.configService.get( "APPLE_SIGNER_KEY_PASSPHRASE", ); + try { + // Validate that passphrase is provided if required + if (!signerKeyPassphrase) { + throw new Error( + "APPLE_SIGNER_KEY_PASSPHRASE environment variable is required for encrypted private keys", + ); + } + this.certificates = { wwdr: fs.readFileSync(wwdrPath), signerCert: fs.readFileSync(signerCertPath), signerKey: fs.readFileSync(signerKeyPath), signerKeyPassphrase, }; - this.logger.log("Apple Wallet certificates loaded"); + this.logger.log("Apple Wallet certificates loaded successfully"); } catch (error) { this.logger.error( - "Error loading Apple Wallet certificates. Please check the paths.", + "Error loading Apple Wallet certificates. Please check the paths and passphrase.", error, ); + throw error; // Re-throw to prevent service from starting with invalid config } } @@ -51,6 +60,21 @@ export class AppleWalletService { return Buffer.from(response.data, "binary"); } + private calculateEventDuration( + startDateTime: string, + endDateTime: string, + ): string { + const start = DateTime.fromISO(startDateTime); + const end = DateTime.fromISO(endDateTime); + const duration = end.diff(start, ["days", "hours"]); + + if (duration.days >= 1) { + return `${Math.floor(duration.days)} day${duration.days > 1 ? "s" : ""}`; + } else { + return `${Math.floor(duration.hours)} hour${duration.hours > 1 ? "s" : ""}`; + } + } + async generatePass( passData: HackathonPassData, userId: string, @@ -62,23 +86,34 @@ export class AppleWalletService { passTypeIdentifier: "pass.hackpsu.wallet", serialNumber: `${Date.now()}-${userId}`, teamIdentifier: "HN6JG96A2Y", - foregroundColor: "rgb(255,255,255)", - backgroundColor: "rgb(134,157,203)", + // HackPSU brand colors - clean white background with grey text and coral accents + foregroundColor: "rgb(60,60,60)", + backgroundColor: "rgb(255,255,255)", + labelColor: "rgb(232,90,90)", + logoText: "HackPSU", + // Add visual enhancements + sharingProhibited: false, + maxDistance: 100, + relevantDate: DateTime.fromJSDate(new Date(passData.startDateTime), { + zone: "America/New_York", + }).toISO(), eventTicket: { primaryFields: [ { key: "event", - label: "Event", + label: "HACKATHON EVENT", value: passData.eventName, + textAlignment: "PKTextAlignmentCenter", }, ], secondaryFields: [ { key: "startTime", - label: "Start Time", + label: "EVENT STARTS", value: DateTime.fromJSDate(new Date(passData.startDateTime), { zone: "America/New_York", - }).toLocaleString(DateTime.DATETIME_MED), + }).toFormat("EEE, MMM d 'at' h:mm a"), + textAlignment: "PKTextAlignmentLeft", semantics: { eventStartDate: DateTime.fromJSDate( new Date(passData.startDateTime), @@ -87,24 +122,21 @@ export class AppleWalletService { }, }, { - key: "endTime", - label: "End Time", - value: DateTime.fromJSDate(new Date(passData.endDateTime), { - zone: "America/New_York", - }).toLocaleString(DateTime.DATETIME_MED), - semantics: { - eventEndDate: DateTime.fromJSDate( - new Date(passData.endDateTime), - { zone: "America/New_York" }, - ).toISO(), - }, + key: "duration", + label: "DURATION", + value: this.calculateEventDuration( + passData.startDateTime, + passData.endDateTime, + ), + textAlignment: "PKTextAlignmentRight", }, ], auxiliaryFields: [ { key: "location", - label: "Location", - value: "ECore Building, University Park, PA", + label: "VENUE", + value: "ECore Building\nUniversity Park, PA", + textAlignment: "PKTextAlignmentLeft", semantics: { location: { latitude: passData.location.latitude, @@ -112,6 +144,47 @@ export class AppleWalletService { }, }, }, + { + key: "attendee", + label: "ATTENDEE", + value: passData.ticketHolderName || `User ${userId}`, + textAlignment: "PKTextAlignmentRight", + }, + ], + backFields: [ + { + key: "website", + label: "Official Website", + value: passData.homepageUri || "https://hackpsu.org", + attributedValue: `${passData.homepageUri || "https://hackpsu.org"}`, + }, + { + key: "eventDetails", + label: "Event Information", + value: `Join us for ${passData.eventName}! This pass serves as your admission ticket. Please have it ready when you arrive.\n\nFor questions or support, visit hackpsu.org or contact our team.`, + }, + { + key: "schedule", + label: "Event Schedule", + value: `Check-in Opens: ${DateTime.fromJSDate( + new Date(passData.startDateTime), + { + zone: "America/New_York", + }, + ).toFormat("h:mm a")}\n\nHackathon Begins: ${DateTime.fromJSDate( + new Date(passData.startDateTime), + { + zone: "America/New_York", + }, + ) + .plus({ hours: 2 }) + .toFormat("h:mm a")}\n\nEvent Ends: ${DateTime.fromJSDate( + new Date(passData.endDateTime), + { + zone: "America/New_York", + }, + ).toFormat("h:mm a")}`, + }, ], }, }; @@ -142,10 +215,8 @@ export class AppleWalletService { iconBuffer = await this.downloadImage(passData.logoUrl); this.logger.log("Downloaded icon from URL"); } catch (error) { - this.logger.error( - "Error downloading icon, using fallback local icon", - error, - ); + this.logger.error("Failed to download icon", error); + throw new Error("Failed to download required icon for pass generation"); } pass.addBuffer("icon.png", iconBuffer); @@ -154,10 +225,8 @@ export class AppleWalletService { logoBuffer = await this.downloadImage(passData.logoUrl); this.logger.log("Downloaded logo from URL"); } catch (error) { - this.logger.error( - "Error downloading logo, using fallback local logo", - error, - ); + this.logger.error("Failed to download logo", error); + throw new Error("Failed to download required logo for pass generation"); } pass.addBuffer("logo.png", logoBuffer); diff --git a/src/common/gcp/wallet/google-wallet.service.ts b/src/common/gcp/wallet/google-wallet.service.ts index 0b5543a2..6a9fc762 100644 --- a/src/common/gcp/wallet/google-wallet.service.ts +++ b/src/common/gcp/wallet/google-wallet.service.ts @@ -110,21 +110,106 @@ export class GoogleWalletService { contentDescription: { defaultValue: { language: "en-US", - value: "Logo image description", + value: "HackPSU Logo", }, }, }, + hexBackgroundColor: "#FFFFFF", dateTime: { start: passData.startDateTime, end: passData.endDateTime, }, + venue: { + name: { + defaultValue: { + language: "en-US", + value: "ECore Building, Penn State University", + }, + }, + address: { + defaultValue: { + language: "en-US", + value: "University Park, PA 16802", + }, + }, + }, + textModulesData: [ + { + id: "attendee", + header: "ATTENDEE", + body: passData.ticketHolderName || "Hackathon Participant", + localizedHeader: { + defaultValue: { + language: "en-US", + value: "ATTENDEE", + }, + }, + localizedBody: { + defaultValue: { + language: "en-US", + value: passData.ticketHolderName || "Hackathon Participant", + }, + }, + }, + { + id: "event_info", + header: "EVENT DETAILS", + body: `Join us for ${passData.eventName}! Check-in opens 2 hours before the hackathon begins. Please have your pass ready when you arrive.`, + localizedHeader: { + defaultValue: { + language: "en-US", + value: "EVENT DETAILS", + }, + }, + localizedBody: { + defaultValue: { + language: "en-US", + value: `Join us for ${passData.eventName}! Check-in opens 2 hours before the hackathon begins. Please have your pass ready when you arrive.`, + }, + }, + }, + { + id: "website", + header: "OFFICIAL WEBSITE", + body: passData.homepageUri || "https://hackpsu.org", + localizedHeader: { + defaultValue: { + language: "en-US", + value: "OFFICIAL WEBSITE", + }, + }, + localizedBody: { + defaultValue: { + language: "en-US", + value: passData.homepageUri || "https://hackpsu.org", + }, + }, + }, + ], ...(passData.location && { linksModuleData: { uris: [ { uri: `https://www.google.com/maps/search/?api=1&query=${passData.location.latitude},${passData.location.longitude}`, - description: "Venue Location", + description: "View Venue Location", id: "VENUE_MAP", + localizedDescription: { + defaultValue: { + language: "en-US", + value: "View Venue Location", + }, + }, + }, + { + uri: passData.homepageUri || "https://hackpsu.org", + description: "Visit HackPSU Website", + id: "WEBSITE", + localizedDescription: { + defaultValue: { + language: "en-US", + value: "Visit HackPSU Website", + }, + }, }, ], }, @@ -188,13 +273,54 @@ export class GoogleWalletService { value: `HACKPSU_${userId}`, alternateText: "", }, + ticketHolderName: passData.ticketHolderName || "Hackathon Participant", + seatInfo: { + seat: { + defaultValue: { + language: "en-US", + value: "General Admission", + }, + }, + row: { + defaultValue: { + language: "en-US", + value: "Hacker", + }, + }, + }, + textModulesData: [ + { + id: "check_in_info", + header: "CHECK-IN", + body: "Check-in opens 2 hours before hackathon start time. Please have this pass ready.", + localizedHeader: { + defaultValue: { + language: "en-US", + value: "CHECK-IN", + }, + }, + localizedBody: { + defaultValue: { + language: "en-US", + value: + "Check-in opens 2 hours before hackathon start time. Please have this pass ready.", + }, + }, + }, + ], ...(passData.location && { linksModuleData: { uris: [ { uri: `https://www.google.com/maps/search/?api=1&query=${passData.location.latitude},${passData.location.longitude}`, - description: "Venue Location", + description: "View Venue Location", id: "OBJECT_VENUE_MAP", + localizedDescription: { + defaultValue: { + language: "en-US", + value: "View Venue Location", + }, + }, }, ], }, @@ -260,21 +386,89 @@ export class GoogleWalletService { contentDescription: { defaultValue: { language: "en-US", - value: "Logo image description", + value: "HackPSU Logo", }, }, }, + hexBackgroundColor: "#FFFFFF", dateTime: { start: passData.startDateTime, end: passData.endDateTime, }, + venue: { + name: { + defaultValue: { + language: "en-US", + value: "ECore Building, Penn State University", + }, + }, + address: { + defaultValue: { + language: "en-US", + value: "University Park, PA 16802", + }, + }, + }, + textModulesData: [ + { + id: "attendee", + header: "ATTENDEE", + body: passData.ticketHolderName || "Hackathon Participant", + localizedHeader: { + defaultValue: { + language: "en-US", + value: "ATTENDEE", + }, + }, + localizedBody: { + defaultValue: { + language: "en-US", + value: passData.ticketHolderName || "Hackathon Participant", + }, + }, + }, + { + id: "event_info", + header: "EVENT DETAILS", + body: `Join us for ${passData.eventName}! Check-in opens 2 hours before the hackathon begins. Please have your pass ready when you arrive.`, + localizedHeader: { + defaultValue: { + language: "en-US", + value: "EVENT DETAILS", + }, + }, + localizedBody: { + defaultValue: { + language: "en-US", + value: `Join us for ${passData.eventName}! Check-in opens 2 hours before the hackathon begins. Please have your pass ready when you arrive.`, + }, + }, + }, + ], ...(passData.location && { linksModuleData: { uris: [ { uri: `https://www.google.com/maps/search/?api=1&query=${passData.location.latitude},${passData.location.longitude}`, - description: "Venue Location", + description: "View Venue Location", id: "VENUE_MAP", + localizedDescription: { + defaultValue: { + language: "en-US", + value: "View Venue Location", + }, + }, + }, + { + uri: passData.homepageUri || "https://hackpsu.org", + description: "Visit HackPSU Website", + id: "WEBSITE", + localizedDescription: { + defaultValue: { + language: "en-US", + value: "Visit HackPSU Website", + }, + }, }, ], }, @@ -290,13 +484,54 @@ export class GoogleWalletService { value: `HACKPSU_${userId}`, alternateText: "", }, + ticketHolderName: passData.ticketHolderName || "Hackathon Participant", + seatInfo: { + seat: { + defaultValue: { + language: "en-US", + value: "General Admission", + }, + }, + row: { + defaultValue: { + language: "en-US", + value: "Hacker", + }, + }, + }, + textModulesData: [ + { + id: "check_in_info", + header: "CHECK-IN", + body: "Check-in opens 2 hours before hackathon start time. Please have this pass ready.", + localizedHeader: { + defaultValue: { + language: "en-US", + value: "CHECK-IN", + }, + }, + localizedBody: { + defaultValue: { + language: "en-US", + value: + "Check-in opens 2 hours before hackathon start time. Please have this pass ready.", + }, + }, + }, + ], ...(passData.location && { linksModuleData: { uris: [ { uri: `https://www.google.com/maps/search/?api=1&query=${passData.location.latitude},${passData.location.longitude}`, - description: "Venue Location", + description: "View Venue Location", id: "OBJECT_VENUE_MAP", + localizedDescription: { + defaultValue: { + language: "en-US", + value: "View Venue Location", + }, + }, }, ], }, diff --git a/src/modules/wallet/apple-wallet.controller.ts b/src/modules/wallet/apple-wallet.controller.ts index 54153d9e..5d73500f 100644 --- a/src/modules/wallet/apple-wallet.controller.ts +++ b/src/modules/wallet/apple-wallet.controller.ts @@ -10,6 +10,7 @@ import { AppleWalletService } from "../../common/apple/apple-wallet.service"; import { HackathonPassData } from "../../common/gcp/wallet/google-wallet.types"; import { InjectRepository, Repository } from "common/objection"; import { Hackathon } from "entities/hackathon.entity"; +import { User } from "entities/user.entity"; import { RestrictedRoles, Role } from "common/gcp"; import { DateTime } from "luxon"; @@ -19,6 +20,8 @@ export class AppleWalletController { private readonly appleWalletService: AppleWalletService, @InjectRepository(Hackathon) private readonly hackathonRepo: Repository, + @InjectRepository(User) + private readonly userRepo: Repository, ) {} @Post(":id/pass") @@ -35,17 +38,25 @@ export class AppleWalletController { throw new NotFoundException("No active hackathon found"); } + const user = await User.query().findById(userId); + if (!user) { + throw new NotFoundException("User not found"); + } + + // Set event time to 2 hours before hackathon start time + const eventStartTime = DateTime.fromJSDate(new Date(hackathon.startTime), { + zone: "America/New_York", + }).minus({ hours: 2 }); + const passData: HackathonPassData = { eventName: `HackPSU ${hackathon.name}`, issuerName: "HackPSU", homepageUri: "https://hackpsu.org", logoUrl: "https://storage.googleapis.com/hackpsu-408118.appspot.com/sponsor-logos/6-Test%20Sponsor-light.png", - ticketHolderName: `User ${userId}`, + ticketHolderName: `${user.firstName} ${user.lastName}`, ticketNumber: userId, - startDateTime: DateTime.fromJSDate(new Date(hackathon.startTime), { - zone: "America/New_York", - }).toISO(), + startDateTime: eventStartTime.toISO(), endDateTime: DateTime.fromJSDate(new Date(hackathon.endTime), { zone: "America/New_York", }).toISO(), diff --git a/src/modules/wallet/wallet.module.ts b/src/modules/wallet/wallet.module.ts index 2b85e558..47feb373 100644 --- a/src/modules/wallet/wallet.module.ts +++ b/src/modules/wallet/wallet.module.ts @@ -3,13 +3,14 @@ import { WalletController } from "./wallet.controller"; import { GoogleWalletModule } from "common/gcp/wallet/google-wallet.module"; import { ObjectionModule } from "common/objection"; import { Hackathon } from "entities/hackathon.entity"; +import { User } from "entities/user.entity"; import { AppleWalletController } from "./apple-wallet.controller"; import { AppleWalletService } from "common/apple/apple-wallet.service"; import { ConfigModule } from "@nestjs/config"; @Module({ imports: [ - ObjectionModule.forFeature([Hackathon]), + ObjectionModule.forFeature([Hackathon, User]), GoogleWalletModule, ConfigModule, ],