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