@farcaster/snap 1.17.0 → 1.17.2

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.
@@ -7,7 +7,7 @@ import { hexToRgba } from "./use-snap-palette.js";
7
7
  export type { JsonValue, SnapPage, SnapActionHandlers } from "./types.js";
8
8
  export { useSnapTheme, hexToRgba };
9
9
  export type { SnapNativeColors };
10
- export declare function SnapCard({ snap, handlers, loading, appearance, colors, borderRadius, showOverflowWarning, onValidationError, validationErrorFallback, }: {
10
+ export declare function SnapCard({ snap, handlers, loading, appearance, colors, borderRadius, showOverflowWarning, onValidationError, validationErrorFallback, actionError, }: {
11
11
  snap: SnapPage;
12
12
  handlers: SnapActionHandlers;
13
13
  loading?: boolean;
@@ -21,4 +21,6 @@ export declare function SnapCard({ snap, handlers, loading, appearance, colors,
21
21
  onValidationError?: (result: ValidationResult) => void;
22
22
  /** Custom fallback rendered when validation fails (v2 only). */
23
23
  validationErrorFallback?: ReactNode;
24
+ /** Server-side action error message to display inline. */
25
+ actionError?: string | null;
24
26
  }): import("react").JSX.Element;
@@ -7,9 +7,9 @@ import { SnapCardV2 } from "./v2/snap-view.js";
7
7
  // ─── Re-exports ───────────────────────────────────────
8
8
  export { useSnapTheme, hexToRgba };
9
9
  // ─── SnapCard (version-switching) ─────────────────────
10
- export function SnapCard({ snap, handlers, loading = false, appearance = "dark", colors, borderRadius = 16, showOverflowWarning = false, onValidationError, validationErrorFallback, }) {
10
+ export function SnapCard({ snap, handlers, loading = false, appearance = "dark", colors, borderRadius = 16, showOverflowWarning = false, onValidationError, validationErrorFallback, actionError, }) {
11
11
  if (snap.version === SPEC_VERSION_2) {
12
- return (_jsx(SnapCardV2, { snap: snap, handlers: handlers, loading: loading, appearance: appearance, colors: colors, borderRadius: borderRadius, showOverflowWarning: showOverflowWarning, onValidationError: onValidationError, validationErrorFallback: validationErrorFallback }));
12
+ return (_jsx(SnapCardV2, { snap: snap, handlers: handlers, loading: loading, appearance: appearance, colors: colors, borderRadius: borderRadius, showOverflowWarning: showOverflowWarning, onValidationError: onValidationError, validationErrorFallback: validationErrorFallback, actionError: actionError }));
13
13
  }
14
- return (_jsx(SnapCardV1, { snap: snap, handlers: handlers, loading: loading, appearance: appearance, colors: colors, borderRadius: borderRadius }));
14
+ return (_jsx(SnapCardV1, { snap: snap, handlers: handlers, loading: loading, appearance: appearance, colors: colors, borderRadius: borderRadius, actionError: actionError }));
15
15
  }
@@ -12,11 +12,12 @@ export declare function SnapViewV1({ snap, handlers, loading, appearance, colors
12
12
  appearance?: "light" | "dark";
13
13
  colors?: Partial<SnapNativeColors>;
14
14
  }): import("react").JSX.Element;
15
- export declare function SnapCardV1({ snap, handlers, loading, appearance, colors, borderRadius, }: {
15
+ export declare function SnapCardV1({ snap, handlers, loading, appearance, colors, borderRadius, actionError, }: {
16
16
  snap: SnapPage;
17
17
  handlers: SnapActionHandlers;
18
18
  loading?: boolean;
19
19
  appearance?: "light" | "dark";
20
20
  colors?: Partial<SnapNativeColors>;
21
21
  borderRadius?: number;
22
+ actionError?: string | null;
22
23
  }): import("react").JSX.Element;
@@ -1,5 +1,5 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { View, StyleSheet } from "react-native";
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { View, Text, StyleSheet } from "react-native";
3
3
  import { SnapThemeProvider, useSnapTheme } from "../theme.js";
4
4
  import { SnapViewCoreInner } from "../snap-view-core.js";
5
5
  // ─── SnapViewV1 (no validation, no height limits) ────
@@ -10,22 +10,30 @@ export function SnapViewV1({ snap, handlers, loading = false, appearance = "dark
10
10
  return (_jsx(SnapThemeProvider, { appearance: appearance, colors: colors, children: _jsx(SnapViewV1Inner, { snap: snap, handlers: handlers, loading: loading }) }));
11
11
  }
12
12
  // ─── SnapCardV1 (card frame, no height limits) ───────
13
- function SnapCardV1Inner({ snap, handlers, loading = false, borderRadius, }) {
13
+ function SnapCardV1Inner({ snap, handlers, loading = false, borderRadius, actionError, appearance, }) {
14
14
  const { colors } = useSnapTheme();
15
- return (_jsx(View, { style: cardStyles.frameRing, children: _jsx(View, { style: [
16
- cardStyles.card,
17
- {
18
- borderRadius,
19
- borderColor: colors.border,
20
- backgroundColor: colors.surface,
21
- },
22
- ], children: _jsx(View, { style: cardStyles.body, children: _jsx(SnapViewV1Inner, { snap: snap, handlers: handlers, loading: loading }) }) }) }));
15
+ return (_jsxs(_Fragment, { children: [_jsx(View, { style: cardStyles.frameRing, children: _jsx(View, { style: [
16
+ cardStyles.card,
17
+ {
18
+ borderRadius,
19
+ borderColor: colors.border,
20
+ backgroundColor: colors.surface,
21
+ },
22
+ ], children: _jsx(View, { style: cardStyles.body, children: _jsx(SnapViewV1Inner, { snap: snap, handlers: handlers, loading: loading }) }) }) }), actionError && (_jsx(Text, { style: [
23
+ cardStyles.actionError,
24
+ {
25
+ color: appearance === "dark"
26
+ ? "rgba(255,100,100,0.9)"
27
+ : "rgba(200,0,0,0.8)",
28
+ },
29
+ ], children: actionError }))] }));
23
30
  }
24
- export function SnapCardV1({ snap, handlers, loading = false, appearance = "dark", colors, borderRadius = 16, }) {
25
- return (_jsx(SnapThemeProvider, { appearance: appearance, colors: colors, children: _jsx(SnapCardV1Inner, { snap: snap, handlers: handlers, loading: loading, borderRadius: borderRadius }) }));
31
+ export function SnapCardV1({ snap, handlers, loading = false, appearance = "dark", colors, borderRadius = 16, actionError, }) {
32
+ return (_jsx(SnapThemeProvider, { appearance: appearance, colors: colors, children: _jsx(SnapCardV1Inner, { snap: snap, handlers: handlers, loading: loading, borderRadius: borderRadius, actionError: actionError, appearance: appearance }) }));
26
33
  }
27
34
  const cardStyles = StyleSheet.create({
28
35
  frameRing: { alignSelf: "stretch" },
29
36
  card: { overflow: "hidden", borderWidth: 1, minHeight: 120 },
30
37
  body: { paddingHorizontal: 16, paddingVertical: 16 },
38
+ actionError: { paddingHorizontal: 12, paddingVertical: 8, fontSize: 13 },
31
39
  });
@@ -18,7 +18,7 @@ export declare function SnapViewV2({ snap, handlers, loading, appearance, colors
18
18
  onValidationError?: (result: ValidationResult) => void;
19
19
  validationErrorFallback?: ReactNode;
20
20
  }): import("react").JSX.Element;
21
- export declare function SnapCardV2({ snap, handlers, loading, appearance, colors, borderRadius, showOverflowWarning, onValidationError, validationErrorFallback, }: {
21
+ export declare function SnapCardV2({ snap, handlers, loading, appearance, colors, borderRadius, showOverflowWarning, onValidationError, validationErrorFallback, actionError, }: {
22
22
  snap: SnapPage;
23
23
  handlers: SnapActionHandlers;
24
24
  loading?: boolean;
@@ -28,4 +28,5 @@ export declare function SnapCardV2({ snap, handlers, loading, appearance, colors
28
28
  showOverflowWarning?: boolean;
29
29
  onValidationError?: (result: ValidationResult) => void;
30
30
  validationErrorFallback?: ReactNode;
31
+ actionError?: string | null;
31
32
  }): import("react").JSX.Element;
@@ -50,26 +50,34 @@ export function SnapViewV2({ snap, handlers, loading = false, appearance = "dark
50
50
  return (_jsx(SnapThemeProvider, { appearance: appearance, colors: colors, children: _jsx(SnapViewV2Inner, { snap: snap, handlers: handlers, loading: loading, onValidationError: onValidationError, validationErrorFallback: validationErrorFallback }) }));
51
51
  }
52
52
  // ─── SnapCardV2 (card frame + height limits) ─────────
53
- function SnapCardV2Inner({ snap, handlers, loading, borderRadius, showOverflowWarning, onValidationError, validationErrorFallback, }) {
53
+ function SnapCardV2Inner({ snap, handlers, loading, borderRadius, showOverflowWarning, onValidationError, validationErrorFallback, actionError, appearance, }) {
54
54
  const { colors } = useSnapTheme();
55
55
  const maxHeight = showOverflowWarning ? SNAP_WARNING_HEIGHT : SNAP_MAX_HEIGHT;
56
- return (_jsx(View, { style: cardStyles.frameRing, children: _jsxs(View, { style: [
57
- cardStyles.card,
58
- {
59
- borderRadius,
60
- maxHeight,
61
- borderColor: colors.border,
62
- backgroundColor: colors.surface,
63
- },
64
- ], children: [_jsx(View, { style: cardStyles.body, children: _jsx(SnapViewV2Inner, { snap: snap, handlers: handlers, loading: loading, onValidationError: onValidationError, validationErrorFallback: validationErrorFallback }) }), showOverflowWarning && (_jsxs(View, { style: cardStyles.warningOverlay, children: [_jsx(View, { style: cardStyles.warningLine }), _jsx(View, { style: cardStyles.warningLabel, children: _jsxs(Text, { style: cardStyles.warningLabelText, children: [SNAP_MAX_HEIGHT, "px"] }) })] }))] }) }));
56
+ return (_jsxs(_Fragment, { children: [_jsx(View, { style: cardStyles.frameRing, children: _jsxs(View, { style: [
57
+ cardStyles.card,
58
+ {
59
+ borderRadius,
60
+ maxHeight,
61
+ borderColor: colors.border,
62
+ backgroundColor: colors.surface,
63
+ },
64
+ ], children: [_jsx(View, { style: cardStyles.body, children: _jsx(SnapViewV2Inner, { snap: snap, handlers: handlers, loading: loading, onValidationError: onValidationError, validationErrorFallback: validationErrorFallback }) }), showOverflowWarning && (_jsxs(View, { style: cardStyles.warningOverlay, children: [_jsx(View, { style: cardStyles.warningLine }), _jsx(View, { style: cardStyles.warningLabel, children: _jsxs(Text, { style: cardStyles.warningLabelText, children: [SNAP_MAX_HEIGHT, "px"] }) })] }))] }) }), actionError && (_jsx(Text, { style: [
65
+ cardStyles.actionError,
66
+ {
67
+ color: appearance === "dark"
68
+ ? "rgba(255,100,100,0.9)"
69
+ : "rgba(200,0,0,0.8)",
70
+ },
71
+ ], children: actionError }))] }));
65
72
  }
66
- export function SnapCardV2({ snap, handlers, loading = false, appearance = "dark", colors, borderRadius = 16, showOverflowWarning = false, onValidationError, validationErrorFallback, }) {
67
- return (_jsx(SnapThemeProvider, { appearance: appearance, colors: colors, children: _jsx(SnapCardV2Inner, { snap: snap, handlers: handlers, loading: loading, borderRadius: borderRadius, showOverflowWarning: showOverflowWarning, onValidationError: onValidationError, validationErrorFallback: validationErrorFallback }) }));
73
+ export function SnapCardV2({ snap, handlers, loading = false, appearance = "dark", colors, borderRadius = 16, showOverflowWarning = false, onValidationError, validationErrorFallback, actionError, }) {
74
+ return (_jsx(SnapThemeProvider, { appearance: appearance, colors: colors, children: _jsx(SnapCardV2Inner, { snap: snap, handlers: handlers, loading: loading, borderRadius: borderRadius, showOverflowWarning: showOverflowWarning, onValidationError: onValidationError, validationErrorFallback: validationErrorFallback, actionError: actionError, appearance: appearance }) }));
68
75
  }
69
76
  const cardStyles = StyleSheet.create({
70
77
  frameRing: { alignSelf: "stretch" },
71
78
  card: { overflow: "hidden", borderWidth: 1, minHeight: 120 },
72
79
  body: { paddingHorizontal: 16, paddingVertical: 16 },
80
+ actionError: { paddingHorizontal: 12, paddingVertical: 8, fontSize: 13 },
73
81
  warningOverlay: {
74
82
  position: "absolute",
75
83
  top: SNAP_MAX_HEIGHT,
@@ -10,7 +10,11 @@ export declare const snapJsonRenderCatalog: import("@json-render/core").Catalog<
10
10
  root: import("@json-render/core").SchemaType<"string", unknown>;
11
11
  elements: import("@json-render/core").SchemaType<"record", import("@json-render/core").SchemaType<"object", {
12
12
  type: import("@json-render/core").SchemaType<"ref", string>;
13
- props: import("@json-render/core").SchemaType<"propsOf", string>;
13
+ props: {
14
+ optional: true;
15
+ kind: "propsOf";
16
+ inner?: string;
17
+ };
14
18
  children: {
15
19
  optional: true;
16
20
  kind: "array";
@@ -7,7 +7,11 @@ export declare const snapJsonRenderSchema: import("@json-render/core").Schema<{
7
7
  root: import("@json-render/core").SchemaType<"string", unknown>;
8
8
  elements: import("@json-render/core").SchemaType<"record", import("@json-render/core").SchemaType<"object", {
9
9
  type: import("@json-render/core").SchemaType<"ref", string>;
10
- props: import("@json-render/core").SchemaType<"propsOf", string>;
10
+ props: {
11
+ optional: true;
12
+ kind: "propsOf";
13
+ inner?: string;
14
+ };
11
15
  children: {
12
16
  optional: true;
13
17
  kind: "array";
package/dist/ui/schema.js CHANGED
@@ -8,7 +8,7 @@ export const snapJsonRenderSchema = defineSchema((s) => ({
8
8
  root: s.string(),
9
9
  elements: s.record(s.object({
10
10
  type: s.ref("catalog.components"),
11
- props: s.propsOf("catalog.components"),
11
+ props: { ...s.propsOf("catalog.components"), optional: true },
12
12
  children: { ...s.array(s.string()), optional: true },
13
13
  })),
14
14
  }),
package/dist/validator.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { snapResponseSchema } from "./schemas.js";
2
2
  import { MAX_CHILDREN, MAX_DEPTH, MAX_ELEMENTS, MAX_ROOT_CHILDREN, SPEC_VERSION_1 } from "./constants.js";
3
+ import { snapJsonRenderCatalog } from "./ui/catalog.js";
3
4
  // ─── Helpers ──────────────────────────────────────────
4
5
  /** Actions whose `params.target` must be a valid URL. */
5
6
  const URL_TARGET_ACTIONS = new Set(["submit", "open_url", "open_mini_app"]);
@@ -202,6 +203,10 @@ export function validateSnapResponse(json) {
202
203
  if (urlIssues.length > 0) {
203
204
  return { valid: false, issues: urlIssues };
204
205
  }
206
+ const catalogResult = snapJsonRenderCatalog.validate(ui);
207
+ if (!catalogResult.success) {
208
+ return { valid: false, issues: catalogResult.error?.issues ?? [] };
209
+ }
205
210
  }
206
211
  return { valid: true, issues: [] };
207
212
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@farcaster/snap",
3
- "version": "1.17.0",
3
+ "version": "1.17.2",
4
4
  "description": "Farcaster Snaps 🫰",
5
5
  "repository": {
6
6
  "type": "git",
@@ -29,6 +29,7 @@ export function SnapCard({
29
29
  showOverflowWarning = false,
30
30
  onValidationError,
31
31
  validationErrorFallback,
32
+ actionError,
32
33
  }: {
33
34
  snap: SnapPage;
34
35
  handlers: SnapActionHandlers;
@@ -43,6 +44,8 @@ export function SnapCard({
43
44
  onValidationError?: (result: ValidationResult) => void;
44
45
  /** Custom fallback rendered when validation fails (v2 only). */
45
46
  validationErrorFallback?: ReactNode;
47
+ /** Server-side action error message to display inline. */
48
+ actionError?: string | null;
46
49
  }) {
47
50
  if (snap.version === SPEC_VERSION_2) {
48
51
  return (
@@ -56,6 +59,7 @@ export function SnapCard({
56
59
  showOverflowWarning={showOverflowWarning}
57
60
  onValidationError={onValidationError}
58
61
  validationErrorFallback={validationErrorFallback}
62
+ actionError={actionError}
59
63
  />
60
64
  );
61
65
  }
@@ -68,6 +72,7 @@ export function SnapCard({
68
72
  appearance={appearance}
69
73
  colors={colors}
70
74
  borderRadius={borderRadius}
75
+ actionError={actionError}
71
76
  />
72
77
  );
73
78
  }
@@ -1,4 +1,4 @@
1
- import { View, StyleSheet } from "react-native";
1
+ import { View, Text, StyleSheet } from "react-native";
2
2
  import { SnapThemeProvider, useSnapTheme, type SnapNativeColors } from "../theme";
3
3
  import { SnapViewCoreInner } from "../snap-view-core";
4
4
  import type { SnapPage, SnapActionHandlers } from "../types";
@@ -46,31 +46,52 @@ function SnapCardV1Inner({
46
46
  handlers,
47
47
  loading = false,
48
48
  borderRadius,
49
+ actionError,
50
+ appearance,
49
51
  }: {
50
52
  snap: SnapPage;
51
53
  handlers: SnapActionHandlers;
52
54
  loading?: boolean;
53
55
  borderRadius: number;
56
+ actionError?: string | null;
57
+ appearance: "light" | "dark";
54
58
  }) {
55
59
  const { colors } = useSnapTheme();
56
60
 
57
61
  return (
58
- <View style={cardStyles.frameRing}>
59
- <View
60
- style={[
61
- cardStyles.card,
62
- {
63
- borderRadius,
64
- borderColor: colors.border,
65
- backgroundColor: colors.surface,
66
- },
67
- ]}
68
- >
69
- <View style={cardStyles.body}>
70
- <SnapViewV1Inner snap={snap} handlers={handlers} loading={loading} />
62
+ <>
63
+ <View style={cardStyles.frameRing}>
64
+ <View
65
+ style={[
66
+ cardStyles.card,
67
+ {
68
+ borderRadius,
69
+ borderColor: colors.border,
70
+ backgroundColor: colors.surface,
71
+ },
72
+ ]}
73
+ >
74
+ <View style={cardStyles.body}>
75
+ <SnapViewV1Inner snap={snap} handlers={handlers} loading={loading} />
76
+ </View>
71
77
  </View>
72
78
  </View>
73
- </View>
79
+ {actionError && (
80
+ <Text
81
+ style={[
82
+ cardStyles.actionError,
83
+ {
84
+ color:
85
+ appearance === "dark"
86
+ ? "rgba(255,100,100,0.9)"
87
+ : "rgba(200,0,0,0.8)",
88
+ },
89
+ ]}
90
+ >
91
+ {actionError}
92
+ </Text>
93
+ )}
94
+ </>
74
95
  );
75
96
  }
76
97
 
@@ -81,6 +102,7 @@ export function SnapCardV1({
81
102
  appearance = "dark",
82
103
  colors,
83
104
  borderRadius = 16,
105
+ actionError,
84
106
  }: {
85
107
  snap: SnapPage;
86
108
  handlers: SnapActionHandlers;
@@ -88,6 +110,7 @@ export function SnapCardV1({
88
110
  appearance?: "light" | "dark";
89
111
  colors?: Partial<SnapNativeColors>;
90
112
  borderRadius?: number;
113
+ actionError?: string | null;
91
114
  }) {
92
115
  return (
93
116
  <SnapThemeProvider appearance={appearance} colors={colors}>
@@ -96,6 +119,8 @@ export function SnapCardV1({
96
119
  handlers={handlers}
97
120
  loading={loading}
98
121
  borderRadius={borderRadius}
122
+ actionError={actionError}
123
+ appearance={appearance}
99
124
  />
100
125
  </SnapThemeProvider>
101
126
  );
@@ -105,4 +130,5 @@ const cardStyles = StyleSheet.create({
105
130
  frameRing: { alignSelf: "stretch" },
106
131
  card: { overflow: "hidden", borderWidth: 1, minHeight: 120 },
107
132
  body: { paddingHorizontal: 16, paddingVertical: 16 },
133
+ actionError: { paddingHorizontal: 12, paddingVertical: 8, fontSize: 13 },
108
134
  });
@@ -121,6 +121,8 @@ function SnapCardV2Inner({
121
121
  showOverflowWarning,
122
122
  onValidationError,
123
123
  validationErrorFallback,
124
+ actionError,
125
+ appearance,
124
126
  }: {
125
127
  snap: SnapPage;
126
128
  handlers: SnapActionHandlers;
@@ -129,42 +131,61 @@ function SnapCardV2Inner({
129
131
  showOverflowWarning: boolean;
130
132
  onValidationError?: (result: ValidationResult) => void;
131
133
  validationErrorFallback?: ReactNode;
134
+ actionError?: string | null;
135
+ appearance: "light" | "dark";
132
136
  }) {
133
137
  const { colors } = useSnapTheme();
134
138
  const maxHeight = showOverflowWarning ? SNAP_WARNING_HEIGHT : SNAP_MAX_HEIGHT;
135
139
 
136
140
  return (
137
- <View style={cardStyles.frameRing}>
138
- <View
139
- style={[
140
- cardStyles.card,
141
- {
142
- borderRadius,
143
- maxHeight,
144
- borderColor: colors.border,
145
- backgroundColor: colors.surface,
146
- },
147
- ]}
148
- >
149
- <View style={cardStyles.body}>
150
- <SnapViewV2Inner
151
- snap={snap}
152
- handlers={handlers}
153
- loading={loading}
154
- onValidationError={onValidationError}
155
- validationErrorFallback={validationErrorFallback}
156
- />
157
- </View>
158
- {showOverflowWarning && (
159
- <View style={cardStyles.warningOverlay}>
160
- <View style={cardStyles.warningLine} />
161
- <View style={cardStyles.warningLabel}>
162
- <Text style={cardStyles.warningLabelText}>{SNAP_MAX_HEIGHT}px</Text>
163
- </View>
141
+ <>
142
+ <View style={cardStyles.frameRing}>
143
+ <View
144
+ style={[
145
+ cardStyles.card,
146
+ {
147
+ borderRadius,
148
+ maxHeight,
149
+ borderColor: colors.border,
150
+ backgroundColor: colors.surface,
151
+ },
152
+ ]}
153
+ >
154
+ <View style={cardStyles.body}>
155
+ <SnapViewV2Inner
156
+ snap={snap}
157
+ handlers={handlers}
158
+ loading={loading}
159
+ onValidationError={onValidationError}
160
+ validationErrorFallback={validationErrorFallback}
161
+ />
164
162
  </View>
165
- )}
163
+ {showOverflowWarning && (
164
+ <View style={cardStyles.warningOverlay}>
165
+ <View style={cardStyles.warningLine} />
166
+ <View style={cardStyles.warningLabel}>
167
+ <Text style={cardStyles.warningLabelText}>{SNAP_MAX_HEIGHT}px</Text>
168
+ </View>
169
+ </View>
170
+ )}
171
+ </View>
166
172
  </View>
167
- </View>
173
+ {actionError && (
174
+ <Text
175
+ style={[
176
+ cardStyles.actionError,
177
+ {
178
+ color:
179
+ appearance === "dark"
180
+ ? "rgba(255,100,100,0.9)"
181
+ : "rgba(200,0,0,0.8)",
182
+ },
183
+ ]}
184
+ >
185
+ {actionError}
186
+ </Text>
187
+ )}
188
+ </>
168
189
  );
169
190
  }
170
191
 
@@ -178,6 +199,7 @@ export function SnapCardV2({
178
199
  showOverflowWarning = false,
179
200
  onValidationError,
180
201
  validationErrorFallback,
202
+ actionError,
181
203
  }: {
182
204
  snap: SnapPage;
183
205
  handlers: SnapActionHandlers;
@@ -188,6 +210,7 @@ export function SnapCardV2({
188
210
  showOverflowWarning?: boolean;
189
211
  onValidationError?: (result: ValidationResult) => void;
190
212
  validationErrorFallback?: ReactNode;
213
+ actionError?: string | null;
191
214
  }) {
192
215
  return (
193
216
  <SnapThemeProvider appearance={appearance} colors={colors}>
@@ -199,6 +222,8 @@ export function SnapCardV2({
199
222
  showOverflowWarning={showOverflowWarning}
200
223
  onValidationError={onValidationError}
201
224
  validationErrorFallback={validationErrorFallback}
225
+ actionError={actionError}
226
+ appearance={appearance}
202
227
  />
203
228
  </SnapThemeProvider>
204
229
  );
@@ -208,6 +233,7 @@ const cardStyles = StyleSheet.create({
208
233
  frameRing: { alignSelf: "stretch" },
209
234
  card: { overflow: "hidden", borderWidth: 1, minHeight: 120 },
210
235
  body: { paddingHorizontal: 16, paddingVertical: 16 },
236
+ actionError: { paddingHorizontal: 12, paddingVertical: 8, fontSize: 13 },
211
237
  warningOverlay: {
212
238
  position: "absolute",
213
239
  top: SNAP_MAX_HEIGHT,
package/src/ui/schema.ts CHANGED
@@ -11,7 +11,7 @@ export const snapJsonRenderSchema = defineSchema(
11
11
  elements: s.record(
12
12
  s.object({
13
13
  type: s.ref("catalog.components"),
14
- props: s.propsOf("catalog.components"),
14
+ props: { ...s.propsOf("catalog.components"), optional: true },
15
15
  children: { ...s.array(s.string()), optional: true },
16
16
  }),
17
17
  ),
package/src/validator.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { snapResponseSchema } from "./schemas";
3
3
  import { MAX_CHILDREN, MAX_DEPTH, MAX_ELEMENTS, MAX_ROOT_CHILDREN, SPEC_VERSION_1 } from "./constants";
4
+ import { snapJsonRenderCatalog } from "./ui/catalog.js";
4
5
 
5
6
  export type ValidationResult = {
6
7
  valid: boolean;
@@ -255,6 +256,11 @@ export function validateSnapResponse(json: unknown): ValidationResult {
255
256
  if (urlIssues.length > 0) {
256
257
  return { valid: false, issues: urlIssues };
257
258
  }
259
+
260
+ const catalogResult = snapJsonRenderCatalog.validate(ui);
261
+ if (!catalogResult.success) {
262
+ return { valid: false, issues: catalogResult.error?.issues ?? [] };
263
+ }
258
264
  }
259
265
 
260
266
  return { valid: true, issues: [] };