@farcaster/snap 1.20.0 → 1.22.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.
@@ -1,6 +1,6 @@
1
1
  export declare const SPEC_VERSION_1: "1.0";
2
2
  export declare const SPEC_VERSION_2: "2.0";
3
- export declare const SPEC_VERSION: "1.0";
3
+ export declare const SPEC_VERSION: "2.0";
4
4
  export declare const SUPPORTED_SPEC_VERSIONS: readonly ["1.0", "2.0"];
5
5
  export type SpecVersion = (typeof SUPPORTED_SPEC_VERSIONS)[number];
6
6
  export declare const MEDIA_TYPE: "application/vnd.farcaster.snap+json";
package/dist/constants.js CHANGED
@@ -1,6 +1,6 @@
1
1
  export const SPEC_VERSION_1 = "1.0";
2
2
  export const SPEC_VERSION_2 = "2.0";
3
- export const SPEC_VERSION = SPEC_VERSION_1;
3
+ export const SPEC_VERSION = SPEC_VERSION_2;
4
4
  export const SUPPORTED_SPEC_VERSIONS = [SPEC_VERSION_1, SPEC_VERSION_2];
5
5
  export const MEDIA_TYPE = "application/vnd.farcaster.snap+json";
6
6
  export const EFFECT_VALUES = ["confetti"];
@@ -10,9 +10,9 @@ function isExternalLinkAction(on) {
10
10
  if (!on)
11
11
  return false;
12
12
  const press = on.press;
13
- if (!press || press.action !== "open_url")
13
+ if (!press)
14
14
  return false;
15
- return press.params?.isSnap !== true;
15
+ return press.action === "open_url";
16
16
  }
