wallet-pass
Version:
A library for managing wallet passes
602 lines (601 loc) • 23.7 kB
JavaScript
"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;