diff --git a/lib/images.js b/lib/images.js index 36e8f5a..bbeff12 100644 --- a/lib/images.js +++ b/lib/images.js @@ -20,11 +20,14 @@ function applyImageMethods(constructor) { // pass.icon(function(callback) { ... }; // console.log(pass.icon()); // - // The 2x suffix is used for high resolution version (file name uses @2x - // suffix). + // The 2x and 3x suffix is used for high resolution version (file name uses + // @2x or @3x suffix). // // pass.icon2x("icon@2x.png"); // console.log(pass.icon2x()); + // + // pass.icon3x("icon@3x.png"); + // console.log(pass.icon3x()); IMAGES.forEach(function(key) { prototype[key] = function(value) { if (arguments.length === 0) { @@ -35,12 +38,22 @@ function applyImageMethods(constructor) { } }; - var retina = key + "2x"; - prototype[retina] = function(value) { + var retina2x = key + "2x"; + prototype[retina2x] = function(value) { + if (arguments.length === 0) { + return this.images[retina2x]; + } else { + this.images[retina2x] = value; + return this; + } + }; + + var retina3x = key + "3x"; + prototype[retina3x] = function(value) { if (arguments.length === 0) { - return this.images[retina]; + return this.images[retina3x]; } else { - this.images[retina] = value; + this.images[retina3x] = value; return this; } }; @@ -56,8 +69,11 @@ function applyImageMethods(constructor) { var files = File.readdirSync(path); files.forEach(function(filename) { var basename = Path.basename(filename, ".png"); - if (/@2x$/.test(basename) && ~IMAGES.indexOf(basename.slice(0, -3))) { - // High resolution + if (/@3x$/.test(basename) && ~IMAGES.indexOf(basename.slice(0, -3))) { + // High resolution 3x + self.images[basename.replace(/@3x$/, "3x")] = Path.resolve(path, filename); + } else if (/@2x$/.test(basename) && ~IMAGES.indexOf(basename.slice(0, -3))) { + // High resolution 2x self.images[basename.replace(/@2x$/, "2x")] = Path.resolve(path, filename); } else if (~IMAGES.indexOf(basename)) { // Normal resolution diff --git a/lib/pass.js b/lib/pass.js index 3fefcfd..2c18bce 100644 --- a/lib/pass.js +++ b/lib/pass.js @@ -11,13 +11,14 @@ var HTTP = require("http"); var HTTPS = require("https"); var Path = require("path"); var Zip = require("./zip"); +var async = require("async"); var zlib = require("zlib"); // Top-level pass fields. var TOP_LEVEL = [ "authenticationToken", "backgroundColor", "barcode", "description", "foregroundColor", "labelColor", "locations", "logoText", - "organizationName", "relevantDate", "serialNumber", + "organizationName", "relevantDate", "serialNumber", "suppressStripShine", "webServiceURL"]; // These top level fields are required for a valid pass. var REQUIRED_TOP_LEVEL = [ "description", "organizationName", "passTypeIdentifier", @@ -33,9 +34,11 @@ var REQUIRED_IMAGES = [ "icon", "logo" ]; // Create a new pass. // -// template - The template -// fields - Pass fields (description, serialNumber, logoText) -function Pass(template, fields, images) { +// tempplate - The template +// fields - Pass fields (description, serialNumber, logoText) +function Pass(template, fields, images, mockSignature, mockModifiedDate) { + this.mockSignature = mockSignature + this.mockModifiedDate = mockModifiedDate this.template = template; this.fields = cloneObject(fields); // Structure is basically reference to all the fields under a given style @@ -139,7 +142,7 @@ Fields.prototype.add = function(key, label, value, options) { // key - Field key // label - Field label (optional) // value - Field value -// Other field options (e.g. dateStyle) +// Other field options (e.g. dateStyle) Fields.prototype.get = function(key) { var fields = this.pass.structure[this.key]; if (fields) { @@ -210,6 +213,8 @@ Pass.prototype.pipe = function(output) { var self = this; var zip = new Zip(output); var lastError; + var doneImages = false; + var doneLocals = false; zip.on("error", function(error) { lastError = error; @@ -228,30 +233,38 @@ Pass.prototype.pipe = function(output) { // Construct manifest here var manifest = {}; // Add file to zip and it's SHA to manifest - function addFile(filename) { - var file = zip.addFile(filename); + function addFile(filename, modified) { + var file = zip.addFile(filename, modified); var sha = new SHAWriteStream(manifest, filename, file); return sha; } // Create pass.json var passJson = new Buffer(JSON.stringify(this.getPassJSON()), "utf-8"); - addFile("pass.json").end(passJson, "utf8"); - - var expecting = 0; - for (var key in this.images) { - var filename = key.replace(/2x$/, "@2x") + ".png"; - addImage(addFile(filename), this.images[key], function(error) { - --expecting; - if (error) - lastError = error; - if (expecting === 0) - doneWithImages(); - }); - ++expecting; - } + addFile("pass.json", self.mockModifiedDate).end(passJson, "utf8"); + + var pass = this + async.eachSeries(Object.keys(pass.images), function (key, cb) { + var filename = key.replace(/3x$/, "@3x"); + filename = filename.replace(/2x$/, "@2x"); + filename += ".png"; + writeFile(addFile(filename, self.mockModifiedDate), pass.images[key], cb); + }, function(err) { + if (err) { + lastError = err; + doneWithWriting(); + } else { + if(!pass.localizations) return doneWithWriting(); + async.eachSeries(Object.keys(pass.localizations), function (key, cb) { + writeFile(addFile(key, self.mockModifiedDate), pass.localizations[key], cb); + }, function(err) { + if (err) lastError = err; + doneWithWriting(); + }) + } + }); - function doneWithImages() { + function doneWithWriting() { if (lastError) { zip.close(); self.emit("error", lastError); @@ -268,7 +281,7 @@ Pass.prototype.pipe = function(output) { zip.on("error", function(error) { self.emit("error", error); }); - }); + }, self.mockSignature, self.mockModifiedDate); }); } } @@ -291,7 +304,7 @@ Pass.prototype.render = function(response, callback) { }; -function addImage(file, source, callback) { +function writeFile(file, source, callback) { if (typeof(source) == "string" || source instanceof String) { if (/^https?:/i.test(source)) { // URL @@ -331,17 +344,56 @@ function addImage(file, source, callback) { } } +function addLocalFile(file, source, callback) { + if (typeof(source) == "string" || source instanceof String) { + if (/^https?:/i.test(source)) { + // URL + var protocol = /^https:/i.test(source) ? HTTPS : HTTP; + protocol.get(source, function(response) { + if (response.statusCode == 200) { + response.on("end", callback); + response.pipe(file); + response.resume(); + } else + callback(new Error("Server returned " + response.statusCode + " for " + source)); + }).on("error", callback); + } else { + // Assume filename + var stream = File.createReadStream(source); + stream.pipe(file); + file.on("close", callback); + } + } else if (source instanceof Buffer) { + file.on("close", callback); + file.write(source); + file.end(); + } else if (typeof(source) == "function") { + try { + source(file); + callback(); + } catch (error) { + callback(error); + } + } else { + // image is not a supported type + callback(new Error("Cannot load image " + file.filename + ", must be String (filename), Buffer or function")); + } +} // Add manifest.json and signature files. -Pass.prototype.signZip = function(zip, manifest, callback) { +Pass.prototype.signZip = function(zip, manifest, callback, mockSignature, mockModifiedDate) { var json = JSON.stringify(manifest); // Add manifest.json - zip.addFile("manifest.json").end(json, "utf-8"); + zip.addFile("manifest.json", mockModifiedDate).end(json, "utf-8"); // Create signature signManifest(this.template, json, function(error, signature) { if (!error) { // Write signature file - zip.addFile("signature").end(signature); + if (mockSignature) { + zip.addFile("signature", mockModifiedDate).end(mockSignature); + } else { + zip.addFile("signature", mockModifiedDate).end(signature); + } } callback(error); }); @@ -360,9 +412,7 @@ function signManifest(template, manifest, callback) { "-passin", "pass:" + template.password ]; var sign = execFile("openssl", args, { stdio: "pipe" }, function(error, stdout, stderr) { - var trimmedStderr = stderr.trim(); - // Windows outputs some unhelpful error messages, but still produces a valid signature - if (error || (trimmedStderr && trimmedStderr.indexOf('- done') < 0)) { + if (error) { callback(new Error(stderr)); } else { var signature = stdout.split(/\n\n/)[3]; diff --git a/lib/template.js b/lib/template.js index abc9895..b3bee3a 100644 --- a/lib/template.js +++ b/lib/template.js @@ -16,7 +16,7 @@ var TEMPLATE = [ "passTypeIdentifier", "teamIdentifier", // Create a new template. // // style - Pass style (coupon, eventTicket, etc) -// fields - Pass fields (passTypeIdentifier, teamIdentifier, etc) +// fields - Pass fields (passTypeIdentifier, teamIdentifier, etc) function Template(style, fields) { if (!~STYLES.indexOf(style)) throw new Error("Unsupported pass style " + style); @@ -44,14 +44,15 @@ Template.prototype.keys = function(path, password) { // Create a new pass from a template. -Template.prototype.createPass = function(fields) { +Template.prototype.createPass = function(fields, mocks) { // Combine template and pass fields var combined = {}; for (var k1 in this.fields) combined[k1] = this.fields[k1]; for (var k2 in fields) combined[k2] = fields[k2]; - return new Pass(this, combined, this.images); + + return new Pass(this, combined, this.images, mocks && mocks.signature || undefined, mocks && mocks.modifiedDate || undefined); };