@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.
Files changed (38) hide show
  1. package/README.md +13 -2
  2. package/dist/compiler/event-ticket.d.ts +38 -0
  3. package/dist/compiler/event-ticket.js +131 -0
  4. package/dist/compiler/loyalty-card.d.ts +28 -0
  5. package/dist/compiler/loyalty-card.js +70 -0
  6. package/dist/compiler/passmint-compiler.d.ts +118 -0
  7. package/dist/compiler/passmint-compiler.js +507 -0
  8. package/dist/compiler/preview-model.d.ts +123 -0
  9. package/dist/compiler/preview-model.js +402 -0
  10. package/dist/compiler/stamp-card.d.ts +23 -0
  11. package/dist/compiler/stamp-card.js +19 -0
  12. package/dist/compiler/template-preview.d.ts +34 -0
  13. package/dist/compiler/template-preview.js +125 -0
  14. package/dist/contract/definition.d.ts +1316 -0
  15. package/dist/contract/definition.js +854 -0
  16. package/dist/contract/event.d.ts +53 -0
  17. package/dist/contract/event.js +91 -0
  18. package/dist/contract/featured-actions.d.ts +45 -0
  19. package/dist/contract/featured-actions.js +81 -0
  20. package/dist/contract/fields.d.ts +153 -0
  21. package/dist/contract/fields.js +141 -0
  22. package/dist/contract/ids.d.ts +2 -0
  23. package/dist/contract/ids.js +6 -0
  24. package/dist/contract/index.d.ts +9 -0
  25. package/dist/contract/index.js +9 -0
  26. package/dist/contract/points.d.ts +41 -0
  27. package/dist/contract/points.js +86 -0
  28. package/dist/contract/primitives.d.ts +26 -0
  29. package/dist/contract/primitives.js +14 -0
  30. package/dist/contract/relevance.d.ts +54 -0
  31. package/dist/contract/relevance.js +52 -0
  32. package/dist/contract/stamps.d.ts +91 -0
  33. package/dist/contract/stamps.js +89 -0
  34. package/dist/contract/upgrade.d.ts +7 -0
  35. package/dist/contract/upgrade.js +39 -0
  36. package/dist/public-api.d.ts +4 -0
  37. package/dist/public-api.js +2 -0
  38. package/package.json +20 -4
