From eecfdb048fdc18fd594ea91198186242175d395f Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Sat, 27 Jul 2019 00:41:48 +0200 Subject: [PATCH] Added support for personalization / Rewards Enrollment passes --- src/parser.ts | 48 +++++++++++++++++++++++++++++++++++++++++++----- src/pass.ts | 17 ++++++++++++++++- 2 files changed, 59 insertions(+), 6 deletions(-) diff --git a/src/parser.ts b/src/parser.ts index 26fb96b..e4ece3e 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -2,10 +2,12 @@ import * as path from "path"; import forge from "node-forge"; import formatMessage from "./messages"; import { FactoryOptions, PartitionedBundle, BundleUnit, Certificates, FinalCertificates, isValid } from "./schema"; -import { removeHidden, splitBufferBundle } from "./utils"; +import { removeHidden, splitBufferBundle, getAllFilesWithName, hasFilesWithName, deletePersonalization } from "./utils"; import { promisify } from "util"; import { readFile as _readFile, readdir as _readdir } from "fs"; +import debug from "debug"; +const prsDebug = debug("Personalization"); const readDir = promisify(_readdir); const readFile = promisify(_readFile); @@ -38,9 +40,45 @@ export async function getModelContents(model: FactoryOptions["model"]) { } const modelFiles = Object.keys(modelContents.bundle); + const isModelInitialized = ( + modelFiles.includes("pass.json") && + hasFilesWithName("icon", modelFiles, "startsWith") + ); - if (!(modelFiles.includes("pass.json") && modelContents.bundle["pass.json"].length && modelFiles.some(file => Boolean(file.includes("icon") && modelContents.bundle[file].length)))) { - throw new Error("missing icon or pass.json"); + if (!isModelInitialized) { + // @TODO: set a good error message + throw new Error(formatMessage("MODEL_UNINITIALIZED", "parse result")); + } + + // ======================= // + // *** Personalization *** // + // ======================= // + + const personalizationJsonFile = "personalization.json"; + + if (!modelFiles.includes(personalizationJsonFile)) { + return modelContents; + } + + const logoFullNames = getAllFilesWithName("personalizationLogo@", modelFiles, "startsWith"); + if (!(logoFullNames.length && modelContents.bundle[personalizationJsonFile].length)) { + deletePersonalization(modelContents.bundle, logoFullNames); + return modelContents; + } + + try { + const parsedPersonalization = JSON.parse(modelContents.bundle[personalizationJsonFile].toString("utf8")); + const isPersonalizationValid = isValid(parsedPersonalization, "personalizationDict"); + + if (!isPersonalizationValid) { + [...logoFullNames, personalizationJsonFile] + .forEach(file => delete modelContents.bundle[file]); + + return modelContents; + } + } catch (err) { + prsDebug(formatMessage("PRS_INVALID", err)); + deletePersonalization(modelContents.bundle, logoFullNames); } return modelContents; @@ -63,7 +101,7 @@ export async function getModelFolderContents(model: string): Promise file.toLowerCase().includes("icon")) + hasFilesWithName("icon", filteredFiles, "startsWith") ); // Icon is required to proceed @@ -171,7 +209,7 @@ export function getModelBufferContents(model: BundleUnit): PartitionedBundle { const isModelInitialized = ( bundleKeys.length && - bundleKeys.some(file => file.toLowerCase().includes("icon")) + hasFilesWithName("icon", bundleKeys, "startsWith") ); // Icon is required to proceed diff --git a/src/pass.ts b/src/pass.ts index d74da1b..a06d1c8 100644 --- a/src/pass.ts +++ b/src/pass.ts @@ -7,7 +7,7 @@ import { ZipFile } from "yazl"; import * as schema from "./schema"; import formatMessage from "./messages"; import FieldsArray from "./fieldsArray"; -import { generateStringFile, dateToW3CString, isValidRGB } from "./utils"; +import { generateStringFile, dateToW3CString, isValidRGB, deletePersonalization, getAllFilesWithName } from "./utils"; const barcodeDebug = debug("passkit:barcode"); const genericDebug = debug("passkit:generic"); @@ -134,6 +134,21 @@ export class Pass { // Editing Pass.json this.bundle["pass.json"] = this._patch(this.bundle["pass.json"]); + /** + * Checking Personalization, as this is available only with NFC + * @see https://apple.co/2SHfb22 + */ + const currentBundleFiles = Object.keys(this.bundle); + + if (!this[passProps].nfc && currentBundleFiles.includes("personalization.json")) { + genericDebug(formatMessage("PRS_REMOVED")); + deletePersonalization(this.bundle, getAllFilesWithName( + "personalizationLogo@", + currentBundleFiles, + "startsWith" + )); + } + const finalBundle = { ...this.bundle } as schema.BundleUnit; /**