@walletpass/pass-js
Version:
Apple Wallet Pass generating and pushing updates from Node.js
972 lines • 32.3 kB
JavaScript
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2017-2026 Konstantin Vyatkin <tino@vtkn.io>
import { BARCODES_FORMAT, STRUCTURE_FIELDS } from '../constants.js';
import { PassColor } from './pass-color.js';
import { PassImages } from './images.js';
import { Localizations } from './localizations.js';
import { getGeoPoint } from './get-geo-point.js';
import { PassStructure } from './pass-structure.js';
import { normalizeSemanticTags } from './semantic-tags.js';
import { isValidW3CDateString, normalizeDatesDeep } from './w3cdate.js';
import { validateUpcomingPassInformationEntries, } from './upcoming-pass-information.js';
import { validatePersonalization, } from './personalization.js';
const STRUCTURE_FIELDS_SET = new Set([...STRUCTURE_FIELDS, 'nfc']);
export class PassBase extends PassStructure {
images;
localization;
options;
personalizationData = undefined;
constructor(fields = {}, images, localizations, options, personalization) {
super(fields);
this.options = options;
// restore via setters
for (const [key, value] of Object.entries(fields)) {
if (!STRUCTURE_FIELDS_SET.has(key) && key in this) {
this[key] = value;
}
}
// copy images
this.images = new PassImages(images);
// copy localizations
this.localization = new Localizations(localizations);
this.personalization = personalization;
}
// Returns the pass.json object (not a string). Any Date anywhere in the
// plain-object tree — top-level field, inside a RelevantDateEntry,
// inside a future nested schema — is converted to the library's W3C
// date string format (YYYY-MM-DDTHH:MM±HH:MM), never the JS default
// ISO 8601 with milliseconds and trailing `Z` that Date.prototype.toJSON
// would emit during JSON.stringify.
//
// Class instances (PassColor, FieldsMap, NFCField, …) have their own
// toJSON and pass through unchanged — the walker only descends into
// plain objects and arrays.
toJSON() {
return {
formatVersion: 1,
...normalizeDatesDeep(this.fields),
};
}
get passTypeIdentifier() {
return this.fields.passTypeIdentifier;
}
set passTypeIdentifier(v) {
if (!v)
delete this.fields.passTypeIdentifier;
else
this.fields.passTypeIdentifier = v;
}
get teamIdentifier() {
return this.fields.teamIdentifier;
}
set teamIdentifier(v) {
if (!v)
delete this.fields.teamIdentifier;
else
this.fields.teamIdentifier = v;
}
get serialNumber() {
return this.fields.serialNumber;
}
set serialNumber(v) {
if (!v)
delete this.fields.serialNumber;
else
this.fields.serialNumber = v;
}
/**
* Indicates that the sharing of pass can be prohibited.
*
* @type {boolean}
*/
get sharingProhibited() {
return this.fields.sharingProhibited;
}
set sharingProhibited(v) {
if (!v)
delete this.fields.sharingProhibited;
else
this.fields.sharingProhibited = true;
}
/**
* Indicates that the pass is void—for example, a one time use coupon that has been redeemed.
*
* @type {boolean}
*/
get voided() {
return !!this.fields.voided;
}
set voided(v) {
if (v)
this.fields.voided = true;
else
delete this.fields.voided;
}
/**
* Date and time when the pass expires.
*
*/
get expirationDate() {
if (typeof this.fields.expirationDate === 'string')
return new Date(this.fields.expirationDate);
return this.fields.expirationDate;
}
set expirationDate(v) {
if (!v)
delete this.fields.expirationDate;
else {
if (v instanceof Date) {
if (!Number.isFinite(v.getTime()))
throw new TypeError(`Value for expirationDate must be a valid Date, received ${v.toString()}`);
this.fields.expirationDate = v;
}
else if (typeof v === 'string') {
if (isValidW3CDateString(v))
this.fields.expirationDate = v;
else {
const date = new Date(v);
if (!Number.isFinite(date.getTime()))
throw new TypeError(`Value for expirationDate must be a valid Date, received ${v}`);
this.fields.expirationDate = date;
}
}
}
}
/**
* Date and time when the pass becomes relevant. For example, the start time of a movie.
* Recommended for event tickets and boarding passes; otherwise optional.
*
* @type {string | Date}
*/
get relevantDate() {
if (typeof this.fields.relevantDate === 'string')
return new Date(this.fields.relevantDate);
return this.fields.relevantDate;
}
set relevantDate(v) {
if (!v)
delete this.fields.relevantDate;
else {
if (v instanceof Date) {
if (!Number.isFinite(v.getTime()))
throw new TypeError(`Value for relevantDate must be a valid Date, received ${v.toString()}`);
this.fields.relevantDate = v;
}
else if (typeof v === 'string') {
if (isValidW3CDateString(v))
this.fields.relevantDate = v;
else {
const date = new Date(v);
if (!Number.isFinite(date.getTime()))
throw new TypeError(`Value for relevantDate must be a valid Date, received ${v}`);
this.fields.relevantDate = date;
}
}
}
}
/**
* List of dates and date ranges during which the pass is relevant
* (iOS 18+). Supersedes `relevantDate` for multi-window passes.
*/
get relevantDates() {
return this.fields.relevantDates;
}
set relevantDates(v) {
if (!v)
delete this.fields.relevantDates;
else
this.fields.relevantDates = v;
}
/**
* Calendar event associated with the pass. `startDate` / `endDate`
* accept either a `Date` or a W3C-formatted string; `Date` values are
* serialized to the W3C format by `normalizeDatesDeep` at `toJSON`
* time.
*/
get calendarEvent() {
return this.fields.calendarEvent;
}
set calendarEvent(v) {
if (!v) {
delete this.fields.calendarEvent;
return;
}
if (typeof v.title !== 'string' || v.title.length === 0)
throw new TypeError(`calendarEvent.title must be a non-empty string, received ${typeof v.title}`);
for (const key of ['startDate', 'endDate']) {
const value = v[key];
if (value instanceof Date) {
if (!Number.isFinite(value.getTime()))
throw new TypeError(`calendarEvent.${key} must be a valid Date, received ${value.toString()}`);
}
else if (typeof value === 'string') {
if (!isValidW3CDateString(value))
throw new TypeError(`calendarEvent.${key} must be a valid W3C date string, received ${value}`);
}
else {
throw new TypeError(`calendarEvent.${key} must be a Date or W3C date string, received ${typeof value}`);
}
}
this.fields.calendarEvent = v;
}
/**
* Ordered list of visual style schemes the pass opts into (iOS 18+).
*/
get preferredStyleSchemes() {
return this.fields.preferredStyleSchemes;
}
set preferredStyleSchemes(v) {
if (!v || v.length === 0)
delete this.fields.preferredStyleSchemes;
else
this.fields.preferredStyleSchemes = v;
}
/**
* A list of iTunes Store item identifiers for the associated apps.
* Only one item in the list is used—the first item identifier for an app
* compatible with the current device.
* If the app is not installed, the link opens the App Store and shows the app.
* If the app is already installed, the link launches the app.
*/
get associatedStoreIdentifiers() {
return this.fields.associatedStoreIdentifiers;
}
set associatedStoreIdentifiers(v) {
if (!v) {
delete this.fields.associatedStoreIdentifiers;
return;
}
const arrayOfNumbers = v.filter(n => Number.isInteger(n));
if (arrayOfNumbers.length > 0)
this.fields.associatedStoreIdentifiers = arrayOfNumbers;
else
delete this.fields.associatedStoreIdentifiers;
}
/**
* A URL the system passes to the associated app from
* `associatedStoreIdentifiers` when the pass is opened.
*/
get appLaunchURL() {
return this.fields.appLaunchURL;
}
set appLaunchURL(v) {
if (!v)
delete this.fields.appLaunchURL;
else
this.fields.appLaunchURL = v;
}
/**
* Brief description of the pass, used by the iOS accessibility technologies.
* Don’t try to include all of the data on the pass in its description,
* just include enough detail to distinguish passes of the same type.
*/
get description() {
return this.fields.description;
}
set description(v) {
if (!v)
delete this.fields.description;
else
this.fields.description = v;
}
/**
* Machine-readable metadata used by Wallet to offer pass-related actions.
*/
get semantics() {
return this.fields.semantics;
}
set semantics(v) {
if (!v) {
delete this.fields.semantics;
return;
}
this.fields.semantics = normalizeSemanticTags(v);
}
/**
* Custom information for companion apps. Not displayed to the user.
*/
get userInfo() {
return this.fields.userInfo;
}
set userInfo(v) {
if (v === undefined || v === null) {
delete this.fields.userInfo;
return;
}
// Deep-clone so Template → Pass → caller don't share mutable state
// through the shallow spread in Template.createPass.
this.fields.userInfo = structuredClone(v);
}
/**
* Contents of `personalization.json`, used by Wallet's NFC reward-card
* signup flow. The file is only emitted when the final bundle also has a
* serialized NFC dictionary and a `personalizationLogo*.png` asset.
*/
get personalization() {
return this.personalizationData;
}
set personalization(v) {
if (!v) {
this.personalizationData = undefined;
return;
}
this.personalizationData = validatePersonalization(v);
}
/**
* Display name of the organization that originated and signed the pass.
*/
get organizationName() {
return this.fields.organizationName;
}
set organizationName(v) {
if (!v)
delete this.fields.organizationName;
else
this.fields.organizationName = v;
}
/**
* Optional for event tickets and boarding passes; otherwise not allowed.
* Identifier used to group related passes.
* If a grouping identifier is specified, passes with the same style,
* pass type identifier, and grouping identifier are displayed as a group.
* Otherwise, passes are grouped automatically.
* Use this to group passes that are tightly related,
* such as the boarding passes for different connections of the same trip.
*/
get groupingIdentifier() {
return this.fields.groupingIdentifier;
}
set groupingIdentifier(v) {
if (!v)
delete this.fields.groupingIdentifier;
else
this.fields.groupingIdentifier = v;
}
/**
* If true, the strip image is displayed without a shine effect.
* The default value prior to iOS 7.0 is false.
* In iOS 7.0, a shine effect is never applied, and this key is deprecated.
*/
get suppressStripShine() {
return !!this.fields.suppressStripShine;
}
set suppressStripShine(v) {
if (!v)
delete this.fields.suppressStripShine;
else
this.fields.suppressStripShine = true;
}
/**
* Text displayed next to the logo on the pass.
*/
get logoText() {
return this.fields.logoText;
}
set logoText(v) {
if (!v)
delete this.fields.logoText;
else
this.fields.logoText = v;
}
/**
* The URL of a web service that conforms to the API described in PassKit Web Service Reference.
* The web service must use the HTTPS protocol in production; the leading https:// is included in the value of this key.
* On devices configured for development, there is UI in Settings to allow HTTP web services. You can use the options
* parameter to set allowHTTP to be able to use URLs that use the HTTP protocol.
*
* @see {@link https://developer.apple.com/library/archive/documentation/PassKit/Reference/PassKit_WebService/WebService.html#//apple_ref/doc/uid/TP40011988}
*/
get webServiceURL() {
return this.fields.webServiceURL;
}
set webServiceURL(v) {
if (!v) {
delete this.fields.webServiceURL;
return;
}
// validating URL, it will throw on bad value
const url = v instanceof URL ? v : new URL(v);
const allowHttp = this.options?.allowHttp ?? false;
if (!allowHttp && url.protocol !== 'https:') {
throw new TypeError(`webServiceURL must be on HTTPS!`);
}
this.fields.webServiceURL = v;
}
/**
* The authentication token to use with the web service.
* The token must be 16 characters or longer.
*/
get authenticationToken() {
return this.fields.authenticationToken;
}
set authenticationToken(v) {
if (!v) {
delete this.fields.authenticationToken;
return;
}
if (typeof v !== 'string')
throw new TypeError(`authenticationToken must be a string, received ${typeof v}`);
if (v.length < 16)
throw new TypeError(`authenticationToken must must be 16 characters or longer`);
this.fields.authenticationToken = v;
}
/**
* Background color of the pass, specified as an CSS-style RGB triple.
*
* @example rgb(23, 187, 82)
*/
get backgroundColor() {
if (!(this.fields.backgroundColor instanceof PassColor))
return undefined;
return this.fields.backgroundColor;
}
set backgroundColor(v) {
if (!v) {
delete this.fields.backgroundColor;
return;
}
if (!(this.fields.backgroundColor instanceof PassColor))
this.fields.backgroundColor = new PassColor(v);
else
this.fields.backgroundColor.set(v);
}
/**
* Foreground color of the pass, specified as a CSS-style RGB triple.
*
* @example rgb(100, 10, 110)
*/
get foregroundColor() {
if (!(this.fields.foregroundColor instanceof PassColor))
return undefined;
return this.fields.foregroundColor;
}
set foregroundColor(v) {
if (!v) {
delete this.fields.foregroundColor;
return;
}
if (!(this.fields.foregroundColor instanceof PassColor))
this.fields.foregroundColor = new PassColor(v);
else
this.fields.foregroundColor.set(v);
}
/**
* Color of the label text, specified as a CSS-style RGB triple.
*
* @example rgb(255, 255, 255)
*/
get labelColor() {
if (!(this.fields.labelColor instanceof PassColor))
return undefined;
return this.fields.labelColor;
}
set labelColor(v) {
if (!v) {
delete this.fields.labelColor;
return;
}
if (!(this.fields.labelColor instanceof PassColor))
this.fields.labelColor = new PassColor(v);
else
this.fields.labelColor.set(v);
}
/**
* Color of the strip text, specified as a CSS-style RGB triple.
*
* @example rgb(255, 255, 255)
*/
get stripColor() {
if (!(this.fields.stripColor instanceof PassColor))
return undefined;
return this.fields.stripColor;
}
set stripColor(v) {
if (!v) {
delete this.fields.stripColor;
return;
}
if (!(this.fields.stripColor instanceof PassColor))
this.fields.stripColor = new PassColor(v);
else
this.fields.stripColor.set(v);
}
/**
* Maximum distance in meters from a relevant latitude and longitude that the pass is relevant.
* This number is compared to the pass’s default distance and the smaller value is used.
*/
get maxDistance() {
return this.fields.maxDistance;
}
set maxDistance(v) {
if (!v) {
delete this.fields.maxDistance;
return;
}
if (!Number.isInteger(v))
throw new TypeError('maxDistance must be a positive integer distance in meters!');
this.fields.maxDistance = v;
}
/**
* Beacons marking locations where the pass is relevant.
*/
get beacons() {
return this.fields.beacons;
}
set beacons(v) {
if (!v || !Array.isArray(v)) {
delete this.fields.beacons;
return;
}
for (const beacon of v) {
if (!beacon.proximityUUID)
throw new TypeError(`each beacon must contain proximityUUID`);
}
// copy array
this.fields.beacons = [...v];
}
/**
* Information specific to the pass’s barcode.
* The system uses the first valid barcode dictionary in the array.
* Additional dictionaries can be added as fallbacks.
*/
get barcodes() {
return this.fields.barcodes;
}
set barcodes(v) {
if (!v) {
delete this.fields.barcodes;
delete this.fields.barcode;
return;
}
if (!Array.isArray(v))
throw new TypeError(`barcodes must be an Array, received ${typeof v}`);
// Barcodes dictionary: https://developer.apple.com/library/content/documentation/UserExperience/Reference/PassKit_Bundle/Chapters/LowerLevel.html#//apple_ref/doc/uid/TP40012026-CH3-SW3
for (const barcode of v) {
if (!BARCODES_FORMAT.has(barcode.format))
throw new TypeError(`Barcode format value ${barcode.format} is invalid!`);
if (typeof barcode.message !== 'string')
throw new TypeError('Barcode message string is required');
if (typeof barcode.messageEncoding !== 'string')
throw new TypeError('Barcode messageEncoding is required');
}
// copy array
this.fields.barcodes = [...v];
}
/**
* Adds a location where a pass is relevant.
*
* @param {number[] | { lat: number, lng: number, alt?: number } | { longitude: number, latitude: number, altitude?: number }} point
* @param {string} [relevantText]
* @returns {this}
*/
addLocation(point, relevantText) {
const { longitude, latitude, altitude } = getGeoPoint(point);
const location = {
longitude,
latitude,
};
if (altitude)
location.altitude = altitude;
if (typeof relevantText === 'string')
location.relevantText = relevantText;
if (!Array.isArray(this.fields.locations))
this.fields.locations = [location];
else
this.fields.locations.push(location);
return this;
}
get locations() {
return this.fields.locations;
}
set locations(v) {
delete this.fields.locations;
if (!v)
return;
if (!Array.isArray(v))
throw new TypeError(`locations must be an array`);
else
for (const location of v)
this.addLocation(location, location.relevantText);
}
// ─── iOS 18 event-ticket: Event Guide URL keys ─────────────────────────
// 12 optional URL fields. `new URL(v)` validates shape only — no scheme
// restriction (Apple recommends https but mailto/tel/custom schemes are
// accepted in the wild). Contact email and phone are plain strings
// (below); they are not URLs.
get bagPolicyURL() {
return this.fields.bagPolicyURL;
}
set bagPolicyURL(v) {
if (!v) {
delete this.fields.bagPolicyURL;
return;
}
void new URL(v);
this.fields.bagPolicyURL = v;
}
get orderFoodURL() {
return this.fields.orderFoodURL;
}
set orderFoodURL(v) {
if (!v) {
delete this.fields.orderFoodURL;
return;
}
void new URL(v);
this.fields.orderFoodURL = v;
}
get parkingInformationURL() {
return this.fields.parkingInformationURL;
}
set parkingInformationURL(v) {
if (!v) {
delete this.fields.parkingInformationURL;
return;
}
void new URL(v);
this.fields.parkingInformationURL = v;
}
get directionsInformationURL() {
return this.fields.directionsInformationURL;
}
set directionsInformationURL(v) {
if (!v) {
delete this.fields.directionsInformationURL;
return;
}
void new URL(v);
this.fields.directionsInformationURL = v;
}
get purchaseParkingURL() {
return this.fields.purchaseParkingURL;
}
set purchaseParkingURL(v) {
if (!v) {
delete this.fields.purchaseParkingURL;
return;
}
void new URL(v);
this.fields.purchaseParkingURL = v;
}
get merchandiseURL() {
return this.fields.merchandiseURL;
}
set merchandiseURL(v) {
if (!v) {
delete this.fields.merchandiseURL;
return;
}
void new URL(v);
this.fields.merchandiseURL = v;
}
get transitInformationURL() {
return this.fields.transitInformationURL;
}
set transitInformationURL(v) {
if (!v) {
delete this.fields.transitInformationURL;
return;
}
void new URL(v);
this.fields.transitInformationURL = v;
}
get accessibilityURL() {
return this.fields.accessibilityURL;
}
set accessibilityURL(v) {
if (!v) {
delete this.fields.accessibilityURL;
return;
}
void new URL(v);
this.fields.accessibilityURL = v;
}
get addOnURL() {
return this.fields.addOnURL;
}
set addOnURL(v) {
if (!v) {
delete this.fields.addOnURL;
return;
}
void new URL(v);
this.fields.addOnURL = v;
}
get contactVenueWebsite() {
return this.fields.contactVenueWebsite;
}
set contactVenueWebsite(v) {
if (!v) {
delete this.fields.contactVenueWebsite;
return;
}
void new URL(v);
this.fields.contactVenueWebsite = v;
}
get transferURL() {
return this.fields.transferURL;
}
set transferURL(v) {
if (!v) {
delete this.fields.transferURL;
return;
}
void new URL(v);
this.fields.transferURL = v;
}
get sellURL() {
return this.fields.sellURL;
}
set sellURL(v) {
if (!v) {
delete this.fields.sellURL;
return;
}
void new URL(v);
this.fields.sellURL = v;
}
// ─── iOS 18 event-ticket: plain-string keys ────────────────────────────
// Not URLs; stored as strings with delete-on-empty semantics.
get contactVenueEmail() {
return this.fields.contactVenueEmail;
}
set contactVenueEmail(v) {
if (!v)
delete this.fields.contactVenueEmail;
else
this.fields.contactVenueEmail = v;
}
get contactVenuePhoneNumber() {
return this.fields.contactVenuePhoneNumber;
}
set contactVenuePhoneNumber(v) {
if (!v)
delete this.fields.contactVenuePhoneNumber;
else
this.fields.contactVenuePhoneNumber = v;
}
get eventLogoText() {
return this.fields.eventLogoText;
}
set eventLogoText(v) {
if (!v)
delete this.fields.eventLogoText;
else
this.fields.eventLogoText = v;
}
// ─── iOS 18 event-ticket: styling + misc ──────────────────────────────
get suppressHeaderDarkening() {
return !!this.fields.suppressHeaderDarkening;
}
set suppressHeaderDarkening(v) {
if (!v)
delete this.fields.suppressHeaderDarkening;
else
this.fields.suppressHeaderDarkening = true;
}
get useAutomaticColors() {
return !!this.fields.useAutomaticColors;
}
set useAutomaticColors(v) {
if (!v)
delete this.fields.useAutomaticColors;
else
this.fields.useAutomaticColors = true;
}
// Overrides the chin/footer color in the new event-ticket layout.
// Uses the same PassColor machinery as backgroundColor.
get footerBackgroundColor() {
if (!(this.fields.footerBackgroundColor instanceof PassColor))
return undefined;
return this.fields.footerBackgroundColor;
}
set footerBackgroundColor(v) {
if (!v) {
delete this.fields.footerBackgroundColor;
return;
}
if (!(this.fields.footerBackgroundColor instanceof PassColor))
this.fields.footerBackgroundColor = new PassColor(v);
else
this.fields.footerBackgroundColor.set(v);
}
// Secondary App Store IDs; filter to integers like
// associatedStoreIdentifiers does (drop non-integer / non-number entries).
get auxiliaryStoreIdentifiers() {
return this.fields.auxiliaryStoreIdentifiers;
}
set auxiliaryStoreIdentifiers(v) {
if (!v) {
delete this.fields.auxiliaryStoreIdentifiers;
return;
}
const arrayOfNumbers = v.filter(n => Number.isInteger(n));
if (arrayOfNumbers.length > 0)
this.fields.auxiliaryStoreIdentifiers = arrayOfNumbers;
else
delete this.fields.auxiliaryStoreIdentifiers;
}
// ─── iOS 26 enhanced / semantic boarding pass: URL keys ───────────────
get changeSeatURL() {
return this.fields.changeSeatURL;
}
set changeSeatURL(v) {
if (!v) {
delete this.fields.changeSeatURL;
return;
}
void new URL(v);
this.fields.changeSeatURL = v;
}
get entertainmentURL() {
return this.fields.entertainmentURL;
}
set entertainmentURL(v) {
if (!v) {
delete this.fields.entertainmentURL;
return;
}
void new URL(v);
this.fields.entertainmentURL = v;
}
get purchaseAdditionalBaggageURL() {
return this.fields.purchaseAdditionalBaggageURL;
}
set purchaseAdditionalBaggageURL(v) {
if (!v) {
delete this.fields.purchaseAdditionalBaggageURL;
return;
}
void new URL(v);
this.fields.purchaseAdditionalBaggageURL = v;
}
get purchaseLoungeAccessURL() {
return this.fields.purchaseLoungeAccessURL;
}
set purchaseLoungeAccessURL(v) {
if (!v) {
delete this.fields.purchaseLoungeAccessURL;
return;
}
void new URL(v);
this.fields.purchaseLoungeAccessURL = v;
}
get purchaseWifiURL() {
return this.fields.purchaseWifiURL;
}
set purchaseWifiURL(v) {
if (!v) {
delete this.fields.purchaseWifiURL;
return;
}
void new URL(v);
this.fields.purchaseWifiURL = v;
}
get upgradeURL() {
return this.fields.upgradeURL;
}
set upgradeURL(v) {
if (!v) {
delete this.fields.upgradeURL;
return;
}
void new URL(v);
this.fields.upgradeURL = v;
}
get managementURL() {
return this.fields.managementURL;
}
set managementURL(v) {
if (!v) {
delete this.fields.managementURL;
return;
}
void new URL(v);
this.fields.managementURL = v;
}
get registerServiceAnimalURL() {
return this.fields.registerServiceAnimalURL;
}
set registerServiceAnimalURL(v) {
if (!v) {
delete this.fields.registerServiceAnimalURL;
return;
}
void new URL(v);
this.fields.registerServiceAnimalURL = v;
}
get reportLostBagURL() {
return this.fields.reportLostBagURL;
}
set reportLostBagURL(v) {
if (!v) {
delete this.fields.reportLostBagURL;
return;
}
void new URL(v);
this.fields.reportLostBagURL = v;
}
get requestWheelchairURL() {
return this.fields.requestWheelchairURL;
}
set requestWheelchairURL(v) {
if (!v) {
delete this.fields.requestWheelchairURL;
return;
}
void new URL(v);
this.fields.requestWheelchairURL = v;
}
get transitProviderWebsiteURL() {
return this.fields.transitProviderWebsiteURL;
}
set transitProviderWebsiteURL(v) {
if (!v) {
delete this.fields.transitProviderWebsiteURL;
return;
}
void new URL(v);
this.fields.transitProviderWebsiteURL = v;
}
// iOS 26 transit provider — contact fields (plain strings, not URLs)
get transitProviderEmail() {
return this.fields.transitProviderEmail;
}
set transitProviderEmail(v) {
if (!v)
delete this.fields.transitProviderEmail;
else
this.fields.transitProviderEmail = v;
}
get transitProviderPhoneNumber() {
return this.fields.transitProviderPhoneNumber;
}
set transitProviderPhoneNumber(v) {
if (!v)
delete this.fields.transitProviderPhoneNumber;
else
this.fields.transitProviderPhoneNumber = v;
}
// ─── iOS 26 poster event ticket: upcomingPassInformation ──────────────
// Setter validates per-entry shape (identifier / name / type / image
// URLs + SHA256 + size). The cross-field check that the pass is an
// eventTicket opted into `posterEventTicket` runs at pass-build time
// in `Pass.validate()` via `assertUpcomingPassInformationContext`,
// so hydrating a pass from a plain object whose keys happen to put
// `upcomingPassInformation` before `preferredStyleSchemes` doesn't
// throw during construction. Dates inside `dateInformation.date` are
// normalized to W3C strings by `normalizeDatesDeep` at toJSON time
// (see PassBase.toJSON above).
get upcomingPassInformation() {
return this.fields.upcomingPassInformation;
}
set upcomingPassInformation(v) {
if (!v) {
delete this.fields.upcomingPassInformation;
return;
}
this.fields.upcomingPassInformation =
validateUpcomingPassInformationEntries(v);
}
}
//# sourceMappingURL=base-pass.js.map