UNPKG

wallet-pass

Version:
602 lines (601 loc) 23.7 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.GoogleGenericPass = void 0; const jwt = __importStar(require("jsonwebtoken")); const fs_1 = __importDefault(require("fs")); class GoogleGenericPass { constructor(issuerId, passId, classId) { this.passObject = { id: `${issuerId}.${passId}`, classId: `${issuerId}.${classId}`, genericType: 'GENERIC_TYPE_UNSPECIFIED', // Initialize the additionalInfo array with a default item to avoid empty array issues additionalInfo: [ { id: 'default_info', labelValue: { label: 'Info', value: 'See details', }, }, ], }; } /** * Load service account credentials for JWT signing */ setServiceAccountCredentials(serviceAccountEmail, privateKeyPathOrJson) { this.serviceAccountEmail = serviceAccountEmail; try { let serviceAccountJson; // Check if the input is a JSON string try { serviceAccountJson = JSON.parse(privateKeyPathOrJson); } catch (jsonError) { // If parsing fails, treat as file path if (!fs_1.default.existsSync(privateKeyPathOrJson)) { throw new Error(`Invalid file path or malformed JSON: ${jsonError instanceof Error ? jsonError.message : 'Unknown error'}`); } serviceAccountJson = JSON.parse(fs_1.default.readFileSync(privateKeyPathOrJson, 'utf8')); } // Extract the private key from the JSON if (serviceAccountJson.private_key) { this.privateKey = serviceAccountJson.private_key; } else { throw new Error('Private key not found in the provided service account JSON.'); } } catch (error) { throw new Error(`Failed to load service account key: ${error instanceof Error ? error.message : 'Unknown error'}`); } return this; } /** * Set service account credentials directly with key content */ setServiceAccountCredentialsFromKeyData(serviceAccountEmail, privateKey) { this.serviceAccountEmail = serviceAccountEmail; this.privateKey = privateKey; return this; } /** * Define the pass class (template) */ setPassClass(issuerName) { this.passClass = { id: this.passObject.classId, issuerName, }; return this; } /** * Set pass class (template) with additional properties */ setPassClassWithDetails(issuerName, reviewStatus, logoImageUrl, logoDescription, heroImageUrl, heroDescription, hexBackgroundColor) { this.passClass = { id: this.passObject.classId, issuerName: this.ensureNonEmptyString(issuerName, 'Issuer'), }; if (reviewStatus) { this.passClass.reviewStatus = reviewStatus; } if (logoImageUrl) { this.passClass.logoImage = this.createImageObject(logoImageUrl, logoDescription); } if (heroImageUrl) { this.passClass.heroImage = this.createImageObject(heroImageUrl, heroDescription); } if (hexBackgroundColor) { this.passClass.hexBackgroundColor = hexBackgroundColor; } return this; } /** * Add class template info to the pass class */ setClassTemplateInfo(cardRowTemplates) { if (!this.passClass) { throw new Error('Pass class must be created before adding template info'); } this.passClass.classTemplateInfo = { cardTemplateOverride: { cardRowTemplateInfos: cardRowTemplates, }, }; return this; } /** * Helper to create a two-item row template */ createTwoItemsRow(startFieldPath, endFieldPath) { const row = { twoItems: { startItem: startFieldPath ? { firstValue: { fields: [{ fieldPath: startFieldPath }], }, } : undefined, endItem: endFieldPath ? { firstValue: { fields: [{ fieldPath: endFieldPath }], }, } : undefined, }, }; return row; } /** * Set basic pass properties */ setBasicInfo(genericType, hexBackgroundColor) { this.passObject.genericType = genericType; if (hexBackgroundColor) this.passObject.hexBackgroundColor = hexBackgroundColor; return this; } /** * Set card title */ setCardTitle(title) { this.passObject.cardTitle = { defaultValue: { language: 'en-US', value: this.ensureNonEmptyString(title, 'Card'), }, }; return this; } /** * Set header and subheader */ setHeaderInfo(header, subheader) { this.passObject.header = { defaultValue: { language: 'en-US', value: this.ensureNonEmptyString(header, 'Header'), }, }; if (subheader !== undefined) { // Changed from if (subheader) to properly handle empty strings this.passObject.subheader = { defaultValue: { language: 'en-US', value: this.ensureNonEmptyString(subheader, 'Subheader'), }, }; } return this; } /** * Add a text module */ addTextModule(id, body, header) { if (!this.passObject.textModulesData) { this.passObject.textModulesData = []; } // Format text modules exactly as expected by Google Wallet API this.passObject.textModulesData.push({ id, body: this.ensureNonEmptyString(body, 'Information'), header: header ? this.ensureNonEmptyString(header, 'Section') : undefined, }); return this; } /** * Add logo to pass */ setLogo(imageUrl, description) { this.passObject.logo = this.createImageObject(imageUrl, description); return this; } /** * Add hero image to pass */ setHeroImage(imageUrl, description) { this.passObject.heroImage = this.createImageObject(imageUrl, description); return this; } /** * Add an image module */ addImageModule(id, imageUrl, description) { if (!this.passObject.imageModulesData) { this.passObject.imageModulesData = []; } this.passObject.imageModulesData.push({ id, mainImage: this.createImageObject(imageUrl, description), }); return this; } /** * Add an item to the additionalInfo section */ addAdditionalInfo(id, label, value) { if (!this.passObject.additionalInfo) { this.passObject.additionalInfo = []; } this.passObject.additionalInfo.push({ id, labelValue: { label: this.ensureNonEmptyString(label, 'Info'), value: this.ensureNonEmptyString(value, 'Value'), }, }); return this; } /** * Add Android app link */ addAndroidAppLink(title, targetUri, description, logoImageUrl, logoDescription) { if (!this.passObject.appLinkData) { this.passObject.appLinkData = {}; } this.passObject.appLinkData.androidAppLinkInfo = { title: this.ensureNonEmptyString(title, 'Android App'), description: description, appTarget: { targetUri, }, }; if (logoImageUrl) { this.passObject.appLinkData.androidAppLinkInfo.appLogoImage = this.createImageObject(logoImageUrl, logoDescription); } return this; } /** * Add iOS app link */ addIosAppLink(title, targetUri, description, logoImageUrl, logoDescription) { if (!this.passObject.appLinkData) { this.passObject.appLinkData = {}; } this.passObject.appLinkData.iosAppLinkInfo = { title: this.ensureNonEmptyString(title, 'iOS App'), description: description, appTarget: { targetUri, }, }; if (logoImageUrl) { this.passObject.appLinkData.iosAppLinkInfo.appLogoImage = this.createImageObject(logoImageUrl, logoDescription); } return this; } /** * Add web app link */ addWebAppLink(title, targetUri, description, logoImageUrl, logoDescription) { if (!this.passObject.appLinkData) { this.passObject.appLinkData = {}; } this.passObject.appLinkData.webAppLinkInfo = { title: this.ensureNonEmptyString(title, 'Web App'), description: description, appTarget: { targetUri, }, }; if (logoImageUrl) { this.passObject.appLinkData.webAppLinkInfo.appLogoImage = this.createImageObject(logoImageUrl, logoDescription); } return this; } /** * Set grouping info */ setGroupingInfo(groupingId, sortIndex) { this.passObject.groupingInfo = { groupingId, ...(sortIndex !== undefined ? { sortIndex } : {}), }; return this; } /** * Helper to create image objects */ createImageObject(imageUrl, description) { // The critical issue: Google Wallet REQUIRES a contentDescription // for all images, even when no description is provided return { sourceUri: { uri: imageUrl, }, // Always include contentDescription, regardless if description is provided contentDescription: { defaultValue: { language: 'en-US', value: description || 'Image', // Default value if no description provided }, }, }; } /** * Helper to ensure localized string values are never empty */ ensureNonEmptyString(value, fallback) { if (!value || value.trim() === '') { return fallback; } return value; } /** * Validate the entire pass object before generating JWT * This ensures no empty localized strings exist */ validatePassObject() { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p; // Validate card title if ((_a = this.passObject.cardTitle) === null || _a === void 0 ? void 0 : _a.defaultValue) { this.passObject.cardTitle.defaultValue.value = this.ensureNonEmptyString(this.passObject.cardTitle.defaultValue.value, 'Card'); } // Validate header if ((_b = this.passObject.header) === null || _b === void 0 ? void 0 : _b.defaultValue) { this.passObject.header.defaultValue.value = this.ensureNonEmptyString(this.passObject.header.defaultValue.value, 'Header'); } // Validate subheader if ((_c = this.passObject.subheader) === null || _c === void 0 ? void 0 : _c.defaultValue) { this.passObject.subheader.defaultValue.value = this.ensureNonEmptyString(this.passObject.subheader.defaultValue.value, 'Subheader'); } // Validate text modules if (this.passObject.textModulesData) { this.passObject.textModulesData = this.passObject.textModulesData.map(module => ({ ...module, body: this.ensureNonEmptyString(module.body, `Info ${module.id}`), header: module.header ? this.ensureNonEmptyString(module.header, `Section ${module.id}`) : undefined, })); } // Validate links if (this.passObject.linksModuleData && this.passObject.linksModuleData.uris) { this.passObject.linksModuleData.uris = this.passObject.linksModuleData.uris.map(link => ({ ...link, description: this.ensureNonEmptyString(link.description, `Link ${link.id}`), })); } // Validate custom info modules if (this.passObject.customInfoModules) { this.passObject.customInfoModules = this.passObject.customInfoModules.map(module => ({ ...module, labelValue: { label: this.ensureNonEmptyString(module.labelValue.label, `Label ${module.id}`), value: this.ensureNonEmptyString(module.labelValue.value, `Value ${module.id}`), }, })); } // Validate additional info modules if (this.passObject.additionalInfo) { this.passObject.additionalInfo = this.passObject.additionalInfo.map(module => ({ ...module, labelValue: { label: this.ensureNonEmptyString(module.labelValue.label, `Label ${module.id}`), value: this.ensureNonEmptyString(module.labelValue.value, `Value ${module.id}`), }, })); } // Validate image descriptions if ((_e = (_d = this.passObject.logo) === null || _d === void 0 ? void 0 : _d.contentDescription) === null || _e === void 0 ? void 0 : _e.defaultValue) { this.passObject.logo.contentDescription.defaultValue.value = this.ensureNonEmptyString(this.passObject.logo.contentDescription.defaultValue.value, 'Logo'); } if ((_g = (_f = this.passObject.heroImage) === null || _f === void 0 ? void 0 : _f.contentDescription) === null || _g === void 0 ? void 0 : _g.defaultValue) { this.passObject.heroImage.contentDescription.defaultValue.value = this.ensureNonEmptyString(this.passObject.heroImage.contentDescription.defaultValue.value, 'Hero Image'); } // Validate image modules if (this.passObject.imageModulesData) { this.passObject.imageModulesData.forEach(module => { var _a, _b; if ((_b = (_a = module.mainImage) === null || _a === void 0 ? void 0 : _a.contentDescription) === null || _b === void 0 ? void 0 : _b.defaultValue) { module.mainImage.contentDescription.defaultValue.value = this.ensureNonEmptyString(module.mainImage.contentDescription.defaultValue.value, `Image ${module.id}`); } }); } // Validate barcode alternate text if ((_h = this.passObject.barcode) === null || _h === void 0 ? void 0 : _h.alternateText) { this.passObject.barcode.alternateText = this.ensureNonEmptyString(this.passObject.barcode.alternateText, 'Scan this code'); } // Validate app link data if (this.passObject.appLinkData) { if (this.passObject.appLinkData.androidAppLinkInfo) { this.passObject.appLinkData.androidAppLinkInfo.title = this.ensureNonEmptyString(this.passObject.appLinkData.androidAppLinkInfo.title, 'Android App'); if ((_k = (_j = this.passObject.appLinkData.androidAppLinkInfo.appLogoImage) === null || _j === void 0 ? void 0 : _j.contentDescription) === null || _k === void 0 ? void 0 : _k.defaultValue) { this.passObject.appLinkData.androidAppLinkInfo.appLogoImage.contentDescription.defaultValue.value = this.ensureNonEmptyString(this.passObject.appLinkData.androidAppLinkInfo.appLogoImage.contentDescription .defaultValue.value, 'Android App Logo'); } } if (this.passObject.appLinkData.iosAppLinkInfo) { this.passObject.appLinkData.iosAppLinkInfo.title = this.ensureNonEmptyString(this.passObject.appLinkData.iosAppLinkInfo.title, 'iOS App'); if ((_m = (_l = this.passObject.appLinkData.iosAppLinkInfo.appLogoImage) === null || _l === void 0 ? void 0 : _l.contentDescription) === null || _m === void 0 ? void 0 : _m.defaultValue) { this.passObject.appLinkData.iosAppLinkInfo.appLogoImage.contentDescription.defaultValue.value = this.ensureNonEmptyString(this.passObject.appLinkData.iosAppLinkInfo.appLogoImage.contentDescription .defaultValue.value, 'iOS App Logo'); } } if (this.passObject.appLinkData.webAppLinkInfo) { this.passObject.appLinkData.webAppLinkInfo.title = this.ensureNonEmptyString(this.passObject.appLinkData.webAppLinkInfo.title, 'Web App'); if ((_p = (_o = this.passObject.appLinkData.webAppLinkInfo.appLogoImage) === null || _o === void 0 ? void 0 : _o.contentDescription) === null || _p === void 0 ? void 0 : _p.defaultValue) { this.passObject.appLinkData.webAppLinkInfo.appLogoImage.contentDescription.defaultValue.value = this.ensureNonEmptyString(this.passObject.appLinkData.webAppLinkInfo.appLogoImage.contentDescription .defaultValue.value, 'Web App Logo'); } } } } /** * Add barcode to pass */ setBarcode(value, type = 'QR_CODE', alternateText) { // Google Wallet expects barcode to have an alternateText, even if it's empty this.passObject.barcode = { type, value, alternateText: alternateText || '', // Include empty string if no value provided }; return this; } /** * Add links module */ addLinks(links) { // Ensure all descriptions are non-empty this.passObject.linksModuleData = { uris: links.map(link => ({ ...link, description: this.ensureNonEmptyString(link.description, `Link ${link.id}`), })), }; return this; } /** * Add locations */ addLocations(locations) { this.passObject.locations = locations; return this; } /** * Set validity time interval */ setValidTimeInterval(start, end) { this.passObject.validTimeInterval = { start: { date: start }, }; if (end) { this.passObject.validTimeInterval.end = { date: end }; } return this; } /** * Add custom info module */ addCustomInfoModule(id, label, value) { if (!this.passObject.customInfoModules) { this.passObject.customInfoModules = []; } this.passObject.customInfoModules.push({ id, labelValue: { label: this.ensureNonEmptyString(label, 'Info'), value: this.ensureNonEmptyString(value, 'Value'), }, }); return this; } /** * Add any custom field to the pass object */ addCustomField(key, value) { this.passObject[key] = value; return this; } /** * Generate signed JWT */ generateJwt(origins = []) { if (!this.serviceAccountEmail || !this.privateKey) { throw new Error('Service account credentials not set'); } // Validate all localized strings before generating the JWT this.validatePassObject(); // Also validate pass class if present if (this.passClass) { this.passClass.issuerName = this.ensureNonEmptyString(this.passClass.issuerName, 'Issuer'); } const payload = { iss: this.serviceAccountEmail, aud: 'google', typ: 'savetowallet', iat: Math.floor(Date.now() / 1000), origins, payload: { genericObjects: [this.passObject], }, }; if (this.passClass) { payload.payload.genericClasses = [this.passClass]; } return jwt.sign(payload, this.privateKey, { algorithm: 'RS256' }); } /** * Generate "Add to Google Wallet" link */ generateAddToWalletLink(origins = []) { const token = this.generateJwt(origins); return `https://pay.google.com/gp/v/save/${token}`; } /** * Debug function to log generated payload */ debugPayload() { if (!this.serviceAccountEmail || !this.privateKey) { console.log('Service account not configured'); return; } try { this.validatePassObject(); const payload = { iss: this.serviceAccountEmail, aud: 'google', typ: 'savetowallet', iat: Math.floor(Date.now() / 1000), origins: [], payload: { genericObjects: [this.passObject], }, }; if (this.passClass) { payload.payload.genericClasses = [this.passClass]; } console.log('JWT payload:', JSON.stringify(payload, null, 2)); } catch (error) { console.error('Error preparing payload:', error); } } /** * Get the pass object */ getPassObject() { return this.passObject; } /** * Get the pass class */ getPassClass() { return this.passClass; } } exports.GoogleGenericPass = GoogleGenericPass;