Skip to content

Commit 4f28f63

Browse files
committed
Add semantic tags support
1 parent 84ddb92 commit 4f28f63

10 files changed

Lines changed: 178 additions & 7 deletions

File tree

README.md

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,30 @@ pass.primaryFields.delete("date");
192192
pass.primaryFields.clear();
193193
```
194194

195+
You can add Apple Wallet semantic tags at the pass level or on a specific
196+
field:
197+
198+
```js
199+
pass.semantics = {
200+
eventName: "Animated Movie",
201+
venueName: "Steve Jobs Theater",
202+
venueLocation: {
203+
latitude: 37.330886,
204+
longitude: -122.007427
205+
}
206+
};
207+
208+
pass.secondaryFields.add({
209+
key: "runtime",
210+
label: "Run Time",
211+
value: "1:21",
212+
semantics: {
213+
eventStartDate: new Date("2025-01-02T19:00:00-08:00"),
214+
eventEndDate: "2025-01-02T20:21-08:00"
215+
}
216+
});
217+
```
218+
195219
Adding images to a pass is the same as adding images to a template (see above).
196220

197221
# Working with Dates
@@ -295,4 +319,3 @@ If the pass file generates without errors but you aren't able to open your pass
295319
# Financial Contributors
296320

297321
Become a financial contributor and help us sustain our community. [[Contribute](https://opencollective.com/walletpass/contribute)]
298-

__tests__/base-pass.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { PassBase } from '../src/lib/base-pass';
22
import { TOP_LEVEL_FIELDS } from '../src/constants';
3+
import { getW3CDateString } from '../src/lib/w3cdate';
34
import 'jest-extended';
45

56
describe('PassBase', () => {
@@ -40,6 +41,41 @@ describe('PassBase', () => {
4041
}).toThrow(TypeError);
4142
});
4243

44+
it('works with semantic tags', () => {
45+
const eventStartDate = new Date(2025, 0, 2, 3, 4);
46+
const bp = new PassBase({
47+
eventTicket: {},
48+
semantics: {
49+
eventName: 'Animated Movie',
50+
eventStartDate,
51+
totalPrice: {
52+
amount: 1250,
53+
currencyCode: 'USD',
54+
},
55+
venueLocation: {
56+
latitude: 37.330886,
57+
longitude: -122.007427,
58+
},
59+
},
60+
});
61+
62+
expect(bp.semantics).toEqual({
63+
eventName: 'Animated Movie',
64+
eventStartDate: getW3CDateString(eventStartDate),
65+
totalPrice: {
66+
amount: 1250,
67+
currencyCode: 'USD',
68+
},
69+
venueLocation: {
70+
latitude: 37.330886,
71+
longitude: -122.007427,
72+
},
73+
});
74+
expect(JSON.stringify(bp)).toContain('"semantics"');
75+
bp.semantics = undefined;
76+
expect(bp.semantics).toBeUndefined();
77+
});
78+
4379
it('webServiceURL', () => {
4480
const bp = new PassBase();
4581
expect(() => {

__tests__/fieldsMap.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,28 @@ test('FieldsMap Class', () => {
4141
)}"}]`,
4242
);
4343
});
44+
45+
test('FieldsMap serializes semantic tags', () => {
46+
const fields = new FieldsMap();
47+
const eventStartDate = new Date(2025, 0, 2, 3, 4);
48+
49+
fields.add({
50+
key: 'event',
51+
label: 'Venue',
52+
value: 'Steve Jobs Theater',
53+
semantics: {
54+
eventName: 'Animated Movie',
55+
eventStartDate,
56+
venueLocation: {
57+
latitude: 37.330886,
58+
longitude: -122.007427,
59+
},
60+
},
61+
});
62+
63+
expect(JSON.stringify(fields)).toBe(
64+
`[{"key":"event","label":"Venue","value":"Steve Jobs Theater","semantics":{"eventName":"Animated Movie","eventStartDate":"${getW3CDateString(
65+
eventStartDate,
66+
)}","venueLocation":{"latitude":37.330886,"longitude":-122.007427}}}]`,
67+
);
68+
});

