@farcaster/snap 1.15.2 → 1.15.4

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,7 @@
1
- export declare function SnapActionButton({ element: { props }, emit, }: {
1
+ export declare function SnapActionButton({ element, emit, }: {
2
2
  element: {
3
3
  props: Record<string, unknown>;
4
+ on?: Record<string, unknown>;
4
5
  };
5
6
  emit: (name: string) => void;
6
7
  }): import("react/jsx-runtime").JSX.Element;
@@ -1,11 +1,21 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useState } from "react";
4
+ import { ExternalLink } from "lucide-react";
4
5
  import { Button } from "@neynar/ui/button";
5
6
  import { cn } from "@neynar/ui/utils";
6
7
  import { useSnapColors } from "../hooks/use-snap-colors.js";
7
8
  import { ICON_MAP } from "./icon.js";
8
- export function SnapActionButton({ element: { props }, emit, }) {
9
+ function isExternalLinkAction(on) {
10
+ if (!on)
11
+ return false;
12
+ const press = on.press;
13
+ if (!press || press.action !== "open_url")
14
+ return false;
15
+ return press.params?.isSnap !== true;
16
+ }
17
+ export function SnapActionButton({ element, emit, }) {
18
+ const { props } = element;
9
19
  const label = String(props.label ?? "Action");
10
20
  const variant = String(props.variant ?? "secondary");
11
21
  const isPrimary = variant === "primary";
@@ -13,6 +23,7 @@ export function SnapActionButton({ element: { props }, emit, }) {
13
23
  const colors = useSnapColors();
14
24
  const [hovered, setHovered] = useState(false);
15
25
  const Icon = iconName ? ICON_MAP[iconName] : undefined;
26
+ const showExternalIcon = isExternalLinkAction(element.on);
16
27
  const style = isPrimary
17
28
  ? {
18
29
  backgroundColor: hovered ? colors.accentHover : colors.accent,
@@ -20,9 +31,11 @@ export function SnapActionButton({ element: { props }, emit, }) {
20
31
  borderColor: "transparent",
21
32
  }
22
33
  : {
23
- backgroundColor: hovered ? `color-mix(in srgb, ${colors.accent} 15%, transparent)` : colors.muted,
34
+ backgroundColor: hovered
35
+ ? `color-mix(in srgb, ${colors.accent} 15%, transparent)`
36
+ : colors.muted,
24
37
  color: colors.text,
25
38
  borderColor: "transparent",
26
39
  };
27
- return (_jsx("div", { className: "w-full min-w-0 flex-1", children: _jsxs(Button, { type: "button", variant: isPrimary ? "default" : "secondary", className: cn("w-full gap-2"), style: style, onClick: () => emit("press"), onPointerEnter: () => setHovered(true), onPointerLeave: () => setHovered(false), children: [Icon && _jsx(Icon, { size: 16 }), label] }) }));
40
+ return (_jsx("div", { className: "w-full min-w-0 flex-1", children: _jsxs(Button, { type: "button", variant: isPrimary ? "default" : "secondary", className: cn("w-full gap-2"), style: style, onClick: () => emit("press"), onPointerEnter: () => setHovered(true), onPointerLeave: () => setHovered(false), children: [Icon && _jsx(Icon, { size: 16 }), label, showExternalIcon && (_jsx(ExternalLink, { size: 14, style: { opacity: 0.6 } }))] }) }));
28
41
  }
@@ -12,7 +12,9 @@ export type SnapPage = {
12
12
  };
13
13
  export type SnapActionHandlers = {
14
14
  submit: (target: string, inputs: Record<string, JsonValue>) => void;
15
- open_url: (target: string) => void;
15
+ open_url: (target: string, options?: {
16
+ isSnap?: boolean;
17
+ }) => void;
16
18
  open_mini_app: (target: string) => void;
17
19
  view_cast: (params: {
18
20
  hash: string;
@@ -90,9 +90,7 @@ function SnapLoadingOverlay({ appearance, accentHex, active, }) {
90
90
  zIndex: 10,
91
91
  background: tint,
92
92
  backdropFilter: active ? "blur(10px) saturate(1.05)" : "none",
93
- WebkitBackdropFilter: active
94
- ? "blur(10px) saturate(1.05)"
95
- : "none",
93
+ WebkitBackdropFilter: active ? "blur(10px) saturate(1.05)" : "none",
96
94
  opacity: active ? 1 : 0,
97
95
  pointerEvents: active ? "auto" : "none",
98
96
  transition: "opacity 0.28s ease, backdrop-filter 0.28s ease",
@@ -182,7 +180,9 @@ export function SnapView({ snap, handlers, loading = false, appearance = "dark",
182
180
  handlers.submit(String(p.target ?? ""), inputs);
183
181
  break;
184
182
  case "open_url":
185
- handlers.open_url(String(p.target ?? ""));
183
+ handlers.open_url(String(p.target ?? ""), {
184
+ isSnap: p.isSnap === true,
185
+ });
186
186
  break;
187
187
  case "open_mini_app":
188
188
  handlers.open_mini_app(String(p.target ?? ""));
@@ -225,7 +225,7 @@ export function SnapView({ snap, handlers, loading = false, appearance = "dark",
225
225
  break;
226
226
  }
227
227
  }, [handlers]);
228
- return (_jsxs("div", { style: { position: "relative", width: "100%" }, children: [showConfetti && _jsx(ConfettiOverlay, {}, confettiEpochRef.current), _jsx(SnapLoadingOverlay, { appearance: appearance, accentHex: accentHex, active: loading }), _jsx("div", { style: previewSurfaceStyle, children: _jsx(SnapPreviewAccentProvider, { pageAccent: snap.theme?.accent, appearance: appearance, children: _jsx(SnapCatalogView, { spec: spec, state: initialState, loading: false, onStateChange: (changes) => {
228
+ return (_jsxs("div", { style: { position: "relative", width: "100%" }, children: [showConfetti && (_jsx(ConfettiOverlay, {}, `confetti-${confettiEpochRef.current}`)), _jsx(SnapLoadingOverlay, { appearance: appearance, accentHex: accentHex, active: loading }), _jsx("div", { style: previewSurfaceStyle, children: _jsx(SnapPreviewAccentProvider, { pageAccent: snap.theme?.accent, appearance: appearance, children: _jsx(SnapCatalogView, { spec: spec, state: initialState, loading: false, onStateChange: (changes) => {
229
229
  applyStatePaths(stateRef.current, changes);
230
230
  }, onAction: handleAction }, pageKey) }) })] }));
231
231
  }
@@ -1,2 +1,2 @@
1
1
  import type { ComponentRenderProps } from "@json-render/react-native";
2
- export declare function SnapActionButton({ element: { props }, emit, }: ComponentRenderProps<Record<string, unknown>>): import("react").JSX.Element;
2
+ export declare function SnapActionButton({ element, emit, }: ComponentRenderProps<Record<string, unknown>>): import("react").JSX.Element;
@@ -1,17 +1,29 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Pressable, StyleSheet, Text, View } from "react-native";
3
+ import { ExternalLink } from "lucide-react-native";
3
4
  import { useSnapPalette } from "../use-snap-palette.js";
4
5
  import { useSnapTheme } from "../theme.js";
5
6
  import { ICON_MAP } from "./snap-icon.js";
6
- export function SnapActionButton({ element: { props }, emit, }) {
7
+ function isExternalLinkAction(on) {
8
+ if (!on)
9
+ return false;
10
+ const press = on.press;
11
+ if (!press || press.action !== "open_url")
12
+ return false;
13
+ return press.params?.isSnap !== true;
14
+ }
15
+ export function SnapActionButton({ element, emit, }) {
7
16
  const { accentHex } = useSnapPalette();
8
17
  const { colors } = useSnapTheme();
18
+ const { props } = element;
9
19
  const label = String(props.label ?? "Action");
10
20
  const variant = String(props.variant ?? "secondary");
11
21
  const isPrimary = variant === "primary";
12
22
  const iconName = props.icon ? String(props.icon) : undefined;
13
23
  const textColor = isPrimary ? "#fff" : colors.text;
14
24
  const iconColor = isPrimary ? "#fff" : colors.text;
25
+ const on = element.on;
26
+ const showExternalIcon = isExternalLinkAction(on);
15
27
  return (_jsx(View, { style: styles.outer, children: _jsxs(Pressable, { style: ({ pressed }) => [
16
28
  styles.btn,
17
29
  isPrimary ? styles.btnDefault : styles.btnOther,
@@ -30,7 +42,12 @@ export function SnapActionButton({ element: { props }, emit, }) {
30
42
  }
31
43
  }
32
44
  })();
33
- }, children: [iconName && ICON_MAP[iconName] ? ((() => { const I = ICON_MAP[iconName]; return _jsx(I, { size: 16, color: iconColor }); })()) : null, _jsx(Text, { style: { color: textColor, fontSize: 14, fontWeight: "600" }, children: label })] }) }));
45
+ }, children: [iconName && ICON_MAP[iconName]
46
+ ? (() => {
47
+ const I = ICON_MAP[iconName];
48
+ return _jsx(I, { size: 16, color: iconColor });
49
+ })()
50
+ : null, _jsx(Text, { style: { color: textColor, fontSize: 14, fontWeight: "600" }, children: label }), showExternalIcon ? (_jsx(ExternalLink, { size: 14, color: iconColor, style: { opacity: 0.6 } })) : null] }) }));
34
51
  }
35
52
  const styles = StyleSheet.create({
36
53
  outer: { flex: 1, minWidth: 0 },
@@ -14,7 +14,9 @@ export type SnapPage = {
14
14
  };
15
15
  export type SnapActionHandlers = {
16
16
  submit: (target: string, inputs: Record<string, JsonValue>) => void;
17
- open_url: (target: string) => void;
17
+ open_url: (target: string, options?: {
18
+ isSnap?: boolean;
19
+ }) => void;
18
20
  open_mini_app: (target: string) => void;
19
21
  view_cast: (params: {
20
22
  hash: string;
@@ -103,7 +103,9 @@ function SnapViewInner({ snap, handlers, loading = false, }) {
103
103
  h.submit(String(p.target ?? ""), inputs);
104
104
  break;
105
105
  case "open_url":
106
- h.open_url(String(p.target ?? ""));
106
+ h.open_url(String(p.target ?? ""), {
107
+ isSnap: p.isSnap === true,
108
+ });
107
109
  break;
108
110
  case "open_mini_app":
109
111
  h.open_mini_app(String(p.target ?? ""));
@@ -149,7 +151,7 @@ function SnapViewInner({ snap, handlers, loading = false, }) {
149
151
  {
150
152
  backgroundColor: mode === "dark" ? "rgba(0,0,0,0.1)" : "rgba(255,255,255,0.2)",
151
153
  },
152
- ], children: _jsx(ActivityIndicator, { size: "large", color: accentHex }) })) : null, showConfetti ? _jsx(ConfettiOverlay, {}, confettiEpochRef.current) : null, _jsx(SnapCatalogView, { spec: spec, state: initialState, loading: false, onStateChange: (changes) => {
154
+ ], children: _jsx(ActivityIndicator, { size: "large", color: accentHex }) })) : null, showConfetti ? (_jsx(ConfettiOverlay, {}, `confetti-${confettiEpochRef.current}`)) : null, _jsx(SnapCatalogView, { spec: spec, state: initialState, loading: false, onStateChange: (changes) => {
153
155
  applyStatePaths(stateRef.current, changes);
154
156
  }, onAction: handleAction }, pageKey)] }));
155
157
  }
@@ -413,6 +413,7 @@ export declare const snapJsonRenderCatalog: import("@json-render/core").Catalog<
413
413
  description: string;
414
414
  params: z.ZodObject<{
415
415
  target: z.ZodString;
416
+ isSnap: z.ZodOptional<z.ZodBoolean>;
416
417
  }, z.core.$strip>;
417
418
  };
418
419
  open_mini_app: {
@@ -99,8 +99,11 @@ export const snapJsonRenderCatalog = defineCatalog(snapJsonRenderSchema, {
99
99
  params: z.object({ target: z.string() }),
100
100
  },
101
101
  open_url: {
102
- description: "Open target URL in the system browser.",
103
- params: z.object({ target: z.string() }),
102
+ description: "Open target snap or external URL.",
103
+ params: z.object({
104
+ target: z.string(),
105
+ isSnap: z.boolean().optional(),
106
+ }),
104
107
  },
105
108
  open_mini_app: {
106
109
  description: "Open target URL as a Farcaster mini app.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@farcaster/snap",
3
- "version": "1.15.2",
3
+ "version": "1.15.4",
4
4
  "description": "Farcaster Snaps 🫰",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,18 +1,34 @@
1
1
  "use client";
2
2
 
3
3
  import { useState } from "react";
4
+ import { ExternalLink } from "lucide-react";
4
5
  import { Button } from "@neynar/ui/button";
5
6
  import { cn } from "@neynar/ui/utils";
6
7
  import { useSnapColors } from "../hooks/use-snap-colors";
7
8
  import { ICON_MAP } from "./icon";
8
9
 
10
+ function isExternalLinkAction(
11
+ on: Record<string, unknown> | undefined,
12
+ ): boolean {
13
+ if (!on) return false;
14
+ const press = on.press as
15
+ | { action?: string; params?: Record<string, unknown> }
16
+ | undefined;
17
+ if (!press || press.action !== "open_url") return false;
18
+ return press.params?.isSnap !== true;
19
+ }
20
+
9
21
  export function SnapActionButton({
10
- element: { props },
22
+ element,
11
23
  emit,
12
24
  }: {
13
- element: { props: Record<string, unknown> };
25
+ element: {
26
+ props: Record<string, unknown>;
27
+ on?: Record<string, unknown>;
28
+ };
14
29
  emit: (name: string) => void;
15
30
  }) {
31
+ const { props } = element;
16
32
  const label = String(props.label ?? "Action");
17
33
  const variant = String(props.variant ?? "secondary");
18
34
  const isPrimary = variant === "primary";
@@ -21,6 +37,7 @@ export function SnapActionButton({
21
37
  const [hovered, setHovered] = useState(false);
22
38
 
23
39
  const Icon = iconName ? ICON_MAP[iconName] : undefined;
40
+ const showExternalIcon = isExternalLinkAction(element.on);
24
41
 
25
42
  const style = isPrimary
26
43
  ? {
@@ -29,7 +46,9 @@ export function SnapActionButton({
29
46
  borderColor: "transparent",
30
47
  }
31
48
  : {
32
- backgroundColor: hovered ? `color-mix(in srgb, ${colors.accent} 15%, transparent)` : colors.muted,
49
+ backgroundColor: hovered
50
+ ? `color-mix(in srgb, ${colors.accent} 15%, transparent)`
51
+ : colors.muted,
33
52
  color: colors.text,
34
53
  borderColor: "transparent",
35
54
  };
@@ -47,6 +66,9 @@ export function SnapActionButton({
47
66
  >
48
67
  {Icon && <Icon size={16} />}
49
68
  {label}
69
+ {showExternalIcon && (
70
+ <ExternalLink size={14} style={{ opacity: 0.6 }} />
71
+ )}
50
72
  </Button>
51
73
  </div>
52
74
  );
@@ -34,7 +34,7 @@ export type SnapPage = {
34
34
 
35
35
  export type SnapActionHandlers = {
36
36
  submit: (target: string, inputs: Record<string, JsonValue>) => void;
37
- open_url: (target: string) => void;
37
+ open_url: (target: string, options?: { isSnap?: boolean }) => void;
38
38
  open_mini_app: (target: string) => void;
39
39
  view_cast: (params: { hash: string }) => void;
40
40
  view_profile: (params: { fid: number }) => void;
@@ -174,9 +174,7 @@ function SnapLoadingOverlay({
174
174
  zIndex: 10,
175
175
  background: tint,
176
176
  backdropFilter: active ? "blur(10px) saturate(1.05)" : "none",
177
- WebkitBackdropFilter: active
178
- ? "blur(10px) saturate(1.05)"
179
- : "none",
177
+ WebkitBackdropFilter: active ? "blur(10px) saturate(1.05)" : "none",
180
178
  opacity: active ? 1 : 0,
181
179
  pointerEvents: active ? "auto" : "none",
182
180
  transition: "opacity 0.28s ease, backdrop-filter 0.28s ease",
@@ -308,7 +306,9 @@ export function SnapView({
308
306
  handlers.submit(String(p.target ?? ""), inputs);
309
307
  break;
310
308
  case "open_url":
311
- handlers.open_url(String(p.target ?? ""));
309
+ handlers.open_url(String(p.target ?? ""), {
310
+ isSnap: p.isSnap === true,
311
+ });
312
312
  break;
313
313
  case "open_mini_app":
314
314
  handlers.open_mini_app(String(p.target ?? ""));
@@ -356,7 +356,9 @@ export function SnapView({
356
356
 
357
357
  return (
358
358
  <div style={{ position: "relative", width: "100%" }}>
359
- {showConfetti && <ConfettiOverlay key={confettiEpochRef.current} />}
359
+ {showConfetti && (
360
+ <ConfettiOverlay key={`confetti-${confettiEpochRef.current}`} />
361
+ )}
360
362
  <SnapLoadingOverlay
361
363
  appearance={appearance}
362
364
  accentHex={accentHex}
@@ -2,16 +2,29 @@ declare const __DEV__: boolean;
2
2
 
3
3
  import type { ComponentRenderProps } from "@json-render/react-native";
4
4
  import { Pressable, StyleSheet, Text, View } from "react-native";
5
+ import { ExternalLink } from "lucide-react-native";
5
6
  import { useSnapPalette } from "../use-snap-palette";
6
7
  import { useSnapTheme } from "../theme";
7
8
  import { ICON_MAP } from "./snap-icon";
8
9
 
10
+ function isExternalLinkAction(
11
+ on: Record<string, unknown> | undefined,
12
+ ): boolean {
13
+ if (!on) return false;
14
+ const press = on.press as
15
+ | { action?: string; params?: Record<string, unknown> }
16
+ | undefined;
17
+ if (!press || press.action !== "open_url") return false;
18
+ return press.params?.isSnap !== true;
19
+ }
20
+
9
21
  export function SnapActionButton({
10
- element: { props },
22
+ element,
11
23
  emit,
12
24
  }: ComponentRenderProps<Record<string, unknown>>) {
13
25
  const { accentHex } = useSnapPalette();
14
26
  const { colors } = useSnapTheme();
27
+ const { props } = element;
15
28
  const label = String(props.label ?? "Action");
16
29
  const variant = String(props.variant ?? "secondary");
17
30
  const isPrimary = variant === "primary";
@@ -20,6 +33,9 @@ export function SnapActionButton({
20
33
  const textColor = isPrimary ? "#fff" : colors.text;
21
34
  const iconColor = isPrimary ? "#fff" : colors.text;
22
35
 
36
+ const on = (element as unknown as { on?: Record<string, unknown> }).on;
37
+ const showExternalIcon = isExternalLinkAction(on);
38
+
23
39
  return (
24
40
  <View style={styles.outer}>
25
41
  <Pressable
@@ -43,12 +59,18 @@ export function SnapActionButton({
43
59
  })();
44
60
  }}
45
61
  >
46
- {iconName && ICON_MAP[iconName] ? (
47
- (() => { const I = ICON_MAP[iconName]!; return <I size={16} color={iconColor} />; })()
48
- ) : null}
62
+ {iconName && ICON_MAP[iconName]
63
+ ? (() => {
64
+ const I = ICON_MAP[iconName]!;
65
+ return <I size={16} color={iconColor} />;
66
+ })()
67
+ : null}
49
68
  <Text style={{ color: textColor, fontSize: 14, fontWeight: "600" }}>
50
69
  {label}
51
70
  </Text>
71
+ {showExternalIcon ? (
72
+ <ExternalLink size={14} color={iconColor} style={{ opacity: 0.6 }} />
73
+ ) : null}
52
74
  </Pressable>
53
75
  </View>
54
76
  );
@@ -36,7 +36,7 @@ export type SnapPage = {
36
36
 
37
37
  export type SnapActionHandlers = {
38
38
  submit: (target: string, inputs: Record<string, JsonValue>) => void;
39
- open_url: (target: string) => void;
39
+ open_url: (target: string, options?: { isSnap?: boolean }) => void;
40
40
  open_mini_app: (target: string) => void;
41
41
  view_cast: (params: { hash: string }) => void;
42
42
  view_profile: (params: { fid: number }) => void;
@@ -184,7 +184,9 @@ function SnapViewInner({
184
184
  h.submit(String(p.target ?? ""), inputs);
185
185
  break;
186
186
  case "open_url":
187
- h.open_url(String(p.target ?? ""));
187
+ h.open_url(String(p.target ?? ""), {
188
+ isSnap: p.isSnap === true,
189
+ });
188
190
  break;
189
191
  case "open_mini_app":
190
192
  h.open_mini_app(String(p.target ?? ""));
@@ -241,7 +243,9 @@ function SnapViewInner({
241
243
  <ActivityIndicator size="large" color={accentHex} />
242
244
  </View>
243
245
  ) : null}
244
- {showConfetti ? <ConfettiOverlay key={confettiEpochRef.current} /> : null}
246
+ {showConfetti ? (
247
+ <ConfettiOverlay key={`confetti-${confettiEpochRef.current}`} />
248
+ ) : null}
245
249
  <SnapCatalogView
246
250
  key={pageKey}
247
251
  spec={spec}
package/src/ui/catalog.ts CHANGED
@@ -117,8 +117,11 @@ export const snapJsonRenderCatalog = defineCatalog(snapJsonRenderSchema, {
117
117
  params: z.object({ target: z.string() }),
118
118
  },
119
119
  open_url: {
120
- description: "Open target URL in the system browser.",
121
- params: z.object({ target: z.string() }),
120
+ description: "Open target snap or external URL.",
121
+ params: z.object({
122
+ target: z.string(),
123
+ isSnap: z.boolean().optional(),
124
+ }),
122
125
  },
123
126
  open_mini_app: {
124
127
  description: "Open target URL as a Farcaster mini app.",