@farcaster/snap 1.5.2 → 1.7.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/dist/constants.d.ts +0 -107
- package/dist/constants.js +0 -148
- package/dist/dataStore.d.ts +12 -0
- package/dist/dataStore.js +35 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.js +4 -3
- package/dist/react/accent-context.d.ts +6 -0
- package/dist/react/accent-context.js +10 -0
- package/dist/react/catalog-renderer.d.ts +5 -0
- package/dist/react/catalog-renderer.js +37 -0
- package/dist/react/components/action-button.d.ts +6 -0
- package/dist/react/components/action-button.js +22 -0
- package/dist/react/components/badge.d.ts +5 -0
- package/dist/react/components/badge.js +18 -0
- package/dist/react/components/icon.d.ts +7 -0
- package/dist/react/components/icon.js +60 -0
- package/dist/react/components/image.d.ts +5 -0
- package/dist/react/components/image.js +15 -0
- package/dist/react/components/input.d.ts +5 -0
- package/dist/react/components/input.js +18 -0
- package/dist/react/components/item-group.d.ts +7 -0
- package/dist/react/components/item-group.js +17 -0
- package/dist/react/components/item.d.ts +7 -0
- package/dist/react/components/item.js +9 -0
- package/dist/react/components/progress.d.ts +5 -0
- package/dist/react/components/progress.js +11 -0
- package/dist/react/components/separator.d.ts +5 -0
- package/dist/react/components/separator.js +7 -0
- package/dist/react/components/slider.d.ts +5 -0
- package/dist/react/components/slider.js +21 -0
- package/dist/react/components/stack.d.ts +7 -0
- package/dist/react/components/stack.js +32 -0
- package/dist/react/components/switch.d.ts +5 -0
- package/dist/react/components/switch.js +23 -0
- package/dist/react/components/text.d.ts +5 -0
- package/dist/react/components/text.js +25 -0
- package/dist/react/components/toggle-group.d.ts +5 -0
- package/dist/react/components/toggle-group.js +52 -0
- package/dist/react/hooks/use-snap-accent.d.ts +13 -0
- package/dist/react/hooks/use-snap-accent.js +32 -0
- package/dist/react/index.d.ts +47 -0
- package/dist/react/index.js +191 -0
- package/dist/react/lib/preview-primary-css.d.ts +6 -0
- package/dist/react/lib/preview-primary-css.js +43 -0
- package/dist/react/lib/resolve-palette-hex.d.ts +2 -0
- package/dist/react/lib/resolve-palette-hex.js +10 -0
- package/dist/react-native/catalog-renderer.d.ts +5 -0
- package/dist/react-native/catalog-renderer.js +36 -0
- package/dist/react-native/components/snap-action-button.d.ts +2 -0
- package/dist/react-native/components/snap-action-button.js +68 -0
- package/dist/react-native/components/snap-badge.d.ts +2 -0
- package/dist/react-native/components/snap-badge.js +38 -0
- package/dist/react-native/components/snap-icon.d.ts +5 -0
- package/dist/react-native/components/snap-icon.js +56 -0
- package/dist/react-native/components/snap-image.d.ts +2 -0
- package/dist/react-native/components/snap-image.js +24 -0
- package/dist/react-native/components/snap-input.d.ts +2 -0
- package/dist/react-native/components/snap-input.js +36 -0
- package/dist/react-native/components/snap-item-group.d.ts +5 -0
- package/dist/react-native/components/snap-item-group.js +23 -0
- package/dist/react-native/components/snap-item.d.ts +5 -0
- package/dist/react-native/components/snap-item.js +45 -0
- package/dist/react-native/components/snap-progress.d.ts +2 -0
- package/dist/react-native/components/snap-progress.js +26 -0
- package/dist/react-native/components/snap-separator.d.ts +2 -0
- package/dist/react-native/components/snap-separator.js +23 -0
- package/dist/react-native/components/snap-slider.d.ts +2 -0
- package/dist/react-native/components/snap-slider.js +42 -0
- package/dist/react-native/components/snap-stack.d.ts +5 -0
- package/dist/react-native/components/snap-stack.js +49 -0
- package/dist/react-native/components/snap-switch.d.ts +2 -0
- package/dist/react-native/components/snap-switch.js +30 -0
- package/dist/react-native/components/snap-text.d.ts +2 -0
- package/dist/react-native/components/snap-text.js +37 -0
- package/dist/react-native/components/snap-toggle-group.d.ts +2 -0
- package/dist/react-native/components/snap-toggle-group.js +100 -0
- package/dist/react-native/index.d.ts +52 -0
- package/dist/react-native/index.js +155 -0
- package/dist/react-native/theme.d.ts +21 -0
- package/dist/react-native/theme.js +37 -0
- package/dist/react-native/use-snap-palette.d.ts +13 -0
- package/dist/react-native/use-snap-palette.js +48 -0
- package/dist/schemas.d.ts +14 -1629
- package/dist/schemas.js +14 -526
- package/dist/ui/badge.d.ts +52 -0
- package/dist/ui/badge.js +9 -0
- package/dist/ui/button.d.ts +42 -28
- package/dist/ui/button.js +7 -9
- package/dist/ui/catalog.d.ts +281 -156
- package/dist/ui/catalog.js +102 -83
- package/dist/ui/icon.d.ts +56 -0
- package/dist/ui/icon.js +51 -0
- package/dist/ui/image.d.ts +1 -0
- package/dist/ui/image.js +2 -2
- package/dist/ui/index.d.ts +20 -22
- package/dist/ui/index.js +10 -11
- package/dist/ui/input.d.ts +17 -0
- package/dist/ui/input.js +13 -0
- package/dist/ui/item-group.d.ts +12 -0
- package/dist/ui/item-group.js +7 -0
- package/dist/ui/item.d.ts +14 -0
- package/dist/ui/item.js +9 -0
- package/dist/ui/progress.d.ts +1 -11
- package/dist/ui/progress.js +21 -4
- package/dist/ui/schema.d.ts +1 -1
- package/dist/ui/schema.js +3 -3
- package/dist/ui/separator.d.ts +9 -0
- package/dist/ui/separator.js +5 -0
- package/dist/ui/slider.d.ts +4 -3
- package/dist/ui/slider.js +34 -5
- package/dist/ui/stack.d.ts +22 -1
- package/dist/ui/stack.js +8 -1
- package/dist/ui/switch.d.ts +8 -0
- package/dist/ui/switch.js +7 -0
- package/dist/ui/text.d.ts +15 -7
- package/dist/ui/text.js +8 -4
- package/dist/ui/toggle-group.d.ts +23 -0
- package/dist/ui/toggle-group.js +19 -0
- package/dist/validator.d.ts +5 -1
- package/dist/validator.js +6 -136
- package/package.json +78 -53
- package/src/constants.ts +0 -179
- package/src/dataStore.ts +62 -0
- package/src/index.ts +10 -20
- package/src/react/accent-context.tsx +29 -0
- package/src/react/catalog-renderer.tsx +39 -0
- package/src/react/components/action-button.tsx +48 -0
- package/src/react/components/badge.tsx +37 -0
- package/src/react/components/icon.tsx +115 -0
- package/src/react/components/image.tsx +33 -0
- package/src/react/components/input.tsx +36 -0
- package/src/react/components/item-group.tsx +43 -0
- package/src/react/components/item.tsx +33 -0
- package/src/react/components/progress.tsx +29 -0
- package/src/react/components/separator.tsx +14 -0
- package/src/react/components/slider.tsx +43 -0
- package/src/react/components/stack.tsx +55 -0
- package/src/react/components/switch.tsx +46 -0
- package/src/react/components/text.tsx +43 -0
- package/src/react/components/toggle-group.tsx +85 -0
- package/src/react/hooks/use-snap-accent.ts +45 -0
- package/src/react/index.tsx +321 -0
- package/src/react/lib/preview-primary-css.ts +57 -0
- package/src/react/lib/resolve-palette-hex.ts +20 -0
- package/src/react-native/catalog-renderer.tsx +37 -0
- package/src/react-native/components/snap-action-button.tsx +92 -0
- package/src/react-native/components/snap-badge.tsx +57 -0
- package/src/react-native/components/snap-icon.tsx +102 -0
- package/src/react-native/components/snap-image.tsx +38 -0
- package/src/react-native/components/snap-input.tsx +57 -0
- package/src/react-native/components/snap-item-group.tsx +43 -0
- package/src/react-native/components/snap-item.tsx +70 -0
- package/src/react-native/components/snap-progress.tsx +40 -0
- package/src/react-native/components/snap-separator.tsx +32 -0
- package/src/react-native/components/snap-slider.tsx +82 -0
- package/src/react-native/components/snap-stack.tsx +66 -0
- package/src/react-native/components/snap-switch.tsx +45 -0
- package/src/react-native/components/snap-text.tsx +53 -0
- package/src/react-native/components/snap-toggle-group.tsx +128 -0
- package/src/react-native/index.tsx +267 -0
- package/src/react-native/theme.tsx +73 -0
- package/src/react-native/use-snap-palette.ts +64 -0
- package/src/schemas.ts +18 -644
- package/src/ui/badge.ts +13 -0
- package/src/ui/button.ts +9 -12
- package/src/ui/catalog.ts +106 -86
- package/src/ui/icon.ts +56 -0
- package/src/ui/image.ts +3 -2
- package/src/ui/index.ts +26 -29
- package/src/ui/input.ts +17 -0
- package/src/ui/item-group.ts +11 -0
- package/src/ui/item.ts +13 -0
- package/src/ui/progress.ts +25 -7
- package/src/ui/schema.ts +3 -3
- package/src/ui/separator.ts +9 -0
- package/src/ui/slider.ts +40 -10
- package/src/ui/stack.ts +9 -1
- package/src/ui/switch.ts +11 -0
- package/src/ui/text.ts +9 -4
- package/src/ui/toggle-group.ts +23 -0
- package/src/validator.ts +6 -176
- package/dist/ui/bar-chart.d.ts +0 -30
- package/dist/ui/bar-chart.js +0 -15
- package/dist/ui/button-group.d.ts +0 -19
- package/dist/ui/button-group.js +0 -18
- package/dist/ui/divider.d.ts +0 -3
- package/dist/ui/divider.js +0 -2
- package/dist/ui/grid.d.ts +0 -22
- package/dist/ui/grid.js +0 -16
- package/dist/ui/group.d.ts +0 -7
- package/dist/ui/group.js +0 -5
- package/dist/ui/list.d.ts +0 -13
- package/dist/ui/list.js +0 -13
- package/dist/ui/spacer.d.ts +0 -9
- package/dist/ui/spacer.js +0 -5
- package/dist/ui/text-input.d.ts +0 -7
- package/dist/ui/text-input.js +0 -12
- package/dist/ui/toggle.d.ts +0 -7
- package/dist/ui/toggle.js +0 -6
- package/src/ui/bar-chart.ts +0 -20
- package/src/ui/button-group.ts +0 -26
- package/src/ui/divider.ts +0 -5
- package/src/ui/grid.ts +0 -25
- package/src/ui/group.ts +0 -8
- package/src/ui/list.ts +0 -17
- package/src/ui/spacer.ts +0 -8
- package/src/ui/text-input.ts +0 -15
- package/src/ui/toggle.ts +0 -9
package/src/schemas.ts
CHANGED
|
@@ -1,64 +1,16 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import type { Spec } from "@json-render/core";
|
|
2
3
|
import {
|
|
3
|
-
BUTTON_ACTION,
|
|
4
|
-
BUTTON_ACTION_VALUES,
|
|
5
|
-
CLIENT_ACTION,
|
|
6
|
-
BUTTON_GROUP_STYLE,
|
|
7
|
-
BUTTON_GROUP_STYLE_VALUES,
|
|
8
|
-
BUTTON_LAYOUT_VALUES,
|
|
9
|
-
BUTTON_STYLE_VALUES,
|
|
10
|
-
DEFAULT_BUTTON_LAYOUT,
|
|
11
|
-
DEFAULT_GRID_GAP,
|
|
12
|
-
DEFAULT_LIST_STYLE,
|
|
13
|
-
DEFAULT_SLIDER_STEP,
|
|
14
4
|
EFFECT_VALUES,
|
|
15
|
-
ELEMENT_TYPE,
|
|
16
|
-
GRID_CELL_SIZE_VALUES,
|
|
17
|
-
GRID_GAP_VALUES,
|
|
18
|
-
GROUP_LAYOUT_VALUES,
|
|
19
|
-
HEX_COLOR_6_RE,
|
|
20
|
-
HTTP_PREFIX,
|
|
21
|
-
HTTPS_PREFIX,
|
|
22
|
-
IMAGE_ASPECT_VALUES,
|
|
23
|
-
INTERACTIVE_ELEMENT_TYPES,
|
|
24
|
-
LIMITS,
|
|
25
|
-
LIST_STYLE_VALUES,
|
|
26
|
-
MEDIA_ELEMENT_TYPES,
|
|
27
|
-
PAGE_ROOT_TYPE,
|
|
28
|
-
SLIDER_STEP_ALIGN_EPS,
|
|
29
|
-
SPACER_SIZE,
|
|
30
|
-
SPACER_SIZE_VALUES,
|
|
31
5
|
SPEC_VERSION,
|
|
32
|
-
TEXT_ALIGN_VALUES,
|
|
33
|
-
TEXT_CONTENT_MAX,
|
|
34
|
-
TEXT_STYLE,
|
|
35
|
-
TEXT_STYLE_VALUES,
|
|
36
6
|
} from "./constants";
|
|
37
7
|
import {
|
|
38
|
-
BAR_CHART_COLOR_VALUES,
|
|
39
8
|
DEFAULT_THEME_ACCENT,
|
|
40
9
|
PALETTE_COLOR_VALUES,
|
|
41
|
-
PROGRESS_COLOR_VALUES,
|
|
42
10
|
} from "./colors";
|
|
11
|
+
import { type SnapDataStore } from "./dataStore";
|
|
43
12
|
|
|
44
|
-
|
|
45
|
-
* post/link/mini_app targets must be HTTPS in production; allow HTTP only for
|
|
46
|
-
* loopback hosts so local snap servers (e.g. http://localhost:3014/snap) validate.
|
|
47
|
-
*/
|
|
48
|
-
function isSecureOrLoopbackHttpButtonTarget(target: string): boolean {
|
|
49
|
-
if (target.startsWith(HTTPS_PREFIX)) return true;
|
|
50
|
-
if (!target.startsWith(HTTP_PREFIX)) return false;
|
|
51
|
-
try {
|
|
52
|
-
const u = new URL(target);
|
|
53
|
-
if (u.protocol !== "http:") return false;
|
|
54
|
-
const h = u.hostname.toLowerCase();
|
|
55
|
-
return (
|
|
56
|
-
h === "localhost" || h === "127.0.0.1" || h === "[::1]" || h === "::1"
|
|
57
|
-
);
|
|
58
|
-
} catch {
|
|
59
|
-
return false;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
13
|
+
// ─── Theme ─────────────────────────────────────────────
|
|
62
14
|
|
|
63
15
|
const themeAccentSchema = z.enum(PALETTE_COLOR_VALUES, {
|
|
64
16
|
message: `accent must be a palette color: ${PALETTE_COLOR_VALUES.join(", ")}`,
|
|
@@ -70,577 +22,36 @@ const themeSchema = z
|
|
|
70
22
|
})
|
|
71
23
|
.strict();
|
|
72
24
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const textAlignSchema = z.enum(TEXT_ALIGN_VALUES);
|
|
78
|
-
|
|
79
|
-
const textElementSchema = z
|
|
80
|
-
.object({
|
|
81
|
-
type: z.literal(ELEMENT_TYPE.text),
|
|
82
|
-
style: z.enum(TEXT_STYLE_VALUES),
|
|
83
|
-
content: z.string(),
|
|
84
|
-
align: textAlignSchema.optional(),
|
|
85
|
-
})
|
|
86
|
-
.superRefine((val, ctx) => {
|
|
87
|
-
const max = TEXT_CONTENT_MAX[val.style];
|
|
88
|
-
if (val.content.length > max) {
|
|
89
|
-
ctx.addIssue({
|
|
90
|
-
code: "custom",
|
|
91
|
-
message: `${val.style} text exceeds ${max} character limit (found ${val.content.length})`,
|
|
92
|
-
path: ["content"],
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
const imageElementSchema = z.object({
|
|
98
|
-
type: z.literal(ELEMENT_TYPE.image),
|
|
99
|
-
url: imageUrlSchema,
|
|
100
|
-
aspect: z.enum(IMAGE_ASPECT_VALUES),
|
|
101
|
-
alt: z.string().optional(),
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
const dividerElementSchema = z.object({
|
|
105
|
-
type: z.literal(ELEMENT_TYPE.divider),
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
const spacerElementSchema = z.object({
|
|
109
|
-
type: z.literal(ELEMENT_TYPE.spacer),
|
|
110
|
-
size: z.enum(SPACER_SIZE_VALUES).default(SPACER_SIZE.medium),
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
const progressElementSchema = z
|
|
114
|
-
.object({
|
|
115
|
-
type: z.literal(ELEMENT_TYPE.progress),
|
|
116
|
-
value: z.number(),
|
|
117
|
-
max: z.number(),
|
|
118
|
-
label: z.string().max(60).optional(),
|
|
119
|
-
color: z.enum(PROGRESS_COLOR_VALUES).optional(),
|
|
120
|
-
})
|
|
121
|
-
.superRefine((val, ctx) => {
|
|
122
|
-
const { value, max } = val;
|
|
123
|
-
if (!Number.isFinite(max)) {
|
|
124
|
-
ctx.addIssue({
|
|
125
|
-
code: "custom",
|
|
126
|
-
message: "progress max must be a finite number",
|
|
127
|
-
path: ["max"],
|
|
128
|
-
});
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
if (max <= 0) {
|
|
132
|
-
ctx.addIssue({
|
|
133
|
-
code: "custom",
|
|
134
|
-
message: `progress max must be greater than 0 (received ${max})`,
|
|
135
|
-
path: ["max"],
|
|
136
|
-
});
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
if (!Number.isFinite(value)) {
|
|
140
|
-
ctx.addIssue({
|
|
141
|
-
code: "custom",
|
|
142
|
-
message: "progress value must be a finite number",
|
|
143
|
-
path: ["value"],
|
|
144
|
-
});
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
|
-
if (value < 0 || value > max) {
|
|
148
|
-
ctx.addIssue({
|
|
149
|
-
code: "custom",
|
|
150
|
-
message: `progress value (${value}) must be between 0 and max (${max})`,
|
|
151
|
-
path: ["value"],
|
|
152
|
-
});
|
|
153
|
-
}
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
const listItemSchema = z.object({
|
|
157
|
-
content: z.string().max(LIMITS.listItemContentMaxChars),
|
|
158
|
-
trailing: z.string().max(LIMITS.listItemTrailingMaxChars).optional(),
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
const listElementSchema = z.object({
|
|
162
|
-
type: z.literal(ELEMENT_TYPE.list),
|
|
163
|
-
style: z.enum(LIST_STYLE_VALUES).default(DEFAULT_LIST_STYLE),
|
|
164
|
-
items: z
|
|
165
|
-
.array(listItemSchema)
|
|
166
|
-
.min(LIMITS.minListItems)
|
|
167
|
-
.max(LIMITS.maxListItems),
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
const gridCellSchema = z.object({
|
|
171
|
-
row: z.number().int().min(0),
|
|
172
|
-
col: z.number().int().min(0),
|
|
173
|
-
/** Hex background (#RRGGBB); omit for transparent */
|
|
174
|
-
color: z
|
|
175
|
-
.string()
|
|
176
|
-
.regex(HEX_COLOR_6_RE, {
|
|
177
|
-
message: "cell color must be a valid 6-digit hex color (#RRGGBB)",
|
|
178
|
-
})
|
|
179
|
-
.optional(),
|
|
180
|
-
content: z.string().optional(),
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
const gridElementSchema = z
|
|
184
|
-
.object({
|
|
185
|
-
type: z.literal(ELEMENT_TYPE.grid),
|
|
186
|
-
cols: z.number().int().min(LIMITS.minGridCols).max(LIMITS.maxGridCols),
|
|
187
|
-
rows: z.number().int().min(LIMITS.minGridRows).max(LIMITS.maxGridRows),
|
|
188
|
-
cells: z.array(gridCellSchema),
|
|
189
|
-
cellSize: z.enum(GRID_CELL_SIZE_VALUES).optional(),
|
|
190
|
-
gap: z.enum(GRID_GAP_VALUES).default(DEFAULT_GRID_GAP),
|
|
191
|
-
interactive: z.boolean().optional(),
|
|
192
|
-
})
|
|
193
|
-
.superRefine((val, ctx) => {
|
|
194
|
-
const { cols, rows, cells } = val;
|
|
195
|
-
for (let i = 0; i < cells.length; i++) {
|
|
196
|
-
const c = cells[i]!;
|
|
197
|
-
const base = ["cells", i] as const;
|
|
198
|
-
if (c.row < 0 || c.row >= rows) {
|
|
199
|
-
ctx.addIssue({
|
|
200
|
-
code: "custom",
|
|
201
|
-
message: `grid cell row ${c.row} is out of bounds (expected 0 to ${
|
|
202
|
-
rows - 1
|
|
203
|
-
})`,
|
|
204
|
-
path: [...base, "row"],
|
|
205
|
-
});
|
|
206
|
-
}
|
|
207
|
-
if (c.col < 0 || c.col >= cols) {
|
|
208
|
-
ctx.addIssue({
|
|
209
|
-
code: "custom",
|
|
210
|
-
message: `grid cell col ${c.col} is out of bounds (expected 0 to ${
|
|
211
|
-
cols - 1
|
|
212
|
-
})`,
|
|
213
|
-
path: [...base, "col"],
|
|
214
|
-
});
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
const textInputElementSchema = z.object({
|
|
220
|
-
type: z.literal(ELEMENT_TYPE.text_input),
|
|
221
|
-
name: z.string().min(1),
|
|
222
|
-
placeholder: z.string().max(60).optional(),
|
|
223
|
-
maxLength: z.number().int().min(1).max(LIMITS.maxTextInputChars).optional(),
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
const sliderElementSchema = z
|
|
227
|
-
.object({
|
|
228
|
-
type: z.literal(ELEMENT_TYPE.slider),
|
|
229
|
-
name: z.string().min(1),
|
|
230
|
-
min: z.number(),
|
|
231
|
-
max: z.number(),
|
|
232
|
-
step: z.number().default(DEFAULT_SLIDER_STEP),
|
|
233
|
-
value: z.number().optional(),
|
|
234
|
-
label: z.string().max(60).optional(),
|
|
235
|
-
minLabel: z.string().max(20).optional(),
|
|
236
|
-
maxLabel: z.string().max(20).optional(),
|
|
237
|
-
})
|
|
238
|
-
.superRefine((val, ctx) => {
|
|
239
|
-
const { min, max, step, value } = val;
|
|
240
|
-
if (min > max) {
|
|
241
|
-
ctx.addIssue({
|
|
242
|
-
code: "custom",
|
|
243
|
-
message: `slider min (${min}) must be less than or equal to max (${max})`,
|
|
244
|
-
path: ["min"],
|
|
245
|
-
});
|
|
246
|
-
return;
|
|
247
|
-
}
|
|
248
|
-
if (step !== undefined) {
|
|
249
|
-
if (step <= 0 || !Number.isFinite(step)) {
|
|
250
|
-
ctx.addIssue({
|
|
251
|
-
code: "custom",
|
|
252
|
-
message: "slider step must be a finite number greater than 0",
|
|
253
|
-
path: ["step"],
|
|
254
|
-
});
|
|
255
|
-
return;
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
if (value !== undefined) {
|
|
259
|
-
if (!Number.isFinite(value)) {
|
|
260
|
-
ctx.addIssue({
|
|
261
|
-
code: "custom",
|
|
262
|
-
message: "slider value must be a finite number",
|
|
263
|
-
path: ["value"],
|
|
264
|
-
});
|
|
265
|
-
return;
|
|
266
|
-
}
|
|
267
|
-
if (value < min || value > max) {
|
|
268
|
-
ctx.addIssue({
|
|
269
|
-
code: "custom",
|
|
270
|
-
message: `slider value (${value}) must be between min (${min}) and max (${max})`,
|
|
271
|
-
path: ["value"],
|
|
272
|
-
});
|
|
273
|
-
return;
|
|
274
|
-
}
|
|
275
|
-
if (step !== undefined && max > min) {
|
|
276
|
-
const delta = value - min;
|
|
277
|
-
const steps = delta / step;
|
|
278
|
-
const rounded = Math.round(steps);
|
|
279
|
-
if (Math.abs(steps - rounded) > SLIDER_STEP_ALIGN_EPS) {
|
|
280
|
-
ctx.addIssue({
|
|
281
|
-
code: "custom",
|
|
282
|
-
message: `slider value (${value}) is not reachable from min (${min}) with step (${step})`,
|
|
283
|
-
path: ["value"],
|
|
284
|
-
});
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
})
|
|
289
|
-
.transform((val) => ({
|
|
290
|
-
...val,
|
|
291
|
-
value: val.value ?? (val.min + val.max) / 2,
|
|
292
|
-
}));
|
|
293
|
-
|
|
294
|
-
const buttonGroupElementSchema = z
|
|
295
|
-
.object({
|
|
296
|
-
type: z.literal(ELEMENT_TYPE.button_group),
|
|
297
|
-
name: z.string().min(1),
|
|
298
|
-
options: z
|
|
299
|
-
.array(z.string().max(LIMITS.maxButtonGroupOptionChars))
|
|
300
|
-
.min(LIMITS.minButtonGroupOptions)
|
|
301
|
-
.max(LIMITS.maxButtonGroupOptions),
|
|
302
|
-
style: z.enum(BUTTON_GROUP_STYLE_VALUES).optional(),
|
|
303
|
-
})
|
|
304
|
-
.transform((val) => ({
|
|
305
|
-
...val,
|
|
306
|
-
style:
|
|
307
|
-
val.style ??
|
|
308
|
-
(val.options.length <= 3
|
|
309
|
-
? BUTTON_GROUP_STYLE.row
|
|
310
|
-
: BUTTON_GROUP_STYLE.stack),
|
|
311
|
-
}));
|
|
312
|
-
|
|
313
|
-
const toggleElementSchema = z.object({
|
|
314
|
-
type: z.literal(ELEMENT_TYPE.toggle),
|
|
315
|
-
name: z.string().min(1),
|
|
316
|
-
label: z.string().max(60),
|
|
317
|
-
value: z.boolean().default(false),
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
const barChartBarSchema = z.object({
|
|
321
|
-
label: z.string().max(LIMITS.barChartLabelMaxChars),
|
|
322
|
-
value: z.number().nonnegative(),
|
|
323
|
-
color: z.enum(PALETTE_COLOR_VALUES).optional(),
|
|
324
|
-
});
|
|
325
|
-
|
|
326
|
-
const barChartElementSchema = z
|
|
327
|
-
.object({
|
|
328
|
-
type: z.literal(ELEMENT_TYPE.bar_chart),
|
|
329
|
-
bars: z.array(barChartBarSchema).min(1).max(LIMITS.maxBarChartBars),
|
|
330
|
-
max: z.number().nonnegative().optional(),
|
|
331
|
-
color: z.enum(BAR_CHART_COLOR_VALUES).optional(),
|
|
332
|
-
})
|
|
333
|
-
.superRefine((val, ctx) => {
|
|
334
|
-
if (val.max !== undefined) {
|
|
335
|
-
for (let i = 0; i < val.bars.length; i++) {
|
|
336
|
-
const bar = val.bars[i]!;
|
|
337
|
-
if (bar.value > val.max) {
|
|
338
|
-
ctx.addIssue({
|
|
339
|
-
code: "custom",
|
|
340
|
-
message: `bar value (${bar.value}) exceeds chart max (${val.max})`,
|
|
341
|
-
path: ["bars", i, "value"],
|
|
342
|
-
});
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
const buttonActionSchema = z.enum(BUTTON_ACTION_VALUES);
|
|
349
|
-
|
|
350
|
-
const buttonStyleSchema = z.enum(BUTTON_STYLE_VALUES);
|
|
351
|
-
|
|
352
|
-
/* ------------------------------------------------------------------ */
|
|
353
|
-
/* Client action schemas */
|
|
354
|
-
/* ------------------------------------------------------------------ */
|
|
355
|
-
|
|
356
|
-
const viewCastClientActionSchema = z
|
|
357
|
-
.object({
|
|
358
|
-
type: z.literal(CLIENT_ACTION.view_cast),
|
|
359
|
-
hash: z.string().min(1),
|
|
360
|
-
})
|
|
361
|
-
.strict();
|
|
362
|
-
|
|
363
|
-
const viewProfileClientActionSchema = z
|
|
364
|
-
.object({
|
|
365
|
-
type: z.literal(CLIENT_ACTION.view_profile),
|
|
366
|
-
fid: z.number().int().nonnegative(),
|
|
367
|
-
})
|
|
368
|
-
.strict();
|
|
369
|
-
|
|
370
|
-
const composeCastClientActionSchema = z
|
|
371
|
-
.object({
|
|
372
|
-
type: z.literal(CLIENT_ACTION.compose_cast),
|
|
373
|
-
text: z.string().optional(),
|
|
374
|
-
embeds: z
|
|
375
|
-
.array(z.string())
|
|
376
|
-
.max(2, { message: "compose_cast embeds: max 2 URLs" })
|
|
377
|
-
.optional(),
|
|
378
|
-
parent: z
|
|
379
|
-
.object({
|
|
380
|
-
type: z.literal("cast"),
|
|
381
|
-
hash: z.string().min(1),
|
|
382
|
-
})
|
|
383
|
-
.strict()
|
|
384
|
-
.optional(),
|
|
385
|
-
channelKey: z.string().optional(),
|
|
386
|
-
})
|
|
387
|
-
.strict();
|
|
388
|
-
|
|
389
|
-
const viewTokenClientActionSchema = z
|
|
390
|
-
.object({
|
|
391
|
-
type: z.literal(CLIENT_ACTION.view_token),
|
|
392
|
-
/** CAIP-19 asset ID (e.g. "eip155:8453/erc20:0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913") */
|
|
393
|
-
token: z.string().min(1),
|
|
394
|
-
})
|
|
395
|
-
.strict();
|
|
396
|
-
|
|
397
|
-
const sendTokenClientActionSchema = z
|
|
398
|
-
.object({
|
|
399
|
-
type: z.literal(CLIENT_ACTION.send_token),
|
|
400
|
-
/** CAIP-19 asset ID */
|
|
401
|
-
token: z.string().optional(),
|
|
402
|
-
/** Amount in raw token units (e.g. "1000000" for 1 USDC) */
|
|
403
|
-
amount: z.string().optional(),
|
|
404
|
-
recipientFid: z.number().int().nonnegative().optional(),
|
|
405
|
-
recipientAddress: z.string().optional(),
|
|
406
|
-
})
|
|
407
|
-
.strict();
|
|
408
|
-
|
|
409
|
-
const swapTokenClientActionSchema = z
|
|
410
|
-
.object({
|
|
411
|
-
type: z.literal(CLIENT_ACTION.swap_token),
|
|
412
|
-
/** CAIP-19 asset ID to sell */
|
|
413
|
-
sellToken: z.string().optional(),
|
|
414
|
-
/** CAIP-19 asset ID to buy */
|
|
415
|
-
buyToken: z.string().optional(),
|
|
416
|
-
/** Amount in raw token units */
|
|
417
|
-
sellAmount: z.string().optional(),
|
|
418
|
-
})
|
|
419
|
-
.strict();
|
|
420
|
-
|
|
421
|
-
export const clientActionSchema = z.discriminatedUnion("type", [
|
|
422
|
-
viewCastClientActionSchema,
|
|
423
|
-
viewProfileClientActionSchema,
|
|
424
|
-
composeCastClientActionSchema,
|
|
425
|
-
viewTokenClientActionSchema,
|
|
426
|
-
sendTokenClientActionSchema,
|
|
427
|
-
swapTokenClientActionSchema,
|
|
428
|
-
]);
|
|
429
|
-
|
|
430
|
-
export type ClientAction = z.infer<typeof clientActionSchema>;
|
|
431
|
-
|
|
432
|
-
/* ------------------------------------------------------------------ */
|
|
433
|
-
/* Button schema */
|
|
434
|
-
/* ------------------------------------------------------------------ */
|
|
435
|
-
|
|
436
|
-
const buttonSchema = z
|
|
437
|
-
.object({
|
|
438
|
-
label: z.string().min(1).max(LIMITS.maxButtonLabelChars),
|
|
439
|
-
action: buttonActionSchema,
|
|
440
|
-
/** URL target for post/link/mini_app buttons */
|
|
441
|
-
target: z.string().min(1).optional(),
|
|
442
|
-
/** Structured client action for client buttons */
|
|
443
|
-
client_action: clientActionSchema.optional(),
|
|
444
|
-
style: buttonStyleSchema.optional(),
|
|
445
|
-
})
|
|
446
|
-
.superRefine((val, ctx) => {
|
|
447
|
-
if (val.action === BUTTON_ACTION.client) {
|
|
448
|
-
// client buttons require client_action, must not have target
|
|
449
|
-
if (val.client_action === undefined) {
|
|
450
|
-
ctx.addIssue({
|
|
451
|
-
code: "custom",
|
|
452
|
-
message: `button with action "client" must include a "client_action" object`,
|
|
453
|
-
path: ["client_action"],
|
|
454
|
-
});
|
|
455
|
-
}
|
|
456
|
-
if (val.target !== undefined) {
|
|
457
|
-
ctx.addIssue({
|
|
458
|
-
code: "custom",
|
|
459
|
-
message: `button with action "client" must not include "target"`,
|
|
460
|
-
path: ["target"],
|
|
461
|
-
});
|
|
462
|
-
}
|
|
463
|
-
} else {
|
|
464
|
-
// post/link/mini_app buttons require target, must not have client_action
|
|
465
|
-
if (val.target === undefined) {
|
|
466
|
-
ctx.addIssue({
|
|
467
|
-
code: "custom",
|
|
468
|
-
message: `button with action "${val.action}" must include a "target" URL`,
|
|
469
|
-
path: ["target"],
|
|
470
|
-
});
|
|
471
|
-
}
|
|
472
|
-
if (val.client_action !== undefined) {
|
|
473
|
-
ctx.addIssue({
|
|
474
|
-
code: "custom",
|
|
475
|
-
message: `button with action "${val.action}" must not include "client_action"`,
|
|
476
|
-
path: ["client_action"],
|
|
477
|
-
});
|
|
478
|
-
}
|
|
479
|
-
if (
|
|
480
|
-
val.target &&
|
|
481
|
-
(val.action === BUTTON_ACTION.post ||
|
|
482
|
-
val.action === BUTTON_ACTION.link ||
|
|
483
|
-
val.action === BUTTON_ACTION.mini_app) &&
|
|
484
|
-
!isSecureOrLoopbackHttpButtonTarget(val.target)
|
|
485
|
-
) {
|
|
486
|
-
ctx.addIssue({
|
|
487
|
-
code: "custom",
|
|
488
|
-
message: `button target must use HTTPS (or http:// on localhost / 127.0.0.1 for development) for action "${val.action}" (received: ${val.target})`,
|
|
489
|
-
path: ["target"],
|
|
490
|
-
});
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
});
|
|
494
|
-
|
|
495
|
-
export type Button = z.infer<typeof buttonSchema>;
|
|
496
|
-
|
|
497
|
-
/** Child elements allowed inside `group` (no media, no nested group) */
|
|
498
|
-
const groupChildElementSchema = z.discriminatedUnion("type", [
|
|
499
|
-
textElementSchema,
|
|
500
|
-
dividerElementSchema,
|
|
501
|
-
spacerElementSchema,
|
|
502
|
-
progressElementSchema,
|
|
503
|
-
listElementSchema,
|
|
504
|
-
textInputElementSchema,
|
|
505
|
-
sliderElementSchema,
|
|
506
|
-
buttonGroupElementSchema,
|
|
507
|
-
toggleElementSchema,
|
|
508
|
-
barChartElementSchema,
|
|
509
|
-
]);
|
|
510
|
-
|
|
511
|
-
export type GroupChildElement = z.infer<typeof groupChildElementSchema>;
|
|
512
|
-
|
|
513
|
-
const groupElementSchema = z.object({
|
|
514
|
-
type: z.literal(ELEMENT_TYPE.group),
|
|
515
|
-
layout: z.enum(GROUP_LAYOUT_VALUES),
|
|
516
|
-
children: z
|
|
517
|
-
.array(groupChildElementSchema)
|
|
518
|
-
.min(LIMITS.minGroupChildren)
|
|
519
|
-
.max(LIMITS.maxGroupChildren),
|
|
520
|
-
});
|
|
521
|
-
|
|
522
|
-
/** Any single page element, including media and `group` */
|
|
523
|
-
const elementSchema = z.discriminatedUnion("type", [
|
|
524
|
-
textElementSchema,
|
|
525
|
-
imageElementSchema,
|
|
526
|
-
dividerElementSchema,
|
|
527
|
-
spacerElementSchema,
|
|
528
|
-
progressElementSchema,
|
|
529
|
-
listElementSchema,
|
|
530
|
-
gridElementSchema,
|
|
531
|
-
textInputElementSchema,
|
|
532
|
-
sliderElementSchema,
|
|
533
|
-
buttonGroupElementSchema,
|
|
534
|
-
toggleElementSchema,
|
|
535
|
-
groupElementSchema,
|
|
536
|
-
barChartElementSchema,
|
|
537
|
-
]);
|
|
538
|
-
|
|
539
|
-
export type Element = z.infer<typeof elementSchema>;
|
|
540
|
-
|
|
541
|
-
export type SnapPageElementInput = z.input<typeof elementSchema>;
|
|
542
|
-
|
|
543
|
-
const elementsSchema = z
|
|
544
|
-
.object({
|
|
545
|
-
type: z.literal(PAGE_ROOT_TYPE.stack),
|
|
546
|
-
children: z
|
|
547
|
-
.array(elementSchema)
|
|
548
|
-
.min(1, { message: "stack must have at least 1 child element" })
|
|
549
|
-
.max(LIMITS.maxElementsPerPage, {
|
|
550
|
-
message: `cannot have more than ${LIMITS.maxElementsPerPage} elements`,
|
|
551
|
-
}),
|
|
552
|
-
})
|
|
553
|
-
.strict();
|
|
554
|
-
|
|
555
|
-
export type Elements = z.infer<typeof elementsSchema>;
|
|
25
|
+
// ─── Snap response ─────────────────────────────────────
|
|
26
|
+
// `ui` is a json-render Spec — validated by the catalog at runtime,
|
|
27
|
+
// typed here via the json-render Spec type.
|
|
556
28
|
|
|
557
29
|
export const snapResponseSchema = z
|
|
558
30
|
.object({
|
|
559
31
|
version: z.literal(SPEC_VERSION),
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
elements
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
.max(LIMITS.maxButtonsPerPage, {
|
|
571
|
-
message: `cannot have more than ${LIMITS.maxButtonsPerPage} buttons`,
|
|
572
|
-
})
|
|
573
|
-
.optional(),
|
|
574
|
-
})
|
|
575
|
-
.strict()
|
|
576
|
-
.superRefine((page, ctx) => {
|
|
577
|
-
const mediaCount = page.elements.children.filter((el) => {
|
|
578
|
-
return MEDIA_ELEMENT_TYPES.includes(el.type);
|
|
579
|
-
}).length;
|
|
580
|
-
if (mediaCount > 1) {
|
|
581
|
-
ctx.addIssue({
|
|
582
|
-
code: "custom",
|
|
583
|
-
message: `cannot have more than 1 media element (image or grid)`,
|
|
584
|
-
path: ["elements", "children"],
|
|
585
|
-
});
|
|
586
|
-
}
|
|
587
|
-
}),
|
|
32
|
+
theme: themeSchema.optional().default({ accent: DEFAULT_THEME_ACCENT }),
|
|
33
|
+
effects: z.array(z.enum(EFFECT_VALUES)).optional(),
|
|
34
|
+
ui: z.custom<Spec>(
|
|
35
|
+
(val) =>
|
|
36
|
+
val != null &&
|
|
37
|
+
typeof val === "object" &&
|
|
38
|
+
"root" in val &&
|
|
39
|
+
"elements" in val,
|
|
40
|
+
{ message: "ui must be a json-render Spec with root and elements" },
|
|
41
|
+
),
|
|
588
42
|
})
|
|
589
43
|
.strict();
|
|
590
44
|
|
|
591
|
-
// canonical snap response type
|
|
592
45
|
export type SnapResponse = z.infer<typeof snapResponseSchema>;
|
|
593
|
-
// what snap handlers may return (keeps optional fields optional)
|
|
594
46
|
export type SnapHandlerResult = z.input<typeof snapResponseSchema>;
|
|
595
47
|
|
|
596
|
-
//
|
|
597
|
-
export const firstPageResponseSchema = snapResponseSchema.superRefine(
|
|
598
|
-
(response, ctx) => {
|
|
599
|
-
const elements = response.page.elements.children;
|
|
600
|
-
|
|
601
|
-
const hasTextTitleOrBody = elements.some(
|
|
602
|
-
(el) =>
|
|
603
|
-
el.type === ELEMENT_TYPE.text &&
|
|
604
|
-
(el.style === TEXT_STYLE.title || el.style === TEXT_STYLE.body),
|
|
605
|
-
);
|
|
606
|
-
if (!hasTextTitleOrBody) {
|
|
607
|
-
ctx.addIssue({
|
|
608
|
-
code: "custom",
|
|
609
|
-
message:
|
|
610
|
-
'first page must have at least one text element with style "title" or "body"',
|
|
611
|
-
path: ["page", "elements", "children"],
|
|
612
|
-
});
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
const hasInteractive = elements.some((el) =>
|
|
616
|
-
INTERACTIVE_ELEMENT_TYPES.includes(el.type),
|
|
617
|
-
);
|
|
618
|
-
const hasMedia = elements.some((el) =>
|
|
619
|
-
MEDIA_ELEMENT_TYPES.includes(el.type),
|
|
620
|
-
);
|
|
621
|
-
if (!hasInteractive && !hasMedia) {
|
|
622
|
-
ctx.addIssue({
|
|
623
|
-
code: "custom",
|
|
624
|
-
message:
|
|
625
|
-
"first page must have at least one interactive element (button_group, slider, text_input, toggle) or media element (image, grid)",
|
|
626
|
-
path: ["page", "elements", "children"],
|
|
627
|
-
});
|
|
628
|
-
}
|
|
629
|
-
},
|
|
630
|
-
);
|
|
631
|
-
|
|
632
|
-
export type FirstPageResponse = z.infer<typeof firstPageResponseSchema>;
|
|
48
|
+
// ─── POST payload ──────────────────────────────────────
|
|
633
49
|
|
|
634
50
|
const postInputValueSchema = z.union([
|
|
635
51
|
z.string(),
|
|
636
52
|
z.number(),
|
|
637
53
|
z.boolean(),
|
|
638
|
-
z
|
|
639
|
-
.object({
|
|
640
|
-
row: z.number().int().nonnegative(),
|
|
641
|
-
col: z.number().int().nonnegative(),
|
|
642
|
-
})
|
|
643
|
-
.strict(),
|
|
54
|
+
z.array(z.string()),
|
|
644
55
|
]);
|
|
645
56
|
|
|
646
57
|
export const payloadSchema = z
|
|
@@ -648,7 +59,6 @@ export const payloadSchema = z
|
|
|
648
59
|
fid: z.number().int().nonnegative(),
|
|
649
60
|
inputs: z.record(z.string(), postInputValueSchema).default({}),
|
|
650
61
|
button_index: z.number().int().nonnegative(),
|
|
651
|
-
/** Unix time in seconds (wire format matches spec examples). */
|
|
652
62
|
timestamp: z.number().int(),
|
|
653
63
|
})
|
|
654
64
|
.strict();
|
|
@@ -679,42 +89,6 @@ export const snapActionSchema = z.discriminatedUnion("type", [
|
|
|
679
89
|
|
|
680
90
|
export type SnapAction = z.infer<typeof snapActionSchema>;
|
|
681
91
|
|
|
682
|
-
export type DataStoreValue =
|
|
683
|
-
| string
|
|
684
|
-
| number
|
|
685
|
-
| boolean
|
|
686
|
-
| null
|
|
687
|
-
| DataStoreValue[]
|
|
688
|
-
| { [key: string]: DataStoreValue };
|
|
689
|
-
|
|
690
|
-
export type SnapDataStoreOperations = {
|
|
691
|
-
get(key: string): Promise<DataStoreValue | null>;
|
|
692
|
-
set(key: string, value: DataStoreValue): Promise<void>;
|
|
693
|
-
};
|
|
694
|
-
|
|
695
|
-
export type SnapDataStore = SnapDataStoreOperations & {
|
|
696
|
-
withLock<T>(fn: (store: SnapDataStoreOperations) => Promise<T>): Promise<T>;
|
|
697
|
-
};
|
|
698
|
-
|
|
699
|
-
export function createDefaultDataStore(): SnapDataStore {
|
|
700
|
-
const err = new Error(
|
|
701
|
-
"Data store is not configured. Use withUpstash() from @farcaster/snap-upstash or provide a data store implementation.",
|
|
702
|
-
);
|
|
703
|
-
return {
|
|
704
|
-
get(_key: string): Promise<never> {
|
|
705
|
-
return Promise.reject(err);
|
|
706
|
-
},
|
|
707
|
-
set(_key: string, _value: DataStoreValue): Promise<never> {
|
|
708
|
-
return Promise.reject(err);
|
|
709
|
-
},
|
|
710
|
-
withLock<T>(
|
|
711
|
-
_fn: (store: SnapDataStoreOperations) => Promise<T>,
|
|
712
|
-
): Promise<never> {
|
|
713
|
-
return Promise.reject(err);
|
|
714
|
-
},
|
|
715
|
-
};
|
|
716
|
-
}
|
|
717
|
-
|
|
718
92
|
export type SnapContext = {
|
|
719
93
|
action: SnapAction;
|
|
720
94
|
request: Request;
|