src/constants.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,9 @@ export const TOP_LEVEL_FIELDS: {
204204
templatable: true,
205205
localizable: true,
206206
},
207+
semantics: {
208+
type: Object,
209+
},
207210
suppressStripShine: {
208211
type: Boolean,
209212
templatable: true,

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ import * as constants from './constants';
33
export { constants };
44
export { Template } from './template';
55
export { Pass } from './pass';
6+
export type { SemanticTags, SemanticTagValue } from './interfaces';

src/interfaces.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,34 @@ export type NumberStyle =
3232
| 'PKNumberStyleScientific'
3333
| 'PKNumberStyleSpellOut';
3434

35+
export interface SemanticTagObject {
36+
[key: string]: SemanticTagValue;
37+
}
38+
39+
export type SemanticTagValue =
40+
| string
41+
| number
42+
| boolean
43+
| Date
44+
| SemanticTagObject
45+
| SemanticTagValue[];
46+
47+
/**
48+
* Machine-readable metadata that Wallet uses to offer a pass and suggest
49+
* related actions.
50+
*
51+
* @see {@link https://developer.apple.com/documentation/walletpasses/supporting-semantic-tags-in-wallet-passes}
52+
* @see {@link https://developer.apple.com/documentation/walletpasses/semantictags}
53+
*/
54+
export type SemanticTags = SemanticTagObject;
55+
3556
export type FieldDescriptor = {
3657
// Standard Field Dictionary Keys
3758
label?: string;
3859
attributedValue?: string | number;
3960
changeMessage?: string;
4061
dataDetectorTypes?: DataDetectors[];
62+
semantics?: SemanticTags;
4163
} & (
4264
| {
4365
value: string;
@@ -176,6 +198,14 @@ export interface PassCompanionAppKeys {
176198
userInfo?: any; // eslint-disable-line @typescript-eslint/no-explicit-any
177199
}
178200

201+
export interface PassSemanticKeys {
202+
/**
203+
* Machine-readable metadata for Wallet suggestions. This dictionary may also
204+
* be specified on individual pass fields.
205+
*/
206+
semantics?: SemanticTags;
207+
}
208+
179209
/**
180210
* Information about when a pass expires and whether it is still valid.
181211
* A pass is marked as expired if the current date is after the pass’s expiration date,
@@ -442,6 +472,7 @@ export type PassStructureFields =
442472
export type ApplePass = PassStandardKeys &
443473
PassAssociatedAppKeys &
444474
PassCompanionAppKeys &
475+
PassSemanticKeys &
445476
PassExpirationKeys &
446477
PassRelevanceKeys &
447478
PassVisualAppearanceKeys &
@@ -450,4 +481,4 @@ export type ApplePass = PassStandardKeys &
450481

451482
export interface Options {
452483
allowHttp: boolean
453-
}
484+
}

src/lib/base-pass.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
import { ApplePass, Options } from '../interfaces';
1+
import { ApplePass, Options, SemanticTags } from '../interfaces';
22
import { BARCODES_FORMAT, STRUCTURE_FIELDS } from '../constants';
33

44
import { PassColor } from './pass-color';
55
import { PassImages } from './images';
66
import { Localizations } from './localizations';
77
import { getGeoPoint } from './get-geo-point';
88
import { PassStructure } from './pass-structure';
9+
import { normalizeSemanticTags } from './semantic-tags';
910
import { getW3CDateString, isValidW3CDateString } from './w3cdate';
1011

1112
const STRUCTURE_FIELDS_SET = new Set([...STRUCTURE_FIELDS, 'nfc']);
@@ -205,6 +206,21 @@ export class PassBase extends PassStructure {
205206
else this.fields.description = v;
206207
}
207208

209+
/**
210+
* Machine-readable metadata used by Wallet to offer pass-related actions.
211+
*/
212+
get semantics(): SemanticTags | undefined {
213+
return this.fields.semantics;
214+
}
215+
216+
set semantics(v: SemanticTags | undefined) {
217+
if (!v) {
218+
delete this.fields.semantics;
219+
return;
220+
}
221+
this.fields.semantics = normalizeSemanticTags(v);
222+
}
223+
208224
/**
209225
* Display name of the organization that originated and signed the pass.
210226
*/

src/lib/fieldsMap.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import { Field, FieldDescriptor, DataStyleFormat } from '../interfaces';
44

5+
import { normalizeSemanticTags } from './semantic-tags';
56
import { getW3CDateString } from './w3cdate';
67

78
export class FieldsMap extends Map<string, FieldDescriptor> {
@@ -12,10 +13,13 @@ export class FieldsMap extends Map<string, FieldDescriptor> {
1213
if (!this.size) return undefined;
1314
return [...this].map(
1415
([key, data]): Field => {
16+
const field = { key, ...data };
1517
// Remap Date objects to string
16-
if (data.value instanceof Date)
17-
data.value = getW3CDateString(data.value);
18-
return { key, ...data };
18+
if (field.value instanceof Date)
19+
field.value = getW3CDateString(field.value);
20+
if (field.semantics)
21+
field.semantics = normalizeSemanticTags(field.semantics);
22+
return field;
1923
},
2024
);
2125
}

src/lib/pass-structure.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import {
99
ApplePass,
10+
BoardingPass,
1011
PassStyle,
1112
TransitType,
1213
PassCommonStructure,
@@ -97,7 +98,10 @@ export class PassStructure {
9798
);
9899

99100
if (!v) {
100-
if (this.fields.boardingPass) delete this.fields.boardingPass.transitType;
101+
if (this.fields.boardingPass)
102+
delete (this.fields.boardingPass as Partial<
103+
BoardingPass['boardingPass']
104+
>).transitType;
101105
} else {
102106
if (Object.values(TRANSIT).includes(v)) {
103107
if (this.fields.boardingPass) this.fields.boardingPass.transitType = v;

src/lib/semantic-tags.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
'use strict';
2+
3+
import type { SemanticTags, SemanticTagValue } from '../interfaces';
4+
5+
import { getW3CDateString } from './w3cdate';
6+
7+
function normalizeSemanticValue(value: SemanticTagValue): SemanticTagValue {
8+
if (value instanceof Date) {
9+
if (!Number.isFinite(value.getTime()))
10+
throw new TypeError(`Semantic tag dates must be valid Date instances`);
11+
return getW3CDateString(value);
12+
}
13+
14+
if (Array.isArray(value)) return value.map(normalizeSemanticValue);
15+
16+
if (value && typeof value === 'object') {
17+
const result: { [key: string]: SemanticTagValue } = {};
18+
for (const [key, nestedValue] of Object.entries(value))
19+
result[key] = normalizeSemanticValue(nestedValue);
20+
return result;
21+
}
22+
23+
return value;
24+
}
25+
26+
export function normalizeSemanticTags(tags: SemanticTags): SemanticTags {
27+
return normalizeSemanticValue(tags) as SemanticTags;
28+
}

0 commit comments

Comments
 (0)