@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,86 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/** The largest balance, limit or change a points card stores (`DECIMAL(9,2)`). */
|
|
3
|
+
export const MAX_POINTS = 9_999_999.99;
|
|
4
|
+
function hasAtMostTwoDecimals(value) {
|
|
5
|
+
const cents = value * 100;
|
|
6
|
+
return Math.abs(cents - Math.round(cents)) < 1e-6;
|
|
7
|
+
}
|
|
8
|
+
const outOfBounds = 'Points must be between -9,999,999.99 and 9,999,999.99.';
|
|
9
|
+
export const PointsAmountSchema = z
|
|
10
|
+
.number()
|
|
11
|
+
.min(-MAX_POINTS, outOfBounds)
|
|
12
|
+
.max(MAX_POINTS, outOfBounds)
|
|
13
|
+
.refine(hasAtMostTwoDecimals, 'Points can have at most 2 decimal places.');
|
|
14
|
+
/**
|
|
15
|
+
* A points card's range and starting balance. Every limit is optional. The goal
|
|
16
|
+
* caps the balance; `max` only limits it when there is no goal, so a goal above
|
|
17
|
+
* it is rejected rather than ignored.
|
|
18
|
+
*/
|
|
19
|
+
export const PointsSettingsSchema = z
|
|
20
|
+
.strictObject({
|
|
21
|
+
min: PointsAmountSchema.optional(),
|
|
22
|
+
max: PointsAmountSchema.optional(),
|
|
23
|
+
goal: PointsAmountSchema.optional(),
|
|
24
|
+
starting: PointsAmountSchema,
|
|
25
|
+
})
|
|
26
|
+
.superRefine((p, ctx) => {
|
|
27
|
+
if (p.goal !== undefined && p.max !== undefined && p.goal > p.max) {
|
|
28
|
+
ctx.addIssue({ code: 'custom', message: 'The goal can be at most the maximum.', path: ['goal'] });
|
|
29
|
+
}
|
|
30
|
+
const top = pointsCap(p);
|
|
31
|
+
if (p.min !== undefined && top !== undefined && p.min > top) {
|
|
32
|
+
ctx.addIssue({
|
|
33
|
+
code: 'custom',
|
|
34
|
+
message: p.goal !== undefined ? 'The minimum can be at most the goal.' : 'The minimum can be at most the maximum.',
|
|
35
|
+
path: ['min'],
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
if (p.min !== undefined && p.starting < p.min) {
|
|
39
|
+
ctx.addIssue({ code: 'custom', message: 'The starting balance is below the minimum.', path: ['starting'] });
|
|
40
|
+
}
|
|
41
|
+
if (top !== undefined && p.starting > top) {
|
|
42
|
+
ctx.addIssue({
|
|
43
|
+
code: 'custom',
|
|
44
|
+
message: p.goal !== undefined ? 'The starting balance is above the goal.' : 'The starting balance is above the maximum.',
|
|
45
|
+
path: ['starting'],
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
const notificationText = z.string().trim().min(1).max(200);
|
|
50
|
+
/**
|
|
51
|
+
* What a points change that notifies says. `{balance}` stands for the balance
|
|
52
|
+
* text ("350 / 1,000") and `{change}` for the requested amount ("+50"). Apple's
|
|
53
|
+
* message is the points field's change message, so it must contain `{balance}`;
|
|
54
|
+
* without a points field it fills the pass's message field instead. The optional
|
|
55
|
+
* `goal…` texts replace them when the change reaches the goal; Apple's always
|
|
56
|
+
* goes in the message field, so it needs no `{balance}`.
|
|
57
|
+
*/
|
|
58
|
+
export const PointsNotificationsSchema = z.strictObject({
|
|
59
|
+
apple: notificationText
|
|
60
|
+
.refine((text) => text.includes('{balance}'), 'Include {balance} where the balance goes.')
|
|
61
|
+
.optional(),
|
|
62
|
+
googleTitle: notificationText.optional(),
|
|
63
|
+
googleBody: notificationText.optional(),
|
|
64
|
+
goalApple: notificationText.optional(),
|
|
65
|
+
goalGoogleTitle: notificationText.optional(),
|
|
66
|
+
goalGoogleBody: notificationText.optional(),
|
|
67
|
+
});
|
|
68
|
+
/** The highest balance: the goal when set, else the maximum. */
|
|
69
|
+
export function pointsCap(points) {
|
|
70
|
+
return points.goal ?? points.max;
|
|
71
|
+
}
|
|
72
|
+
/** "1,250", "12.50", "-9,999,999.99": decimals only on numbers that have them. */
|
|
73
|
+
export function formatPoints(value) {
|
|
74
|
+
return new Intl.NumberFormat('en-US', {
|
|
75
|
+
minimumFractionDigits: Number.isInteger(value) ? 0 : 2,
|
|
76
|
+
maximumFractionDigits: 2,
|
|
77
|
+
}).format(value);
|
|
78
|
+
}
|
|
79
|
+
/** "350 / 1,000" with a goal, "350" without. */
|
|
80
|
+
export function pointsBalanceText(balance, goal) {
|
|
81
|
+
return goal === undefined ? formatPoints(balance) : `${formatPoints(balance)} / ${formatPoints(goal)}`;
|
|
82
|
+
}
|
|
83
|
+
/** A change with its sign: "+50", "−20" (a true minus, as device-tested). */
|
|
84
|
+
export function pointsChangeText(change) {
|
|
85
|
+
return change < 0 ? `−${formatPoints(-change)}` : `+${formatPoints(change)}`;
|
|
86
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/** `#RGB` or `#RRGGBB`. Passmint also accepts `rgb(...)`; TapAce standardises on hex. */
|
|
3
|
+
export declare const HexColorSchema: z.ZodString;
|
|
4
|
+
/** An `asset_…` id owned by the same organization as the template. */
|
|
5
|
+
export declare const AssetRefSchema: z.ZodString;
|
|
6
|
+
export declare const BarcodeFormatSchema: z.ZodEnum<{
|
|
7
|
+
qr: "qr";
|
|
8
|
+
pdf417: "pdf417";
|
|
9
|
+
aztec: "aztec";
|
|
10
|
+
code128: "code128";
|
|
11
|
+
}>;
|
|
12
|
+
export type BarcodeFormat = z.infer<typeof BarcodeFormatSchema>;
|
|
13
|
+
export declare const BarcodeEncodingSchema: z.ZodEnum<{
|
|
14
|
+
"iso-8859-1": "iso-8859-1";
|
|
15
|
+
"utf-8": "utf-8";
|
|
16
|
+
"utf-16": "utf-16";
|
|
17
|
+
}>;
|
|
18
|
+
export declare const TextAlignmentSchema: z.ZodEnum<{
|
|
19
|
+
left: "left";
|
|
20
|
+
center: "center";
|
|
21
|
+
right: "right";
|
|
22
|
+
natural: "natural";
|
|
23
|
+
}>;
|
|
24
|
+
/** A field value. Passmint accepts string or number; TapAce preserves both. */
|
|
25
|
+
export declare const FieldValueSchema: z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>;
|
|
26
|
+
export type FieldValue = z.infer<typeof FieldValueSchema>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/** `#RGB` or `#RRGGBB`. Passmint also accepts `rgb(...)`; TapAce standardises on hex. */
|
|
3
|
+
export const HexColorSchema = z
|
|
4
|
+
.string()
|
|
5
|
+
.regex(/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/, 'Expected a hex color like #18566A');
|
|
6
|
+
/** An `asset_…` id owned by the same organization as the template. */
|
|
7
|
+
export const AssetRefSchema = z
|
|
8
|
+
.string()
|
|
9
|
+
.regex(/^asset_[A-Za-z0-9_-]+$/, 'Expected an asset id');
|
|
10
|
+
export const BarcodeFormatSchema = z.enum(['qr', 'pdf417', 'aztec', 'code128']);
|
|
11
|
+
export const BarcodeEncodingSchema = z.enum(['iso-8859-1', 'utf-8', 'utf-16']);
|
|
12
|
+
export const TextAlignmentSchema = z.enum(['left', 'center', 'right', 'natural']);
|
|
13
|
+
/** A field value. Passmint accepts string or number; TapAce preserves both. */
|
|
14
|
+
export const FieldValueSchema = z.union([z.string(), z.number()]);
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const MAX_RELEVANT_LOCATIONS = 10;
|
|
3
|
+
export declare const MAX_RELEVANT_DATES = 10;
|
|
4
|
+
export declare const RELEVANCE_MESSAGE_MAX = 120;
|
|
5
|
+
/** Wallet needs the timezone: older iOS silently drops a date without one. */
|
|
6
|
+
export declare const DateTimeSchema: z.ZodString;
|
|
7
|
+
export declare const RelevantLocationSchema: z.ZodObject<{
|
|
8
|
+
latitude: z.ZodNumber;
|
|
9
|
+
longitude: z.ZodNumber;
|
|
10
|
+
message: z.ZodOptional<z.ZodString>;
|
|
11
|
+
}, z.core.$strict>;
|
|
12
|
+
export type RelevantLocation = z.infer<typeof RelevantLocationSchema>;
|
|
13
|
+
export declare const RelevantDateSchema: z.ZodObject<{
|
|
14
|
+
start: z.ZodString;
|
|
15
|
+
end: z.ZodString;
|
|
16
|
+
}, z.core.$strict>;
|
|
17
|
+
export type RelevantDate = z.infer<typeof RelevantDateSchema>;
|
|
18
|
+
/**
|
|
19
|
+
* Apple Lock Screen relevance ("When and where"). On a pass, each list that is
|
|
20
|
+
* present replaces the template's; `[]` means none for that pass.
|
|
21
|
+
*/
|
|
22
|
+
export declare const RelevanceSchema: z.ZodObject<{
|
|
23
|
+
locations: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
24
|
+
latitude: z.ZodNumber;
|
|
25
|
+
longitude: z.ZodNumber;
|
|
26
|
+
message: z.ZodOptional<z.ZodString>;
|
|
27
|
+
}, z.core.$strict>>>;
|
|
28
|
+
dates: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
29
|
+
start: z.ZodString;
|
|
30
|
+
end: z.ZodString;
|
|
31
|
+
}, z.core.$strict>>>;
|
|
32
|
+
}, z.core.$strict>;
|
|
33
|
+
export type Relevance = z.infer<typeof RelevanceSchema>;
|
|
34
|
+
export declare function isTimeZone(name: string): boolean;
|
|
35
|
+
/** The template's lists, plus the timezone the dashboard shows and enters window times in. */
|
|
36
|
+
export declare const TemplateRelevanceSchema: z.ZodObject<{
|
|
37
|
+
locations: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
38
|
+
latitude: z.ZodNumber;
|
|
39
|
+
longitude: z.ZodNumber;
|
|
40
|
+
message: z.ZodOptional<z.ZodString>;
|
|
41
|
+
}, z.core.$strict>>>;
|
|
42
|
+
dates: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
43
|
+
start: z.ZodString;
|
|
44
|
+
end: z.ZodString;
|
|
45
|
+
}, z.core.$strict>>>;
|
|
46
|
+
timezone: z.ZodOptional<z.ZodString>;
|
|
47
|
+
}, z.core.$strict>;
|
|
48
|
+
export type TemplateRelevance = z.infer<typeof TemplateRelevanceSchema>;
|
|
49
|
+
export interface ResolvedRelevance {
|
|
50
|
+
locations: RelevantLocation[];
|
|
51
|
+
dates: RelevantDate[];
|
|
52
|
+
}
|
|
53
|
+
/** A pass's stored relevance; anything unreadable counts as none, so the template's applies. */
|
|
54
|
+
export declare function storedRelevance(raw: unknown): Relevance | null;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export const MAX_RELEVANT_LOCATIONS = 10;
|
|
3
|
+
export const MAX_RELEVANT_DATES = 10;
|
|
4
|
+
export const RELEVANCE_MESSAGE_MAX = 120;
|
|
5
|
+
/** Wallet needs the timezone: older iOS silently drops a date without one. */
|
|
6
|
+
export const DateTimeSchema = z
|
|
7
|
+
.string()
|
|
8
|
+
.regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})$/, 'Expected a date and time with a timezone, like 2026-11-02T18:00:00+05:30.');
|
|
9
|
+
function coordinate(name, limit) {
|
|
10
|
+
const error = `Enter a ${name} from -${limit} to ${limit}.`;
|
|
11
|
+
return z.number({ error }).min(-limit, { error }).max(limit, { error });
|
|
12
|
+
}
|
|
13
|
+
export const RelevantLocationSchema = z.strictObject({
|
|
14
|
+
latitude: coordinate('latitude', 90),
|
|
15
|
+
longitude: coordinate('longitude', 180),
|
|
16
|
+
message: z.string().trim().min(1).max(RELEVANCE_MESSAGE_MAX).optional(),
|
|
17
|
+
});
|
|
18
|
+
export const RelevantDateSchema = z
|
|
19
|
+
.strictObject({ start: DateTimeSchema, end: DateTimeSchema })
|
|
20
|
+
.refine((window) => Date.parse(window.end) > Date.parse(window.start), {
|
|
21
|
+
message: 'A date window must end after it starts.',
|
|
22
|
+
path: ['end'],
|
|
23
|
+
});
|
|
24
|
+
/**
|
|
25
|
+
* Apple Lock Screen relevance ("When and where"). On a pass, each list that is
|
|
26
|
+
* present replaces the template's; `[]` means none for that pass.
|
|
27
|
+
*/
|
|
28
|
+
export const RelevanceSchema = z.strictObject({
|
|
29
|
+
locations: z.array(RelevantLocationSchema).max(MAX_RELEVANT_LOCATIONS).optional(),
|
|
30
|
+
dates: z.array(RelevantDateSchema).max(MAX_RELEVANT_DATES).optional(),
|
|
31
|
+
});
|
|
32
|
+
export function isTimeZone(name) {
|
|
33
|
+
try {
|
|
34
|
+
new Intl.DateTimeFormat('en', { timeZone: name });
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/** The template's lists, plus the timezone the dashboard shows and enters window times in. */
|
|
42
|
+
export const TemplateRelevanceSchema = RelevanceSchema.extend({
|
|
43
|
+
timezone: z
|
|
44
|
+
.string()
|
|
45
|
+
.refine(isTimeZone, 'Expected a timezone like Asia/Colombo.')
|
|
46
|
+
.optional(),
|
|
47
|
+
});
|
|
48
|
+
/** A pass's stored relevance; anything unreadable counts as none, so the template's applies. */
|
|
49
|
+
export function storedRelevance(raw) {
|
|
50
|
+
const parsed = RelevanceSchema.safeParse(raw);
|
|
51
|
+
return parsed.success ? parsed.data : null;
|
|
52
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const MAX_STAMPS = 10;
|
|
3
|
+
/** Built-in stamps, drawn from Phosphor icons of the same name. */
|
|
4
|
+
export declare const STAMP_ICONS: readonly ["coffee", "star", "heart", "check", "gift", "scissors", "pizza", "sparkle"];
|
|
5
|
+
export type StampIcon = (typeof STAMP_ICONS)[number];
|
|
6
|
+
/**
|
|
7
|
+
* How stamps are drawn on the iOS 27 poster. The background is the template's
|
|
8
|
+
* poster artwork or its own colour (left out, the pass background). The disc sits
|
|
9
|
+
* behind each stamp so it stays readable on any artwork.
|
|
10
|
+
*/
|
|
11
|
+
export declare const StampPosterSettingsSchema: z.ZodObject<{
|
|
12
|
+
background: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
13
|
+
kind: z.ZodLiteral<"artwork">;
|
|
14
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
15
|
+
kind: z.ZodLiteral<"color">;
|
|
16
|
+
color: z.ZodOptional<z.ZodString>;
|
|
17
|
+
}, z.core.$strict>], "kind">;
|
|
18
|
+
discColor: z.ZodString;
|
|
19
|
+
discOpacity: z.ZodNumber;
|
|
20
|
+
}, z.core.$strict>;
|
|
21
|
+
export type StampPosterSettings = z.infer<typeof StampPosterSettingsSchema>;
|
|
22
|
+
/**
|
|
23
|
+
* How a stamp card draws its stamps. Built-in icons are tinted with `earnedColor`
|
|
24
|
+
* and `emptyColor`; an uploaded stamp keeps its own colours. `emptyStyle` is how
|
|
25
|
+
* an unearned slot looks when there is no `emptyAssetId`. A background colour left
|
|
26
|
+
* out follows the pass background.
|
|
27
|
+
*/
|
|
28
|
+
export declare const StampSettingsSchema: z.ZodObject<{
|
|
29
|
+
total: z.ZodNumber;
|
|
30
|
+
perRow: z.ZodNumber;
|
|
31
|
+
stamp: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
32
|
+
kind: z.ZodLiteral<"icon">;
|
|
33
|
+
icon: z.ZodEnum<{
|
|
34
|
+
check: "check";
|
|
35
|
+
coffee: "coffee";
|
|
36
|
+
star: "star";
|
|
37
|
+
heart: "heart";
|
|
38
|
+
gift: "gift";
|
|
39
|
+
scissors: "scissors";
|
|
40
|
+
pizza: "pizza";
|
|
41
|
+
sparkle: "sparkle";
|
|
42
|
+
}>;
|
|
43
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
44
|
+
kind: z.ZodLiteral<"image">;
|
|
45
|
+
assetId: z.ZodString;
|
|
46
|
+
}, z.core.$strict>], "kind">;
|
|
47
|
+
emptyAssetId: z.ZodOptional<z.ZodString>;
|
|
48
|
+
emptyStyle: z.ZodEnum<{
|
|
49
|
+
faded: "faded";
|
|
50
|
+
ring: "ring";
|
|
51
|
+
both: "both";
|
|
52
|
+
}>;
|
|
53
|
+
earnedColor: z.ZodString;
|
|
54
|
+
emptyColor: z.ZodString;
|
|
55
|
+
background: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
56
|
+
kind: z.ZodLiteral<"color">;
|
|
57
|
+
color: z.ZodOptional<z.ZodString>;
|
|
58
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
59
|
+
kind: z.ZodLiteral<"image">;
|
|
60
|
+
assetId: z.ZodString;
|
|
61
|
+
}, z.core.$strict>], "kind">;
|
|
62
|
+
poster: z.ZodOptional<z.ZodObject<{
|
|
63
|
+
background: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
64
|
+
kind: z.ZodLiteral<"artwork">;
|
|
65
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
66
|
+
kind: z.ZodLiteral<"color">;
|
|
67
|
+
color: z.ZodOptional<z.ZodString>;
|
|
68
|
+
}, z.core.$strict>], "kind">;
|
|
69
|
+
discColor: z.ZodString;
|
|
70
|
+
discOpacity: z.ZodNumber;
|
|
71
|
+
}, z.core.$strict>>;
|
|
72
|
+
}, z.core.$strict>;
|
|
73
|
+
export type StampSettings = z.infer<typeof StampSettingsSchema>;
|
|
74
|
+
/**
|
|
75
|
+
* What a stamp change that notifies says. `%@` stands for the count text
|
|
76
|
+
* ("7 / 10"). Apple's message is the count field's change message, so it must
|
|
77
|
+
* contain `%@`; without a count field it fills the pass's message field instead.
|
|
78
|
+
* The optional `full…` texts replace them when the change fills the card; Apple's
|
|
79
|
+
* always goes in the message field, so it needs no `%@`.
|
|
80
|
+
*/
|
|
81
|
+
export declare const StampNotificationsSchema: z.ZodObject<{
|
|
82
|
+
apple: z.ZodOptional<z.ZodString>;
|
|
83
|
+
googleTitle: z.ZodOptional<z.ZodString>;
|
|
84
|
+
googleBody: z.ZodOptional<z.ZodString>;
|
|
85
|
+
fullApple: z.ZodOptional<z.ZodString>;
|
|
86
|
+
fullGoogleTitle: z.ZodOptional<z.ZodString>;
|
|
87
|
+
fullGoogleBody: z.ZodOptional<z.ZodString>;
|
|
88
|
+
}, z.core.$strict>;
|
|
89
|
+
export type StampNotifications = z.infer<typeof StampNotificationsSchema>;
|
|
90
|
+
export declare function stampCountText(count: number, total: number): string;
|
|
91
|
+
export declare function stampAssetIds(stamps: StampSettings): string[];
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { AssetRefSchema, HexColorSchema } from './primitives.js';
|
|
3
|
+
export const MAX_STAMPS = 10;
|
|
4
|
+
/** Built-in stamps, drawn from Phosphor icons of the same name. */
|
|
5
|
+
export const STAMP_ICONS = [
|
|
6
|
+
'coffee',
|
|
7
|
+
'star',
|
|
8
|
+
'heart',
|
|
9
|
+
'check',
|
|
10
|
+
'gift',
|
|
11
|
+
'scissors',
|
|
12
|
+
'pizza',
|
|
13
|
+
'sparkle',
|
|
14
|
+
];
|
|
15
|
+
const stampCount = z.number().int().min(1).max(MAX_STAMPS);
|
|
16
|
+
/** An uploaded image the designer may not have yet, with a message saying so. */
|
|
17
|
+
const uploaded = (message) => z.string().regex(/^asset_[A-Za-z0-9_-]+$/, message);
|
|
18
|
+
/**
|
|
19
|
+
* How stamps are drawn on the iOS 27 poster. The background is the template's
|
|
20
|
+
* poster artwork or its own colour (left out, the pass background). The disc sits
|
|
21
|
+
* behind each stamp so it stays readable on any artwork.
|
|
22
|
+
*/
|
|
23
|
+
export const StampPosterSettingsSchema = z.strictObject({
|
|
24
|
+
background: z.discriminatedUnion('kind', [
|
|
25
|
+
z.strictObject({ kind: z.literal('artwork') }),
|
|
26
|
+
z.strictObject({ kind: z.literal('color'), color: HexColorSchema.optional() }),
|
|
27
|
+
]),
|
|
28
|
+
discColor: HexColorSchema,
|
|
29
|
+
discOpacity: z.number().min(0).max(1),
|
|
30
|
+
});
|
|
31
|
+
/**
|
|
32
|
+
* How a stamp card draws its stamps. Built-in icons are tinted with `earnedColor`
|
|
33
|
+
* and `emptyColor`; an uploaded stamp keeps its own colours. `emptyStyle` is how
|
|
34
|
+
* an unearned slot looks when there is no `emptyAssetId`. A background colour left
|
|
35
|
+
* out follows the pass background.
|
|
36
|
+
*/
|
|
37
|
+
export const StampSettingsSchema = z
|
|
38
|
+
.strictObject({
|
|
39
|
+
total: stampCount,
|
|
40
|
+
perRow: stampCount,
|
|
41
|
+
stamp: z.discriminatedUnion('kind', [
|
|
42
|
+
z.strictObject({ kind: z.literal('icon'), icon: z.enum(STAMP_ICONS) }),
|
|
43
|
+
z.strictObject({ kind: z.literal('image'), assetId: uploaded('Upload a stamp image.') }),
|
|
44
|
+
]),
|
|
45
|
+
emptyAssetId: AssetRefSchema.optional(),
|
|
46
|
+
emptyStyle: z.enum(['faded', 'ring', 'both']),
|
|
47
|
+
earnedColor: HexColorSchema,
|
|
48
|
+
emptyColor: HexColorSchema,
|
|
49
|
+
background: z.discriminatedUnion('kind', [
|
|
50
|
+
z.strictObject({ kind: z.literal('color'), color: HexColorSchema.optional() }),
|
|
51
|
+
z.strictObject({ kind: z.literal('image'), assetId: uploaded('Upload a strip image.') }),
|
|
52
|
+
]),
|
|
53
|
+
poster: StampPosterSettingsSchema.optional(),
|
|
54
|
+
})
|
|
55
|
+
.refine((s) => s.perRow <= s.total, {
|
|
56
|
+
message: 'Stamps per row can be at most the number of stamps.',
|
|
57
|
+
path: ['perRow'],
|
|
58
|
+
});
|
|
59
|
+
const notificationText = z.string().trim().min(1).max(200);
|
|
60
|
+
/**
|
|
61
|
+
* What a stamp change that notifies says. `%@` stands for the count text
|
|
62
|
+
* ("7 / 10"). Apple's message is the count field's change message, so it must
|
|
63
|
+
* contain `%@`; without a count field it fills the pass's message field instead.
|
|
64
|
+
* The optional `full…` texts replace them when the change fills the card; Apple's
|
|
65
|
+
* always goes in the message field, so it needs no `%@`.
|
|
66
|
+
*/
|
|
67
|
+
export const StampNotificationsSchema = z.strictObject({
|
|
68
|
+
apple: notificationText
|
|
69
|
+
.refine((text) => text.includes('%@'), 'Include %@ where the stamp count goes.')
|
|
70
|
+
.optional(),
|
|
71
|
+
googleTitle: notificationText.optional(),
|
|
72
|
+
googleBody: notificationText.optional(),
|
|
73
|
+
fullApple: notificationText.optional(),
|
|
74
|
+
fullGoogleTitle: notificationText.optional(),
|
|
75
|
+
fullGoogleBody: notificationText.optional(),
|
|
76
|
+
});
|
|
77
|
+
export function stampCountText(count, total) {
|
|
78
|
+
return `${count} / ${total}`;
|
|
79
|
+
}
|
|
80
|
+
export function stampAssetIds(stamps) {
|
|
81
|
+
const ids = [];
|
|
82
|
+
if (stamps.stamp.kind === 'image')
|
|
83
|
+
ids.push(stamps.stamp.assetId);
|
|
84
|
+
if (stamps.emptyAssetId)
|
|
85
|
+
ids.push(stamps.emptyAssetId);
|
|
86
|
+
if (stamps.background.kind === 'image')
|
|
87
|
+
ids.push(stamps.background.assetId);
|
|
88
|
+
return ids;
|
|
89
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convert a schema version 1 definition (fields grouped by Apple slot) to
|
|
3
|
+
* version 2 (one field list plus a standard layout that places them by key), and
|
|
4
|
+
* move "When and where" from where it was first stored (`apple.relevance`) to the
|
|
5
|
+
* top level. Anything else is returned unchanged for the schema to judge.
|
|
6
|
+
*/
|
|
7
|
+
export declare function upgradeDefinition(input: unknown): unknown;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { STANDARD_SLOTS } from './fields.js';
|
|
2
|
+
const isObject = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
3
|
+
/**
|
|
4
|
+
* Convert a schema version 1 definition (fields grouped by Apple slot) to
|
|
5
|
+
* version 2 (one field list plus a standard layout that places them by key), and
|
|
6
|
+
* move "When and where" from where it was first stored (`apple.relevance`) to the
|
|
7
|
+
* top level. Anything else is returned unchanged for the schema to judge.
|
|
8
|
+
*/
|
|
9
|
+
export function upgradeDefinition(input) {
|
|
10
|
+
return moveRelevance(upgradeVersion1(input));
|
|
11
|
+
}
|
|
12
|
+
function moveRelevance(input) {
|
|
13
|
+
if (!isObject(input) || !isObject(input.apple) || !('relevance' in input.apple))
|
|
14
|
+
return input;
|
|
15
|
+
const { relevance, ...apple } = input.apple;
|
|
16
|
+
return { ...input, apple, ...(input.relevance === undefined ? { relevance } : {}) };
|
|
17
|
+
}
|
|
18
|
+
function upgradeVersion1(input) {
|
|
19
|
+
if (!isObject(input) || input.schemaVersion !== 1)
|
|
20
|
+
return input;
|
|
21
|
+
const config = isObject(input.config) ? input.config : {};
|
|
22
|
+
const groups = isObject(config.fields) ? config.fields : {};
|
|
23
|
+
const fields = [];
|
|
24
|
+
const standard = {};
|
|
25
|
+
for (const slot of STANDARD_SLOTS) {
|
|
26
|
+
const group = Array.isArray(groups[slot]) ? groups[slot] : [];
|
|
27
|
+
standard[slot] = [];
|
|
28
|
+
for (const field of group) {
|
|
29
|
+
fields.push(field);
|
|
30
|
+
if (isObject(field) && typeof field.key === 'string')
|
|
31
|
+
standard[slot].push(field.key);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
...input,
|
|
36
|
+
schemaVersion: 2,
|
|
37
|
+
config: { ...config, fields, layouts: { standard } },
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export * from './contract/index.js';
|
|
2
|
+
export * from './compiler/template-preview.js';
|
|
3
|
+
export type { CompileIssue } from './compiler/passmint-compiler.js';
|
|
4
|
+
export type { ApplePreview, BarcodePreview, FieldPreview, GoogleCardItemPreview, GoogleEventPreview, GoogleLoyaltyPreview, GooglePreview, PosterPreview, PreviewStyle, } from './compiler/preview-model.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@owask/tapace-templates",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "TapAce template contract and wallet preview compiler, shared by the API and the dashboard.",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/public-api.d.ts",
|
|
11
|
+
"default": "./dist/public-api.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": ["dist"],
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"passmint": "npm:@owask/passmint@0.7.0-tapace.6",
|
|
17
|
+
"zod": "^4.5.4"
|
|
18
|
+
},
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"access": "public"
|
|
21
|
+
}
|
|
22
|
+
}
|