@owask/tapace-templates 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -2
- package/dist/compiler/event-ticket.d.ts +38 -0
- package/dist/compiler/event-ticket.js +131 -0
- package/dist/compiler/loyalty-card.d.ts +28 -0
- package/dist/compiler/loyalty-card.js +70 -0
- package/dist/compiler/passmint-compiler.d.ts +118 -0
- package/dist/compiler/passmint-compiler.js +507 -0
- package/dist/compiler/preview-model.d.ts +123 -0
- package/dist/compiler/preview-model.js +402 -0
- package/dist/compiler/stamp-card.d.ts +23 -0
- package/dist/compiler/stamp-card.js +19 -0
- package/dist/compiler/template-preview.d.ts +34 -0
- package/dist/compiler/template-preview.js +125 -0
- package/dist/contract/definition.d.ts +1316 -0
- package/dist/contract/definition.js +854 -0
- package/dist/contract/event.d.ts +53 -0
- package/dist/contract/event.js +91 -0
- package/dist/contract/featured-actions.d.ts +45 -0
- package/dist/contract/featured-actions.js +81 -0
- package/dist/contract/fields.d.ts +153 -0
- package/dist/contract/fields.js +141 -0
- package/dist/contract/ids.d.ts +2 -0
- package/dist/contract/ids.js +6 -0
- package/dist/contract/index.d.ts +9 -0
- package/dist/contract/index.js +9 -0
- package/dist/contract/points.d.ts +41 -0
- package/dist/contract/points.js +86 -0
- package/dist/contract/primitives.d.ts +26 -0
- package/dist/contract/primitives.js +14 -0
- package/dist/contract/relevance.d.ts +54 -0
- package/dist/contract/relevance.js +52 -0
- package/dist/contract/stamps.d.ts +91 -0
- package/dist/contract/stamps.js +89 -0
- package/dist/contract/upgrade.d.ts +7 -0
- package/dist/contract/upgrade.js +39 -0
- package/dist/public-api.d.ts +4 -0
- package/dist/public-api.js +2 -0
- package/package.json +20 -4
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { type EventDateFormat, type EventTimeValue, type FieldDef, type GoogleRole } from './fields.js';
|
|
3
|
+
/**
|
|
4
|
+
* An event ticket template's live event: kept on the template row, outside
|
|
5
|
+
* revisions, and pushed to every issued pass when saved. Every part is optional.
|
|
6
|
+
*/
|
|
7
|
+
export declare const EventDetailsSchema: z.ZodObject<{
|
|
8
|
+
name: z.ZodOptional<z.ZodString>;
|
|
9
|
+
venue: z.ZodOptional<z.ZodObject<{
|
|
10
|
+
name: z.ZodOptional<z.ZodString>;
|
|
11
|
+
address: z.ZodOptional<z.ZodString>;
|
|
12
|
+
location: z.ZodOptional<z.ZodObject<{
|
|
13
|
+
latitude: z.ZodNumber;
|
|
14
|
+
longitude: z.ZodNumber;
|
|
15
|
+
}, z.core.$strict>>;
|
|
16
|
+
}, z.core.$strict>>;
|
|
17
|
+
doorsOpen: z.ZodOptional<z.ZodString>;
|
|
18
|
+
start: z.ZodOptional<z.ZodString>;
|
|
19
|
+
end: z.ZodOptional<z.ZodString>;
|
|
20
|
+
doorsOpenLabel: z.ZodOptional<z.ZodEnum<{
|
|
21
|
+
doorsOpen: "doorsOpen";
|
|
22
|
+
gatesOpen: "gatesOpen";
|
|
23
|
+
}>>;
|
|
24
|
+
timezone: z.ZodOptional<z.ZodString>;
|
|
25
|
+
}, z.core.$strict>;
|
|
26
|
+
export type EventDetails = z.infer<typeof EventDetailsSchema>;
|
|
27
|
+
/** A template's stored event details; anything unreadable counts as none. */
|
|
28
|
+
export declare function storedEvent(raw: unknown): EventDetails | undefined;
|
|
29
|
+
export declare const MAX_CARD_ROWS = 3;
|
|
30
|
+
export declare const MAX_CARD_ROW_ITEMS = 3;
|
|
31
|
+
export type SeatRole = Exclude<GoogleRole, 'ticketType'>;
|
|
32
|
+
/**
|
|
33
|
+
* A card row item: a field's text, an event time (`event.<time>:<format>`) or a
|
|
34
|
+
* seat value (`seat.<role>`). Event-time and seat-role fields resolve to the
|
|
35
|
+
* same built-in items. Undefined for an unknown key or a field the Google
|
|
36
|
+
* ticket already shows (event name, venue, ticket type).
|
|
37
|
+
*/
|
|
38
|
+
export type CardItem = {
|
|
39
|
+
kind: 'field';
|
|
40
|
+
field: FieldDef;
|
|
41
|
+
} | {
|
|
42
|
+
kind: 'event';
|
|
43
|
+
event: EventTimeValue;
|
|
44
|
+
format: EventDateFormat;
|
|
45
|
+
} | {
|
|
46
|
+
kind: 'seat';
|
|
47
|
+
role: SeatRole;
|
|
48
|
+
};
|
|
49
|
+
export declare function cardItemRef(item: CardItem): string;
|
|
50
|
+
export declare function resolveCardItem(ref: string, fields: readonly FieldDef[]): CardItem | undefined;
|
|
51
|
+
/** Google's card-front rows on an event ticket, each 1–3 items. */
|
|
52
|
+
export declare const CardRowsSchema: z.ZodArray<z.ZodArray<z.ZodString>>;
|
|
53
|
+
export type CardRows = z.infer<typeof CardRowsSchema>;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { EVENT_TIME_VALUES, FieldKeySchema, isEventTime, } from './fields.js';
|
|
3
|
+
import { DateTimeSchema, isTimeZone } from './relevance.js';
|
|
4
|
+
/**
|
|
5
|
+
* An event ticket template's live event: kept on the template row, outside
|
|
6
|
+
* revisions, and pushed to every issued pass when saved. Every part is optional.
|
|
7
|
+
*/
|
|
8
|
+
export const EventDetailsSchema = z
|
|
9
|
+
.strictObject({
|
|
10
|
+
name: z.string().trim().min(1).max(120).optional(),
|
|
11
|
+
venue: z
|
|
12
|
+
.strictObject({
|
|
13
|
+
name: z.string().trim().min(1).max(120).optional(),
|
|
14
|
+
address: z.string().trim().min(1).max(300).optional(),
|
|
15
|
+
location: z
|
|
16
|
+
.strictObject({
|
|
17
|
+
latitude: z.number().min(-90).max(90),
|
|
18
|
+
longitude: z.number().min(-180).max(180),
|
|
19
|
+
})
|
|
20
|
+
.optional(),
|
|
21
|
+
})
|
|
22
|
+
.optional(),
|
|
23
|
+
doorsOpen: DateTimeSchema.optional(),
|
|
24
|
+
start: DateTimeSchema.optional(),
|
|
25
|
+
end: DateTimeSchema.optional(),
|
|
26
|
+
doorsOpenLabel: z.enum(['doorsOpen', 'gatesOpen']).optional(),
|
|
27
|
+
timezone: z.string().refine(isTimeZone, 'Expected a timezone like Asia/Colombo.').optional(),
|
|
28
|
+
})
|
|
29
|
+
.superRefine((event, ctx) => {
|
|
30
|
+
const times = EVENT_TIME_VALUES.filter((key) => event[key] !== undefined);
|
|
31
|
+
for (let i = 1; i < times.length; i++) {
|
|
32
|
+
const before = times[i - 1];
|
|
33
|
+
const after = times[i];
|
|
34
|
+
if (Date.parse(event[after]) < Date.parse(event[before])) {
|
|
35
|
+
ctx.addIssue({
|
|
36
|
+
code: 'custom',
|
|
37
|
+
message: `${TIME_LABELS[after]} can't be before ${TIME_LABELS[before].toLowerCase()}.`,
|
|
38
|
+
path: [after],
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
/** A template's stored event details; anything unreadable counts as none. */
|
|
44
|
+
export function storedEvent(raw) {
|
|
45
|
+
const parsed = EventDetailsSchema.safeParse(raw);
|
|
46
|
+
return parsed.success ? parsed.data : undefined;
|
|
47
|
+
}
|
|
48
|
+
const TIME_LABELS = {
|
|
49
|
+
doorsOpen: 'Doors open',
|
|
50
|
+
start: 'Start',
|
|
51
|
+
end: 'End',
|
|
52
|
+
};
|
|
53
|
+
export const MAX_CARD_ROWS = 3;
|
|
54
|
+
export const MAX_CARD_ROW_ITEMS = 3;
|
|
55
|
+
const EVENT_ITEM_RE = /^event\.(doorsOpen|start|end):(dateTime|date|time)$/;
|
|
56
|
+
const SEAT_ITEM_RE = /^seat\.(seat|row|section|gate)$/;
|
|
57
|
+
export function cardItemRef(item) {
|
|
58
|
+
if (item.kind === 'event')
|
|
59
|
+
return `event.${item.event}:${item.format}`;
|
|
60
|
+
if (item.kind === 'seat')
|
|
61
|
+
return `seat.${item.role}`;
|
|
62
|
+
return item.field.key;
|
|
63
|
+
}
|
|
64
|
+
export function resolveCardItem(ref, fields) {
|
|
65
|
+
const event = EVENT_ITEM_RE.exec(ref);
|
|
66
|
+
if (event) {
|
|
67
|
+
return { kind: 'event', event: event[1], format: event[2] };
|
|
68
|
+
}
|
|
69
|
+
const seat = SEAT_ITEM_RE.exec(ref);
|
|
70
|
+
if (seat)
|
|
71
|
+
return { kind: 'seat', role: seat[1] };
|
|
72
|
+
const field = fields.find((f) => f.key === ref);
|
|
73
|
+
if (!field || field.googleRole === 'ticketType')
|
|
74
|
+
return undefined;
|
|
75
|
+
if (field.googleRole)
|
|
76
|
+
return { kind: 'seat', role: field.googleRole };
|
|
77
|
+
if (field.binding === 'event') {
|
|
78
|
+
if (!field.event || !isEventTime(field.event))
|
|
79
|
+
return undefined;
|
|
80
|
+
return { kind: 'event', event: field.event, format: field.dateFormat ?? 'dateTime' };
|
|
81
|
+
}
|
|
82
|
+
return { kind: 'field', field };
|
|
83
|
+
}
|
|
84
|
+
const CardItemRefSchema = z
|
|
85
|
+
.string()
|
|
86
|
+
.refine((ref) => FieldKeySchema.safeParse(ref).success || EVENT_ITEM_RE.test(ref) || SEAT_ITEM_RE.test(ref), 'Expected a field key, "event.<doorsOpen|start|end>:<dateTime|date|time>" or "seat.<seat|row|section|gate>".');
|
|
87
|
+
/** Google's card-front rows on an event ticket, each 1–3 items. */
|
|
88
|
+
export const CardRowsSchema = z
|
|
89
|
+
.array(z.array(CardItemRefSchema).min(1).max(MAX_CARD_ROW_ITEMS))
|
|
90
|
+
.min(1)
|
|
91
|
+
.max(MAX_CARD_ROWS);
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const ACTION_ID_RE: RegExp;
|
|
3
|
+
/**
|
|
4
|
+
* The types that showed on an iOS 27 device. `place` is left out: it never
|
|
5
|
+
* showed with any of 10 link formats.
|
|
6
|
+
*/
|
|
7
|
+
export declare const FEATURED_ACTION_TYPES: readonly ["viewSchedule", "watchTrailer", "listenToMusic", "call", "addToBalance", "order", "shop", "membershipBenefits", "bookAppointment", "bookCar", "bookFlight", "bookStay", "viewOffersRewards"];
|
|
8
|
+
export type FeaturedActionType = (typeof FEATURED_ACTION_TYPES)[number];
|
|
9
|
+
export declare const MAX_FEATURED_ACTIONS = 2;
|
|
10
|
+
/** Wallet shows `call` only with a `tel:` link and every other type only with `https:`. */
|
|
11
|
+
export declare function featuredActionUrlError(type: FeaturedActionType, url: string): string | null;
|
|
12
|
+
/**
|
|
13
|
+
* A button Wallet shows below the pass (iOS 27+, Apple only). `binding` says
|
|
14
|
+
* where the link comes from:
|
|
15
|
+
* - `fixed` — `url`, the same on every pass.
|
|
16
|
+
* - `overridable` — `url` unless the pass supplies its own.
|
|
17
|
+
* - `perPass` — every pass supplies its own; `sampleUrl` is preview-only.
|
|
18
|
+
*/
|
|
19
|
+
export declare const FeaturedActionDefSchema: z.ZodObject<{
|
|
20
|
+
id: z.ZodOptional<z.ZodString>;
|
|
21
|
+
key: z.ZodString;
|
|
22
|
+
type: z.ZodEnum<{
|
|
23
|
+
viewSchedule: "viewSchedule";
|
|
24
|
+
watchTrailer: "watchTrailer";
|
|
25
|
+
listenToMusic: "listenToMusic";
|
|
26
|
+
call: "call";
|
|
27
|
+
addToBalance: "addToBalance";
|
|
28
|
+
order: "order";
|
|
29
|
+
shop: "shop";
|
|
30
|
+
membershipBenefits: "membershipBenefits";
|
|
31
|
+
bookAppointment: "bookAppointment";
|
|
32
|
+
bookCar: "bookCar";
|
|
33
|
+
bookFlight: "bookFlight";
|
|
34
|
+
bookStay: "bookStay";
|
|
35
|
+
viewOffersRewards: "viewOffersRewards";
|
|
36
|
+
}>;
|
|
37
|
+
binding: z.ZodEnum<{
|
|
38
|
+
perPass: "perPass";
|
|
39
|
+
fixed: "fixed";
|
|
40
|
+
overridable: "overridable";
|
|
41
|
+
}>;
|
|
42
|
+
url: z.ZodOptional<z.ZodString>;
|
|
43
|
+
sampleUrl: z.ZodOptional<z.ZodString>;
|
|
44
|
+
}, z.core.$strict>;
|
|
45
|
+
export type FeaturedActionDef = z.infer<typeof FeaturedActionDefSchema>;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { FieldKeySchema } from './fields.js';
|
|
3
|
+
export const ACTION_ID_RE = /^fa_[A-Za-z0-9_-]{1,64}$/;
|
|
4
|
+
/**
|
|
5
|
+
* The types that showed on an iOS 27 device. `place` is left out: it never
|
|
6
|
+
* showed with any of 10 link formats.
|
|
7
|
+
*/
|
|
8
|
+
export const FEATURED_ACTION_TYPES = [
|
|
9
|
+
'viewSchedule',
|
|
10
|
+
'watchTrailer',
|
|
11
|
+
'listenToMusic',
|
|
12
|
+
'call',
|
|
13
|
+
'addToBalance',
|
|
14
|
+
'order',
|
|
15
|
+
'shop',
|
|
16
|
+
'membershipBenefits',
|
|
17
|
+
'bookAppointment',
|
|
18
|
+
'bookCar',
|
|
19
|
+
'bookFlight',
|
|
20
|
+
'bookStay',
|
|
21
|
+
'viewOffersRewards',
|
|
22
|
+
];
|
|
23
|
+
export const MAX_FEATURED_ACTIONS = 2;
|
|
24
|
+
const TEL_RE = /^tel:\+?[0-9][0-9 ()-]{2,30}$/;
|
|
25
|
+
/** Wallet shows `call` only with a `tel:` link and every other type only with `https:`. */
|
|
26
|
+
export function featuredActionUrlError(type, url) {
|
|
27
|
+
if (type === 'call') {
|
|
28
|
+
return TEL_RE.test(url) ? null : 'A call action needs a phone number link like tel:+94112345678.';
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
return new URL(url).protocol === 'https:' ? null : 'The link must start with https://.';
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return 'The link must be a valid https:// URL.';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* A button Wallet shows below the pass (iOS 27+, Apple only). `binding` says
|
|
39
|
+
* where the link comes from:
|
|
40
|
+
* - `fixed` — `url`, the same on every pass.
|
|
41
|
+
* - `overridable` — `url` unless the pass supplies its own.
|
|
42
|
+
* - `perPass` — every pass supplies its own; `sampleUrl` is preview-only.
|
|
43
|
+
*/
|
|
44
|
+
export const FeaturedActionDefSchema = z
|
|
45
|
+
.strictObject({
|
|
46
|
+
id: z.string().regex(ACTION_ID_RE, 'Invalid action id').optional(),
|
|
47
|
+
key: FieldKeySchema,
|
|
48
|
+
type: z.enum(FEATURED_ACTION_TYPES),
|
|
49
|
+
binding: z.enum(['fixed', 'overridable', 'perPass']),
|
|
50
|
+
url: z.string().max(2048).optional(),
|
|
51
|
+
sampleUrl: z.string().max(2048).optional(),
|
|
52
|
+
})
|
|
53
|
+
.superRefine((action, ctx) => {
|
|
54
|
+
if (action.binding === 'perPass') {
|
|
55
|
+
if (action.url !== undefined) {
|
|
56
|
+
ctx.addIssue({
|
|
57
|
+
code: 'custom',
|
|
58
|
+
message: 'A link set per pass has no template link.',
|
|
59
|
+
path: ['url'],
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
else if (action.url === undefined) {
|
|
64
|
+
ctx.addIssue({ code: 'custom', message: 'Add a link for this action.', path: ['url'] });
|
|
65
|
+
}
|
|
66
|
+
if (action.sampleUrl !== undefined && action.binding !== 'perPass') {
|
|
67
|
+
ctx.addIssue({
|
|
68
|
+
code: 'custom',
|
|
69
|
+
message: 'Only a link set per pass has a sample link.',
|
|
70
|
+
path: ['sampleUrl'],
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
for (const prop of ['url', 'sampleUrl']) {
|
|
74
|
+
const value = action[prop];
|
|
75
|
+
if (value === undefined)
|
|
76
|
+
continue;
|
|
77
|
+
const message = featuredActionUrlError(action.type, value);
|
|
78
|
+
if (message)
|
|
79
|
+
ctx.addIssue({ code: 'custom', message, path: [prop] });
|
|
80
|
+
}
|
|
81
|
+
});
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* A field id is stable across revisions of one template. Its full identity is
|
|
4
|
+
* `(templateId, fieldId)`, so independent templates may use the same local id.
|
|
5
|
+
*/
|
|
6
|
+
export declare const FIELD_ID_RE: RegExp;
|
|
7
|
+
export declare const FieldKeySchema: z.ZodString;
|
|
8
|
+
export declare const EVENT_VALUES: readonly ["name", "venueName", "venueAddress", "doorsOpen", "start", "end"];
|
|
9
|
+
export type EventValue = (typeof EVENT_VALUES)[number];
|
|
10
|
+
export declare const EVENT_TIME_VALUES: readonly ["doorsOpen", "start", "end"];
|
|
11
|
+
export type EventTimeValue = (typeof EVENT_TIME_VALUES)[number];
|
|
12
|
+
export declare const EVENT_DATE_FORMATS: readonly ["dateTime", "date", "time"];
|
|
13
|
+
export type EventDateFormat = (typeof EVENT_DATE_FORMATS)[number];
|
|
14
|
+
/** Google shows these natively on an event ticket; `ticketType` is not a seat value. */
|
|
15
|
+
export declare const GOOGLE_ROLES: readonly ["seat", "row", "section", "gate", "ticketType"];
|
|
16
|
+
export type GoogleRole = (typeof GOOGLE_ROLES)[number];
|
|
17
|
+
export declare function isEventTime(value: EventValue): value is EventTimeValue;
|
|
18
|
+
/**
|
|
19
|
+
* One field on the pass face. `binding` is the shared-vs-per-holder boundary:
|
|
20
|
+
* - `shared` — the value is part of the design (same on every issued pass).
|
|
21
|
+
* - `perPass` — the value is supplied at issuance; `required` gates it then.
|
|
22
|
+
* - `event` — the value comes from the template's live event details (`event`),
|
|
23
|
+
* with `dateFormat` for a time; event ticket templates only.
|
|
24
|
+
* - `stamps` — the pass's stamp count as "7 / 10"; stamp card templates only.
|
|
25
|
+
* - `points` — the pass's balance as "350 / 1,000" (or "350" with no goal);
|
|
26
|
+
* points card templates only.
|
|
27
|
+
* - `stampsLeft` — stamps still to earn, as "3"; stamp card templates only.
|
|
28
|
+
* - `pointsToGoal` — points still to the goal, as "160"; points card templates
|
|
29
|
+
* with a goal only.
|
|
30
|
+
*
|
|
31
|
+
* `googleRole` makes a per-pass field Google's native seat, row, section, gate or
|
|
32
|
+
* ticket type on an event ticket; Apple shows it as an ordinary field.
|
|
33
|
+
*
|
|
34
|
+
* `defaultValue` is the production default; `sampleValue` is preview-only and
|
|
35
|
+
* never persisted into an issued pass. Keeping them apart stops sample data
|
|
36
|
+
* from leaking into real passes.
|
|
37
|
+
*/
|
|
38
|
+
export declare const FieldDefSchema: z.ZodObject<{
|
|
39
|
+
id: z.ZodOptional<z.ZodString>;
|
|
40
|
+
key: z.ZodString;
|
|
41
|
+
label: z.ZodString;
|
|
42
|
+
binding: z.ZodEnum<{
|
|
43
|
+
shared: "shared";
|
|
44
|
+
perPass: "perPass";
|
|
45
|
+
event: "event";
|
|
46
|
+
stamps: "stamps";
|
|
47
|
+
points: "points";
|
|
48
|
+
stampsLeft: "stampsLeft";
|
|
49
|
+
pointsToGoal: "pointsToGoal";
|
|
50
|
+
}>;
|
|
51
|
+
required: z.ZodBoolean;
|
|
52
|
+
defaultValue: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>;
|
|
53
|
+
sampleValue: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>;
|
|
54
|
+
textAlignment: z.ZodOptional<z.ZodEnum<{
|
|
55
|
+
left: "left";
|
|
56
|
+
center: "center";
|
|
57
|
+
right: "right";
|
|
58
|
+
natural: "natural";
|
|
59
|
+
}>>;
|
|
60
|
+
event: z.ZodOptional<z.ZodEnum<{
|
|
61
|
+
name: "name";
|
|
62
|
+
venueName: "venueName";
|
|
63
|
+
venueAddress: "venueAddress";
|
|
64
|
+
doorsOpen: "doorsOpen";
|
|
65
|
+
start: "start";
|
|
66
|
+
end: "end";
|
|
67
|
+
}>>;
|
|
68
|
+
dateFormat: z.ZodOptional<z.ZodEnum<{
|
|
69
|
+
date: "date";
|
|
70
|
+
dateTime: "dateTime";
|
|
71
|
+
time: "time";
|
|
72
|
+
}>>;
|
|
73
|
+
googleRole: z.ZodOptional<z.ZodEnum<{
|
|
74
|
+
seat: "seat";
|
|
75
|
+
row: "row";
|
|
76
|
+
section: "section";
|
|
77
|
+
gate: "gate";
|
|
78
|
+
ticketType: "ticketType";
|
|
79
|
+
}>>;
|
|
80
|
+
}, z.core.$strict>;
|
|
81
|
+
export type FieldDef = z.infer<typeof FieldDefSchema>;
|
|
82
|
+
export declare const MAX_FIELDS = 40;
|
|
83
|
+
export declare const STANDARD_SLOTS: readonly ["header", "primary", "secondary", "auxiliary", "back"];
|
|
84
|
+
export type StandardSlot = (typeof STANDARD_SLOTS)[number];
|
|
85
|
+
export declare const POSTER_SLOTS: readonly ["header", "primary", "footer", "additionalInfo", "back"];
|
|
86
|
+
export type PosterSlot = (typeof POSTER_SLOTS)[number];
|
|
87
|
+
export type FieldSlot = StandardSlot | PosterSlot;
|
|
88
|
+
export type LayoutName = 'standard' | 'poster';
|
|
89
|
+
/**
|
|
90
|
+
* The Apple style block (`generic` / `eventTicket`) that older iOS shows, and the
|
|
91
|
+
* basis of the whole Google pass. Caps mirror the wallet layouts; passmint
|
|
92
|
+
* enforces the authoritative native limits at compile time.
|
|
93
|
+
*/
|
|
94
|
+
export declare const StandardLayoutSchema: z.ZodObject<{
|
|
95
|
+
header: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
96
|
+
primary: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
97
|
+
secondary: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
98
|
+
auxiliary: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
99
|
+
back: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
100
|
+
}, z.core.$strict>;
|
|
101
|
+
export type StandardLayout = z.infer<typeof StandardLayoutSchema>;
|
|
102
|
+
/** iOS 27 poster layout. Caps are what Wallet showed on an iOS 27 device. */
|
|
103
|
+
export declare const PosterLayoutSchema: z.ZodObject<{
|
|
104
|
+
header: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
105
|
+
primary: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
106
|
+
footer: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
107
|
+
additionalInfo: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
108
|
+
back: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
109
|
+
}, z.core.$strict>;
|
|
110
|
+
export type PosterLayout = z.infer<typeof PosterLayoutSchema>;
|
|
111
|
+
export declare const FieldListSchema: z.ZodArray<z.ZodObject<{
|
|
112
|
+
id: z.ZodOptional<z.ZodString>;
|
|
113
|
+
key: z.ZodString;
|
|
114
|
+
label: z.ZodString;
|
|
115
|
+
binding: z.ZodEnum<{
|
|
116
|
+
shared: "shared";
|
|
117
|
+
perPass: "perPass";
|
|
118
|
+
event: "event";
|
|
119
|
+
stamps: "stamps";
|
|
120
|
+
points: "points";
|
|
121
|
+
stampsLeft: "stampsLeft";
|
|
122
|
+
pointsToGoal: "pointsToGoal";
|
|
123
|
+
}>;
|
|
124
|
+
required: z.ZodBoolean;
|
|
125
|
+
defaultValue: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>;
|
|
126
|
+
sampleValue: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>;
|
|
127
|
+
textAlignment: z.ZodOptional<z.ZodEnum<{
|
|
128
|
+
left: "left";
|
|
129
|
+
center: "center";
|
|
130
|
+
right: "right";
|
|
131
|
+
natural: "natural";
|
|
132
|
+
}>>;
|
|
133
|
+
event: z.ZodOptional<z.ZodEnum<{
|
|
134
|
+
name: "name";
|
|
135
|
+
venueName: "venueName";
|
|
136
|
+
venueAddress: "venueAddress";
|
|
137
|
+
doorsOpen: "doorsOpen";
|
|
138
|
+
start: "start";
|
|
139
|
+
end: "end";
|
|
140
|
+
}>>;
|
|
141
|
+
dateFormat: z.ZodOptional<z.ZodEnum<{
|
|
142
|
+
date: "date";
|
|
143
|
+
dateTime: "dateTime";
|
|
144
|
+
time: "time";
|
|
145
|
+
}>>;
|
|
146
|
+
googleRole: z.ZodOptional<z.ZodEnum<{
|
|
147
|
+
seat: "seat";
|
|
148
|
+
row: "row";
|
|
149
|
+
section: "section";
|
|
150
|
+
gate: "gate";
|
|
151
|
+
ticketType: "ticketType";
|
|
152
|
+
}>>;
|
|
153
|
+
}, z.core.$strict>>;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { FieldValueSchema, TextAlignmentSchema } from './primitives.js';
|
|
3
|
+
/**
|
|
4
|
+
* A field id is stable across revisions of one template. Its full identity is
|
|
5
|
+
* `(templateId, fieldId)`, so independent templates may use the same local id.
|
|
6
|
+
*/
|
|
7
|
+
export const FIELD_ID_RE = /^fld_[A-Za-z0-9_-]{1,64}$/;
|
|
8
|
+
export const FieldKeySchema = z
|
|
9
|
+
.string()
|
|
10
|
+
.regex(/^[A-Za-z0-9_]{1,64}$/, 'Field key must be 1–64 chars of [A-Za-z0-9_]');
|
|
11
|
+
export const EVENT_VALUES = ['name', 'venueName', 'venueAddress', 'doorsOpen', 'start', 'end'];
|
|
12
|
+
export const EVENT_TIME_VALUES = ['doorsOpen', 'start', 'end'];
|
|
13
|
+
export const EVENT_DATE_FORMATS = ['dateTime', 'date', 'time'];
|
|
14
|
+
/** Google shows these natively on an event ticket; `ticketType` is not a seat value. */
|
|
15
|
+
export const GOOGLE_ROLES = ['seat', 'row', 'section', 'gate', 'ticketType'];
|
|
16
|
+
export function isEventTime(value) {
|
|
17
|
+
return EVENT_TIME_VALUES.includes(value);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* One field on the pass face. `binding` is the shared-vs-per-holder boundary:
|
|
21
|
+
* - `shared` — the value is part of the design (same on every issued pass).
|
|
22
|
+
* - `perPass` — the value is supplied at issuance; `required` gates it then.
|
|
23
|
+
* - `event` — the value comes from the template's live event details (`event`),
|
|
24
|
+
* with `dateFormat` for a time; event ticket templates only.
|
|
25
|
+
* - `stamps` — the pass's stamp count as "7 / 10"; stamp card templates only.
|
|
26
|
+
* - `points` — the pass's balance as "350 / 1,000" (or "350" with no goal);
|
|
27
|
+
* points card templates only.
|
|
28
|
+
* - `stampsLeft` — stamps still to earn, as "3"; stamp card templates only.
|
|
29
|
+
* - `pointsToGoal` — points still to the goal, as "160"; points card templates
|
|
30
|
+
* with a goal only.
|
|
31
|
+
*
|
|
32
|
+
* `googleRole` makes a per-pass field Google's native seat, row, section, gate or
|
|
33
|
+
* ticket type on an event ticket; Apple shows it as an ordinary field.
|
|
34
|
+
*
|
|
35
|
+
* `defaultValue` is the production default; `sampleValue` is preview-only and
|
|
36
|
+
* never persisted into an issued pass. Keeping them apart stops sample data
|
|
37
|
+
* from leaking into real passes.
|
|
38
|
+
*/
|
|
39
|
+
export const FieldDefSchema = z
|
|
40
|
+
.strictObject({
|
|
41
|
+
id: z.string().regex(FIELD_ID_RE, 'Invalid field id').optional(),
|
|
42
|
+
key: FieldKeySchema,
|
|
43
|
+
label: z.string().min(1).max(64),
|
|
44
|
+
binding: z.enum(['shared', 'perPass', 'event', 'stamps', 'points', 'stampsLeft', 'pointsToGoal']),
|
|
45
|
+
required: z.boolean(),
|
|
46
|
+
defaultValue: FieldValueSchema.optional(),
|
|
47
|
+
sampleValue: FieldValueSchema.optional(),
|
|
48
|
+
textAlignment: TextAlignmentSchema.optional(),
|
|
49
|
+
event: z.enum(EVENT_VALUES).optional(),
|
|
50
|
+
dateFormat: z.enum(EVENT_DATE_FORMATS).optional(),
|
|
51
|
+
googleRole: z.enum(GOOGLE_ROLES).optional(),
|
|
52
|
+
})
|
|
53
|
+
.superRefine((f, ctx) => {
|
|
54
|
+
if (f.binding !== 'perPass' && f.required) {
|
|
55
|
+
ctx.addIssue({
|
|
56
|
+
code: 'custom',
|
|
57
|
+
message: 'Only a per-pass field can be required (the others have no issue-time input).',
|
|
58
|
+
path: ['required'],
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
if (f.binding === 'event') {
|
|
62
|
+
if (!f.event) {
|
|
63
|
+
ctx.addIssue({ code: 'custom', message: 'Pick which event detail this field shows.', path: ['event'] });
|
|
64
|
+
}
|
|
65
|
+
if (f.defaultValue !== undefined) {
|
|
66
|
+
ctx.addIssue({
|
|
67
|
+
code: 'custom',
|
|
68
|
+
message: 'An event field takes its value from the event details.',
|
|
69
|
+
path: ['defaultValue'],
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
if (f.dateFormat && f.event && !isEventTime(f.event)) {
|
|
73
|
+
ctx.addIssue({
|
|
74
|
+
code: 'custom',
|
|
75
|
+
message: 'Only an event time has a date format.',
|
|
76
|
+
path: ['dateFormat'],
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
else if (f.binding === 'stamps' || f.binding === 'stampsLeft') {
|
|
81
|
+
if (f.defaultValue !== undefined) {
|
|
82
|
+
ctx.addIssue({
|
|
83
|
+
code: 'custom',
|
|
84
|
+
message: f.binding === 'stamps'
|
|
85
|
+
? 'The stamp count field takes its value from the pass.'
|
|
86
|
+
: 'The stamps left field takes its value from the pass.',
|
|
87
|
+
path: ['defaultValue'],
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
else if (f.binding === 'points' || f.binding === 'pointsToGoal') {
|
|
92
|
+
if (f.defaultValue !== undefined) {
|
|
93
|
+
ctx.addIssue({
|
|
94
|
+
code: 'custom',
|
|
95
|
+
message: f.binding === 'points'
|
|
96
|
+
? 'The points field takes its value from the pass.'
|
|
97
|
+
: 'The points to goal field takes its value from the pass.',
|
|
98
|
+
path: ['defaultValue'],
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (f.binding !== 'event' && (f.event !== undefined || f.dateFormat !== undefined)) {
|
|
103
|
+
ctx.addIssue({
|
|
104
|
+
code: 'custom',
|
|
105
|
+
message: 'Only an event field shows an event detail.',
|
|
106
|
+
path: ['event'],
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
if (f.googleRole && f.binding !== 'perPass') {
|
|
110
|
+
ctx.addIssue({
|
|
111
|
+
code: 'custom',
|
|
112
|
+
message: 'Only a per-pass field can have a Google role.',
|
|
113
|
+
path: ['googleRole'],
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
export const MAX_FIELDS = 40;
|
|
118
|
+
export const STANDARD_SLOTS = ['header', 'primary', 'secondary', 'auxiliary', 'back'];
|
|
119
|
+
export const POSTER_SLOTS = ['header', 'primary', 'footer', 'additionalInfo', 'back'];
|
|
120
|
+
const slotKeys = (max) => z.array(FieldKeySchema).max(max).default([]);
|
|
121
|
+
/**
|
|
122
|
+
* The Apple style block (`generic` / `eventTicket`) that older iOS shows, and the
|
|
123
|
+
* basis of the whole Google pass. Caps mirror the wallet layouts; passmint
|
|
124
|
+
* enforces the authoritative native limits at compile time.
|
|
125
|
+
*/
|
|
126
|
+
export const StandardLayoutSchema = z.strictObject({
|
|
127
|
+
header: slotKeys(3),
|
|
128
|
+
primary: slotKeys(1),
|
|
129
|
+
secondary: slotKeys(2),
|
|
130
|
+
auxiliary: slotKeys(2),
|
|
131
|
+
back: slotKeys(20),
|
|
132
|
+
});
|
|
133
|
+
/** iOS 27 poster layout. Caps are what Wallet showed on an iOS 27 device. */
|
|
134
|
+
export const PosterLayoutSchema = z.strictObject({
|
|
135
|
+
header: slotKeys(1),
|
|
136
|
+
primary: slotKeys(4),
|
|
137
|
+
footer: slotKeys(1),
|
|
138
|
+
additionalInfo: slotKeys(8),
|
|
139
|
+
back: slotKeys(20),
|
|
140
|
+
});
|
|
141
|
+
export const FieldListSchema = z.array(FieldDefSchema).max(MAX_FIELDS);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Same shape as `common/ids` `newId`, using Web Crypto so it also runs in the browser. */
|
|
2
|
+
export function newFieldId(prefix) {
|
|
3
|
+
const bytes = crypto.getRandomValues(new Uint8Array(15));
|
|
4
|
+
const base64 = btoa(String.fromCharCode(...bytes));
|
|
5
|
+
return `${prefix}_${base64.replace(/\+/g, '-').replace(/\//g, '_')}`;
|
|
6
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from './primitives.js';
|
|
2
|
+
export * from './fields.js';
|
|
3
|
+
export * from './featured-actions.js';
|
|
4
|
+
export * from './relevance.js';
|
|
5
|
+
export * from './event.js';
|
|
6
|
+
export * from './stamps.js';
|
|
7
|
+
export * from './points.js';
|
|
8
|
+
export * from './definition.js';
|
|
9
|
+
export * from './upgrade.js';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from './primitives.js';
|
|
2
|
+
export * from './fields.js';
|
|
3
|
+
export * from './featured-actions.js';
|
|
4
|
+
export * from './relevance.js';
|
|
5
|
+
export * from './event.js';
|
|
6
|
+
export * from './stamps.js';
|
|
7
|
+
export * from './points.js';
|
|
8
|
+
export * from './definition.js';
|
|
9
|
+
export * from './upgrade.js';
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/** The largest balance, limit or change a points card stores (`DECIMAL(9,2)`). */
|
|
3
|
+
export declare const MAX_POINTS = 9999999.99;
|
|
4
|
+
export declare const PointsAmountSchema: z.ZodNumber;
|
|
5
|
+
/**
|
|
6
|
+
* A points card's range and starting balance. Every limit is optional. The goal
|
|
7
|
+
* caps the balance; `max` only limits it when there is no goal, so a goal above
|
|
8
|
+
* it is rejected rather than ignored.
|
|
9
|
+
*/
|
|
10
|
+
export declare const PointsSettingsSchema: z.ZodObject<{
|
|
11
|
+
min: z.ZodOptional<z.ZodNumber>;
|
|
12
|
+
max: z.ZodOptional<z.ZodNumber>;
|
|
13
|
+
goal: z.ZodOptional<z.ZodNumber>;
|
|
14
|
+
starting: z.ZodNumber;
|
|
15
|
+
}, z.core.$strict>;
|
|
16
|
+
export type PointsSettings = z.infer<typeof PointsSettingsSchema>;
|
|
17
|
+
/**
|
|
18
|
+
* What a points change that notifies says. `{balance}` stands for the balance
|
|
19
|
+
* text ("350 / 1,000") and `{change}` for the requested amount ("+50"). Apple's
|
|
20
|
+
* message is the points field's change message, so it must contain `{balance}`;
|
|
21
|
+
* without a points field it fills the pass's message field instead. The optional
|
|
22
|
+
* `goal…` texts replace them when the change reaches the goal; Apple's always
|
|
23
|
+
* goes in the message field, so it needs no `{balance}`.
|
|
24
|
+
*/
|
|
25
|
+
export declare const PointsNotificationsSchema: z.ZodObject<{
|
|
26
|
+
apple: z.ZodOptional<z.ZodString>;
|
|
27
|
+
googleTitle: z.ZodOptional<z.ZodString>;
|
|
28
|
+
googleBody: z.ZodOptional<z.ZodString>;
|
|
29
|
+
goalApple: z.ZodOptional<z.ZodString>;
|
|
30
|
+
goalGoogleTitle: z.ZodOptional<z.ZodString>;
|
|
31
|
+
goalGoogleBody: z.ZodOptional<z.ZodString>;
|
|
32
|
+
}, z.core.$strict>;
|
|
33
|
+
export type PointsNotifications = z.infer<typeof PointsNotificationsSchema>;
|
|
34
|
+
/** The highest balance: the goal when set, else the maximum. */
|
|
35
|
+
export declare function pointsCap(points: Pick<PointsSettings, 'goal' | 'max'>): number | undefined;
|
|
36
|
+
/** "1,250", "12.50", "-9,999,999.99": decimals only on numbers that have them. */
|
|
37
|
+
export declare function formatPoints(value: number): string;
|
|
38
|
+
/** "350 / 1,000" with a goal, "350" without. */
|
|
39
|
+
export declare function pointsBalanceText(balance: number, goal: number | undefined): string;
|
|
40
|
+
/** A change with its sign: "+50", "−20" (a true minus, as device-tested). */
|
|
41
|
+
export declare function pointsChangeText(change: number): string;
|