@farcaster/snap 2.0.0 → 2.0.1
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/colors.d.ts +4 -4
- package/dist/colors.js +20 -20
- package/dist/constants.d.ts +17 -1
- package/dist/constants.js +19 -1
- package/dist/index.d.ts +4 -6
- package/dist/index.js +2 -4
- package/dist/react/accent-context.d.ts +3 -1
- package/dist/react/accent-context.js +7 -4
- package/dist/react/catalog-renderer.js +4 -0
- package/dist/react/components/action-button.d.ts +2 -1
- package/dist/react/components/action-button.js +32 -13
- package/dist/react/components/badge.js +8 -8
- package/dist/react/components/bar-chart.d.ts +5 -0
- package/dist/react/components/bar-chart.js +26 -0
- package/dist/react/components/cell-grid.d.ts +5 -0
- package/dist/react/components/cell-grid.js +87 -0
- package/dist/react/components/icon.js +4 -10
- package/dist/react/components/input.js +12 -6
- package/dist/react/components/item-group.js +3 -1
- package/dist/react/components/item.d.ts +3 -3
- package/dist/react/components/item.js +4 -3
- package/dist/react/components/progress.js +3 -3
- package/dist/react/components/separator.js +3 -1
- package/dist/react/components/slider.js +15 -10
- package/dist/react/components/switch.js +10 -12
- package/dist/react/components/text.js +6 -14
- package/dist/react/components/toggle-group.js +20 -6
- package/dist/react/hooks/use-snap-colors.d.ts +38 -0
- package/dist/react/hooks/use-snap-colors.js +81 -0
- package/dist/react/index.d.ts +13 -1
- package/dist/react/index.js +9 -188
- package/dist/react/snap-view-core.d.ts +11 -0
- package/dist/react/snap-view-core.js +224 -0
- package/dist/react/v1/snap-view.d.ts +16 -0
- package/dist/react/v1/snap-view.js +90 -0
- package/dist/react/v2/snap-view.d.ts +23 -0
- package/dist/react/v2/snap-view.js +91 -0
- package/dist/react-native/catalog-renderer.d.ts +5 -0
- package/dist/react-native/catalog-renderer.js +40 -0
- package/dist/react-native/components/snap-action-button.d.ts +2 -0
- package/dist/react-native/components/snap-action-button.js +69 -0
- package/dist/react-native/components/snap-badge.d.ts +2 -0
- package/dist/react-native/components/snap-badge.js +41 -0
- package/dist/react-native/components/snap-bar-chart.d.ts +2 -0
- package/dist/react-native/components/snap-bar-chart.js +39 -0
- package/dist/react-native/components/snap-cell-grid.d.ts +2 -0
- package/dist/react-native/components/snap-cell-grid.js +94 -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 +23 -0
- package/dist/react-native/components/snap-input.d.ts +2 -0
- package/dist/react-native/components/snap-input.js +37 -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 +42 -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 +43 -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 +31 -0
- package/dist/react-native/components/snap-text.d.ts +2 -0
- package/dist/react-native/components/snap-text.js +35 -0
- package/dist/react-native/components/snap-toggle-group.d.ts +2 -0
- package/dist/react-native/components/snap-toggle-group.js +99 -0
- package/dist/react-native/confetti-overlay.d.ts +1 -0
- package/dist/react-native/confetti-overlay.js +106 -0
- package/dist/react-native/index.d.ts +28 -0
- package/dist/react-native/index.js +15 -0
- package/dist/react-native/snap-view-core.d.ts +11 -0
- package/dist/react-native/snap-view-core.js +153 -0
- package/dist/react-native/theme.d.ts +27 -0
- package/dist/react-native/theme.js +43 -0
- package/dist/react-native/types.d.ts +42 -0
- package/dist/react-native/types.js +1 -0
- package/dist/react-native/use-snap-palette.d.ts +13 -0
- package/dist/react-native/use-snap-palette.js +48 -0
- package/dist/react-native/v1/snap-view.d.ts +24 -0
- package/dist/react-native/v1/snap-view.js +96 -0
- package/dist/react-native/v2/snap-view.d.ts +33 -0
- package/dist/react-native/v2/snap-view.js +114 -0
- package/dist/schemas.d.ts +100 -13
- package/dist/schemas.js +28 -10
- package/dist/server/parseRequest.d.ts +10 -0
- package/dist/server/parseRequest.js +48 -7
- package/dist/server/verify.d.ts +1 -0
- package/dist/server/verify.js +1 -0
- package/dist/ui/badge.d.ts +7 -2
- package/dist/ui/badge.js +2 -0
- package/dist/ui/bar-chart.d.ts +30 -0
- package/dist/ui/bar-chart.js +30 -0
- package/dist/ui/button.d.ts +4 -6
- package/dist/ui/button.js +1 -1
- package/dist/ui/catalog.d.ts +90 -16
- package/dist/ui/catalog.js +17 -3
- package/dist/ui/cell-grid.d.ts +34 -0
- package/dist/ui/cell-grid.js +39 -0
- package/dist/ui/icon.d.ts +2 -2
- package/dist/ui/image.d.ts +1 -2
- package/dist/ui/image.js +1 -1
- package/dist/ui/index.d.ts +4 -0
- package/dist/ui/index.js +2 -0
- package/dist/ui/item.d.ts +1 -3
- package/dist/ui/item.js +1 -1
- package/dist/ui/schema.d.ts +6 -2
- package/dist/ui/schema.js +2 -2
- package/dist/ui/slider.d.ts +1 -0
- package/dist/ui/slider.js +2 -0
- package/dist/ui/text.d.ts +2 -4
- package/dist/ui/text.js +2 -2
- package/dist/validator.d.ts +3 -2
- package/dist/validator.js +198 -2
- package/llms.txt +199 -0
- package/package.json +9 -3
- package/src/colors.ts +20 -20
- package/src/constants.ts +23 -1
- package/src/index.ts +16 -13
- package/src/react/accent-context.tsx +13 -6
- package/src/react/catalog-renderer.tsx +4 -0
- package/src/react/components/action-button.tsx +47 -20
- package/src/react/components/badge.tsx +14 -18
- package/src/react/components/bar-chart.tsx +69 -0
- package/src/react/components/cell-grid.tsx +128 -0
- package/src/react/components/icon.tsx +5 -18
- package/src/react/components/input.tsx +20 -9
- package/src/react/components/item-group.tsx +4 -1
- package/src/react/components/item.tsx +13 -10
- package/src/react/components/progress.tsx +12 -7
- package/src/react/components/separator.tsx +8 -1
- package/src/react/components/slider.tsx +28 -15
- package/src/react/components/switch.tsx +12 -16
- package/src/react/components/text.tsx +14 -23
- package/src/react/components/toggle-group.tsx +26 -9
- package/src/react/hooks/use-snap-colors.ts +128 -0
- package/src/react/index.tsx +49 -265
- package/src/react/snap-view-core.tsx +340 -0
- package/src/react/v1/snap-view.tsx +176 -0
- package/src/react/v2/snap-view.tsx +199 -0
- package/src/react-native/catalog-renderer.tsx +41 -0
- package/src/react-native/components/snap-action-button.tsx +96 -0
- package/src/react-native/components/snap-badge.tsx +60 -0
- package/src/react-native/components/snap-bar-chart.tsx +73 -0
- package/src/react-native/components/snap-cell-grid.tsx +150 -0
- package/src/react-native/components/snap-icon.tsx +102 -0
- package/src/react-native/components/snap-image.tsx +37 -0
- package/src/react-native/components/snap-input.tsx +58 -0
- package/src/react-native/components/snap-item-group.tsx +43 -0
- package/src/react-native/components/snap-item.tsx +66 -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 +85 -0
- package/src/react-native/components/snap-stack.tsx +66 -0
- package/src/react-native/components/snap-switch.tsx +46 -0
- package/src/react-native/components/snap-text.tsx +51 -0
- package/src/react-native/components/snap-toggle-group.tsx +127 -0
- package/src/react-native/confetti-overlay.tsx +134 -0
- package/src/react-native/index.tsx +83 -0
- package/src/react-native/snap-view-core.tsx +209 -0
- package/src/react-native/theme.tsx +85 -0
- package/src/react-native/types.ts +38 -0
- package/src/react-native/use-snap-palette.ts +64 -0
- package/src/react-native/v1/snap-view.tsx +229 -0
- package/src/react-native/v2/snap-view.tsx +283 -0
- package/src/schemas.ts +68 -17
- package/src/server/parseRequest.ts +68 -9
- package/src/server/verify.ts +2 -0
- package/src/ui/README.md +8 -8
- package/src/ui/badge.ts +2 -0
- package/src/ui/bar-chart.ts +38 -0
- package/src/ui/button.ts +1 -1
- package/src/ui/catalog.ts +19 -3
- package/src/ui/cell-grid.ts +49 -0
- package/src/ui/image.ts +1 -1
- package/src/ui/index.ts +6 -0
- package/src/ui/item.ts +1 -1
- package/src/ui/schema.ts +2 -2
- package/src/ui/slider.ts +2 -0
- package/src/ui/text.ts +2 -2
- package/src/validator.ts +246 -2
- package/dist/dataStore.d.ts +0 -12
- package/dist/dataStore.js +0 -35
- package/dist/middleware.d.ts +0 -3
- package/dist/middleware.js +0 -3
- package/dist/react/hooks/use-snap-accent.d.ts +0 -13
- package/dist/react/hooks/use-snap-accent.js +0 -32
- package/src/dataStore.ts +0 -62
- package/src/middleware.ts +0 -7
- package/src/react/hooks/use-snap-accent.ts +0 -45
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { useEffect, useMemo, useState } from "react";
|
|
3
|
+
import { Platform, StyleSheet, Text, View } from "react-native";
|
|
4
|
+
import { SnapThemeProvider, useSnapTheme, type SnapNativeColors } from "../theme";
|
|
5
|
+
import { SnapViewCoreInner } from "../snap-view-core";
|
|
6
|
+
import {
|
|
7
|
+
validateSnapResponse,
|
|
8
|
+
type ValidationResult,
|
|
9
|
+
} from "@farcaster/snap";
|
|
10
|
+
import type { SnapPage, SnapActionHandlers } from "../types";
|
|
11
|
+
|
|
12
|
+
// ─── Constants ───────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
const SNAP_MAX_HEIGHT = 500;
|
|
15
|
+
const SNAP_WARNING_HEIGHT = 700;
|
|
16
|
+
|
|
17
|
+
// ─── Validation fallback ─────────────────────────────
|
|
18
|
+
|
|
19
|
+
function SnapValidationFallback({ message }: { message?: string }) {
|
|
20
|
+
const { colors } = useSnapTheme();
|
|
21
|
+
return (
|
|
22
|
+
<View style={fallbackStyles.container}>
|
|
23
|
+
<Text style={[fallbackStyles.text, { color: colors.textSecondary }]}>
|
|
24
|
+
{message ? `Unable to render snap: ${message}` : "Unable to render snap"}
|
|
25
|
+
</Text>
|
|
26
|
+
</View>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const fallbackStyles = StyleSheet.create({
|
|
31
|
+
container: {
|
|
32
|
+
width: "100%",
|
|
33
|
+
padding: 16,
|
|
34
|
+
alignItems: "center",
|
|
35
|
+
justifyContent: "center",
|
|
36
|
+
},
|
|
37
|
+
text: {
|
|
38
|
+
fontSize: 14,
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// ─── SnapViewV2 (with validation) ────────────────────
|
|
43
|
+
|
|
44
|
+
export function SnapViewV2Inner({
|
|
45
|
+
snap,
|
|
46
|
+
handlers,
|
|
47
|
+
loading = false,
|
|
48
|
+
onValidationError,
|
|
49
|
+
validationErrorFallback,
|
|
50
|
+
}: {
|
|
51
|
+
snap: SnapPage;
|
|
52
|
+
handlers: SnapActionHandlers;
|
|
53
|
+
loading?: boolean;
|
|
54
|
+
onValidationError?: (result: ValidationResult) => void;
|
|
55
|
+
validationErrorFallback?: ReactNode;
|
|
56
|
+
}) {
|
|
57
|
+
const validation = useMemo(() => validateSnapResponse(snap), [snap]);
|
|
58
|
+
const valid = validation.valid;
|
|
59
|
+
const validationMessage = validation.issues[0]?.message;
|
|
60
|
+
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
if (!valid) {
|
|
63
|
+
if (onValidationError) {
|
|
64
|
+
onValidationError(validation);
|
|
65
|
+
} else {
|
|
66
|
+
// eslint-disable-next-line no-console
|
|
67
|
+
console.warn("[Snap] validation issues:", validation.issues);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}, [valid, validation, onValidationError]);
|
|
71
|
+
|
|
72
|
+
if (!valid) {
|
|
73
|
+
if (validationErrorFallback === null) return null;
|
|
74
|
+
return (
|
|
75
|
+
<>{validationErrorFallback ?? <SnapValidationFallback message={validationMessage} />}</>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<SnapViewCoreInner snap={snap} handlers={handlers} loading={loading} />
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function SnapViewV2({
|
|
85
|
+
snap,
|
|
86
|
+
handlers,
|
|
87
|
+
loading = false,
|
|
88
|
+
appearance = "dark",
|
|
89
|
+
colors,
|
|
90
|
+
onValidationError,
|
|
91
|
+
validationErrorFallback,
|
|
92
|
+
}: {
|
|
93
|
+
snap: SnapPage;
|
|
94
|
+
handlers: SnapActionHandlers;
|
|
95
|
+
loading?: boolean;
|
|
96
|
+
appearance?: "light" | "dark";
|
|
97
|
+
colors?: Partial<SnapNativeColors>;
|
|
98
|
+
onValidationError?: (result: ValidationResult) => void;
|
|
99
|
+
validationErrorFallback?: ReactNode;
|
|
100
|
+
}) {
|
|
101
|
+
return (
|
|
102
|
+
<SnapThemeProvider appearance={appearance} colors={colors}>
|
|
103
|
+
<SnapViewV2Inner
|
|
104
|
+
snap={snap}
|
|
105
|
+
handlers={handlers}
|
|
106
|
+
loading={loading}
|
|
107
|
+
onValidationError={onValidationError}
|
|
108
|
+
validationErrorFallback={validationErrorFallback}
|
|
109
|
+
/>
|
|
110
|
+
</SnapThemeProvider>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ─── SnapCardV2 (card frame + height limits) ─────────
|
|
115
|
+
|
|
116
|
+
function SnapCardV2Inner({
|
|
117
|
+
snap,
|
|
118
|
+
handlers,
|
|
119
|
+
loading,
|
|
120
|
+
borderRadius,
|
|
121
|
+
showOverflowWarning,
|
|
122
|
+
onValidationError,
|
|
123
|
+
validationErrorFallback,
|
|
124
|
+
actionError,
|
|
125
|
+
appearance,
|
|
126
|
+
plain,
|
|
127
|
+
}: {
|
|
128
|
+
snap: SnapPage;
|
|
129
|
+
handlers: SnapActionHandlers;
|
|
130
|
+
loading?: boolean;
|
|
131
|
+
borderRadius: number;
|
|
132
|
+
showOverflowWarning: boolean;
|
|
133
|
+
onValidationError?: (result: ValidationResult) => void;
|
|
134
|
+
validationErrorFallback?: ReactNode;
|
|
135
|
+
actionError?: string | null;
|
|
136
|
+
appearance: "light" | "dark";
|
|
137
|
+
plain: boolean;
|
|
138
|
+
}) {
|
|
139
|
+
const { colors } = useSnapTheme();
|
|
140
|
+
const [contentHeight, setContentHeight] = useState(0);
|
|
141
|
+
|
|
142
|
+
const content = (
|
|
143
|
+
<SnapViewV2Inner
|
|
144
|
+
snap={snap}
|
|
145
|
+
handlers={handlers}
|
|
146
|
+
loading={loading}
|
|
147
|
+
onValidationError={onValidationError}
|
|
148
|
+
validationErrorFallback={validationErrorFallback}
|
|
149
|
+
/>
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
if (plain) {
|
|
153
|
+
return content;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const overflowAmount = showOverflowWarning ? contentHeight - SNAP_MAX_HEIGHT : 0;
|
|
157
|
+
|
|
158
|
+
return (
|
|
159
|
+
<>
|
|
160
|
+
<View
|
|
161
|
+
style={{
|
|
162
|
+
borderRadius,
|
|
163
|
+
borderWidth: 1,
|
|
164
|
+
borderColor: colors.border,
|
|
165
|
+
backgroundColor: colors.surface,
|
|
166
|
+
maxHeight: showOverflowWarning ? undefined : SNAP_MAX_HEIGHT,
|
|
167
|
+
overflow: "hidden",
|
|
168
|
+
minHeight: 120,
|
|
169
|
+
}}
|
|
170
|
+
>
|
|
171
|
+
<View
|
|
172
|
+
collapsable={false}
|
|
173
|
+
onLayout={(e) => setContentHeight(Math.round(e.nativeEvent.layout.height))}
|
|
174
|
+
style={{ paddingHorizontal: 16, paddingVertical: 16 }}
|
|
175
|
+
>
|
|
176
|
+
{content}
|
|
177
|
+
</View>
|
|
178
|
+
{showOverflowWarning && contentHeight > SNAP_MAX_HEIGHT && (
|
|
179
|
+
<View style={{ position: "absolute", top: SNAP_MAX_HEIGHT, left: 0, right: 0, height: overflowAmount, zIndex: 10, pointerEvents: "none" }}>
|
|
180
|
+
<View style={{ height: 1, borderTopWidth: 1, borderStyle: "dashed", borderColor: "rgba(255,100,100,0.6)" }} />
|
|
181
|
+
<View style={{ position: "absolute", top: -10, right: 4, backgroundColor: "rgba(0,0,0,0.7)", paddingHorizontal: 4, paddingVertical: 1, borderRadius: 3 }}>
|
|
182
|
+
<Text style={{ fontSize: 10, color: "rgba(255,100,100,0.7)", fontFamily: Platform.select({ ios: "Menlo", default: "monospace" }) }}>{SNAP_MAX_HEIGHT}px</Text>
|
|
183
|
+
</View>
|
|
184
|
+
<View style={{ flex: 1, backgroundColor: "rgba(255,50,50,0.15)" }} />
|
|
185
|
+
</View>
|
|
186
|
+
)}
|
|
187
|
+
</View>
|
|
188
|
+
{actionError && (
|
|
189
|
+
<Text
|
|
190
|
+
style={{
|
|
191
|
+
paddingHorizontal: 12,
|
|
192
|
+
paddingVertical: 8,
|
|
193
|
+
fontSize: 13,
|
|
194
|
+
color:
|
|
195
|
+
appearance === "dark"
|
|
196
|
+
? "rgba(255,100,100,0.9)"
|
|
197
|
+
: "rgba(200,0,0,0.8)",
|
|
198
|
+
}}
|
|
199
|
+
>
|
|
200
|
+
{actionError}
|
|
201
|
+
</Text>
|
|
202
|
+
)}
|
|
203
|
+
</>
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function SnapCardV2({
|
|
208
|
+
snap,
|
|
209
|
+
handlers,
|
|
210
|
+
loading = false,
|
|
211
|
+
appearance = "dark",
|
|
212
|
+
colors,
|
|
213
|
+
borderRadius = 16,
|
|
214
|
+
showOverflowWarning = false,
|
|
215
|
+
onValidationError,
|
|
216
|
+
validationErrorFallback,
|
|
217
|
+
actionError,
|
|
218
|
+
plain = false,
|
|
219
|
+
}: {
|
|
220
|
+
snap: SnapPage;
|
|
221
|
+
handlers: SnapActionHandlers;
|
|
222
|
+
loading?: boolean;
|
|
223
|
+
appearance?: "light" | "dark";
|
|
224
|
+
colors?: Partial<SnapNativeColors>;
|
|
225
|
+
borderRadius?: number;
|
|
226
|
+
showOverflowWarning?: boolean;
|
|
227
|
+
onValidationError?: (result: ValidationResult) => void;
|
|
228
|
+
validationErrorFallback?: ReactNode;
|
|
229
|
+
actionError?: string | null;
|
|
230
|
+
plain?: boolean;
|
|
231
|
+
}) {
|
|
232
|
+
return (
|
|
233
|
+
<SnapThemeProvider appearance={appearance} colors={colors}>
|
|
234
|
+
<SnapCardV2Inner
|
|
235
|
+
snap={snap}
|
|
236
|
+
handlers={handlers}
|
|
237
|
+
loading={loading}
|
|
238
|
+
borderRadius={borderRadius}
|
|
239
|
+
showOverflowWarning={showOverflowWarning}
|
|
240
|
+
onValidationError={onValidationError}
|
|
241
|
+
validationErrorFallback={validationErrorFallback}
|
|
242
|
+
actionError={actionError}
|
|
243
|
+
appearance={appearance}
|
|
244
|
+
plain={plain}
|
|
245
|
+
/>
|
|
246
|
+
</SnapThemeProvider>
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const cardStyles = StyleSheet.create({
|
|
251
|
+
frameRing: { alignSelf: "stretch" },
|
|
252
|
+
card: { borderWidth: 1, minHeight: 120, overflow: "hidden" },
|
|
253
|
+
body: { paddingHorizontal: 16, paddingVertical: 16 },
|
|
254
|
+
actionError: { paddingHorizontal: 12, paddingVertical: 8, fontSize: 13 },
|
|
255
|
+
warningOverlay: {
|
|
256
|
+
position: "absolute",
|
|
257
|
+
top: SNAP_MAX_HEIGHT,
|
|
258
|
+
left: 0,
|
|
259
|
+
right: 0,
|
|
260
|
+
bottom: 0,
|
|
261
|
+
zIndex: 10,
|
|
262
|
+
},
|
|
263
|
+
warningLine: {
|
|
264
|
+
height: 1,
|
|
265
|
+
borderTopWidth: 1,
|
|
266
|
+
borderStyle: "dashed",
|
|
267
|
+
borderColor: "rgba(255,100,100,0.6)",
|
|
268
|
+
},
|
|
269
|
+
warningLabel: {
|
|
270
|
+
position: "absolute",
|
|
271
|
+
top: -10,
|
|
272
|
+
right: 4,
|
|
273
|
+
backgroundColor: "rgba(0,0,0,0.7)",
|
|
274
|
+
paddingHorizontal: 4,
|
|
275
|
+
paddingVertical: 1,
|
|
276
|
+
borderRadius: 3,
|
|
277
|
+
},
|
|
278
|
+
warningLabelText: {
|
|
279
|
+
fontSize: 10,
|
|
280
|
+
color: "rgba(255,100,100,0.7)",
|
|
281
|
+
fontFamily: Platform.select({ ios: "Menlo", default: "monospace" }),
|
|
282
|
+
},
|
|
283
|
+
});
|
package/src/schemas.ts
CHANGED
|
@@ -2,13 +2,10 @@ import { z } from "zod";
|
|
|
2
2
|
import type { Spec } from "@json-render/core";
|
|
3
3
|
import {
|
|
4
4
|
EFFECT_VALUES,
|
|
5
|
-
|
|
5
|
+
SUPPORTED_SPEC_VERSIONS,
|
|
6
|
+
type SpecVersion,
|
|
6
7
|
} from "./constants";
|
|
7
|
-
import {
|
|
8
|
-
DEFAULT_THEME_ACCENT,
|
|
9
|
-
PALETTE_COLOR_VALUES,
|
|
10
|
-
} from "./colors";
|
|
11
|
-
import { type SnapDataStore } from "./dataStore";
|
|
8
|
+
import { DEFAULT_THEME_ACCENT, PALETTE_COLOR_VALUES } from "./colors";
|
|
12
9
|
|
|
13
10
|
// ─── Theme ─────────────────────────────────────────────
|
|
14
11
|
|
|
@@ -28,7 +25,7 @@ const themeSchema = z
|
|
|
28
25
|
|
|
29
26
|
export const snapResponseSchema = z
|
|
30
27
|
.object({
|
|
31
|
-
version: z.
|
|
28
|
+
version: z.enum(SUPPORTED_SPEC_VERSIONS),
|
|
32
29
|
theme: themeSchema.optional().default({ accent: DEFAULT_THEME_ACCENT }),
|
|
33
30
|
effects: z.array(z.enum(EFFECT_VALUES)).optional(),
|
|
34
31
|
ui: z.custom<Spec>(
|
|
@@ -43,7 +40,41 @@ export const snapResponseSchema = z
|
|
|
43
40
|
.strict();
|
|
44
41
|
|
|
45
42
|
export type SnapResponse = z.infer<typeof snapResponseSchema>;
|
|
46
|
-
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Permissive element input type for snap handler authors.
|
|
46
|
+
* Allows dynamic element construction without requiring exact UIElement types.
|
|
47
|
+
*/
|
|
48
|
+
export type SnapElementInput = {
|
|
49
|
+
type: string;
|
|
50
|
+
props?: Record<string, unknown>;
|
|
51
|
+
children?: string[];
|
|
52
|
+
on?: Record<string, unknown>;
|
|
53
|
+
[key: string]: unknown;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Permissive input type for the `ui` field in snap handler return values.
|
|
58
|
+
* Accepts dynamically-built element maps (e.g. `Record<string, SnapElementInput>`)
|
|
59
|
+
* without requiring exact UIElement types.
|
|
60
|
+
*/
|
|
61
|
+
export type SnapSpecInput = {
|
|
62
|
+
root: string;
|
|
63
|
+
elements: Record<string, SnapElementInput>;
|
|
64
|
+
state?: Record<string, unknown>;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Return type for snap handler functions.
|
|
69
|
+
* Uses permissive input types so handlers can build elements dynamically
|
|
70
|
+
* without type casts. Runtime validation via the Zod schema still catches invalid shapes.
|
|
71
|
+
*/
|
|
72
|
+
export type SnapHandlerResult = {
|
|
73
|
+
version: SpecVersion;
|
|
74
|
+
theme?: { accent?: z.input<typeof themeAccentSchema> };
|
|
75
|
+
effects?: z.input<typeof snapResponseSchema>["effects"];
|
|
76
|
+
ui: SnapSpecInput;
|
|
77
|
+
};
|
|
47
78
|
|
|
48
79
|
// ─── POST payload ──────────────────────────────────────
|
|
49
80
|
|
|
@@ -54,14 +85,37 @@ const postInputValueSchema = z.union([
|
|
|
54
85
|
z.array(z.string()),
|
|
55
86
|
]);
|
|
56
87
|
|
|
88
|
+
const standaloneSurfaceSchema = z.object({
|
|
89
|
+
type: z.literal("standalone"),
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const castSurfaceSchema = z.object({
|
|
93
|
+
type: z.literal("cast"),
|
|
94
|
+
cast: z.object({
|
|
95
|
+
hash: z.string(),
|
|
96
|
+
author: z.object({
|
|
97
|
+
fid: z.number().int().nonnegative(),
|
|
98
|
+
}),
|
|
99
|
+
}),
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const surfaceSchema = z.discriminatedUnion("type", [
|
|
103
|
+
castSurfaceSchema,
|
|
104
|
+
standaloneSurfaceSchema,
|
|
105
|
+
]);
|
|
106
|
+
|
|
57
107
|
export const payloadSchema = z
|
|
58
108
|
.object({
|
|
59
|
-
fid: z.number().int().nonnegative(),
|
|
109
|
+
fid: z.number().int().nonnegative().optional(), // deprecated in favor of user.fid
|
|
60
110
|
inputs: z.record(z.string(), postInputValueSchema).default({}),
|
|
61
|
-
button_index: z.number().int().nonnegative(),
|
|
62
111
|
timestamp: z.number().int(),
|
|
112
|
+
audience: z.string(),
|
|
113
|
+
user: z.object({
|
|
114
|
+
fid: z.number().int().nonnegative(),
|
|
115
|
+
}),
|
|
116
|
+
surface: surfaceSchema,
|
|
63
117
|
})
|
|
64
|
-
.
|
|
118
|
+
.strip();
|
|
65
119
|
|
|
66
120
|
export type SnapPayload = z.infer<typeof payloadSchema>;
|
|
67
121
|
|
|
@@ -74,11 +128,9 @@ const snapGetActionSchema = z.object({
|
|
|
74
128
|
|
|
75
129
|
export type SnapGetAction = z.infer<typeof snapGetActionSchema>;
|
|
76
130
|
|
|
77
|
-
const snapPostActionSchema = payloadSchema
|
|
78
|
-
.
|
|
79
|
-
|
|
80
|
-
})
|
|
81
|
-
.strict();
|
|
131
|
+
const snapPostActionSchema = payloadSchema.extend({
|
|
132
|
+
type: z.literal(ACTION_TYPE_POST),
|
|
133
|
+
});
|
|
82
134
|
|
|
83
135
|
export type SnapPostAction = z.infer<typeof snapPostActionSchema>;
|
|
84
136
|
|
|
@@ -92,7 +144,6 @@ export type SnapAction = z.infer<typeof snapActionSchema>;
|
|
|
92
144
|
export type SnapContext = {
|
|
93
145
|
action: SnapAction;
|
|
94
146
|
request: Request;
|
|
95
|
-
data: SnapDataStore;
|
|
96
147
|
};
|
|
97
148
|
|
|
98
149
|
export type SnapFunction = (ctx: SnapContext) => Promise<SnapHandlerResult>;
|
|
@@ -29,6 +29,14 @@ export type ParseRequestError =
|
|
|
29
29
|
| {
|
|
30
30
|
type: "signature";
|
|
31
31
|
message: string;
|
|
32
|
+
}
|
|
33
|
+
| {
|
|
34
|
+
type: "origin_mismatch";
|
|
35
|
+
message: string;
|
|
36
|
+
}
|
|
37
|
+
| {
|
|
38
|
+
type: "fid_mismatch";
|
|
39
|
+
message: string;
|
|
32
40
|
};
|
|
33
41
|
|
|
34
42
|
export type ParseRequestOptions = {
|
|
@@ -43,6 +51,11 @@ export type ParseRequestOptions = {
|
|
|
43
51
|
* potential replays. Defaults to 300 (5 minutes) when not provided.
|
|
44
52
|
*/
|
|
45
53
|
maxSkewSeconds?: number;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The origin of the request. Derived from the request when not provided.
|
|
57
|
+
*/
|
|
58
|
+
requestOrigin?: string;
|
|
46
59
|
};
|
|
47
60
|
|
|
48
61
|
export type ParseRequestResult =
|
|
@@ -107,6 +120,18 @@ export async function parseRequest(
|
|
|
107
120
|
};
|
|
108
121
|
}
|
|
109
122
|
|
|
123
|
+
const payloadParsed = payloadSchema.safeParse(
|
|
124
|
+
decodePayload(parsed.data.payload),
|
|
125
|
+
);
|
|
126
|
+
if (!payloadParsed.success) {
|
|
127
|
+
return {
|
|
128
|
+
success: false,
|
|
129
|
+
error: { type: "validation", issues: payloadParsed.error.issues },
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const body = payloadParsed.data;
|
|
134
|
+
|
|
110
135
|
if (!options.skipJFSVerification) {
|
|
111
136
|
const jfs = await verifyJFSRequestBody(parsed.data);
|
|
112
137
|
if (!jfs.valid) {
|
|
@@ -115,28 +140,62 @@ export async function parseRequest(
|
|
|
115
140
|
error: { type: "signature", message: jfs.error.message },
|
|
116
141
|
};
|
|
117
142
|
}
|
|
143
|
+
if (jfs.signingUserFid !== body.user.fid) {
|
|
144
|
+
return {
|
|
145
|
+
success: false,
|
|
146
|
+
error: {
|
|
147
|
+
type: "fid_mismatch",
|
|
148
|
+
message: `JFS header fid "${jfs.signingUserFid}" does not match user.fid "${body.user.fid}"`,
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
}
|
|
118
152
|
}
|
|
119
153
|
|
|
120
|
-
|
|
121
|
-
decodePayload(parsed.data.payload),
|
|
122
|
-
);
|
|
123
|
-
if (!payloadParsed.success) {
|
|
154
|
+
if (Math.abs(nowSec - body.timestamp) > maxSkew) {
|
|
124
155
|
return {
|
|
125
156
|
success: false,
|
|
126
|
-
error: {
|
|
157
|
+
error: {
|
|
158
|
+
type: "replay",
|
|
159
|
+
message: `timestamp outside allowed skew of ${maxSkew}s`,
|
|
160
|
+
},
|
|
127
161
|
};
|
|
128
162
|
}
|
|
129
163
|
|
|
130
|
-
|
|
131
|
-
|
|
164
|
+
// Audience validation: ensure the payload audience matches the server origin.
|
|
165
|
+
let expectedOrigin = options.requestOrigin;
|
|
166
|
+
if (expectedOrigin === undefined) {
|
|
167
|
+
try {
|
|
168
|
+
const url = new URL(request.url);
|
|
169
|
+
const proto =
|
|
170
|
+
request.headers.get("x-forwarded-proto") ??
|
|
171
|
+
url.protocol.replace(":", "");
|
|
172
|
+
const host = request.headers.get("x-forwarded-host") ?? url.host;
|
|
173
|
+
expectedOrigin = `${proto}://${host}`;
|
|
174
|
+
} catch {
|
|
175
|
+
// do nothing
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (expectedOrigin !== undefined && body.audience !== expectedOrigin) {
|
|
132
180
|
return {
|
|
133
181
|
success: false,
|
|
134
182
|
error: {
|
|
135
|
-
type: "
|
|
136
|
-
message: `
|
|
183
|
+
type: "origin_mismatch",
|
|
184
|
+
message: `payload audience "${body.audience}" does not match expected origin "${expectedOrigin}"`,
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (body.fid !== undefined && body.fid !== body.user.fid) {
|
|
190
|
+
return {
|
|
191
|
+
success: false,
|
|
192
|
+
error: {
|
|
193
|
+
type: "fid_mismatch",
|
|
194
|
+
message: `fid "${body.fid}" does not match user.fid "${body.user.fid}"`,
|
|
137
195
|
},
|
|
138
196
|
};
|
|
139
197
|
}
|
|
198
|
+
|
|
140
199
|
return {
|
|
141
200
|
success: true,
|
|
142
201
|
action: {
|
package/src/server/verify.ts
CHANGED
|
@@ -27,6 +27,7 @@ export async function verifyJFSRequestBody<TPayload>(
|
|
|
27
27
|
}
|
|
28
28
|
| {
|
|
29
29
|
valid: true;
|
|
30
|
+
signingUserFid: number; // the FID of the user who signed the request
|
|
30
31
|
data: TPayload;
|
|
31
32
|
}
|
|
32
33
|
> {
|
|
@@ -108,6 +109,7 @@ export async function verifyJFSRequestBody<TPayload>(
|
|
|
108
109
|
return {
|
|
109
110
|
valid: true,
|
|
110
111
|
data: payload,
|
|
112
|
+
signingUserFid: header.fid,
|
|
111
113
|
};
|
|
112
114
|
}
|
|
113
115
|
|
package/src/ui/README.md
CHANGED
|
@@ -8,14 +8,14 @@ Snaps use a fixed set of named colors called the **palette**:
|
|
|
8
8
|
|
|
9
9
|
| Name | Light hex | Dark hex |
|
|
10
10
|
| -------- | --------- | --------- |
|
|
11
|
-
| `gray` | `#
|
|
12
|
-
| `blue` | `#
|
|
13
|
-
| `red` | `#
|
|
14
|
-
| `amber` | `#
|
|
15
|
-
| `green` | `#
|
|
16
|
-
| `teal` | `#
|
|
17
|
-
| `purple` | `#
|
|
18
|
-
| `pink` | `#
|
|
11
|
+
| `gray` | `#6E6A86` | `#908CAA` |
|
|
12
|
+
| `blue` | `#286983` | `#9CCFD8` |
|
|
13
|
+
| `red` | `#B4637A` | `#EB6F92` |
|
|
14
|
+
| `amber` | `#EA9D34` | `#F6C177` |
|
|
15
|
+
| `green` | `#3E8F8F` | `#56D4A4` |
|
|
16
|
+
| `teal` | `#56949F` | `#3E8FB0` |
|
|
17
|
+
| `purple` | `#907AA9` | `#C4A7E7` |
|
|
18
|
+
| `pink` | `#D7827E` | `#EBBCBA` |
|
|
19
19
|
|
|
20
20
|
These are exported from `@farcaster/snap` as `PALETTE_LIGHT_HEX`, `PALETTE_DARK_HEX`, and the `PaletteColor` type. Clients resolve the correct hex for their current light/dark mode.
|
|
21
21
|
|
package/src/ui/badge.ts
CHANGED
|
@@ -2,10 +2,12 @@ import { z } from "zod";
|
|
|
2
2
|
import { PROGRESS_COLOR_VALUES } from "../colors.js";
|
|
3
3
|
import { ICON_NAMES } from "./icon.js";
|
|
4
4
|
|
|
5
|
+
export const BADGE_VARIANTS = ["default", "outline"] as const;
|
|
5
6
|
export const BADGE_MAX_LABEL_CHARS = 30;
|
|
6
7
|
|
|
7
8
|
export const badgeProps = z.object({
|
|
8
9
|
label: z.string().min(1).max(BADGE_MAX_LABEL_CHARS),
|
|
10
|
+
variant: z.enum(BADGE_VARIANTS).optional(),
|
|
9
11
|
color: z.enum(PROGRESS_COLOR_VALUES).optional(),
|
|
10
12
|
icon: z.enum(ICON_NAMES).optional(),
|
|
11
13
|
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { BAR_CHART_COLOR_VALUES, PALETTE_COLOR_VALUES } from "../colors.js";
|
|
3
|
+
import {
|
|
4
|
+
BAR_CHART_MAX_BARS,
|
|
5
|
+
BAR_CHART_LABEL_MAX_CHARS,
|
|
6
|
+
} from "../constants.js";
|
|
7
|
+
|
|
8
|
+
export const barChartProps = z
|
|
9
|
+
.object({
|
|
10
|
+
bars: z
|
|
11
|
+
.array(
|
|
12
|
+
z.object({
|
|
13
|
+
label: z.string().min(1).max(BAR_CHART_LABEL_MAX_CHARS),
|
|
14
|
+
value: z.number().nonnegative(),
|
|
15
|
+
color: z.enum(PALETTE_COLOR_VALUES).optional(),
|
|
16
|
+
}),
|
|
17
|
+
)
|
|
18
|
+
.min(1)
|
|
19
|
+
.max(BAR_CHART_MAX_BARS),
|
|
20
|
+
max: z.number().nonnegative().optional(),
|
|
21
|
+
color: z.enum(BAR_CHART_COLOR_VALUES).optional(),
|
|
22
|
+
})
|
|
23
|
+
.superRefine((val, ctx) => {
|
|
24
|
+
if (val.max !== undefined) {
|
|
25
|
+
for (let i = 0; i < val.bars.length; i++) {
|
|
26
|
+
const bar = val.bars[i]!;
|
|
27
|
+
if (bar.value > val.max) {
|
|
28
|
+
ctx.addIssue({
|
|
29
|
+
code: "custom",
|
|
30
|
+
message: `bar value (${bar.value}) exceeds chart max (${val.max})`,
|
|
31
|
+
path: ["bars", i, "value"],
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
export type BarChartProps = z.infer<typeof barChartProps>;
|
package/src/ui/button.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { ICON_NAMES } from "./icon.js";
|
|
3
3
|
|
|
4
|
-
export const BUTTON_VARIANTS = ["
|
|
4
|
+
export const BUTTON_VARIANTS = ["secondary", "primary"] as const;
|
|
5
5
|
export const BUTTON_MAX_LABEL_CHARS = 30;
|
|
6
6
|
|
|
7
7
|
export const buttonProps = z.object({
|
package/src/ui/catalog.ts
CHANGED
|
@@ -15,6 +15,8 @@ import { separatorProps } from "./separator.js";
|
|
|
15
15
|
import { sliderProps } from "./slider.js";
|
|
16
16
|
import { stackProps } from "./stack.js";
|
|
17
17
|
import { textProps } from "./text.js";
|
|
18
|
+
import { barChartProps } from "./bar-chart.js";
|
|
19
|
+
import { cellGridProps } from "./cell-grid.js";
|
|
18
20
|
|
|
19
21
|
const snapClientParams = z.object({
|
|
20
22
|
client_action: z.record(z.string(), z.unknown()),
|
|
@@ -31,7 +33,7 @@ export const snapJsonRenderCatalog = defineCatalog(snapJsonRenderSchema, {
|
|
|
31
33
|
badge: {
|
|
32
34
|
props: badgeProps,
|
|
33
35
|
description:
|
|
34
|
-
"Inline label — variant: default
|
|
36
|
+
"Inline label — variant: default (filled) or outline (bordered). Optional color and icon.",
|
|
35
37
|
},
|
|
36
38
|
button: {
|
|
37
39
|
props: buttonProps,
|
|
@@ -95,7 +97,17 @@ export const snapJsonRenderCatalog = defineCatalog(snapJsonRenderSchema, {
|
|
|
95
97
|
text: {
|
|
96
98
|
props: textProps,
|
|
97
99
|
description:
|
|
98
|
-
"Text block — size:
|
|
100
|
+
"Text block — size: md (body, default), sm (caption). Optional weight and align.",
|
|
101
|
+
},
|
|
102
|
+
bar_chart: {
|
|
103
|
+
props: barChartProps,
|
|
104
|
+
description:
|
|
105
|
+
"Horizontal bar chart — 1–6 bars with label, value, and optional per-bar color. Optional max and default color.",
|
|
106
|
+
},
|
|
107
|
+
cell_grid: {
|
|
108
|
+
props: cellGridProps,
|
|
109
|
+
description:
|
|
110
|
+
"Cell grid — sparse colored cells on a rows×cols grid. Optional gap and selection mode (taps write to inputs[name]).",
|
|
99
111
|
},
|
|
100
112
|
},
|
|
101
113
|
actions: {
|
|
@@ -105,7 +117,11 @@ export const snapJsonRenderCatalog = defineCatalog(snapJsonRenderSchema, {
|
|
|
105
117
|
params: z.object({ target: z.string() }),
|
|
106
118
|
},
|
|
107
119
|
open_url: {
|
|
108
|
-
description: "Open
|
|
120
|
+
description: "Open external URL in browser.",
|
|
121
|
+
params: z.object({ target: z.string() }),
|
|
122
|
+
},
|
|
123
|
+
open_snap: {
|
|
124
|
+
description: "Open a snap URL inline. The client renders the target as a snap rather than opening a browser.",
|
|
109
125
|
params: z.object({ target: z.string() }),
|
|
110
126
|
},
|
|
111
127
|
open_mini_app: {
|