package/README.md CHANGED
@@ -1,3 +1,14 @@
1
- # Temporary Holding Version
1
+ # @owask/tapace-templates
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ TapAce's template contract and wallet preview compiler. The source lives in
4
+ `tapace-backend/src/templates` (`contract/`, `compiler/`, `public-api.ts`); this
5
+ folder only builds and publishes it, so the dashboard previews a design with the
6
+ same code the API uses to validate and issue passes.
7
+
8
+ ```bash
9
+ npm run templates:build
10
+ npm run templates:publish
11
+ ```
12
+
13
+ Bump `version` here before publishing, then update the dependency in
14
+ `tapace-frontend`.
@@ -0,0 +1,38 @@
1
+ import type { GoogleEvent, SemanticTags } from 'passmint';
2
+ import { type EventDetails, type FieldDef, type FieldValue, type TemplateDefinition } from '../contract/index.js';
3
+ type Mode = 'preview' | 'issue';
4
+ type ValueOf = (field: FieldDef) => FieldValue | undefined;
5
+ /** The event detail an event-bound field shows, or undefined when the event lacks it. */
6
+ export declare function eventValue(field: FieldDef, event: EventDetails | undefined): string | undefined;
7
+ /**
8
+ * Apple formats an event time on the device. `ignoresTimeZone` keeps the venue's
9
+ * local time, which is what a ticket shows wherever the holder is.
10
+ */
11
+ export declare function appleDateStyle(field: FieldDef): {
12
+ ignoresTimeZone: boolean;
13
+ dateStyle: "medium" | "none";
14
+ timeStyle: "short" | "none";
15
+ } | undefined;
16
+ /** Event and seat metadata: Apple's semantic tags, and the source of Google's event ticket. */
17
+ export declare function eventSemantics(def: TemplateDefinition, valueOf: ValueOf): SemanticTags | undefined;
18
+ /** The Google-only part of an event ticket. */
19
+ export declare function googleEvent(def: TemplateDefinition, valueOf: ValueOf, holderName: string | undefined): GoogleEvent | undefined;
20
+ /** Within this distance Google's geofence treats a point as the venue itself. */
21
+ export declare const NEAR_VENUE_METRES = 50;
22
+ /**
23
+ * The pass's Lock Screen locations as Google `merchantLocations`. An event
24
+ * ticket's venue is already on its Google class, so points at the venue are left
25
+ * out. Google writes its own notification text, so messages are not sent.
26
+ */
27
+ export declare function googleLocations(def: TemplateDefinition, locations: readonly {
28
+ latitude: number;
29
+ longitude: number;
30
+ }[]): {
31
+ latitude: number;
32
+ longitude: number;
33
+ }[];
34
+ /** Google shows the event and the seat natively, so their fields aren't text rows. */
35
+ export declare function googleShowsNatively(def: TemplateDefinition, field: FieldDef): boolean;
36
+ /** An event-bound field's value: the event's, else a preview sample. */
37
+ export declare function eventFieldValue(def: TemplateDefinition, field: FieldDef, mode: Mode): FieldValue | undefined;
38
+ export {};
@@ -0,0 +1,131 @@
1
+ import { isEventTime, resolveCardItem, } from '../contract/index.js';
2
+ /** The event detail an event-bound field shows, or undefined when the event lacks it. */
3
+ export function eventValue(field, event) {
4
+ switch (field.event) {
5
+ case 'name':
6
+ return event?.name;
7
+ case 'venueName':
8
+ return event?.venue?.name;
9
+ case 'venueAddress':
10
+ return event?.venue?.address;
11
+ case 'doorsOpen':
12
+ case 'start':
13
+ case 'end':
14
+ return event?.[field.event];
15
+ default:
16
+ return undefined;
17
+ }
18
+ }
19
+ const APPLE_STYLES = {
20
+ dateTime: { dateStyle: 'medium', timeStyle: 'short' },
21
+ date: { dateStyle: 'medium', timeStyle: 'none' },
22
+ time: { dateStyle: 'none', timeStyle: 'short' },
23
+ };
24
+ /**
25
+ * Apple formats an event time on the device. `ignoresTimeZone` keeps the venue's
26
+ * local time, which is what a ticket shows wherever the holder is.
27
+ */
28
+ export function appleDateStyle(field) {
29
+ if (field.binding !== 'event' || !field.event || !isEventTime(field.event))
30
+ return undefined;
31
+ return { ...APPLE_STYLES[field.dateFormat ?? 'dateTime'], ignoresTimeZone: true };
32
+ }
33
+ const GOOGLE_DATE_FORMATS = {
34
+ dateTime: 'DATE_TIME',
35
+ date: 'DATE_ONLY',
36
+ time: 'TIME_ONLY',
37
+ };
38
+ function roleField(def, role) {
39
+ return def.config.fields.find((f) => f.googleRole === role);
40
+ }
41
+ function text(value) {
42
+ if (value === undefined)
43
+ return undefined;
44
+ const out = String(value).trim();
45
+ return out === '' ? undefined : out;
46
+ }
47
+ /** Event and seat metadata: Apple's semantic tags, and the source of Google's event ticket. */
48
+ export function eventSemantics(def, valueOf) {
49
+ if (def.type !== 'event_ticket')
50
+ return undefined;
51
+ const event = def.event;
52
+ const role = (r) => {
53
+ const field = roleField(def, r);
54
+ return field ? text(valueOf(field)) : undefined;
55
+ };
56
+ const seat = {
57
+ ...(role('seat') ? { seatNumber: role('seat') } : {}),
58
+ ...(role('row') ? { seatRow: role('row') } : {}),
59
+ ...(role('section') ? { seatSection: role('section') } : {}),
60
+ };
61
+ const tags = {
62
+ ...(event?.name ? { eventName: event.name } : {}),
63
+ ...(event?.venue?.name ? { venueName: event.venue.name } : {}),
64
+ ...(event?.venue?.location ? { venueLocation: event.venue.location } : {}),
65
+ ...(event?.doorsOpen ? { venueDoorsOpenDate: event.doorsOpen } : {}),
66
+ ...(event?.start ? { eventStartDate: event.start } : {}),
67
+ ...(event?.end ? { eventEndDate: event.end } : {}),
68
+ ...(role('gate') ? { venueEntrance: role('gate') } : {}),
69
+ ...(Object.keys(seat).length > 0 ? { seats: [seat] } : {}),
70
+ };
71
+ return Object.keys(tags).length > 0 ? tags : undefined;
72
+ }
73
+ function cardItem(item) {
74
+ if (item.kind === 'seat')
75
+ return { seat: item.role };
76
+ if (item.kind === 'event')
77
+ return { event: item.event, format: GOOGLE_DATE_FORMATS[item.format] };
78
+ return { field: item.field.key };
79
+ }
80
+ /** The Google-only part of an event ticket. */
81
+ export function googleEvent(def, valueOf, holderName) {
82
+ if (def.type !== 'event_ticket')
83
+ return undefined;
84
+ const ticketTypeField = roleField(def, 'ticketType');
85
+ const ticketType = ticketTypeField ? text(valueOf(ticketTypeField)) : undefined;
86
+ const cardRows = def.google.cardRows?.map((row) => row.flatMap((ref) => {
87
+ const item = resolveCardItem(ref, def.config.fields);
88
+ return item ? [cardItem(item)] : [];
89
+ }));
90
+ return {
91
+ ...(def.event?.venue?.address ? { venueAddress: def.event.venue.address } : {}),
92
+ ...(def.event?.doorsOpenLabel ? { doorsOpenLabel: def.event.doorsOpenLabel } : {}),
93
+ ...(text(holderName) ? { ticketHolderName: text(holderName) } : {}),
94
+ ...(ticketType ? { ticketType } : {}),
95
+ ...(cardRows?.length ? { cardRows } : {}),
96
+ };
97
+ }
98
+ /** Within this distance Google's geofence treats a point as the venue itself. */
99
+ export const NEAR_VENUE_METRES = 50;
100
+ function metresBetween(a, b) {
101
+ const rad = Math.PI / 180;
102
+ const dLat = (b.latitude - a.latitude) * rad;
103
+ const dLng = (b.longitude - a.longitude) * rad;
104
+ const h = Math.sin(dLat / 2) ** 2 +
105
+ Math.cos(a.latitude * rad) * Math.cos(b.latitude * rad) * Math.sin(dLng / 2) ** 2;
106
+ return 2 * 6_371_000 * Math.asin(Math.sqrt(h));
107
+ }
108
+ /**
109
+ * The pass's Lock Screen locations as Google `merchantLocations`. An event
110
+ * ticket's venue is already on its Google class, so points at the venue are left
111
+ * out. Google writes its own notification text, so messages are not sent.
112
+ */
113
+ export function googleLocations(def, locations) {
114
+ const venue = def.type === 'event_ticket' ? def.event?.venue?.location : undefined;
115
+ return locations
116
+ .filter((point) => !venue || metresBetween(point, venue) > NEAR_VENUE_METRES)
117
+ .map(({ latitude, longitude }) => ({ latitude, longitude }));
118
+ }
119
+ /** Google shows the event and the seat natively, so their fields aren't text rows. */
120
+ export function googleShowsNatively(def, field) {
121
+ return def.type === 'event_ticket' && (field.binding === 'event' || field.googleRole !== undefined);
122
+ }
123
+ /** An event-bound field's value: the event's, else a preview sample. */
124
+ export function eventFieldValue(def, field, mode) {
125
+ const value = eventValue(field, def.event);
126
+ if (value !== undefined)
127
+ return value;
128
+ if (mode === 'preview')
129
+ return field.sampleValue ?? '';
130
+ return undefined;
131
+ }
@@ -0,0 +1,28 @@
1
+ import type { GoogleLoyalty } from 'passmint';
2
+ import { type FieldDef, type FieldValue, type TemplateDefinition } from '../contract/index.js';
3
+ export type LoyaltyCardDefinition = Extract<TemplateDefinition, {
4
+ type: 'stamp_card' | 'points_card';
5
+ }>;
6
+ type ValueOf = (field: FieldDef) => FieldValue | undefined;
7
+ export declare function isLoyaltyCard(def: TemplateDefinition): def is LoyaltyCardDefinition;
8
+ /**
9
+ * The balance a loyalty card shows, in its field and as Google's balance: the stamp
10
+ * count ("7 / 10") or the points ("350 / 1,000"). A compile without a points balance
11
+ * shows the template's starting balance.
12
+ */
13
+ export declare function loyaltyBalanceText(def: TemplateDefinition, stampCount: number, pointsBalance: number | undefined): string | undefined;
14
+ export type LoyaltyFieldTexts = Partial<Record<'stamps' | 'points' | 'stampsLeft' | 'pointsToGoal', string>>;
15
+ /** What each loyalty-bound field shows on this pass. */
16
+ export declare function loyaltyFieldTexts(def: TemplateDefinition, stampCount: number, pointsBalance: number | undefined): LoyaltyFieldTexts;
17
+ /** Google shows the balance as its loyalty balance and the member fields as the account. */
18
+ export declare function loyaltyShowsNatively(def: TemplateDefinition, field: FieldDef): boolean;
19
+ /** The balance field's label (else "Stamps" / "Points") and the Member name field. */
20
+ export declare function googleLoyalty(def: LoyaltyCardDefinition, balance: string, valueOf: ValueOf): GoogleLoyalty;
21
+ /** Google's account id, from the Member ID field; undefined keeps passmint's (the serial). */
22
+ export declare function googleAccountId(def: LoyaltyCardDefinition, valueOf: ValueOf): string | undefined;
23
+ /** The loyalty class's names: the Google-only texts, else logo text, else the organization. */
24
+ export declare function googleProgramNames(def: LoyaltyCardDefinition): {
25
+ issuerName: string;
26
+ programName: string;
27
+ };
28
+ export {};
@@ -0,0 +1,70 @@
1
+ import { formatPoints, pointsBalanceText, stampCountText, } from '../contract/index.js';
2
+ const BALANCE_FIELD = {
3
+ stamp_card: { binding: 'stamps', label: 'Stamps' },
4
+ points_card: { binding: 'points', label: 'Points' },
5
+ };
6
+ export function isLoyaltyCard(def) {
7
+ return def.type === 'stamp_card' || def.type === 'points_card';
8
+ }
9
+ /**
10
+ * The balance a loyalty card shows, in its field and as Google's balance: the stamp
11
+ * count ("7 / 10") or the points ("350 / 1,000"). A compile without a points balance
12
+ * shows the template's starting balance.
13
+ */
14
+ export function loyaltyBalanceText(def, stampCount, pointsBalance) {
15
+ if (def.type === 'stamp_card')
16
+ return stampCountText(stampCount, def.config.stamps.total);
17
+ if (def.type === 'points_card') {
18
+ return pointsBalanceText(pointsBalance ?? def.config.points.starting, def.config.points.goal);
19
+ }
20
+ return undefined;
21
+ }
22
+ /** What each loyalty-bound field shows on this pass. */
23
+ export function loyaltyFieldTexts(def, stampCount, pointsBalance) {
24
+ const balance = loyaltyBalanceText(def, stampCount, pointsBalance);
25
+ if (def.type === 'stamp_card') {
26
+ return { stamps: balance, stampsLeft: String(Math.max(def.config.stamps.total - stampCount, 0)) };
27
+ }
28
+ if (def.type === 'points_card') {
29
+ const { goal, starting } = def.config.points;
30
+ return {
31
+ points: balance,
32
+ ...(goal === undefined ? {} : { pointsToGoal: formatPoints(Math.max(goal - (pointsBalance ?? starting), 0)) }),
33
+ };
34
+ }
35
+ return {};
36
+ }
37
+ /** Google shows the balance as its loyalty balance and the member fields as the account. */
38
+ export function loyaltyShowsNatively(def, field) {
39
+ return (isLoyaltyCard(def) &&
40
+ (field.binding === BALANCE_FIELD[def.type].binding ||
41
+ field.key === def.google.memberIdField ||
42
+ field.key === def.google.memberNameField));
43
+ }
44
+ function memberValue(def, key, valueOf) {
45
+ const field = key === undefined ? undefined : def.config.fields.find((f) => f.key === key);
46
+ const value = field ? valueOf(field) : undefined;
47
+ const text = value === undefined ? '' : String(value).trim();
48
+ return text === '' ? undefined : text;
49
+ }
50
+ /** The balance field's label (else "Stamps" / "Points") and the Member name field. */
51
+ export function googleLoyalty(def, balance, valueOf) {
52
+ const { binding, label } = BALANCE_FIELD[def.type];
53
+ const balanceField = def.config.fields.find((f) => f.binding === binding);
54
+ return {
55
+ points: { label: balanceField?.label ?? label, balance },
56
+ accountName: memberValue(def, def.google.memberNameField, valueOf) ?? null,
57
+ };
58
+ }
59
+ /** Google's account id, from the Member ID field; undefined keeps passmint's (the serial). */
60
+ export function googleAccountId(def, valueOf) {
61
+ return memberValue(def, def.google.memberIdField, valueOf);
62
+ }
63
+ /** The loyalty class's names: the Google-only texts, else logo text, else the organization. */
64
+ export function googleProgramNames(def) {
65
+ const fallback = def.logoText?.trim() || def.organizationName;
66
+ return {
67
+ issuerName: def.google.issuerName ?? fallback,
68
+ programName: def.google.programName ?? fallback,
69
+ };
70
+ }
@@ -0,0 +1,118 @@
1
+ import { type PassInput } from 'passmint';
2
+ import { type BarcodeConfig, type BarcodeFormat, type FieldDef, type FieldValue, type Relevance, type TemplateDefinition } from '../contract/index.js';
3
+ import { type StampRender } from './stamp-card.js';
4
+ export type { StampRender } from './stamp-card.js';
5
+ /**
6
+ * This module is the ONLY place that imports `passmint`. It resolves a
7
+ * validated TemplateDefinition (plus resolved assets and, at issuance, per-pass
8
+ * values) into a concrete passmint pass — the adapter boundary the rest of the
9
+ * app depends on. Signing lives in a later feature; here we only build/render.
10
+ */
11
+ export type Platform = 'apple' | 'google';
12
+ /** Apple needs inline bytes; Google needs an HTTPS URL. A slot must provide the one its platform uses. */
13
+ export interface ResolvedAsset {
14
+ bytes?: Uint8Array;
15
+ url?: string;
16
+ /** The image fitted to the poster logo box (126×30 pt), when this asset is the poster logo. */
17
+ posterLogoBytes?: Uint8Array;
18
+ }
19
+ export type ResolvedAssets = Record<string, ResolvedAsset>;
20
+ /** Apple signing identity is injected at issuance (Feature 2); previews use placeholders. */
21
+ export interface IssuanceContext {
22
+ passTypeIdentifier: string;
23
+ teamIdentifier: string;
24
+ serialNumber: string;
25
+ }
26
+ export declare const PREVIEW_ISSUANCE: IssuanceContext;
27
+ export interface CompileOptions {
28
+ platform: Platform;
29
+ assets: ResolvedAssets;
30
+ values?: Record<string, FieldValue>;
31
+ barcodeValue?: string;
32
+ /** Per-pass Featured Action links by action id. */
33
+ actionLinks?: Record<string, string>;
34
+ /** The pass's own Lock Screen relevance; its lists replace the template's. Apple-only. */
35
+ relevance?: Relevance | null;
36
+ /** The pass holder; Google event tickets show it as the ticket holder. */
37
+ holderName?: string | null;
38
+ mode?: 'preview' | 'issue';
39
+ issuance?: IssuanceContext;
40
+ /** Numeric Google Wallet issuer id. Placeholder in preview; real value at issuance (Feature 2). */
41
+ issuerId?: string;
42
+ /** Google class id suffix (`{issuerId}.{classSuffix}`). One stable class per template. */
43
+ classSuffix?: string;
44
+ /** Google object id suffix (`{issuerId}.{objectSuffix}`). The globally unique pass id. */
45
+ objectSuffix?: string;
46
+ /** Apple-only; ignored for Google. Embedding it makes the pass register for updates. */
47
+ webService?: {
48
+ url: string;
49
+ authToken: string;
50
+ };
51
+ /** Apple-only `changeMessage` per field id; each must contain `%@`. */
52
+ changeMessages?: Record<string, string>;
53
+ /** Apple-only. When set, the reserved notification fields lead the back of the pass. */
54
+ walletMessages?: WalletMessages;
55
+ /** Apple `voided: true`; Google object state `INACTIVE`. */
56
+ voided?: boolean;
57
+ /** A stamp card's count and drawn stamps; stand-ins are used when absent. */
58
+ stamps?: StampRender;
59
+ /** A points card's balance; the template's starting balance when absent. */
60
+ points?: number;
61
+ }
62
+ export interface WalletMessages {
63
+ announcement?: string;
64
+ personal?: string;
65
+ }
66
+ export declare const WALLET_MESSAGE_FIELDS: readonly [{
67
+ readonly kind: "announcement";
68
+ readonly key: "tapace_announcement";
69
+ readonly label: "Announcements";
70
+ readonly placeholder: "No announcements yet";
71
+ }, {
72
+ readonly kind: "personal";
73
+ readonly key: "tapace_message";
74
+ readonly label: "Message for you";
75
+ readonly placeholder: "No messages yet";
76
+ }];
77
+ export declare class AssetResolutionError extends Error {
78
+ }
79
+ /**
80
+ * Stand-in assets for structural validation (field counts, colors, required
81
+ * icon, barcode) without reading real bytes. Ownership + image validity are
82
+ * checked separately at upload/reference time; real bytes/URLs are needed only
83
+ * at signing (a later feature).
84
+ */
85
+ export declare function placeholderAssets(ids: string[]): ResolvedAssets;
86
+ export declare function resolveFieldValue(field: FieldDef, values: Record<string, FieldValue>, mode: 'preview' | 'issue'): FieldValue | undefined;
87
+ /**
88
+ * The barcode message an already-issued pass renders, honoring the `passValue`
89
+ * serial-number fallback. Shared by the sign path and the deployment engine so the
90
+ * fallback lives in one place.
91
+ */
92
+ export declare function resolveIssuedBarcodeMessage(barcode: BarcodeConfig, barcodeValue: string | null | undefined, serialNumber: string): string;
93
+ /**
94
+ * Whether passmint accepts a barcode payload for a format/encoding — used by the
95
+ * deployment engine to decide when a format/encoding change makes an existing
96
+ * payload unrenderable (omit until corrected). Everything but the barcode is a
97
+ * valid placeholder, so a schema error isolates the barcode.
98
+ */
99
+ export declare function barcodeRenderable(format: BarcodeFormat, message: string, messageEncoding?: string): boolean;
100
+ /** Build the validated passmint pass for one platform. Throws PassmintSchemaError on invalid mappings. */
101
+ export declare function compile(def: TemplateDefinition, options: CompileOptions): PassInput;
102
+ /** The resolved barcode payload for preview/display, or undefined if no barcode. */
103
+ export declare function previewBarcodeMessage(def: TemplateDefinition): string | undefined;
104
+ export declare function renderApple(def: TemplateDefinition, options: Omit<CompileOptions, 'platform'>): import("passmint").AppleRenderedPass;
105
+ export declare function renderGoogle(def: TemplateDefinition, options: Omit<CompileOptions, 'platform'>): import("passmint").GoogleSavePayload;
106
+ export interface CompileIssue {
107
+ /** `template` marks a structural (pre-compile) issue that is not platform-specific. */
108
+ platform: Platform | 'template';
109
+ path: string;
110
+ message: string;
111
+ }
112
+ /**
113
+ * Compile each enabled platform and collect passmint's own schema issues.
114
+ * Used by the preview/validate endpoint so the designer surfaces native-limit
115
+ * violations (field counts, missing icon, bad color) the way it surfaces ours.
116
+ */
117
+ export declare function collectCompileIssues(def: TemplateDefinition, options: Pick<CompileOptions, 'assets' | 'values' | 'mode' | 'issuance'>): CompileIssue[];
118
+ export declare function toCompileIssues(platform: Platform, err: unknown): CompileIssue[];