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" "node": ">=14.18.1"
}, },
"devDependencies": { "devDependencies": {
"@jest/globals": "^29.7.0",
"@types/do-not-zip": "^1.0.2", "@types/do-not-zip": "^1.0.2",
"@types/node": "^16.11.26", "@types/node": "^16.11.26",
"@types/node-forge": "^1.3.11", "@types/node-forge": "^1.3.11",

2688
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"; const EXAMPLE_PATH_RELATIVE = "../examples/models/examplePass.pass";
/**
* @param {string} folder
* @returns
*/
function unpackFolder(folder) { function unpackFolder(folder) {
const entryList = fs.readdirSync(path.resolve(__dirname, 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", () => { it("should set pass relevant date", () => {
pkpass.setRelevantDate(new Date("2023-04-11T00:15+10:00")); 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", () => { it("should throw if an invalid date is received", () => {
expect(() =>
// @ts-expect-error // @ts-expect-error
expect(() => pkpass.setRelevantDate("32/18/228317")).toThrowError(); pkpass.setRelevantDate("32/18/228317"),
).toThrowError();
// @ts-expect-error // @ts-expect-error
expect(() => pkpass.setRelevantDate(undefined)).toThrowError(); expect(() => pkpass.setRelevantDate(undefined)).toThrowError();
// @ts-expect-error // @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", () => { describe("barcodes", () => {
it("should create all barcode structures if a message is used", () => { it("should create all barcode structures if a message is used", () => {
pkpass.setBarcodes("a test barcode"); 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 * Allows setting a relevant date in which the OS
* should show this pass. * should show this pass.
@@ -964,6 +1012,13 @@ export default class PKPass extends Bundle {
* *
* @param {Date | null} date * @param {Date | null} date
* @throws if pass is frozen due to previous export * @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 { public setRelevantDate(date: Date | null): void {
@@ -1086,3 +1141,9 @@ function validateJSONBuffer(
return Schemas.validate(schema, contentAsJSON); 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.", "Cannot add field with key '%s': another field already owns this key. Ignored.",
} as const; } as const;
export const RELEVANT_DATE = {
INVALID: "Cannot set relevant date. Date format is invalid",
} as const;
export const DATE = { export const DATE = {
INVALID: "Cannot set %s. Invalid date %s", INVALID: "Cannot set %s. Invalid date %s",
} as const; } as const;

View File

@@ -19,6 +19,14 @@ export interface PassFields {
primaryFields: Field[]; primaryFields: Field[];
secondaryFields: Field[]; secondaryFields: Field[];
transitType?: TransitType; transitType?: TransitType;
/**
* @iOSVersion 18
* @passStyle eventTicket (new layout)
* @passDomain dashboard
*
* @see \<undiclosed>
*/
additionalInfoFields?: Field[]; additionalInfoFields?: Field[];
} }
@@ -29,5 +37,13 @@ export const PassFields = Joi.object<PassFields>().keys({
primaryFields: Joi.array().items(Field), primaryFields: Joi.array().items(Field),
secondaryFields: Joi.array().items(Field), secondaryFields: Joi.array().items(Field),
transitType: TransitType, transitType: TransitType,
/**
* @iOSVersion 18
* @passStyle eventTicket (new layout)
* @passDomain dashboard
*
* @see \<undiclosed>
*/
additionalInfoFields: Joi.array().items(Field), 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 Joi from "joi";
import { RGB_HEX_COLOR_REGEX } from "./regexps"; import * as SemanticTagType from "./SemanticTagType";
/** /**
* For a better description of every single field, * 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/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 * Alphabetical order
* @see https://developer.apple.com/documentation/walletpasses/semantictags * @see https://developer.apple.com/documentation/walletpasses/semantictags
@@ -126,14 +15,14 @@ const WifiNetwork = Joi.object<SemanticTagType.WifiNetwork>().keys({
export interface Semantics { export interface Semantics {
/** /**
* For newly-introduced event tickets * @iOSVersion 18
* in iOS 18 * @passStyle eventTicket (new layout)
*/ */
admissionLevel?: string; admissionLevel?: string;
/** /**
* For newly-introduced event tickets * @iOSVersion 18
* in iOS 18 * @passStyle eventTicket (new layout)
*/ */
admissionLevelAbbreviation?: string; admissionLevelAbbreviation?: string;
@@ -141,22 +30,22 @@ export interface Semantics {
artistIDs?: string[]; artistIDs?: string[];
/** /**
* For newly-introduced event tickets * @iOSVersion 18
* in iOS 18 * @passStyle eventTicket (new layout)
*/ */
albumIDs?: string[]; albumIDs?: string[];
/** /**
* For newly-introduced event tickets * @iOSVersion 18
* in iOS 18 * @passStyle eventTicket (new layout)
*/ */
airplay?: { airplay?: {
airPlayDeviceGroupToken: string; airPlayDeviceGroupToken: string;
}[]; }[];
/** /**
* For newly-introduced event tickets * @iOSVersion 18
* in iOS 18 * @passStyle eventTicket (new layout)
*/ */
attendeeName?: string; attendeeName?: string;
@@ -165,8 +54,8 @@ export interface Semantics {
awayTeamName?: string; awayTeamName?: string;
/** /**
* For newly-introduced event tickets * @iOSVersion 18
* in iOS 18 * @passStyle eventTicket (new layout)
*/ */
additionalTicketAttributes?: string; additionalTicketAttributes?: string;
@@ -199,33 +88,64 @@ export interface Semantics {
duration?: number; duration?: number;
/** /**
* For newly-introduced event tickets * @iOSVersion 18
* in iOS 18 * @passStyle eventTicket (new layout)
*/ */
entranceDescription?: string; entranceDescription?: string;
eventEndDate?: string; eventEndDate?: string;
/** /**
* For newly-introduced event tickets * @iOSVersion 18
* in iOS 18 * @passStyle eventTicket (new layout)
* *
* This seem to exists but it is not * Shows a message in the live activity
* known yet what it does... * when the activity starts.
*/ */
eventLiveMessage?: string; eventLiveMessage?: string;
eventName?: string; eventName?: string;
eventStartDate?: 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?: eventType?:
| "PKEventTypeGeneric" | "PKEventTypeGeneric"
| "PKEventTypeLivePerformance"
| "PKEventTypeMovie" | "PKEventTypeMovie"
| "PKEventTypeSports"
| "PKEventTypeConference" | "PKEventTypeConference"
| "PKEventTypeConvention" | "PKEventTypeConvention"
| "PKEventTypeWorkshop" | "PKEventTypeWorkshop"
| "PKEventTypeSocialGathering"; | "PKEventTypeSocialGathering"
| "PKEventTypeSports"
| "PKEventTypeLivePerformance";
flightCode?: string; flightCode?: string;
flightNumber?: number; flightNumber?: number;
@@ -250,8 +170,8 @@ export interface Semantics {
priorityStatus?: string; priorityStatus?: string;
/** /**
* For newly-introduced event tickets * @iOSVersion 18
* in iOS 18 * @passStyle eventTicket (new layout)
*/ */
playlistIDs?: string[]; playlistIDs?: string[];
@@ -261,8 +181,8 @@ export interface Semantics {
sportName?: string; sportName?: string;
/** /**
* For newly-introduced event tickets * @iOSVersion 18
* in iOS 18 * @passStyle eventTicket (new layout)
*/ */
tailgatingAllowed?: boolean; tailgatingAllowed?: boolean;
@@ -279,40 +199,46 @@ export interface Semantics {
venueLocation?: SemanticTagType.Location; venueLocation?: SemanticTagType.Location;
/** /**
* For newly-introduced event tickets * @iOSVersion 18
* in iOS 18 * @passStyle eventTicket (new layout)
*/ */
venueGatesOpenDate?: string; venueGatesOpenDate?: string;
venueName?: string; venueName?: string;
/** /**
* For newly-introduced event tickets * @iOSVersion 18
* in iOS 18 * @passStyle eventTicket (new layout)
*/ */
venueParkingLotsOpenDate?: string; venueParkingLotsOpenDate?: string;
/** /**
* For newly-introduced event tickets * @iOSVersion 18
* in iOS 18 * @passStyle eventTicket (new layout)
*/ */
venueBoxOfficeOpenDate?: string; venueBoxOfficeOpenDate?: string;
/** /**
* For newly-introduced event tickets * @iOSVersion 18
* in iOS 18 * @passStyle eventTicket (new layout)
*/ */
venueDoorsOpenDate?: string; venueDoorsOpenDate?: string;
/** /**
* For newly-introduced event tickets * @iOSVersion 18
* in iOS 18 * @passStyle eventTicket (new layout)
*/ */
venueFanZoneOpenDate?: string; venueFanZoneOpenDate?: string;
/** /**
* For newly-introduced event tickets * @iOSVersion 18
* in iOS 18 * @passStyle eventTicket (new layout)
*/
venueOpenDate?: string;
/**
* @iOSVersion 18
* @passStyle eventTicket (new layout)
*/ */
venueCloseDate?: string; venueCloseDate?: string;
@@ -320,26 +246,26 @@ export interface Semantics {
venueRoom?: string; venueRoom?: string;
/** /**
* For newly-introduced event tickets * @iOSVersion 18
* in iOS 18 * @passStyle eventTicket (new layout)
*/ */
venueRegionName?: string; venueRegionName?: string;
/** /**
* For newly-introduced event tickets * @iOSVersion 18
* in iOS 18 * @passStyle eventTicket (new layout)
*/ */
venueEntranceGate?: string; venueEntranceGate?: string;
/** /**
* For newly-introduced event tickets * @iOSVersion 18
* in iOS 18 * @passStyle eventTicket (new layout)
*/ */
venueEntranceDoor?: string; venueEntranceDoor?: string;
/** /**
* For newly-introduced event tickets * @iOSVersion 18
* in iOS 18 * @passStyle eventTicket (new layout)
*/ */
venueEntrancePortal?: string; venueEntrancePortal?: string;
@@ -348,14 +274,14 @@ export interface Semantics {
export const Semantics = Joi.object<Semantics>().keys({ export const Semantics = Joi.object<Semantics>().keys({
/** /**
* For newly-introduced event tickets * @iOSVersion 18
* in iOS 18 * @passStyle eventTicket (new layout)
*/ */
admissionLevel: Joi.string(), admissionLevel: Joi.string(),
/** /**
* For newly-introduced event tickets * @iOSVersion 18
* in iOS 18 * @passStyle eventTicket (new layout)
*/ */
admissionLevelAbbreviation: Joi.string(), admissionLevelAbbreviation: Joi.string(),
@@ -363,22 +289,22 @@ export const Semantics = Joi.object<Semantics>().keys({
artistIDs: Joi.array().items(Joi.string()), artistIDs: Joi.array().items(Joi.string()),
/** /**
* For newly-introduced event tickets * @iOSVersion 18
* in iOS 18 * @passStyle eventTicket (new layout)
*/ */
albumIDs: Joi.array().items(Joi.string()), albumIDs: Joi.array().items(Joi.string()),
/** /**
* For newly-introduced event tickets * @iOSVersion 18
* in iOS 18 * @passStyle eventTicket (new layout)
*/ */
airplay: Joi.array().items({ airplay: Joi.array().items({
airplayDeviceGroupToken: Joi.string(), airplayDeviceGroupToken: Joi.string(),
}), }),
/** /**
* For newly-introduced event tickets * @iOSVersion 18
* in iOS 18 * @passStyle eventTicket (new layout)
*/ */
attendeeName: Joi.string(), attendeeName: Joi.string(),
@@ -388,7 +314,7 @@ export const Semantics = Joi.object<Semantics>().keys({
additionalTicketAttributes: Joi.string(), additionalTicketAttributes: Joi.string(),
balance: CurrencyAmount, balance: SemanticTagType.CurrencyAmount,
boardingGroup: Joi.string(), boardingGroup: Joi.string(),
boardingSequenceNumber: Joi.string(), boardingSequenceNumber: Joi.string(),
@@ -401,7 +327,7 @@ export const Semantics = Joi.object<Semantics>().keys({
departureAirportCode: Joi.string(), departureAirportCode: Joi.string(),
departureAirportName: Joi.string(), departureAirportName: Joi.string(),
departureGate: Joi.string(), departureGate: Joi.string(),
departureLocation: LocationSemantics, departureLocation: SemanticTagType.Location,
departureLocationDescription: Joi.string(), departureLocationDescription: Joi.string(),
departurePlatform: Joi.string(), departurePlatform: Joi.string(),
departureStationName: Joi.string(), departureStationName: Joi.string(),
@@ -409,7 +335,7 @@ export const Semantics = Joi.object<Semantics>().keys({
destinationAirportCode: Joi.string(), destinationAirportCode: Joi.string(),
destinationAirportName: Joi.string(), destinationAirportName: Joi.string(),
destinationGate: Joi.string(), destinationGate: Joi.string(),
destinationLocation: LocationSemantics, destinationLocation: SemanticTagType.Location,
destinationLocationDescription: Joi.string(), destinationLocationDescription: Joi.string(),
destinationPlatform: Joi.string(), destinationPlatform: Joi.string(),
destinationStationName: Joi.string(), destinationStationName: Joi.string(),
@@ -417,8 +343,8 @@ export const Semantics = Joi.object<Semantics>().keys({
duration: Joi.number(), duration: Joi.number(),
/** /**
* For newly-introduced event tickets * @iOSVersion 18
* in iOS 18 * @passStyle eventTicket (new layout)
*/ */
entranceDescription: Joi.string(), entranceDescription: Joi.string(),
@@ -426,14 +352,26 @@ export const Semantics = Joi.object<Semantics>().keys({
eventName: Joi.string(), eventName: Joi.string(),
/** /**
* For newly-introduced event tickets * @iOSVersion 18
* in iOS 18 * @passStyle eventTicket (new layout)
* *
* This seem to exists but it is not * Shows a message in the live activity
* known yet what it does... * when the activity starts.
*/ */
eventLiveMessage: Joi.string(), 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(), eventStartDate: Joi.string(),
eventType: Joi.string().regex( eventType: Joi.string().regex(
/(PKEventTypeGeneric|PKEventTypeLivePerformance|PKEventTypeMovie|PKEventTypeSports|PKEventTypeConference|PKEventTypeConvention|PKEventTypeWorkshop|PKEventTypeSocialGathering)/, /(PKEventTypeGeneric|PKEventTypeLivePerformance|PKEventTypeMovie|PKEventTypeSports|PKEventTypeConference|PKEventTypeConvention|PKEventTypeWorkshop|PKEventTypeSocialGathering)/,
@@ -457,20 +395,20 @@ export const Semantics = Joi.object<Semantics>().keys({
originalBoardingDate: Joi.string(), originalBoardingDate: Joi.string(),
originalDepartureDate: Joi.string(), originalDepartureDate: Joi.string(),
passengerName: PersonNameComponent, passengerName: SemanticTagType.PersonNameComponents,
performerNames: Joi.array().items(Joi.string()), performerNames: Joi.array().items(Joi.string()),
priorityStatus: Joi.string(), priorityStatus: Joi.string(),
playlistIDs: Joi.array().items(Joi.string()), playlistIDs: Joi.array().items(Joi.string()),
seats: Joi.array().items(SeatSemantics), seats: Joi.array().items(SemanticTagType.Seat),
securityScreening: Joi.string(), securityScreening: Joi.string(),
silenceRequested: Joi.boolean(), silenceRequested: Joi.boolean(),
sportName: Joi.string(), sportName: Joi.string(),
tailgatingAllowed: Joi.boolean(), tailgatingAllowed: Joi.boolean(),
totalPrice: CurrencyAmount, totalPrice: SemanticTagType.CurrencyAmount,
transitProvider: Joi.string(), transitProvider: Joi.string(),
transitStatus: Joi.string(), transitStatus: Joi.string(),
transitStatusReason: Joi.string(), transitStatusReason: Joi.string(),
@@ -482,41 +420,47 @@ export const Semantics = Joi.object<Semantics>().keys({
venueEntrance: Joi.string(), venueEntrance: Joi.string(),
/** /**
* For newly-introduced event tickets * @iOSVersion 18
* in iOS 18 * @passStyle eventTicket (new layout)
*/ */
venueGatesOpenDate: Joi.string(), venueGatesOpenDate: Joi.string(),
venueLocation: LocationSemantics, venueLocation: SemanticTagType.Location,
venueName: Joi.string(), venueName: Joi.string(),
/** /**
* For newly-introduced event tickets * @iOSVersion 18
* in iOS 18 * @passStyle eventTicket (new layout)
*/ */
venueParkingLotsOpenDate: Joi.string(), venueParkingLotsOpenDate: Joi.string(),
/** /**
* For newly-introduced event tickets * @iOSVersion 18
* in iOS 18 * @passStyle eventTicket (new layout)
*/ */
venueBoxOfficeOpenDate: Joi.string(), venueBoxOfficeOpenDate: Joi.string(),
/** /**
* For newly-introduced event tickets * @iOSVersion 18
* in iOS 18 * @passStyle eventTicket (new layout)
*/ */
venueDoorsOpenDate: Joi.string(), venueDoorsOpenDate: Joi.string(),
/** /**
* For newly-introduced event tickets * @iOSVersion 18
* in iOS 18 * @passStyle eventTicket (new layout)
*/ */
venueFanZoneOpenDate: Joi.string(), venueFanZoneOpenDate: Joi.string(),
/** /**
* For newly-introduced event tickets * @iOSVersion 18
* in iOS 18 * @passStyle eventTicket (new layout)
*/
venueOpenDate: Joi.string(),
/**
* @iOSVersion 18
* @passStyle eventTicket (new layout)
*/ */
venueCloseDate: Joi.string(), venueCloseDate: Joi.string(),
@@ -524,28 +468,28 @@ export const Semantics = Joi.object<Semantics>().keys({
venueRoom: Joi.string(), venueRoom: Joi.string(),
/** /**
* For newly-introduced event tickets * @iOSVersion 18
* in iOS 18 * @passStyle eventTicket (new layout)
*/ */
venueRegionName: Joi.string(), venueRegionName: Joi.string(),
/** /**
* For newly-introduced event tickets * @iOSVersion 18
* in iOS 18 * @passStyle eventTicket (new layout)
*/ */
venueEntranceGate: Joi.string(), venueEntranceGate: Joi.string(),
/** /**
* For newly-introduced event tickets * @iOSVersion 18
* in iOS 18 * @passStyle eventTicket (new layout)
*/ */
venueEntranceDoor: Joi.string(), venueEntranceDoor: Joi.string(),
/** /**
* For newly-introduced event tickets * @iOSVersion 18
* in iOS 18 * @passStyle eventTicket (new layout)
*/ */
venueEntrancePortal: Joi.string(), 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>; ) satisfies Joi.Schema<PreferredStyleSchemes>;
/** /**
* For newly-introduced event tickets * A single interval can span at most 24 hours
* in iOS 18
*/ */
export interface RelevancyInterval {
startDate: string | Date;
endDate: string | Date;
}
interface RelevantDate { export interface RelevancyEntry {
startDate: string; relevantDate: string | Date;
endDate: string;
} }
/** /**
* 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({ export type RelevantDate = RelevancyInterval | RelevancyEntry;
startDate: Joi.string().required(),
endDate: Joi.string().required(), 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 { export interface FileBuffers {
[key: string]: Buffer; [key: string]: Buffer;
@@ -80,6 +104,11 @@ export interface PassProps {
nfc?: NFC; nfc?: NFC;
beacons?: Beacon[]; beacons?: Beacon[];
barcodes?: Barcode[]; barcodes?: Barcode[];
/**
* @deprecated starting from iOS 18
* Use `relevantDates`
*/
relevantDate?: string; relevantDate?: string;
relevantDates?: RelevantDate[]; relevantDates?: RelevantDate[];
@@ -94,142 +123,248 @@ export interface PassProps {
storeCard?: PassFields; storeCard?: PassFields;
/** /**
* New field for iOS 18 * @iOSVersion 18
* Event Ticket * @passStyle eventTicket (new layout)
*/ */
preferredStyleSchemes?: PreferredStyleSchemes; preferredStyleSchemes?: PreferredStyleSchemes;
/** /**
* New field for iOS 18 Event Ticket. * @iOSVersion 18
* @domain event guide * @passStyle eventTicket (new layout)
* @passDomain Event Guide
* *
* To show buttons in the event guide, * To show buttons in the event guide,
* at least two among those marked with * at least two among those marked with
* "@domain event guide" must be used. * "@passDomain event guide" must be used.
*/ */
bagPolicyURL?: string; bagPolicyURL?: string;
/** /**
* New field for iOS 18 Event Ticket. * @iOSVersion 18
* @domain event guide * @passStyle eventTicket (new layout)
* @passDomain Event Guide
* *
* To show buttons in the event guide, * To show buttons in the event guide,
* at least two among those marked with * at least two among those marked with
* "@domain event guide" must be used. * "@passDomain Event Guide" must be used.
*/ */
orderFoodURL?: string; orderFoodURL?: string;
/** /**
* New field for iOS 18 Event Ticket. * @iOSVersion 18
* @domain event guide * @passStyle eventTicket (new layout)
* @passDomain Event Guide
* *
* To show buttons in the event guide, * To show buttons in the event guide,
* at least two among those marked with * at least two among those marked with
* "@domain event guide" must be used. * "@passDomain Event Guide" must be used.
*/ */
parkingInformationURL?: string; parkingInformationURL?: string;
/** /**
* New field for iOS 18 Event Ticket. * @iOSVersion 18
* @domain event guide * @passStyle eventTicket (new layout)
* @passDomain Event Guide
* *
* To show buttons in the event guide, * To show buttons in the event guide,
* at least two among those marked with * at least two among those marked with
* "@domain event guide" must be used. * "@passDomain Event Guide" must be used.
*/ */
directionsInformationURL?: string; directionsInformationURL?: string;
/** /**
* New field for iOS 18 Event Ticket. * @iOSVersion 18
* @domain event guide * @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, * To show buttons in the event guide,
* at least two among those marked with * at least two among those marked with
* "@domain event guide" must be used. * "@passDomain 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.
*/ */
purchaseParkingURL?: string; purchaseParkingURL?: string;
/** /**
* New field for iOS 18 Event Ticket. * @iOSVersion 18
* @domain event guide * @passStyle eventTicket (new layout)
* @passDomain Event Guide
*
* @description
*
* URL to a resource to buy the
* merchandise.
* *
* To show buttons in the event guide, * To show buttons in the event guide,
* at least two among those marked with * at least two among those marked with
* "@domain event guide" must be used. * "@passDomain Event Guide" must be used.
*/ */
merchandiseURL?: string; merchandiseURL?: string;
/** /**
* New field for iOS 18 Event Ticket. * @iOSVersion 18
* @domain event guide * @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, * To show buttons in the event guide,
* at least two among those marked with * at least two among those marked with
* "@domain event guide" must be used. * "@passDomain Event Guide" must be used.
*/ */
transitInformationURL?: string; transitInformationURL?: string;
/** /**
* New field for iOS 18 Event Ticket. * @iOSVersion 18
* @domain event guide * @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, * To show buttons in the event guide,
* at least two among those marked with * at least two among those marked with
* "@domain event guide" must be used. * "@passDomain Event Guide" must be used.
*/ */
accessibilityURL?: string; accessibilityURL?: string;
/** /**
* New field for iOS 18 Event Ticket. * @iOSVersion 18
* @domain event guide * @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, * To show buttons in the event guide,
* at least two among those marked with * at least two among those marked with
* "@domain event guide" must be used. * "@passDomain Event Guide" must be used.
*/ */
addOnURL?: string; 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" * Will add a button among options near "share"
*/ */
transferURL?: string; 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" * Will add a button among options near "share"
*/ */
sellURL?: string; 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), webServiceURL: Joi.string().regex(URL_REGEX),
/** /**
* New field for iOS 18 Event Ticket. * @iOSVersion 18
* @domain event guide * @passStyle eventTicket (new layout)
* @passDomain Event Guide
* *
* To show buttons in the event guide, * To show buttons in the event guide,
* at least two among those marked with * 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), bagPolicyURL: Joi.string().regex(URL_REGEX),
/** /**
* New field for iOS 18 Event Ticket. * @iOSVersion 18
* @domain event guide * @passStyle eventTicket (new layout)
* @passDomain Event Guide
* *
* To show buttons in the event guide, * To show buttons in the event guide,
* at least two among those marked with * 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), orderFoodURL: Joi.string().regex(URL_REGEX),
/** /**
* New field for iOS 18 Event Ticket. * @iOSVersion 18
* @domain event guide * @passStyle eventTicket (new layout)
* @passDomain Event Guide
* *
* To show buttons in the event guide, * To show buttons in the event guide,
* at least two among those marked with * 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), parkingInformationURL: Joi.string().regex(URL_REGEX),
/** /**
* New field for iOS 18 Event Ticket. * @iOSVersion 18
* @domain event guide * @passStyle eventTicket (new layout)
* @passDomain Event Guide
* *
* To show buttons in the event guide, * To show buttons in the event guide,
* at least two among those marked with * at least two among those marked with
* "@domain event guide" must be used. * "@passDomain Event Guide" must be used.
*/ */
directionsInformationURL: Joi.string(), directionsInformationURL: Joi.string(),
/** /**
* New field for iOS 18 Event Ticket. * @iOSVersion 18
* @domain event guide * @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, * To show buttons in the event guide,
* at least two among those marked with * at least two among those marked with
* "@domain event guide" must be used. * "@passDomain 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.
*/ */
purchaseParkingURL: Joi.string(), purchaseParkingURL: Joi.string(),
/** /**
* New field for iOS 18 Event Ticket. * @iOSVersion 18
* @domain event guide * @passStyle eventTicket (new layout)
* @passDomain Event Guide
*
* @description
*
* URL to a resource to buy the
* merchandise.
* *
* To show buttons in the event guide, * To show buttons in the event guide,
* at least two among those marked with * at least two among those marked with
* "@domain event guide" must be used. * "@passDomain Event Guide" must be used.
*/ */
merchandiseURL: Joi.string(), merchandiseURL: Joi.string(),
/** /**
* New field for iOS 18 Event Ticket. * @iOSVersion 18
* @domain event guide * @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, * To show buttons in the event guide,
* at least two among those marked with * at least two among those marked with
* "@domain event guide" must be used. * "@passDomain Event Guide" must be used.
*/ */
transitInformationURL: Joi.string(), transitInformationURL: Joi.string(),
/** /**
* New field for iOS 18 Event Ticket. * @iOSVersion 18
* @domain event guide * @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, * To show buttons in the event guide,
* at least two among those marked with * at least two among those marked with
* "@domain event guide" must be used. * "@passDomain Event Guide" must be used.
*/ */
accessibilityURL: Joi.string(), accessibilityURL: Joi.string(),
/** /**
* New field for iOS 18 Event Ticket. * @iOSVersion 18
* @domain event guide * @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, * To show buttons in the event guide,
* at least two among those marked with * at least two among those marked with
* "@domain event guide" must be used. * "@passDomain Event Guide" must be used.
*/ */
addOnURL: Joi.string(), 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" * Will add a button among options near "share"
*/ */
transferURL: Joi.string(), 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" * Will add a button among options near "share"
*/ */
sellURL: Joi.string(), 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"); }).with("webServiceURL", "authenticationToken");
export const PassProps = Joi.object< export const PassProps = Joi.object<
@@ -473,7 +718,7 @@ export const Template = Joi.object<Template>({
*/ */
export function assertValidity<T>( export function assertValidity<T>(
schema: Joi.ObjectSchema<T> | Joi.StringSchema | Joi.Schema<T>, schema: Joi.Schema<T>,
data: T, data: T,
customErrorMessage?: string, customErrorMessage?: string,
): void { ): void {
@@ -505,7 +750,7 @@ export function assertValidity<T>(
*/ */
export function validate<T extends Object>( export function validate<T extends Object>(
schema: Joi.ObjectSchema<T> | Joi.StringSchema, schema: Joi.Schema<T>,
options: T, options: T,
): T { ): T {
const validationResult = schema.validate(options, { const validationResult = schema.validate(options, {