Merge branch 'feature/iOS18-changes'

This commit is contained in:
Alexander Cerutti
2025-01-11 14:20:52 +01:00
9 changed files with 2382 additions and 1585 deletions

View File

@@ -30,6 +30,7 @@
"node": ">=14.18.1"
},
"devDependencies": {
"@jest/globals": "^29.7.0",
"@types/do-not-zip": "^1.0.2",
"@types/node": "^16.11.26",
"@types/node-forge": "^1.3.11",

2692
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -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,7 +864,8 @@ describe("PKPass", () => {
});
});
describe("relevant date", () => {
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"));
@@ -880,8 +886,10 @@ describe("PKPass", () => {
});
it("should throw if an invalid date is received", () => {
expect(() =>
// @ts-expect-error
expect(() => pkpass.setRelevantDate("32/18/228317")).toThrowError();
pkpass.setRelevantDate("32/18/228317"),
).toThrowError();
// @ts-expect-error
expect(() => pkpass.setRelevantDate(undefined)).toThrowError();
// @ts-expect-error
@@ -891,6 +899,75 @@ describe("PKPass", () => {
});
});
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);
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 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();
});
});
});
describe("barcodes", () => {
it("should create all barcode structures if a message is used", () => {
pkpass.setBarcodes("a test barcode");

View File

@@ -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");
}

View File

@@ -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;

View File

@@ -19,6 +19,14 @@ export interface PassFields {
primaryFields: Field[];
secondaryFields: Field[];
transitType?: TransitType;
/**
* @iOSVersion 18
* @passStyle eventTicket (new layout)
* @passDomain dashboard
*
* @see \<undiclosed>
*/
additionalInfoFields?: Field[];
}
@@ -29,5 +37,13 @@ export const PassFields = Joi.object<PassFields>().keys({
primaryFields: Joi.array().items(Field),
secondaryFields: Joi.array().items(Field),
transitType: TransitType,
/**
* @iOSVersion 18
* @passStyle eventTicket (new layout)
* @passDomain dashboard
*
* @see \<undiclosed>
*/
additionalInfoFields: Joi.array().items(Field),
});

View File

@@ -0,0 +1,147 @@
import Joi from "joi";
import { RGB_HEX_COLOR_REGEX } from "./regexps";
/**
* These couple of structures are organized alphabetically,
* according to the order on the developer documentation.
*
* @see https://developer.apple.com/documentation/walletpasses/semantictagtype
*/
/**
* @see https://developer.apple.com/documentation/walletpasses/semantictagtype/currencyamount-data.dictionary
*/
export interface CurrencyAmount {
currencyCode?: string; // ISO 4217 currency code
amount?: string;
}
export const CurrencyAmount = Joi.object<CurrencyAmount>().keys({
currencyCode: Joi.string(),
amount: Joi.string(),
});
/**
* @iOSVersion 18
* @passStyle eventTicket (new layout)
*
* @see \<undiclosed>
*/
export interface EventDateInfo {
date: string;
ignoreTimeComponents?: boolean;
timeZone?: string;
}
export const EventDateInfo = Joi.object<EventDateInfo>().keys({
date: Joi.string().isoDate().required(),
ignoreTimeComponents: Joi.boolean(),
timeZone: Joi.string(),
});
/**
* @see https://developer.apple.com/documentation/walletpasses/semantictagtype/location-data.dictionary
*/
export interface Location {
latitude: number;
longitude: number;
}
export const Location = Joi.object<Location>().keys({
latitude: Joi.number().required(),
longitude: Joi.number().required(),
});
/**
* @see https://developer.apple.com/documentation/walletpasses/semantictagtype/personnamecomponents-data.dictionary
*/
export interface PersonNameComponents {
familyName?: string;
givenName?: string;
middleName?: string;
namePrefix?: string;
nameSuffix?: string;
nickname?: string;
phoneticRepresentation?: string;
}
export const PersonNameComponents = Joi.object<PersonNameComponents>().keys({
givenName: Joi.string(),
familyName: Joi.string(),
middleName: Joi.string(),
namePrefix: Joi.string(),
nameSuffix: Joi.string(),
nickname: Joi.string(),
phoneticRepresentation: Joi.string(),
});
/**
* @see https://developer.apple.com/documentation/walletpasses/semantictagtype/seat-data.dictionary
*/
export interface Seat {
seatSection?: string;
seatRow?: string;
seatNumber?: string;
seatIdentifier?: string;
seatType?: string;
seatDescription?: string;
/**
* @iOSVersion 18
* @passStyle eventTicket (new layout)
*/
seatAisle?: string;
/**
* @iOSVersion 18
* @passStyle eventTicket (new layout)
*/
seatLevel?: string;
/**
* @iOSVersion 18
* @passStyle eventTicket (new layout)
*/
seatSectionColor?: string;
}
export const Seat = Joi.object<Seat>().keys({
seatSection: Joi.string(),
seatRow: Joi.string(),
seatNumber: Joi.string(),
seatIdentifier: Joi.string(),
seatType: Joi.string(),
seatDescription: Joi.string(),
/**
* @iOSVersion 18
* @passStyle eventTicket (new layout)
*/
seatAisle: Joi.string(),
/**
* @iOSVersion 18
* @passStyle eventTicket (new layout)
*/
seatLevel: Joi.string(),
/**
* @iOSVersion 18
* @passStyle eventTicket (new layout)
*/
seatSectionColor: Joi.string().regex(RGB_HEX_COLOR_REGEX),
});
/**
* @see https://developer.apple.com/documentation/walletpasses/semantictagtype/wifinetwork-data.dictionary
*/
export interface WifiNetwork {
password: string;
ssid: string;
}
export const WifiNetwork = Joi.object<WifiNetwork>().keys({
password: Joi.string().required(),
ssid: Joi.string().required(),
});

View File

@@ -1,5 +1,5 @@
import Joi from "joi";
import { RGB_HEX_COLOR_REGEX } from "./regexps";
import * as SemanticTagType from "./SemanticTagType";
/**
* For a better description of every single field,
@@ -8,117 +8,6 @@ import { RGB_HEX_COLOR_REGEX } from "./regexps";
* @see https://developer.apple.com/documentation/walletpasses/semantictags
*/
/**
* @see https://developer.apple.com/documentation/walletpasses/semantictagtype
*/
declare namespace SemanticTagType {
interface PersonNameComponents {
familyName?: string;
givenName?: string;
middleName?: string;
namePrefix?: string;
nameSuffix?: string;
nickname?: string;
phoneticRepresentation?: string;
}
interface CurrencyAmount {
currencyCode?: string; // ISO 4217 currency code
amount?: string;
}
interface Location {
latitude: number;
longitude: number;
}
interface Seat {
seatSection?: string;
seatRow?: string;
seatNumber?: string;
seatIdentifier?: string;
seatType?: string;
seatDescription?: string;
/**
* For newly-introduced event tickets
* in iOS 18
*/
seatAisle?: string;
/**
* For newly-introduced event tickets
* in iOS 18
*/
seatLevel?: string;
/**
* For newly-introduced event tickets
* in iOS 18
*/
seatSectionColor?: string;
}
interface WifiNetwork {
password: string;
ssid: string;
}
}
const CurrencyAmount = Joi.object<SemanticTagType.CurrencyAmount>().keys({
currencyCode: Joi.string(),
amount: Joi.string(),
});
const PersonNameComponent =
Joi.object<SemanticTagType.PersonNameComponents>().keys({
givenName: Joi.string(),
familyName: Joi.string(),
middleName: Joi.string(),
namePrefix: Joi.string(),
nameSuffix: Joi.string(),
nickname: Joi.string(),
phoneticRepresentation: Joi.string(),
});
const SeatSemantics = Joi.object<SemanticTagType.Seat>().keys({
seatSection: Joi.string(),
seatRow: Joi.string(),
seatNumber: Joi.string(),
seatIdentifier: Joi.string(),
seatType: Joi.string(),
seatDescription: Joi.string(),
/**
* Newly-introduced in iOS 18
* Used in poster event tickets
*/
seatAisle: Joi.string(),
/**
* Newly-introduced in iOS 18
* Used in poster event tickets
*/
seatLevel: Joi.string(),
/**
* For newly-introduced event tickets
* in iOS 18
*/
seatSectionColor: Joi.string().regex(RGB_HEX_COLOR_REGEX),
});
const LocationSemantics = Joi.object<SemanticTagType.Location>().keys({
latitude: Joi.number().required(),
longitude: Joi.number().required(),
});
const WifiNetwork = Joi.object<SemanticTagType.WifiNetwork>().keys({
password: Joi.string().required(),
ssid: Joi.string().required(),
});
/**
* Alphabetical order
* @see https://developer.apple.com/documentation/walletpasses/semantictags
@@ -126,14 +15,14 @@ const WifiNetwork = Joi.object<SemanticTagType.WifiNetwork>().keys({
export interface Semantics {
/**
* For newly-introduced event tickets
* in iOS 18
* @iOSVersion 18
* @passStyle eventTicket (new layout)
*/
admissionLevel?: string;
/**
* For newly-introduced event tickets
* in iOS 18
* @iOSVersion 18
* @passStyle eventTicket (new layout)
*/
admissionLevelAbbreviation?: string;
@@ -141,22 +30,22 @@ export interface Semantics {
artistIDs?: string[];
/**
* For newly-introduced event tickets
* in iOS 18
* @iOSVersion 18
* @passStyle eventTicket (new layout)
*/
albumIDs?: string[];
/**
* For newly-introduced event tickets
* in iOS 18
* @iOSVersion 18
* @passStyle eventTicket (new layout)
*/
airplay?: {
airPlayDeviceGroupToken: string;
}[];
/**
* For newly-introduced event tickets
* in iOS 18
* @iOSVersion 18
* @passStyle eventTicket (new layout)
*/
attendeeName?: string;
@@ -165,8 +54,8 @@ export interface Semantics {
awayTeamName?: string;
/**
* For newly-introduced event tickets
* in iOS 18
* @iOSVersion 18
* @passStyle eventTicket (new layout)
*/
additionalTicketAttributes?: string;
@@ -199,33 +88,64 @@ export interface Semantics {
duration?: number;
/**
* For newly-introduced event tickets
* in iOS 18
* @iOSVersion 18
* @passStyle eventTicket (new layout)
*/
entranceDescription?: string;
eventEndDate?: string;
/**
* For newly-introduced event tickets
* in iOS 18
* @iOSVersion 18
* @passStyle eventTicket (new layout)
*
* This seem to exists but it is not
* known yet what it does...
* Shows a message in the live activity
* when the activity starts.
*/
eventLiveMessage?: string;
eventName?: string;
eventStartDate?: string;
/**
* @iOSVersion 18
* @passStyle eventTicket (new layout).
*
* Can be used as an alternative way to
* show show start date, with more control
* on time and timeZone details and as
* a way to show the event guide, both
* instead of `eventStartDate`.
*/
eventStartDateInfo?: SemanticTagType.EventDateInfo;
/**
* @iOSVersion < 18
* Since iOS 18, for the event tickets these determine
* the template to be used when rendering the pass.
*
* - Generic Template
* - "PKEventTypeGeneric"
* - "PKEventTypeMovie"
* - "PKEventTypeConference"
* - "PKEventTypeConvention"
* - "PKEventTypeWorkshop"
* - "PKEventTypeSocialGathering"
* - Sport Template
* - "PKEventTypeSports"
* - Live Performance Template
* - "PKEventTypeLivePerformance";
*/
eventType?:
| "PKEventTypeGeneric"
| "PKEventTypeLivePerformance"
| "PKEventTypeMovie"
| "PKEventTypeSports"
| "PKEventTypeConference"
| "PKEventTypeConvention"
| "PKEventTypeWorkshop"
| "PKEventTypeSocialGathering";
| "PKEventTypeSocialGathering"
| "PKEventTypeSports"
| "PKEventTypeLivePerformance";
flightCode?: string;
flightNumber?: number;
@@ -250,8 +170,8 @@ export interface Semantics {
priorityStatus?: string;
/**
* For newly-introduced event tickets
* in iOS 18
* @iOSVersion 18
* @passStyle eventTicket (new layout)
*/
playlistIDs?: string[];
@@ -261,8 +181,8 @@ export interface Semantics {
sportName?: string;
/**
* For newly-introduced event tickets
* in iOS 18
* @iOSVersion 18
* @passStyle eventTicket (new layout)
*/
tailgatingAllowed?: boolean;
@@ -279,40 +199,46 @@ export interface Semantics {
venueLocation?: SemanticTagType.Location;
/**
* For newly-introduced event tickets
* in iOS 18
* @iOSVersion 18
* @passStyle eventTicket (new layout)
*/
venueGatesOpenDate?: string;
venueName?: string;
/**
* For newly-introduced event tickets
* in iOS 18
* @iOSVersion 18
* @passStyle eventTicket (new layout)
*/
venueParkingLotsOpenDate?: string;
/**
* For newly-introduced event tickets
* in iOS 18
* @iOSVersion 18
* @passStyle eventTicket (new layout)
*/
venueBoxOfficeOpenDate?: string;
/**
* For newly-introduced event tickets
* in iOS 18
* @iOSVersion 18
* @passStyle eventTicket (new layout)
*/
venueDoorsOpenDate?: string;
/**
* For newly-introduced event tickets
* in iOS 18
* @iOSVersion 18
* @passStyle eventTicket (new layout)
*/
venueFanZoneOpenDate?: string;
/**
* For newly-introduced event tickets
* in iOS 18
* @iOSVersion 18
* @passStyle eventTicket (new layout)
*/
venueOpenDate?: string;
/**
* @iOSVersion 18
* @passStyle eventTicket (new layout)
*/
venueCloseDate?: string;
@@ -320,26 +246,26 @@ export interface Semantics {
venueRoom?: string;
/**
* For newly-introduced event tickets
* in iOS 18
* @iOSVersion 18
* @passStyle eventTicket (new layout)
*/
venueRegionName?: string;
/**
* For newly-introduced event tickets
* in iOS 18
* @iOSVersion 18
* @passStyle eventTicket (new layout)
*/
venueEntranceGate?: string;
/**
* For newly-introduced event tickets
* in iOS 18
* @iOSVersion 18
* @passStyle eventTicket (new layout)
*/
venueEntranceDoor?: string;
/**
* For newly-introduced event tickets
* in iOS 18
* @iOSVersion 18
* @passStyle eventTicket (new layout)
*/
venueEntrancePortal?: string;
@@ -348,14 +274,14 @@ export interface Semantics {
export const Semantics = Joi.object<Semantics>().keys({
/**
* For newly-introduced event tickets
* in iOS 18
* @iOSVersion 18
* @passStyle eventTicket (new layout)
*/
admissionLevel: Joi.string(),
/**
* For newly-introduced event tickets
* in iOS 18
* @iOSVersion 18
* @passStyle eventTicket (new layout)
*/
admissionLevelAbbreviation: Joi.string(),
@@ -363,22 +289,22 @@ export const Semantics = Joi.object<Semantics>().keys({
artistIDs: Joi.array().items(Joi.string()),
/**
* For newly-introduced event tickets
* in iOS 18
* @iOSVersion 18
* @passStyle eventTicket (new layout)
*/
albumIDs: Joi.array().items(Joi.string()),
/**
* For newly-introduced event tickets
* in iOS 18
* @iOSVersion 18
* @passStyle eventTicket (new layout)
*/
airplay: Joi.array().items({
airplayDeviceGroupToken: Joi.string(),
}),
/**
* For newly-introduced event tickets
* in iOS 18
* @iOSVersion 18
* @passStyle eventTicket (new layout)
*/
attendeeName: Joi.string(),
@@ -388,7 +314,7 @@ export const Semantics = Joi.object<Semantics>().keys({
additionalTicketAttributes: Joi.string(),
balance: CurrencyAmount,
balance: SemanticTagType.CurrencyAmount,
boardingGroup: Joi.string(),
boardingSequenceNumber: Joi.string(),
@@ -401,7 +327,7 @@ export const Semantics = Joi.object<Semantics>().keys({
departureAirportCode: Joi.string(),
departureAirportName: Joi.string(),
departureGate: Joi.string(),
departureLocation: LocationSemantics,
departureLocation: SemanticTagType.Location,
departureLocationDescription: Joi.string(),
departurePlatform: Joi.string(),
departureStationName: Joi.string(),
@@ -409,7 +335,7 @@ export const Semantics = Joi.object<Semantics>().keys({
destinationAirportCode: Joi.string(),
destinationAirportName: Joi.string(),
destinationGate: Joi.string(),
destinationLocation: LocationSemantics,
destinationLocation: SemanticTagType.Location,
destinationLocationDescription: Joi.string(),
destinationPlatform: Joi.string(),
destinationStationName: Joi.string(),
@@ -417,8 +343,8 @@ export const Semantics = Joi.object<Semantics>().keys({
duration: Joi.number(),
/**
* For newly-introduced event tickets
* in iOS 18
* @iOSVersion 18
* @passStyle eventTicket (new layout)
*/
entranceDescription: Joi.string(),
@@ -426,14 +352,26 @@ export const Semantics = Joi.object<Semantics>().keys({
eventName: Joi.string(),
/**
* For newly-introduced event tickets
* in iOS 18
* @iOSVersion 18
* @passStyle eventTicket (new layout)
*
* This seem to exists but it is not
* known yet what it does...
* Shows a message in the live activity
* when the activity starts.
*/
eventLiveMessage: Joi.string(),
/**
* @iOSVersion 18
* @passStyle eventTicket (new layout).
*
* Can be used as an alternative way to
* show show start date, with more control
* on time and timeZone details and as
* a way to show the event guide, both
* instead of `eventStartDate`.
*/
eventStartDateInfo: SemanticTagType.EventDateInfo,
eventStartDate: Joi.string(),
eventType: Joi.string().regex(
/(PKEventTypeGeneric|PKEventTypeLivePerformance|PKEventTypeMovie|PKEventTypeSports|PKEventTypeConference|PKEventTypeConvention|PKEventTypeWorkshop|PKEventTypeSocialGathering)/,
@@ -457,20 +395,20 @@ export const Semantics = Joi.object<Semantics>().keys({
originalBoardingDate: Joi.string(),
originalDepartureDate: Joi.string(),
passengerName: PersonNameComponent,
passengerName: SemanticTagType.PersonNameComponents,
performerNames: Joi.array().items(Joi.string()),
priorityStatus: Joi.string(),
playlistIDs: Joi.array().items(Joi.string()),
seats: Joi.array().items(SeatSemantics),
seats: Joi.array().items(SemanticTagType.Seat),
securityScreening: Joi.string(),
silenceRequested: Joi.boolean(),
sportName: Joi.string(),
tailgatingAllowed: Joi.boolean(),
totalPrice: CurrencyAmount,
totalPrice: SemanticTagType.CurrencyAmount,
transitProvider: Joi.string(),
transitStatus: Joi.string(),
transitStatusReason: Joi.string(),
@@ -482,41 +420,47 @@ export const Semantics = Joi.object<Semantics>().keys({
venueEntrance: Joi.string(),
/**
* For newly-introduced event tickets
* in iOS 18
* @iOSVersion 18
* @passStyle eventTicket (new layout)
*/
venueGatesOpenDate: Joi.string(),
venueLocation: LocationSemantics,
venueLocation: SemanticTagType.Location,
venueName: Joi.string(),
/**
* For newly-introduced event tickets
* in iOS 18
* @iOSVersion 18
* @passStyle eventTicket (new layout)
*/
venueParkingLotsOpenDate: Joi.string(),
/**
* For newly-introduced event tickets
* in iOS 18
* @iOSVersion 18
* @passStyle eventTicket (new layout)
*/
venueBoxOfficeOpenDate: Joi.string(),
/**
* For newly-introduced event tickets
* in iOS 18
* @iOSVersion 18
* @passStyle eventTicket (new layout)
*/
venueDoorsOpenDate: Joi.string(),
/**
* For newly-introduced event tickets
* in iOS 18
* @iOSVersion 18
* @passStyle eventTicket (new layout)
*/
venueFanZoneOpenDate: Joi.string(),
/**
* For newly-introduced event tickets
* in iOS 18
* @iOSVersion 18
* @passStyle eventTicket (new layout)
*/
venueOpenDate: Joi.string(),
/**
* @iOSVersion 18
* @passStyle eventTicket (new layout)
*/
venueCloseDate: Joi.string(),
@@ -524,28 +468,28 @@ export const Semantics = Joi.object<Semantics>().keys({
venueRoom: Joi.string(),
/**
* For newly-introduced event tickets
* in iOS 18
* @iOSVersion 18
* @passStyle eventTicket (new layout)
*/
venueRegionName: Joi.string(),
/**
* For newly-introduced event tickets
* in iOS 18
* @iOSVersion 18
* @passStyle eventTicket (new layout)
*/
venueEntranceGate: Joi.string(),
/**
* For newly-introduced event tickets
* in iOS 18
* @iOSVersion 18
* @passStyle eventTicket (new layout)
*/
venueEntranceDoor: Joi.string(),
/**
* For newly-introduced event tickets
* in iOS 18
* @iOSVersion 18
* @passStyle eventTicket (new layout)
*/
venueEntrancePortal: Joi.string(),
wifiAccess: Joi.array().items(WifiNetwork),
wifiAccess: Joi.array().items(SemanticTagType.WifiNetwork),
});

View File

@@ -30,23 +30,47 @@ export const PreferredStyleSchemes = Joi.array().items(
) satisfies Joi.Schema<PreferredStyleSchemes>;
/**
* For newly-introduced event tickets
* in iOS 18
* A single interval can span at most 24 hours
*/
export interface RelevancyInterval {
startDate: string | Date;
endDate: string | Date;
}
interface RelevantDate {
startDate: string;
endDate: string;
export interface RelevancyEntry {
relevantDate: string | Date;
}
/**
* Minimum supported version: iOS 18
* @iOSVersion 18
*
* Using a RelevancyInterval, will trigger a live activity on
* new event ticket passes.
*
* Using a RelevancyEntry, will match the behavior of the
* currently deprecated property `relevantDate`.
*/
const RelevantDate = Joi.object<RelevantDate>().keys({
startDate: Joi.string().required(),
endDate: Joi.string().required(),
});
export type RelevantDate = RelevancyInterval | RelevancyEntry;
export const RelevantDate = Joi.alternatives(
Joi.object<RelevancyInterval>().keys({
startDate: Joi.alternatives(
Joi.string().isoDate(),
Joi.date().iso(),
).required(),
endDate: Joi.alternatives(
Joi.string().isoDate(),
Joi.date().iso(),
).required(),
}),
Joi.object<RelevancyEntry>().keys({
relevantDate: Joi.alternatives(
Joi.string().isoDate(),
Joi.date().iso(),
).required(),
}),
);
export interface FileBuffers {
[key: string]: Buffer;
@@ -80,6 +104,11 @@ export interface PassProps {
nfc?: NFC;
beacons?: Beacon[];
barcodes?: Barcode[];
/**
* @deprecated starting from iOS 18
* Use `relevantDates`
*/
relevantDate?: string;
relevantDates?: RelevantDate[];
@@ -94,142 +123,248 @@ export interface PassProps {
storeCard?: PassFields;
/**
* New field for iOS 18
* Event Ticket
* @iOSVersion 18
* @passStyle eventTicket (new layout)
*/
preferredStyleSchemes?: PreferredStyleSchemes;
/**
* New field for iOS 18 Event Ticket.
* @domain event guide
* @iOSVersion 18
* @passStyle eventTicket (new layout)
* @passDomain Event Guide
*
* To show buttons in the event guide,
* at least two among those marked with
* "@domain event guide" must be used.
* "@passDomain event guide" must be used.
*/
bagPolicyURL?: string;
/**
* New field for iOS 18 Event Ticket.
* @domain event guide
* @iOSVersion 18
* @passStyle eventTicket (new layout)
* @passDomain Event Guide
*
* To show buttons in the event guide,
* at least two among those marked with
* "@domain event guide" must be used.
* "@passDomain Event Guide" must be used.
*/
orderFoodURL?: string;
/**
* New field for iOS 18 Event Ticket.
* @domain event guide
* @iOSVersion 18
* @passStyle eventTicket (new layout)
* @passDomain Event Guide
*
* To show buttons in the event guide,
* at least two among those marked with
* "@domain event guide" must be used.
* "@passDomain Event Guide" must be used.
*/
parkingInformationURL?: string;
/**
* New field for iOS 18 Event Ticket.
* @domain event guide
* @iOSVersion 18
* @passStyle eventTicket (new layout)
* @passDomain Event Guide
*
* To show buttons in the event guide,
* at least two among those marked with
* "@domain event guide" must be used.
* "@passDomain Event Guide" must be used.
*/
directionsInformationURL?: string;
/**
* New field for iOS 18 Event Ticket.
* @domain event guide
* @iOSVersion 18
* @passStyle eventTicket (new layout)
* @passDomain Event Guide
*
* @description
*
* URL to a resource to buy or access
* the parking spot.
*
* To show buttons in the event guide,
* at least two among those marked with
* "@domain event guide" must be used.
*/
contactVenueEmail?: string;
/**
* New field for iOS 18 Event Ticket.
* @domain event guide
*
* To show buttons in the event guide,
* at least two among those marked with
* "@domain event guide" must be used.
*/
contactVenuePhoneNumber?: string;
/**
* New field for iOS 18 Event Ticket.
* @domain event guide
*
* To show buttons in the event guide,
* at least two among those marked with
* "@domain event guide" must be used.
*/
contactVenueWebsite?: string;
/**
* New field for iOS 18 Event Ticket.
* @domain event guide
*
* To show buttons in the event guide,
* at least two among those marked with
* "@domain event guide" must be used.
* "@passDomain Event Guide" must be used.
*/
purchaseParkingURL?: string;
/**
* New field for iOS 18 Event Ticket.
* @domain event guide
* @iOSVersion 18
* @passStyle eventTicket (new layout)
* @passDomain Event Guide
*
* @description
*
* URL to a resource to buy the
* merchandise.
*
* To show buttons in the event guide,
* at least two among those marked with
* "@domain event guide" must be used.
* "@passDomain Event Guide" must be used.
*/
merchandiseURL?: string;
/**
* New field for iOS 18 Event Ticket.
* @domain event guide
* @iOSVersion 18
* @passStyle eventTicket (new layout)
* @passDomain Event Guide
*
* @description
*
* URL to a resource about public or
* private transportation to reach the
* venue.
*
* To show buttons in the event guide,
* at least two among those marked with
* "@domain event guide" must be used.
* "@passDomain Event Guide" must be used.
*/
transitInformationURL?: string;
/**
* New field for iOS 18 Event Ticket.
* @domain event guide
* @iOSVersion 18
* @passStyle eventTicket (new layout)
* @passDomain Event Guide
*
* @description
*
* URL to a resource about accessibility
* in the events venue.
*
* To show buttons in the event guide,
* at least two among those marked with
* "@domain event guide" must be used.
* "@passDomain Event Guide" must be used.
*/
accessibilityURL?: string;
/**
* New field for iOS 18 Event Ticket.
* @domain event guide
* @iOSVersion 18
* @passStyle eventTicket (new layout)
* @passDomain Event Guide
*
* @description
*
* An URL to link experiences to the
* pass (upgrades and more).
*
* To show buttons in the event guide,
* at least two among those marked with
* "@domain event guide" must be used.
* "@passDomain Event Guide" must be used.
*/
addOnURL?: string;
/**
* New field for iOS 18 Event Ticket.
* @iOSVersion 18
* @passStyle eventTicket (new layout)
* @passDomain Event Guide
*
* To show buttons in the event guide,
* at least two among those marked with
* "@passDomain Event Guide" must be used.
*/
contactVenueEmail?: string;
/**
* @iOSVersion 18
* @passStyle eventTicket (new layout)
* @passDomain Event Guide
*
* To show buttons in the event guide,
* at least two among those marked with
* "@passDomain Event Guide" must be used.
*/
contactVenuePhoneNumber?: string;
/**
* @iOSVersion 18
* @passStyle eventTicket (new layout)
* @passDomain Event Guide
*
* To show buttons in the event guide,
* at least two among those marked with
* "@passDomain Event Guide" must be used.
*/
contactVenueWebsite?: string;
/**
* @iOSVersion 18
* @passStyle eventTicket (new layout)
* @passDomain Menu dropdown
*
* @description
*
* Will add a button among options near "share"
*/
transferURL?: string;
/**
* New field for iOS 18 Event Ticket.
* @iOSVersion 18
* @passStyle eventTicket (new layout)
* @passDomain Menu dropdown
*
* @description
*
* Will add a button among options near "share"
*/
sellURL?: string;
/**
* @iOSVersion 18
* @passStyle eventTicket (new layout)
*
* @description
*
* Will remove an automatic shadow in the new
* event ticket layouts.
*/
suppressHeaderDarkening?: boolean;
/**
* @iOSVersion 18
* @passStyle eventTicket (new layout)
*
* @description
*
* By default, the chin is colored with a
* blur. Through this option, it is possible
* to specify a different and specific color
* for it.
*/
footerBackgroundColor?: string;
/**
* @iOSVersion 18
* @passStyle eventTicket (new layout)
*
* @description
*
* Enables the automatic calculation of the
* `foregroundColor` and `labelColor` based
* on the background image in the new event
* ticket passes.
*
* If enabled, `foregroundColor` and `labelColor`
* are ignored.
*/
useAutomaticColor?: boolean;
/**
* @iOSVersion 18
* @passStyle eventTicket (new layout)
*
* @description
*
* Applications AppStore Identifiers
* related to the event ticket.
*
* It is not mandatory for the app to
* be related to the pass issuer.
*
* Such applications won't be able to read
* the passes users has (probably differently
* by `associatedStoreIdentifiers`).
*/
auxiliaryStoreIdentifiers?: number[];
}
/**
@@ -313,136 +448,246 @@ export const OverridablePassProps = Joi.object<OverridablePassProps>({
webServiceURL: Joi.string().regex(URL_REGEX),
/**
* New field for iOS 18 Event Ticket.
* @domain event guide
* @iOSVersion 18
* @passStyle eventTicket (new layout)
* @passDomain Event Guide
*
* To show buttons in the event guide,
* at least two among those marked with
* "@domain event guide" must be used.
* "@passDomain Event Guide" must be used.
*/
bagPolicyURL: Joi.string().regex(URL_REGEX),
/**
* New field for iOS 18 Event Ticket.
* @domain event guide
* @iOSVersion 18
* @passStyle eventTicket (new layout)
* @passDomain Event Guide
*
* To show buttons in the event guide,
* at least two among those marked with
* "@domain event guide" must be used.
* "@passDomain Event Guide" must be used.
*/
orderFoodURL: Joi.string().regex(URL_REGEX),
/**
* New field for iOS 18 Event Ticket.
* @domain event guide
* @iOSVersion 18
* @passStyle eventTicket (new layout)
* @passDomain Event Guide
*
* To show buttons in the event guide,
* at least two among those marked with
* "@domain event guide" must be used.
* "@passDomain Event Guide" must be used.
*/
parkingInformationURL: Joi.string().regex(URL_REGEX),
/**
* New field for iOS 18 Event Ticket.
* @domain event guide
* @iOSVersion 18
* @passStyle eventTicket (new layout)
* @passDomain Event Guide
*
* To show buttons in the event guide,
* at least two among those marked with
* "@domain event guide" must be used.
* "@passDomain Event Guide" must be used.
*/
directionsInformationURL: Joi.string(),
/**
* New field for iOS 18 Event Ticket.
* @domain event guide
* @iOSVersion 18
* @passStyle eventTicket (new layout)
* @passDomain Event Guide
*
* @description
*
* URL to a resource to buy or access
* the parking spot.
*
* To show buttons in the event guide,
* at least two among those marked with
* "@domain event guide" must be used.
*/
contactVenueEmail: Joi.string(),
/**
* New field for iOS 18 Event Ticket.
* @domain event guide
*
* To show buttons in the event guide,
* at least two among those marked with
* "@domain event guide" must be used.
*/
contactVenuePhoneNumber: Joi.string(),
/**
* New field for iOS 18 Event Ticket.
* @domain event guide
*
* To show buttons in the event guide,
* at least two among those marked with
* "@domain event guide" must be used.
*/
contactVenueWebsite: Joi.string(),
/**
* New field for iOS 18 Event Ticket.
* @domain event guide
*
* To show buttons in the event guide,
* at least two among those marked with
* "@domain event guide" must be used.
* "@passDomain Event Guide" must be used.
*/
purchaseParkingURL: Joi.string(),
/**
* New field for iOS 18 Event Ticket.
* @domain event guide
* @iOSVersion 18
* @passStyle eventTicket (new layout)
* @passDomain Event Guide
*
* @description
*
* URL to a resource to buy the
* merchandise.
*
* To show buttons in the event guide,
* at least two among those marked with
* "@domain event guide" must be used.
* "@passDomain Event Guide" must be used.
*/
merchandiseURL: Joi.string(),
/**
* New field for iOS 18 Event Ticket.
* @domain event guide
* @iOSVersion 18
* @passStyle eventTicket (new layout)
* @passDomain Event Guide
*
* @description
*
* URL to a resource about public or
* private transportation to reach the
* venue.
*
* To show buttons in the event guide,
* at least two among those marked with
* "@domain event guide" must be used.
* "@passDomain Event Guide" must be used.
*/
transitInformationURL: Joi.string(),
/**
* New field for iOS 18 Event Ticket.
* @domain event guide
* @iOSVersion 18
* @passStyle eventTicket (new layout)
* @passDomain Event Guide
*
* @description
*
* URL to a resource about accessibility
* in the events venue.
*
* To show buttons in the event guide,
* at least two among those marked with
* "@domain event guide" must be used.
* "@passDomain Event Guide" must be used.
*/
accessibilityURL: Joi.string(),
/**
* New field for iOS 18 Event Ticket.
* @domain event guide
* @iOSVersion 18
* @passStyle eventTicket (new layout)
* @passDomain Event Guide
*
* @description
*
* An URL to link experiences to the
* pass (upgrades and more).
*
* To show buttons in the event guide,
* at least two among those marked with
* "@domain event guide" must be used.
* "@passDomain Event Guide" must be used.
*/
addOnURL: Joi.string(),
/**
* New field for iOS 18 Event Ticket.
* @iOSVersion 18
* @passStyle eventTicket (new layout)
* @passDomain Event Guide
*
* @description
*
* To show buttons in the event guide,
* at least two among those marked with
* "@passDomain Event Guide" must be used.
*/
contactVenueEmail: Joi.string(),
/**
* @iOSVersion 18
* @passStyle eventTicket (new layout)
* @passDomain Event Guide
*
* @description
*
* To show buttons in the event guide,
* at least two among those marked with
* "@passDomain Event Guide" must be used.
*/
contactVenuePhoneNumber: Joi.string(),
/**
* @iOSVersion 18
* @passStyle eventTicket (new layout)
* @passDomain Event Guide
*
* @description
*
* To show buttons in the event guide,
* at least two among those marked with
* "@passDomain Event Guide" must be used.
*/
contactVenueWebsite: Joi.string(),
/**
* @iOSVersion 18
* @passStyle eventTicket (new layout)
*
* @description
*
* Will add a button among options near "share"
*/
transferURL: Joi.string(),
/**
* New field for iOS 18 Event Ticket.
* @iOSVersion 18
* @passStyle eventTicket (new layout)
*
* @description
*
* Will add a button among options near "share"
*/
sellURL: Joi.string(),
/**
* @iOSVersion 18
* @passStyle eventTicket (new layout)
*
* @description
*
* Will remove an automatic shadow in the new
* event ticket layouts.
*/
suppressHeaderDarkening: Joi.boolean(),
/**
* @iOSVersion 18
* @passStyle eventTicket (new layout)
*
* @description
*
* By default, the chin is colored with a
* blur. Through this option, it is possible
* to specify a different and specific color
* for it.
*/
footerBackgroundColor: Joi.string().regex(RGB_HEX_COLOR_REGEX),
/**
* @iOSVersion 18
* @passStyle eventTicket (new layout)
*
* @description
*
* Enables the automatic calculation of the
* `foregroundColor` and `labelColor` based
* on the background image in the new event
* ticket passes.
*
* If enabled, `foregroundColor` and `labelColor`
* are ignored.
*/
useAutomaticColor: Joi.boolean(),
/**
* @iOSVersion 18
* @passStyle eventTicket (new layout)
*
* @description
*
* Applications AppStore Identifiers
* related to the event ticket.
*
* It is not mandatory for the app to
* be related to the pass issuer.
*
* Such applications won't be able to read
* the passes users has (probably differently
* by `associatedStoreIdentifiers`).
*/
auxiliaryStoreIdentifiers: Joi.array().items(Joi.number()),
}).with("webServiceURL", "authenticationToken");
export const PassProps = Joi.object<
@@ -473,7 +718,7 @@ export const Template = Joi.object<Template>({
*/
export function assertValidity<T>(
schema: Joi.ObjectSchema<T> | Joi.StringSchema | Joi.Schema<T>,
schema: Joi.Schema<T>,
data: T,
customErrorMessage?: string,
): void {
@@ -505,7 +750,7 @@ export function assertValidity<T>(
*/
export function validate<T extends Object>(
schema: Joi.ObjectSchema<T> | Joi.StringSchema,
schema: Joi.Schema<T>,
options: T,
): T {
const validationResult = schema.validate(options, {