Closed alexandercerutti closed 1 year ago
Originally posted by @johannesmutter in https://github.com/alexandercerutti/passkit-generator/issues/119#issuecomment-1756288658
Yes, works like a charm. Plug & Play.
For others who want to implement passkit-generator in Svelte/ SvelteKit, here's a comprehensive example:
From a server-side route e.g. /api/tickets/[event_id]/+server.js the ticket generation function is called (PDF, Apple Wallet, Google Wallet):
/api/tickets/[event_id]/+server.js
import { generateAppleWallet } from '$lib/generateAppleWallet'; // ... other imports export async function GET({ fetch, params, setHeaders }) { // ... read params const apple_wallet_pass = await generateAppleWallet(fetch, ticketData, visitor_data, designOptions, apple_wallet_images, isPrimaryTicket); // ... error logic setHeaders({ 'Content-Type': 'application/vnd.apple.pkpass', // MIME type for Apple Wallet pass files 'Last-Modified': new Date().toUTCString(), 'Cache-Control': 'public, max-age=600', 'Content-Disposition': `attachment; filename=${event_id}-ticket.pkpass` // File extension for Apple Wallet pass files }); return new Response(apple_wallet_pass); }
(it's a bit messy. also the part for generating the .pass model folder on the fly is missing)
import passkit from "passkit-generator"; const PKPass = passkit.PKPass; import { fileURLToPath } from 'url'; import { dirname, join } from 'path'; import { promises as fs } from 'fs'; const signerKeyPassphrase = import.meta.env.VITE_APPLE_WALLET_SIGNER_KEY_PASSPHRASE; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); /** @param {string} file */ function getFilePathForStatic(file) { if (file.includes('..') || file.startsWith('/')) { throw new Error('Invalid file path provided'); } const allowedExtensions = ['.jpg', '.png', '.ttf', '.otf', '.pass']; // Check if the file has an allowed extension const hasValidExtension = allowedExtensions.some(extension => file.endsWith(extension)); if (!hasValidExtension) { throw new Error('File request not allowed'); } // Split the file path into components to construct the full path safely const components = file.split('/'); const path_to_file = join(__dirname, '..', '..', 'static', ...components); return path_to_file; } /** * @param {string} path - Path to the file * @returns {Promise<Buffer>} The content of the file */ async function readFileAsBuffer(path) { return fs.readFile(path); } /** * Formats an array of event dates into a readable string. * * @param {Array<{ Date: string, Start?: string, End?: string, Description?: string }>} datesArr - Array of event date objects. * @returns {string} Formatted string representation of event dates. */ const formateEventDates = (datesArr) => { return datesArr.reduce((acc, val) => { let dateString = `${new Date(val.Date).toLocaleDateString('en-GB', { weekday: 'long', day: '2-digit', month: 'long' })}`; // Check if Start and End values exist before appending if (val.Start && val.End) { dateString += `: ${val.Start} to ${val.End} `; } if (val.Description) { dateString += `(${val.Description})`; } dateString += '\n\n'; acc.push(dateString); return acc; },[]).join(''); } /** * @typedef {Object} Ticket * @property {string} event_id * @property {string} name * @property {string} title * @property {string} primary_ticket_title * @property {string} secondary_ticket_title * @property {Array<{Label: string, Text: string}>} back_fields * @property {string} date_range * @property {Array<{Date: string, Start: string, End: string, Description: string}>} dates * @property {string} location * @property {string} organiser_name * @property {string} organiser_website * @property {string} organiser_email * @property {string} organiser_phone * @property {boolean} allow_sharing */ /** * @typedef {Object} VisitorData * @property {string} visitor_id * @property {string} visitor_name * @property {string[]} codes */ /** * @typedef {Object} DesignOptions * @property {string} background_color * @property {string} body_color * @property {string} title_color * @property {("qr"|"barcode")} type_of_code */ /** * @typedef {Object} WalletImages * @property {string} icon * @property {string} logo * @property {string} thumbnail * @property {string} strip_image * @property {string} background_image */ /** * Generates an Apple Wallet based on the given Ticket and design options. * @param {any} fetch * @param {Ticket} ticket - The Ticket details. * @param {VisitorData} visitor_data * @param {DesignOptions} designOptions - The design options for the Ticket * @param {WalletImages} images - The design options for the Ticket * @param {boolean} isPrimaryTicket */ export async function generateAppleWallet(fetch, ticket, visitor_data, designOptions,images,isPrimaryTicket) { // data const { event_id, name, title, primary_ticket_title, secondary_ticket_title, back_fields, date_range, dates, location, organiser_name, organiser_website, organiser_email, organiser_phone, allow_sharing } = ticket; const { visitor_id, visitor_name, codes } = visitor_data; const { background_color, body_color, title_color, type_of_code } = designOptions; // TODO: Generate model with image files on the fly // const { icon, logo, thumbnail, strip_image, background_image } = images; // const icon_file = icon && await fetch( icon ).then((res) => res.arrayBuffer()); // const logo_file = logo && await fetch( logo ).then((res) => res.arrayBuffer()); // const thumbnail_file = thumbnail && await fetch( thumbnail ).then((res) => res.arrayBuffer()); // const strip_image_file = strip_image && await fetch( strip_image ).then((res) => res.arrayBuffer()); // const background_image_file = background_image && await fetch( background_image ).then((res) => res.arrayBuffer()); const website_short = organiser_website && organiser_website.match(/^(?:https?:\/\/)?(?:[^@\n]+@)?(?:www\.)?([^:\/\n\?\=]+)/im)[1]; const serialNumber = isPrimaryTicket ? `${event_id}-${codes[0]}` : `${event_id}-${codes[1]}-guest`; /** * Each event requires a separate "Wallet model" (stored in folder static/[event_id]/[event_id].pass) * To create a new event, clone an existing model and modify accordingly */ const model = getFilePathForStatic(`${event_id}/${organiser_name.toLowerCase().replace(/\s/g,'')}.pass`); const passConfig = { // IMPORTANT: model must have name ending in .pass and cointain only latin characters model: model, certificates: { // DOCS: How to generate new certificates: // https://github.com/alexandercerutti/passkit-generator/wiki/Generating-Certificates wwdr: await readFileAsBuffer( join(__dirname, 'certs', 'AppleWWDRCA2030.pem') ), signerCert: await readFileAsBuffer( join(__dirname, 'certs', 'signerCert.pem') ), signerKey: await readFileAsBuffer( join(__dirname, 'certs', 'signerKey.pem') ), signerKeyPassphrase: signerKeyPassphrase }, } const overrides = { // keys to be added or overridden serialNumber: serialNumber, // uniquly identifies the pass organizationName: organiser_name, description: `Electronic invitations by ${organiser_name}`, backgroundColor: background_color.replace(/\s/g,''), foregroundColor: body_color.replace(/\s/g,''), labelColor: title_color.replace(/\s/g,''), sharingProhibited: !allow_sharing } try { let eventPass = await PKPass.from( passConfig, overrides ); eventPass.type = "eventTicket"; if(isPrimaryTicket){ if(codes[0]){ eventPass.setBarcodes({ altText: codes[0] + ' (' + primary_ticket_title.replace(/:$/,'') + ')', format: type_of_code === 'qr' ? 'PKBarcodeFormatQR' : 'PKBarcodeFormatCode128', message: codes[0], messageEncoding: "iso-8859-1" }); } } else { if(codes[1]){ eventPass.setBarcodes({ altText: codes[1] + ' (' + secondary_ticket_title.replace(/:$/,'') + ')', format: type_of_code === 'qr' ? 'PKBarcodeFormatQR' : 'PKBarcodeFormatCode128', message: codes[1], messageEncoding: "iso-8859-1" }); } } // HEADER if(title !== ''){ eventPass.headerFields.push({ key: "walletHeaderText", label: name ? name : 'Access', value: title, textAlignment: "PKTextAlignmentRight" }); } // SECONDARY FIELDS if(visitor_name !== ''){ eventPass.secondaryFields.push( { key: "fullname", label: "Visitor", value: `${isPrimaryTicket ? 'Guest of ' : ''}${visitor_name ? visitor_name : ''}`, textAlignment: "PKTextAlignmentLeft" }); } // AUXILIARY FIELDS if(date_range){ eventPass.auxiliaryFields.push({ key: "eventduration", label: "Dates", value: date_range, textAlignment: "PKTextAlignmentLeft" }); } if(location !== ''){ eventPass.auxiliaryFields.push({ key: "location", label: "Location", value: location, textAlignment: "PKTextAlignmentLeft" }); } // BACK FIELDS [ { Label: `Dates for ${name}`, Text: formateEventDates(dates) }, { Label: 'Print or download invitation as PDF', Text: `https://XXXXXXXXX.com/api/tickets/${event_id}?pdf=${visitor_id}`, attributedValue: `<a href=\"https://XXXXXXXXX.com/api/tickets/${event_id}?pdf=${visitor_id}\">Download your invitation (PDF)</a>` }, // Generic Back Fields ...back_fields, { Label: `Visit our website`, Text: organiser_website, attributedValue: `<a href=\"${organiser_website}\">${website_short}</a>` }, { Label: `Contact us by email`, Text: organiser_email, attributedValue: `<a href=\"mailto:${organiser_email}\">${organiser_email}</a>` }, { Label: `Contact us by phone`, Text: organiser_phone, attributedValue: `<a href=\"tel:${organiser_phone.replace(/\s/g,'')}\">${organiser_phone}</a>` }, ].forEach( (field, index) => { // f = field eventPass.backFields.push({ key: `back${index}`, label: field.Label, value: field.Text, textAlignment: "PKTextAlignmentLeft", ...(field.attributedValue && { attributedValue: field.attributedValue }) }); }); const buffer = eventPass.getAsBuffer(); return buffer // Alternatively return a stream // let stream = eventPass.getAsStream(); // return stream; } catch (err) { console.error(err); return { error: true, message: err } } }
Might be interesting creating an example to add to the repository...
Originally posted by @johannesmutter in https://github.com/alexandercerutti/passkit-generator/issues/119#issuecomment-1756288658
Yes, works like a charm. Plug & Play.
For others who want to implement passkit-generator in Svelte/ SvelteKit, here's a comprehensive example:
From a server-side route e.g.
/api/tickets/[event_id]/+server.js
the ticket generation function is called (PDF, Apple Wallet, Google Wallet):+server.js
generateAppleWallet.js
(it's a bit messy. also the part for generating the .pass model folder on the fly is missing)