From 6462a852fcc68161292125467a5c5b9720c9a2eb Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Thu, 9 Jan 2025 00:17:19 +0100 Subject: [PATCH] Added new method setRelevantDates --- specs/PKPass.spec.cjs | 121 ++++++++++++++++++++++++++++++++++-------- src/PKPass.ts | 61 +++++++++++++++++++++ src/messages.ts | 4 ++ src/schemas/index.ts | 31 +++++++---- 4 files changed, 184 insertions(+), 33 deletions(-) diff --git a/specs/PKPass.spec.cjs b/specs/PKPass.spec.cjs index 80dc760..195ed79 100644 --- a/specs/PKPass.spec.cjs +++ b/specs/PKPass.spec.cjs @@ -87,6 +87,11 @@ const modelFiles = {}; const EXAMPLE_PATH_RELATIVE = "../examples/models/examplePass.pass"; +/** + * @param {string} folder + * @returns + */ + function unpackFolder(folder) { const entryList = fs.readdirSync(path.resolve(__dirname, folder)); @@ -859,35 +864,107 @@ describe("PKPass", () => { }); }); - describe("relevant date", () => { - it("should set pass relevant date", () => { - pkpass.setRelevantDate(new Date("2023-04-11T00:15+10:00")); + describe("Date relevancy", () => { + describe("(deprecated iOS 18) (root).relevantDate", () => { + it("should set pass relevant date", () => { + pkpass.setRelevantDate(new Date("2023-04-11T00:15+10:00")); - const passjsonGenerated = getGeneratedPassJson(pkpass); + const passjsonGenerated = getGeneratedPassJson(pkpass); - expect(passjsonGenerated.relevantDate).toBe( - "2023-04-10T14:15:00.000Z", - ); + expect(passjsonGenerated.relevantDate).toBe( + "2023-04-10T14:15:00.000Z", + ); + }); + + it("should reset relevant date", () => { + pkpass.setRelevantDate(new Date(2023, 3, 10, 14, 15)); + pkpass.setRelevantDate(null); + + const passjsonGenerated = getGeneratedPassJson(pkpass); + + expect(passjsonGenerated.relevantDate).toBeUndefined(); + }); + + it("should throw if an invalid date is received", () => { + expect(() => + // @ts-expect-error + pkpass.setRelevantDate("32/18/228317"), + ).toThrowError(); + // @ts-expect-error + expect(() => pkpass.setRelevantDate(undefined)).toThrowError(); + // @ts-expect-error + expect(() => pkpass.setRelevantDate(5)).toThrowError(); + // @ts-expect-error + expect(() => pkpass.setRelevantDate({})).toThrowError(); + }); }); - it("should reset relevant date", () => { - pkpass.setRelevantDate(new Date(2023, 3, 10, 14, 15)); - pkpass.setRelevantDate(null); + describe("setRelevantDates", () => { + it("should accept strings", () => { + pkpass.setRelevantDates([ + { + startDate: "2025-01-08T22:17:30.000Z", + endDate: "2025-01-08T23:58:25.000Z", + }, + { + relevantDate: "2025-01-08T22:17:30.000Z", + }, + ]); - const passjsonGenerated = getGeneratedPassJson(pkpass); + const passjsonGenerated = getGeneratedPassJson(pkpass); - expect(passjsonGenerated.relevantDate).toBeUndefined(); - }); + expect(passjsonGenerated.relevantDates).toMatchObject([ + { + startDate: "2025-01-08T22:17:30.000Z", + endDate: "2025-01-08T23:58:25.000Z", + }, + { + relevantDate: "2025-01-08T22:17:30.000Z", + }, + ]); + }); - it("should throw if an invalid date is received", () => { - // @ts-expect-error - expect(() => pkpass.setRelevantDate("32/18/228317")).toThrowError(); - // @ts-expect-error - expect(() => pkpass.setRelevantDate(undefined)).toThrowError(); - // @ts-expect-error - expect(() => pkpass.setRelevantDate(5)).toThrowError(); - // @ts-expect-error - expect(() => pkpass.setRelevantDate({})).toThrowError(); + it("should accept dates", () => { + pkpass.setRelevantDates([ + { + startDate: new Date(2025, 1, 8, 23, 58, 25), + endDate: new Date(2025, 1, 8, 23, 58, 25), + }, + { + relevantDate: new Date(2025, 1, 8, 23, 58, 25), + }, + ]); + + const passjsonGenerated = getGeneratedPassJson(pkpass); + + expect(passjsonGenerated.relevantDates).toMatchObject([ + { + startDate: "2025-02-08T22:58:25.000Z", + endDate: "2025-02-08T22:58:25.000Z", + }, + { + relevantDate: "2025-02-08T22:58:25.000Z", + }, + ]); + }); + + it("should allow resetting", () => { + pkpass.setRelevantDates([ + { + startDate: "2025-01-08T22:17:30.000Z", + endDate: "2025-01-08T23:58:25.000Z", + }, + { + relevantDate: "2025-01-08T22:17:30.000Z", + }, + ]); + + pkpass.setRelevantDates(null); + + const passjsonGenerated = getGeneratedPassJson(pkpass); + + expect(passjsonGenerated.relevantDates).toBeUndefined(); + }); }); }); diff --git a/src/PKPass.ts b/src/PKPass.ts index fe5f1d8..9a7e57a 100644 --- a/src/PKPass.ts +++ b/src/PKPass.ts @@ -956,6 +956,54 @@ export default class PKPass extends Bundle { ); } + /** + * Allows setting a series of relevancy intervals or + * relevancy entries for the pass. + * + * @param {Schemas.RelevantDate[] | null} relevancyEntries + * @returns {void} + */ + + public setRelevantDates( + relevancyEntries: Schemas.RelevantDate[] | null, + ): void { + Utils.assertUnfrozen(this); + + if (relevancyEntries === null) { + this[propsSymbol]["relevantDates"] = undefined; + return; + } + + const processedDateEntries = relevancyEntries.reduce< + Schemas.RelevantDate[] + >((acc, entry) => { + try { + Schemas.validate(Schemas.RelevantDate, entry); + + if (isRelevantEntry(entry)) { + acc.push({ + relevantDate: Utils.processDate( + new Date(entry.relevantDate), + ), + }); + + return acc; + } + + acc.push({ + startDate: Utils.processDate(new Date(entry.startDate)), + endDate: Utils.processDate(new Date(entry.endDate)), + }); + } catch (err) { + console.warn(new TypeError(Messages.RELEVANT_DATE.INVALID)); + } + + return acc; + }, []); + + this[propsSymbol]["relevantDates"] = processedDateEntries; + } + /** * Allows setting a relevant date in which the OS * should show this pass. @@ -964,6 +1012,13 @@ export default class PKPass extends Bundle { * * @param {Date | null} date * @throws if pass is frozen due to previous export + * + * @warning `relevantDate` property has been deprecated in iOS 18 + * in order to get replaced by `relevantDates` array of intervals + * (`relevantDates[].startDate` + `relevantDates[].endDate`) + * or single date (`relevantDates[].relevantDate`). This method will + * set both the original, as the new one will get ignored in older + * iOS versions. */ public setRelevantDate(date: Date | null): void { @@ -1086,3 +1141,9 @@ function validateJSONBuffer( return Schemas.validate(schema, contentAsJSON); } + +function isRelevantEntry( + entry: Schemas.RelevantDate, +): entry is Schemas.RelevancyEntry { + return Object.prototype.hasOwnProperty.call(entry, "relevantDate"); +} diff --git a/src/messages.ts b/src/messages.ts index a520cc8..0bc130b 100644 --- a/src/messages.ts +++ b/src/messages.ts @@ -43,6 +43,10 @@ export const FIELDS = { "Cannot add field with key '%s': another field already owns this key. Ignored.", } as const; +export const RELEVANT_DATE = { + INVALID: "Cannot set relevant date. Date format is invalid", +} as const; + export const DATE = { INVALID: "Cannot set %s. Invalid date %s", } as const; diff --git a/src/schemas/index.ts b/src/schemas/index.ts index f0431d5..86dbd77 100644 --- a/src/schemas/index.ts +++ b/src/schemas/index.ts @@ -32,13 +32,13 @@ export const PreferredStyleSchemes = Joi.array().items( /** * A single interval can span at most 24 hours */ -interface RelevancyInterval { - startDate: string; - endDate: string; +export interface RelevancyInterval { + startDate: string | Date; + endDate: string | Date; } -interface RelevancyEntry { - relevantDate: string; +export interface RelevancyEntry { + relevantDate: string | Date; } /** @@ -52,13 +52,22 @@ interface RelevancyEntry { export type RelevantDate = RelevancyInterval | RelevancyEntry; -const RelevantDate = Joi.alternatives( +export const RelevantDate = Joi.alternatives( Joi.object().keys({ - startDate: Joi.string().required(), - endDate: Joi.string().required(), + startDate: Joi.alternatives( + Joi.string().isoDate(), + Joi.date().iso(), + ).required(), + endDate: Joi.alternatives( + Joi.string().isoDate(), + Joi.date().iso(), + ).required(), }), Joi.object().keys({ - relevantDate: Joi.string().required(), + relevantDate: Joi.alternatives( + Joi.string().isoDate(), + Joi.date().iso(), + ).required(), }), ); @@ -492,7 +501,7 @@ export const Template = Joi.object