17
17
  export function SnapActionButton({ element, emit, }) {
18
18
  const { props } = element;
@@ -15,6 +15,7 @@ export type SnapPage = {
15
15
  export type SnapActionHandlers = {
16
16
  submit: (target: string, inputs: Record<string, JsonValue>) => void;
17
17
  open_url: (target: string) => void;
18
+ open_snap: (target: string) => void;
18
19
  open_mini_app: (target: string) => void;
19
20
  view_cast: (params: {
20
21
  hash: string;
@@ -8,9 +8,9 @@ function isExternalLinkAction(on) {
8
8
  if (!on)
9
9
  return false;
10
10
  const press = on.press;
11
- if (!press || press.action !== "open_url")
11
+ if (!press)
12
12
  return false;
13
- return press.params?.isSnap !== true;
13
+ return press.action === "open_url";
14
14
  }
15
15
  export function SnapActionButton({ element, emit, }) {
16
16
  const { accentHex } = useSnapPalette();
@@ -11,6 +11,7 @@ export function SnapCellGrid({ element: { props }, }) {
11
11
  const cols = Number(props.cols ?? 2);
12
12
  const rows = Number(props.rows ?? 2);
13
13
  const cells = Array.isArray(props.cells) ? props.cells : [];
14
+ const rowHeight = typeof props.rowHeight === "number" ? props.rowHeight : 28;
14
15
  const gap = String(props.gap ?? "sm");
15
16
  const gapMap = { none: 0, sm: 1, md: 2, lg: 4 };
16
17
  const gapPx = gapMap[gap] ?? 1;
@@ -60,10 +61,10 @@ export function SnapCellGrid({ element: { props }, }) {
60
61
  const bg = cell?.color ? hex(cell.color) : "transparent";
61
62
  const cellContent = cell?.content ? (_jsx(Text, { style: [styles.cellText, { color: colors.textPrimary }], children: cell.content })) : null;
62
63
  // Two-tone ring: outer View with contrasting border, inner View with inverse border
63
- const cellView = selected ? (_jsx(View, { style: [styles.cell, { borderWidth: 1, borderColor: ringOuter, borderRadius: 4 }], children: _jsx(View, { style: [
64
+ const cellView = selected ? (_jsx(View, { style: [styles.cell, { height: rowHeight, borderWidth: 1, borderColor: ringOuter, borderRadius: 4 }], children: _jsx(View, { style: [
64
65
  styles.innerCell,
65
66
  { backgroundColor: bg, borderWidth: 1, borderColor: ringInner, borderRadius: 3 },
66
- ], children: cellContent }) })) : (_jsx(View, { style: [styles.cell, { backgroundColor: bg }], children: cellContent }));
67
+ ], children: cellContent }) })) : (_jsx(View, { style: [styles.cell, { height: rowHeight, backgroundColor: bg }], children: cellContent }));
67
68
  rowCells.push(interactive ? (_jsx(Pressable, { onPress: () => handleTap(r, c), style: styles.cellWrap, children: cellView }, `${r}-${c}`)) : (_jsx(View, { style: styles.cellWrap, children: cellView }, `${r}-${c}`)));
68
69
  }
69
70
  rowEls.push(_jsx(View, { style: [styles.gridRow, { gap: gapPx }], children: rowCells }, r));
@@ -78,16 +79,13 @@ const styles = StyleSheet.create({
78
79
  gridRow: { flexDirection: "row" },
79
80
  cellWrap: { flex: 1 },
80
81
  cell: {
81
- flex: 1,
82
- minHeight: 28,
83
82
  borderRadius: 4,
84
83
  alignItems: "center",
85
84
  justifyContent: "center",
86
85
  },
87
86
  innerCell: {
88
- flex: 1,
89
87
  width: "100%",
90
- minHeight: 26,
88
+ height: "100%",
91
89
  alignItems: "center",
92
90
  justifyContent: "center",
93
91
  },
@@ -13,6 +13,7 @@ export type SnapPage = {
13
13
  export type SnapActionHandlers = {
14
14
  submit: (target: string, inputs: Record<string, JsonValue>) => void;
15
15
  open_url: (target: string) => void;
16
+ open_snap: (target: string) => void;
16
17
  open_mini_app: (target: string) => void;
17
18
  view_cast: (params: {
18
19
  hash: string;
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useEffect, useMemo } from "react";
2
+ import { useEffect, useMemo, useState } from "react";
3
3
  import { Platform, StyleSheet, Text, View } from "react-native";
4
4
  import { SnapThemeProvider, useSnapTheme } from "../theme.js";
5
5
  import { SnapViewCoreInner } from "../snap-view-core.js";
@@ -52,20 +52,21 @@ export function SnapViewV2({ snap, handlers, loading = false, appearance = "dark
52
52
  // ─── SnapCardV2 (card frame + height limits) ─────────
53
53
  function SnapCardV2Inner({ snap, handlers, loading, borderRadius, showOverflowWarning, onValidationError, validationErrorFallback, actionError, appearance, plain, }) {
54
54
  const { colors } = useSnapTheme();
55
- const clipHeight = showOverflowWarning ? undefined : SNAP_MAX_HEIGHT;
55
+ const [contentHeight, setContentHeight] = useState(0);
56
56
  const content = (_jsx(SnapViewV2Inner, { snap: snap, handlers: handlers, loading: loading, onValidationError: onValidationError, validationErrorFallback: validationErrorFallback }));
57
57
  if (plain) {
58
58
  return content;
59
59
  }
60
+ const overflowAmount = showOverflowWarning ? contentHeight - SNAP_MAX_HEIGHT : 0;
60
61
  return (_jsxs(_Fragment, { children: [_jsxs(View, { style: {
61
62
  borderRadius,
62
63
  borderWidth: 1,
63
64
  borderColor: colors.border,
64
65
  backgroundColor: colors.surface,
65
- maxHeight: clipHeight,
66
+ maxHeight: showOverflowWarning ? undefined : SNAP_MAX_HEIGHT,
66
67
  overflow: "hidden",
67
68
  minHeight: 120,
68
- }, children: [_jsx(View, { style: { paddingHorizontal: 16, paddingVertical: 16 }, children: content }), showOverflowWarning && (_jsxs(View, { style: { position: "absolute", top: SNAP_MAX_HEIGHT, left: 0, right: 0, bottom: 0, zIndex: 10, pointerEvents: "none" }, children: [_jsx(View, { style: { height: 1, borderTopWidth: 1, borderStyle: "dashed", borderColor: "rgba(255,100,100,0.6)" } }), _jsx(View, { style: { position: "absolute", top: -10, right: 4, backgroundColor: "rgba(0,0,0,0.7)", paddingHorizontal: 4, paddingVertical: 1, borderRadius: 3 }, children: _jsxs(Text, { style: { fontSize: 10, color: "rgba(255,100,100,0.7)", fontFamily: Platform.select({ ios: "Menlo", default: "monospace" }) }, children: [SNAP_MAX_HEIGHT, "px"] }) }), _jsx(View, { style: { flex: 1, backgroundColor: "rgba(255,50,50,0.15)" } })] }))] }), actionError && (_jsx(Text, { style: {
69
+ }, children: [_jsx(View, { collapsable: false, onLayout: (e) => setContentHeight(Math.round(e.nativeEvent.layout.height)), style: { paddingHorizontal: 16, paddingVertical: 16 }, children: content }), showOverflowWarning && contentHeight > SNAP_MAX_HEIGHT && (_jsxs(View, { style: { position: "absolute", top: SNAP_MAX_HEIGHT, left: 0, right: 0, height: overflowAmount, zIndex: 10, pointerEvents: "none" }, children: [_jsx(View, { style: { height: 1, borderTopWidth: 1, borderStyle: "dashed", borderColor: "rgba(255,100,100,0.6)" } }), _jsx(View, { style: { position: "absolute", top: -10, right: 4, backgroundColor: "rgba(0,0,0,0.7)", paddingHorizontal: 4, paddingVertical: 1, borderRadius: 3 }, children: _jsxs(Text, { style: { fontSize: 10, color: "rgba(255,100,100,0.7)", fontFamily: Platform.select({ ios: "Menlo", default: "monospace" }) }, children: [SNAP_MAX_HEIGHT, "px"] }) }), _jsx(View, { style: { flex: 1, backgroundColor: "rgba(255,50,50,0.15)" } })] }))] }), actionError && (_jsx(Text, { style: {
69
70
  paddingHorizontal: 12,
70
71
  paddingVertical: 8,
71
72
  fontSize: 13,
@@ -420,6 +420,12 @@ export declare const snapJsonRenderCatalog: import("@json-render/core").Catalog<
420
420
  target: z.ZodString;
421
421
  }, z.core.$strip>;
422
422
  };
423
+ open_snap: {
424
+ description: string;
425
+ params: z.ZodObject<{
426
+ target: z.ZodString;
427
+ }, z.core.$strip>;
428
+ };
423
429
  open_mini_app: {
424
430
  description: string;
425
431
  params: z.ZodObject<{
@@ -99,10 +99,12 @@ export const snapJsonRenderCatalog = defineCatalog(snapJsonRenderSchema, {
99
99
  params: z.object({ target: z.string() }),
100
100
  },
101
101
  open_url: {
102
- description: "Open URL in browser.",
103
- params: z.object({
104
- target: z.string(),
105
- }),
102
+ description: "Open external URL in browser.",
103
+ params: z.object({ target: z.string() }),
104
+ },
105
+ open_snap: {
106
+ description: "Open a snap URL inline. The client renders the target as a snap rather than opening a browser.",
107
+ params: z.object({ target: z.string() }),
106
108
  },
107
109
  open_mini_app: {
108
110
  description: "Open target URL as a Farcaster mini app.",
package/llms.txt CHANGED
@@ -8,7 +8,7 @@ Every snap handler returns a `SnapResponse`:
8
8
 
9
9
  ```json
10
10
  {
11
- "version": "1.0",
11
+ "version": "2.0",
12
12
  "theme": { "accent": "purple" },
13
13
  "effects": ["confetti"],
14
14
  "ui": {
@@ -26,7 +26,7 @@ Every snap handler returns a `SnapResponse`:
26
26
  }
27
27
  ```
28
28
 
29
- Top-level fields: `version` (required, `"1.0"`), `theme` (optional, `{ accent: PaletteColor }`), `effects` (optional, `["confetti"]`), `ui` (required).
29
+ Top-level fields: `version` (required, `"1.0"` or `"2.0"`), `theme` (optional, `{ accent: PaletteColor }`), `effects` (optional, `["confetti"]`), `ui` (required).
30
30
 
31
31
  `ui.root` is the ID of the root element. `ui.elements` is a flat map of element ID to element definition.
32
32
 
@@ -153,14 +153,15 @@ Field values are sent in POST `inputs[name]` when a `submit` action fires.
153
153
  - `label` (string, optional, max 60)
154
154
  - POST value: string (single) or string[] (multiple)
155
155
 
156
- ## Actions (9 types)
156
+ ## Actions (10 types)
157
157
 
158
158
  Bound to buttons via `on.press`:
159
159
 
160
160
  | Action | Params | Description |
161
161
  |--------|--------|-------------|
162
162
  | `submit` | `target` (URL) | POST to server, get next page |
163
- | `open_url` | `target` (URL) | Open in system browser |
163
+ | `open_url` | `target` (URL) | Open external URL in browser |
164
+ | `open_snap` | `target` (URL) | Open a snap URL inline |
164
165
  | `open_mini_app` | `target` (URL) | Open as Farcaster mini app |
165
166
  | `view_cast` | `hash` (string) | Navigate to a cast |
166
167
  | `view_profile` | `fid` (number) | Navigate to a profile |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@farcaster/snap",
3
- "version": "1.20.0",
3
+ "version": "1.22.0",
4
4
  "description": "Farcaster Snaps 🫰",
5
5
  "repository": {
6
6
  "type": "git",
package/src/constants.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export const SPEC_VERSION_1 = "1.0" as const;
2
2
  export const SPEC_VERSION_2 = "2.0" as const;
3
- export const SPEC_VERSION = SPEC_VERSION_1;
3
+ export const SPEC_VERSION = SPEC_VERSION_2;
4
4
  export const SUPPORTED_SPEC_VERSIONS = [SPEC_VERSION_1, SPEC_VERSION_2] as const;
5
5
  export type SpecVersion = (typeof SUPPORTED_SPEC_VERSIONS)[number];
6
6
 
@@ -14,8 +14,8 @@ function isExternalLinkAction(
14
14
  const press = on.press as
15
15
  | { action?: string; params?: Record<string, unknown> }
16
16
  | undefined;
17
- if (!press || press.action !== "open_url") return false;
18
- return press.params?.isSnap !== true;
17
+ if (!press) return false;
18
+ return press.action === "open_url";
19
19
  }
20
20
 
21
21
  export function SnapActionButton({
@@ -27,6 +27,7 @@ export type SnapPage = {
27
27
  export type SnapActionHandlers = {
28
28
  submit: (target: string, inputs: Record<string, JsonValue>) => void;
29
29
  open_url: (target: string) => void;
30
+ open_snap: (target: string) => void;
30
31
  open_mini_app: (target: string) => void;
31
32
  view_cast: (params: { hash: string }) => void;
32
33
  view_profile: (params: { fid: number }) => void;
@@ -14,8 +14,8 @@ function isExternalLinkAction(
14
14
  const press = on.press as
15
15
  | { action?: string; params?: Record<string, unknown> }
16
16
  | undefined;
17
- if (!press || press.action !== "open_url") return false;
18
- return press.params?.isSnap !== true;
17
+ if (!press) return false;
18
+ return press.action === "open_url";
19
19
  }
20
20
 
21
21
  export function SnapActionButton({
@@ -14,6 +14,7 @@ export function SnapCellGrid({
14
14
  const cols = Number(props.cols ?? 2);
15
15
  const rows = Number(props.rows ?? 2);
16
16
  const cells = Array.isArray(props.cells) ? props.cells : [];
17
+ const rowHeight = typeof props.rowHeight === "number" ? props.rowHeight : 28;
17
18
  const gap = String(props.gap ?? "sm");
18
19
  const gapMap: Record<string, number> = { none: 0, sm: 1, md: 2, lg: 4 };
19
20
  const gapPx = gapMap[gap] ?? 1;
@@ -74,7 +75,7 @@ export function SnapCellGrid({
74
75
 
75
76
  // Two-tone ring: outer View with contrasting border, inner View with inverse border
76
77
  const cellView = selected ? (
77
- <View style={[styles.cell, { borderWidth: 1, borderColor: ringOuter, borderRadius: 4 }]}>
78
+ <View style={[styles.cell, { height: rowHeight, borderWidth: 1, borderColor: ringOuter, borderRadius: 4 }]}>
78
79
  <View
79
80
  style={[
80
81
  styles.innerCell,
@@ -85,7 +86,7 @@ export function SnapCellGrid({
85
86
  </View>
86
87
  </View>
87
88
  ) : (
88
- <View style={[styles.cell, { backgroundColor: bg }]}>
89
+ <View style={[styles.cell, { height: rowHeight, backgroundColor: bg }]}>
89
90
  {cellContent}
90
91
  </View>
91
92
  );
@@ -134,16 +135,13 @@ const styles = StyleSheet.create({
134
135
  gridRow: { flexDirection: "row" },
135
136
  cellWrap: { flex: 1 },
136
137
  cell: {
137
- flex: 1,
138
- minHeight: 28,
139
138
  borderRadius: 4,
140
139
  alignItems: "center",
141
140
  justifyContent: "center",
142
141
  },
143
142
  innerCell: {
144
- flex: 1,
145
143
  width: "100%",
146
- minHeight: 26,
144
+ height: "100%",
147
145
  alignItems: "center",
148
146
  justifyContent: "center",
149
147
  },
@@ -18,6 +18,7 @@ export type SnapPage = {
18
18
  export type SnapActionHandlers = {
19
19
  submit: (target: string, inputs: Record<string, JsonValue>) => void;
20
20
  open_url: (target: string) => void;
21
+ open_snap: (target: string) => void;
21
22
  open_mini_app: (target: string) => void;
22
23
  view_cast: (params: { hash: string }) => void;
23
24
  view_profile: (params: { fid: number }) => void;
@@ -1,5 +1,5 @@
1
1
  import type { ReactNode } from "react";
2
- import { useEffect, useMemo } from "react";
2
+ import { useEffect, useMemo, useState } from "react";
3
3
  import { Platform, StyleSheet, Text, View } from "react-native";
4
4
  import { SnapThemeProvider, useSnapTheme, type SnapNativeColors } from "../theme";
5
5
  import { SnapViewCoreInner } from "../snap-view-core";
@@ -137,7 +137,7 @@ function SnapCardV2Inner({
137
137
  plain: boolean;
138
138
  }) {
139
139
  const { colors } = useSnapTheme();
140
- const clipHeight = showOverflowWarning ? undefined : SNAP_MAX_HEIGHT;
140
+ const [contentHeight, setContentHeight] = useState(0);
141
141
 
142
142
  const content = (
143
143
  <SnapViewV2Inner
@@ -153,6 +153,8 @@ function SnapCardV2Inner({
153
153
  return content;
154
154
  }
155
155
 
156
+ const overflowAmount = showOverflowWarning ? contentHeight - SNAP_MAX_HEIGHT : 0;
157
+
156
158
  return (
157
159
  <>
158
160
  <View
@@ -161,16 +163,20 @@ function SnapCardV2Inner({
161
163
  borderWidth: 1,
162
164
  borderColor: colors.border,
163
165
  backgroundColor: colors.surface,
164
- maxHeight: clipHeight,
166
+ maxHeight: showOverflowWarning ? undefined : SNAP_MAX_HEIGHT,
165
167
  overflow: "hidden",
166
168
  minHeight: 120,
167
169
  }}
168
170
  >
169
- <View style={{ paddingHorizontal: 16, paddingVertical: 16 }}>
171
+ <View
172
+ collapsable={false}
173
+ onLayout={(e) => setContentHeight(Math.round(e.nativeEvent.layout.height))}
174
+ style={{ paddingHorizontal: 16, paddingVertical: 16 }}
175
+ >
170
176
  {content}
171
177
  </View>
172
- {showOverflowWarning && (
173
- <View style={{ position: "absolute", top: SNAP_MAX_HEIGHT, left: 0, right: 0, bottom: 0, zIndex: 10, pointerEvents: "none" }}>
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" }}>
174
180
  <View style={{ height: 1, borderTopWidth: 1, borderStyle: "dashed", borderColor: "rgba(255,100,100,0.6)" }} />
175
181
  <View style={{ position: "absolute", top: -10, right: 4, backgroundColor: "rgba(0,0,0,0.7)", paddingHorizontal: 4, paddingVertical: 1, borderRadius: 3 }}>
176
182
  <Text style={{ fontSize: 10, color: "rgba(255,100,100,0.7)", fontFamily: Platform.select({ ios: "Menlo", default: "monospace" }) }}>{SNAP_MAX_HEIGHT}px</Text>
package/src/ui/catalog.ts CHANGED
@@ -117,10 +117,12 @@ export const snapJsonRenderCatalog = defineCatalog(snapJsonRenderSchema, {
117
117
  params: z.object({ target: z.string() }),
118
118
  },
119
119
  open_url: {
120
- description: "Open URL in browser.",
121
- params: z.object({
122
- target: z.string(),
123
- }),
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.",
125
+ params: z.object({ target: z.string() }),
124
126
  },
125
127
  open_mini_app: {
126
128
  description: "Open target URL as a Farcaster mini app.",